1//////////////////////////////////////////////////////////////////////
  2// LibFile: turtle3d.scad
  3//   Three dimensional turtle graphics to generate 3d paths or sequences
  4//   of 3d transformations. 
  5// Includes:
  6//   include <BOSL2/std.scad>
  7//   include <BOSL2/turtle3d.scad>
  8// FileGroup: Advanced Modeling
  9// FileSummary: 3D turtle graphics for making paths or lists of transformations.
 10//////////////////////////////////////////////////////////////////////
 11include<structs.scad>
 12
 13// Section: Functions
 14
 15// Translation vector from a matrix
 16function _transpart(T) = [for(row=[0:2]) T[row][3]];
 17
 18// The non-translation part of a matrix
 19function _rotpart(T) = [for(i=[0:3]) [for(j=[0:3]) j<3 || i==3 ? T[i][j] : 0]];
 20
 21
 22// Function: turtle3d()
 23// Usage:
 24//   turtle3d(commands, [state], [transforms], [full_state], [repeat])
 25// Description:
 26//   Like the classic two dimensional turtle, the 3d turtle flies through space following a sequence
 27//   of turtle graphics commands to generate either a sequence of transformations (suitable for input
 28//   to sweep) or a 3d path.  The turtle state keeps track of the position and orientation (including twist)
 29//   and scale of the turtle.  By default the turtle begins pointing along the X axis with the "right" direction
 30//   along the -Y axis and the "up" direction aligned with the Z axis.  You can give a direction vector
 31//   for the state input to change the starting direction.  Because of the complexity of object positioning
 32//   in three space, some types of movement require compound commands.  These compound commands are lists that specify several operations
 33//   all applied to one turtle step.  For example:  ["move", 4, "twist", 25] executes a twist while moving, and
 34//   the command ["arc", 4, "grow", 2, "right", 45, "up", 30] turns to the right and up while also growing the object.
 35//   .
 36//   You can turn the turtle using relative commands, "right", "left", "up" and "down", which operate relative
 37//   to the turtle's current orientation.   This is sometimes confusing, so you can also use absolute
 38//   commands which turn the turtle relative to the absolute coordinate system, the "xrot", "yrot" and "zrot"
 39//   commands.  You can use "setdir" to point the turtle along a given vector.
 40//   If you want a valid transformation list for use with sweep you will usually want to avoid abrupt changes
 41//   in the orientation of the turtle.  To do this, use the "arc"
 42//   forms for turns.  This form, with commands like "arcright" and "arcup" creates an arc with a gradual
 43//   change in the turtle orientation, which usually produces a better result for sweep operations.
 44//   .
 45//   Another potential problem for sweep is a command that makes movements not relative to the turtle's current direction such as
 46//   "jump" or "untily".  These commands are not a problem for tracing out a path, but if you want a swept shape to
 47//   maintain a constant cross sectional shape then you need to avoid them.  operations and avoid the movement commands
 48//   which do not move relative to the turtle direction such as the "jump" commands.
 49//   .
 50//   If you use sweep to convert a turtle path into a 3d shape the result depends both on the path the shape traces out but also
 51//   the twist and size of the shape.  The "twist" parameter described below to the compound commands has no effect on
 52//   the turtle orientation for the purpose of defining movement, but it will rotate the swept shape around the origin
 53//   as it traces out the path.  Similarly the "grow" and "shrink" options allow you to change the size of the swept
 54//   polygon without any effect on the turtle.  The "roll" command differs from "twist" in that it both rotates the swept
 55//   polygon but also changes the turtle's orientation, so it will alter subsequent operations of the turtle.  Note that
 56//   when making a path, "twist" will have no effect, but "roll" may have an effect because of how it changes the path.  
 57//   .
 58//   The compound "move" command accepts a "reverse" argument.  If you specify "reverse" it reflects the
 59//   turtle direction to point backwards.  This enables you to back out to create a hollow shape.  But be
 60//   aware that everything is reversed, so turns will be the opposite direction.  So for example if you
 61//   used "arcright" on the outside you might expect arcleft when reversed on the inside, but it will
 62//   be "arcright" again.  (Note that "reverse" is the only command that appears by itself with no argument
 63//   .
 64//   By default you get a simple path (like the 2d turtle) which ignores growing/shrinking or twisting in the
 65//   transformation.  If you select transform=true then you will get a list of transformations returned.  Some of
 66//   of the commands are likely to produce transformation lists that are invalid for sweep.  The "jump" commands
 67//   can move in directions not perpendicular to the current direction of movement, which may produce bad results.
 68//   The turning commands like "left" or "up" can rotate the frame so that a sweep operation is invalid.
 69//   The `T` column in the list below marks commands that operate relative
 70//   to the current frame that should generally produce valid sweep transformations.
 71//   Be aware that it is possible to create a self intersection, and hence an invalid swept shape, if the radii of
 72//   arcs in turtle are smaller than the width of the polygon you use with sweep.  
 73//   .
 74//   The turtle state is a list containing:
 75//     - a list of path transformations, the transformations that move the turtle along the path
 76//     - a list of object transformations, the transformations that twist or scale the cross section as the turtle moves
 77//     - the current movement step size (scalar)
 78//     - the current default angle
 79//     - the current default arcsteps  
 80//   .
 81//   Commands   |T | Arguments          | What it does
 82//   ---------- |--| ------------------ | -------------------------------
 83//   "move"     |x | [dist]             | Move turtle scale*dist units in the turtle direction.  Default dist=1.  
 84//   "xmove"    |  | [dist]             | Move turtle scale*dist units in the x direction. Default dist=1.  Does not change turtle direction.
 85//   "ymove"    |  | [dist]             | Move turtle scale*dist units in the y direction. Default dist=1.  Does not change turtle direction.
 86//   "zmove"    |  | [dist]             | Move turtle scale*dist units in the y direction. Default dist=1.  Does not change turtle direction.
 87//   "xyzmove"  |  | vector             | Move turtle by the specified vector.  Does not change turtle direction. 
 88//   "untilx"   |x | xtarget            | Move turtle in turtle direction until x==xtarget.  Produces an error if xtarget is not reachable.
 89//   "untily"   |x | ytarget            | Move turtle in turtle direction until y==ytarget.  Produces an error if ytarget is not reachable.
 90//   "untilz"   |x | ytarget            | Move turtle in turtle direction until y==ytarget.  Produces an error if ztarget is not reachable.
 91//   "jump"     |  | point              | Move the turtle to the specified point
 92//   "xjump"    |  | x                  | Move the turtle's x position to the specified value
 93//   "yjump     |  | y                  | Move the turtle's y position to the specified value
 94//   "zjump     |  | y                  | Move the turtle's y position to the specified value
 95//   "left"     |  | [angle]            | Turn turtle left by specified angle or default angle
 96//   "right"    |  | [angle]            | Turn turtle to the right by specified angle or default angle
 97//   "up"       |  | [angle]            | Turn turtle up by specified angle or default angle
 98//   "down"     |  | [angle]            | Turn turtle down by specified angle or default angle
 99//   "xrot"     |x | [angle]            | Turn turtle around x-axis by specified angle or default angle
