Initial script to generate tutorial images.

This commit is contained in:
Revar Desmera 2020-03-24 00:02:24 -07:00
parent cb42c0d9aa
commit a3c917bfe9
4 changed files with 400 additions and 60 deletions

41
scripts/make_tutorials.sh Executable file
View file

@ -0,0 +1,41 @@
#!/bin/bash
FORCED=""
FILES=""
DISPMD=""
for opt in "$@" ; do
case $opt in
-f ) FORCED=$opt ;;
-d ) DISPMD=$opt ;;
-* ) echo "Unknown option $opt"; exit -1 ;;
* ) FILES="$FILES $opt" ;;
esac
done
if [[ "$FILES" != "" ]]; then
PREVIEW_LIBS="$FILES"
else
PREVIEW_LIBS="FractalTree"
fi
dir="$(basename $PWD)"
if [ "$dir" = "BOSL2" ]; then
cd BOSL2.wiki
elif [ "$dir" != "BOSL2.wiki" ]; then
echo "Must run this script from the BOSL2 or BOSL2/BOSL2.wiki directories."
exit 1
fi
rm -f tmp_*.scad
for base in $PREVIEW_LIBS; do
base="$(basename $base .md)"
mkdir -p images/tutorials
rm -f images/tutorials/*.png images/tutorials/*.gif
echo "$base.md"
../scripts/tutorial_gen.py ../tutorials/$base.md -o Tutorial-$base.md $FORCED -I images/tutorials/ || exit 1
if [ "$DISPMD" != "" ]; then
open -a Typora Tutorial-$base.md
fi
done

320
scripts/tutorial_gen.py Executable file
View file

@ -0,0 +1,320 @@
#!/usr/bin/env python3.7
from __future__ import print_function
import os
import re
import sys
import math
import random
import hashlib
import filecmp
import dbm.gnu
import os.path
import platform
import argparse
import subprocess
from PIL import Image, ImageChops
if platform.system() == "Darwin":
OPENSCAD = "/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD"
GIT = "git"
else:
OPENSCAD = "openscad"
GIT = "git"
def image_compare(file1, file2):
img1 = Image.open(file1)
img2 = Image.open(file2)
if img1.size != img2.size or img1.getbands() != img2.getbands():
return False
diff = ImageChops.difference(img1, img2).histogram()
sq = (value * (i % 256) ** 2 for i, value in enumerate(diff))
sum_squares = sum(sq)
rms = math.sqrt(sum_squares / float(img1.size[0] * img1.size[1]))
return rms<10
def image_resize(infile, outfile, newsize=(320,240)):
im = Image.open(infile)
im.thumbnail(newsize, Image.ANTIALIAS)
im.save(outfile)
def make_animated_gif(imgfiles, outfile, size):
imgs = []
for file in imgfiles:
img = Image.open(file)
img.thumbnail(size, Image.ANTIALIAS)
imgs.append(img)
imgs[0].save(
outfile,
save_all=True,
append_images=imgs[1:],
duration=250,
loop=0
)
def git_checkout(filename):
# Pull previous committed image from git, if it exists.
gitcmd = [GIT, "checkout", filename]
p = subprocess.Popen(gitcmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
err = p.stdout.read()
def run_openscad_script(libfile, infile, imgfile, imgsize=(320,240), eye=None, show_edges=False, render=False):
scadcmd = [
OPENSCAD,
"-o", imgfile,
"--imgsize={},{}".format(imgsize[0]*2, imgsize[1]*2),
"--hardwarnings",
"--projection=o",
"--autocenter",
"--viewall"
]
if eye is not None:
scadcmd.extend(["--camera", eye+",0,0,0"])
if show_edges:
scadcmd.extend(["--view=axes,scales,edges"])
else:
scadcmd.extend(["--view=axes,scales"])
if render: # Force render
scadcmd.extend(["--render", ""])
scadcmd.append(infile)
with open(infile, "r") as f:
script = "".join(f.readlines());
p = subprocess.Popen(scadcmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
(stdoutdata, stderrdata) = p.communicate(None)
res = p.returncode
if res != 0 or b"ERROR:" in stderrdata or b"TRACE:" in stderrdata:
print("\n\n{}".format(stderrdata.decode('utf-8')))
print("////////////////////////////////////////////////////")
print("// {}: {} for {}".format(libfile, infile, imgfile))
print("////////////////////////////////////////////////////")
print(script)
print("////////////////////////////////////////////////////")
print("")
with open("FAILED.scad", "w") as f:
print("////////////////////////////////////////////////////", file=f)
print("// {}: {} for {}".format(libfile, infile, imgfile), file=f)
print("////////////////////////////////////////////////////", file=f)
print(script, file=f)
print("////////////////////////////////////////////////////", file=f)
print("", file=f)
sys.exit(-1)
return imgfile
class ImageProcessing(object):
def __init__(self):
self.examples = []
self.commoncode = []
self.imgroot = ""
self.keep_scripts = False
self.force = False
def set_keep_scripts(self, x):
self.keep_scripts = x
def add_image(self, libfile, imgfile, code, extype):
self.examples.append((libfile, imgfile, code, extype))
def set_commoncode(self, code):
self.commoncode = code
def process_examples(self, imgroot, force=False):
self.imgroot = imgroot
self.force = force
self.hashes = {}
with dbm.gnu.open("examples_hashes.gdbm", "c") as db:
for libfile, imgfile, code, extype in self.examples:
self.gen_example_image(db, libfile, imgfile, code, extype)
for key, hash in self.hashes.items():
db[key] = hash
def gen_example_image(self, db, libfile, imgfile, code, extype):
if extype == "NORENDER":
return
print(" {}".format(imgfile), end='')
sys.stdout.flush()
scriptfile = "tmp_{0}.scad".format(imgfile.replace(".", "_").replace("/","_"))
targimgfile = self.imgroot + imgfile
newimgfile = self.imgroot + "_new_" + imgfile
# Pull previous committed image from git, if it exists.
git_checkout(targimgfile)
m = hashlib.sha256()
m.update(extype.encode("utf8"))
for line in code:
m.update(line.encode("utf8"))
hash = m.digest()
key = "{0} - {1}".format(libfile, imgfile)
if key in db and db[key] == hash and not self.force:
print("")
return
stdlibs = ["std.scad", "debug.scad"]
script = ""
for lib in stdlibs:
script += "include <BOSL2/%s>\n" % lib
for line in self.commoncode:
script += line+"\n"
for line in code:
script += line+"\n"
with open(scriptfile, "w") as f:
f.write(script)
if "Big" in extype:
imgsize = (640, 480)
elif "Med" in extype or "distribute" in script or "show_anchors" in script:
imgsize = (480, 360)
else: # Small
imgsize = (320, 240)
show_edges = "Edges" in extype
render = "FR" in extype
tmpimgs = []
if "Spin" in extype:
for ang in range(0,359,10):
tmpimgfile = "{0}tmp_{2}_{1}.png".format(self.imgroot, ang, imgfile.replace(".", "_"))
arad = ang * math.pi / 180;
eye = "{0},{1},{2}".format(
500*math.cos(arad),
500*math.sin(arad),
500 if "Flat" in extype else 500*math.sin(arad)
)
run_openscad_script(
libfile, scriptfile, tmpimgfile,
imgsize=(imgsize[0]*2,imgsize[1]*2),
eye=eye,
show_edges=show_edges,
render=render
)
tmpimgs.append(tmpimgfile)
print(".", end='')
sys.stdout.flush()
else:
tmpimgfile = self.imgroot + "tmp_" + imgfile
eye = "0,0,500" if "2D" in extype else None
run_openscad_script(
libfile, scriptfile, tmpimgfile,
imgsize=(imgsize[0]*2,imgsize[1]*2),
eye=eye,
show_edges=show_edges,
render=render
)
tmpimgs.append(tmpimgfile)
if not self.keep_scripts:
os.unlink(scriptfile)
if len(tmpimgs) == 1:
image_resize(tmpimgfile, newimgfile, imgsize)
os.unlink(tmpimgs.pop(0))
else:
make_animated_gif(tmpimgs, newimgfile, size=imgsize)
for tmpimg in tmpimgs:
os.unlink(tmpimg)
print("")
# Time to compare image.
if not os.path.isfile(targimgfile):
print(" NEW IMAGE\n")
os.rename(newimgfile, targimgfile)
else:
if targimgfile.endswith(".gif"):
issame = filecmp.cmp(targimgfile, newimgfile, shallow=False)
else:
issame = image_compare(targimgfile, newimgfile);
if issame:
os.unlink(newimgfile)
else:
print(" UPDATED IMAGE\n")
os.unlink(targimgfile)
os.rename(newimgfile, targimgfile)
self.hashes[key] = hash
imgprc = ImageProcessing()
def processFile(infile, outfile=None, imgroot=""):
if imgroot and not imgroot.endswith('/'):
imgroot += "/"
fileroot = os.path.splitext(os.path.basename(infile))[0]
outdata = []
with open(infile, "r") as f:
script = []
extyp = ""
in_script = False
imgnum = 0
for line in f.readlines():
line = line.rstrip("\n")
outdata.append(line)
if in_script:
if line == "```":
in_script = False
imgfile = "{}_{}.png".format(fileroot, imgnum)
imgprc.add_image(fileroot+".md", imgfile, script, extyp)
outdata.append("![Figure {}]({})".format(imgnum, imgroot + imgfile))
script = []
else:
script.append(line)
if line.startswith("```openscad"):
in_script = True
if "-" in line:
extyp = line.split("-")[1]
else:
extyp = ""
script = []
imgnum = imgnum + 1
if outfile == None:
f = sys.stdout
else:
f = open(outfile, "w")
for line in outdata:
print(line, file=f)
if outfile:
f.close()
def main():
parser = argparse.ArgumentParser(prog='docs_gen')
parser.add_argument('-k', '--keep-scripts', action="store_true",
help="If given, don't delete the temporary image OpenSCAD scripts.")
parser.add_argument('-f', '--force', action="store_true",
help='If given, force generation of images when the code is unchanged.')
parser.add_argument('-I', '--imgroot', default="",
help='The directory to put generated images in.')
parser.add_argument('-o', '--outfile',
help='Output file, if different from infile.')
parser.add_argument('infile', help='Input filename.')
args = parser.parse_args()
imgprc.set_keep_scripts(args.keep_scripts)
processFile(
args.infile,
outfile=args.outfile,
imgroot=args.imgroot
)
imgprc.process_examples(args.imgroot, force=args.force)
sys.exit(0)
if __name__ == "__main__":
main()
# vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap

View file

@ -2,78 +2,55 @@
### Start with a Tree Trunk
Firstoff, include the BOSL2 library, then add a tapered cylinder for the tree trunk.
Firstoff, include the BOSL2 library, then make a starting module that just has a tapered cylinder for the tree trunk.
```openscad-example
```openscad
include <BOSL2/std.scad>
cylinder(l=1500, d1=300, d2=210);
module tree(l=1500, sc=0.7)
cylinder(l=l, d1=l/5, d2=l/5*sc);
tree();
```
### Parameterize It
### Attaching a Branch
It's easier to adjust a model if you split out the defining parameters.
You can attach a branch to the top of the trunk by using `attach()` as a child of the trunk cylinder.
```openscad-example
include <BOSL2/std.scad>
l = 1500;
sc = 0.7;
cylinder(l=l, d1=l/5, d2=l/5*sc);
```
### Attaching Branches
You can attach branches to the top of the trunk by using `attach()` as a child of the trunk cylinder.
```openscad-example
include <BOSL2/std.scad>
l = 1500;
sc = 0.7;
cylinder(l=l, d1=l/5, d2=l/5*sc) {
attach(TOP) yrot( 30) cylinder(l=l*sc, d1=l/5*sc, d2=l/5*sc*sc);
attach(TOP) yrot(-30) cylinder(l=l*sc, d1=l/5*sc, d2=l/5*sc*sc);
}
```
### Replicate Branches
Instead of attaching each branch individually, you can attach multiple branch copies at once.
```openscad-example
include <BOSL2/std.scad>
l = 1500;
sc = 0.7;
cylinder(l=l, d1=l/5, d2=l/5*sc)
attach(TOP)
zrot_copies(n=2) // Make multiple rotated copies
yrot(30) cylinder(l=l*sc, d1=l/5*sc, d2=l/5*sc*sc);
```
### Make it a Module
Lets make this into a module, for convenience.
```openscad-example
```openscad
include <BOSL2/std.scad>
module tree(l=1500, sc=0.7)
cylinder(l=l, d1=l/5, d2=l/5*sc)
attach(TOP)
zrot_copies(n=2)
yrot(30) cylinder(l=l*sc, d1=l/5*sc, d2=l/5*sc*sc);
tree();
```
### Replicating the Branch
Instead of attaching each branch individually, you can make multiple copies of one branch, that are rotated relative to each other.
```openscad
include <BOSL2/std.scad>
module tree(l=1500, sc=0.7)
cylinder(l=l, d1=l/5, d2=l/5*sc)
attach(TOP)
zrot_copies(n=2) // Replicate that branch
yrot(30) cylinder(l=l*sc, d1=l/5*sc, d2=l/5*sc*sc);
tree();
```
### Use Recursion
Since branches look much like the main trunk, we can make it recursive. Don't forget the termination clause, or else it'll try to recurse forever!
Since branches look much like the main trunk, we can make the tree recursive. Don't forget the termination clause, or else it'll try to recurse forever!
```openscad-example
```openscad-Med
include <BOSL2/std.scad>
module tree(l=1500, sc=0.7, depth=10)
cylinder(l=l, d1=l/5, d2=l/5*sc)
attach(TOP)
if (depth>0) // Important!
if (depth>0) { // Important!
zrot_copies(n=2)
yrot(30) tree(depth=depth-1, l=l*sc, sc=sc);
}
tree();
```
@ -81,15 +58,16 @@ tree();
A flat planar tree isn't what we want, so lets bush it out a bit by rotating each level 90 degrees.
```openscad-example
```openscad-Med
include <BOSL2/std.scad>
module tree(l=1500, sc=0.7, depth=10)
cylinder(l=l, d1=l/5, d2=l/5*sc)
attach(TOP)
if (depth>0)
if (depth>0) {
zrot(90) // Bush it out
zrot_copies(n=2)
yrot(30) tree(depth=depth-1, l=l*sc, sc=sc);
}
tree();
```
@ -97,18 +75,19 @@ tree();
Let's add leaves. They look much like squashed versions of the standard teardrop() module, so lets use that.
```openscad-example
```openscad-Big
include <BOSL2/std.scad>
module tree(l=1500, sc=0.7, depth=10)
cylinder(l=l, d1=l/5, d2=l/5*sc)
attach(TOP)
if (depth>0)
if (depth>0) {
zrot(90)
zrot_copies(n=2)
yrot(30) tree(depth=depth-1, l=l*sc, sc=sc);
else
} else {
yscale(0.67)
teardrop(d=l*3, l=1, anchor=BOT, spin=90);
}
tree();
```
@ -119,21 +98,21 @@ their descendants to the new color, even if they were colored before. The `recol
however, will only color children and decendants that don't already have a color set by a more
nested `recolor()`.
```openscad-example
```openscad-Big
include <BOSL2/std.scad>
module tree(l=1500, sc=0.7, depth=10)
recolor("lightgray")
cylinder(l=l, d1=l/5, d2=l/5*sc)
attach(TOP)
if (depth>0)
if (depth>0) {
zrot(90)
zrot_copies(n=2)
yrot(30)
tree(depth=depth-1, l=l*sc, sc=sc);
else
yrot(30) tree(depth=depth-1, l=l*sc, sc=sc);
} else {
recolor("springgreen")
yscale(0.67)
teardrop(d=l*3, l=1, anchor=BOT, spin=90);
}
tree();
```

View file

@ -8,7 +8,7 @@
//////////////////////////////////////////////////////////////////////
BOSL_VERSION = [2,0,218];
BOSL_VERSION = [2,0,219];
// Section: BOSL Library Version Functions