Skip to content

packages/core/src/lib/geometry/transform-mesh.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import {
2   constant,
3   add,
4   sub,
5   scale,
6   lerp,
7   type Pair,
8 } from './transform-math.js';
9 import {
10   snapshotGuide,
11   sampleGuide,
12   type PathGuide,
13 } from './transform-guide.js';
14 import {
15   make,
16   rectangle,
17   coordinate,
18   normalize,
19   mergePairs,
20   sourceDomain,
21   type PathRect,
22   type PathTransform,
23 } from './path-transforms.js';
24 
25 /**
26  * Source rectangle, normalized cell boundaries, and horizontal/vertical path guides for a mesh
27  * transform.
28  *
29  * @see {@link PathRect}
30  * @see {@link PathGuide}
31  * @see {@link meshTransform}
32  */
33 export interface MeshTransformOptions {
34   /** Source rectangle whose normalized cells are mapped through the guide mesh. See {@link PathRect}. */
35   readonly source: PathRect;
36   /**
37    * Strictly increasing normalized column boundaries, beginning at 0 and ending at 1. See
38    * {@link MeshTransformOptions}.
39    */
40   readonly columns: readonly number[];
41   /**
42    * Strictly increasing normalized row boundaries, beginning at 0 and ending at 1. See
43    * {@link MeshTransformOptions}.
44    */
45   readonly rows: readonly number[];
46   /** Boundary rows; each contains columns.length - 1 left-to-right edges. */
47   readonly horizontal: readonly (readonly PathGuide[])[];
48   /** Boundary columns; each contains rows.length - 1 top-to-bottom edges. */
49   readonly vertical: readonly (readonly PathGuide[])[];
50 }
51 /**
52  * Creates a mapping through a mesh bounded by horizontal and vertical path guides.
53  *
54  * @param options - Source rectangle and the four boundary guides used to deform it. See
55  * {@link MeshTransformOptions} .
56  * @returns A point mapping suitable for transformPath. See {@link PathTransform}.
57  *
58  * @see {@link MeshTransformOptions}
59  * @see {@link PathTransform}
60  */
61 export function meshTransform(options: MeshTransformOptions): PathTransform {
62   const source = rectangle(options.source);
63   const axis = (values: readonly number[]) => {
64     const result = Array.from(values, (n) => coordinate(n));
65     if (
66       result.length < 2 ||
67       result[0] !== 0 ||
68       result.at(-1) !== 1 ||
69       result.some((n, i) => i > 0 && n <= result[i - 1])
70     )
71       throw new RangeError(
72         'Mesh axes must strictly increase from zero to one.',
73       );
74     return result;
75   };
76   const columns = axis(options.columns),
77     rows = axis(options.rows);
78   if (
79     options.horizontal.length !== rows.length ||
80     options.vertical.length !== columns.length ||
81     options.horizontal.some((row) => row.length !== columns.length - 1) ||
82     options.vertical.some((col) => col.length !== rows.length - 1)
83   )
84     throw new RangeError('Mesh edge dimensions must match its axes.');
85   if ((columns.length - 1) * (rows.length - 1) > 4096)
86     throw new RangeError('Mesh exceeds 4096 cells.');
87   const horizontal = options.horizontal.map((row) => row.map(snapshotGuide));
88   const vertical = options.vertical.map((col) => col.map(snapshotGuide));
89   const ends = (guide: ReturnType<typeof snapshotGuide>) => {
90     const first = guide.spans[0],
91       last = guide.spans.at(-1)!;
92     const a = bendSpanPoint(first, 0),
93       b = bendSpanPoint(last, 1);
94     return guide.reverse ? [b, a] : [a, b];
95   };
96   const equal = (a: readonly number[], b: readonly number[]) => {
97     if (a[0] !== b[0] || a[1] !== b[1])
98       throw new RangeError('Mesh edges must share exactly matching endpoints.');
99   };
100   for (let row = 0; row < rows.length - 1; row++)
101     for (let col = 0; col < columns.length - 1; col++) {
102       const t = ends(horizontal[row][col]),
103         b = ends(horizontal[row + 1][col]),
104         l = ends(vertical[col][row]),
105         r = ends(vertical[col + 1][row]);
106       equal(t[0], l[0]);
107       equal(t[1], r[0]);
108       equal(b[0], l[1]);
109       equal(b[1], r[1]);
110     }
111   return make([
112     (accuracy) => {
113       const h = horizontal.map((row) =>
114           row.map((g) => sampleGuide(g, accuracy)),
115         ),
116         v = vertical.map((col) => col.map((g) => sampleGuide(g, accuracy)));
117       const error =
118         2 *
119         Math.max(
120           ...h.flat().map((g) => g.error),
121           ...v.flat().map((g) => g.error),
122         );
123       return {
124         error: () => error,
125         domain: (p, margin) => sourceDomain(p, source, margin),
126         evaluate: (input) => {
127           const [u, w] = normalize(input, source),
128             cells: Pair[] = [];
129           for (let row = 0; row < rows.length - 1; row++) {
130             if (w.v[1] < rows[row] || w.v[0] > rows[row + 1]) continue;
131             for (let col = 0; col < columns.length - 1; col++) {
132               if (u.v[1] < columns[col] || u.v[0] > columns[col + 1]) continue;
133               const localU = scale(
134                 sub(
135                   {
136                     ...u,
137                     v: [
138                       Math.max(u.v[0], columns[col]),
139                       Math.min(u.v[1], columns[col + 1]),
140                     ],
141                   },
142                   constant(columns[col]),
143                 ),
144                 1 / (columns[col + 1] - columns[col]),
145               );
146               const localV = scale(
147                 sub(
148                   {
149                     ...w,
150                     v: [
151                       Math.max(w.v[0], rows[row]),
152                       Math.min(w.v[1], rows[row + 1]),
153                     ],
154                   },
155                   constant(rows[row]),
156                 ),
157                 1 / (rows[row + 1] - rows[row]),
158               );
159               const top = h[row][col].evaluate(localU),
160                 bottom = h[row + 1][col].evaluate(localU),
161                 left = v[col][row].evaluate(localV),
162                 right = v[col + 1][row].evaluate(localV);
163               const a = ends(horizontal[row][col]),
164                 b = ends(horizontal[row + 1][col]);
165               cells.push(
166                 [0, 1].map((axis) =>
167                   sub(
168                     add(
169                       lerp(top[axis], bottom[axis], localV),
170                       lerp(left[axis], right[axis], localU),
171                     ),
172                     lerp(
173                       lerp(constant(a[0][axis]), constant(a[1][axis]), localU),
174                       lerp(constant(b[0][axis]), constant(b[1][axis]), localU),
175                       localV,
176                     ),
177                   ),
178                 ) as unknown as Pair,
179               );
180             }
181           }
182           if (!cells.length)
183             throw new RangeError('Point lies outside the mesh source.');
184           return cells.length === 1 ? cells[0] : mergePairs(cells);
185         },
186       };
187     },
188   ]);
189 }
190 import { bendSpanPoint } from './path-bend-spans.js';
191 

Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.