100//   "yrot"     |x | [angle]            | Turn turtle around y-axis by specified angle or default angle
101//   "zrot"     |x | [angle]            | Turn turtle around z-axis by specified angle or default angle
102//   "rot"      |x | rotation           | Turn turtle by specified rotation relative to absolute coordinates
103//   "angle"    |x | angle              | Set the default turn angle.
104//   "setdir"   |  | vector             | Rotate the reference frame along the shortest path to specified direction
105//   "length"   |x | length             | Change the turtle move distance to `length`
106//   "scale"    |x | factor             | Multiply turtle move distances by `factor`.  Does not rescale the cross sectional shape in transformation lists.  
107//   "addlength"|x | length             | Add `length` to the turtle move distance
108//   "repeat"   |x | count, commands    | Repeats a list of commands `count` times.  (To repeat a compound command put it in a list: `[["move",10,"grow",2]]`)
109//   "arcleft"  |x | radius, [angle]    | Draw an arc from the current position toward the left at the specified radius and angle.  The turtle turns by `angle`.
110//   "arcright" |x | radius, [angle]    | Draw an arc from the current position upward at the specified radius and angle
111//   "arcup"    |x | radius, [angle]    | Draw an arc from the current position down at the specified radius and angle
112//   "arcdown"  |x | radius, [angle]    | Draw an arc from the current position down at the specified radius and angle
113//   "arcxrot"  |x | radius, [angle]    | Draw an arc turning around x-axis by specified angle or default angle
114//   "arcyrot"  |x | radius, [angle]    | Draw an arc turning around y-axis by specified angle or default angle
115//   "arczrot"  |x | radius, [angle]    | Draw an arc turning around z-axis by specified angle or default angle
116//   "arcrot"   |x | radius, rotation   | Draw an arc turning by the specified absolute rotation with given radius
117//   "arctodir" |x | radius, vector     | Draw an arc turning to point in the (absolute) direction of given vector
118//   "arcsteps" |x | count              | Specifies the number of segments to use for drawing arcs.  If you set it to zero then the standard `$fn`, `$fa` and `$fs` variables define the number of segments.
119//   .
120//   Compound commands are lists that group multiple commands to be applied simultaneously during a
121//   turtle movement.  Example: `["move", 5, "shrink", 2]`.  The subcommands that may appear are
122//   listed below.  Each compound command must begin with either "move" or "arc".  The order of
123//   subcommands is not important.  Left/right turning is applied before up/down.  You cannot combine
124//   "rot" or "todir" with any other turning commands.  
125//   .
126//   Subcommands  | Arguments          | What it does
127//   ------------ | ------------------ | -------------------------------
128//   "move"       | dist               | Compound command is a forward movement operation
129//   "arc"        | radius             | Compound command traces an arc
130//   "grow"       | factor             | Increase size by specified factor (e.g. 2 doubles the size); factor can be a 2-vector
131//   "shrink"     | factor             | Decrease size by specified factor (e.g. 2 halves the size); factor can be a 2-vector
132//   "twist"      | angle              | Twist by the specified angle over the arc or segment (does not change frame orientation)
133//   "roll"       | angle              | Roll by the specified angle over the arc or segment (changes the orientation of the frame)
134//   "steps"      | count              | Divide arc or segment into this many steps.  Default is 1 for segments, arcsteps for arcs
135//   "reverse"    |                    | For "move" only: If given then reverses the turtle after the move
136//   "right"      | angle              | For "arc" only: Turn to the right by specified angle
137//   "left"       | angle              | For "arc" only: Turn to the left by specified angle
138//   "up"         | angle              | For "arc" only: Turn up by specified angle
139//   "down"       | angle              | For "arc" only: Turn down by specified angle
140//   "xrot"       | angle              | For "arc" only: Absolute rotation around x axis. Cannot be combined with any other rotation.
141//   "yrot"       | angle              | For "arc" only: Absolute rotation around y axis. Cannot be combined with any other rotation.
142//   "zrot"       | angle              | For "arc" only: Absolute rotation around z axis. Cannot be combined with any other rotation.
143//   "rot"        | rotation           | For "arc" only: Turn by specified absolute rotation as a matrix, e.g. xrot(33)*zrot(47).  Cannot be combined with any other rotation.
144//   "todir"      | vector             | For "arc" only: Turn to point in the specified direction
145//   .
146//   The "twist", "shrink" and "grow" subcommands will only have an effect if you return a transformation list.  They do not
147//   change the path the turtle traces.  The "roll" subcommand, on the other hand, changes the turtle frame orientation, so it can alter the path.
148//   The "xrot", "yrot" and "zrot" subcommands can make turns larger than 180 degrees, and even larger than 360 degrees.  If you use "up",
149//   "down", "left" or "right" alone then you can give any angle, but if you combine "up"/"down" with "left"/"right" then the specified
150//   angles must be smaller than 180 degrees.  (This is because the algorithm decodes the rotation into an angle smaller than 180, so
151//   the results are very strange if larger angles are permitted.)
152// Arguments:
153//   commands = List of turtle3d commands
154//   state = Starting turtle direction or full turtle state (from a previous call).  Default: RIGHT
155//   transforms = If true teturn list of transformations instead of points.  Default: false
156//   full_state = If true return full turtle state for continuing the path in subsequent turtle calls.  Default: false
157//   repeat = Number of times to repeat the command list.  Default: 1
158// Example(3D): Angled rectangle
159//   path = turtle3d(["up",25,"move","left","move",3,"left","move"]);
160//   stroke(path,closed=true, width=.2);
161// Example(3D): Path with rounded corners.  Note first and last point of the path are duplicates.  
162//   r = 0.25;
163//   path = turtle3d(["up",25,"move","arcleft",r,"move",3,"arcleft",r,"move","arcleft",r,"move",3,"arcleft",r]);
164//   stroke(path,closed=true, width=.2);
165// Example(3D): Non-coplanar figure
166//   path = turtle3d(["up",25,"move","left","move",3,"up","left",0,"move"]);
167//   stroke(path,closed=true, width=.2);
168// Example(3D): Square spiral.  Note that the core twists because the "up" and "left" turns are relative to the previous turns.
169//   include<BOSL2/skin.scad>
170//   path = turtle3d(["move",10,"left","up",15],repeat=50);
171//   path_sweep(circle(d=1, $fn=12), path);
172// Example(3D): Square spiral, second try.  Use roll to create the spiral instead of turning up.  It still twists because the left turns are inclined.
173//   include<BOSL2/skin.scad>
174//   path = turtle3d(["move",10,"left","roll",10],repeat=50);
175//   path_sweep(circle(d=1, $fn=12), path);
176// Example(3D): Square spiral, third try.  One way to avoid the core twisting in the spiral is to use absolute turns.  Note that the vertical rise is controlled by the starting upward angle of the turtle, which is preserved as we rotate around the z axis.  
177//   include<BOSL2/skin.scad>
178//   path = turtle3d(["up", 5, "repeat", 12, ["move",10,"zrot"]]);
179//   path_sweep(circle(d=1, $fn=12), path);
180// Example(3D): Square spiral, rounded corners.  Careful use of rotations can work for sweep, but it may be better to round the corners.  Here we return a list of transforms and use sweep instead of path_sweep:
181//   include<BOSL2/skin.scad>
182//   path = turtle3d(["up", 5, "repeat", 12, ["move",10,"arczrot",4]],transforms=true);
183//   sweep(circle(d=1, $fn=12), path);
184// Example(3D): Mixing relative and absolute commands
185//   include<BOSL2/skin.scad>
186//   path = turtle3d(["repeat", 4, ["move",80,"arczrot",40],
187//                    "arcyrot",40,-90,
188//                    "move",40,
189//                    "arcxrot",40,90,
190//                    ["arc",14,"rot",xrot(90)*zrot(-33)],
191//                    "move",80,
192//                    "arcyrot",40,
193//                    "arcup",40,
194//                    "arcleft",40,
195//                    "arcup",30,
196//                    ["move",100,"twist",90,"steps",20],
197//                   ],
198//                   state=[1,0,.2],transforms=true);
199//   ushape = rot(90,p=[[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]]);
200//   sweep(ushape, path);
201// Example(3D): Generic helix, constructed by a sequence of movements and then rotations
202//   include<BOSL2/skin.scad>
203//   radius=14;       // Helix radius
204//   pitch=20;        // Distance from one turn to the next
205//   turns=3;         // Number of turns
206//   turn_steps=32;   // Number of steps on each turn
207//   axis = [1,4,1];  // Helix axis
208//   up_angle = atan2(pitch,2*PI*radius);
209//   helix = turtle3d([
210//                      "up", up_angle,
211//                      "zrot", 360/turn_steps/2,
212//                      "rot", rot(from=UP,to=axis), // to correct the turtle direction
213//                      "repeat", turn_steps*turns,
214//                      [
215//                       "move", norm([2*PI*radius, pitch])/turn_steps,
216//                       "rot",  rot(360/turn_steps,v=axis)
217//                      ],
218//                     ], transforms=true);
219//   sweep(subdivide_path(square([5,1]),20), helix);
220// Example(3D): Helix generated by a single command.  Note this only works for x, y, or z aligned helixes because the generic rot cannot handle multi-turn angles.  
221//   include<BOSL2/skin.scad>
222//   pitch=20;       // Distance from one turn to the next
223//   radius=14;      // Helix radius
224//   turns=3;        // Number of turns
225//   turn_steps=33;  // Steps on each turn
226//   up_angle = atan2(pitch,2*PI*radius);
227//   helix = turtle3d([
228//                     "up", up_angle,
229//                     [
230//                       "arc", radius,
231//                       "zrot", 360*turns,
232//                       "steps", turn_steps*turns,
233//                     ]
234//                    ], transforms=true);
235//   sweep(subdivide_path(square([5,1]),80), helix);
236// Example(3D): Expanding helix
237//   include<BOSL2/skin.scad>
238//   path = turtle3d(["length",.2,"angle",360/20,"up",5,"repeat",50,["move","zrot","addlength",0.05]]);
239//   path_sweep(circle(d=1, $fn=12), path);
240// Example(3D): Adding some twist to the model
241//   include<BOSL2/skin.scad>
242//   r = 2.5;
243//   trans = turtle3d(["move",10,
244//                     "arcleft",r,
245//                     ["move",30,"twist",180,"steps",40],
246//                     "arcleft",r,
247//                     "move",10,
248//                     "arcleft",r,
249//                     ["move",30,"twist",360,"steps",40],
250//                     "arcleft",r],
251//                    state=yrot(25,p=RIGHT),transforms=true);
252//   sweep(supershape(m1=4,n1=4,n2=16,n3=1.5,a=.9,b=9,step=5),trans);
253// Example(3D): Twist does not change the turtle orientation, but roll does.  The only change from the previous example is twist was changed to roll.
254//   include<BOSL2/skin.scad>
255//   r = 2;
256//   trans = turtle3d(["move",10,
257//                     "arcleft",r,
258//                     ["move",30,"roll",180,"steps",40],
259//                     "arcleft",r,
260//                     "move",10,
261//                     "arcleft",r,
262//                     ["move",30,"roll",360,"steps",40],
263//                     "arcleft",r],
264//                    state=yrot(25,p=RIGHT),transforms=true);
265//   sweep(supershape(m1=4,n1=4,n2=16,n3=1.5,a=.9,b=9,step=5),trans);
266// Example(3D): Use of shrink and grow
267//   include<BOSL2/skin.scad>
268//   $fn=32;
269//   T = turtle3d([
270//                 "move",10,
271//                 ["arc",8,"right", 90, "twist", 90, "grow", 2],
272//                 ["move", 5,"shrink",4,"steps",4],
273//                 ["arc",8, "right", 45, "up", 90],
274//                 "move", 10,
275//                 "arcright", 5, 90,
276//                 "arcleft", 5, 90,
277//                 "arcup", 5, 90,
278//                 "untily", -1,
279//                ],state=RIGHT, transforms=true);   
280//   sweep(square(2,center=true),T);
281// Example(3D): After several moves you may not understand the turtle orientation. An absolute reorientation with "arctodir" is helpful to head in a known direction
282//   include<BOSL2/skin.scad>
283//   trans = turtle3d([
284//                  "move",5,
285//                  "arcup",1,
286//                  "move",8,
287//                  "arcright",1,
288//                  "move",6,
289//                  "arcdown",1,
290//                  "move",4,
291//                  ["arc",2,"right",45,"up",25,"roll",25],
292//                  "untilz",4,
293//                  "move",1,
294//                  "arctodir",1,DOWN,
295//                  "untilz",0
296//                  ],transforms=true);
297//   sweep(square(1,center=true),trans);
298// Example(3D): The "grow" and "shrink" commands can take a vector giving x and y scaling
299//   include<BOSL2/skin.scad>
300//   tr = turtle3d([
301//                   "move", 1.5, 
302//                   ["move", 5, "grow", [1,2],  "steps", 10],
303//                   ["move", 5, "grow", [2,0.5],"steps", 10]
304//                  ], transforms=true);
305//   sweep(circle($fn=32,r=1), tr);
306// Example(3D): With "twist" added the anisotropic "grow" interacts with "twist", producing a complex form
307//   include<BOSL2/skin.scad>
308//   tr = turtle3d([
309//                   "move", 1.5, 
310//                   ["move", 5, "grow", [1,2],  "steps", 20, "twist",90],
311//                   ["move", 5, "grow", [0.5,2],"steps", 20, "twist",90]
312//                  ], transforms=true);
313//   sweep(circle($fn=64,r=1), tr);
314// Example(3D): Making a tube with "reverse".  Note that the move direction is the same even though the direction is reversed.  
315//   include<BOSL2/skin.scad>
316//   tr = turtle3d([ "move", 4,
317//                   ["move",0, "grow", .8, "reverse"],
318//                   "move", 4
319//                 ],  transforms=true);
320//   back_half(s=10)
321//     sweep(circle(r=1,$fn=16), tr, closed=true);
322// Example(3D): To close the tube at one end we set closed to false in sweep.  
323//   include<BOSL2/skin.scad>
324//   tr = turtle3d([ "move", 4,
325//                   ["move",0, "grow", .8, "reverse"],
326//                   "move", 3.75
327//                 ],  transforms=true);
328//   back_half(s=10)
329//     sweep(circle(r=1,$fn=16), tr, closed=false);
330// Example(3D): Cookie cutter using "reverse" 
331//   include<BOSL2/skin.scad>
332//   cutter = turtle3d( [ 
333//                       ["move", 10, "shrink", 1.3, ],
334//                       ["move", 2, "reverse" ],
335//                       ["move", 8, "shrink", 1.3 ],
336//                      ], transforms=true,state=UP);
337//   cookie_shape = star(5, r=10, ir=5);
338//   sweep(cookie_shape, cutter, closed=true);
339// Example(3D): angled shopvac adapter.  Shopvac tubing wedges together because the tubes are slightly tapered.  We can make this part without using any difference() operations by using "reverse" to trace out the interior portion of the part.  Note that it's "arcright" even when reversed.  
340//   include<BOSL2/skin.scad>
341//   inch = 25.4;
342//   insert_ID = 2.3*inch;        // Size of shopvac tube at larger end of taper
343//   wall = 1.7;                  // Desired wall thickness
344//   seg1_bot_ID = insert_ID;     // Bottom section, to have tube inserted, specify ID
345//   seg2_bot_OD = insert_ID+.03; // Top section inserts into a tube, so specify tapered OD
346//   seg2_top_OD = 2.26*inch;     // The slightly oversized value gave me a better fit
347//   seg1_len = 3*inch;           // Length of bottom section
348//   seg2_len = 2*inch;           // Length of top section
349//   bend_angle=45;               // Angle to bend, 45 or less to print without supports!
350//   // Other diameters derived from the wall thickness
351//   seg1_bot_OD = seg1_bot_ID+2*wall;
352//   seg2_bot_ID = seg2_bot_OD-2*wall;
353//   seg2_top_ID = seg2_top_OD-2*wall;
354//   bend_r = 0.5*inch+seg1_bot_OD/2;   // Bend radius to get constant wall thickness
355//   trans = turtle3d([
356//                       ["move", seg1_len, "grow", seg2_bot_OD/seg1_bot_OD],  
357//                       "arcright", bend_r, bend_angle,
358//                       ["move", seg2_len, "grow", seg2_top_OD/seg2_bot_OD],
359//                       ["move", 0, "reverse", "grow", seg2_top_ID/seg2_top_OD],
360//                       ["move", seg2_len, "grow", seg2_bot_ID/seg2_top_ID],
361//                       "arcright", bend_r, bend_angle,
362//                       ["move", seg1_len, "grow", seg1_bot_ID/seg2_bot_ID]
363//                    ],
364//                    state=UP, transforms=true);
365//   back_half()      // Remove this to get a usable part
366//     sweep(circle(d=seg1_bot_OD, $fn=128), trans, closed=true);
367// Example(3D): Closed spiral
368//   include<BOSL2/skin.scad>
369//   steps = 500;
370//   spiral = turtle3d([
371//                      ["arc", 20,
372//                       "twist", 120,
373//                       "zrot", 360*4,
374//                       "steps",steps,
375//                       "shrink",1.5],
376//                      ["arc", 20,
377//                       "twist", 120,
378//                       "zrot", 360*4,
379//                       "steps",steps/5 ],
380//                      ["arc", 20,
381//                       "twist", 120,
382//                       "zrot", 360*4,
383//                       "steps",steps,
384//                       "grow",1.5],
385//                      ], transforms=true);
386//   sweep(fwd(25,p=circle(r=2,$fn=24)), spiral, caps=false);
387// Example(3D): Mobius strip (square)
388//   include<BOSL2/skin.scad>
389//   mobius = turtle3d([["arc", 20, "zrot", 360,"steps",100,"twist",180]], transforms=true);
390//   sweep(subdivide_path(square(8,center=true),16), mobius, closed=false);
391// Example(3D): Torus knot
392//   include<BOSL2/skin.scad>
393//   p = 3;      // (number of turns)*gcd(p,q)
394//   q = 10;     // (number of dives)*gcd(p,q)
395//   steps = 60; // steps per turn
396//   cordR  = 2; // knot cord radius
397//   torusR = 20;// torus major radius
398//   torusr = 4; // torus minor radius
399//   knot_radius = torusr + 0.75*cordR; // inner radius of knot, set to torusr to put knot
400//   wind_angle = atan(p / q *torusR / torusr);            // center on torus surface
401//   m = gcd(p,q);
402//   torus_knot0 =
403//       turtle3d([ "arcsteps", 1,
404//                  "repeat", p*steps/m-1 ,
405//                     [ [ "arc", torusR, "left", 360/steps, "twist", 360*q/p/steps ] ]
406//                ], transforms=true);
407//   torus_knot = [for(tr=torus_knot0) tr*xrot(wind_angle+90)];
408//   torus = turtle3d( ["arcsteps", steps, "arcleft", torusR, 360], transforms=true);
409//   fwd(torusR){ // to center the torus and knot at the origin
410//       color([.8,.7,1])
411//         sweep(right(knot_radius,p=circle(cordR,$fn=16)), torus_knot,closed=true);
412//       color("blue")
413//         sweep(circle(torusr,$fn=24), torus);
414//   }
415
416/*
417turtle state: sequence of transformations ("path") so far
418              sequence of pre-transforms that apply to the polygon (scaling and twist)
419              default move
420              default angle
421              default arc steps
422*/
423
424function _turtle3d_state_valid(state) =
425    is_list(state)
426        && is_consistent(state[0],ident(4))
427        && is_consistent(state[1],ident(4))
428        && is_num(state[2])
429        && is_num(state[3])
430        && is_num(state[4]);
431
432module turtle3d(commands, state=RIGHT, transforms=false, full_state=false, repeat=1) {no_module();}
433function turtle3d(commands, state=RIGHT, transforms=false, full_state=false, repeat=1) =
434  assert(is_bool(transforms))
435  let(
436       state = is_matrix(state,4,4) ? [[state],[yrot(90)],1,90,0] :
437               is_vector(state,3) ?
438                  let( updir = UP - (UP * state) * state / (state*state) )
439                  [[frame_map(x=state, z=approx(norm(updir),0) ? FWD : updir)], [yrot(90)],1, 90, 0]
440                : assert(_turtle3d_state_valid(state), "Supplied state is not valid")
441                  state,
442       finalstate = _turtle3d_repeat(commands, state, repeat)
443  )
444    assert(is_integer(repeat) && repeat>=0, "turtle3d repeat argument must be a nonnegative integer")
445    full_state  ? finalstate 
446  : !transforms ? deduplicate([for(T=finalstate[0]) apply(T,[0,0,0])])
447  : [for(i=idx(finalstate[0])) finalstate[0][i]*finalstate[1][i]];
448
449function _turtle3d_repeat(commands, state, repeat) =
450   repeat<=0 ? state : _turtle3d_repeat(commands, _turtle3d(commands, state), repeat-1);
451
452function _turtle3d_command_len(commands, index) =
453    let( one_or_two_arg = ["arcleft","arcright", "arcup", "arcdown", "arczrot", "arcyrot", "arcxrot"] )
454    in_list(commands[index],["repeat","arctodir","arcrot"]) ? 3 :   // Repeat, arctodir and arcrot commands require 2 args
455    // For these, the first arg is required, second arg is present if it is not a string or list
456    in_list(commands[index], one_or_two_arg) && len(commands)>index+2 && !is_string(commands[index+2]) && !is_list(commands[index+2])  ? 3 :  
457    is_string(commands[index+1]) || is_list(commands[index])? 1 :  // If 2nd item is a string it's must be a new command; 
458                                                                   // If first item is a list it's a compound command
459    2;                                 // Otherwise we have command and arg
460       
461function _turtle3d(commands, state, index=0) =
462    index >= len(commands) ? state :
463    _turtle3d(commands,
464            _turtle3d_command(commands[index],commands[index+1],commands[index+2],state,index),
465            index+_turtle3d_command_len(commands,index)
466        );
467
468function _turtle3d_rotation(command,angle,center) =
469  let(
470      myangle = (ends_with(command,"right") || ends_with(command,"up") ? -1 : 1 ) * angle
471  )
472  ends_with(command,"xrot") ? xrot(myangle,cp=center) :
473  ends_with(command,"yrot") ? yrot(myangle,cp=center) :
474  ends_with(command,"zrot") ? zrot(myangle,cp=center) :
475  ends_with(command,"right") || ends_with(command,"left") ? zrot(myangle,cp=center) :
476                                                            yrot(myangle,cp=center);
477
478// The turtle3d state maintains two lists of transformations that must be updated together. 
479// This function updates the state by appending a list of transforms and list of pre-transforms
480// to the state.
481function _tupdate(state, tran, pretran) =
482    [
483     concat(state[0],tran),
484     concat(state[1],pretran),
485     each list_tail(state,2)
486    ];
487
488function _turtle3d_command(command, parm, parm2, state, index) =
489    command == "repeat"?
490        assert(is_int(parm) && parm>=0,str("\"repeat\" command requires an integer repeat count at index ",index))
491        assert(is_list(parm2),str("\"repeat\" command requires a command list parameter at index ",index))
492        _turtle3d_repeat(parm2, state, parm) :
493    let(
494        trlist = 0,
495        prelist = 1,
496        movestep=2,
497        angle=3,
498        arcsteps=4,
499        parm = !is_string(parm) ? parm : undef,
500        parm2 = command=="arctodir" || command=="arcrot" ? parm2 
501              : !is_string(parm2) && !is_list(parm2) ? parm2 : undef,
502        needvec = ["jump", "xyzmove","setdir"],
503        neednum = ["untilx","untily","untilz","xjump","yjump","zjump","angle","length","scale","addlength"],
504        numornothing = ["right","left","up","down","xrot","yrot","zrot", "roll", "move"],
505        needtran = ["rot"],
506        chvec = !in_list(command,needvec) || is_vector(parm,3),
507        chnum = (!in_list(command,neednum) || is_num(parm))
508                && (!in_list(command,numornothing) || (is_undef(parm) || is_num(parm))),
509        chtran = !in_list(command,needtran) || is_matrix(parm,4,4),
510        lastT = last(state[trlist]),
511        lastPre = last(state[prelist]),
512        lastpt = apply(lastT,[0,0,0])
513    )
514    assert(chvec,str("\"",command,"\" requires a 3d vector parameter at index ",index))
515    assert(chnum,str("\"",command,"\" requires a numeric parameter at index ",index))
516    assert(chtran,str("\"",command,"\" requires a 4x4 transformation matrix at index ",index))
517    command=="move" ? _tupdate(state, [lastT*right(default(parm,1)*state[movestep])], [lastPre]):
518    in_list(command,["untilx","untily","untilz"]) ? (
519        let(
520            dirlist=[RIGHT, BACK, UP],
521            plane = [each dirlist[search([command],["untilx","untily","untilz"])[0]], parm],
522            step = [lastpt,apply(lastT,RIGHT)],
523            int = plane_line_intersection(plane, step, bounded=[true,false])
524        )
525        assert(is_def(int), str("\"",command,"\" never reaches desired goal at index ",index))
526        let(
527            size = is_vector(int,3) ? norm(int-lastpt) / norm(step[1]-step[0]) : 0
528        )
529        _tupdate(state, [lastT*right(size)], [lastPre])
530    ) :
531    command=="xmove" ? _tupdate(state,[right(default(parm,1)*state[movestep])*lastT],[lastPre]):
532    command=="ymove" ? _tupdate(state,[back(default(parm,1)*state[movestep])*lastT],[lastPre]):
533    command=="zmove" ? _tupdate(state,[up(default(parm,1)*state[movestep])*lastT],[lastPre]):
534    command=="xyzmove" ? _tupdate(state,[move(parm)*lastT],[lastPre]):
535    command=="jump" ? _tupdate(state,[move(parm-lastpt)*lastT],[lastPre]):
536    command=="xjump" ? _tupdate(state,[move([parm,lastpt.y,lastpt.z]-lastpt)*lastT],[lastPre]):
537    command=="yjump" ? _tupdate(state,[move([lastpt.x,parm,lastpt.z]-lastpt)*lastT],[lastPre]):
538    command=="yjump" ? _tupdate(state,[move([lastpt.x,lastpt.y,parm]-lastpt)*lastT],[lastPre]):
539    command=="angle" ? assert(parm!=0,str("\"",command,"\" requires nonnegative argument at index ",index))
540                       list_set(state, angle, parm) :
541    command=="length" ? list_set(state, movestep, parm) :
542    command=="scale" ?  list_set(state, movestep, parm*state[movestep]) :
543    command=="addlength" ?  list_set(state, movestep, state[movestep]+parm) :
544    command=="arcsteps" ?  assert(is_int(parm) && parm>0, str("\"",command,"\" requires a postive integer argument at index ",index))
545                           list_set(state, arcsteps, parm) :
546    command=="roll" ? list_set(state, trlist, concat(list_head(state[trlist]), [lastT*xrot(parm)])):
547    in_list(command,["right","left","up","down"]) ? 
548        list_set(state, trlist, concat(list_head(state[trlist]), [lastT*_turtle3d_rotation(command,default(parm,state[angle]))])):
549    in_list(command,["xrot","yrot","zrot"]) ?
550        let(
551             Trot = _rotpart(lastT),      // Extract rotational part of lastT
552             shift = _transpart(lastT)    // Translation part of lastT
553        )
554        list_set(state, trlist, concat(list_head(state[trlist]),
555                                       [move(shift)*_turtle3d_rotation(command,default(parm,state[angle])) * Trot])):
556    command=="rot" ?
557        let(
558             Trot = _rotpart(lastT),      // Extract rotational part of lastT
559             shift = _transpart(lastT)    // Translation part of lastT
560        )
561        list_set(state, trlist, concat(list_head(state[trlist]),[move(shift) * parm * Trot])):
562    command=="setdir" ?
563        let(
564             Trot = _rotpart(lastT),
565             shift = _transpart(lastT)
566        )
567        list_set(state, trlist, concat(list_head(state[trlist]),
568                                       [move(shift)*rot(from=apply(Trot,RIGHT),to=parm) * Trot ])):
569    in_list(command,["arcleft","arcright","arcup","arcdown"]) ?
570        assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index))
571        let(
572            radius = state[movestep]*parm,
573            myangle = default(parm2,state[angle])
574        )
575        assert(myangle!=0, str("\"",command,"\" command requires a nonzero angle at index ",index))
576        let(
577            length = 2*PI*radius * abs(myangle)/360, 
578            center = [0,
579                      command=="arcleft"?radius:command=="arcright"?-radius:0,
580                      command=="arcdown"?-radius:command=="arcup"?radius:0],
581            steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps]
582        )    
583        _tupdate(state,
584                 [for(n=[1:1:steps]) lastT*_turtle3d_rotation(command,myangle*n/steps,center)],
585                 repeat(lastPre,steps)):
586    in_list(command,["arcxrot","arcyrot","arczrot"]) ?
587        assert(is_num(parm),str("\"",command,"\" command requires a numeric radius value at index ",index))  
588        let(
589            radius = state[movestep]*parm,
590            myangle = default(parm2,state[angle])
591        )
592        assert(myangle!=0, str("\"",command,"\" command requires a nonzero angle at index ",index))
593        let(
594            length = 2*PI*radius * abs(myangle)/360, 
595            steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps],
596            Trot = _rotpart(lastT),
597            shift = _transpart(lastT),
598            v = apply(Trot,RIGHT),
599            dir = command=="arcxrot" ? RIGHT
600                : command=="arcyrot" ? BACK
601                : UP,
602            projv = v - (dir*v)*dir,
603            center = sign(myangle) * radius * cross(dir,projv),
604            slope = dir*v / norm(projv),
605            vshift = dir*slope*length
606        )
607        assert(!all_zero(projv), str("Rotation acts as twist, which does not produce a valid arc, at index ",index))
608        _tupdate(state,
609                 [for(n=[1:1:steps]) move(shift+vshift*n/steps)*_turtle3d_rotation(command,myangle*n/steps,center)*Trot],
610                 repeat(lastPre,steps)):
611    command=="arctodir" || command=="arcrot"?
612        assert(command!="arctodir" || is_vector(parm2,3),str("\"",command,"\" command requires a direction vector at index ",index))
613        assert(command!="arcrot" || is_matrix(parm2,4,4),str("\"",command,"\" command requires a transformation matrix at index ",index))  
614        let(
615            Trot = _rotpart(lastT),
616            shift = _transpart(lastT),
617            v = apply(Trot,RIGHT),
618            rotparms = command=="arctodir"
619                              ? rot_decode(rot(from=v,to=parm2))
620                              : rot_decode(parm2),
621            dir = rotparms[1],
622            myangle = rotparms[0],
623            projv = v - (dir*v)*dir,
624            slope = dir*v / norm(projv),
625            radius = state[movestep]*parm,
626            length = 2*PI*radius * myangle/360, 
627            vshift = dir*slope*length,
628            steps = state[arcsteps]==0 ? segs(abs(radius)) : state[arcsteps],
629            center = radius * cross(dir,projv)
630        )
631        assert(!all_zero(projv), str("Rotation acts as twist, which does not produce a valid arc, at index ",index))
632        _tupdate(state,
633                 [for(n=[1:1:steps]) move(shift+vshift*n/steps)*rot(n/steps*myangle,v=rotparms[1],cp=center)*Trot],
634                 repeat(lastPre,steps)):
635    is_list(command) ?
636        let(list_update = _turtle3d_list_command(command, state[arcsteps], state[movestep], lastT, lastPre, index))
637        _tupdate(state, list_update[0], list_update[1]):
638    assert(false,str("Unknown turtle command \"",command,"\" at index",index))
639    [];
640       
641
642function _turtle3d_list_command(command,arcsteps,movescale, lastT,lastPre,index) =
643   let(
644       reverse_index = search(["reverse"], command, 0)[0],
645       reverse = len(reverse_index)==1,
646       arcind = search(["arc"], command, 0)[0],
647       moveind = search(["move"], command, 0)[0],
648       movearcok = (arcind==[] || max(arcind)==0) && (moveind==[] || max(moveind)==0)
649   )
650   assert(len(reverse_index)<=1, str("Only one \"reverse\" is allowed at index ",index))
651   assert(!reverse || reverse_index[0]%2==0, str("Error processing compound command at index ",index))
652   assert(movearcok, str("\"move\" or \"arc\" must appear at the beginning of the compound command at index ",index))
653   assert(!reverse || len(command)%2==1,str("Odd number of entries in [keyword,value] list (after removing \"reverse\") at index ",index))
654   assert(reverse || len(command)%2==0,str("Odd number of entries in [keyword,value] list at index ",index))
655   let(
656       
657       command = list_remove(command, reverse_index),
658       keys=command[0]=="move" ?
659               struct_set([
660                           ["move", 0],
661                           ["twist",0],
662                           ["grow",1],
663                           ["shrink",1],
664                           ["steps",1],
665                           ["roll",0],
666                          ],
667                          command, grow=false)
668          :command[0]=="arc" ?
669               struct_set([
670                           ["arc", 0],
671                           ["up", 0],
672                           ["down", 0],
673                           ["left", 0],
674                           ["right", 0],
675                           ["twist",0],
676                           ["grow",1],
677                           ["shrink",1],
678                           ["steps",0],
679                           ["roll",0],
680                           ["rot", 0],
681                           ["todir", 0],
682                           ["xrot", 0],
683                           ["yrot", 0],
684                           ["zrot", 0],
685                          ],
686                          command, grow=false)
687          :assert(false,str("Unknown compound turtle3d command \"",command,"\" at index ",index)),
688       move = command[0]=="move" ? movescale*struct_val(keys,"move") : 0,
689       flip = reverse ? xflip() : ident(4),            // If reverse is given we set flip 
690       radius = movescale*first_defined([struct_val(keys,"arc"),0]),  // arc radius if given
691       twist = struct_val(keys,"twist"),
692       grow = force_list(struct_val(keys,"grow"),2),
693       shrink = force_list(struct_val(keys, "shrink"),2)
694   )
695   assert(is_num(radius), str("Radius parameter to \"arc\" must be a number in command at index ",index))
696   assert(is_vector(grow,2), str("Parameter to \"grow\" must be a scalar or 2d vector at index ",index))
697   assert(is_vector(shrink,2), str("Parameter to \"shrink\" must be a scalar or 2d vector at index ",index))
698   let(
699       scaling = point3d(v_div(grow,shrink),1),
700       usersteps = struct_val(keys,"steps"),
701       roll = struct_val(keys,"roll"),
702       ////////////////////////////////////////////////////////////////////////////////////////
703       ////  Next section is computations for relative rotations: "left", "right", "up" or "down"
704       right = default(struct_val(keys,"right"),0),
705       left = default(struct_val(keys,"left"),0),
706       up = default(struct_val(keys,"up"),0),
707       down = default(struct_val(keys,"down"),0),
708       angleok = assert(command[0]=="move" || (is_num(right) && is_num(left) && is_num(up) && is_num(down)),
709                        str("Must give numeric argument to \"left\", \"right\", \"up\" and \"down\" in command at index ",index))
710                 command[0]=="move" || ((up-down==0 || abs(left-right)<180) && (left-right==0 || abs(up-down)<180))
711   )
712   assert(command[0]=="move" || right==0 || left==0, str("Cannot specify both \"left\" and \"right\" in command at index ",index))
713   assert(command[0]=="move" || up==0 || down==0, str("Cannot specify both \"up\" and \"down\" in command at index ",index))
714   assert(angleok, str("Mixed angles must all be below 180 at index ",index))
715   let(
716        newdir = apply(zrot(left-right)*yrot(down-up),RIGHT),     // This is the new direction turtle points relative to RIGHT
717        relaxis = left-right == 0 ? BACK
718                : down-up == 0 ? UP
719                : cross(RIGHT,newdir),         // This is the axis of rotation for "right", "left", "up" or "down"
720        angle = command[0]=="move" ? 0 :
721                  left-right==0 || down-up==0 ? down-up+left-right :
722                  vector_angle(RIGHT,newdir),    // And this is the angle for that case.
723        center = -radius * (                     // Center of rotation for this case
724                      left-right == 0 ? [0,0,sign(down-up)]
725                    : down-up == 0 ? [0,sign(right-left),0]
726                    :       unit(cross(RIGHT,cross(RIGHT,newdir)),[0,0,0])
727                 ),    
728        ///////////////////////////////////////////////
729        // Next we compute values for absolute rotations: "xrot", "xrot", "yrot", "zrot", and "todir"
730        //
731        xrotangle = struct_val(keys,"xrot"),
732        yrotangle = struct_val(keys,"yrot"),
733        zrotangle = struct_val(keys,"zrot"),
734        rot = struct_val(keys,"rot"),
735        todir = struct_val(keys,"todir"),
736        // Compute rotation angle and axis for the absolute rotation (or undef if no absolute rotation is given)
737        abs_angle_axis =
738            command[0]=="move" ? [undef,CENTER] :
739            let(nzcount=len([for(entry=[xrotangle,yrotangle,zrotangle,rot,todir]) if (entry!=0) 1]))
740            assert(nzcount<=1, str("You can only define one of \"xrot\", \"yrot\", \"zrot\", \"rot\", and \"todir\" at index ",index))
741            rot!=0 ?   assert(is_matrix(rot,4,4),str("Argument to \"rot\" is not a 3d transformation matrix at index ",index))
742                       rot_decode(rot)
743          : todir!=0 ? assert(is_vector(todir,3),str("Argument to \"todir\" is not a length 3 vector at index ",index))
744                       rot_decode(rot(from=v, to=todir))
745          : xrotangle!=0 ? [xrotangle, RIGHT]
746          : yrotangle!=0 ? [yrotangle, BACK]
747          : zrotangle!=0 ? [zrotangle, UP]
748          : [undef,CENTER],
749        absangle = abs_angle_axis[0],
750        absaxis = abs_angle_axis[1],
751        // Computes the extra shift and center with absolute rotation
752        Trot = _rotpart(lastT),  
753        shift = _transpart(lastT), 
754        v = apply(Trot,RIGHT),           // Current direction
755        projv = v - (absaxis*v)*absaxis, // Component of rotation axis orthogonal to v
756        abscenter = is_undef(absangle) ? undef : sign(absangle) * radius * cross(absaxis,projv),    // absangle might be undef if command is "move"
757        slope = absaxis*v / norm(projv),       // This computes the shift in the direction along the rotational axis
758        vshift = is_undef(absangle) ? undef : absaxis*slope* 2*PI*radius*absangle/360
759    )
760    // At this point angle is nonzero if and only if a relative angle command (left, right, up down) was given,
761    //               absangle is defined if and only if an absolute angle command was given
762    assert(is_undef(absangle) || absangle!=0, str("Arc rotation with zero angle at index ",index))
763    assert(angle==0 || is_undef(absangle), str("Mixed relative and absolute rotations at index ",index))
764    assert(is_int(usersteps) && usersteps>=0 && (command[0]=="arc" || usersteps>=1),
765           str("Steps value ",usersteps," invalid at index ",index))
766    assert(is_undef(absangle) || !all_zero(projv), str("Rotation acts as twist, which does not produce a valid arc at index ",index))
767    let( 
768        steps = usersteps != 0 ? usersteps
769              : arcsteps != 0 ? arcsteps
770              : ceil(segs(abs(radius)) * abs(first_defined([absangle,angle]))/360),
771        // The next line computes a list of pairs [trans,pretrans] for the segment or arc
772        result =  is_undef(absangle)
773                  ? [for(n=[1:1:steps]) let(frac=n/steps)
774                              [lastT * flip * right(frac*move) * (angle==0?ident(4):rot(frac*angle,v=relaxis,cp=center)) * xrot(frac*roll),
775                               lastPre * zrot(frac*twist) * scale(lerp([1,1,1],scaling,frac))]
776                    ]
777                  : [for(n=[1:1:steps]) let(frac=n/steps) 
778                              [move(shift+vshift*frac) * rot(frac*absangle,v=absaxis,cp=abscenter)*Trot * xrot(frac*roll),
779                               lastPre * zrot(frac*twist) * scale(lerp([1,1,1],scaling,frac))]
780                    ]
781    )                     // Transpose converts the result into a list of the form [[trans1,trans2,...],[pretran1,pretran2,...]],
782    transpose(result);    // which is required by _tupdate
783
784