Skip to content

Instantly share code, notes, and snippets.

@runsun
Created September 27, 2015 07:18
Show Gist options
  • Save runsun/995250a8002386ab9abc to your computer and use it in GitHub Desktop.
Save runsun/995250a8002386ab9abc to your computer and use it in GitHub Desktop.
Run an openscad file once or n times and save shape output to file(s)
##########################################################################
## runscad.py
##
## Run an openscad file once or n times and save shape output to file(s)
##
## by Runsun Pan (2015.9.25)
###########################################3##############################
import sys, os, re
narg = len(sys.argv)
if narg<3:
print '''
runscad.py
>>> python runscad_animate.py <scad_file> <output_file> <opt> <opt> ...
For example:
>>> python runscad.py tmp.scad out.png n=3 D=t=0.2*(%(n)s-1);run()
outfile:
Setting outfile to xxx.png will save the shape to xxx.png. But if
n (number of runs)>0, it will run n times and save to
xxx00001.png, xxx00002.png, xxx00003.png ...
This is done by converting "." to "00001.", so the outfile name
must have a ".".
opt:
Options: All name/value pairs are in 'name=value' format or simply 'name'
if it's a flag (for example, version). Pairs are separated by spaces:
imgsize=100,200 version D="..." dryrun
opt has 2 parts:
(1) Original openscad command line options:
d=deps_file
m=make_command
D=a=2;b=3;run();
version
info
camera=translatex,y,z,rotx,y,z,dist | camera=eyex,y,z,centerx,y,z
imgsize=width,height
projection=(o)rtho|(p)ersp]
render | preview[=throwntogether]
csglimit=num
(2) runscad.py options:
dryrun:
Debug flag. When set, it will not call openscad but will output
parameters and settings for debugging.
>>> python runscad.py tmp.scad dump.png D="a=3;run();" dryrun
n: # of repeat run. To run it 3 times:
>>> python runscad.py tmp.scad dump.png D="a=3;run();" n=3
If n >0, we also get two internal vars:
i: index of run in each repeating run
t: i*1.0/n. It is between 0~1. This can be used to run animation.
n,i,t can be sent to your scad file by giving them in the D option:
D="index=%(i)s;time_fraction=%(t)s;nRun=%(n)s"
>>> python runscad.py tmp.scad dump.png D="n=%(n)s" n=3
Dquote: either "'" or '"'
The D to send statements to openscad and is used as:
python runscad.py tmp.scad out.scad D="a=3;"
wihch is converted to an openscad command-line:
openscad tmp.scad out.scad -D "a=3;"
But if you want to send string by attempting :
python runscad.py tmp.scad out.scad D="a=\"left\";"
which creates a syntex error:
openscad tmp.scad out.scad -D "a="left";"
To avoid this, set Dquote="'":
python runscad.py tmp.scad out.scad D="a=3;" Dquote="'"
which converts to the following command line:
openscad tmp.scad out.scad -D 'a="left";'
https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Using_OpenSCAD_in_a_command_line_environment
"
'''
else:
scadfile = sys.argv[1]
outfile = sys.argv[2]
print "\n* num of args = ", narg
p = re.compile("^([a-zA-Z]*)=(.*)") # match "o somefile.scad" or "camera=..."
print "\n* Extract opt:"
opt = [ p.match(x)!=None and p.match(x).groups() or (x,"") for x in sys.argv[3:]
]
print " opt = ", opt
print "* Convert opt to dict:"
opt = dict( opt )
print " opt = ", opt
if 'o' in opt:
print "* Remove the 'o' setting from opt --- which should be set in outfile=..."
del opt['o']
n = int(opt.get('n',0))
print "* n is found to be %s"%n
if 'n' in opt:
print "* Remove n from opt:"
del opt['n']
print " opt = ", opt
Dquote= opt.get('Dquote','"')
if 'Dquote' in opt:
print "* Remove Dquote from opt:"
del opt['Dquote']
print " opt = ", opt
dryrun= False
if 'dryrun' in opt:
dryrun=True
print "* dryrun is found to be true. Remove Dquote from opt:"
del opt['dryrun']
print " opt = ", opt
print "* Convert python dict to openscad command line:"
opt = [ k in ("o","d","m","D")
and "-"+k+ " "+ (k=="D" and Dquote or '')+ opt[k]+ (k=="D" and Dquote or '')
or "--"+k + (opt[k] and ("="+opt[k]) or "")
for k in opt ]
print " opt = ", opt
opt = " ".join( opt )
print " opt = ", opt
if dryrun:
print "\nStart dryrun --- openscad is not executed"
for i in range(n):
print "\nn=", n, ", i=", i
t = i*1.0/n
_outfile = n and outfile.replace(".", str(i+1).rjust(5,'0')+".") or outfile
optvars = {"i":i,"n":n,"t":t,"outfile":_outfile}
print "* vars available to -D: ", optvars
params={ "scadfile": scadfile
, "outfile" : _outfile
, "opt": opt%optvars
}
cmd = 'openscad %(scadfile)s -o %(outfile)s %(opt)s '%params
if not dryrun:
os.system(cmd)
print "* cmd=", cmd
##############################
@runsun
Copy link
Author

