#!/bin/bash # trace2scad # 2015 by Hank Dietz, http://aggregate.org/hankd/ # This script is distributed as full source subject # to the Creative Commons - Attribution constraints. # You use it at your own risk, with no warranty, etc. version=20150415 complex=4096 filter=2 gamma=1 pixerr=1.0 xres=2000 yres=2000 level=0.48 levels=$level negate="" turd=5 subres=5 align=0 verbose="" prefix="no_prefix_set" ppm='/tmp/trace2scad'$$'.ppm' pgm='/tmp/trace2scad'$$'.pgm' svg='/tmp/trace2scad'$$'.svg' raw='/tmp/trace2scad'$$'.raw' stmp='/tmp/trace2scad'$$'.scad' scad="" myname="$0" if [[ $# < 1 ]] then echo "Error: type $myname -h for usage" 1>&2 exit 1 fi while [[ $# > 0 ]] do key="$1" shift case $key in -a|--align) align=1 ;; -c|--complex) complex="$1" shift ;; -e|--pixerr) pixerr="$1" shift ;; -f|--filter) filter="$1" shift ;; -g|--gamma) gamma="$1" shift ;; -x|--xres) xres="$1" shift ;; -y|--yres) yres="$1" shift ;; -s|--subres) subres="$1" shift ;; -t|--turd) turd="$1" shift ;; -o) scad="$1" shift ;; -n) negate="-negate" ;; -p|--prefix) prefix="${1%.*}" # allow use of filename, but strip .ext shift ;; -l|--level|--levels) levels="$1" # if integer number of levels was given, # convert that to a list of thresholds # evenly spaced between 1 and 0 if [[ ${levels#*.} = $levels ]] then levels=`awk -v levels=$levels ' BEGIN { s = (levels-0.5)/levels for (i=levels-1; i>0; --i) { s = s "," ((i-0.5)/levels) } printf s } ' &2 ;; -h|--help) echo "Usage: $myname {options} inputimagefile" 1>&2 echo "-a force alignment based on image (not object bounds)" 1>&2 echo "-c # set target max complexity (default $complex)" 1>&2 echo "-e # set max line-combining error in pixels (default $pixerr)" 1>&2 echo "-f # set highpass filter radius (default $filter)" 1>&2 echo "-g # set gamma (default $gamma)" 1>&2 echo "-h show this help message" 1>&2 echo "-l levs set level threshold, thresholds, or number of layers (default $levels)" 1>&2 echo "-n negate the image" 1>&2 echo "-o file set OpenSCAD output filename to file (default from input filename)" 1>&2 echo "-p pre set prefix for OpenSCAD module names (default from input filename)" 1>&2 echo "-s # set subpixel potrace resolution (default $subres)" 1>&2 echo "-t # set potrace turd size (default $turd)" 1>&2 echo "-v enable verbose output, keep temp files" 1>&2 echo "-x # set X resolution (default $xres)" 1>&2 echo "-y # set Y resolution (default $yres)" 1>&2 exit ;; *) if [ -n "$infile" ] then echo "Error: can only have one input filename; type $myname -h for usage" 1>&2 exit 1 fi infile="$key" if [ $prefix = "no_prefix_set" ] then prefix="${key%.*}" # strip .ext prefix="${prefix##*/}" # strip leading path prefix=`echo $prefix |tr '-' '_' |tr -cd '[_a-zA-Z0-9]'` fi if [ -z "$scad" ] then scad="${key%.*}.scad" # strip .ext, add .scad fi shift ;; esac done if [ -z "$infile" ] then echo "Error: no input filename given; type $myname -h for usage" 1>&2 exit 1 fi # remember reference image size refxres=$xres refyres=$yres # empty the output file >$scad # process multiple-level stuff curlev=0 if [ -n "${levels#*,}" ] then align=1 # force center based on image, not object bounds fi while [ -n "$levels" ] do level="${levels%%,*}" # extract a level levels="${levels#$level}" # remove it from list levels="${levels#,}" # remove comma after it curlev=$(( $curlev + 1 )) # one more level if [[ $align > 0 ]] then echo "Creating layer $curlev with level $level ..." fi # loop trying to meet complexity constraint xres=$refxres yres=$refyres echo -n "Adjusting complexity:" while true; do echo -n " $xres"'x'"$yres" if [ "$verbose" = "true" ] then echo "" echo "trace2scad version $version" 1>&2 if [[ $align > 0 ]] then echo "--align" 1>&2 fi echo "--complex $complex" 1>&2 echo "--filter $filter" 1>&2 echo "--gamma $gamma" 1>&2 echo "--level $level" 1>&2 if [[ -n $negate ]] then echo "--negate" 1>&2 fi echo "--prefix $prefix" 1>&2 echo "--subres $subres" 1>&2 echo "--turd $turd" 1>&2 echo "--xres $xres" 1>&2 echo "--yres $yres" 1>&2 echo "input filename is $infile" 1>&2 echo "prefix is $prefix" 1>&2 echo "OpenSCAD output filename is $scad" 1>&2 echo "Converting image to" $pgm 1>&2 fi convert $negate -gravity center -background white -gamma $gamma -adaptive-resize $xres'x'$yres $infile $ppm if [ "$filter" != 0 ] then mkbitmap -f $filter -s 2 -t $level <$ppm >$pgm else mkbitmap -n -s 2 -t $level <$ppm >$pgm fi if [ "$verbose" = "true" ] then echo "Running potrace to make" $svg 1>&2 fi # note potrace version dependence: # potrace 1.11 says -a 0 to disable smoothing # potrace 1.10 says -a -1 (or any negative) to disable smoothing # -a -1 seem to work on both versions, so we just do that... potrace --opaque -a -1 -u $subres --group -sct $turd <$pgm >$svg # compute max possible bounding box... # based on the actual PGM file dimensions xdim=$(( `convert $pgm -print "%w" /dev/null` * $subres )) ydim=$(( `convert $pgm -print "%h" /dev/null` * $subres )) if [ "$verbose" = "true" ] then echo "Running sed to make" $raw 1>&2 fi sed '1,/\/metadata/d s// s//begin/ s/<\/g>/end/ s/ /\ /g /svg/d' <$svg >$raw if [ "$verbose" = "true" ] then echo "Running awk to make" $scad 1>&2 fi awk -v align=$align -v xdim=$xdim -v ydim=$ydim -v curlev=$curlev -v version=$version -v prefix=$prefix -v level=$level -v complex=$complex -v pixerr=$pixerr ' function r(x) { return(0 + x) } function indent(x) { for (xi=0; xi= pixerr) { # too far away return(1) } } return(0) } return(1) } BEGIN { objno = 0 lev = 1 big = 10000000 minx = big miny = big maxx = -big maxy = -big nsp = 1 byebye = 0 shadow = 0 } /begin/ { ++lev nrec[nsp] = "{" ++nsp next } /end/ { --lev if ((nrec[nsp-1] == "+") && (nrec[nsp-2] == "{")) { # optimize out {x} becomes x nrec[nsp-2] = nrec[nsp-1] --nsp } else { nrec[nsp] = "}" ++nsp } next } /add/ { ++objno typ[objno] = lev nrec[nsp] = "+" ++nsp pts = 0 curx = 0 cury = 0 ncur = "x" next } /sub/ { ++objno typ[objno] = -lev nrec[nsp] = "-" ++nsp pts = 0 curx = 0 cury = 0 ncur = "x" next } /close/ { # build string for polygon # compute approximate convexity as number of turns dx=0 dy=0 bufpts[objno] = pts buf[objno] = sprintf("polygon([[%d,%d]", curx, cury) convexity=2 for (i=2; i<=pts; ++i) { buf[objno] = buf[objno] sprintf(",[%d,%d]", xpt[i], ypt[i]) ndx=(xpt[i-1]>xpt[i]) ndy=(ypt[i-1]>ypt[i]) if ((dx != ndx) || (dy != ndy)) ++convexity dx=ndx dy=ndy } buf[objno] = buf[objno] sprintf("], convexity=%d);", convexity) ncur = "" next } /[-0-9][0-9]*/ { if (ncur == "x") { prevx = curx curx += r($1) ncur = "y" } else if (ncur == "y") { prevy = cury cury += r($1) ncur = "x" if (pts == 0) { firstx = curx firsty = cury ++pts xpt[pts] = curx ypt[pts] = cury ++allpts } else if (((prevx != curx) || (prevy != cury)) && ((firstx != curx) || (firsty != cury))) { if ((pts > 2) && (needed(curx, cury, xpt[pts], ypt[pts], xpt[pts-1], ypt[pts-1]) < 1)) { # get rid of previous point shadx[shadow] = xpt[pts] shady[shadow] = ypt[pts] shadow = shadow + 1 ++byebye --pts --allpts } else { # no points shadowed now shadow = 0 } ++pts xpt[pts] = curx ypt[pts] = cury ++allpts # update bounding box info if (minx > curx) minx = curx if (miny > cury) miny = cury if (maxx < curx) maxx = curx if (maxy < cury) maxy = cury } else { ++byebye } # abort with no output if complexity exceeded if (allpts > (0+complex)) { exit } } else { # this should never happen print "Error parsing SVG generated by potrace." } next } /^/ { next } END { if (allpts < 2) { # dummy output if no points print "module " prefix "_" curlev "()" print "{" print " /* Generated by trace2scad version " version print " http://aggregate.org/MAKE/TRACE2SCAD/" print " There ws no model content with level " level print " */" print "}" print "" } else if (allpts < (0+complex)) { # no output if complexity exceeded print "module " prefix "_" curlev "()" print "{" print " /* Generated by trace2scad version " version print " http://aggregate.org/MAKE/TRACE2SCAD/" print " Optimized model has " allpts "/" (allpts+byebye) " original points" print " */" print " color([" level ", " level ", " level "])" if (align > 0) { print " assign(minx=0) /* polygon minx=" minx " */" print " assign(miny=0) /* polygon miny=" miny " */" print " assign(maxx=" xdim ") /* polygon maxx=" maxx " */" print " assign(maxy=" ydim ") /* polygon maxy=" maxy " */" } else { print " assign(minx=" minx ")" print " assign(miny=" miny ")" print " assign(maxx=" maxx ")" print " assign(maxy=" maxy ")" } print " assign(dx=maxx-minx)" print " assign(dy=maxy-miny)" print " assign(maxd=((dx>dy)?dx:dy))" print " scale([1/maxd, 1/maxd, 1])" print " translate([-minx-dx/2, -miny-dy/2, 0])" print sprintf(" linear_extrude(height=1, convexity=%d)", allpts) print " union() {" i = 1 j = 1 k = 2 while (j < nsp) { if (nrec[j] == "{") { indent(k) if (nrec[j+2] == "-") { print "difference() {" } else { print "union() {" } ++k } else if (nrec[j] == "}") { --k indent(k) print "}" } else { indent(k) print buf[i] ++i } ++j } print " }" print "}" print "" } } ' <$raw >$stmp if [ -s "$stmp" ] then echo "" break fi # reduce resolution to simplify xres=`expr $xres '*' 9 '/' 10` yres=`expr $yres '*' 9 '/' 10` if [ "$verbose" = "true" ] then echo -n "Test complexity:" fi done # add this layer to output file cat $stmp >>$scad done # stick end on it all awk -v curlev=$curlev -v version=$version -v prefix=$prefix -v complex=$complex ' BEGIN { print "module " prefix "()" print "{" print " /* all layers combined, scaled to within a 1mm cube */" print " scale([1, 1, 1/" curlev "])" print " difference() {" print " union() {" for (i=0; i>$scad rm -f "$stmp" if [ "$verbose" = "true" ] then echo "Leaving tmp files:" $ppm $pgm $svg $raw 1>&2 else rm -f $ppm $pgm $svg $raw fi