@code3d/core 0.0.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +824 -0
  3. package/THIRD_PARTY.md +28 -0
  4. package/bld/library/alignment-geometry.d.ts +50 -0
  5. package/bld/library/alignment-geometry.d.ts.map +1 -0
  6. package/bld/library/alignment-geometry.js +322 -0
  7. package/bld/library/alignment-geometry.js.map +1 -0
  8. package/bld/library/bound-solver.d.ts +33 -0
  9. package/bld/library/bound-solver.d.ts.map +1 -0
  10. package/bld/library/bound-solver.js +252 -0
  11. package/bld/library/bound-solver.js.map +1 -0
  12. package/bld/library/extrude.d.ts +8 -0
  13. package/bld/library/extrude.d.ts.map +1 -0
  14. package/bld/library/extrude.js +28 -0
  15. package/bld/library/extrude.js.map +1 -0
  16. package/bld/library/index.d.ts +7 -0
  17. package/bld/library/index.d.ts.map +1 -0
  18. package/bld/library/index.js +3 -0
  19. package/bld/library/index.js.map +1 -0
  20. package/bld/library/kernel-artifact-codec.d.ts +3 -0
  21. package/bld/library/kernel-artifact-codec.d.ts.map +1 -0
  22. package/bld/library/kernel-artifact-codec.js +148 -0
  23. package/bld/library/kernel-artifact-codec.js.map +1 -0
  24. package/bld/library/kernel-cache.d.ts +63 -0
  25. package/bld/library/kernel-cache.d.ts.map +1 -0
  26. package/bld/library/kernel-cache.js +245 -0
  27. package/bld/library/kernel-cache.js.map +1 -0
  28. package/bld/library/kernel-shapes.d.ts +20 -0
  29. package/bld/library/kernel-shapes.d.ts.map +1 -0
  30. package/bld/library/kernel-shapes.js +108 -0
  31. package/bld/library/kernel-shapes.js.map +1 -0
  32. package/bld/library/loft.d.ts +7 -0
  33. package/bld/library/loft.d.ts.map +1 -0
  34. package/bld/library/loft.js +161 -0
  35. package/bld/library/loft.js.map +1 -0
  36. package/bld/library/material.d.ts +12 -0
  37. package/bld/library/material.d.ts.map +1 -0
  38. package/bld/library/material.js +176 -0
  39. package/bld/library/material.js.map +1 -0
  40. package/bld/library/model-color.d.ts +8 -0
  41. package/bld/library/model-color.d.ts.map +1 -0
  42. package/bld/library/model-color.js +36 -0
  43. package/bld/library/model-color.js.map +1 -0
  44. package/bld/library/open-cascade-error.d.ts +2 -0
  45. package/bld/library/open-cascade-error.d.ts.map +1 -0
  46. package/bld/library/open-cascade-error.js +22 -0
  47. package/bld/library/open-cascade-error.js.map +1 -0
  48. package/bld/library/relation-solver.d.ts +29 -0
  49. package/bld/library/relation-solver.d.ts.map +1 -0
  50. package/bld/library/relation-solver.js +258 -0
  51. package/bld/library/relation-solver.js.map +1 -0
  52. package/bld/library/replicad.d.ts +17 -0
  53. package/bld/library/replicad.d.ts.map +1 -0
  54. package/bld/library/replicad.js +24 -0
  55. package/bld/library/replicad.js.map +1 -0
  56. package/bld/library/retained-memory.d.ts +7 -0
  57. package/bld/library/retained-memory.d.ts.map +1 -0
  58. package/bld/library/retained-memory.js +27 -0
  59. package/bld/library/retained-memory.js.map +1 -0
  60. package/bld/library/runtime.d.ts +775 -0
  61. package/bld/library/runtime.d.ts.map +1 -0
  62. package/bld/library/runtime.js +2954 -0
  63. package/bld/library/runtime.js.map +1 -0
  64. package/bld/library/shell.d.ts +7 -0
  65. package/bld/library/shell.d.ts.map +1 -0
  66. package/bld/library/shell.js +221 -0
  67. package/bld/library/shell.js.map +1 -0
  68. package/bld/library/sketch-curve-intersections.d.ts +13 -0
  69. package/bld/library/sketch-curve-intersections.d.ts.map +1 -0
  70. package/bld/library/sketch-curve-intersections.js +117 -0
  71. package/bld/library/sketch-curve-intersections.js.map +1 -0
  72. package/bld/library/sketch-curves.d.ts +31 -0
  73. package/bld/library/sketch-curves.d.ts.map +1 -0
  74. package/bld/library/sketch-curves.js +89 -0
  75. package/bld/library/sketch-curves.js.map +1 -0
  76. package/bld/library/sketch-drag-rules.d.ts +35 -0
  77. package/bld/library/sketch-drag-rules.d.ts.map +1 -0
  78. package/bld/library/sketch-drag-rules.js +522 -0
  79. package/bld/library/sketch-drag-rules.js.map +1 -0
  80. package/bld/library/sketch-face.d.ts +4 -0
  81. package/bld/library/sketch-face.d.ts.map +1 -0
  82. package/bld/library/sketch-face.js +72 -0
  83. package/bld/library/sketch-face.js.map +1 -0
  84. package/bld/library/sketch-incidence.d.ts +26 -0
  85. package/bld/library/sketch-incidence.d.ts.map +1 -0
  86. package/bld/library/sketch-incidence.js +114 -0
  87. package/bld/library/sketch-incidence.js.map +1 -0
  88. package/bld/library/sketch-precision.d.ts +19 -0
  89. package/bld/library/sketch-precision.d.ts.map +1 -0
  90. package/bld/library/sketch-precision.js +55 -0
  91. package/bld/library/sketch-precision.js.map +1 -0
  92. package/bld/library/sketch-regions.d.ts +12 -0
  93. package/bld/library/sketch-regions.d.ts.map +1 -0
  94. package/bld/library/sketch-regions.js +193 -0
  95. package/bld/library/sketch-regions.js.map +1 -0
  96. package/bld/library/sketch-solver.d.ts +97 -0
  97. package/bld/library/sketch-solver.d.ts.map +1 -0
  98. package/bld/library/sketch-solver.js +578 -0
  99. package/bld/library/sketch-solver.js.map +1 -0
  100. package/bld/library/sketch.d.ts +142 -0
  101. package/bld/library/sketch.d.ts.map +1 -0
  102. package/bld/library/sketch.js +650 -0
  103. package/bld/library/sketch.js.map +1 -0
  104. package/bld/library/spatial.d.ts +28 -0
  105. package/bld/library/spatial.d.ts.map +1 -0
  106. package/bld/library/spatial.js +195 -0
  107. package/bld/library/spatial.js.map +1 -0
  108. package/bld/library/three.d.ts +3 -0
  109. package/bld/library/three.d.ts.map +1 -0
  110. package/bld/library/three.js +3 -0
  111. package/bld/library/three.js.map +1 -0
  112. package/bld/library/topology-id.d.ts +24 -0
  113. package/bld/library/topology-id.d.ts.map +1 -0
  114. package/bld/library/topology-id.js +67 -0
  115. package/bld/library/topology-id.js.map +1 -0
  116. package/bld/library/topology-inspection.d.ts +64 -0
  117. package/bld/library/topology-inspection.d.ts.map +1 -0
  118. package/bld/library/topology-inspection.js +267 -0
  119. package/bld/library/topology-inspection.js.map +1 -0
  120. package/bld/library/topology.d.ts +94 -0
  121. package/bld/library/topology.d.ts.map +1 -0
  122. package/bld/library/topology.js +455 -0
  123. package/bld/library/topology.js.map +1 -0
  124. package/bld/node/index.d.ts +2 -0
  125. package/bld/node/index.d.ts.map +1 -0
  126. package/bld/node/index.js +14 -0
  127. package/bld/node/index.js.map +1 -0
  128. package/bld/node/replicad.d.ts +3 -0
  129. package/bld/node/replicad.d.ts.map +1 -0
  130. package/bld/node/replicad.js +3 -0
  131. package/bld/node/replicad.js.map +1 -0
  132. package/bld/tooling/index.d.ts +26 -0
  133. package/bld/tooling/index.d.ts.map +1 -0
  134. package/bld/tooling/index.js +19 -0
  135. package/bld/tooling/index.js.map +1 -0
  136. package/package.json +53 -0
  137. package/src/library/alignment-geometry.ts +431 -0
  138. package/src/library/bound-solver.ts +410 -0
  139. package/src/library/extrude.ts +38 -0
  140. package/src/library/index.ts +88 -0
  141. package/src/library/kernel-artifact-codec.ts +166 -0
  142. package/src/library/kernel-cache.ts +337 -0
  143. package/src/library/kernel-shapes.ts +135 -0
  144. package/src/library/loft.ts +196 -0
  145. package/src/library/material.ts +228 -0
  146. package/src/library/model-color.ts +47 -0
  147. package/src/library/open-cascade-error.ts +26 -0
  148. package/src/library/relation-solver.ts +479 -0
  149. package/src/library/replicad.ts +40 -0
  150. package/src/library/retained-memory.ts +23 -0
  151. package/src/library/runtime.ts +5146 -0
  152. package/src/library/shell.ts +281 -0
  153. package/src/library/sketch-curve-intersections.ts +154 -0
  154. package/src/library/sketch-curves.ts +161 -0
  155. package/src/library/sketch-drag-rules.ts +767 -0
  156. package/src/library/sketch-face.ts +89 -0
  157. package/src/library/sketch-incidence.ts +204 -0
  158. package/src/library/sketch-precision.ts +81 -0
  159. package/src/library/sketch-regions.ts +271 -0
  160. package/src/library/sketch-solver.ts +973 -0
  161. package/src/library/sketch.ts +977 -0
  162. package/src/library/spatial.ts +254 -0
  163. package/src/library/three.ts +2 -0
  164. package/src/library/topology-id.ts +81 -0
  165. package/src/library/topology-inspection.ts +368 -0
  166. package/src/library/topology.ts +776 -0
  167. package/src/library/tsconfig.json +11 -0
  168. package/src/node/index.ts +20 -0
  169. package/src/node/replicad.ts +3 -0
  170. package/src/node/tsconfig.json +16 -0
  171. package/src/tooling/index.ts +160 -0
  172. package/src/tooling/tsconfig.json +15 -0