runsun commented Sep 27, 2015

An actual use case in animation gif generation :

The scad code handling 3d animation:

module animateRot3d( t=$t, loc=[0,0,0] ){

        tt = 540*t;
        rot = t< 1/3 ? [ tt,0,0 ]
              : t<2/3 ? [ 180, tt-180,0 ]
              : [180,180,tt];

        translate(loc)
        rotate(rot)    
        translate( -1*loc) children();
}

Your scad code: animate_gif.scad

$fn=100;
module run(t=$t){

    echo("t = ", t);  

    // Add timer
    color("black")
    linear_extrude(0.1)
        translate( [0,20,0])
        text(str("t=", floor(t*100)/100), size=2, valign="center",halign="center");

    animateRot3d(t)
    translate( [0,0,-1] )
    linear_extrude(2, scale=0.95)
    text("Scadx", valign="center",halign="center");
}   

Use animation parameters in your scad:

3 animation parameters, i (index), n (# of frames) and t (time fraction), can be sent from commandline to your scad using the python-style string substitution format:

$ python ... D="i=%(i)s;n=%(n)s;run(t=%(t)s);" ...

and then in your scad file:

i=0; n=0; t=0;
module run(t=t){
      ...
      label= str( "frame:", i, "/", n, ", t:", t);
      ...
}

Do a dryrun to check parameters with n set to just 3:

$ python ../../../runscad.py ./animate_gif.scad scadx_animate.png n=3 D="run(t=%(t)s);" camera=0,0,0,0,0,0,140 dryrun

Output of dryrun to the shell:

* num of args = 9

* Extract opt:
opt = [('n', '3'), ('D', 'run(t=%(t)s);'), ('Dquote', "'"), ('camera', '0,0,0,0,0,0,140'), ('imgsize', '250,250'), ('dryrun', '')]

  • Convert opt to dict:
    opt = {'dryrun': '', 'imgsize': '250,250', 'Dquote': "'", 'n': '3', 'camera': '0,0,0,0,0,0,140', 'D': 'run(t=%(t)s);'}
  • n is found to be 3
  • Remove n from opt:
    opt = {'dryrun': '', 'imgsize': '250,250', 'Dquote': "'", 'camera': '0,0,0,0,0,0,140', 'D': 'run(t=%(t)s);'}
  • Remove Dquote from opt:
    opt = {'dryrun': '', 'imgsize': '250,250', 'camera': '0,0,0,0,0,0,140', 'D': 'run(t=%(t)s);'}
  • dryrun is found to be true. Remove Dquote from opt:
    opt = {'imgsize': '250,250', 'camera': '0,0,0,0,0,0,140', 'D': 'run(t=%(t)s);'}
  • Convert python dict to openscad command line:
    opt = ['--imgsize=250,250', '--camera=0,0,0,0,0,0,140', "-D 'run(t=%(t)s);'"]
    opt = --imgsize=250,250 --camera=0,0,0,0,0,0,140 -D 'run(t=%(t)s);'

Start dryrun --- openscad is not executed

n= 3 , i= 0

  • vars available to -D: {'i': 0, 'outfile': 'scadx_animate00001.png', 't': 0.0, 'n': 3}
  • cmd= openscad ./animate_gif.scad -o scadx_animate00001.png --imgsize=250,250 --camera=0,0,0,0,0,0,140 -D 'run(t=0.0);'

n= 3 , i= 1

  • vars available to -D: {'i': 1, 'outfile': 'scadx_animate00002.png', 't': 0.3333333333333333, 'n': 3}
  • cmd= openscad ./animate_gif.scad -o scadx_animate00002.png --imgsize=250,250 --camera=0,0,0,0,0,0,140 -D 'run(t=0.333333333333);'

n= 3 , i= 2

  • vars available to -D: {'i': 2, 'outfile': 'scadx_animate00003.png', 't': 0.6666666666666666, 'n': 3}
  • cmd= openscad ./animate_gif.scad -o scadx_animate00003.png --imgsize=250,250 --camera=0,0,0,0,0,0,140 -D 'run(t=0.666666666667);'

Generate 3 png files for a test run:

Before we go into full production, which could take long depending on your scad, we do a 3-file test run:

$ python ../../../runscad.py ./animate_gif.scad scadx_animate.png n=3 D="run(t=%(t)s);" camera=0,0,0,0,0,0,140

Check the generated png files to see if they look ok. Adjust parameters properly (in your scad file and in the parameters given to runscad.py). A good parameter to try is imgsize=400,400 for runscad.py.

Actual execution to generate 120 png files:

$ python ../../../runscad.py ./animate_gif.scad scadx_animate.png n=120 D="run(t=%(t)s);" camera=0,0,0,0,0,0,140

Convert pngs to animating gif:

$ convert -delay 10 -loop 0 scadx_animate*.png -crop 378x426+64+32 scadx_animate_test_crop.gif

The command convert is part of ImageMagick. Try out different values for delay and crop. For crop:

$ convert ... crop WidthxHeight+Starting_x+Starting_y

where the starting 0 is on the top-left corner.

For delay: a combination of n=120 and delay=10 gives a smooth ride like what's shown blow.

The gif:
scadx_animate_test_crop

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment