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