omdl  v1.0
OpenSCAD Mechanical Design Library
linear_algebra.scad
Go to the documentation of this file.
1 //! Linear algebra mathematical functions.
2 /***************************************************************************//**
3  \file
4  \author Roy Allen Sutton
5  \date 2015-2023, 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 (Linear Algebra)
31  \amu_define group_brief (Euclidean linear algebra functions.)
32 
33  \amu_include (include/amu/doxyg_init_pd_gds_ipg.amu)
34 *******************************************************************************/
35 
36 // auto-tests (add to test results page)
37 /***************************************************************************//**
38  \amu_include (include/amu/validate_log.amu)
39  \amu_include (include/amu/validate_results.amu)
40 *******************************************************************************/
41 
42 // group(s) begin (test summary and includes-required)
43 /***************************************************************************//**
44  \amu_include (include/amu/doxyg_define_in_parent_open.amu)
45  \amu_include (include/amu/validate_summary.amu)
46  \amu_include (include/amu/includes_required.amu)
47 *******************************************************************************/
48 
49 // member-wide reference definitions
50 /***************************************************************************//**
51  \amu_define group_references
52  (
53  )
54 *******************************************************************************/
55 
56 // member-wide documentation and conventions
57 /***************************************************************************//**
58  \addtogroup \amu_eval(${group})
59  \details
60  \anchor \amu_eval(${group})_conventions
61  \par Conventions
62 
63  The following conventions apply to all functions in this group.
64  - The first parameter is always \p c (a list of nd coordinate
65  points). All functions return a list of the same length and
66  dimensionality as \p c.
67  - Dimensionality \p d is auto-detected from the first element of
68  \p c using \c len(first(c)). Passing an empty list always returns
69  an empty list. Mixing point dimensions within \p c is undefined
70  behaviour.
71  - The origin parameter \p o is optional in all spatial functions
72  that have a fixed point (mirror, rotate, scale, shear, resize).
73  When \b undef (default), \p o is set to \p origin2d or \p origin3d
74  based on \p d. Exception: in the 3D Euler rotation branch of
75  rotate_p(), \p o is silently ignored — see rotate_p() for details.
76  When \p center is \b true, \p o is also ignored — see below.
77  - The optional \p center parameter (default \b false) is available on
78  all spatial functions that have a fixed point (rotate_p(),
79  transform_p(), shear_p(), scale_p(), resize_p()). When \b true,
80  the transformed result is passed through \c center_p() so that the
81  output bounding box is centered about the coordinate origin. Centering
82  is always the last step in the pipeline, applied after all other
83  transformations including translation. When \p center is \b true,
84  \p o is ignored.
85  - When a transformation parameter (\p v, \p a, \p m, \p s, \p av)
86  is \b undef, that transformation is a no-op and \p c is returned
87  unchanged. This allows safe use in composed pipelines without
88  conditional wrappers at the call site.
89  - The parameter name \p m is overloaded by role across the group:
90  a 4x4 homogeneous matrix in multmatrix_p(); a mirror-plane or
91  mirror-line normal vector in mirror_p() and transform_p(); and a
92  shear-factor list in shear_p() (where the parameter has been
93  renamed \p s). The type is stated explicitly in each function's
94  \p m or \p s parameter entry.
95  - The parameter name \p v is overloaded by role across the group:
96  a per-dimension translation list or scalar in translate_p(); a
97  per-dimension scale list or scalar in scale_p(); and a
98  per-dimension target-extent list or scalar in resize_p(). In
99  rotate_p() and transform_p() the rotation axis vector uses the
100  distinct name \p av to avoid ambiguity.
101  - All angles are in \b degrees, following OpenSCAD convention.
102  - No input validation is performed unless an explicit \c assert is
103  present. Wrong-dimension inputs, non-numeric values, or zero
104  vectors produce \b undef, \b nan, or \b inf without warning.
105 *******************************************************************************/
106 
107 //----------------------------------------------------------------------------//
108 // members
109 //----------------------------------------------------------------------------//
110 
111 //! Multiply all coordinates by a 4x4 transformation matrix in 3D.
112 /***************************************************************************//**
113  \param c <points-3d> A list of 3d coordinate points.
114 
115  \param m <matrix-4x4> A 4x4 transformation matrix. Only the first
116  three rows are used; the fourth row of the standard
117  homogeneous matrix `[0, 0, 0, 1]` is implicit and need
118  not be supplied, so a 3x4 matrix is sufficient.
119 
120  \returns <points-3d> A list of 3d coordinate points multiplied by
121  the transformation matrix.
122 
123  \details
124 
125  Applies a homogeneous transformation matrix \p m to a list of 3D
126  coordinate points. Each input point `[x, y, z]` is treated as a
127  homogeneous vector `[x, y, z, 1]` (i.e. `w = 1` is implicit — no
128  perspective divide is performed). The output is the 3-component
129  result of multiplying the upper 3×4 block of \p m by each
130  homogeneous input vector:
131 
132  ```
133  | m[0][0] m[0][1] m[0][2] m[0][3] | | x | | x' |
134  | m[1][0] m[1][1] m[1][2] m[1][3] | × | y | = | y' |
135  | m[2][0] m[2][1] m[2][2] m[2][3] | | z | | z' |
136  | 1 |
137  ```
138 
139  The fourth row of \p m (normally `[0, 0, 0, 1]` for affine
140  transforms) is never read, so a 3×4 matrix is sufficient.
141  Combined rotation and translation can be encoded as:
142  - Columns 0–2: the 3×3 rotation/scale/shear sub-matrix.
143  - Column 3: the translation vector `[tx, ty, tz]`.
144 
145  See [Wikipedia] and [multmatrix] for more information.
146 
147  \note Unlike the other spatial functions in this group, multmatrix_p()
148  takes no \p o origin parameter. Any origin offset for the
149  transformation is encoded directly in the fourth column of \p m
150  (i.e. `m[0][3]`, `m[1][3]`, `m[2][3]`), as is standard for
151  homogeneous transformation matrices.
152 
153  [Wikipedia]: https://en.wikipedia.org/wiki/Transformation_matrix
154  [multmatrix]: https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Transformations#multmatrix
155 *******************************************************************************/
156 function multmatrix_p
157 (
158  c,
159  m
160 ) = let
161  (
162  m11=m[0][0], m12=m[0][1], m13=m[0][2], m14=m[0][3],
163  m21=m[1][0], m22=m[1][1], m23=m[1][2], m24=m[1][3],
164  m31=m[2][0], m32=m[2][1], m33=m[2][2], m34=m[2][3]
165  )
166  [
167  for (ci=c)
168  let
169  (
170  x = ci[0], y = ci[1], z = ci[2]
171  )
172  [m11*x+m12*y+m13*z+m14, m21*x+m22*y+m23*z+m24, m31*x+m32*y+m33*z+m34]
173  ];
174 
175 //! Translate all coordinates dimensions.
176 /***************************************************************************//**
177  \param c <points-nd> A list of nd coordinate points.
178 
179  \param v <decimal-list-n | decimal> A list of translations for
180  each dimension, or a single decimal to translate
181  uniformly across all dimensions.
182 
183  \returns <points-nd> A list of translated coordinate points.
184 
185  \details
186 
187  When \p v is a scalar, the same translation is applied to every
188  dimension. When \p v is a list shorter than the point dimensionality,
189  missing elements default to zero. When \p v is \b undef the point
190  list is returned unchanged.
191 
192  \note Unlike the other spatial functions in this group, translate_p()
193  takes no \p o origin parameter. Translation has no fixed point —
194  every point moves by the same vector \p v regardless of position,
195  so an origin offset would have no effect.
196 
197  See [Wikipedia] for more information and [transformation matrix].
198 
199  [Wikipedia]: https://en.wikipedia.org/wiki/Translation_(geometry)
200  [transformation matrix]: https://en.wikipedia.org/wiki/Transformation_matrix
201 *******************************************************************************/
202 function translate_p
203 (
204  c,
205  v
206 ) = is_undef(v) ? c
207  : (len(c) == 0) ? c
208  : let
209  (
210  d = len(first(c)),
211  u = is_scalar(v) ? v : 0,
212 
213  w = [for (i=[0 : d-1]) defined_e_or(v, i, u)]
214  )
215  [for (ci=c) [for (di=[0 : d-1]) ci[di] + w[di]]];
216 
217 //! Mirror all coordinates about a plane or line defined by a normal vector.
218 /***************************************************************************//**
219  \param c <points-3d | points-2d> A list of 3d or 2d coordinate
220  points.
221 
222  \param m <vector-3d | vector-2d> The normal vector of the mirror
223  plane or line. A 2D vector `[nx, ny]` defines the normal
224  to the mirror line; a 3D vector `[nx, ny, nz]` defines
225  the normal to the mirror plane. Follows the same
226  convention as OpenSCAD's built-in `mirror()` module.
227 
228  \param o <point-3d | point-2d> The point through which the mirror
229  plane or line passes. When \b undef (default), the origin
230  is set automatically to \p origin2d or \p origin3d based
231  on the dimensionality of \p c.
232 
233  \returns <points-3d | points-2d> A list of mirrored coordinate
234  points.
235 
236  \details
237 
238  Reflects points about the plane or line defined by the normal
239  vector \p m passing through \p o using the standard reflection
240  matrix `M = I - 2*(n*nT)/|n|^2`. When \p m is a zero vector or
241  \b undef the point list is returned unchanged.
242 
243  See [Wikipedia] for more information on [reflection matrix].
244 
245  [Wikipedia]: https://en.wikipedia.org/wiki/Transformation_matrix
246  [reflection matrix]: https://en.wikipedia.org/wiki/Transformation_matrix#Reflection
247 *******************************************************************************/
248 function mirror_p
249 (
250  c,
251  m,
252  o
253 ) = is_undef(m) ? c
254  : (len(c) == 0) ? c
255  : let
256  (
257  d = len(first(c)),
258  eo = defined_or( o, d == 2 ? origin2d : origin3d ),
259  ox = eo[0], oy = eo[1],
260  nx = m[0], ny = m[1],
261  nz = (d == 3) ? m[2] : 0,
262  l2 = nx*nx + ny*ny + nz*nz
263  )
264  (l2 == 0) ? c
265  : (d == 2) ?
266  let
267  (
268  // 2D reflection matrix about line through o with normal [nx,ny]:
269  // M = I - 2*(n*nT)/|n|^2
270  f = 2 / l2,
271 
272  r11 = 1 - f*nx*nx,
273  r12 = - f*nx*ny,
274  r21 = - f*ny*nx,
275  r22 = 1 - f*ny*ny
276  )
277  [
278  for (ci=c)
279  let( dx = ci[0]-ox, dy = ci[1]-oy )
280  [
281  ox + r11*dx + r12*dy,
282  oy + r21*dx + r22*dy
283  ]
284  ]
285  : let
286  (
287  // 3D reflection matrix about plane through o with normal [nx,ny,nz]:
288  oz = eo[2],
289  f = 2 / l2,
290 
291  r11 = 1 - f*nx*nx,
292  r12 = - f*nx*ny,
293  r13 = - f*nx*nz,
294  r21 = - f*ny*nx,
295  r22 = 1 - f*ny*ny,
296  r23 = - f*ny*nz,
297  r31 = - f*nz*nx,
298  r32 = - f*nz*ny,
299  r33 = 1 - f*nz*nz
300  )
301  [
302  for (ci=c)
303  let( dx = ci[0]-ox, dy = ci[1]-oy, dz = ci[2]-oz )
304  [
305  ox + r11*dx + r12*dy + r13*dz,
306  oy + r21*dx + r22*dy + r23*dz,
307  oz + r31*dx + r32*dy + r33*dz
308  ]
309  ];
310 
311 //! Rotate all coordinates about one or more axes in 2D or 3D.
312 /***************************************************************************//**
313  \param c <points-3d | points-2d> A list of 3d or 2d coordinate
314  points.
315 
316  \param a <decimal-list-3 | decimal> The axis rotation angle; A
317  list [ax, ay, az] or a single decimal to specify az only.
318  When \b undef the point list is returned unchanged.
319 
320  \param av <vector-3d> An arbitrary axis for the rotation. When
321  specified, the rotation angle will be \p a or az about
322  the line \p av that passes through point \p o.
323 
324  \param center <boolean> When \b true, the rotated result is passed
325  through \c center_p() so that the output bounding box
326  is centered about the origin. When \b false (default),
327  the result is positioned as determined by \p o.
328 
329  \param o <point-3d | point-2d> The origin for the rotation. In 2D,
330  the center of rotation. In 3D, used only when \p av is
331  specified. When \b undef (default), the origin is set
332  automatically to \p origin2d or \p origin3d based on the
333  dimensionality of \p c. Ignored when \p center is \b true.
334 
335  \returns <points-3d | points-2d> A list of rotated coordinate
336  points.
337 
338  \details
339 
340  Three rotation branches are selected based on dimensionality and
341  the presence of \p av:
342  - \b 2D (\p d = 2): rotates about point \p o by angle \p az
343  (extracted as \c a[2], or \p a itself when scalar). \p o is
344  the center of rotation.
345  - \b 3D Euler (\p d = 3, \p av undef or \p a is a list): applies
346  extrinsic rotations in the order Z (`az`), Y (`ay`), X (`ax`),
347  equivalent to the standard OpenSCAD `rotate([ax, ay, az])`
348  convention. Missing angle components default to \b 0.
349  - \b 3D arbitrary axis (\p d = 3, \p av defined, \p a scalar):
350  rotates by \p az about the line through \p o with direction
351  \p av, using the Rodrigues rotation formula. \p av need not be
352  a unit vector; it is normalised internally.
353 
354  When \p center is \b true, \c center_p() is called on the rotated
355  result to center the output bounding box about the origin.
356 
357  \note In the 3D Euler branch the origin \p o is silently ignored
358  — rotation always occurs about the world origin. Only the 2D
359  branch and the 3D arbitrary-axis branch respect \p o.
360 
361  See [Wikipedia] for more information on [transformation matrix]
362  and [axis rotation].
363 
364  [Wikipedia]: https://en.wikipedia.org/wiki/Rotation_matrix
365  [transformation matrix]: https://en.wikipedia.org/wiki/Transformation_matrix
366  [axis rotation]: http://inside.mines.edu/fs_home/gmurray/ArbitraryAxisRotation
367 *******************************************************************************/
368 function rotate_p
369 (
370  c,
371  a,
372  av,
373  center = false,
374  o
375 ) = is_undef(a) ? c
376  : (len(c) == 0) ? c
377  : let
378  (
379  d = len(first(c)),
380  eo = defined_or( o, d == 2 ? origin2d : origin3d ),
381  az = defined_e_or(a, 2, is_scalar(a) ? a : 0),
382  cg = cos(az), sg = sin(az),
383 
384  rc = (d == 2) ?
385  let( ox = eo[0], oy = eo[1] )
386  [
387  for (ci=c)
388  let( dx = ci[0]-ox, dy = ci[1]-oy )
389  [
390  ox + cg*dx - sg*dy,
391  oy + sg*dx + cg*dy
392  ]
393  ]
394 
395  : (d != 3) ? c
396 
397  : (is_undef(av) || is_list(a)) ?
398  let
399  (
400  ax = defined_e_or(a, 0, 0),
401  ay = defined_e_or(a, 1, 0),
402 
403  ca = cos(ax), cb = cos(ay),
404  sa = sin(ax), sb = sin(ay),
405 
406  m11 = cb*cg,
407  m12 = cg*sa*sb-ca*sg,
408  m13 = ca*cg*sb+sa*sg,
409  m21 = cb*sg,
410  m22 = ca*cg+sa*sb*sg,
411  m23 = -cg*sa+ca*sb*sg,
412  m31 = -sb,
413  m32 = cb*sa,
414  m33 = ca*cb
415  )
416  multmatrix_p(c, [[m11,m12,m13,0], [m21,m22,m23,0], [m31,m32,m33,0]])
417 
418  : let
419  (
420  vx = av[0], vy = av[1], vz = av[2],
421  vx2 = vx*vx, vy2 = vy*vy, vz2 = vz*vz,
422  l2 = vx2 + vy2 + vz2
423  )
424  (l2 == 0) ? c
425 
426  : let
427  (
428  // normalise av to a unit vector; all matrix coefficients then
429  // follow the standard unit-vector Rodrigues rotation formula
430  ll = sqrt(l2),
431  ux = vx/ll, uy = vy/ll, uz = vz/ll,
432  ux2 = ux*ux, uy2 = uy*uy, uz2 = uz*uz,
433  ox = eo[0], oy = eo[1], oz = eo[2],
434  oc = 1 - cg,
435 
436  m11 = ux2+(uy2+uz2)*cg,
437  m12 = ux*uy*oc-uz*sg,
438  m13 = ux*uz*oc+uy*sg,
439  m14 = (ox*(uy2+uz2)-ux*(oy*uy+oz*uz))*oc+(oy*uz-oz*uy)*sg,
440  m21 = ux*uy*oc+uz*sg,
441  m22 = uy2+(ux2+uz2)*cg,
442  m23 = uy*uz*oc-ux*sg,
443  m24 = (oy*(ux2+uz2)-uy*(ox*ux+oz*uz))*oc+(oz*ux-ox*uz)*sg,
444  m31 = ux*uz*oc-uy*sg,
445  m32 = uy*uz*oc+ux*sg,
446  m33 = uz2+(ux2+uy2)*cg,
447  m34 = (oz*(ux2+uy2)-uz*(ox*ux+oy*uy))*oc+(ox*uy-oy*ux)*sg
448  )
449  multmatrix_p(c, [[m11,m12,m13,m14], [m21,m22,m23,m24], [m31,m32,m33,m34]])
450  )
451  center ? center_p(rc) : rc;
452 
453 //! Apply an optional mirror, rotation, and translation to a list of 2D or 3D coordinates.
454 /***************************************************************************//**
455  \param c <points-3d | points-2d> A list of 3d or 2d coordinate
456  points.
457 
458  \param m <vector-3d | vector-2d> The normal vector of the mirror
459  plane or line. The mirror is applied about the plane or
460  line passing through \p o with the given normal. When \b
461  undef (default), no mirror is applied.
462 
463  \param a <decimal-list-3 | decimal> The axis rotation angle; A
464  list [ax, ay, az] or a single decimal to specify az only.
465  When \b undef, no rotation is applied.
466 
467  \param av <vector-3d> An arbitrary axis for the rotation. When
468  specified, the rotation angle will be \p a or az about
469  the line \p av that passes through point \p o.
470 
471  \param center <boolean> When \b true, the fully transformed result is
472  passed through \c center_p() so that the output bounding
473  box is centered about the origin. Centering is applied
474  after mirror, rotation, and translation. When \b false
475  (default), the result is positioned as determined by \p o
476  and \p t.
477 
478  \param o <point-3d | point-2d> The origin for the rotation and
479  mirror. In 2D, the center of rotation. In 3D, used only
480  when \p av is specified. When \b undef (default), the
481  origin is set automatically to \p origin2d or \p origin3d
482  based on the dimensionality of \p c.
483 
484  \param t <point-3d | point-2d> A translation vector applied after
485  mirror and rotation. When \b undef (default), no
486  translation is applied. Translation is always applied
487  last before centering.
488 
489  \returns <points-3d | points-2d> A list of 3d or 2d transformed
490  coordinates. Operations are applied in order: mirror about
491  \p o, rotate about \p o, translate by \p t, then optionally
492  center via \c center_p(). In 3D Euler mode, rotation order is
493  extrinsic Z, Y, X (equivalent to OpenSCAD \c rotate([ax,ay,az])).
494 
495  \details
496 
497  Applies a transformation to a list of coordinate points by
498  composing mirror_p(), rotate_p(), and translate_p() in sequence.
499  The mirror \p m, when specified, reflects points about the plane or
500  line defined by the normal vector \p m passing through \p o.
501  Rotation \p a is then applied about \p o, followed by the optional
502  translation \p t. Any combination of the three operations may be
503  used independently — in particular, \p m and \p t may be specified
504  without \p a to mirror and then translate without rotation. When
505  \p center is \b true, \c center_p() is called on the final result
506  to center the output bounding box about the origin.
507 
508  The mirror normal \p m follows the same convention as OpenSCAD's
509  built-in `mirror()` module: a 2D vector `[nx, ny]` defines the
510  normal to the mirror line, and a 3D vector `[nx, ny, nz]` defines
511  the normal to the mirror plane.
512 
513  When \p a, \p m, \p t, and \p center are all \b undef or \b false
514  the point list is returned unchanged.
515 
516  See [Wikipedia] for more information on [transformation matrix],
517  [axis rotation], and [reflection matrix].
518 
519  [Wikipedia]: https://en.wikipedia.org/wiki/Rotation_matrix
520  [transformation matrix]: https://en.wikipedia.org/wiki/Transformation_matrix
521  [axis rotation]: http://inside.mines.edu/fs_home/gmurray/ArbitraryAxisRotation
522  [reflection matrix]: https://en.wikipedia.org/wiki/Transformation_matrix#Reflection
523 *******************************************************************************/
524 function transform_p
525 (
526  c,
527  m,
528  a,
529  av,
530  center = false,
531  o,
532  t
533 ) = let( r = translate_p( rotate_p( mirror_p( c, m, o ), a, av, o=o ), t ) )
534  center ? center_p(r) : r;
535 
536 //! Shear all coordinates in 2D or 3D.
537 /***************************************************************************//**
538  \param c <points-3d | points-2d> A list of 3d or 2d coordinate
539  points.
540 
541  \param s <decimal-list> The shear factors. In 2D, a list
542  `[sxy, syx]` where \c sxy shifts x proportional to y and
543  \c syx shifts y proportional to x. In 3D, a list `[sxy,
544  sxz, syx, syz, szx, szy]` following the standard shear
545  matrix convention.
546 
547  \param center <boolean> When \b true, the sheared result is passed
548  through \c center_p() so that the output bounding box
549  is centered about the origin. When \b false (default),
550  the result is positioned as determined by \p o.
551 
552  \param o <point-3d | point-2d> The origin about which shearing is
553  applied. When \b undef (default), the origin is set
554  automatically to \p origin2d or \p origin3d based on the
555  dimensionality of \p c. Shearing about an explicit origin
556  is equivalent to translating by `-o`, shearing, then
557  translating back by `+o`. Ignored when \p center is
558  \b true.
559 
560  \returns <points-3d | points-2d> A list of sheared coordinate
561  points.
562 
563  \details
564 
565  Applies a shear transformation to a list of coordinate points.
566  Shearing displaces each coordinate in one axis proportionally to
567  its position along another axis, leaving the shear origin fixed.
568 
569  The 2D shear matrix for factors `[sxy, syx]` is:
570  ```
571  | 1 sxy |
572  | syx 1 |
573  ```
574 
575  The 3D shear matrix for factors `[sxy, sxz, syx, syz, szx, szy]` is:
576  ```
577  | 1 sxy sxz |
578  | syx 1 syz |
579  | szx szy 1 |
580  ```
581 
582  Missing list elements default to \b 0 (no shear in that direction).
583  When \p center is \b true, \c center_p() is called on the sheared
584  result to center the output bounding box about the origin, and \p o
585  is ignored. When \p s is \b undef the point list is returned unchanged.
586 
587  See [Wikipedia] for more information on [shear mapping].
588 
589  [Wikipedia]: https://en.wikipedia.org/wiki/Shear_mapping
590  [shear mapping]: https://en.wikipedia.org/wiki/Shear_mapping
591 *******************************************************************************/
592 function shear_p
593 (
594  c,
595  s,
596  center = false,
597  o
598 ) = is_undef(s) ? c
599  : (len(c) == 0) ? c
600  : let
601  (
602  d = len(first(c)),
603  eo = defined_or( o, d == 2 ? origin2d : origin3d ),
604 
605  r = (d == 2) ?
606  let
607  (
608  ox = eo[0], oy = eo[1],
609  sxy = defined_e_or(s, 0, 0),
610  syx = defined_e_or(s, 1, 0)
611  )
612  [
613  for (ci=c)
614  let( dx = ci[0]-ox, dy = ci[1]-oy )
615  [
616  ox + dx + sxy*dy,
617  oy + syx*dx + dy
618  ]
619  ]
620  : (d == 3) ?
621  let
622  (
623  ox = eo[0], oy = eo[1], oz = eo[2],
624  sxy = defined_e_or(s, 0, 0),
625  sxz = defined_e_or(s, 1, 0),
626  syx = defined_e_or(s, 2, 0),
627  syz = defined_e_or(s, 3, 0),
628  szx = defined_e_or(s, 4, 0),
629  szy = defined_e_or(s, 5, 0)
630  )
631  [
632  for (ci=c)
633  let( dx = ci[0]-ox, dy = ci[1]-oy, dz = ci[2]-oz )
634  [
635  ox + dx + sxy*dy + sxz*dz,
636  oy + syx*dx + dy + syz*dz,
637  oz + szx*dx + szy*dy + dz
638  ]
639  ]
640  : c
641  )
642  center ? center_p(r) : r;
643 
644 //! Scale all coordinates dimensions.
645 /***************************************************************************//**
646  \param c <points-nd> A list of nd coordinate points.
647 
648  \param v <decimal-list-n | decimal> A list of scale factors for
649  each dimension, or a single decimal to scale uniformly
650  across all dimensions.
651 
652  \param center <boolean> When \b true, the scaled result is passed
653  through \c center_p() so that the output bounding box
654  is centered about the origin. When \b false (default),
655  the result is positioned as determined by \p o.
656 
657  \param o <point-nd> The origin about which scaling is applied.
658  When \b undef (default), the origin is set automatically
659  to \p origin2d or \p origin3d based on the dimensionality
660  of \p c. Scaling about an explicit origin is equivalent
661  to translating by `-o`, scaling, then translating back by
662  `+o`. Ignored when \p center is \b true.
663 
664  \returns <points-nd> A list of scaled coordinate points.
665 
666  \details
667 
668  When \p v is a scalar, the same scale factor is applied to every
669  dimension. When \p v is a list shorter than the point dimensionality,
670  missing elements default to \b 1 (no scaling). When \p o is the
671  origin the result is identical to scaling about the origin. When
672  \p center is \b true, \c center_p() is called on the scaled result
673  to center the output bounding box about the origin, and \p o is
674  ignored. When \p v is \b undef the point list is returned unchanged.
675 
676  See [Wikipedia] for more information on [transformation matrix].
677 
678  [Wikipedia]: https://en.wikipedia.org/wiki/Scaling_(geometry)
679  [transformation matrix]: https://en.wikipedia.org/wiki/Transformation_matrix
680 *******************************************************************************/
681 function scale_p
682 (
683  c,
684  v,
685  center = false,
686  o
687 ) = is_undef(v) ? c
688  : (len(c) == 0) ? c
689  : let
690  (
691  d = len(first(c)),
692  eo = defined_or( o, d == 2 ? origin2d : origin3d ),
693 
694  u = is_scalar(v) ? v : 1,
695  w = [for (i=[0 : d-1]) defined_e_or(v, i, u)],
696 
697  r = [for (ci=c) [for (di=[0 : d-1]) eo[di] + (ci[di] - eo[di]) * w[di]]]
698  )
699  center ? center_p(r) : r;
700 
701 //! Scale all coordinates dimensions proportionately to fit inside a region.
702 /***************************************************************************//**
703  \param c <points-nd> A list of nd coordinate points.
704 
705  \param v <decimal-list-n | decimal> A list of target extents
706  for each dimension. When a scalar, the same target
707  extent is applied to every dimension (the aspect
708  ratio of the input is not preserved). When a list
709  shorter than the point dimensionality, missing
710  elements default to \b 1.
711 
712  \param center <boolean> When \b true, the scaled result is centered
713  about the origin by passing the scaled point list
714  through \c center_p(). When \b false (default), the
715  bounding box minimum is placed at the origin before
716  scaling so that the result spans `[0, v[i]]` in each
717  dimension.
718 
719  \param o <point-nd> The origin to which the bounding box
720  minimum is aligned before scaling. When \b undef
721  (default), the origin is set automatically to
722  \p origin2d or \p origin3d based on the
723  dimensionality of \p c. Ignored when \p center is
724  \b true — the result is centered about the coordinate
725  origin regardless of \p o.
726 
727  \returns <points-nd> A list of proportionately scaled coordinate
728  points which exactly fit the region bounds \p v.
729 
730  \details
731 
732  Points are first translated so that the bounding box minimum of
733  each dimension is aligned to \p o, then scaled to fit \p v. This
734  ensures a consistent result regardless of where the input points
735  are positioned. When a dimension has zero extent (all points share
736  the same coordinate), that dimension is left unchanged to avoid
737  division by zero. When \p center is \b true, the scaled result is
738  passed through \c center_p() to center it about the coordinate
739  origin, and \p o is ignored. When \p v is \b undef the point list
740  is returned unchanged.
741 
742  \note The bounding box is computed by iterating \p c once per
743  dimension, giving O(n*d) total work where n = len(c) and
744  d = len(first(c)). When \p center is \b true an additional
745  O(n*d) pass is performed by \c center_p(). For typical 2D or
746  3D inputs this is negligible, but callers passing very large
747  point lists should be aware of the linear scaling with both
748  n and d.
749 
750  See [Wikipedia] for more information on [transformation matrix].
751 
752  [Wikipedia]: https://en.wikipedia.org/wiki/Scaling_(geometry)
753  [transformation matrix]: https://en.wikipedia.org/wiki/Transformation_matrix
754 *******************************************************************************/
755 function resize_p
756 (
757  c,
758  v,
759  center = false,
760  o
761 ) = is_undef(v) ? c
762  : (len(c) == 0) ? c
763  : let
764  (
765  d = len(first(c)),
766  eo = defined_or( o, d == 2 ? origin2d : origin3d ),
767 
768  u = is_scalar(v) ? v : 1,
769  w = [for (i=[0 : d-1]) defined_e_or(v, i, u)],
770 
771  // per-dimension [min, max] bounding values
772  bv = [for (i=[0 : d-1]) let (cv = [for (ci=c) ci[i]]) [min(cv), max(cv)]],
773 
774  // per-dimension extent; zero extent yields scale factor 1 (no change)
775  s = [for (i=[0 : d-1]) let (e = bv[i][1] - bv[i][0]) e == 0 ? 1 : w[i]/e],
776 
777  // scaled result aligned to eo; centering is delegated to center_p()
778  r = [for (ci=c) [for (di=[0 : d-1]) (ci[di] - bv[di][0]) * s[di] + eo[di]]]
779  )
780  center ? center_p(r) : r;
781 
782 //! Center all coordinates about the origin.
783 /***************************************************************************//**
784  \param c <points-nd> A list of nd coordinate points.
785 
786  \returns <points-nd> A list of coordinate points translated so that
787  the bounding box of the result is centered about the origin.
788 
789  \details
790 
791  Computes the per-dimension bounding box midpoint of \p c and
792  translates all points by the negation of that midpoint, so that the
793  output bounding box is symmetric about the origin in every dimension.
794  The shape, size, and relative positions of all points are preserved —
795  only the position of the point cloud as a whole changes.
796 
797  Centering is performed by a single bounding-box pass (O(n*d) where
798  n = len(c) and d = len(first(c))) followed by a single translation
799  pass, making the total work O(n*d). Passing an empty list returns
800  an empty list unchanged.
801 
802  \note This function is called directly by \c resize_p when its
803  \p center parameter is \b true. It may be composed freely
804  with any other spatial function in this group to post-center
805  the result of an arbitrary transformation pipeline.
806 
807  See [Wikipedia] for more information on [translation].
808 
809  [Wikipedia]: https://en.wikipedia.org/wiki/Translation_(geometry)
810  [translation]: https://en.wikipedia.org/wiki/Translation_(geometry)
811 *******************************************************************************/
812 function center_p
813 (
814  c
815 ) = (len(c) == 0) ? c
816  : let
817  (
818  d = len(first(c)),
819 
820  // per-dimension [min, max] bounding values
821  bv = [for (i=[0 : d-1]) let (cv = [for (ci=c) ci[i]]) [min(cv), max(cv)]],
822 
823  // per-dimension midpoint of bounding box
824  mid = [for (i=[0 : d-1]) (bv[i][0] + bv[i][1]) / 2]
825  )
826  [for (ci=c) [for (di=[0 : d-1]) ci[di] - mid[di]]];
827 
828 //! @}
829 //! @}
830 
831 //----------------------------------------------------------------------------//
832 // openscad-amu auxiliary scripts
833 //----------------------------------------------------------------------------//
834 
835 /*
836 BEGIN_SCOPE validate;
837  BEGIN_OPENSCAD;
838  include <omdl-base.scad>;
839  include <common/validation.scad>;
840 
841  echo( str("openscad version ", version()) );
842  for (i=[1:9]) echo( "not tested:" );
843 
844  // end_include
845  END_OPENSCAD;
846 
847  BEGIN_MFSCRIPT;
848  include --path "${INCLUDE_PATH}" {var_init,var_gen_term}.mfs;
849  include --path "${INCLUDE_PATH}" scr_make_mf.mfs;
850  END_MFSCRIPT;
851 END_SCOPE;
852 */
853 
854 //----------------------------------------------------------------------------//
855 // end of file
856 //----------------------------------------------------------------------------//
origin2d
<point-2d> The origin point coordinate in 2d Euclidean space.
Definition: constants.scad:407
origin3d
<point-3d> The origin point coordinate in 3-dimensional Euclidean space.
Definition: constants.scad:425
function defined_e_or(v, i, d)
Returns an element from an iterable if it exists, or a default value if not.
function first(v)
Return the first element of an iterable value.
function defined_or(v, d)
Return given value, if defined, or a secondary value, if primary is not defined.
function is_scalar(v)
Test if a value is a single non-iterable value.
function rotate_p(c, a, av, center=false, o)
Rotate all coordinates about one or more axes in 2D or 3D.
function mirror_p(c, m, o)
Mirror all coordinates about a plane or line defined by a normal vector.
function multmatrix_p(c, m)
Multiply all coordinates by a 4x4 transformation matrix in 3D.
function resize_p(c, v, center=false, o)
Scale all coordinates dimensions proportionately to fit inside a region.
function center_p(c)
Center all coordinates about the origin.
function shear_p(c, s, center=false, o)
Shear all coordinates in 2D or 3D.
function scale_p(c, v, center=false, o)
Scale all coordinates dimensions.
function translate_p(c, v)
Translate all coordinates dimensions.
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.