Initial commit

This commit is contained in:
Jeff Epler 2020-08-08 16:16:19 -05:00
commit e9f8cadc98
30 changed files with 6236 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_site/

43
LICENSE Normal file
View file

@ -0,0 +1,43 @@
## creative commons
# CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER.
### Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
1. __Copyright and Related Rights.__ A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
2. __Waiver.__ To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.
3. __Public License Fallback.__ Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.
4. __Limitations and Disclaimers.__
a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.

27
README.md Normal file
View file

@ -0,0 +1,27 @@
## cookiecutter-mystl
A Jekyll (github pages) boilerplate for hosting your own STL files.
For use with [cookiecutter](https://github.com/audreyr/cookiecutter).
## License
The license for the "original parts" of this cookiecutter is CC0, the Creative
Commons Public Dedication. However, included parts are under other licenses
such as MIT (the html, js, css) and CC-BY-SA
([the monolith photograph](https://commons.wikimedia.org/wiki/File:HAL_2001_monolith_(color_correction).jpg)).
The intent is not that the works you exhibit with your site be covered under
any specific license. You get a choice of several popular licenses when you
run this cookie-cutter.
The intent is to give you as much freedom as possible with the site that
cookiecutter-mystl produces.
## Requests for Contributions
Contributions welcome! Here are some of the directions I'd like to see
cookiecutter-mystl project grow:
- integrations with other hosting and CI services (gitlab, travis, etc)
- improve lightbox & 3d viewer
- work with other file types (.stp? FreeCAD?)

10
cookiecutter.json Normal file
View file

@ -0,0 +1,10 @@
{
"project_name": "MySTL",
"repo_name": "mystl",
"main_branch_name": ["master", "main"],
"author": "Jeff Epler",
"copyrightyears" : "{% now 'local', '%Y' %}",
"project_short_description": "Demonstration site for MySTL",
"license": ["CC-BY", "CC-BY-SA", "CC-BY-NC", "CC-BY-NC-SA", "CC-BY-ND", "CC-BY-NC-ND", "CC0", "GPL-3.0-or-later", "GPL-3.0-only"],
"_extensions": ["jinja2_time.TimeExtension"]
}

View file

@ -0,0 +1,41 @@
name: Build and Publish Website
on:
push:
pull_request:
release:
types: [published]
check_suite:
types: [rerequested]
jobs:
build:
runs-on: ubuntu-18.04
container: debian:buster
steps:
- name: install dependencies
run: |
apt-get update
apt-get --no-install-recommends -y install admesh git imagemagick jekyll make openscad python3 python3-yaml xauth xvfb
- name: checkout
uses: actions/checkout@v2
- name: build
run: |
xvfb-run make V=2 -O -j$(nproc) jekyll
- if: github.event_name == 'push' && github.ref == 'refs/heads/{{ cookiecutter.main_branch_name }}'
name: publish
run: |
{% raw %}
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
git remote set-url --push origin https://${GITHUB_ACTOR}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
make -O V=1 publish
{% endraw %}
- if: github.event_name != 'push' || github.ref != 'refs/heads/{{ cookiecutter.main_branch_name }}'
name: artifact
uses: actions/upload-artifact@v2
with:
name: website
path: _site

1
{{cookiecutter.repo_name}}/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_site/

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
ifeq ("$(origin V)", "command line")
BUILD_VERBOSE = $(V)
endif
ifndef BUILD_VERBOSE
BUILD_VERBOSE = 0
endif
ifeq "$(findstring s,$(MAKEFLAGS))" ""
ECHO=@echo
VECHO=echo
else
ECHO=@true
VECHO=true
endif
default:
clean:
$(ECHO) CLEAN
$(Q)rm -rf resources/gen
$(Q)jekyll clean
resources/gen/rules.mk: _data/assets.yml _lib/rules.py
$(ECHO) RULES
$(Q)mkdir -p resources/gen
$(Q)_lib/rules.py $< > $@
-include resources/gen/rules.mk
.PHONY: publish jekyll
jekyll: default
$(ECHO) "JEKYLL"
$(Q)jekyll build
$(Q)touch _site/.nojekyll
publish: jekyll default
$(ECHO) "PUBLISH"
$(Q)git branch -D gh-pages || true
$(Q)./_lib/docimport.py | git fast-import --date-format=now
$(Q)git push -f origin gh-pages

View file

@ -0,0 +1,49 @@
# cookiecutter.repo_name
{{ cookiecutter.project_short_description }}
This is a project created with [cookiecutter-mystl](https://github.com/jepler/cookiecutter-mystl).
Use cookiecutter-mystl to host your own OpenSCAD and STL models!
Have improvements to your site? Contribute them back to cookiecutter-mystl!
My STL
## Requirements
See .github/workflows/publish.yml for the canonical requirements.
Intended to work on debian systems with a minimum of fuss.
* admesh
* git
* Imagemagick
* jekyll
* make
* OpenSCAD
* python3
* python3-yaml
For headless building,
* xauth
* xvfb
## Models and STL generation from .scad files
`_data/assets.yml` describes the files that make up your project.
It should be a series of blocks.
At a minimum the block must have a name and an stl file.
It can optionally have notes, an scad file, scad generation flags, images, and other downloads.
You can refer to a source scad file multiple times with different flags,
in order to generate related or parametric models.
## Build assets and render Locally
$ make
$ jekyll serve --watch
## Build locally and push site to github pages
$ make publish
## Push to {{ cookiecutter.main_branch_name }} branch and get a gh-pages build:
$ git push origin {{ cookiecutter.main_branch_name }}

View file

@ -0,0 +1,2 @@
exclude: [README.md,Makefile,resources/gen/rules.mk,.*,resources/gen/*.d]
highligter: rouge

View file

@ -0,0 +1,12 @@
- name: Featureless Cube
scad: main.scad
stl: gen/main.stl
images: ['gen/main.png']
- name: Monolith from 2001
notes: ""
scad: main.scad
flags: -Dsize=[1,4,9]
stl: gen/monolith.stl
images: ['gen/monolith.png', 'HAL_2001_monolith.jpg']
downloads: ['monolith.txt']

View file

@ -0,0 +1,55 @@
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{{ cookiecutter.project_short_description }}">
<meta name="author" content="{{ cookiecutter.author }}">
{% raw %}
{% capture lvl %}{{ page.url | append:'index.html' | split:'/' | size }}{% endcapture %}
{% capture relative %}{% for i in (3..lvl) %}../{% endfor %}{% endcapture %}
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700|PT+Sans:400,700,400italic' rel='stylesheet' type='text/css'>
<link href="{{ relative }}resources/css/bootstrap.min.css" rel="stylesheet">
<link href="{{ relative }}resources/css/style.css" rel="stylesheet">
<link href="{{ relative }}resources/css/lightbox.css" rel="stylesheet">
<script src="{{ relative }}resources/js/three.min.js"></script>
<script src="{{ relative }}resources/js/STLLoader.js"></script>
<script src="{{ relative }}resources/js/OrbitControls.js"></script>
<script src="{{ relative }}resources/js/stlviewer.js"></script>
<script src="{{ relative }}resources/js/lightbox.js"></script>
<title>{{ page.title }}</title>
{% endraw %}
</head>
<body>
<div class="wrapper">
<div id="header">
<div class="container">
{% raw %}
<h1>{{ page.title }}</h1>
{% endraw %}
</div>
</div>
{% raw %}
<div id="content">
<div class="container">
{{ content }}
</div>
</div>
{% endraw %}
</div>
<hr>
<div id="footer">
<div class="container">
<p>
{% raw %}
This 3D printable project is offered under <a href="{{ relative }}LICENSE.html">an open source license</a>.
The site is built with software under <a href="{{ relative }}other-licenses.html">various open source licenses</a>.
{% endraw %}
Create your own MySTL site with <a href="https://github.com/jepler/cookiecutter-mystl">cookiecutter-mystl</a>.
</p>
<a class="pull-right" href="#">Back to top</a>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,33 @@
#!/usr/bin/python3
import argparse
import glob
import os
import subprocess
import sys
parser = argparse.ArgumentParser()
parser.add_argument("branch", default="gh-pages", nargs="?")
args = parser.parse_args()
version = subprocess.getoutput("git describe --always")
fd = os.fdopen(sys.stdout.fileno(), 'wb')
fd.write(b"commit refs/heads/" + args.branch.encode('utf-8') + b"\n")
fd.write(b"committer Doc Man <noreply@example.com> now" + b"\n")
fd.write(b"data <<EOF" + b"\n")
fd.write(b"Docs built at " + version.encode('utf-8') + b"\n")
fd.write(b"EOF" + b"\n")
for root, dirs, files in os.walk("_site"):
for fn in files:
fn = os.path.join(root, fn)
gfn = fn.split("/", 1)[1]
print(fn, "->", gfn, file=sys.stderr)
with open(fn, 'rb') as f: contents = f.read()
fd.write(b"M 644 inline " + gfn.encode('utf-8') + b"\n")
fd.write(b"data " + str(len(contents)).encode("utf-8") + b"\n")
fd.write(contents)
fd.write(b"done\n")

View file

@ -0,0 +1,2 @@
CONVEXITY=4;
import(input, convexity=CONVEXITY);

View file

@ -0,0 +1,80 @@
#!/usr/bin/python3
import os
import pathlib
import shlex
import sys
import yaml
resources = pathlib.Path("resources")
MAKE_SCAD_STL = """
-include {target}.d
{target}: {src}
\t$(ECHO) $target
\t$(Q)mkdir -p $(dir {target})
\t$(Q)openscad -o {target}.tmp.stl -d {target}.d $(SCAD_FLAGS) {flags} {src}
\t$(Q)admesh -b {target} {target}.tmp.stl
\t$(Q)rm -f {target}.tmp.stl
default: {target}
"""
MAKE_SCAD_SVG = """
-include {target}.d
{target}: {src}
\t$(ECHO) $target
\t$(Q)mkdir -p $(dir {target})
\t$(Q)openscad -o {target} -d {target}.d $(SCAD_FLAGS) {flags} {src}
default: {target}
"""
MAKE_SCAD_PNG = """
-include {target}.d
{target}: {src}
\t$(ECHO) $target
\t$(Q)mkdir -p $(dir {target})
\t$(Q)openscad --imgsize=1800,1800 -o {target}.tmp.png -d {target}.d $(SCAD_FLAGS) {flags} {src}
\t$(Q)convert -geometry 25% {target}.tmp.png {target}
\t$(Q)rm -f {target}.tmp.png
default: {target}
"""
MAKE_STL_PNG = """
-include {target}.d
{target}: {src}
\t$(ECHO) $target
\t$(Q)mkdir -p $(dir {target})
\t$(Q)openscad --imgsize=1800,1800 -o {target}.tmp.png -d {target}.d $(SCAD_FLAGS) {flags} -Dinput=\\\"{src}\\\" _lib/readfile.scad
\t$(Q)convert -geometry 25% {target}.tmp.png {target}
\t$(Q)rm -f {target}.tmp.png
default: {target}
"""
with open(sys.argv[1]) as f:
doc = yaml.safe_load(f)
for row in doc:
flags = row.get('flags', '')
if isinstance(flags, str):
flags = flags.split()
flags = " ".join(shlex.quote(f) for f in flags)
scad = row.get('scad', '')
stl = row.get('stl', '')
svg = row.get('svg', '')
if stl.startswith('gen/'):
print(MAKE_SCAD_STL.format(
**{'target': resources / stl, 'src': resources / scad, 'flags': flags}))
if svg.startswith('gen/'):
print(MAKE_SCAD_SVG.format(
**{'target': resources / svg, 'src': resources / scad, 'flags': flags}))
for png in row.get('images', []):
if png.startswith('gen/'):
if scad:
print(MAKE_SCAD_PNG.format(
**{'target': resources / png, 'src': resources / scad, 'flags': flags}))
else:
print(MAKE_STL_PNG.format(
**{'target': resources / png, 'src': resources / stl, 'flags': flags}))

View file

@ -0,0 +1,63 @@
---
layout: base
title: {{ cookiecutter.project_name }}
---
{% raw %}
{% for asset in site.data.assets %}
{% assign stlbasename = asset.stl | split: "/" | last | split: "." | first %}
### {{ asset.name }}
<html>
{% for image in asset.images %}
{% assign basename = image | split: "/" | last | split: "." | first %}
{% if basename == stlbasename %}
<img src="{{ relative }}resources/{{ image }}" title="{{ asset.name }}" data-stl="{{ relative }}resources/{{ asset.stl }}">
{% else %}
<img src="{{ relative }}resources/{{ image }}" title="{{ asset.name }}">
{% endif %}
{% endfor %}
</html>
{% endfor %}
{% comment %}
Sections to consider adding:
## How I made this
## Print Settings
## Remixed from
{% endcomment %}
## Downloads
{% for asset in site.data.assets %}
### {{ asset.name }}:
{{ asset.notes }}
{% if asset.flags %}
STL generated with `{{ asset.flags }}`
{% endif %}
{% if asset.scad %}
{% assign basename = asset.scad | split: "/" | last %}
- [{{ basename }}](resources/{{ asset.scad }})
{% endif %}
{% if asset.stl %}
{% assign basename = asset.stl | split: "/" | last %}
- [{{ basename }}](resources/{{ asset.stl }})
{% endif %}
{% if asset.svg %}
{% assign basename = asset.svg | split: "/" | last %}
- [{{ basename }}](resources/{{ asset.svg }})
{% endif %}
{% for download in asset.downloads %}
{% assign basename = download | split: "/" | last %}
- [{{ basename }}](resources/{{ download }})
{% endfor %}
{% endfor %}
{% endraw %}

View file

@ -0,0 +1,11 @@
---
layout: base
title: Web Site Licenses
---
This website is built on various open source technologies:
- [Jekyll](https://github.com/jekyll/jekyll/blob/master/LICENSE) [MIT]
- [Jekyll Codex](https://github.com/jhvanderschee/jekyllcodex/#mit-license) [MIT]
- [three.js](https://github.com/mrdoob/three.js/blob/dev/LICENSE) [MIT]
- [simple-stl-viewer](https://tonybox.net/posts/simple-stl-viewer/) [MIT]

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,95 @@
#lightbox {width: 100%; height: 100%; position: fixed; top: 0; left: 0; background: rgba(0,0,0,0.85); z-index: 9999999; line-height: 0; cursor: pointer; display: none;}
#lightbox .img {
position: relative;
top: 50%;
left: 50%;
-ms-transform: translateX(-50%) translateY(-50%);
-webkit-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
max-width: 100%;
max-height: 100%;
}
#lightbox .img img {opacity: 0; pointer-events: none; width: auto;}
@media screen and (min-width: 1200px) {
#lightbox .img {
max-width: 1200px;
}
}
@media screen and (min-height: 1200px) {
#lightbox .img {
max-height: 1200px;
}
}
#lightbox span {display: block; position: fixed; bottom: 13px; height: 1.5em; line-height: 1.4em; width: 100%; text-align: center; color: white; text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
}
#lightbox span {display: none;}
#lightbox .videoWrapperContainer {
position: relative;
top: 50%;
left: 50%;
-ms-transform: translateX(-50%) translateY(-50%);
-webkit-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
max-width: 900px;
max-height: 100%;
}
#lightbox .videoWrapperContainer .videoWrapper {
height: 0;
line-height: 0;
margin: 0;
padding: 0;
position: relative;
padding-bottom: 56.333%; /* custom */
background: black;
}
#lightbox .videoWrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
display: block;
}
#lightbox #prev, #lightbox #next {height: 50px; line-height: 36px; display: none; margin-top: -25px; position: fixed; top: 50%; padding: 0 15px; cursor: pointer; text-decoration: none; z-index: 99; color: white; font-size: 60px;}
#lightbox.gallery #prev, #lightbox.gallery #next {display: block;}
#lightbox #prev {left: 0;}
#lightbox #next {right: 0;}
#lightbox #close {height: 50px; width: 50px; position: fixed; cursor: pointer; text-decoration: none; z-index: 99; right: 0; top: 0;}
#lightbox #close:after, #lightbox #close:before {position: absolute; margin-top: 22px; margin-left: 14px; content: ""; height: 3px; background: white; width: 23px;
-webkit-transform-origin: 50% 50%;
-moz-transform-origin: 50% 50%;
-o-transform-origin: 50% 50%;
transform-origin: 50% 50%;
/* Safari */
-webkit-transform: rotate(-45deg);
/* Firefox */
-moz-transform: rotate(-45deg);
/* IE */
-ms-transform: rotate(-45deg);
/* Opera */
-o-transform: rotate(-45deg);
}
#lightbox #close:after {
/* Safari */
-webkit-transform: rotate(45deg);
/* Firefox */
-moz-transform: rotate(45deg);
/* IE */
-ms-transform: rotate(45deg);
/* Opera */
-o-transform: rotate(45deg);
}
#lightbox, #lightbox * {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.lightbox-image-stl { cursor: pointer; }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,393 @@
/**
* @author aleeper / http://adamleeper.com/
* @author mrdoob / http://mrdoob.com/
* @author gero3 / https://github.com/gero3
* @author Mugen87 / https://github.com/Mugen87
* @author neverhood311 / https://github.com/neverhood311
*
* Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.
*
* Supports both binary and ASCII encoded files, with automatic detection of type.
*
* The loader returns a non-indexed buffer geometry.
*
* Limitations:
* Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).
* There is perhaps some question as to how valid it is to always assume little-endian-ness.
* ASCII decoding assumes file is UTF-8.
*
* Usage:
* var loader = new THREE.STLLoader();
* loader.load( './models/stl/slotted_disk.stl', function ( geometry ) {
* scene.add( new THREE.Mesh( geometry ) );
* });
*
* For binary STLs geometry might contain colors for vertices. To use it:
* // use the same code to load STL as above
* if (geometry.hasColors) {
* material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: true });
* } else { .... }
* var mesh = new THREE.Mesh( geometry, material );
*
* For ASCII STLs containing multiple solids, each solid is assigned to a different group.
* Groups can be used to assign a different color by defining an array of materials with the same length of
* geometry.groups and passing it to the Mesh constructor:
*
* var mesh = new THREE.Mesh( geometry, material );
*
* For example:
*
* var materials = [];
* var nGeometryGroups = geometry.groups.length;
*
* var colorMap = ...; // Some logic to index colors.
*
* for (var i = 0; i < nGeometryGroups; i++) {
*
* var material = new THREE.MeshPhongMaterial({
* color: colorMap[i],
* wireframe: false
* });
*
* }
*
* materials.push(material);
* var mesh = new THREE.Mesh(geometry, materials);
*/
THREE.STLLoader = function ( manager ) {
THREE.Loader.call( this, manager );
};
THREE.STLLoader.prototype = Object.assign( Object.create( THREE.Loader.prototype ), {
constructor: THREE.STLLoader,
load: function ( url, onLoad, onProgress, onError ) {
var scope = this;
var loader = new THREE.FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setResponseType( 'arraybuffer' );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
},
parse: function ( data ) {
function isBinary( data ) {
var expect, face_size, n_faces, reader;
reader = new DataView( data );
face_size = ( 32 / 8 * 3 ) + ( ( 32 / 8 * 3 ) * 3 ) + ( 16 / 8 );
n_faces = reader.getUint32( 80, true );
expect = 80 + ( 32 / 8 ) + ( n_faces * face_size );
if ( expect === reader.byteLength ) {
return true;
}
// An ASCII STL data must begin with 'solid ' as the first six bytes.
// However, ASCII STLs lacking the SPACE after the 'd' are known to be
// plentiful. So, check the first 5 bytes for 'solid'.
// Several encodings, such as UTF-8, precede the text with up to 5 bytes:
// https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
// Search for "solid" to start anywhere after those prefixes.
// US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd'
var solid = [ 115, 111, 108, 105, 100 ];
for ( var off = 0; off < 5; off ++ ) {
// If "solid" text is matched to the current offset, declare it to be an ASCII STL.
if ( matchDataViewAt( solid, reader, off ) ) return false;
}
// Couldn't find "solid" text at the beginning; it is binary STL.
return true;
}
function matchDataViewAt( query, reader, offset ) {
// Check if each byte in query matches the corresponding byte from the current offset
for ( var i = 0, il = query.length; i < il; i ++ ) {
if ( query[ i ] !== reader.getUint8( offset + i, false ) ) return false;
}
return true;
}
function parseBinary( data ) {
var reader = new DataView( data );
var faces = reader.getUint32( 80, true );
var r, g, b, hasColors = false, colors;
var defaultR, defaultG, defaultB, alpha;
// process STL header
// check for default color in header ("COLOR=rgba" sequence).
for ( var index = 0; index < 80 - 10; index ++ ) {
if ( ( reader.getUint32( index, false ) == 0x434F4C4F /*COLO*/ ) &&
( reader.getUint8( index + 4 ) == 0x52 /*'R'*/ ) &&
( reader.getUint8( index + 5 ) == 0x3D /*'='*/ ) ) {
hasColors = true;
colors = new Float32Array( faces * 3 * 3 );
defaultR = reader.getUint8( index + 6 ) / 255;
defaultG = reader.getUint8( index + 7 ) / 255;
defaultB = reader.getUint8( index + 8 ) / 255;
alpha = reader.getUint8( index + 9 ) / 255;
}
}
var dataOffset = 84;
var faceLength = 12 * 4 + 2;
var geometry = new THREE.BufferGeometry();
var vertices = new Float32Array( faces * 3 * 3 );
var normals = new Float32Array( faces * 3 * 3 );
for ( var face = 0; face < faces; face ++ ) {
var start = dataOffset + face * faceLength;
var normalX = reader.getFloat32( start, true );
var normalY = reader.getFloat32( start + 4, true );
var normalZ = reader.getFloat32( start + 8, true );
if ( hasColors ) {
var packedColor = reader.getUint16( start + 48, true );
if ( ( packedColor & 0x8000 ) === 0 ) {
// facet has its own unique color
r = ( packedColor & 0x1F ) / 31;
g = ( ( packedColor >> 5 ) & 0x1F ) / 31;
b = ( ( packedColor >> 10 ) & 0x1F ) / 31;
} else {
r = defaultR;
g = defaultG;
b = defaultB;
}
}
for ( var i = 1; i <= 3; i ++ ) {
var vertexstart = start + i * 12;
var componentIdx = ( face * 3 * 3 ) + ( ( i - 1 ) * 3 );
vertices[ componentIdx ] = reader.getFloat32( vertexstart, true );
vertices[ componentIdx + 1 ] = reader.getFloat32( vertexstart + 4, true );
vertices[ componentIdx + 2 ] = reader.getFloat32( vertexstart + 8, true );
normals[ componentIdx ] = normalX;
normals[ componentIdx + 1 ] = normalY;
normals[ componentIdx + 2 ] = normalZ;
if ( hasColors ) {
colors[ componentIdx ] = r;
colors[ componentIdx + 1 ] = g;
colors[ componentIdx + 2 ] = b;
}
}
}
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) );
if ( hasColors ) {
geometry.setAttribute( 'color', new THREE.BufferAttribute( colors, 3 ) );
geometry.hasColors = true;
geometry.alpha = alpha;
}
return geometry;
}
function parseASCII( data ) {
var geometry = new THREE.BufferGeometry();
var patternSolid = /solid([\s\S]*?)endsolid/g;
var patternFace = /facet([\s\S]*?)endfacet/g;
var faceCounter = 0;
var patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/.source;
var patternVertex = new RegExp( 'vertex' + patternFloat + patternFloat + patternFloat, 'g' );
var patternNormal = new RegExp( 'normal' + patternFloat + patternFloat + patternFloat, 'g' );
var vertices = [];
var normals = [];
var normal = new THREE.Vector3();
var result;
var groupCount = 0;
var startVertex = 0;
var endVertex = 0;
while ( ( result = patternSolid.exec( data ) ) !== null ) {
startVertex = endVertex;
var solid = result[ 0 ];
while ( ( result = patternFace.exec( solid ) ) !== null ) {
var vertexCountPerFace = 0;
var normalCountPerFace = 0;
var text = result[ 0 ];
while ( ( result = patternNormal.exec( text ) ) !== null ) {
normal.x = parseFloat( result[ 1 ] );
normal.y = parseFloat( result[ 2 ] );
normal.z = parseFloat( result[ 3 ] );
normalCountPerFace ++;
}
while ( ( result = patternVertex.exec( text ) ) !== null ) {
vertices.push( parseFloat( result[ 1 ] ), parseFloat( result[ 2 ] ), parseFloat( result[ 3 ] ) );
normals.push( normal.x, normal.y, normal.z );
vertexCountPerFace ++;
endVertex ++;
}
// every face have to own ONE valid normal
if ( normalCountPerFace !== 1 ) {
console.error( 'THREE.STLLoader: Something isn\'t right with the normal of face number ' + faceCounter );
}
// each face have to own THREE valid vertices
if ( vertexCountPerFace !== 3 ) {
console.error( 'THREE.STLLoader: Something isn\'t right with the vertices of face number ' + faceCounter );
}
faceCounter ++;
}
var start = startVertex;
var count = endVertex - startVertex;
geometry.addGroup( start, count, groupCount );
groupCount ++;
}
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) );
return geometry;
}
function ensureString( buffer ) {
if ( typeof buffer !== 'string' ) {
return THREE.LoaderUtils.decodeText( new Uint8Array( buffer ) );
}
return buffer;
}
function ensureBinary( buffer ) {
if ( typeof buffer === 'string' ) {
var array_buffer = new Uint8Array( buffer.length );
for ( var i = 0; i < buffer.length; i ++ ) {
array_buffer[ i ] = buffer.charCodeAt( i ) & 0xff; // implicitly assumes little-endian
}
return array_buffer.buffer || array_buffer;
} else {
return buffer;
}
}
// start
var binData = ensureBinary( data );
return isBinary( binData ) ? parseBinary( binData ) : parseASCII( ensureString( data ) );
}
} );