@@ -0,0 +1,977 @@
1
+ import {
2
+ solveSketchProblem,
3
+ type SketchSolveProblem,
4
+ type SketchSolveConstraint,
5
+ } from './sketch-solver.js';
6
+ import {solveSketchDrag} from './sketch-drag-rules.js';
7
+ import {sketchRegions} from './sketch-regions.js';
8
+ import {
9
+ sketchFaceModel,
10
+ disposeModelObjects,
11
+ isModelObject,
12
+ type FaceModel,
13
+ } from './runtime.js';
14
+ import {
15
+ sketchIncidences,
16
+ sketchIncidencePoints,
17
+ sketchIncidenceCurve,
18
+ isPointOnSketchCurve,
19
+ type SketchIncidenceGeometry,
20
+ } from './sketch-incidence.js';
21
+
22
+ /** Current coordinates in a sketch's local two-dimensional plane. */
23
+ export type SketchPosition = readonly [x: number, y: number];
24
+ export type SketchArcDirection = 'cw' | 'ccw';
25
+
26
+ export type SketchEntry =
27
+ | readonly [
28
+ kind: 'point',
29
+ id: number,
30
+ data: SketchPosition | number | SketchPoint,
31
+ ]
32
+ | readonly [
33
+ kind: 'line',
34
+ id: number,
35
+ points: readonly [start: number | SketchPoint, end: number | SketchPoint],
36
+ ]
37
+ | readonly [
38
+ kind: 'circle',
39
+ id: number,
40
+ data: readonly [center: number | SketchPoint, radius: number],
41
+ ]
42
+ | readonly [
43
+ kind: 'arc',
44
+ id: number,
45
+ data: readonly [
46
+ center: number | SketchPoint,
47
+ radius: number,
48
+ start: number | SketchPoint,
49
+ end: number | SketchPoint,
50
+ direction: SketchArcDirection,
51
+ ],
52
+ ];
53
+
54
+ /** A point reference carries its defining layer, not just a numeric ID. */
55
+ export interface SketchPoint {
56
+ readonly sketch: Sketch;
57
+ readonly id: number;
58
+ }
59
+
60
+ /** Constraints are separate from geometry and do not have persistent IDs. */
61
+ export type SketchConstraint<P = number | SketchPoint> =
62
+ | readonly [kind: 'fixed', point: P]
63
+ | readonly [kind: 'horizontal' | 'vertical', line: number]
64
+ | readonly [
65
+ kind: 'parallel' | 'perpendicular',
66
+ lines: readonly [number, number],
67
+ ]
68
+ /** Signed rotation from the first line's authored direction to the second, in degrees. */
69
+ | readonly [kind: 'angle', lines: readonly [number, number], value: number]
70
+ | readonly [kind: 'coincident', points: readonly [P, P]]
71
+ | readonly [
72
+ kind: 'midpoint',
73
+ points: readonly [midpoint: P, start: P, end: P],
74
+ ]
75
+ | readonly [
76
+ kind: 'length' | 'angle' | 'radius' | 'sweep',
77
+ curve: number,
78
+ value: number,
79
+ ]
80
+ | readonly [kind: 'x' | 'y', point: P, value: number];
81
+
82
+ export type SketchOptions = Readonly<{
83
+ constraints?: readonly SketchConstraint[];
84
+ }>;
85
+
86
+ /** An immutable sketch definition, independent of B-Rep construction. */
87
+ export interface Sketch {
88
+ /** References a point defined in this layer. */
89
+ point(id: number): SketchPoint;
90
+ /** Adds a local layer while retaining the upstream sketch as read-only input. */
91
+ derive(entries?: readonly SketchEntry[], options?: SketchOptions): Sketch;
92
+ /** Creates the only closed region, including holes. Requires exactly one face. */
93
+ face(): FaceModel;
94
+ /** Creates all closed regions, including upstream boundaries. Returns an ordinary array. */
95
+ faces(): readonly FaceModel[];
96
+ }
97
+
98
+ type Definition = Readonly<{
99
+ base?: Sketch;
100
+ /** Used only to identify the authored argument during source tracing. */
101
+ input?: readonly SketchEntry[];
102
+ inputOptions?: SketchOptions;
103
+ entries: readonly SketchEntry[];
104
+ constraints: readonly SketchConstraint[];
105
+ points: ReadonlyMap<number, SketchPosition>;
106
+ radii: ReadonlyMap<number, number>;
107
+ degreesOfFreedom: number;
108
+ redundant: readonly number[];
109
+ }>;
110
+
111
+ const definitions = new WeakMap<Sketch, Definition>();
112
+ const references = new WeakSet<SketchPoint>();
113
+
114
+ class SketchValue implements Sketch {
115
+ constructor(
116
+ entries?: readonly SketchEntry[],
117
+ options?: SketchOptions,
118
+ base?: Sketch,
119
+ ) {
120
+ const ids = new Set<number>();
121
+ const points = new Map<number, SketchPosition>();
122
+ const copied = (entries ?? []).map<SketchEntry>(entry => {
123
+ const [kind, id, data] = entry;
124
+ if (!Number.isSafeInteger(id) || id < 1)
125
+ throw new Error('Sketch entity IDs must be positive safe integers.');
126
+ if (ids.has(id)) throw new Error(`Duplicate sketch entity ID ${id}.`);
127
+ ids.add(id);
128
+ if (kind === 'point') {
129
+ if (!Array.isArray(data)) return ['point', id, data];
130
+ if (data.length !== 2 || !data.every(Number.isFinite))
131
+ throw new Error(
132
+ `Sketch point ${id} requires two finite coordinates.`,
133
+ );
134
+ const position: SketchPosition = [data[0], data[1]];
135
+ points.set(id, position);
136
+ return ['point', id, position];
137
+ }
138
+ if (kind === 'circle' || kind === 'arc') {
139
+ if (!Number.isFinite(data[1]) || data[1] <= 0)
140
+ throw new Error(
141
+ `Sketch ${kind} ${id} requires a positive finite radius.`,
142
+ );
143
+ }
144
+ if (kind === 'circle') {
145
+ if (data.length !== 2)
146
+ throw new Error(`Sketch circle ${id} requires center and radius.`);
147
+ return ['circle', id, [data[0], data[1]]];
148
+ }
149
+ if (kind === 'arc') {
150
+ if (data.length !== 5 || (data[4] !== 'cw' && data[4] !== 'ccw'))
151
+ throw new Error(
152
+ `Sketch arc ${id} requires center, radius, start, end and cw/ccw direction.`,
153
+ );
154
+ return ['arc', id, [data[0], data[1], data[2], data[3], data[4]]];
155
+ }
156
+ if (kind !== 'line' || data.length !== 2)
157
+ throw new Error(`Invalid sketch entity ${id}.`);
158
+ return ['line', id, [data[0], data[1]]];
159
+ });
160
+ const ancestors = new Set<Sketch>();
161
+ for (
162
+ let ancestor = base;
163
+ ancestor;
164
+ ancestor = definitions.get(ancestor)?.base
165
+ )
166
+ ancestors.add(ancestor);
167
+ for (const [kind, id, data] of copied) {
168
+ if (kind === 'point' && Array.isArray(data)) continue;
169
+ const refs =
170
+ kind === 'point'
171
+ ? [data as number | SketchPoint]
172
+ : kind === 'circle'
173
+ ? [data[0]]
174
+ : kind === 'arc'
175
+ ? [data[0], data[2], data[3]]
176
+ : data;
177
+ for (const ref of refs) {
178
+ if (typeof ref === 'number') {
179
+ if (!copied.some(e => e[0] === 'point' && e[1] === ref))
180
+ throw new Error(
181
+ `Sketch ${kind} ${id} references missing local point ${ref}.`,
182
+ );
183
+ } else if (!references.has(ref) || !ancestors.has(ref.sketch)) {
184
+ throw new Error(
185
+ `Sketch ${kind} ${id} must reference a local or upstream point.`,
186
+ );
187
+ }
188
+ }
189
+ }
190
+ const resolving = new Set<number>();
191
+ const resolve = (id: number): SketchPosition => {
192
+ const position = points.get(id);
193
+ if (position) return position;
194
+ if (resolving.has(id))
195
+ throw new Error(`Cyclic sketch point alias at ${id}.`);
196
+ resolving.add(id);
197
+ const entry = copied.find(e => e[0] === 'point' && e[1] === id)!;
198
+ const ref = entry[2] as number | SketchPoint;
199
+ const resolved =
200
+ typeof ref === 'number'
201
+ ? resolve(ref)
202
+ : definitions.get(ref.sketch)!.points.get(ref.id)!;
203
+ resolving.delete(id);
204
+ points.set(id, resolved);
205
+ return resolved;
206
+ };
207
+ for (const [kind, id] of copied) if (kind === 'point') resolve(id);
208
+ const pointRef = (ref: number | SketchPoint) => {
209
+ if (
210
+ typeof ref === 'number'
211
+ ? !points.has(ref)
212
+ : !references.has(ref) || !ancestors.has(ref.sketch)
213
+ )
214
+ throw new Error(
215
+ 'Sketch constraints must reference a local or upstream point.',
216
+ );
217
+ };
218
+ const curveRef = (id: number, kind: 'line' | 'arc' | 'circular curve') => {
219
+ if (
220
+ !copied.some(
221
+ e =>
222
+ (kind === 'circular curve'
223
+ ? e[0] === 'circle' || e[0] === 'arc'
224
+ : e[0] === kind) && e[1] === id,
225
+ )
226
+ )
227
+ throw new Error(
228
+ `Sketch constraint references missing local ${kind} ${id}.`,
229
+ );
230
+ };
231
+ const constraints = (options?.constraints ?? []).map<SketchConstraint>(
232
+ ([kind, data, value]) => {
233
+ if (kind === 'fixed') {
234
+ pointRef(data);
235
+ return [kind, data];
236
+ }
237
+ if (kind === 'horizontal' || kind === 'vertical') {
238
+ curveRef(data, 'line');
239
+ return [kind, data];
240
+ }
241
+ if (
242
+ kind === 'parallel' ||
243
+ kind === 'perpendicular' ||
244
+ (kind === 'angle' && typeof data !== 'number')
245
+ ) {
246
+ if (!Array.isArray(data) || data.length !== 2 || data[0] === data[1])
247
+ throw new Error(
248
+ `Sketch ${kind} constraint requires two distinct local lines.`,
249
+ );
250
+ data.forEach(id => curveRef(id, 'line'));
251
+ if (kind === 'angle') {
252
+ if (!Number.isFinite(value))
253
+ throw new Error(
254
+ 'Sketch angle constraint requires a finite value.',
255
+ );
256
+ return [kind, [data[0], data[1]], value];
257
+ }
258
+ return [kind, [data[0], data[1]]];
259
+ }
260
+ if (kind === 'coincident') {
261
+ data.forEach(pointRef);
262
+ return [kind, [data[0], data[1]]];
263
+ }
264
+ if (kind === 'midpoint') {
265
+ if (data.length !== 3)
266
+ throw new Error(
267
+ 'Sketch midpoint constraint requires three points.',
268
+ );
269
+ data.forEach(pointRef);
270
+ return [kind, [data[0], data[1], data[2]]];
271
+ }
272
+ if (
273
+ kind === 'x' ||
274
+ kind === 'y' ||
275
+ kind === 'length' ||
276
+ kind === 'angle' ||
277
+ kind === 'radius' ||
278
+ kind === 'sweep'
279
+ ) {
280
+ if (kind === 'x' || kind === 'y') pointRef(data);
281
+ else
282
+ curveRef(
283
+ data as number,
284
+ kind === 'radius'
285
+ ? 'circular curve'
286
+ : kind === 'sweep'
287
+ ? 'arc'
288
+ : 'line',
289
+ );
290
+ if (kind === 'sweep' && !(value > 0 && value < 360))
291
+ throw new Error(
292
+ 'Sketch sweep constraint requires degrees strictly between 0 and 360.',
293
+ );
294
+ const positive = kind === 'length' || kind === 'radius';
295
+ if (!Number.isFinite(value) || (positive && value <= 0))
296
+ throw new Error(
297
+ `Sketch ${kind} constraint requires ${positive ? 'a positive' : 'a'} finite value.`,
298
+ );
299
+ return [kind, data, value] as SketchConstraint;
300
+ }
301
+ throw new Error(`Unknown sketch constraint ${kind}.`);
302
+ },
303
+ );
304
+ const local = 'local';
305
+ const layers = new Map<Sketch, string>();
306
+ const identity = (value: Sketch) => {
307
+ let id = layers.get(value);
308
+ if (!id) layers.set(value, (id = `upstream:${layers.size}`));
309
+ return id;
310
+ };
311
+ const snapshots = [...ancestors]
312
+ .reverse()
313
+ .map(value => snapshotSketch(value, identity));
314
+ const unresolved: SketchSnapshot = {
315
+ id: local,
316
+ entities: snapshotEntries(copied, local, identity, points),
317
+ constraints: snapshotConstraints(constraints, local, identity),
318
+ degreesOfFreedom: 0,
319
+ redundant: [],
320
+ };
321
+ const solved = solveSketchSnapshot([...snapshots, unresolved]);
322
+ const radii = new Map<number, number>();
323
+ for (const entity of solved.entities)
324
+ if (entity.kind === 'point') points.set(entity.id, entity.position);
325
+ else if (entity.kind === 'circle' || entity.kind === 'arc')
326
+ radii.set(entity.id, entity.radius);
327
+ definitions.set(this, {
328
+ base,
329
+ input: entries,
330
+ inputOptions: options,
331
+ entries: copied,
332
+ constraints,
333
+ points,
334
+ radii,
335
+ degreesOfFreedom: solved.degreesOfFreedom,
336
+ redundant: solved.redundant,
337
+ });
338
+ }
339
+
340
+ point(id: number): SketchPoint {
341
+ if (!definitions.get(this)!.points.has(id))
342
+ throw new Error(`Unknown local sketch point ${id}.`);
343
+ const ref = {sketch: this, id};
344
+ references.add(ref);
345
+ return ref;
346
+ }
347
+
348
+ derive(entries?: readonly SketchEntry[], options?: SketchOptions): Sketch {
349
+ return new SketchValue(entries, options, this);
350
+ }
351
+
352
+ face(): FaceModel {
353
+ const regions = sketchRegions(this.layers());
354
+ if (regions.length !== 1)
355
+ throw new Error(
356
+ `sketch.face() requires exactly one closed region; found ${regions.length}. Use faces() for multiple regions.`,
357
+ );
358
+ return sketchFaceModel(regions[0]);
359
+ }
360
+
361
+ faces(): readonly FaceModel[] {
362
+ const faces: FaceModel[] = [];
363
+ try {
364
+ for (const region of sketchRegions(this.layers()))
365
+ faces.push(sketchFaceModel(region));
366
+ return faces;
367
+ } catch (error) {
368
+ disposeModelObjects(
369
+ faces.flatMap(face => (isModelObject(face) ? [face] : [])),
370
+ );
371
+ throw error;
372
+ }
373
+ }
374
+
375
+ private layers(): readonly SketchSnapshot[] {
376
+ const values: Sketch[] = [];
377
+ for (
378
+ let value: Sketch | undefined = this;
379
+ value;
380
+ value = definitions.get(value)!.base
381
+ )
382
+ values.unshift(value);
383
+ return values.map(value =>
384
+ snapshotSketch(value, s => String(values.indexOf(s))),
385
+ );
386
+ }
387
+ }
388
+
389
+ /** Defines a sketch using [kind, ID, data] tuples. IDs belong to this layer. */
390
+ export function sketch(
391
+ entries?: readonly SketchEntry[],
392
+ options?: SketchOptions,
393
+ ): Sketch {
394
+ return new SketchValue(entries, options);
395
+ }
396
+
397
+ export function isSketch(value: unknown): value is Sketch {
398
+ return value instanceof SketchValue;
399
+ }
400
+
401
+ export function sketchDefinition(value: Sketch): Definition {
402
+ return definitions.get(value)!;
403
+ }
404
+
405
+ export type SketchPointSnapshot = Readonly<{
406
+ kind: 'point';
407
+ id: number;
408
+ position: SketchPosition;
409
+ /** Same geometric point, retaining this authored ID for references. */
410
+ alias?: SketchPointAddress;
411
+ }>;
412
+
413
+ export type SketchPointAddress = Readonly<{layer: string; id: number}>;
414
+
415
+ export type SketchLineSnapshot = Readonly<{
416
+ kind: 'line';
417
+ id: number;
418
+ points: readonly [SketchPointAddress, SketchPointAddress];
419
+ }>;
420
+
421
+ export type SketchCircleSnapshot = Readonly<{
422
+ kind: 'circle';
423
+ id: number;
424
+ center: SketchPointAddress;
425
+ radius: number;
426
+ }>;
427
+
428
+ export type SketchArcSnapshot = Readonly<{
429
+ kind: 'arc';
430
+ id: number;
431
+ center: SketchPointAddress;
432
+ radius: number;
433
+ points: readonly [SketchPointAddress, SketchPointAddress];
434
+ direction: SketchArcDirection;
435
+ }>;
436
+
437
+ export type SketchEntitySnapshot =
438
+ | SketchPointSnapshot
439
+ | SketchLineSnapshot
440
+ | SketchCircleSnapshot
441
+ | SketchArcSnapshot;
442
+
443
+ /** Numeric geometry parameters, excluding identity and point references. */
444
+ export function sketchEntityParameters(
445
+ entity: SketchEntitySnapshot,
446
+ ): readonly number[] {
447
+ switch (entity.kind) {
448
+ case 'point':
449
+ return entity.alias ? [] : entity.position;
450
+ case 'circle':
451
+ case 'arc':
452
+ return [entity.radius];
453
+ case 'line':
454
+ return [];
455
+ }
456
+ }
457
+
458
+ export function withSketchEntityParameters(
459
+ entity: SketchEntitySnapshot,
460
+ parameters: readonly number[],
461
+ ): SketchEntitySnapshot {
462
+ switch (entity.kind) {
463
+ case 'point':
464
+ return entity.alias
465
+ ? entity
466
+ : {...entity, position: [parameters[0], parameters[1]]};
467
+ case 'circle':
468
+ case 'arc':
469
+ return {...entity, radius: parameters[0]};
470
+ case 'line':
471
+ return entity;
472
+ }
473
+ }
474
+
475
+ /** Each snapshot contains only its own definitions; base retains the lineage. */
476
+ export type SketchSnapshot = Readonly<{
477
+ id: string;
478
+ base?: string;
479
+ entities: readonly SketchEntitySnapshot[];
480
+ constraints: readonly SketchConstraint<SketchPointAddress>[];
481
+ degreesOfFreedom: number;
482
+ /** Evaluation-local indices into constraints, not persistent identity. */
483
+ redundant: readonly number[];
484
+ }>;
485
+
486
+ export function snapshotSketch(
487
+ value: Sketch,
488
+ identity: (sketch: Sketch) => string,
489
+ ): SketchSnapshot {
490
+ const {
491
+ base,
492
+ entries,
493
+ constraints,
494
+ points,
495
+ radii,
496
+ degreesOfFreedom,
497
+ redundant,
498
+ } = sketchDefinition(value);
499
+ const id = identity(value);
500
+ return {
501
+ id,
502
+ base: base && identity(base),
503
+ entities: snapshotEntries(entries, id, identity, points).map(e =>
504
+ e.kind === 'point'
505
+ ? {...e, position: points.get(e.id)!}
506
+ : e.kind === 'circle' || e.kind === 'arc'
507
+ ? {...e, radius: radii.get(e.id)!}
508
+ : e,
509
+ ),
510
+ constraints: snapshotConstraints(constraints, id, identity),
511
+ degreesOfFreedom,
512
+ redundant,
513
+ };
514
+ }
515
+
516
+ function pointAddress(id: string, identity: (sketch: Sketch) => string) {
517
+ return (ref: number | SketchPoint): SketchPointAddress =>
518
+ typeof ref === 'number'
519
+ ? {layer: id, id: ref}
520
+ : {layer: identity(ref.sketch), id: ref.id};
521
+ }
522
+
523
+ function snapshotEntries(
524
+ entries: readonly SketchEntry[],
525
+ id: string,
526
+ identity: (sketch: Sketch) => string,
527
+ positions: ReadonlyMap<number, SketchPosition>,
528
+ ): SketchSnapshot['entities'] {
529
+ const point = pointAddress(id, identity);
530
+ return entries.map(([kind, entityId, data]) =>
531
+ kind === 'point'
532
+ ? Array.isArray(data)
533
+ ? {kind, id: entityId, position: data as SketchPosition}
534
+ : {
535
+ kind,
536
+ id: entityId,
537
+ position: positions.get(entityId)!,
538
+ alias: point(data as number | SketchPoint),
539
+ }
540
+ : kind === 'circle'
541
+ ? {kind, id: entityId, center: point(data[0]), radius: data[1]}
542
+ : kind === 'arc'
543
+ ? {
544
+ kind,
545
+ id: entityId,
546
+ center: point(data[0]),
547
+ radius: data[1],
548
+ points: [point(data[2]), point(data[3])],
549
+ direction: data[4],
550
+ }
551
+ : {kind, id: entityId, points: [point(data[0]), point(data[1])]},
552
+ );
553
+ }
554
+
555
+ /** Resolve identity once for solving, dragging, snapping and topology edits. */
556
+ export function sketchPointResolver(layers: readonly SketchSnapshot[]) {
557
+ const points = new Map(
558
+ layers.flatMap(layer =>
559
+ layer.entities.flatMap(e =>
560
+ e.kind === 'point'
561
+ ? [
562
+ [
563
+ JSON.stringify([layer.id, e.id]),
564
+ {...e, layer: layer.id},
565
+ ] as const,
566
+ ]
567
+ : [],
568
+ ),
569
+ ),
570
+ );
571
+ const resolved = new Map<string, SketchPointAddress>();
572
+ const visiting = new Set<string>();
573
+ const resolve = (ref: SketchPointAddress): SketchPointAddress => {
574
+ const key = JSON.stringify([ref.layer, ref.id]);
575
+ const cached = resolved.get(key);
576
+ if (cached) return cached;
577
+ const point = points.get(key);
578
+ if (!point) throw new Error(`Missing sketch point ${ref.id}.`);
579
+ if (visiting.has(key))
580
+ throw new Error(`Cyclic sketch point alias at ${ref.id}.`);
581
+ visiting.add(key);
582
+ const target = point.alias
583
+ ? resolve(point.alias)
584
+ : {layer: ref.layer, id: ref.id};
585
+ visiting.delete(key);
586
+ resolved.set(key, target);
587
+ return target;
588
+ };
589
+ for (const point of points.values()) resolve(point);
590
+ return resolve;
591
+ }
592
+
593
+ function snapshotConstraints(
594
+ constraints: readonly SketchConstraint[],
595
+ id: string,
596
+ identity: (sketch: Sketch) => string,
597
+ ): SketchSnapshot['constraints'] {
598
+ const point = pointAddress(id, identity);
599
+ return constraints.map(([kind, data, value]) => {
600
+ switch (kind) {
601
+ case 'fixed':
602
+ return [kind, point(data)];
603
+ case 'coincident':
604
+ return [kind, [point(data[0]), point(data[1])]];
605
+ case 'midpoint':
606
+ return [kind, [point(data[0]), point(data[1]), point(data[2])]];
607
+ case 'x':
608
+ case 'y':
609
+ return [kind, point(data), value];
610
+ case 'horizontal':
611
+ case 'vertical':
612
+ return [kind, data];
613
+ case 'parallel':
614
+ case 'perpendicular':
615
+ return [kind, [data[0], data[1]]];
616
+ case 'length':
617
+ case 'radius':
618
+ case 'sweep':
619
+ return [kind, data, value];
620
+ case 'angle':
621
+ return typeof data === 'number'
622
+ ? [kind, data, value]
623
+ : [kind, [data[0], data[1]], value];
624
+ }
625
+ });
626
+ }
627
+
628
+ function sketchGeometry(layers: readonly SketchSnapshot[]) {
629
+ const resolve = sketchPointResolver(layers);
630
+ // Array order is source layout, not numeric priority. Keep a stable local
631
+ // parameter basis (including the normalization origin), without reordering
632
+ // author entries, snapshots, layers, or constraint diagnostic indices.
633
+ const entities = layers.flatMap(layer =>
634
+ [...layer.entities]
635
+ .sort((a, b) => a.id - b.id)
636
+ .map(e => ({...e, layer: layer.id})),
637
+ );
638
+ const points = entities.filter(e => e.kind === 'point').filter(e => !e.alias);
639
+ const pointIndex = (ref: SketchPointAddress) => {
640
+ ref = resolve(ref);
641
+ const index = points.findIndex(
642
+ p => p.id === ref.id && p.layer === ref.layer,
643
+ );
644
+ if (index < 0) throw new Error(`Missing sketch point ${ref.id}.`);
645
+ return index;
646
+ };
647
+ const lines = entities
648
+ .filter(e => e.kind === 'line')
649
+ .map(e => [pointIndex(e.points[0]), pointIndex(e.points[1])] as const);
650
+ const circles = entities.filter(e => e.kind === 'circle');
651
+ const arcs = entities.filter(e => e.kind === 'arc');
652
+ const geometry: SketchIncidenceGeometry = {
653
+ points: points.map(p => p.position),
654
+ lines,
655
+ circles: circles.map(c => ({
656
+ center: pointIndex(c.center),
657
+ radius: c.radius,
658
+ })),
659
+ arcs: arcs.map(a => ({
660
+ center: pointIndex(a.center),
661
+ radius: a.radius,
662
+ points: [pointIndex(a.points[0]), pointIndex(a.points[1])],
663
+ direction: a.direction,
664
+ })),
665
+ };
666
+ return {points, pointIndex, lines, circles, arcs, geometry};
667
+ }
668
+
669
+ /** Includes gesture-only equations, not merely authored constraints. */
670
+ export function sketchDragRequiresSolver(
671
+ layers: readonly SketchSnapshot[],
672
+ ): boolean {
673
+ const local = layers.at(-1)!;
674
+ if (
675
+ local.constraints.length ||
676
+ layers.some(layer => layer.entities.some(e => e.kind === 'arc'))
677
+ )
678
+ return true;
679
+ const {points, geometry} = sketchGeometry(layers);
680
+ return sketchIncidences(geometry).some(contact =>
681
+ sketchIncidencePoints(geometry, contact).some(
682
+ i => points[i].layer === local.id,
683
+ ),
684
+ );
685
+ }
686
+
687
+ /** Source replay must retain every gesture-start incidence, including aliases. */
688
+ export function assertSketchDragConnections(
689
+ layers: readonly SketchSnapshot[],
690
+ reference: SketchSnapshot,
691
+ ): void {
692
+ const original = sketchGeometry([...layers.slice(0, -1), reference]);
693
+ const current = sketchGeometry(layers);
694
+ for (const contact of sketchIncidences(original.geometry)) {
695
+ const point =
696
+ current.points[current.pointIndex(original.points[contact.point])]
697
+ .position;
698
+ if (
699
+ !isPointOnSketchCurve(
700
+ point,
701
+ sketchIncidenceCurve(current.geometry, contact),
702
+ )
703
+ )
704
+ throw new Error(
705
+ 'The source replay could not retain a point on its curve. No changes were applied.',
706
+ );
707
+ }
708
+ }
709
+
710
+ /** The same solver and snapshot contract serve evaluation and interactive dragging. */
711
+ export function solveSketchSnapshot(
712
+ layers: readonly SketchSnapshot[],
713
+ drag?: Readonly<{
714
+ id: number;
715
+ position: SketchPosition;
716
+ /** Gesture-start geometry; previous-frame geometry remains the numeric seed. */
717
+ reference?: SketchSnapshot;
718
+ /** Numeric, gesture-only parameter locks; never author constraints. */
719
+ locks?: readonly Readonly<{id: number; parameter: number; value: number}>[];
720
+ }>,
721
+ ): SketchSnapshot {
722
+ const local = layers.at(-1)!;
723
+ const {points, pointIndex, lines, circles, arcs, geometry} =
724
+ sketchGeometry(layers);
725
+ const linePoints = (id: number): readonly [number, number] => {
726
+ const line = local.entities.find(e => e.kind === 'line' && e.id === id) as
727
+ SketchLineSnapshot | undefined;
728
+ if (!line) throw new Error(`Missing sketch line ${id}.`);
729
+ return [pointIndex(line.points[0]), pointIndex(line.points[1])];
730
+ };
731
+ const circleIndex = (id: number) => {
732
+ const index = circles.findIndex(c => c.id === id && c.layer === local.id);
733
+ if (index < 0) throw new Error(`Missing sketch circle ${id}.`);
734
+ return index;
735
+ };
736
+ const arcIndex = (id: number) =>
737
+ arcs.findIndex(a => a.id === id && a.layer === local.id);
738
+ const constraints = local.constraints.map<SketchSolveConstraint>(
739
+ ([kind, data, value]) => {
740
+ switch (kind) {
741
+ case 'fixed': {
742
+ const point = pointIndex(data);
743
+ return {kind, point, position: points[point].position};
744
+ }
745
+ case 'x':
746
+ case 'y':
747
+ return {kind, point: pointIndex(data), value};
748
+ case 'horizontal':
749
+ case 'vertical':
750
+ return {kind, points: linePoints(data)};
751
+ case 'parallel':
752
+ case 'perpendicular':
753
+ return {
754
+ kind,
755
+ points: [...linePoints(data[0]), ...linePoints(data[1])],
756
+ };
757
+ case 'coincident':
758
+ return {kind, points: [pointIndex(data[0]), pointIndex(data[1])]};
759
+ case 'midpoint':
760
+ return {
761
+ kind,
762
+ points: [
763
+ pointIndex(data[0]),
764
+ pointIndex(data[1]),
765
+ pointIndex(data[2]),
766
+ ],
767
+ };
768
+ case 'length':
769
+ return {kind, points: linePoints(data), value};
770
+ case 'angle':
771
+ return typeof data === 'number'
772
+ ? {kind, points: linePoints(data), value}
773
+ : {
774
+ kind: 'lineAngle',
775
+ points: [...linePoints(data[0]), ...linePoints(data[1])],
776
+ value,
777
+ };
778
+ case 'radius':
779
+ return arcIndex(data) >= 0
780
+ ? {
781
+ kind,
782
+ curve: 'arc',
783
+ index: arcIndex(data),
784
+ value,
785
+ }
786
+ : {
787
+ kind,
788
+ curve: 'circle',
789
+ index: circleIndex(data),
790
+ value,
791
+ };
792
+ case 'sweep':
793
+ return {
794
+ kind,
795
+ index: arcIndex(data),
796
+ value,
797
+ };
798
+ }
799
+ },
800
+ );
801
+ const problem: SketchSolveProblem = {
802
+ points: points.map(p => {
803
+ const position: [number, number] = [...p.position];
804
+ const upstream = p.layer !== local.id;
805
+ const locked: [boolean, boolean] = [upstream, upstream];
806
+ if (!upstream)
807
+ for (const lock of drag?.locks ?? [])
808
+ if (
809
+ local.entities.some(e => e.kind === 'point' && e.id === lock.id) &&
810
+ pointIndex({layer: local.id, id: lock.id}) === pointIndex(p)
811
+ ) {
812
+ position[lock.parameter as 0 | 1] = lock.value;
813
+ locked[lock.parameter as 0 | 1] = true;
814
+ }
815
+ return {position, locked};
816
+ }),
817
+ lines,
818
+ circles: circles.map(c => {
819
+ const upstream = c.layer !== local.id;
820
+ const lock = !upstream && drag?.locks?.find(lock => lock.id === c.id);
821
+ return {
822
+ center: pointIndex(c.center),
823
+ radius: lock ? lock.value : c.radius,
824
+ locked: upstream || !!lock,
825
+ };
826
+ }),
827
+ arcs: arcs.map(a => {
828
+ const upstream = a.layer !== local.id;
829
+ const lock = !upstream && drag?.locks?.find(lock => lock.id === a.id);
830
+ return {
831
+ center: pointIndex(a.center),
832
+ radius: lock ? lock.value : a.radius,
833
+ locked: upstream || !!lock,
834
+ points: [pointIndex(a.points[0]), pointIndex(a.points[1])],
835
+ direction: a.direction,
836
+ };
837
+ }),
838
+ constraints,
839
+ };
840
+ const target = drag && local.entities.find(e => e.id === drag.id)!;
841
+ const objective =
842
+ drag &&
843
+ target &&
844
+ (target.kind === 'circle' || target.kind === 'arc'
845
+ ? {
846
+ kind: 'radius' as const,
847
+ curve: target.kind,
848
+ index:
849
+ target.kind === 'circle'
850
+ ? circleIndex(target.id)
851
+ : arcIndex(target.id),
852
+ value: Math.hypot(
853
+ ...drag.position.map(
854
+ (v, axis) => v - points[pointIndex(target.center)].position[axis],
855
+ ),
856
+ ),
857
+ }
858
+ : {
859
+ kind: 'point' as const,
860
+ point: pointIndex({layer: local.id, id: drag.id}),
861
+ position: drag.position,
862
+ });
863
+ const prepared =
864
+ // Applying authored coordinate locks can change the displayed geometry.
865
+ // Satisfy those locks before matching a drag rule or preparing its reference.
866
+ drag &&
867
+ (problem.points.some((p, i) =>
868
+ p.position.some((v, axis) => v !== points[i].position[axis]),
869
+ ) ||
870
+ problem.circles.some((c, i) => c.radius !== circles[i].radius) ||
871
+ problem.arcs.some((a, i) => a.radius !== arcs[i].radius))
872
+ ? solvedProblem(problem)
873
+ : problem;
874
+ const referenceEntities = new Map(
875
+ drag?.reference?.entities.map(e => [e.id, e]),
876
+ );
877
+ const reference = drag?.reference
878
+ ? solvedProblem({
879
+ ...prepared,
880
+ points: prepared.points.map((p, index) => {
881
+ const entity =
882
+ points[index].layer === local.id &&
883
+ referenceEntities.get(points[index].id);
884
+ return entity && entity.kind === 'point'
885
+ ? {
886
+ ...p,
887
+ position: p.position.map((v, axis) =>
888
+ p.locked[axis] ? v : entity.position[axis],
889
+ ) as [number, number],
890
+ }
891
+ : p;
892
+ }),
893
+ circles: prepared.circles.map((c, index) => {
894
+ const entity =
895
+ circles[index].layer === local.id &&
896
+ referenceEntities.get(circles[index].id);
897
+ return !c.locked && entity && entity.kind === 'circle'
898
+ ? {...c, radius: entity.radius}
899
+ : c;
900
+ }),
901
+ arcs: prepared.arcs.map((a, index) => {
902
+ const entity =
903
+ arcs[index].layer === local.id &&
904
+ referenceEntities.get(arcs[index].id);
905
+ return !a.locked && entity && entity.kind === 'arc'
906
+ ? {...a, radius: entity.radius}
907
+ : a;
908
+ }),
909
+ })
910
+ : prepared;
911
+ const result = objective
912
+ ? solveSketchDrag(
913
+ prepared,
914
+ objective,
915
+ reference,
916
+ drag?.reference
917
+ ? sketchGeometry([...layers.slice(0, -1), drag.reference]).geometry
918
+ : geometry,
919
+ )
920
+ : solveSketchProblem(prepared);
921
+ const entities = local.entities.map(e =>
922
+ e.kind === 'point'
923
+ ? {
924
+ ...e,
925
+ position: result.positions[pointIndex({layer: local.id, id: e.id})],
926
+ }
927
+ : e.kind === 'circle'
928
+ ? {...e, radius: result.radii[circleIndex(e.id)]}
929
+ : e.kind === 'arc'
930
+ ? {...e, radius: result.arcRadii[arcIndex(e.id)]}
931
+ : e,
932
+ );
933
+ for (const entity of entities) {
934
+ if (
935
+ (entity.kind === 'circle' || entity.kind === 'arc') &&
936
+ (!Number.isFinite(entity.radius) || entity.radius <= 0)
937
+ )
938
+ throw new Error(
939
+ `Sketch ${entity.kind} ${entity.id} requires a positive finite radius.`,
940
+ );
941
+ if (entity.kind === 'arc') {
942
+ const [center, a, b] = [entity.center, ...entity.points].map(
943
+ p => result.positions[pointIndex(p)],
944
+ );
945
+ if (
946
+ Math.hypot(a[0] - center[0], a[1] - center[1]) === 0 ||
947
+ Math.hypot(a[0] - b[0], a[1] - b[1]) === 0
948
+ )
949
+ throw new Error(
950
+ `Sketch arc ${entity.id} requires a nonzero radius and distinct endpoints; use circle for a full circle.`,
951
+ );
952
+ }
953
+ if (entity.kind !== 'line') continue;
954
+ const [a, b] = entity.points.map(p => result.positions[pointIndex(p)]);
955
+ if (Math.hypot(a[0] - b[0], a[1] - b[1]) === 0)
956
+ throw new Error(`Sketch line ${entity.id} has zero length.`);
957
+ }
958
+ return {
959
+ ...local,
960
+ entities,
961
+ degreesOfFreedom: drag ? local.degreesOfFreedom : result.degreesOfFreedom,
962
+ redundant: drag ? local.redundant : result.redundant,
963
+ };
964
+ }
965
+
966
+ function solvedProblem(problem: SketchSolveProblem): SketchSolveProblem {
967
+ const solved = solveSketchProblem(problem);
968
+ return {
969
+ ...problem,
970
+ points: problem.points.map((p, i) => ({
971
+ ...p,
972
+ position: solved.positions[i],
973
+ })),
974
+ circles: problem.circles.map((c, i) => ({...c, radius: solved.radii[i]})),
975
+ arcs: problem.arcs.map((a, i) => ({...a, radius: solved.arcRadii[i]})),
976
+ };
977
+ }