diff --git a/scripts/make_tutorials.sh b/scripts/make_tutorials.sh new file mode 100755 index 0000000..66805aa --- /dev/null +++ b/scripts/make_tutorials.sh @@ -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 + + diff --git a/scripts/tutorial_gen.py b/scripts/tutorial_gen.py new file mode 100755 index 0000000..a9f2785 --- /dev/null +++ b/scripts/tutorial_gen.py @@ -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 \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 diff --git a/tutorials/Fractal_Tree.md b/tutorials/FractalTree.md similarity index 57% rename from tutorials/Fractal_Tree.md rename to tutorials/FractalTree.md index 095f6d6..609b8d8 100644 --- a/tutorials/Fractal_Tree.md +++ b/tutorials/FractalTree.md @@ -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 -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 -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 -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 -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 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 +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 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 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 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 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(); ``` diff --git a/version.scad b/version.scad index fb47a1b..86c63a9 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,218]; +BOSL_VERSION = [2,0,219]; // Section: BOSL Library Version Functions