View file

@ -0,0 +1,87 @@
function setGallery(el) {
var elements = document.body.querySelectorAll(".gallery");
elements.forEach(element => {
element.classList.remove('gallery');
});
if(el.closest('ul, p')) {
var link_elements = el.closest('ul, p').querySelectorAll("a[class*='lightbox-']");
link_elements.forEach(link_element => {
link_element.classList.remove('current');
});
link_elements.forEach(link_element => {
if(el.getAttribute('href') == link_element.getAttribute('href')) {
link_element.classList.add('current');
}
});
if(link_elements.length>1) {
document.getElementById('lightbox').classList.add('gallery');
link_elements.forEach(link_element => {
link_element.classList.add('gallery');
});
}
var currentkey;
var gallery_elements = document.querySelectorAll('a.gallery');
Object.keys(gallery_elements).forEach(function (k) {
if(gallery_elements[k].classList.contains('current')) currentkey = k;
});
if(currentkey==(gallery_elements.length-1)) var nextkey = 0;
else var nextkey = parseInt(currentkey)+1;
if(currentkey==0) var prevkey = parseInt(gallery_elements.length-1);
else var prevkey = parseInt(currentkey)-1;
document.getElementById('next').addEventListener("click", function() {
gallery_elements[nextkey].click();
});
document.getElementById('prev').addEventListener("click", function() {
gallery_elements[prevkey].click();
});
}
}
document.addEventListener("DOMContentLoaded", function() {
//create lightbox div in the footer
var newdiv = document.createElement("div");
newdiv.tabIndex = 0;
newdiv.setAttribute('id',"lightbox");
document.body.appendChild(newdiv);
//add classes to links to be able to initiate lightboxes
var elements = document.querySelectorAll('img');
elements.forEach(element => {
var url = element.getAttribute('data-stl');
if(url) {
element.classList.add('lightbox-image-stl');
}
});
//remove the clicked lightbox
document.getElementById('lightbox').addEventListener("click", function(event) {
if(event.target.id == 'close'){
this.innerHTML = '';
document.getElementById('lightbox').style.display = 'none';
}
});
// hit esc to dismiss lightbox
document.getElementById('lightbox').addEventListener("keydown", function(event) {
if(event.key == 'Escape'){
this.innerHTML = '';
document.getElementById('lightbox').style.display = 'none';
}
});
//add the stl lightbox on click
var elements = document.querySelectorAll('img.lightbox-image-stl');
elements.forEach(element => {
element.addEventListener("click", function(event) {
event.preventDefault();
document.getElementById('lightbox').innerHTML = '<a id="close"></a><a id="next">&rsaquo;</a><a id="prev">&lsaquo;</a><div id="model" class="img" style="width: 100%; height:100%;" title="'+this.getAttribute('title')+'" ><</div><span>'+this.getAttribute('title')+'</span>';
document.getElementById('lightbox').style.display = 'block';
document.getElementById('model').setAttribute('data-rotate', 'x');
document.getElementById('model').setAttribute('data-zdistance', '4');
STLViewer(document.getElementById("model"), this.getAttribute('data-stl'));
setGallery(this);
document.getElementById('lightbox').focus();
});
});
});

