omdl  v1.0
OpenSCAD Mechanical Design Library
turtle_path.scad
Go to the documentation of this file.
1 //! Turtle-style step language to generate coordinate points for polygon construction.
2 /***************************************************************************//**
3  \file
4  \author Roy Allen Sutton
5  \date 2024,2026
6 
7  \copyright
8 
9  This file is part of [omdl] (https://github.com/royasutton/omdl),
10  an OpenSCAD mechanical design library.
11 
12  The \em omdl is free software; you can redistribute it and/or modify
13  it under the terms of the [GNU Lesser General Public License]
14  (http://www.gnu.org/licenses/lgpl.html) as published by the Free
15  Software Foundation; either version 2.1 of the License, or (at
16  your option) any later version.
17 
18  The \em omdl is distributed in the hope that it will be useful,
19  but WITHOUT ANY WARRANTY; without even the implied warranty of
20  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21  Lesser General Public License for more details.
22 
23  You should have received a copy of the GNU Lesser General Public
24  License along with the \em omdl; if not, write to the Free Software
25  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26  02110-1301, USA; or see <http://www.gnu.org/licenses/>.
27 
28  \details
29 
30  \amu_define group_name (Turtle)
31  \amu_define group_brief (Turtle-style step language to generate coordinate points for polygon construction..)
32 
33  \amu_include (include/amu/doxyg_init_pd_gds_ipg.amu)
34 *******************************************************************************/
35 
36 //----------------------------------------------------------------------------//
37 // group.
38 //----------------------------------------------------------------------------//
39 
40 /***************************************************************************//**
41  \amu_include (include/amu/doxyg_define_in_parent_open.amu)
42  \amu_include (include/amu/includes_required.amu)
43 *******************************************************************************/
44 
45 //----------------------------------------------------------------------------//
46 // private helper functions
47 //----------------------------------------------------------------------------//
48 
49 //! Dispatches a line segment to a straight or wave-line constructor.
50 /***************************************************************************//**
51  \param p0 <point-2d> The line start coordinate [x, y].
52  \param t <point-2d> The line end coordinate [x, y].
53  \param wc <datastruct> The waveform configuration `[p, a, w, m]`;
54  undef for straight line.
55  \param fn <integer> The number of [facets]; optional.
56 
57  \returns <point-2d-list> The list of coordinate points.
58 
59  \details
60 
61  Dispatches to either a straight line or a wave-line based on whether
62  \p wc is defined. When \p wc is undef, returns `[t]` as a
63  single-element point list. When \p wc is defined, delegates to
64  polygon_line_wave_p() using the waveform configuration parameters.
65 
66  [facets]: \ref get_fn()
67 
68  \private
69 *******************************************************************************/
70 function _polygon_turtle_path_p_line_p
71 (
72  p0,
73  t,
74  wc,
75  fn
76 ) = is_undef( wc ) ? [t]
77  : polygon_line_wave_p( p1=p0, p2=t, p=wc[0], a=wc[1], w=wc[2], m=wc[3], fn=fn );
78 
79 //! Recursively evaluates a sub-step list n times, threading state between iterations.
80 /***************************************************************************//**
81  \param sub_s <datastruct> The sub-step list to repeat.
82  \param n <integer> The number of iterations remaining.
83  \param p0 <point-2d> The current position.
84  \param h <decimal> The current heading in degrees.
85  \param _p0_g <point-2d> The global origin coordinate.
86 
87  \returns <datastruct> `[point-2d-list, decimal]`; the concatenated
88  point list of all iterations and the final heading after
89  the last iteration.
90 
91  \private
92 *******************************************************************************/
93 function _polygon_turtle_path_p_repeat
94 (
95  sub_s,
96  n,
97  p0,
98  h,
99  _p0_g
100 ) = (n <= 0) ? [empty_lst, h]
101  : let
102  (
103  result = polygon_turtle_path_p( sub_s, p0, h, 1, 0, _p0_g ),
104  pts = result[0],
105  next_p0 = is_empty(pts) ? p0 : last(pts),
106  next_h = result[1]
107  )
108  (n == 1) ? result
109  : let
110  (
111  rest = _polygon_turtle_path_p_repeat( sub_s, n-1, next_p0, next_h, _p0_g )
112  )
113  [ concat( pts, rest[0] ), rest[1] ];
114 
115 //----------------------------------------------------------------------------//
116 // interpreter
117 //----------------------------------------------------------------------------//
118 
119 //! Interprets a turtle-style step language to generate coordinate points for polygon construction.
120 /***************************************************************************//**
121  \param s <datastruct> The list of steps.
122 
123  \param p0 <point-2d> The initial coordinate [x, y].
124 
125  \param h <decimal> The current heading in degrees; 0 =
126  positive x-axis, positive angles rotate
127  counter-clockwise.
128 
129  \param m <integer> The return mode. When \b 0 (default),
130  returns a `<point-2d-list>`. When \b 1, returns
131  `[point-2d-list, decimal]` where the second element
132  is the final heading after all steps have been
133  evaluated.
134 
135  \param _s_n <integer> The current step number.
136 
137  \param _p0_g <point-2d> The global origin coordinate [x, y].
138 
139  \returns (1) <point-2d-list> when \p m is \b 0;
140  (2) <datastruct> `[point-2d-list, decimal]` when \p m is \b 1.
141 
142  The returned list contains the coordinate points of the path
143  described in the list of steps \p s. When \p m is \b 1 the final
144  heading is appended as the second element of the returned pair.
145 
146  \details
147 
148  This function is a lightweight interpreter that converts a list of
149  operation steps into coordinate points for polygon construction. It
150  provides a convenient mechanism for defining polygons using a
151  compact, step-based notation inspired by the Turtle graphics
152  geometric drawing language. Each step produces one or more output
153  points according to the schema described below.
154 
155  Data structure schema:
156 
157  name | schema
158  -----------------:|:----------------------------------------------
159  s | [ step, step, ..., step ]
160  step | [ operation, arg1, arg2, arg3, arg4 ]
161 
162  ## Operations
163 
164  The following table summarizes the supported operations, arguments,
165  and their semantics.
166 
167  operation | short | arguments || output coordinate point(s)
168  :-----------------:|:-------:|:--------------------:|:----------------------:|:-----------------------:
169  ^ | ^ | minimum arguments | extended arguments | ^
170  <h3> heading operations </h3> |||||
171  \ref turn_left | tl | a || (none)
172  \ref turn_right | tr | a || (none)
173  <h3> line operations </h3> |||||
174  \ref close | cl | (none) | wc, fn | _p0_g
175  \ref goto_xy | gxy | x, y | x, y, wc, fn | [x, y]
176  \ref goto_x | gx | x | x, wc, fn | [x, p0.y]
177  \ref goto_y | gy | y | y, wc, fn | [p0.x, y]
178  \ref delta_xy | dxy | x, y | x, y, wc, fn | p0 + [x, y]
179  \ref delta_x | dx | x | x, wc, fn | p0 + [x, 0]
180  \ref delta_y | dy | y | y, wc, fn | p0 + [0, y]
181  \ref delta_xa | dxa | x, a | x, a, wc, fn | p0 + [ x, x * tan(a) ]
182  \ref delta_ya | dya | y, a | y, a, wc, fn | p0 + [ y / tan(a), y ]
183  \ref move_ar | mar | m, a | m, a, wc, fn | p0 + line(m, a)
184  \ref move_rr | mrr | m, a | m, a, wc, fn | p0 + line(m, h+a)
185  \ref move_fw | mfw | m | m, wc, fn | p0 + line(m, h)
186  <h3> arc operations </h3> |||||
187  \ref arc_fw | afw | r, a | r, a, [o], fn | \ref arc_fw "(see below)"
188  \ref arc_pv | apv | c, v, cw, fn || \ref arc_pv "(see below)"
189  \ref arc_vv | avv | v, v, cw, fn || \ref arc_vv "(see below)"
190  \ref arc_blend | ab | p2, p3, r | p2, p3, r, fn | \ref arc_blend "(see below)"
191  <h3> curve operations </h3> |||||
192  \ref bezier | bz | ctrl_pts | ctrl_pts, [o], fn | \ref bezier "(see below)"
193  \ref spline | spl | knots | knots, [o], fn | \ref spline "(see below)"
194  <h3> sub-step operations </h3> |||||
195  \ref repeat | rpt | steps | steps, n | \ref repeat "(see below)"
196  \ref repeat_mx | rptmx | steps, axis | steps, axis, [o] | \ref repeat_mx "(see below)"
197  \ref repeat_my | rptmy | steps, axis | steps, axis, [o] | \ref repeat_my "(see below)"
198  \ref transform | xfrm | steps, r | steps, r, t, mn, [o] | \ref transform "(see below)"
199  <h3> point operations </h3> |||||
200  \ref path_p | pp | [p1, p2, ..., pn] || \ref path_p "(see below)"
201 
202  The two argument columns divide the minimum form from the extended
203  form: the left column shows the minimum required arguments, and the
204  right column shows the extended form with additional optional
205  parameters.
206 
207  Some operations may generate either straight or periodic waveform
208  lines. When a periodic waveform line is desired, additional
209  parameters specify the waveform configuration and period fragment
210  count as shown in the table above.
211 
212  ## Wave-line waveform configuration
213 
214  e | data type | default value | parameter description
215  :--:|:---------------------:|:-------------:|:------------------------------------
216  \* | datastruct | required | \p wc : waveform configuration `[p, a, w, m]`
217  \* | integer | | \p fn : number of [facets]; optional
218 
219  #### wc
220 
221  e | data type | default value | parameter description
222  :--:|:---------------------:|:-------------:|:------------------------------------
223  0 | decimal | see below | \p p : period length
224  1 | decimal-list-3 \| decimal | see below | \p a : amplitude configuration
225  2 | datastruct \| integer | see below | \p w : waveform shape configuration
226  3 | datastruct \| integer | see below | \p m : time-axis remapping mode configuration
227 
228  Wave-line constructs a line with periodic waveform lateral
229  displacement to the next point using \p polygon_line_wave_p(). See
230  its documentation for more details and default value. All line
231  operations accept the optional \p wc and \p fn parameters to produce
232  a wave-line in place of a straight segment.
233 
234  ## Heading operations
235 
236  \subsubsection turn_left
237 
238  Rotates the current heading counter-clockwise by \p a degrees.
239  Produces no output coordinate points; only the heading \p h is
240  updated for subsequent steps.
241 
242  \subsubsection turn_right
243 
244  Rotates the current heading clockwise by \p a degrees. Produces no
245  output coordinate points; only the heading \p h is updated for
246  subsequent steps.
247 
248  ## Line operations
249 
250  \subsubsection close
251 
252  Closes the path by returning to the global origin \p _p0_g with a
253  straight or wave-line segment. When no arguments are given, a
254  straight line is drawn. The global origin is set automatically on the
255  first step and remains fixed for the lifetime of the recursion.
256 
257  \subsubsection goto_xy
258 
259  Moves to the absolute coordinate `[x, y]`, ignoring the current
260  position.
261 
262  \subsubsection goto_x
263 
264  Moves to the absolute x-axis coordinate \p x, retaining the current
265  y-axis coordinate.
266 
267  \subsubsection goto_y
268 
269  Moves to the absolute y-axis coordinate \p y, retaining the current
270  x-axis coordinate.
271 
272  \subsubsection delta_xy
273 
274  Moves from the current position by the relative offset `[x, y]`.
275 
276  \subsubsection delta_x
277 
278  Moves from the current position by the relative x-axis offset \p x,
279  with no y-axis displacement.
280 
281  \subsubsection delta_y
282 
283  Moves from the current position by the relative y-axis offset \p y,
284  with no x-axis displacement.
285 
286  \subsubsection delta_xa
287 
288  e | data type | default value | parameter description
289  :--:|:---------------------:|:-------------:|:------------------------------------
290  0 | decimal | required | \p x : x-axis displacement
291  1 | decimal | required | \p a : angle in degrees from the x-axis
292 
293  Moves from the current position by the relative offset `[x, x *
294  tan(a)]`, where the y-axis displacement is derived from the x-axis
295  displacement \p x and the angle \p a. This is convenient when the
296  horizontal extent of a step is known and the slope angle is the
297  natural constraint.
298 
299  \subsubsection delta_ya
300 
301  e | data type | default value | parameter description
302  :--:|:---------------------:|:-------------:|:------------------------------------
303  0 | decimal | required | \p y : y-axis displacement
304  1 | decimal | required | \p a : angle in degrees from the x-axis
305 
306  Moves from the current position by the relative offset `[y / tan(a),
307  y]`, where the x-axis displacement is derived from the y-axis
308  displacement \p y and the angle \p a. This is the complement of \p
309  delta_xa and is convenient when the vertical extent of a step is
310  known and the slope angle is the natural constraint.
311 
312  \subsubsection move_ar
313 
314  e | data type | default value | parameter description
315  :--:|:---------------------:|:-------------:|:------------------------------------
316  0 | decimal | required | \p m : radial distance
317  1 | decimal | required | \p a : absolute angle in degrees
318 
319  Moves from the current position by distance \p m at the absolute
320  angle \p a, measured from the positive x-axis. The heading \p h is
321  not consulted.
322 
323  \subsubsection move_rr
324 
325  e | data type | default value | parameter description
326  :--:|:---------------------:|:-------------:|:------------------------------------
327  0 | decimal | required | \p m : radial distance
328  1 | decimal | required | \p a : angle offset in degrees relative to current heading
329 
330  Moves from the current position by distance \p m at an angle of `h +
331  a`, where \p h is the current heading. When \p a is zero this
332  operation is equivalent to \p move_fw.
333 
334  \subsubsection move_fw
335 
336  Moves forward from the current position by distance \p m along the
337  current heading \p h. Equivalent to \p move_rr with \p a set to zero.
338 
339  ## Arc operations
340 
341  \subsubsection arc_fw
342 
343  e | data type | default value | parameter description
344  :--:|:---------------------:|:-------------:|:------------------------------------
345  0 | decimal | required | \p r : arc radius; must be positive
346  1 | decimal | required | \p a : signed sweep angle in degrees; positive = CCW (left turn), negative = CW (right turn)
347  2 | datastruct | | \p [o] : options `[update_heading]`; optional; assign \b undef to accept all option defaults when only \p fn is needed
348  3 | integer | | \p fn : number of [facets]; optional
349 
350  #### arc_fw[2]: o
351 
352  e | data type | default value | parameter description
353  :--:|:---------------------:|:-------------:|:------------------------------------
354  0 | boolean | true | \p update_heading : when \b true the heading is advanced by \p a degrees on exit; when \b false the entry heading \p h is restored
355 
356  Sweeps an arc of radius \p r through the signed angle \p a starting
357  from the current position \p p0 and following the current heading \p
358  h. The arc center lies perpendicular to \p h at distance \p r: to the
359  left (`h + 90°`) when \p a is positive and to the right (`h - 90°`)
360  when \p a is negative. The arc is delegated to polygon_arc_sweep_p().
361  Returns \b empty_lst when \p a is zero. By default the heading is
362  updated to `h + a` on exit, so chained `arc_fw` steps flow naturally
363  without manual heading bookkeeping. Pass `[o] = [false]` to suppress
364  the heading update. Pass `[o] = undef` to accept all option defaults
365  while still supplying \p fn.
366 
367  \subsubsection arc_pv
368 
369  e | data type | default value | parameter description
370  :--:|:---------------------:|:-------------:|:------------------------------------
371  0 | point-2d | required | \p c : arc center point [x, y]
372  1 | point-2d \| decimal | required | \p v : arc stop angle [x, y] or a
373  2 | boolean | required | \p cw : arc sweep direction
374  3 | integer | | \p fn : the number of [facets]; optional
375 
376  This operation constructs an arc about a center point specified as a
377  coordinate. The arc begins at the angle defined by the vector `[c,
378  p0]` and ends at the angle defined by either the vector `[c, v]` or
379  by the scalar angle \p v (in degrees). The sweep direction is
380  controlled by \p cw; when \p cw is set to \p true, the arc is swept
381  clockwise from the start angle to the stop angle. The optional
382  parameter \p fn specifies the number of facets and, when omitted, is
383  determined automatically by get_fn().
384 
385  \subsubsection arc_vv
386 
387  e | data type | default value | parameter description
388  :--:|:---------------------:|:-------------:|:------------------------------------
389  0 | decimal-list-2 | required | \p c : arc center point [m, a]
390  1 | point-2d \| decimal | required | \p v : arc stop angle [x, y] or a
391  2 | boolean | required | \p cw : arc sweep direction
392  3 | integer | | \p fn : number of [facets]; optional
393 
394  This operation constructs an arc about a center point specified as a
395  vector `[m, a]` originating from the current position. The arc begins
396  at the angle defined by the vector `[c, p0]` and ends at the angle
397  defined by either the vector `[c, v]` or by the scalar angle \p v (in
398  degrees). The sweep direction is controlled by \p cw; when \p cw is
399  set to \p true, the arc is swept clockwise from the start angle to
400  the stop angle. The optional parameter \p fn specifies the number of
401  facets and, when omitted, is determined automatically by get_fn().
402 
403  \subsubsection arc_blend
404 
405  e | data type | default value | parameter description
406  :--:|:---------------------:|:-------------:|:------------------------------------
407  0 | point-2d | required | \p p2 : corner vertex coordinate [x, y]; the point where the two segments meet
408  1 | point-2d | required | \p p3 : end point of the outgoing segment [x, y]
409  2 | decimal | required | \p r : blend radius; must be positive
410  3 | integer | | \p fn : number of [facets]; optional
411 
412  Replaces the sharp corner at \p p2 with a circular arc of radius \p r
413  that is tangent to both the incoming segment `[p0, p2]` and the
414  outgoing segment `[p2, p3]`. The current position \p p0 supplies the
415  start point of the incoming segment. Only the arc points are emitted;
416  the corner vertex \p p2 is not included. Delegates to
417  polygon_arc_blend_p(). The heading \p h is not updated by this
418  operation.
419 
420  ## Curve operations
421 
422  \subsubsection bezier
423 
424  e | data type | default value | parameter description
425  :--:|:---------------------:|:-------------:|:------------------------------------
426  0 | point-2d-list | required | \p ctrl_pts : Bézier control point list [[x, y], ...]
427  1 | datastruct | | \p [o] : options `[prepend_p0]`; optional; assign \b undef to accept all option defaults when only \p fn is needed
428  2 | integer | | \p fn : number of [facets]; optional
429 
430  #### bezier[1]: o
431 
432  e | data type | default value | parameter description
433  :--:|:---------------------:|:-------------:|:------------------------------------
434  0 | boolean | true | \p prepend_p0 : when \b true the current position \p p0 is automatically prepended to \p ctrl_pts as the first control point
435 
436  Evaluates a degree-n Bézier curve defined by the control point list
437  \p ctrl_pts using the de Casteljau algorithm. When \p prepend_p0 is
438  \b true (the default) the current position \p p0 is inserted as the
439  first control point so the curve departs smoothly from the last
440  emitted point. Pass `[o] = [false]` when \p ctrl_pts already contains
441  the intended start point. Pass `[o] = undef` to accept all option
442  defaults while still supplying \p fn. The heading \p h is not updated
443  by this operation. Delegates to polygon_bezier_p().
444 
445  \subsubsection spline
446 
447  e | data type | default value | parameter description
448  :--:|:---------------------:|:-------------:|:------------------------------------
449  0 | point-2d-list | required | \p knots : Catmull-Rom knot list [[x, y], ...]
450  1 | datastruct | | \p [o] : options `[prepend_p0, closed]`; optional; assign \b undef to accept all option defaults when only \p fn is needed
451  2 | integer | | \p fn : facets per segment; optional
452 
453  #### spline[1]: o
454 
455  e | data type | default value | parameter description
456  :--:|:---------------------:|:-------------:|:------------------------------------
457  0 | boolean | true | \p prepend_p0 : when \b true the current position \p p0 is automatically prepended to \p knots as the first knot
458  1 | boolean | false | \p closed : when \b true a closing segment is added from the last knot back to the first knot
459 
460  Evaluates a centripetal Catmull-Rom spline through the knot list \p
461  knots. The curve passes through every knot point. When \p prepend_p0
462  is \b true (the default) the current position \p p0 is inserted as
463  the first knot so the spline departs from the last emitted point.
464  When \p closed is \b true a closing segment from the last knot back
465  to the first is included. Pass `[o] = undef` to accept all option
466  defaults while still supplying \p fn. The heading \p h is not updated
467  by this operation. Delegates to polygon_spline_p().
468 
469  ## Sub-step operations
470 
471  \subsubsection repeat
472 
473  e | data type | default value | parameter description
474  :--:|:---------------------:|:-------------:|:------------------------------------
475  0 | datastruct | required | \p steps : sub-step list
476  1 | integer | 1 | \p n : number of repetitions; optional
477 
478  Evaluates the sub-step list \p steps \p n times in sequence. Each
479  iteration begins where the previous left off, carrying \p p0, \p h,
480  and \p _p0_g forward between iterations. The heading evolved inside
481  the sub-steps carries back into the outer step list after all
482  iterations complete. When \p n is omitted the sub-steps are evaluated
483  once, making \p repeat useful as a plain grouping mechanism. Nesting
484  is supported to arbitrary depth.
485 
486  \subsubsection repeat_mx
487 
488  e | data type | default value | parameter description
489  :--:|:---------------------:|:-------------:|:------------------------------------
490  0 | datastruct | required | \p steps : sub-step list
491  1 | decimal | 0 | \p axis : scalar offset of the mirror axis from \p p0 along the y-axis; optional
492  2 | datastruct | | \p o : options `[reverse, heading, mirror_start]`; optional
493 
494  #### repeat_mx[2]: o
495 
496  e | data type | default value | parameter description
497  :--:|:---------------------:|:-------------:|:------------------------------------
498  0 | boolean | true | \p reverse : reverse the mirrored point list for correct polygon winding
499  1 | boolean | true | \p heading : when true, carry the final heading of the forward pass back into the outer step list; when false, restore the entry heading
500  2 | boolean | true | \p mirror_start : begin the mirrored pass from \p p0; when false, begin from the end of the forward pass
501 
502  Evaluates \p steps once to produce a forward point list, then mirrors
503  all output points about a horizontal axis at `p0.y + axis`. The
504  mirrored list is appended after the forward list to produce a
505  top-bottom symmetric profile in a single operation. The options
506  bundle \p o controls winding order, heading continuity, and the start
507  point of the mirrored pass. Nesting is supported to arbitrary depth.
508 
509  \subsubsection repeat_my
510 
511  e | data type | default value | parameter description
512  :--:|:---------------------:|:-------------:|:------------------------------------
513  0 | datastruct | required | \p steps : sub-step list
514  1 | decimal | 0 | \p axis : scalar offset of the mirror axis from \p p0 along the x-axis; optional
515  2 | datastruct | | \p o : options `[reverse, heading, mirror_start]`; optional
516 
517  #### repeat_my[2]: o
518 
519  e | data type | default value | parameter description
520  :--:|:---------------------:|:-------------:|:------------------------------------
521  0 | boolean | true | \p reverse : reverse the mirrored point list for correct polygon winding
522  1 | boolean | true | \p heading : when true, carry the final heading of the forward pass back into the outer step list; when false, restore the entry heading
523  2 | boolean | true | \p mirror_start : begin the mirrored pass from \p p0; when false, begin from the end of the forward pass
524 
525  Evaluates \p steps once to produce a forward point list, then mirrors
526  all output points about a vertical axis at `p0.x + axis`. The
527  mirrored list is appended after the forward list to produce a
528  left-right symmetric profile in a single operation. The options
529  bundle \p o controls winding order, heading continuity, and the start
530  point of the mirrored pass. Nesting is supported to arbitrary depth.
531 
532  \subsubsection transform
533 
534  e | data type | default value | parameter description
535  :--:|:---------------------:|:-------------:|:------------------------------------
536  0 | datastruct | required | \p steps : sub-step list
537  1 | decimal | required | \p r : rotation angle in degrees, applied about \p p0
538  2 | point-2d | [0, 0] | \p t : translation vector `[x, y]`; optional
539  3 | vector-2d | undef | \p mn : mirror normal vector `[nx, ny]`; applied before rotation; optional
540  4 | datastruct | | \p o : options `[update_p0, update_h]`; optional
541 
542  #### transform[4]: o
543 
544  e | data type | default value | parameter description
545  :--:|:---------------------:|:-------------:|:------------------------------------
546  0 | boolean | false | \p update_p0 : when \b true, advance the parent \p p0 to the last transformed point after the operation
547  1 | boolean | o[0] | \p update_h : when \b true, update the parent \p h to `h_final + r` after the operation; defaults to \p update_p0 when not specified
548 
549  Evaluates \p steps to produce a point list, then applies a 2D affine
550  transformation to every output point via transform_p(). Mirror \p mn
551  is applied first about the entry position \p p0, followed by rotation
552  \p r about \p p0, then translation \p t. When \p mn is \b undef no
553  mirror is applied. When both options are false (the default) the
554  parent \p p0 and \p h are unchanged after the operation, making \p
555  transform a pure geometric post-processor. \p update_p0 and \p
556  update_h may be set independently: for example, pass `[true, false]`
557  to advance \p p0 to the last transformed point while preserving the
558  entry heading, or `[false, true]` to carry the rotated heading forward
559  while keeping the turtle anchored at its entry position. When only
560  \p update_p0 is supplied (e.g. `[true]`), \p update_h inherits the
561  same value. Nesting is supported to arbitrary depth.
562 
563  ## Point operations
564 
565  \subsubsection path_p
566 
567  e | data type | default value | parameter description
568  :--:|:---------------------:|:-------------:|:------------------------------------
569  0 | point-2d-list | required | path list of 2d points
570 
571  Inserts a list of 2D points `[x, y]` into the output at the current
572  step, specified as a single argument containing the point list. Care
573  should be taken to ensure a smooth transition between the end of the
574  previous step and the start of the inserted points.
575 
576  \amu_define title (Motor mount plate design example)
577  \amu_define image_views (top diag)
578  \amu_define image_size (sxga)
579  \amu_define scope_id (polygon_turtle_path_p)
580  \amu_define output_scad (true)
581 
582  \amu_include (include/amu/scope_diagrams_3d.amu)
583 
584  The corners of this example 2d design plate have been rounded with
585  the library function polygon_round_eve_all_p().
586 
587  [facets]: \ref get_fn()
588  [Turtle graphics]: https://en.wikipedia.org/wiki/Turtle_(robot)
589 *******************************************************************************/
590 function polygon_turtle_path_p
591 (
592  s,
593  p0 = origin2d,
594  h = 0,
595  m = 0,
596  _s_n = 0,
597  _p0_g
598 ) =
599  ! is_list( s ) ? ( m == 1 ? [empty_lst, h] : empty_lst )
600  : let
601  (
602  // get current step
603  step = first( s ),
604 
605  // get operation, argument vector, and argument count
606  oper = first( step ),
607  argv = tailn( step ),
608  argc = is_undef( argv ) ? 0 : is_list( argv ) ? len( argv ) : 1,
609 
610  // assign arguments
611  a1 = argv[0],
612  a2 = argv[1],
613  a3 = argv[2],
614  a4 = argv[3],
615  a5 = argv[4],
616 
617  //
618  // compute the coordinate point(s) and heading for this operation step.
619  // ph = [point-2d-list, heading] for every branch.
620  //
621  ph =
622  //
623  // heading; turn left (counter-clockwise)
624  //
625  is_oneof( oper, ["turn_left", "tl"] ) && (argc > 0) ?
626  [ empty_lst, h + a1 ]
627 
628  //
629  // heading; turn right (clockwise)
630  //
631  : is_oneof( oper, ["turn_right", "tr"] ) && (argc > 0) ?
632  [ empty_lst, h - a1 ]
633 
634  //
635  // lines; close path (return to global origin)
636  //
637  : is_oneof( oper, ["close", "cl"] ) ?
638  let
639  (
640  g = defined_or( _p0_g, p0 ), // step-1 guard
641  wc = a1,
642  fn = a2
643  )
644  [ _polygon_turtle_path_p_line_p( p0=p0, t=g, wc=wc, fn=fn ), h ]
645 
646  //
647  // lines; goto (absolute position)
648  //
649  : is_oneof( oper, ["goto_xy", "gxy"] ) && (argc > 1) ?
650  let( t = [a1, a2], wc = a3, fn = a4 )
651  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
652 
653  : is_oneof( oper, ["goto_x", "gx"] ) && (argc > 0) ?
654  let( t = [a1, p0.y], wc = a2, fn = a3 )
655  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
656 
657  : is_oneof( oper, ["goto_y", "gy"] ) && (argc > 0) ?
658  let( t = [p0.x, a1], wc = a2, fn = a3 )
659  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
660 
661  //
662  // lines; delta
663  //
664  : is_oneof( oper, ["delta_xy", "dxy"] ) && (argc > 1) ?
665  let( t = p0 + [a1, a2], wc = a3, fn = a4 )
666  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
667 
668  : is_oneof( oper, ["delta_x", "dx"] ) && (argc > 0) ?
669  let( t = p0 + [a1, 0], wc = a2, fn = a3 )
670  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
671 
672  : is_oneof( oper, ["delta_y", "dy"] ) && (argc > 0) ?
673  let( t = p0 + [0, a1], wc = a2, fn = a3 )
674  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
675 
676  //
677  // lines; delta angle
678  //
679  : is_oneof( oper, ["delta_xa", "dxa"] ) && (argc > 1) ?
680  let( t = p0 + [a1, a1 * tan(a2)], wc = a3, fn = a4 )
681  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
682 
683  : is_oneof( oper, ["delta_ya", "dya"] ) && (argc > 1) ?
684  let( t = p0 + [a1 / tan(a2), a1], wc = a3, fn = a4 )
685  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
686 
687  //
688  // lines; move radial absolute
689  //
690  : is_oneof( oper, ["move_ar", "mar"] ) && (argc > 1) ?
691  let( t = line_tp( line2d_new(m=a1, a=a2, p1=p0) ), wc = a3, fn = a4 )
692  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
693 
694  //
695  // lines; move radial relative (to current heading)
696  //
697  : is_oneof( oper, ["move_rr", "mrr"] ) && (argc > 1) ?
698  let( t = line_tp( line2d_new(m=a1, a=h+a2, p1=p0) ), wc = a3, fn = a4 )
699  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
700 
701  //
702  // lines; move forward (along current heading)
703  //
704  : is_oneof( oper, ["move_fw", "mfw"] ) && (argc > 0) ?
705  let( t = line_tp( line2d_new(m=a1, a=h, p1=p0) ), wc = a2, fn = a3 )
706  [ _polygon_turtle_path_p_line_p( p0=p0, t=t, wc=wc, fn=fn ), h ]
707 
708  //
709  // arc; forward sweep (heading-relative, optional heading update)
710  //
711  : is_oneof( oper, ["arc_fw", "afw"] ) && (argc > 1) ?
712  let
713  (
714  r = a1,
715  a = a2,
716  o = a3,
717  fn = a4,
718  upd = defined_eonb_or( o, 0, true ),
719 
720  // arc center: perpendicular to heading, left for a>0 right for a<0
721  perp_sign = (a >= 0) ? 1 : -1,
722  h_perp = h + perp_sign * 90,
723  c_arc = p0 + r * [cos(h_perp), sin(h_perp)],
724 
725  // start and end angles measured from arc center
726  a_start = h + perp_sign * (-90), // direction c_arc to p0
727  a_end = a_start + a, // sweep by a (signed)
728 
729  pts = (a == 0) ? empty_lst
731  (
732  r = r,
733  o = c_arc,
734  v1 = a_start,
735  v2 = a_end,
736  fn = fn,
737  cw = (a < 0)
738  ),
739 
740  out_h = upd ? h + a : h
741  )
742  [ pts, out_h ]
743 
744  //
745  // arc; center point
746  //
747  : is_oneof( oper, ["arc_pv", "apv"] ) && (argc > 2) ?
748  let( v2 = is_list(a2) ? [a1, a2] : a2 )
749  [
750  polygon_arc_sweep_p( r=distance_pp(p0, a1), o=a1, v1=[a1, p0], v2=v2, cw=a3, fn=a4 ),
751  h
752  ]
753 
754  //
755  // arc; center vector
756  //
757  : is_oneof( oper, ["arc_vv", "avv"] ) && (argc > 2) ?
758  let
759  (
760  b1 = line_tp( line2d_new(m=first(a1), a=second(a1), p1=p0) ),
761  v2 = is_list(a2) ? [b1, a2] : a2
762  )
763  [
764  polygon_arc_sweep_p( r=distance_pp(p0, b1), o=b1, v1=[b1, p0], v2=v2, cw=a3, fn=a4 ),
765  h
766  ]
767 
768  //
769  // arc; blend corner (tangent arc through corner vertex)
770  //
771  : is_oneof( oper, ["arc_blend", "ab"] ) && (argc > 2) ?
772  [
773  polygon_arc_blend_p( p1=p0, p2=a1, p3=a2, r=a3, fn=a4 ),
774  h
775  ]
776 
777  //
778  // curve; Bézier (degree-n, de Casteljau)
779  //
780  : is_oneof( oper, ["bezier", "bz"] ) && (argc > 0) ?
781  let
782  (
783  ctrl_pts = a1,
784  o = a2,
785  fn = a3,
786  pre = defined_eonb_or( o, 0, true ),
787  pts = pre ? concat( [p0], ctrl_pts ) : ctrl_pts
788  )
789  [ polygon_bezier_p( c=pts, fn=fn ), h ]
790 
791  //
792  // curve; Catmull-Rom spline (through knots)
793  //
794  : is_oneof( oper, ["spline", "spl"] ) && (argc > 0) ?
795  let
796  (
797  knots = a1,
798  o = a2,
799  fn = a3,
800  pre = defined_eonb_or( o, 0, true ),
801  closed = defined_e_or ( o, 1, false ),
802  pts = pre ? concat( [p0], knots ) : knots
803  )
804  [ polygon_spline_p( c=pts, fn=fn, closed=closed ), h ]
805 
806  //
807  // sub-steps; repeat
808  //
809  : is_oneof( oper, ["repeat", "rpt"] ) && (argc > 0) ?
810  let
811  (
812  sub_s = a1,
813  n = defined_or( a2, 1 ),
814  result = _polygon_turtle_path_p_repeat( sub_s, n, p0, h, _p0_g )
815  )
816  result // already [pts, final_h]
817 
818  //
819  // sub-steps; repeat mirror-x (about horizontal axis)
820  //
821  : is_oneof( oper, ["repeat_mx", "rptmx"] ) && (argc > 0) ?
822  let
823  (
824  sub_s = a1,
825  axis = defined_or( a2, 0 ),
826  o = a3,
827  rev = defined_eonb_or( o, 0, true ),
828  kh = defined_e_or ( o, 1, true ),
829  ms = defined_e_or ( o, 2, true ),
830 
831  // forward pass — recover points and final heading
832  fwd_r = polygon_turtle_path_p( sub_s, p0, h, 1, _s_n, _p0_g ),
833  fwd = fwd_r[0],
834  fwd_h = fwd_r[1],
835  fwd_p0_end = is_empty(fwd) ? p0 : last(fwd),
836 
837  // mirror axis origin: p0.y + axis offset
838  ax_o = [ p0.x, p0.y + axis ],
839 
840  // start point for mirrored pass (reflected onto axis)
841  mir_p0 = ms ? mirror_p( [p0], [0,1], ax_o )[0]
842  : mirror_p( [fwd_p0_end], [0,1], ax_o )[0],
843 
844  // mirrored pass: run sub-steps from reflected start
845  mir_raw = polygon_turtle_path_p( sub_s, mir_p0, h, 1, _s_n, _p0_g ),
846 
847  // reflect output points back about the axis using mirror_p
848  mir = mirror_p( mir_raw[0], [0,1], ax_o ),
849  mir_out = rev ? list_reverse(mir) : mir,
850 
851  // heading carried back: forward final heading or entry heading
852  out_h = kh ? fwd_h : h
853  )
854  [ concat( fwd, mir_out ), out_h ]
855 
856  //
857  // sub-steps; repeat mirror-y (about vertical axis)
858  //
859  : is_oneof( oper, ["repeat_my", "rptmy"] ) && (argc > 0) ?
860  let
861  (
862  sub_s = a1,
863  axis = defined_or( a2, 0 ),
864  o = a3,
865  rev = defined_eonb_or( o, 0, true ),
866  kh = defined_e_or ( o, 1, true ),
867  ms = defined_e_or ( o, 2, true ),
868 
869  // forward pass — recover points and final heading
870  fwd_r = polygon_turtle_path_p( sub_s, p0, h, 1, _s_n, _p0_g ),
871  fwd = fwd_r[0],
872  fwd_h = fwd_r[1],
873  fwd_p0_end = is_empty(fwd) ? p0 : last(fwd),
874 
875  // mirror axis origin: p0.x + axis offset
876  ax_o = [ p0.x + axis, p0.y ],
877 
878  // start point for mirrored pass (reflected onto axis)
879  mir_p0 = ms ? mirror_p( [p0], [1,0], ax_o )[0]
880  : mirror_p( [fwd_p0_end], [1,0], ax_o )[0],
881 
882  // mirrored pass: run sub-steps from reflected start
883  mir_raw = polygon_turtle_path_p( sub_s, mir_p0, h, 1, _s_n, _p0_g ),
884 
885  // reflect output points back about the axis using mirror_p
886  mir = mirror_p( mir_raw[0], [1,0], ax_o ),
887  mir_out = rev ? list_reverse(mir) : mir,
888 
889  // heading carried back: forward final heading or entry heading
890  out_h = kh ? fwd_h : h
891  )
892  [ concat( fwd, mir_out ), out_h ]
893 
894  //
895  // sub-steps; transform (rotate then translate)
896  //
897  : is_oneof( oper, ["transform", "xfrm"] ) && (argc > 1) ?
898  let
899  (
900  sub_s = a1,
901  r = a2,
902  t = defined_or( a3, [0,0] ),
903  mn = a4,
904  o = a5,
905  upd_p0 = defined_eonb_or( o, 0, false ),
906  upd_h = defined_e_or ( o, 1, upd_p0 ),
907 
908  // evaluate sub-steps — recover points and final heading
909  sub_r = polygon_turtle_path_p( sub_s, p0, h, 1, _s_n, _p0_g ),
910  sub_h = sub_r[1],
911 
912  // apply mirror about p0, rotation about p0, then translation
913  xfmd = transform_p( c=sub_r[0], m=mn, a=r, t=t, o=p0 ),
914 
915  // heading: rotated sub-step final heading when updating, else entry heading
916  out_h = upd_h ? sub_h + r : h
917  )
918  [ xfmd, out_h ]
919 
920  //
921  // points
922  //
923  : is_oneof( oper, ["path_p", "pp"] ) && (argc > 0) ?
924  [ a1, h ]
925 
926  //
927  // assert error
928  //
929  : assert
930  (
931  false,
932  strl
933  ([
934  "ERROR: p0=", p0, ", h=", h, ", _s_n=", _s_n, ", _p0_g=", _p0_g,
935  ", operation=", oper, ", argv=", argv, ", argc=", argc
936  ])
937  ),
938 
939  //
940  // unpack this step's point list and evolved heading from ph
941  //
942  p = ph[0],
943  step_h = ph[1],
944 
945  //
946  // recursion next step updates
947  //
948 
949  next_s = tailn(s),
950 
951  // transform: advance only when upd_p0=true, else restore entry position
952  // repeat, repeat_mx, repeat_my: always advance to end of combined output
953  next_p0 = let ( end_p = is_empty(p) ? p0 : last(p) )
954  is_oneof( oper, ["transform", "xfrm"] ) ?
955  let ( upd_p0 = defined_eonb_or( a5, 0, false ) )
956  upd_p0 ? end_p : p0
957  : end_p,
958 
959  // transform: only update when upd_h=true, heading already encoded in step_h
960  next_h = is_oneof( oper, ["transform", "xfrm"] ) ?
961  let
962  (
963  upd_p0 = defined_eonb_or( a5, 0, false ),
964  upd_h = defined_e_or ( a5, 1, upd_p0 )
965  )
966  upd_h ? step_h : h
967  : step_h,
968 
969  next_s_n = _s_n + 1,
970  next_p0_g = defined_or( _p0_g, p0 ),
971 
972  //
973  // recurse or terminate — single recursive call, unpack once
974  //
975  term = len(s) == 1,
976 
977  rest = term ? [empty_lst, next_h]
978  : polygon_turtle_path_p( next_s, next_p0, next_h, 1, next_s_n, next_p0_g ),
979 
980  result = term ? p : concat( p, rest[0] ),
981  final_h = term ? next_h : rest[1]
982  )
983  m == 1 ? [result, final_h] : result;
984 
985 //! @}
986 //! @}
987 
988 //----------------------------------------------------------------------------//
989 // openscad-amu auxiliary scripts
990 //----------------------------------------------------------------------------//
991 
992 /*
993 BEGIN_SCOPE polygon_turtle_path_p;
994  BEGIN_OPENSCAD;
995  include <omdl-base.scad>;
996  include <tools/2d/turtle_path.scad>;
997 
998  $fn=36;
999 
1000  h1 = 7.5; h2 = 5; h3 = 7.5;
1001  w1 = 5; w2 = 20; w3 = 5;
1002  r1 = 5; r2 = 1/2; rr = 1;
1003 
1004  sm =
1005  [
1006  ["delta_y", h1],
1007  ["delta_x", w1],
1008  ["delta_y", h2],
1009  ["delta_xy", w3, h3],
1010  ["delta_x", w1+w2-w3*2],
1011  ["delta_xy", w3, -h3],
1012  ["goto_y", 0],
1013  ["goto_x", 0],
1014  ];
1015 
1016  // convert the step moves into coordinates
1017  pp = polygon_turtle_path_p( sm );
1018 
1019  // round all of the vertices
1020  rp = polygon_round_eve_all_p( pp, rr );
1021 
1022  difference()
1023  {
1024  polygon( rp );
1025 
1026  c = [w1+w2/2+r1/2, h1+h2/2];
1027  translate(c) circle(r=r1);
1028  for(x=[-1,1], y=[-1,1]) translate(c+[x,y]*r1) circle(r=r2);
1029  }
1030 
1031  // end_include
1032  END_OPENSCAD;
1033 
1034  BEGIN_MFSCRIPT;
1035  include --path "${INCLUDE_PATH}" {var_init,var_gen_png2eps}.mfs;
1036  table_unset_all sizes;
1037 
1038  images name "sizes" types "sxga";
1039  views name "views" views "top diag";
1040 
1041  variables set_opts_combine "sizes views";
1042  variables add_opts "--viewall --autocenter";
1043 
1044  include --path "${INCLUDE_PATH}" scr_make_mf.mfs;
1045  END_MFSCRIPT;
1046 END_SCOPE;
1047 */
1048 
1049 //----------------------------------------------------------------------------//
1050 // end of file
1051 //----------------------------------------------------------------------------//
origin2d
<point-2d> The origin point coordinate in 2d Euclidean space.
Definition: constants.scad:407
empty_lst
<list> A list with no values (the empty list).
Definition: constants.scad:304
function line_tp(l)
Return the terminal point of a line or vector.
function line2d_new(m=1, a=0, p1=origin2d, p2, v)
Construct a 2 dimensional directed line.
function distance_pp(p1, p2)
Compute the distance between two points.
function defined_eonb_or(v, i, d)
Returns the list element or scalar numeric or boolean value if defined, otherwise returns the default...
function defined_e_or(v, i, d)
Returns an element from an iterable if it exists, or a default value if not.
function last(v)
Return the last element of an iterable value.
function second(v)
Return the second element of an iterable value.
function first(v)
Return the first element of an iterable value.
function tailn(v, n=1)
Return a list containing all but the first n elements of an iterable value.
function is_oneof(v, cv)
Test if a value equals one of the comparison values.
function is_empty(v)
Test if an iterable value is empty.
function strl(v)
Convert a list of values to a concatenated string.
function defined_or(v, d)
Return given value, if defined, or a secondary value, if primary is not defined.
function mirror_p(c, m, o)
Mirror all coordinates about a plane or line defined by a normal vector.
function transform_p(c, m, a, av, center=false, o, t)
Apply an optional mirror, rotation, and translation to a list of 2D or 3D coordinates.
function polygon_arc_sweep_p(r=1, o=origin2d, v1=x_axis2d_uv, v2=x_axis2d_uv, fn, cw=true)
Compute coordinates of an arc with constant radius between two vectors in 2D.
function polygon_bezier_p(c, fn, cw=true)
Compute coordinates for a degree-n Bézier curve in 2D.
function polygon_spline_p(c, fn, closed=false, cw=true)
Compute coordinates for a Catmull-Rom spline through a list of knot points in 2D.
function polygon_line_wave_p(p1=origin2d, p2=x_axis2d_uv, p=1, a=1, w=1, m=0, t, fn)
Generate 2D coordinate points along a line with periodic waveform lateral displacement.
function polygon_arc_blend_p(p1, p2, p3, r=1, fn, cw=true)
Compute coordinates for a circular arc blend between two line segments in 2D.
function polygon_turtle_path_p(s, p0=origin2d, h=0, m=0, _s_n=0, _p0_g)
Interprets a turtle-style step language to generate coordinate points for polygon construction.