View file

@ -0,0 +1,86 @@
function STLViewerEnable(classname) {
var models = document.getElementsByClassName(classname);
for (var i = 0; i < models.length; i++) {
STLViewer(models[i], models[i].getAttribute("data-src"));
}
}
function STLViewer(elem, model) {
var renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
var camera = new THREE.PerspectiveCamera(50, elem.clientWidth / elem.clientHeight, 1, 1000);
renderer.setSize(elem.clientWidth, elem.clientHeight);
elem.appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.rotateSpeed = 0.7;
controls.dampingFactor = 0.1;
controls.enableZoom = true;
controls.enablePan = true;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.75;
var scene = new THREE.Scene();
scene.add(new THREE.HemisphereLight(0xffffff, 0x080820, 1.5));
(new THREE.STLLoader()).load(model, function (geometry) {
// Determine the color
var colorString = elem.getAttribute("data-color")
if(colorString != null) { var color = new THREE.Color(colorString); }
else { var color = 0xffff33 }
// Set up the material
var material = new THREE.MeshPhongMaterial({ color: color, specular: 100, shininess: 100 });
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Compute the middle
var middle = new THREE.Vector3();
geometry.computeBoundingBox();
geometry.boundingBox.getCenter(middle);
// Center it
mesh.geometry.applyMatrix(new THREE.Matrix4().makeTranslation( -middle.x, -middle.y, -middle.z ) );
// Rotate, if desired
if(elem.getAttribute("data-rotate") == "x")
mesh.rotation.x = -Math.PI/3
// Pull the camera away as needed
var largestDimension = Math.max(geometry.boundingBox.max.x - geometry.boundingBox.min.x,
geometry.boundingBox.max.y - geometry.boundingBox.min.y,
geometry.boundingBox.max.z - geometry.boundingBox.min.z)
camera.position.z = largestDimension * elem.getAttribute("data-zdistance");
var animate = function () {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}; animate();
const observer = new MutationObserver(function(mutationList, observer) {
mutationList.forEach(element => {
if(element.removedNodes.length > 0) {
renderer.dispose();
geometry.dispose();
material.dispose();
observer.disconnect();
}
})
});
observer.observe(elem.parentElement, {childList: true});
window.addEventListener('resize', function () {
renderer.setSize(elem.clientWidth, elem.clientHeight);
camera.aspect = elem.clientWidth / elem.clientHeight;
camera.updateProjectionMatrix();
}, false);
});
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,3 @@
size = 10;
cube(size);

View file

@ -0,0 +1,9 @@
The next time you see the full moon high in the south, look carefully at its
right-hand edge and let your eye travel upward along the curve of the disk.
Round about two o'clock you will notice a small, dark oval: anyone with normal
eyesight can find it quite easily. It is the great walled plain, one of the
finest on the Moon, known as the Mare Crisium---the Sea of Crises. Three hundred
miles in diameter, and almost completely surrounded by a ring of magnificent
mountains, it had never been explored until we entered it in the late summer of
1996.
[extract from SENTINEL OF ETERNITY (1951) by Arthur C. Clarke]

View file

@ -0,0 +1,17 @@
# resources/gen/main has no recognized extension
-include resources/gen/main.stl.d
resources/gen/main.stl: resources/main.scad
mkdir -p $(dir resources/gen/main.stl)
openscad -o resources/gen/main.stl.tmp.stl -d resources/gen/main.stl.d $(SCAD_FLAGS) resources/main.scad
admesh -b resources/gen/main.stl resources/gen/main.stl.tmp.stl
rm -f resources/gen/main.stl.tmp.stl
default:: resources/gen/main.stl
-include resources/gen/main.png.d
resources/gen/main.png: resources/main.scad
mkdir -p $(dir resources/gen/main.png)
openscad -o resources/gen/main.png -d resources/gen/main.png.d $(SCAD_FLAGS) resources/main.scad
default:: resources/gen/main.png

View file

@ -0,0 +1,69 @@
#!/usr/bin/python3
import os
import sys
MAKE_SCAD_STL = """
-include {target}.d
{target}: {src}
\tmkdir -p $(dir {target})
\topenscad -o {target}.tmp.stl -d {target}.d $(SCAD_FLAGS) {flags} {src}
\tadmesh -b {target} {target}.tmp.stl
\trm -f {target}.tmp.stl
default:: {target}
"""
MAKE_SCAD_PNG = """
-include {target}.d
{target}: {src}
\tmkdir -p $(dir {target})
\topenscad -o {target} -d {target}.d $(SCAD_FLAGS) {flags} {src}
default:: {target}
"""
MAKE_STL_PNG = """
-include {target}.d
{target}: {src}
\tmkdir -p $(dir {target})
\topenscad -o {target} -d {target}.d $(SCAD_FLAGS) {flags} -Dinput=\"{src}\" readfile.scad
default:: {target}
"""
with open(sys.argv[1]) as f:
for row in f:
parts = row.split(":", 2)
if len(parts) == 1:
target = parts
src = f"{target}.scad"
if not os.exists(src):
src = f"{target}.stl"
flags = ""
if len(parts) == 2:
target, src = parts
flags = ""
else:
target, src, flags = parts
target = target.strip()
src = src.strip()
flags = flags.strip()
src = f"resources/{src}"
target = f"resources/gen/{target}"
if src.lower().endswith(".scad"):
if target.lower().endswith(".stl"):
print(MAKE_SCAD_STL.format(**{'target': target, 'src': src, 'flags': flags}))
elif target.lower().endswith(".png"):
print(MAKE_SCAD_PNG.format(**{'target': target, 'src': src, 'flags': flags}))
else:
print(f"# {target} has no recognized extension")
print(MAKE_SCAD_STL.format(**{'target': f"{target}.stl", 'src': src, 'flags': flags}))
print(MAKE_SCAD_PNG.format(**{'target': f"{target}.png", 'src': src, 'flags': flags}))
elif src.lower().endswith(".stl"):
if target.lower().endswith(".png"):
print(MAKE_STL_PNG.format(**{'target': target, 'src': src, 'flags': flags}))
else:
print(MAKE_STL_PNG.format(**{'target': f"{target}.png", 'src': src, 'flags': flags}))
else:
raise RuntimeError(f"Don't know how to handle {src} -> {target}")