@code3d/core 0.0.1-alpha.0 → 0.0.1-alpha.3

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 (131) hide show
  1. package/README.md +223 -786
  2. package/THIRD_PARTY.md +40 -0
  3. package/bld/THIRD_PARTY_NOTICES.txt +182 -0
  4. package/bld/chunks/chunk-BF55AIEQ.js +31 -0
  5. package/bld/chunks/chunk-BF55AIEQ.js.map +7 -0
  6. package/bld/chunks/chunk-P5VGKAEM.js +30 -0
  7. package/bld/chunks/chunk-P5VGKAEM.js.map +7 -0
  8. package/bld/chunks/chunk-RPY2WVCB.js +24834 -0
  9. package/bld/chunks/chunk-RPY2WVCB.js.map +7 -0
  10. package/bld/chunks/chunk-TJIN6S4R.js +1 -0
  11. package/bld/chunks/chunk-TJIN6S4R.js.map +7 -0
  12. package/bld/chunks/chunk-U5G2VU2I.js +19 -0
  13. package/bld/chunks/chunk-U5G2VU2I.js.map +7 -0
  14. package/bld/library/cached.d.ts +33 -0
  15. package/bld/library/cached.d.ts.map +1 -0
  16. package/bld/library/font.d.ts +39 -0
  17. package/bld/library/font.d.ts.map +1 -0
  18. package/bld/library/google-font.d.ts +14 -0
  19. package/bld/library/google-font.d.ts.map +1 -0
  20. package/bld/library/index.d.ts +8 -2
  21. package/bld/library/index.d.ts.map +1 -1
  22. package/bld/library/index.js +60 -3
  23. package/bld/library/index.js.map +7 -1
  24. package/bld/library/kernel-artifact-codec.d.ts.map +1 -1
  25. package/bld/library/kernel-cache.d.ts +24 -6
  26. package/bld/library/kernel-cache.d.ts.map +1 -1
  27. package/bld/library/material.d.ts +1 -1
  28. package/bld/library/material.d.ts.map +1 -1
  29. package/bld/library/open-cascade.d.ts +4 -0
  30. package/bld/library/open-cascade.d.ts.map +1 -0
  31. package/bld/library/replicad.d.ts +3 -2
  32. package/bld/library/replicad.d.ts.map +1 -1
  33. package/bld/library/replicad.js +10 -24
  34. package/bld/library/replicad.js.map +7 -1
  35. package/bld/library/retained-memory.d.ts.map +1 -1
  36. package/bld/library/runtime.d.ts +146 -80
  37. package/bld/library/runtime.d.ts.map +1 -1
  38. package/bld/library/sketch-solver.d.ts +1 -1
  39. package/bld/library/sketch-solver.d.ts.map +1 -1
  40. package/bld/library/sketch.d.ts +9 -1
  41. package/bld/library/sketch.d.ts.map +1 -1
  42. package/bld/library/text.d.ts +53 -0
  43. package/bld/library/text.d.ts.map +1 -0
  44. package/bld/library/three.js +3 -3
  45. package/bld/library/three.js.map +7 -1
  46. package/bld/library/topology.d.ts.map +1 -1
  47. package/bld/node/index.d.ts.map +1 -1
  48. package/bld/node/index.js +62 -14
  49. package/bld/node/index.js.map +7 -1
  50. package/bld/node/replicad.js +13 -3
  51. package/bld/node/replicad.js.map +7 -1
  52. package/bld/tooling/index.d.ts +17 -15
  53. package/bld/tooling/index.d.ts.map +1 -1
  54. package/bld/tooling/index.js +147 -19
  55. package/bld/tooling/index.js.map +7 -1
  56. package/package.json +18 -6
  57. package/src/library/cached.ts +132 -0
  58. package/src/library/font.ts +186 -0
  59. package/src/library/google-font.ts +97 -0
  60. package/src/library/index.ts +8 -3
  61. package/src/library/kernel-artifact-codec.ts +129 -20
  62. package/src/library/kernel-cache.ts +128 -34
  63. package/src/library/loft.ts +4 -0
  64. package/src/library/material.ts +3 -4
  65. package/src/library/open-cascade.ts +13 -0
  66. package/src/library/replicad.ts +5 -5
  67. package/src/library/retained-memory.ts +6 -1
  68. package/src/library/runtime.ts +2064 -1730
  69. package/src/library/sketch-solver.ts +3 -1
  70. package/src/library/sketch.ts +81 -13
  71. package/src/library/text.ts +477 -0
  72. package/src/library/topology.ts +24 -0
  73. package/src/library/tsconfig.json +2 -1
  74. package/src/node/index.ts +14 -3
  75. package/src/node/tsconfig.json +2 -1
  76. package/src/tooling/index.ts +48 -46
  77. package/src/tooling/tsconfig.json +2 -1
  78. package/bld/library/alignment-geometry.js +0 -322
  79. package/bld/library/alignment-geometry.js.map +0 -1
  80. package/bld/library/bound-solver.js +0 -252
  81. package/bld/library/bound-solver.js.map +0 -1
  82. package/bld/library/extrude.js +0 -28
  83. package/bld/library/extrude.js.map +0 -1
  84. package/bld/library/kernel-artifact-codec.js +0 -148
  85. package/bld/library/kernel-artifact-codec.js.map +0 -1
  86. package/bld/library/kernel-cache.js +0 -245
  87. package/bld/library/kernel-cache.js.map +0 -1
  88. package/bld/library/kernel-shapes.js +0 -108
  89. package/bld/library/kernel-shapes.js.map +0 -1
  90. package/bld/library/loft.js +0 -161
  91. package/bld/library/loft.js.map +0 -1
  92. package/bld/library/material.js +0 -176
  93. package/bld/library/material.js.map +0 -1
  94. package/bld/library/model-color.js +0 -36
  95. package/bld/library/model-color.js.map +0 -1
  96. package/bld/library/open-cascade-error.js +0 -22
  97. package/bld/library/open-cascade-error.js.map +0 -1
  98. package/bld/library/relation-solver.js +0 -258
  99. package/bld/library/relation-solver.js.map +0 -1
  100. package/bld/library/retained-memory.js +0 -27
  101. package/bld/library/retained-memory.js.map +0 -1
  102. package/bld/library/runtime.js +0 -2954
  103. package/bld/library/runtime.js.map +0 -1
  104. package/bld/library/shell.js +0 -221
  105. package/bld/library/shell.js.map +0 -1
  106. package/bld/library/sketch-curve-intersections.js +0 -117
  107. package/bld/library/sketch-curve-intersections.js.map +0 -1
  108. package/bld/library/sketch-curves.js +0 -89
  109. package/bld/library/sketch-curves.js.map +0 -1
  110. package/bld/library/sketch-drag-rules.js +0 -522
  111. package/bld/library/sketch-drag-rules.js.map +0 -1
  112. package/bld/library/sketch-face.js +0 -72
  113. package/bld/library/sketch-face.js.map +0 -1
  114. package/bld/library/sketch-incidence.js +0 -114
  115. package/bld/library/sketch-incidence.js.map +0 -1
  116. package/bld/library/sketch-precision.js +0 -55
  117. package/bld/library/sketch-precision.js.map +0 -1
  118. package/bld/library/sketch-regions.js +0 -193
  119. package/bld/library/sketch-regions.js.map +0 -1
  120. package/bld/library/sketch-solver.js +0 -578
  121. package/bld/library/sketch-solver.js.map +0 -1
  122. package/bld/library/sketch.js +0 -650
  123. package/bld/library/sketch.js.map +0 -1
  124. package/bld/library/spatial.js +0 -195
  125. package/bld/library/spatial.js.map +0 -1
  126. package/bld/library/topology-id.js +0 -67
  127. package/bld/library/topology-id.js.map +0 -1
  128. package/bld/library/topology-inspection.js +0 -267
  129. package/bld/library/topology-inspection.js.map +0 -1
  130. package/bld/library/topology.js +0 -455
  131. package/bld/library/topology.js.map +0 -1
@@ -1,2954 +0,0 @@
1
- import { captureModelMaterial } from './material.js';
2
- import { inspectShapeTopology, } from './topology-inspection.js';
3
- import { assembleWire, basicFaceExtrusion, BoundingBox, genericSweep, getOC, makeBSplineApproximation, makeBezierCurve, makeCircle, makeCylinder, makeFace, makeHelix, makeLine, makeSphere, makeThreePointArc, makeVertex, sketchCircle, sketchEllipse, sketchPolysides, sketchRectangle, Vector, } from 'replicad';
4
- import { castOwnedShape, castOwnedShape3D, centeredBoxShape, shapeSubshapes, transformShape, } from './kernel-shapes.js';
5
- import { addVectors, composeTransforms, frameFromYAxis, identityRigidTransform, invertTransform, rotateVector, origin, negateVector, quaternionAxisAngle, relativeTransform, rotation, rotationAround, translation, } from './spatial.js';
6
- import { axisRotation, solveBodies, } from './relation-solver.js';
7
- import { edgeGeometry, faceGeometry, transformGeometry, } from './alignment-geometry.js';
8
- import { shellWithTopology } from './shell.js';
9
- import { beginKernelOperationEvaluation, evaluateKernelOperation, kernelOperationKey, findKernelOperation, acceptKernelOperation, } from './kernel-cache.js';
10
- import { loftWithTopology } from './loft.js';
11
- import { estimateRetainedBytes } from './retained-memory.js';
12
- import { extrudeWithTopology } from './extrude.js';
13
- import { sketchRegionFace } from './sketch-face.js';
14
- import { formatTopologyId } from './topology-id.js';
15
- import { booleanWithTopology, chamferEdges, filletEdges, initialShapeTopology, preserveShapeTopology, resolveEdgeSelection, resolveTopologySelection, stableEdgeGroups, stableSurfaceGroups, stableVertexData, topologyEdgeDirections, topologySurfaceDirections, topologyVertexPoints, topologyChildren, withTopologyShape, } from './topology.js';
16
- import { sketch } from './sketch.js';
17
- import { encodeKernelArtifact, decodeKernelArtifact, } from './kernel-artifact-codec.js';
18
- let activeRelate;
19
- // Geometry and relations are persistent model values. Source provenance belongs
20
- // only to the evaluation that observed them, including models cached by packages.
21
- let valueTraces = new WeakMap();
22
- let operationTraces = new WeakMap();
23
- /**
24
- * Start a fresh, serial tooling evaluation without invalidating model geometry.
25
- * Finish in finally after snapshotting to retain this evaluation's kernel work.
26
- * An optional cancellation check may throw before any kernel operation starts;
27
- * complete results survive cancellation, and cleanup removes the check.
28
- */
29
- export function beginModelEvaluation(checkCancelled) {
30
- valueTraces = new WeakMap();
31
- operationTraces = new WeakMap();
32
- return beginKernelOperationEvaluation(checkCancelled);
33
- }
34
- function valueTrace(value) {
35
- let trace = valueTraces.get(value);
36
- if (!trace) {
37
- trace = { sourceRefs: [], parameters: [] };
38
- valueTraces.set(value, trace);
39
- }
40
- return trace;
41
- }
42
- function transformSolveContext(context, transform) {
43
- return {
44
- poses: new Map([...context.poses].map(([model, pose]) => [
45
- model,
46
- composeTransforms(transform, pose),
47
- ])),
48
- };
49
- }
50
- const unitScale = [1, 1, 1];
51
- let nextNodeId = 1;
52
- let nextConstraintId = 1;
53
- let nextOperationId = 1;
54
- const combineModels = Symbol('combineModels');
55
- const loftModels = Symbol('loftModels');
56
- const modelElementKinds = {
57
- solid: 'frame',
58
- face: 'face',
59
- edge: 'line',
60
- vertex: 'point',
61
- group: 'frame',
62
- };
63
- const defaultModelNames = {
64
- solid: 'Solid',
65
- face: 'Face',
66
- edge: 'Edge',
67
- vertex: 'Vertex',
68
- group: 'Group',
69
- };
70
- const anchorKind = Symbol('anchorKind');
71
- const anchorReferenceValue = Symbol('anchorReference');
72
- const modelFamily = Symbol('modelFamily');
73
- const modelNamedElements = Symbol('modelNamedElements');
74
- const boundKind = Symbol('boundKind');
75
- class ModelAnchor {
76
- elementKind;
77
- [anchorReferenceValue];
78
- constructor(reference) {
79
- this[anchorReferenceValue] = reference;
80
- this.elementKind = this[anchorReferenceValue].kind;
81
- for (const [name, member] of Object.entries(reference.members ?? {})) {
82
- const pointMember = ['center', 'start', 'midpoint', 'end'].includes(name);
83
- if (name in this &&
84
- !pointMember &&
85
- !(name === 'id' && reference.topology?.selection.kind === 'solid')) {
86
- throw new Error(`The exposed member ${name} conflicts with the topology API.`);
87
- }
88
- Object.defineProperty(this, name, {
89
- value: modelAnchor(reference.model, `${reference.name}.${name}`, member),
90
- });
91
- }
92
- }
93
- get up() {
94
- return directionalBound(this[anchorReferenceValue], 'up');
95
- }
96
- get down() {
97
- return directionalBound(this[anchorReferenceValue], 'down');
98
- }
99
- get left() {
100
- return directionalBound(this[anchorReferenceValue], 'left');
101
- }
102
- get right() {
103
- return directionalBound(this[anchorReferenceValue], 'right');
104
- }
105
- get front() {
106
- return directionalBound(this[anchorReferenceValue], 'front');
107
- }
108
- get back() {
109
- return directionalBound(this[anchorReferenceValue], 'back');
110
- }
111
- flip() {
112
- const ref = this[anchorReferenceValue];
113
- const facing = -(ref.bound?.facing ?? ref.facing ?? 1);
114
- return modelAnchor(ref.model, ref.name, {
115
- ...ref,
116
- facing,
117
- bound: ref.bound ? { ...ref.bound, facing } : undefined,
118
- });
119
- }
120
- reverse() {
121
- const ref = this[anchorReferenceValue];
122
- return modelAnchor(ref.model, ref.name, {
123
- ...ref,
124
- direction: -(ref.direction ?? 1),
125
- });
126
- }
127
- on(target) {
128
- return Constraint.create(this[anchorReferenceValue], boundReference(target));
129
- }
130
- align(target) {
131
- return Constraint.create(this[anchorReferenceValue], anchorReference(target), 'align');
132
- }
133
- }
134
- class ModelTopologyElement extends ModelAnchor {
135
- get kind() {
136
- return this.#topology.selection.kind;
137
- }
138
- get id() {
139
- const selection = this.#topology.selection;
140
- return selection.kind === 'solid' ? undefined : selection.id;
141
- }
142
- get #topology() {
143
- return this[anchorReferenceValue].topology;
144
- }
145
- get center() {
146
- const geometry = this.#topology.source[modelGeometry]();
147
- return this.#pointAnchor('center', topologyCenter(geometry, this.#topology.selection));
148
- }
149
- get start() {
150
- return this.#curvePoint('start', 0);
151
- }
152
- get midpoint() {
153
- return this.#curvePoint('midpoint', 0.5);
154
- }
155
- get end() {
156
- return this.#curvePoint('end', 1);
157
- }
158
- #curvePoint(name, parameter) {
159
- const geometry = this.#topology.source[modelGeometry]().value;
160
- const basis = geometry.referenceBasis;
161
- const element = withTopologyShape(basis?.shape ?? geometry.shape, basis?.topology ?? geometry.topology, this.#topology.selection, shape => curveAnchor(shape, parameter));
162
- return this.#pointAnchor(name, basis ? topologyTransform(basis, element.transform) : element.transform);
163
- }
164
- #pointAnchor(name, transform) {
165
- return modelAnchor(this[anchorReferenceValue].model, `${this[anchorReferenceValue].name}.${name}`, {
166
- kind: 'point',
167
- transform: topologyTransform(this.#topology, transform),
168
- });
169
- }
170
- vertex(id) {
171
- return this.vertices([id])[0];
172
- }
173
- vertices(ids) {
174
- return this.#select('vertex', ids);
175
- }
176
- edge(id) {
177
- return this.edges([id])[0];
178
- }
179
- edges(ids) {
180
- return this.#select('edge', ids);
181
- }
182
- surface(id) {
183
- return this.surfaces([id])[0];
184
- }
185
- surfaces(ids) {
186
- return this.#select('surface', ids);
187
- }
188
- #select(kind, ids) {
189
- const geometry = this.#topology.source[modelGeometry]().value;
190
- const selected = topologyChildren(geometry.shape, geometry.topology, this.#topology.selection, kind, ids);
191
- return topologyReferences(this[anchorReferenceValue].model, `${this[anchorReferenceValue].name}.`, this.#topology, kind, selected);
192
- }
193
- }
194
- const topologyElementKinds = {
195
- solid: 'frame',
196
- vertex: 'point',
197
- edge: 'line',
198
- surface: 'face',
199
- };
200
- export function modelElementReference(value) {
201
- if (!(value instanceof ModelAnchor))
202
- return undefined;
203
- return {
204
- model: value[anchorReferenceValue].model,
205
- name: value[anchorReferenceValue].name,
206
- kind: value[anchorReferenceValue].kind,
207
- transform: toTransform(value[anchorReferenceValue].transform),
208
- bound: value[anchorReferenceValue].bound,
209
- facing: value[anchorReferenceValue].facing ?? 1,
210
- direction: value[anchorReferenceValue].direction ?? 1,
211
- };
212
- }
213
- export function modelTopologyReference(value) {
214
- if (!(value instanceof ModelTopologyElement))
215
- return undefined;
216
- const topology = value[anchorReferenceValue].topology;
217
- return {
218
- model: value[anchorReferenceValue].model,
219
- geometry: topology.source,
220
- transform: {
221
- ...topology.transform,
222
- scale: [topology.scale, topology.scale, topology.scale],
223
- },
224
- ...topology.selection,
225
- };
226
- }
227
- export function modelTopologyIds(value, kind) {
228
- const topology = value instanceof ModelTopologyElement
229
- ? value[anchorReferenceValue].topology
230
- : undefined;
231
- const source = isModelObject(value)
232
- ? value
233
- : value instanceof ConstraintExpression
234
- ? value.self()
235
- : topology?.source;
236
- const geometry = source?.[modelGeometry]()?.value;
237
- if (!geometry)
238
- return undefined;
239
- return topologyChildren(geometry.shape, geometry.topology, topology?.selection ?? { kind: 'solid' }, kind);
240
- }
241
- /** Traceable relation values, including unfinished pivot and axis selections. */
242
- export class ConstraintExpression {
243
- kind;
244
- source;
245
- target;
246
- displacement;
247
- rotations;
248
- context;
249
- constraintId;
250
- get sourceRefs() {
251
- return valueTrace(this).sourceRefs;
252
- }
253
- get parameters() {
254
- return valueTrace(this).parameters;
255
- }
256
- spatialOperation;
257
- constructor(kind, source, target, displacement, rotations, context, constraintId, previous) {
258
- this.kind = kind;
259
- this.source = source;
260
- this.target = target;
261
- this.displacement = displacement;
262
- this.rotations = rotations;
263
- this.context = context;
264
- this.constraintId = constraintId;
265
- valueTraces.set(this, {
266
- sourceRefs: [...(previous ? valueTrace(previous).sourceRefs : [])],
267
- parameters: [...(previous ? valueTrace(previous).parameters : [])],
268
- });
269
- }
270
- /** @internal */
271
- attachSource(sourceRef) {
272
- const refs = this.sourceRefs;
273
- if (!refs.some(ref => ref.file === sourceRef.file &&
274
- ref.start === sourceRef.start &&
275
- ref.end === sourceRef.end))
276
- refs.push(sourceRef);
277
- }
278
- /** @internal */
279
- attachParameters(parameters) {
280
- appendUniqueParameters(this.parameters, parameters);
281
- }
282
- /** @internal */
283
- self() {
284
- return this.context?.self;
285
- }
286
- /** @internal */
287
- traceReference() {
288
- const rebind = (model) => model === this.context?.original ? this.context.self : model;
289
- return {
290
- constraintId: this.constraintId,
291
- source: rebind(this.source.model),
292
- target: rebind(this.target.model),
293
- self: this.context?.self,
294
- };
295
- }
296
- /** @internal */
297
- storeFor(model, original) {
298
- const isSelf = (candidate) => candidate === model || candidate === original;
299
- if (!isSelf(this.source.model) && !isSelf(this.target.model))
300
- throw new Error('The constraint returned by relate() must involve self or the original receiver.');
301
- const bind = (reference) => ({
302
- ...reference,
303
- model: isSelf(reference.model) ? undefined : reference.model,
304
- });
305
- const stored = {
306
- id: this.constraintId,
307
- kind: this.kind,
308
- source: bind(this.source),
309
- target: bind(this.target),
310
- offset: this.displacement,
311
- rotations: this.rotations.map(action => ({
312
- ...action,
313
- pivot: action.pivot.kind === 'around'
314
- ? { ...action.pivot, axis: bind(action.pivot.axis) }
315
- : action.pivot,
316
- })),
317
- };
318
- valueTraces.set(stored, {
319
- sourceRefs: [...this.sourceRefs],
320
- parameters: [...this.parameters],
321
- });
322
- return stored;
323
- }
324
- /** @internal */
325
- preview() {
326
- if (!this.context)
327
- return undefined;
328
- const pivot = this.spatialOperation?.pivot;
329
- const selection = pivot?.kind === 'around'
330
- ? {
331
- ...this.spatialOperation,
332
- pivot: {
333
- ...pivot,
334
- axis: {
335
- ...pivot.axis,
336
- model: pivot.axis.model === this.context.self ||
337
- pivot.axis.model === this.context.original
338
- ? undefined
339
- : pivot.axis.model,
340
- },
341
- },
342
- }
343
- : this.spatialOperation;
344
- return this.context.original[relationPreview](this.context.self.nodeId, this.storeFor(this.context.self, this.context.original), selection);
345
- }
346
- }
347
- export class Constraint extends ConstraintExpression {
348
- constructor(kind, source, target, displacement = undefined, rotations = [], context = activeRelate, constraintId = `constraint-${nextConstraintId++}`, previous, spatialOperation) {
349
- super(kind, source, target, displacement, rotations, context, constraintId, previous);
350
- this.spatialOperation = spatialOperation;
351
- }
352
- /** @internal */
353
- static create(source, target, kind = 'on') {
354
- if (kind === 'on')
355
- source.model[referenceBounds](source, identityRigidTransform);
356
- else if (source.kind === 'frame' || target.kind === 'frame')
357
- throw new Error('align() requires points, curves, or surfaces; select geometry on a solid or group.');
358
- return new Constraint(kind, source, target);
359
- }
360
- /**
361
- * In the target reference axes, on() pins matching bound centers; align()
362
- * translates self after alignment, retaining the relation's free modes.
363
- * Explicit zero pins tangential coordinates for on() only.
364
- * @code3d.param x {kind: 'length', label: 'ΔX'}
365
- * @code3d.param y {kind: 'length', label: 'ΔY'}
366
- * @code3d.param z {kind: 'length', label: 'ΔZ'}
367
- */
368
- offset(x, y, z) {
369
- assertFiniteVector('offset', [x, y, z]);
370
- return new Constraint(this.kind, this.source, this.target, addVectors(this.displacement ?? origin, [x, y, z]), this.rotations, this.context, this.constraintId, this);
371
- }
372
- /**
373
- * Select this rotation's pivot in self's local coordinates.
374
- * @code3d.param x {kind: 'length', label: 'Pivot X'}
375
- * @code3d.param y {kind: 'length', label: 'Pivot Y'}
376
- * @code3d.param z {kind: 'length', label: 'Pivot Z'}
377
- */
378
- pivot([x, y, z]) {
379
- assertFiniteVector('pivot', [x, y, z]);
380
- return new ConstraintPivotChain(this, { kind: 'pivot', point: [x, y, z] });
381
- }
382
- /** @code3d.param id {kind: 'vertex', label: 'Pivot vertex'} */
383
- pivotVertex(id) {
384
- return new ConstraintPivotChain(this, { kind: 'pivotVertex', id });
385
- }
386
- /** Select a positioned axis in the composition. */
387
- around(axis) {
388
- const reference = anchorReference(axis);
389
- if (reference.kind !== 'line')
390
- throw new Error('around() requires an axis or straight edge.');
391
- const geometry = reference.topology?.source[modelGeometry]()?.value ??
392
- (reference.whole ? reference.model[modelGeometry]()?.value : undefined);
393
- if (geometry) {
394
- const straight = reference.topology
395
- ? withTopologyShape(geometry.shape, geometry.topology, reference.topology.selection, shape => shape.geomType === 'LINE')
396
- : geometry.shape.geomType === 'LINE';
397
- if (!straight)
398
- throw new Error('around() requires a straight axis; curved edges do not define one rotation axis.');
399
- }
400
- return new ConstraintAroundChain(this, { kind: 'around', axis: reference });
401
- }
402
- /**
403
- * Rotate about self's origin and local X, Y, then Z axes, in degrees.
404
- * @code3d.param x {kind: 'angle', label: 'Rotate X'}
405
- * @code3d.param y {kind: 'angle', label: 'Rotate Y'}
406
- * @code3d.param z {kind: 'angle', label: 'Rotate Z'}
407
- */
408
- rotate(x, y, z) {
409
- return this.withRotation({ kind: 'pivot', point: origin }, [x, y, z], this);
410
- }
411
- /** @internal */
412
- withRotation(pivot, angles, previous) {
413
- assertFiniteVector('rotate', typeof angles === 'number' ? [angles, 0, 0] : angles);
414
- return new Constraint(this.kind, this.source, this.target, this.displacement, [...this.rotations, { pivot, angles }], this.context, this.constraintId, previous, { kind: 'rotate', pivot });
415
- }
416
- /** @internal */
417
- chainArguments() {
418
- return [
419
- this.kind,
420
- this.source,
421
- this.target,
422
- this.displacement,
423
- this.rotations,
424
- this.context,
425
- this.constraintId,
426
- this,
427
- ];
428
- }
429
- }
430
- export class ConstraintPivotChain extends ConstraintExpression {
431
- constraint;
432
- selection;
433
- /** @internal */
434
- constructor(constraint, selection) {
435
- super(...constraint.chainArguments());
436
- this.constraint = constraint;
437
- this.selection = selection;
438
- this.spatialOperation = { kind: selection.kind, pivot: selection };
439
- }
440
- /**
441
- * @code3d.param x {kind: 'angle', label: 'Rotate X'}
442
- * @code3d.param y {kind: 'angle', label: 'Rotate Y'}
443
- * @code3d.param z {kind: 'angle', label: 'Rotate Z'}
444
- */
445
- rotate(x, y, z) {
446
- return this.constraint.withRotation(this.selection, [x, y, z], this);
447
- }
448
- }
449
- export class ConstraintAroundChain extends ConstraintExpression {
450
- constraint;
451
- selection;
452
- /** @internal */
453
- constructor(constraint, selection) {
454
- super(...constraint.chainArguments());
455
- this.constraint = constraint;
456
- this.selection = selection;
457
- this.spatialOperation = { kind: selection.kind, pivot: selection };
458
- }
459
- /** @code3d.param angle {kind: 'angle', label: 'Rotate'} */
460
- rotate(angle) {
461
- return this.constraint.withRotation(this.selection, angle, this);
462
- }
463
- }
464
- const modelGeometry = Symbol('modelGeometry');
465
- const referenceBounds = Symbol('referenceBounds');
466
- const referenceBoundsParts = Symbol('referenceBoundsParts');
467
- const modelSnapshotQueries = Symbol('modelSnapshotQueries');
468
- const relationPreview = Symbol('relationPreview');
469
- export class ModelObject {
470
- /** @internal */
471
- elementKind;
472
- /** @internal */
473
- nodeId;
474
- /** @internal */
475
- kind;
476
- /** @internal */
477
- name;
478
- /** @internal */
479
- materialSnapshot;
480
- /** @internal */
481
- children;
482
- /** @internal */
483
- get sourceRefs() {
484
- return valueTrace(this).sourceRefs;
485
- }
486
- /** @internal */
487
- get parameters() {
488
- return valueTrace(this).parameters;
489
- }
490
- geometry;
491
- assembly;
492
- meshTolerance;
493
- geometryAnchor;
494
- elements;
495
- constraints;
496
- operation;
497
- /** @internal */
498
- [modelGeometry]() {
499
- return this.geometry;
500
- }
501
- constructor(init) {
502
- if (init.kind !== 'group' && !init.geometry) {
503
- throw new Error('A geometric model object must contain an OpenCascade shape.');
504
- }
505
- this.nodeId = init.nodeId ?? `node-${nextNodeId++}`;
506
- this.kind = init.kind;
507
- this.elementKind = modelElementKinds[init.kind];
508
- this.geometry = init.geometry;
509
- this.geometryAnchor = init.geometryAnchor ?? {
510
- kind: this.elementKind,
511
- transform: identityRigidTransform,
512
- };
513
- this.meshTolerance = init.meshTolerance ?? 0.2;
514
- this.name = init.name ?? defaultModelNames[init.kind];
515
- this.materialSnapshot = init.material;
516
- this.children = init.children ?? [];
517
- this.assembly =
518
- init.assembly ??
519
- (this.kind === 'group'
520
- ? ModelObject.createAssembly(this.children)
521
- : undefined);
522
- this.constraints = [...(init.constraints ?? [])];
523
- valueTraces.set(this, {
524
- sourceRefs: [...(init.sourceRefs ?? [])],
525
- parameters: [...(init.parameters ?? [])],
526
- });
527
- this.operation = init.operation;
528
- const elements = init.elements ??
529
- (this.kind === 'solid' && this.geometry
530
- ? solidElements(this.geometry.value.localBounds)
531
- : {});
532
- this.elements = this.geometry
533
- ? {
534
- center: {
535
- kind: 'point',
536
- transform: translation(boundsCenter(this.geometry.value.localBounds)),
537
- },
538
- ...elements,
539
- }
540
- : elements;
541
- for (const [name, element] of Object.entries(this.elements)) {
542
- if (name in this) {
543
- throw new Error(`The element name ${name} conflicts with the model API.`);
544
- }
545
- Object.defineProperty(this, name, {
546
- value: modelAnchor(this, name, element),
547
- });
548
- }
549
- }
550
- /** @internal */
551
- static create(init) {
552
- return new ModelObject(init);
553
- }
554
- get up() {
555
- return directionalBound(this.relationAnchorReference(), 'up');
556
- }
557
- get down() {
558
- return directionalBound(this.relationAnchorReference(), 'down');
559
- }
560
- get left() {
561
- return directionalBound(this.relationAnchorReference(), 'left');
562
- }
563
- get right() {
564
- return directionalBound(this.relationAnchorReference(), 'right');
565
- }
566
- get front() {
567
- return directionalBound(this.relationAnchorReference(), 'front');
568
- }
569
- get back() {
570
- return directionalBound(this.relationAnchorReference(), 'back');
571
- }
572
- on(target) {
573
- return Constraint.create(this.relationAnchorReference(), boundReference(target));
574
- }
575
- /** @internal */
576
- relationAnchorReference() {
577
- return { model: this, name: 'geometry', ...this.geometryAnchor, whole: true };
578
- }
579
- align(target) {
580
- return Constraint.create(this.relationAnchorReference(), anchorReference(target), 'align');
581
- }
582
- reverse() {
583
- return this.edges()[0].reverse();
584
- }
585
- flip() {
586
- return this.surfaces()[0].flip();
587
- }
588
- relate(build) {
589
- const operation = storedOperation('relate', [
590
- { model: this, role: 'source', index: 0 },
591
- ]);
592
- const related = this.copy({}, operation);
593
- const previous = activeRelate;
594
- activeRelate = { self: related, original: this };
595
- let built;
596
- try {
597
- built = build(related);
598
- }
599
- finally {
600
- activeRelate = previous;
601
- }
602
- const constraints = Array.isArray(built)
603
- ? built
604
- : [built];
605
- const stored = constraints.map(constraint => {
606
- if (!(constraint instanceof Constraint))
607
- throw new Error('relate() requires a completed Constraint; finish pivot/around with rotate().');
608
- return constraint.storeFor(related, this);
609
- });
610
- related.constraints.push(...stored);
611
- operation.inputs.push(...uniqueModels(stored.flatMap(constraintReferences)).map((model, index) => ({
612
- model,
613
- role: 'reference',
614
- index,
615
- })));
616
- return related;
617
- }
618
- expose(sources) {
619
- const entries = Object.entries(sources);
620
- const elements = this.localElements(entries.map(([, source]) => source));
621
- const exposed = Object.fromEntries(entries.map(([name], index) => [name, elements[index]]));
622
- const references = entries.map(([, source]) => anchorReference(source).model);
623
- const operation = storedOperation('expose', [
624
- { model: this, role: 'source', index: 0 },
625
- ...uniqueModels(references)
626
- .filter(model => model !== this)
627
- .map((model, index) => ({
628
- model,
629
- role: 'reference',
630
- index,
631
- })),
632
- ]);
633
- return this.copy({ elements: { ...this.elements, ...exposed } }, operation);
634
- }
635
- /** Resolve both own and occurrence references into this model's local frame. */
636
- localElements(sources) {
637
- const members = this.memberPoses();
638
- const external = sources
639
- .map(source => anchorReference(source).model)
640
- .filter(model => !members.has(model));
641
- const context = external.length
642
- ? ModelObject.createSolveContext([this, ...external])
643
- : undefined;
644
- return sources.map(source => {
645
- const reference = anchorReference(source);
646
- const { kind, transform: frame, topology, members: nested, bound, facing, } = source instanceof ModelObject ? source.exposedElement() : reference;
647
- const memberPose = members.get(reference.model);
648
- if (memberPose === null)
649
- throw new Error("The point or element belongs to multiple occurrences. Expose it through the intended child model's named reference.");
650
- const transform = memberPose ??
651
- relativeTransform(reference.model.solvePose(context), this.solvePose(context));
652
- return transformElement({ kind, transform: frame, topology, members: nested, bound, facing }, transform);
653
- });
654
- }
655
- vertex(id) {
656
- return this.vertices([id])[0];
657
- }
658
- vertices(ids) {
659
- return this.selectTopology('vertex', ids);
660
- }
661
- edge(id) {
662
- return this.edges([id])[0];
663
- }
664
- edges(ids) {
665
- return this.selectTopology('edge', ids);
666
- }
667
- surface(id) {
668
- return this.surfaces([id])[0];
669
- }
670
- surfaces(ids) {
671
- return this.selectTopology('surface', ids);
672
- }
673
- selectTopology(kind, ids) {
674
- const geometry = this.requireGeometry().value;
675
- const selected = topologyChildren(geometry.shape, geometry.topology, { kind: 'solid' }, kind, ids);
676
- return topologyReferences(this, '', { source: this, transform: identityRigidTransform, scale: 1 }, kind, selected);
677
- }
678
- /** @internal */
679
- exposedElement() {
680
- if (this.kind === 'group')
681
- return { ...this.geometryAnchor, members: this.elements };
682
- const geometry = this.requireGeometry().value;
683
- const kind = (this.kind === 'face' ? 'surface' : this.kind);
684
- const selection = kind === 'solid'
685
- ? { kind }
686
- : {
687
- kind,
688
- id: geometry.topology[kind === 'surface'
689
- ? 'surfaces'
690
- : kind === 'edge'
691
- ? 'edges'
692
- : 'vertices'].ids[0],
693
- };
694
- const context = {
695
- source: this,
696
- transform: identityRigidTransform,
697
- scale: 1,
698
- };
699
- const anchor = selection.kind === 'solid'
700
- ? this.geometryAnchor
701
- : topologyReferences(this, '', context, selection.kind, [
702
- selection.id,
703
- ])[0][anchorReferenceValue];
704
- return {
705
- kind: anchor.kind,
706
- transform: anchor.transform,
707
- members: this.elements,
708
- topology: {
709
- ...context,
710
- selection,
711
- },
712
- };
713
- }
714
- material(material) {
715
- return this.copy({ material: captureModelMaterial(material) }, storedOperation('material', [{ model: this, role: 'source', index: 0 }]));
716
- }
717
- originOffset(dx, dy, dz) {
718
- const offset = [dx, dy, dz];
719
- assertFiniteVector('originOffset', offset);
720
- return this.withOrigin(offset, { kind: 'originOffset' });
721
- }
722
- originPoint(point) {
723
- const reference = anchorReference(point);
724
- if (reference.kind !== 'point')
725
- throw new Error('originPoint() requires a point reference.');
726
- const [element] = this.localElements([point]);
727
- return this.withOrigin(element.transform.position, {
728
- kind: 'originPoint',
729
- model: reference.model,
730
- });
731
- }
732
- originVertex(id) {
733
- const geometry = this.requireGeometry().value;
734
- const [{ position }] = topologyVertexPoints(geometry.shape, geometry.topology.vertices, [id]);
735
- return this.withOrigin(position, { kind: 'originVertex', id });
736
- }
737
- originCenter() {
738
- this.requireGeometry();
739
- const position = this.elements.center.transform.position;
740
- return this.withOrigin(position, { kind: 'originCenter' });
741
- }
742
- withOrigin(offset, selection) {
743
- const transform = translation(negateVector(offset));
744
- const operation = storedOperation(selection.kind, [
745
- { model: this, role: 'source', index: 0 },
746
- ]);
747
- if (selection.kind === 'originPoint' && selection.model !== this)
748
- operation.inputs.push({
749
- model: selection.model,
750
- role: 'reference',
751
- index: 0,
752
- });
753
- operation.spatial = { origin, vector: offset };
754
- if (selection.kind === 'originVertex') {
755
- operation.selections.push({
756
- kind: 'vertex',
757
- input: this,
758
- ids: [selection.id],
759
- transform,
760
- });
761
- }
762
- return this.transformed(transform, operation);
763
- }
764
- rotate(x, y, z) {
765
- const angles = [x, y, z];
766
- assertFiniteVector('rotate', angles);
767
- const operation = storedOperation('rotate', [
768
- { model: this, role: 'source', index: 0 },
769
- ]);
770
- operation.spatial = { origin, vector: angles };
771
- return this.transformed(rotationAround(origin, angles), operation);
772
- }
773
- transformed(transform, operation) {
774
- const overrides = {
775
- geometryAnchor: transformElement(this.geometryAnchor, transform),
776
- elements: Object.fromEntries(Object.entries(this.elements).map(([name, element]) => [
777
- name,
778
- transformElement(element, transform),
779
- ])),
780
- constraints: this.mapConstraintGeometry(element => transformElement(element, transform), point => composeTransforms(transform, translation(point)).position),
781
- };
782
- if (this.assembly)
783
- return this.copy({
784
- ...overrides,
785
- assembly: transformSolveContext(this.assembly, transform),
786
- }, operation);
787
- const source = this.requireGeometry();
788
- const geometry = evaluateModelGeometry('transform', [transform.position, transform.quaternion], [source], () => {
789
- const shape = shapeWithTransform(source.value.shape, transform);
790
- try {
791
- return {
792
- shape,
793
- topology: preserveShapeTopology(shape, source.value.topology),
794
- referenceBasis: transformedReferenceBasis(source, transform, 1),
795
- };
796
- }
797
- catch (error) {
798
- shape.delete();
799
- throw error;
800
- }
801
- });
802
- return this.copyWithGeometry(geometry, overrides, operation);
803
- }
804
- mapConstraintGeometry(mapElement, mapPoint) {
805
- const mapReference = (reference) => reference.model ? reference : { ...reference, ...mapElement(reference) };
806
- return this.constraints.map(constraint => {
807
- const mapped = {
808
- ...constraint,
809
- source: mapReference(constraint.source),
810
- target: mapReference(constraint.target),
811
- rotations: constraint.rotations.map(action => ({
812
- ...action,
813
- pivot: action.pivot.kind === 'around'
814
- ? { ...action.pivot, axis: mapReference(action.pivot.axis) }
815
- : action.pivot.kind === 'pivot'
816
- ? { ...action.pivot, point: mapPoint(action.pivot.point) }
817
- : action.pivot,
818
- })),
819
- };
820
- valueTraces.set(mapped, valueTrace(constraint));
821
- return mapped;
822
- });
823
- }
824
- scaled(factor) {
825
- assertPositive('scale', factor);
826
- const source = this.requireGeometry();
827
- const geometry = evaluateModelGeometry('scaled', [factor], [source], () => {
828
- const shape = shapeWithScale(source.value.shape, factor);
829
- try {
830
- return {
831
- shape,
832
- topology: preserveShapeTopology(shape, source.value.topology),
833
- referenceBasis: transformedReferenceBasis(source, identityRigidTransform, factor),
834
- };
835
- }
836
- catch (error) {
837
- shape.delete();
838
- throw error;
839
- }
840
- });
841
- return this.copyWithGeometry(geometry, {
842
- geometryAnchor: scaleElement(this.geometryAnchor, factor),
843
- elements: scaleElements(this.elements, factor),
844
- constraints: this.mapConstraintGeometry(element => scaleElement(element, factor), point => scaleFrame(translation(point), factor).position),
845
- }, storedOperation('scaled', [{ model: this, role: 'source', index: 0 }]));
846
- }
847
- extrude(distance) {
848
- if (this.kind !== 'face')
849
- throw new Error('extrude requires a single face model.');
850
- if (!Number.isFinite(distance) || distance === 0)
851
- throw new Error('Extrusion distance must be finite and non-zero.');
852
- const source = this.requireGeometry();
853
- const direction = rotateVector([0, distance, 0], this.geometryAnchor.transform.quaternion);
854
- const geometry = evaluateSolidGeometry('extrude', [direction], [source], () => extrudeWithTopology({
855
- shape: source.value.shape,
856
- topology: source.value.topology,
857
- index: 1,
858
- }, direction));
859
- return ModelObject.create({
860
- kind: 'solid',
861
- name: 'Extrude',
862
- geometry,
863
- material: this.materialSnapshot,
864
- constraints: this.constraints,
865
- sourceRefs: this.sourceRefs,
866
- parameters: this.allParameters(),
867
- meshTolerance: this.meshTolerance,
868
- operation: storedOperation('extrude', [{ model: this, role: 'receiver', index: 0 }], {
869
- dimensions: {
870
- distance: {
871
- origin: this.elements.center.transform.position,
872
- vector: direction,
873
- },
874
- },
875
- }),
876
- });
877
- }
878
- cut(tools) {
879
- return cut(this, tools);
880
- }
881
- fillet(radius, edgeIds) {
882
- assertPositive('radius', radius);
883
- const source = this.requireSolidGeometry();
884
- const selectedEdgeIds = resolveEdgeSelection(source.value.topology.edges, edgeIds);
885
- const geometry = evaluateSolidGeometry('fillet', [radius, selectedEdgeIds], [source], () => {
886
- const result = filletEdges(source.value.shape, source.value.topology, radius, selectedEdgeIds);
887
- return { shape: result.shape, topology: result.topology };
888
- });
889
- return this.copyWithGeometry(geometry, {}, storedOperation('fillet', [{ model: this, role: 'source', index: 0 }], {
890
- selections: [
891
- {
892
- kind: 'edge',
893
- input: this,
894
- ids: selectedEdgeIds,
895
- transform: identityRigidTransform,
896
- },
897
- ],
898
- }));
899
- }
900
- chamfer(distance, edgeIds) {
901
- assertPositive('distance', distance);
902
- const source = this.requireSolidGeometry();
903
- const selectedEdgeIds = resolveEdgeSelection(source.value.topology.edges, edgeIds);
904
- const geometry = evaluateSolidGeometry('chamfer', [distance, selectedEdgeIds], [source], () => {
905
- const result = chamferEdges(source.value.shape, source.value.topology, distance, selectedEdgeIds);
906
- return { shape: result.shape, topology: result.topology };
907
- });
908
- return this.copyWithGeometry(geometry, {}, storedOperation('chamfer', [{ model: this, role: 'source', index: 0 }], {
909
- selections: [
910
- {
911
- kind: 'edge',
912
- input: this,
913
- ids: selectedEdgeIds,
914
- transform: identityRigidTransform,
915
- },
916
- ],
917
- }));
918
- }
919
- shell(thickness, removedSurfaceIds = []) {
920
- if (!Number.isFinite(thickness) || thickness === 0) {
921
- throw new Error('thickness must be a nonzero finite number.');
922
- }
923
- const source = this.requireSolidGeometry();
924
- const selectedIds = resolveTopologySelection('surface', source.value.topology.surfaces, removedSurfaceIds);
925
- const geometry = evaluateSolidGeometry('shell', [thickness, selectedIds], [source], () => shellWithTopology(source.value.shape, source.value.topology, thickness, selectedIds));
926
- return this.copyWithGeometry(geometry, {}, storedOperation('shell', [{ model: this, role: 'source', index: 0 }], {
927
- selections: [
928
- {
929
- kind: 'surface',
930
- input: this,
931
- ids: selectedIds,
932
- transform: identityRigidTransform,
933
- },
934
- ],
935
- }));
936
- }
937
- /** @internal */
938
- attachSource(sourceRef) {
939
- const previous = this.sourceRefs.at(-1);
940
- if (previous?.file !== sourceRef.file ||
941
- previous.start !== sourceRef.start ||
942
- previous.end !== sourceRef.end) {
943
- this.sourceRefs.push(sourceRef);
944
- }
945
- }
946
- /** @internal */
947
- attachParameters(parameters) {
948
- appendUniqueParameters(this.parameters, parameters);
949
- }
950
- /** @internal */
951
- attachOperationTrace(siteId, execution, order, sourceRef) {
952
- if (operationTraces.has(this.operation)) {
953
- return;
954
- }
955
- operationTraces.set(this.operation, { siteId, execution, order, sourceRef });
956
- }
957
- /** @internal */
958
- relatedObjects() {
959
- return [
960
- ...this.children,
961
- ...this.operation.inputs.map(input => input.model),
962
- ...this.constraints.flatMap(constraintReferences),
963
- ];
964
- }
965
- /** @internal */
966
- toSnapshot(meshCache = new Map()) {
967
- const solveContext = ModelObject.createSolveContext([this]);
968
- return this.snapshotNode(meshCache, solveContext);
969
- }
970
- snapshotNode(meshCache, solveContext, inComposition = false, overrideMaterial) {
971
- const material = overrideMaterial ?? this.materialSnapshot;
972
- const pose = this.solvePose(solveContext);
973
- const constraints = this.constraints.map(constraint => this.constraintSnapshot(constraint, solveContext));
974
- const parameters = uniqueParameters([
975
- ...this.parameters,
976
- ...this.constraints.flatMap(constraint => valueTrace(constraint).parameters),
977
- ]);
978
- const common = {
979
- nodeId: this.nodeId,
980
- kind: this.kind,
981
- name: this.name,
982
- material,
983
- compositionTransform: toTransform(pose),
984
- transform: toTransform(inComposition ? pose : identityRigidTransform),
985
- constraints,
986
- origin,
987
- elements: snapshotElements({
988
- ...this.elements,
989
- ...(this.geometry || this.children.length
990
- ? Object.fromEntries(Object.keys(boundDirections).map(direction => [
991
- direction,
992
- anchorReference(this[direction]),
993
- ]))
994
- : {}),
995
- }),
996
- sourceRefs: [...this.sourceRefs],
997
- parameters,
998
- operation: this.operationSnapshot(meshCache),
999
- };
1000
- if (this.kind === 'group') {
1001
- const childContext = this.assembly;
1002
- return {
1003
- ...common,
1004
- children: this.children.map(child => child.snapshotNode(meshCache, childContext, true, material)),
1005
- };
1006
- }
1007
- const geometry = this.requireGeometry();
1008
- return {
1009
- ...common,
1010
- children: [],
1011
- mesh: renderMesh(geometry, geometry.value.shape, meshCache, this.meshTolerance, geometry.value.topology),
1012
- };
1013
- }
1014
- /** @internal */
1015
- disposeShape(disposed) {
1016
- const shapes = [
1017
- ...(this.geometry ? [this.geometry.value.shape] : []),
1018
- ...(this.geometry?.value.referenceBasis
1019
- ? [this.geometry.value.referenceBasis.shape]
1020
- : []),
1021
- ...this.operation.regions.map(region => region.artifact.value),
1022
- ];
1023
- for (const shape of shapes) {
1024
- if (!disposed.has(shape)) {
1025
- disposed.add(shape);
1026
- shape.delete();
1027
- }
1028
- }
1029
- this.children.forEach(child => child.disposeShape(disposed));
1030
- }
1031
- /** @internal */
1032
- [loftModels](others, spine, ruled) {
1033
- const sections = [this, ...others];
1034
- const solveContext = ModelObject.createSolveContext([
1035
- ...sections,
1036
- ...(spine ? [spine] : []),
1037
- ]);
1038
- const resultPose = this.solvePose(solveContext);
1039
- const sectionInputs = sections.map(section => ({
1040
- model: section,
1041
- geometry: section.requireGeometry(),
1042
- transform: relativeTransform(section.solvePose(solveContext), resultPose),
1043
- }));
1044
- const spineInput = spine
1045
- ? {
1046
- model: spine,
1047
- geometry: spine.requireGeometry(),
1048
- transform: relativeTransform(spine.solvePose(solveContext), resultPose),
1049
- }
1050
- : undefined;
1051
- const geometry = evaluateSolidGeometry(spine ? 'spine-loft' : 'loft', [
1052
- ruled,
1053
- sectionInputs.map(input => [
1054
- input.transform.position,
1055
- input.transform.quaternion,
1056
- ]),
1057
- spineInput
1058
- ? [spineInput.transform.position, spineInput.transform.quaternion]
1059
- : null,
1060
- ], [
1061
- ...sectionInputs.map(input => input.geometry),
1062
- ...(spineInput ? [spineInput.geometry] : []),
1063
- ], () => buildLoftGeometry(sectionInputs, spineInput, ruled));
1064
- const inputs = [...sections, ...(spine ? [spine] : [])];
1065
- return ModelObject.create({
1066
- kind: 'solid',
1067
- name: 'Loft',
1068
- geometry,
1069
- material: this.materialSnapshot,
1070
- constraints: this.constraints,
1071
- sourceRefs: inputs.flatMap(input => input.sourceRefs),
1072
- parameters: uniqueParameters(inputs.flatMap(input => input.allParameters())),
1073
- meshTolerance: Math.min(...inputs.map(input => input.meshTolerance)),
1074
- operation: storedOperation('loft', [
1075
- { model: this, role: 'receiver', index: 0 },
1076
- ...others.map((model, index) => ({
1077
- model,
1078
- role: 'section',
1079
- index: index + 1,
1080
- })),
1081
- ...(spine
1082
- ? [{ model: spine, role: 'spine', index: sections.length }]
1083
- : []),
1084
- ]),
1085
- });
1086
- }
1087
- /** @internal */
1088
- [combineModels](operation, others) {
1089
- const evaluation = this.evaluateBoolean(operation, others);
1090
- let transferred = false;
1091
- try {
1092
- const combined = ModelObject.create({
1093
- kind: 'solid',
1094
- geometryAnchor: this.geometryAnchor,
1095
- geometry: evaluation.geometry,
1096
- name: this.name,
1097
- material: this.materialSnapshot,
1098
- constraints: this.constraints,
1099
- sourceRefs: [this, ...others].flatMap(model => model.sourceRefs),
1100
- parameters: uniqueParameters([this, ...others].flatMap(model => model.allParameters())),
1101
- meshTolerance: Math.min(this.meshTolerance, ...others.map(model => model.meshTolerance)),
1102
- operation: storedOperation(operation === 'fuse' ? 'union' : operation, [
1103
- { model: this, role: 'receiver', index: 0 },
1104
- ...others.map((model, index) => ({
1105
- model,
1106
- role: operation === 'cut' ? 'tool' : 'operand',
1107
- index: index + 1,
1108
- })),
1109
- ], { regions: evaluation.regions }),
1110
- });
1111
- transferred = true;
1112
- return combined;
1113
- }
1114
- finally {
1115
- if (!transferred) {
1116
- disposeModelGeometryValue(evaluation.geometry.value);
1117
- evaluation.regions.forEach(region => region.artifact.value.delete());
1118
- }
1119
- }
1120
- }
1121
- evaluateBoolean(operation, others) {
1122
- const solveContext = ModelObject.createSolveContext([this, ...others]);
1123
- const targetPose = this.solvePose(solveContext);
1124
- let geometry = this.requireSolidGeometry();
1125
- let temporaryGeometry;
1126
- const regions = [];
1127
- let evaluated = false;
1128
- try {
1129
- for (const [otherIndex, other] of others.entries()) {
1130
- const otherGeometry = other.requireSolidGeometry();
1131
- const transform = relativeTransform(other.solvePose(solveContext), targetPose);
1132
- const operand = evaluateKernelShape('transform', [transform.position, transform.quaternion], [otherGeometry], () => shapeWithTransform(otherGeometry.value.shape, transform));
1133
- try {
1134
- if (operation === 'cut' || operation === 'fuse') {
1135
- regions.push({
1136
- kind: 'intersection',
1137
- input: other,
1138
- frame: this,
1139
- artifact: evaluateKernelShape('boolean-intersection-region', [], [geometry, operand], () => geometry.value.shape.intersect(operand.value)),
1140
- });
1141
- }
1142
- if (operation === 'fuse') {
1143
- regions.push({
1144
- kind: 'section',
1145
- input: other,
1146
- frame: this,
1147
- artifact: evaluateKernelShape('boolean-section-region', [], [geometry, operand], () => unionSectionShape(geometry.value.shape, operand.value)),
1148
- });
1149
- }
1150
- const nextGeometry = evaluateSolidGeometry(`boolean-${operation}`, [otherIndex], [geometry, operand], () => {
1151
- const result = booleanWithTopology({
1152
- shape: geometry.value.shape,
1153
- topology: geometry.value.topology,
1154
- index: otherIndex === 0 ? 1 : undefined,
1155
- }, {
1156
- shape: operand.value,
1157
- topology: otherGeometry.value.topology,
1158
- index: otherIndex + 2,
1159
- }, operation);
1160
- return { shape: result.shape, topology: result.topology };
1161
- });
1162
- temporaryGeometry?.value.shape.delete();
1163
- temporaryGeometry = nextGeometry;
1164
- geometry = nextGeometry;
1165
- }
1166
- finally {
1167
- operand.value.delete();
1168
- }
1169
- }
1170
- evaluated = true;
1171
- return { geometry, regions };
1172
- }
1173
- finally {
1174
- if (!evaluated) {
1175
- temporaryGeometry?.value.shape.delete();
1176
- regions.forEach(region => region.artifact.value.delete());
1177
- }
1178
- }
1179
- }
1180
- allParameters() {
1181
- return uniqueParameters([
1182
- ...this.parameters,
1183
- ...this.constraints.flatMap(constraint => valueTrace(constraint).parameters),
1184
- ]);
1185
- }
1186
- solvePose(context) {
1187
- return context.poses.get(this);
1188
- }
1189
- memberPoses() {
1190
- const members = new Map([
1191
- [this, identityRigidTransform],
1192
- ]);
1193
- if (this.children.length) {
1194
- const context = this.assembly;
1195
- for (const child of this.children) {
1196
- const pose = child.solvePose(context);
1197
- for (const [member, localPose] of child.memberPoses()) {
1198
- members.set(member, members.has(member) || localPose === null
1199
- ? null
1200
- : composeTransforms(pose, localPose));
1201
- }
1202
- }
1203
- }
1204
- return members;
1205
- }
1206
- /** Bounds of the selected finite geometry after a rigid transform. */
1207
- [referenceBoundsParts](reference, transform) {
1208
- if (reference.bound) {
1209
- const frame = composeTransforms(transform, reference.transform);
1210
- const [x, z] = reference.bound.size;
1211
- return [
1212
- {
1213
- bounds: pointBounds([-1, 1].flatMap(a => [-1, 1].map(b => composeTransforms(frame, translation([(a * x) / 2, 0, (b * z) / 2])).position))),
1214
- },
1215
- ];
1216
- }
1217
- if (reference.whole) {
1218
- if (this.geometry)
1219
- return [boundsQuery(this.geometry, transform)];
1220
- const context = this.assembly;
1221
- return this.children.flatMap(child => child[referenceBoundsParts](child.relationAnchorReference(), composeTransforms(transform, child.solvePose(context))));
1222
- }
1223
- if (reference.topology) {
1224
- const topology = reference.topology;
1225
- return [
1226
- boundsQuery(topology.source.requireGeometry(), composeTransforms(transform, topology.transform), topology.selection, topology.scale),
1227
- ];
1228
- }
1229
- if (reference.kind === 'point') {
1230
- const point = composeTransforms(transform, reference.transform).position;
1231
- return [{ bounds: [point, point] }];
1232
- }
1233
- throw new Error(`The reference ${reference.name} has no finite geometry. Select a model, vertex, edge, or surface for on().`);
1234
- }
1235
- /** Bounds of the selected finite geometry after a rigid transform. */
1236
- [referenceBounds](reference, transform) {
1237
- const bounds = this[referenceBoundsParts](reference, transform).map(part => 'bounds' in part
1238
- ? part.bounds
1239
- : evaluateSnapshotQuery(part));
1240
- return bounds.length === 1 ? bounds[0] : combineBounds(bounds);
1241
- }
1242
- /** Pure query collection: it never substitutes geometry or runs author code. */
1243
- [modelSnapshotQueries]() {
1244
- const result = [];
1245
- if (this.geometry || this.children.length) {
1246
- for (const direction of Object.values(boundDirections)) {
1247
- const transform = invertTransform(rotation(frameFromYAxis(origin, direction).quaternion));
1248
- for (const part of this[referenceBoundsParts](this.relationAnchorReference(), transform))
1249
- if (!('bounds' in part))
1250
- result.push(part);
1251
- }
1252
- }
1253
- if (this.geometry)
1254
- result.push(meshQuery(this.geometry, this.geometry.value.shape, this.meshTolerance, this.geometry.value.topology));
1255
- for (const region of this.operation.regions)
1256
- result.push(meshQuery(region.artifact, region.artifact.value, this.meshTolerance));
1257
- return result;
1258
- }
1259
- alignmentGeometry(reference) {
1260
- if (reference.kind === 'point')
1261
- return {
1262
- kind: 'point',
1263
- point: reference.whole
1264
- ? this.requireGeometry().value.shape.asTuple()
1265
- : reference.transform.position,
1266
- };
1267
- const read = (shape) => reference.kind === 'line'
1268
- ? edgeGeometry(shape, reference.direction ?? 1)
1269
- : faceGeometry(shape, reference.facing ?? reference.bound?.facing ?? 1);
1270
- if (reference.topology) {
1271
- const topology = reference.topology, geometry = topology.source.requireGeometry().value;
1272
- return withTopologyShape(geometry.shape, geometry.topology, topology.selection, shape => {
1273
- const scaled = topology.scale === 1
1274
- ? shape
1275
- : shapeWithScale(shape, topology.scale);
1276
- try {
1277
- return transformGeometry(read(scaled), topology.transform);
1278
- }
1279
- finally {
1280
- if (scaled !== shape)
1281
- scaled.delete();
1282
- }
1283
- });
1284
- }
1285
- if (reference.whole)
1286
- return read(this.requireGeometry().value.shape);
1287
- const direction = rotateVector([
1288
- 0,
1289
- reference.kind === 'line'
1290
- ? (reference.direction ?? 1)
1291
- : (reference.facing ?? reference.bound?.facing ?? 1),
1292
- 0,
1293
- ], reference.transform.quaternion);
1294
- return reference.kind === 'line'
1295
- ? { kind: 'line', point: reference.transform.position, direction }
1296
- : { kind: 'plane', point: reference.transform.position, normal: direction };
1297
- }
1298
- relationElement(reference) {
1299
- const element = reference.whole
1300
- ? {
1301
- ...this.exposedElement(),
1302
- name: reference.name,
1303
- direction: reference.direction,
1304
- facing: reference.facing,
1305
- }
1306
- : reference;
1307
- const topology = element.topology;
1308
- let arrow;
1309
- if (reference.kind === 'line' && topology) {
1310
- const geometry = topology.source.requireGeometry().value;
1311
- arrow = withTopologyShape(geometry.shape, geometry.topology, topology.selection, shape => {
1312
- const edge = shape;
1313
- const position = (reference.direction ?? 1) === 1 ? 1 : 0;
1314
- const point = edge.pointAt(position), tangent = edge.tangentAt(position);
1315
- try {
1316
- return topologyTransform(topology, frameFromYAxis(point.toTuple(), tangent
1317
- .toTuple()
1318
- .map(v => v * (reference.direction ?? 1))));
1319
- }
1320
- finally {
1321
- point.delete();
1322
- tangent.delete();
1323
- }
1324
- });
1325
- }
1326
- return {
1327
- ...snapshotElements({ [reference.name]: element })[0],
1328
- arrow,
1329
- };
1330
- }
1331
- bodyRotations(constraint, indices) {
1332
- return constraint.rotations.map(({ pivot, angles }) => {
1333
- if (pivot.kind === 'around') {
1334
- return pivot.axis.model
1335
- ? {
1336
- body: indices.get(pivot.axis.model),
1337
- axis: pivot.axis.transform,
1338
- angle: angles * (pivot.axis.direction ?? 1),
1339
- }
1340
- : {
1341
- local: axisRotation(pivot.axis.transform, angles * (pivot.axis.direction ?? 1)),
1342
- };
1343
- }
1344
- const frame = translation(pivot.kind === 'pivot'
1345
- ? pivot.point
1346
- : anchorReference(this.vertex(pivot.id)).transform.position);
1347
- return {
1348
- local: composeTransforms(composeTransforms(frame, rotationAround(origin, angles)), invertTransform(frame)),
1349
- };
1350
- });
1351
- }
1352
- /** @internal */
1353
- [relationPreview](nodeId, constraint, selection) {
1354
- // This expression starts from the relate receiver, before sibling constraints
1355
- // are returned and committed to self. Geometry and node identity stay shared.
1356
- const owner = this.copy({ nodeId, constraints: [...this.constraints, constraint] }, this.operation);
1357
- const context = ModelObject.createSolveContext([
1358
- owner,
1359
- ...(selection?.pivot.kind === 'around' && selection.pivot.axis.model
1360
- ? [selection.pivot.axis.model]
1361
- : []),
1362
- ]);
1363
- return {
1364
- object: {
1365
- nodeId,
1366
- compositionTransform: toTransform(owner.solvePose(context)),
1367
- constraints: owner.constraints.map(value => owner.constraintSnapshot(value, context)),
1368
- },
1369
- spatial: selection
1370
- ? owner.constraintSpatial(constraint, selection, context)
1371
- : undefined,
1372
- };
1373
- }
1374
- constraintSpatial(constraint, selection, context) {
1375
- const models = [...context.poses.keys()];
1376
- const actions = this.bodyRotations(constraint, new Map(models.map((model, i) => [model, i])));
1377
- const stage = actions.length - (selection.kind === 'rotate' ? 1 : 0);
1378
- const finalPose = this.solvePose(context);
1379
- let before = finalPose;
1380
- for (let i = actions.length - 1; i >= stage; i--) {
1381
- const action = actions[i];
1382
- before =
1383
- 'local' in action
1384
- ? composeTransforms(before, invertTransform(action.local))
1385
- : composeTransforms(axisRotation(composeTransforms(models[action.body].solvePose(context), action.axis), -action.angle), before);
1386
- }
1387
- const pivot = selection.pivot;
1388
- let frame;
1389
- if (pivot.kind === 'around') {
1390
- frame = composeTransforms(pivot.axis.model ? pivot.axis.model.solvePose(context) : before, (pivot.axis.direction ?? 1) === 1
1391
- ? pivot.axis.transform
1392
- : composeTransforms(pivot.axis.transform, rotation([1, 0, 0, 0])));
1393
- }
1394
- else {
1395
- const local = translation(pivot.kind === 'pivot'
1396
- ? pivot.point
1397
- : anchorReference(this.vertex(pivot.id)).transform.position);
1398
- frame = composeTransforms(before, local);
1399
- }
1400
- const angles = constraint.rotations[stage]?.angles ?? origin;
1401
- const rotationVector = typeof angles === 'number' ? [0, angles, 0] : angles;
1402
- const localFrame = relativeTransform(frame, finalPose);
1403
- return {
1404
- nodeId: this.nodeId,
1405
- kind: selection.kind,
1406
- spatial: {
1407
- origin: localFrame.position,
1408
- vector: selection.kind === 'rotate'
1409
- ? rotationVector
1410
- : pivot.kind === 'pivot'
1411
- ? pivot.point
1412
- : origin,
1413
- frame: localFrame,
1414
- rotation: rotationVector,
1415
- axisOnly: pivot.kind === 'around',
1416
- },
1417
- };
1418
- }
1419
- /** Fix the assembly frame once, using only its direct member origins. */
1420
- static createAssembly(children) {
1421
- const context = ModelObject.createSolveContext(children);
1422
- const center = children.length
1423
- ? boundsCenter(pointBounds(children.map(child => child.solvePose(context).position)))
1424
- : origin;
1425
- return transformSolveContext(context, translation(negateVector(center)));
1426
- }
1427
- static createSolveContext(roots) {
1428
- const models = [];
1429
- const indices = new Map();
1430
- const collect = (model) => {
1431
- if (indices.has(model))
1432
- return;
1433
- indices.set(model, models.length);
1434
- models.push(model);
1435
- model.constraints.flatMap(constraintReferences).forEach(collect);
1436
- };
1437
- roots.forEach(collect);
1438
- const poses = solveBodies(models.map((model) => ({
1439
- name: model.name,
1440
- relations: model.constraints.map(constraint => constraint.kind === 'align'
1441
- ? {
1442
- kind: 'align',
1443
- id: constraint.id,
1444
- source: {
1445
- body: indices.get(constraint.source.model ?? model),
1446
- geometry: (constraint.source.model ?? model).alignmentGeometry(constraint.source),
1447
- transform: constraint.source.transform,
1448
- },
1449
- target: {
1450
- body: indices.get(constraint.target.model ?? model),
1451
- geometry: (constraint.target.model ?? model).alignmentGeometry(constraint.target),
1452
- transform: constraint.target.transform,
1453
- },
1454
- offset: constraint.offset,
1455
- rotations: model.bodyRotations(constraint, indices),
1456
- }
1457
- : {
1458
- kind: 'on',
1459
- id: constraint.id,
1460
- source: {
1461
- body: indices.get(constraint.source.model ?? model),
1462
- key: constraint.source.name,
1463
- bounds: (orientation) => (constraint.source.model ?? model)[referenceBounds](constraint.source, rotation(orientation)),
1464
- },
1465
- target: {
1466
- body: indices.get(constraint.target.model ?? model),
1467
- transform: constraint.target.transform,
1468
- facing: constraint.target.bound.facing,
1469
- },
1470
- offset: constraint.offset,
1471
- rotations: model.bodyRotations(constraint, indices),
1472
- }),
1473
- })));
1474
- return {
1475
- poses: new Map(models.map((model, index) => [model, poses[index]])),
1476
- };
1477
- }
1478
- constraintSnapshot(constraint, context) {
1479
- const source = constraint.source.model ?? this;
1480
- const target = constraint.target.model ?? this;
1481
- const models = [...context.poses.keys()];
1482
- const actions = this.bodyRotations(constraint, new Map(models.map((model, i) => [model, i])));
1483
- let before = this.solvePose(context);
1484
- for (const action of [...actions].reverse()) {
1485
- before =
1486
- 'local' in action
1487
- ? composeTransforms(before, invertTransform(action.local))
1488
- : composeTransforms(axisRotation(composeTransforms(models[action.body].solvePose(context), action.axis), -action.angle), before);
1489
- }
1490
- const sourcePose = source === this ? before : source.solvePose(context);
1491
- const targetPose = target === this ? before : target.solvePose(context);
1492
- let offsetFrame = composeTransforms(targetPose, constraint.target.transform);
1493
- if (constraint.kind === 'align') {
1494
- for (const action of actions)
1495
- if ('body' in action)
1496
- offsetFrame = composeTransforms(axisRotation(composeTransforms(models[action.body].solvePose(context), action.axis), action.angle), offsetFrame);
1497
- return {
1498
- id: constraint.id,
1499
- kind: 'align',
1500
- source: anchorSnapshot(source, constraint.source),
1501
- target: anchorSnapshot(target, constraint.target),
1502
- sourceElement: source.relationElement(constraint.source),
1503
- targetElement: target.relationElement(constraint.target),
1504
- offsetDirection: 1,
1505
- offset: constraint.offset ?? origin,
1506
- offsetFrame: toTransform(offsetFrame),
1507
- sourceRefs: [...valueTrace(constraint).sourceRefs],
1508
- parameters: [...valueTrace(constraint).parameters],
1509
- };
1510
- }
1511
- const orientation = composeTransforms(invertTransform(rotation(offsetFrame.quaternion)), rotation(sourcePose.quaternion)).quaternion;
1512
- const bounds = source[referenceBounds](constraint.source, rotation(orientation));
1513
- const center = [
1514
- (bounds[0][0] + bounds[1][0]) / 2,
1515
- bounds[constraint.target.bound.facing === 1 ? 0 : 1][1],
1516
- (bounds[0][2] + bounds[1][2]) / 2,
1517
- ];
1518
- const sourceFrame = relativeTransform({
1519
- position: addVectors(sourcePose.position, rotateVector(center, offsetFrame.quaternion)),
1520
- quaternion: offsetFrame.quaternion,
1521
- }, sourcePose);
1522
- const sourceSize = [
1523
- bounds[1][0] - bounds[0][0],
1524
- bounds[1][1] - bounds[0][1],
1525
- bounds[1][2] - bounds[0][2],
1526
- ];
1527
- for (const action of actions) {
1528
- if ('body' in action)
1529
- offsetFrame = composeTransforms(axisRotation(composeTransforms(models[action.body].solvePose(context), action.axis), action.angle), offsetFrame);
1530
- }
1531
- return {
1532
- id: constraint.id,
1533
- kind: constraint.kind,
1534
- source: anchorSnapshot(source, constraint.source),
1535
- target: anchorSnapshot(target, constraint.target),
1536
- sourceBounds: {
1537
- size: sourceSize,
1538
- transform: toTransform(composeTransforms(sourceFrame, {
1539
- position: [
1540
- 0,
1541
- (constraint.target.bound.facing * sourceSize[1]) / 2,
1542
- 0,
1543
- ],
1544
- quaternion: [0, 0, 0, 1],
1545
- })),
1546
- },
1547
- sourceElement: {
1548
- name: `${constraint.source.name}:bound`,
1549
- kind: 'face',
1550
- transform: toTransform(sourceFrame),
1551
- bound: {
1552
- size: [sourceSize[0], sourceSize[2]],
1553
- facing: constraint.target.bound.facing === 1 ? -1 : 1,
1554
- },
1555
- },
1556
- targetElement: {
1557
- name: constraint.target.name,
1558
- kind: 'face',
1559
- transform: toTransform(constraint.target.transform),
1560
- bound: constraint.target.bound,
1561
- },
1562
- offsetDirection: source === this ? 1 : -1,
1563
- offset: constraint.offset ?? origin,
1564
- offsetFrame: toTransform(offsetFrame),
1565
- sourceRefs: [...valueTrace(constraint).sourceRefs],
1566
- parameters: [...valueTrace(constraint).parameters],
1567
- };
1568
- }
1569
- requireGeometry() {
1570
- if (!this.geometry) {
1571
- throw new Error('This operation requires geometry and cannot act on a group.');
1572
- }
1573
- return this.geometry;
1574
- }
1575
- requireSolidGeometry() {
1576
- if (this.kind !== 'solid') {
1577
- throw new Error('This operation requires a solid model.');
1578
- }
1579
- return this.requireGeometry();
1580
- }
1581
- operationSnapshot(meshCache) {
1582
- const { kind, inputs, regions, selections } = this.operation;
1583
- const { siteId, execution, order, sourceRef } = operationTraces.get(this.operation) ?? {};
1584
- return {
1585
- id: storedOperationId(this.operation),
1586
- siteId,
1587
- execution,
1588
- kind,
1589
- order,
1590
- outputNodeId: this.nodeId,
1591
- inputs: inputs.map(({ model, role, index }) => ({
1592
- nodeId: model.nodeId,
1593
- role,
1594
- index,
1595
- })),
1596
- regions: regions.map(region => ({
1597
- kind: region.kind,
1598
- inputNodeId: region.input.nodeId,
1599
- frameNodeId: region.frame.nodeId,
1600
- mesh: renderMesh(region.artifact, region.artifact.value, meshCache, this.meshTolerance),
1601
- })),
1602
- selections: selections.map(selection => ({
1603
- kind: selection.kind,
1604
- inputNodeId: selection.input.nodeId,
1605
- ids: [...selection.ids],
1606
- transform: toTransform(selection.transform),
1607
- })),
1608
- sourceRef,
1609
- spatial: this.operation.spatial,
1610
- dimensions: this.operation.dimensions,
1611
- };
1612
- }
1613
- copyWithGeometry(geometry, overrides, operation) {
1614
- try {
1615
- return this.copy({ ...overrides, geometry }, operation);
1616
- }
1617
- catch (error) {
1618
- disposeModelGeometryValue(geometry.value);
1619
- throw error;
1620
- }
1621
- }
1622
- copy(overrides, operation) {
1623
- return ModelObject.create({
1624
- kind: this.kind,
1625
- geometry: this.geometry,
1626
- geometryAnchor: this.geometryAnchor,
1627
- name: this.name,
1628
- material: this.materialSnapshot,
1629
- children: this.children,
1630
- assembly: this.assembly,
1631
- constraints: this.constraints,
1632
- elements: this.elements,
1633
- sourceRefs: this.sourceRefs,
1634
- parameters: this.parameters,
1635
- meshTolerance: this.meshTolerance,
1636
- operation,
1637
- ...overrides,
1638
- });
1639
- }
1640
- }
1641
- export function circle(radius = 5) {
1642
- assertPositive('radius', radius);
1643
- return planarFaceModel('circle', 'Circle', [radius], () => sketchCircle(radius, { plane: 'XZ' }));
1644
- }
1645
- export function ellipse(xRadius = 5, zRadius = 3) {
1646
- assertPositive('xRadius', xRadius);
1647
- assertPositive('zRadius', zRadius);
1648
- return planarFaceModel('ellipse', 'Ellipse', [xRadius, zRadius], () => sketchEllipse(xRadius, zRadius, { plane: 'XZ' }));
1649
- }
1650
- export function rectangle(x = 10, z = 10) {
1651
- assertPositive('x', x);
1652
- assertPositive('z', z);
1653
- return planarFaceModel('rectangle', 'Rectangle', [x, z], () => sketchRectangle(x, z, { plane: 'XZ' }));
1654
- }
1655
- export function regularPolygon(radius = 5, sides = 6, rotation = 0) {
1656
- assertPositive('radius', radius);
1657
- if (!Number.isInteger(sides) || sides < 3) {
1658
- throw new Error('sides must be an integer greater than or equal to 3.');
1659
- }
1660
- if (!Number.isFinite(rotation)) {
1661
- throw new Error('rotation must be a finite number.');
1662
- }
1663
- return planarFaceModel('regularPolygon', `${sides}-sided polygon`, [radius, sides, rotation], () => sketchPolysides(radius, sides, 0, { plane: 'XZ' }), face => rotation === 0 ? face : face.rotate(rotation, toPoint(origin), [0, 1, 0]));
1664
- }
1665
- /**
1666
- * @code3d.param x {kind: 'length', label: 'X'}
1667
- * @code3d.param y {kind: 'length', label: 'Y'}
1668
- * @code3d.param z {kind: 'length', label: 'Z'}
1669
- */
1670
- export function point([x, y, z] = origin) {
1671
- const position = [x, y, z];
1672
- assertFiniteVector('point', position);
1673
- const geometry = evaluateModelGeometry('point', [position], [], () => ({
1674
- shape: makeVertex(toPoint(position)),
1675
- }));
1676
- return ModelObject.create({
1677
- kind: 'vertex',
1678
- name: 'Point',
1679
- geometry,
1680
- geometryAnchor: { kind: 'point', transform: translation(position) },
1681
- operation: storedOperation('point'),
1682
- });
1683
- }
1684
- export function line(startOrEnd, end) {
1685
- const start = end ? startOrEnd : origin;
1686
- end ??= startOrEnd;
1687
- assertCurvePoints('line', [start, end], 2);
1688
- return curveModel('line', 'Line', [start, end], () => makeLine(toPoint(start), toPoint(end)));
1689
- }
1690
- export function arc(start, middle, end) {
1691
- assertCurvePoints('arc', [start, middle, end], 3);
1692
- return curveModel('arc', 'Arc', [start, middle, end], () => makeThreePointArc(toPoint(start), toPoint(middle), toPoint(end)));
1693
- }
1694
- export function bezier(points) {
1695
- assertCurvePoints('bezier', points, 2);
1696
- return curveModel('bezier', 'Bezier curve', points, () => makeBezierCurve(points.map(toPoint)));
1697
- }
1698
- export function spline(points) {
1699
- assertCurvePoints('spline', points, 2);
1700
- return curveModel('spline', 'Spline', points, () => makeBSplineApproximation(points.map(toPoint)));
1701
- }
1702
- export function loft(sections, { spine, ruled = false } = {}) {
1703
- if (sections.length < 2) {
1704
- throw new Error('loft requires at least two planar sections.');
1705
- }
1706
- const runtimeSections = sections.map(section => requireModelKind(section, 'face', 'Every loft section must be a planar face model.'));
1707
- const runtimeSpine = spine
1708
- ? requireModelKind(spine, 'edge', 'A loft spine must be a curve model.')
1709
- : undefined;
1710
- const [first, ...others] = runtimeSections;
1711
- return first[loftModels](others, runtimeSpine, ruled);
1712
- }
1713
- export function box(x = 10, y = 10, z = 10) {
1714
- assertPositive('x', x);
1715
- assertPositive('y', y);
1716
- assertPositive('z', z);
1717
- return ModelObject.create({
1718
- kind: 'solid',
1719
- name: 'Box',
1720
- geometry: evaluateSolidGeometry('box', [x, y, z], [], () => ({
1721
- shape: centeredBoxShape(x, y, z),
1722
- })),
1723
- elements: solidElements([
1724
- [0, -y / 2, 0],
1725
- [0, y / 2, 0],
1726
- ]),
1727
- operation: storedOperation('box', [], {
1728
- dimensions: {
1729
- x: { origin: [-x / 2, -y / 2, -z / 2], vector: [x, 0, 0] },
1730
- y: { origin: [-x / 2, -y / 2, -z / 2], vector: [0, y, 0] },
1731
- z: { origin: [-x / 2, -y / 2, -z / 2], vector: [0, 0, z] },
1732
- },
1733
- }),
1734
- });
1735
- }
1736
- export function cylinder(radius = 5, y = 10) {
1737
- assertPositive('radius', radius);
1738
- assertPositive('y', y);
1739
- return ModelObject.create({
1740
- kind: 'solid',
1741
- name: 'Cylinder',
1742
- geometry: evaluateSolidGeometry('cylinder', [radius, y], [], () => ({
1743
- shape: makeCylinder(radius, y, [0, -y / 2, 0], [0, 1, 0]),
1744
- })),
1745
- elements: solidElements([
1746
- [0, -y / 2, 0],
1747
- [0, y / 2, 0],
1748
- ]),
1749
- operation: storedOperation('cylinder'),
1750
- });
1751
- }
1752
- export function tube(outerRadius = 5, innerRadius = 3, y = 10) {
1753
- assertPositive('outerRadius', outerRadius);
1754
- assertPositive('innerRadius', innerRadius);
1755
- assertPositive('y', y);
1756
- if (innerRadius >= outerRadius) {
1757
- throw new Error('innerRadius must be smaller than outerRadius.');
1758
- }
1759
- return ModelObject.create({
1760
- kind: 'solid',
1761
- name: 'Tube',
1762
- geometry: evaluateSolidGeometry('tube', [outerRadius, innerRadius, y], [], () => {
1763
- const outer = sketchCircle(outerRadius, {
1764
- plane: 'XZ',
1765
- origin: [0, -y / 2, 0],
1766
- });
1767
- const inner = sketchCircle(innerRadius, {
1768
- plane: 'XZ',
1769
- origin: [0, -y / 2, 0],
1770
- });
1771
- // Hole wires run opposite to the outer boundary.
1772
- inner.wire.wrapped.Reverse();
1773
- const section = makeFace(outer.wire, [inner.wire]);
1774
- const direction = new Vector([0, y, 0]);
1775
- try {
1776
- return { shape: basicFaceExtrusion(section, direction) };
1777
- }
1778
- finally {
1779
- direction.delete();
1780
- section.delete();
1781
- inner.delete();
1782
- outer.delete();
1783
- }
1784
- }),
1785
- elements: solidElements([
1786
- [0, -y / 2, 0],
1787
- [0, y / 2, 0],
1788
- ]),
1789
- operation: storedOperation('tube'),
1790
- });
1791
- }
1792
- export function coil(coilRadius = 5, wireRadius = 1, pitch = 3, turns = 3) {
1793
- assertPositive('coilRadius', coilRadius);
1794
- assertPositive('wireRadius', wireRadius);
1795
- assertPositive('pitch', pitch);
1796
- assertPositive('turns', turns);
1797
- if (wireRadius >= coilRadius) {
1798
- throw new Error('wireRadius must be smaller than coilRadius.');
1799
- }
1800
- if (pitch <= 2 * wireRadius) {
1801
- throw new Error('pitch must be greater than the wire diameter.');
1802
- }
1803
- assertCoilClearance(coilRadius, wireRadius, pitch, turns);
1804
- const y = pitch * turns;
1805
- assertPositive('pitch * turns', y);
1806
- const geometry = evaluateSolidGeometry('coil', [coilRadius, wireRadius, pitch, turns], [], () => {
1807
- const spine = makeHelix(pitch, y, coilRadius, [0, -y / 2, 0], [0, 1, 0]);
1808
- const start = spine.pointAt(0);
1809
- const tangent = spine.tangentAt(0);
1810
- const circle = makeCircle(wireRadius, start, tangent);
1811
- const section = assembleWire([circle]);
1812
- try {
1813
- return { shape: genericSweep(section, spine, { frenet: true }) };
1814
- }
1815
- finally {
1816
- section.delete();
1817
- circle.delete();
1818
- tangent.delete();
1819
- start.delete();
1820
- spine.delete();
1821
- }
1822
- });
1823
- // A fractional turn has asymmetric X/Z bounds, but its axis is still Y.
1824
- // The circular end sections extend beyond the centerline's Y interval.
1825
- const circumference = 2 * Math.PI * coilRadius;
1826
- const halfHeight = y / 2 + wireRadius * (circumference / Math.hypot(circumference, pitch));
1827
- return ModelObject.create({
1828
- kind: 'solid',
1829
- name: 'Coil',
1830
- geometry,
1831
- elements: solidElements([
1832
- [0, -halfHeight, 0],
1833
- [0, halfHeight, 0],
1834
- ]),
1835
- operation: storedOperation('coil'),
1836
- });
1837
- }
1838
- function assertCoilClearance(radius, wireRadius, pitch, turns) {
1839
- // Neighboring turns approach obliquely: pitch alone overestimates clearance.
1840
- // For angular separation t, squared centerline distance is
1841
- // 2 R² (1 - cos(t)) + (pitch * t / 2π)². Its only possible minimum
1842
- // between half a turn and a full turn lies after the derivative's minimum.
1843
- // Beyond a full turn, the Y separation already exceeds the wire diameter.
1844
- if (turns <= 0.5)
1845
- return;
1846
- const fullTurn = 2 * Math.PI;
1847
- const slopeSquared = (pitch / (fullTurn * radius)) ** 2;
1848
- if (slopeSquared >= 1)
1849
- return;
1850
- let lower = fullTurn - Math.acos(-slopeSquared);
1851
- if (Math.sin(lower) + slopeSquared * lower >= 0)
1852
- return;
1853
- let upper = fullTurn;
1854
- for (let iteration = 0; iteration < 48; iteration += 1) {
1855
- const middle = (lower + upper) / 2;
1856
- if (Math.sin(middle) + slopeSquared * middle < 0)
1857
- lower = middle;
1858
- else
1859
- upper = middle;
1860
- }
1861
- const separation = Math.min(fullTurn * turns, (lower + upper) / 2);
1862
- const distance = Math.hypot(2 * radius * Math.sin(separation / 2), (pitch * separation) / fullTurn);
1863
- if (distance <= 2 * wireRadius) {
1864
- throw new Error('Coil turns must not touch or overlap; increase pitch or decrease wireRadius.');
1865
- }
1866
- }
1867
- export function sphere(radius = 5) {
1868
- assertPositive('radius', radius);
1869
- return ModelObject.create({
1870
- kind: 'solid',
1871
- name: 'Sphere',
1872
- geometry: evaluateSolidGeometry('sphere', [radius], [], () => ({
1873
- shape: makeSphere(radius),
1874
- })),
1875
- elements: solidElements([
1876
- [0, -radius, 0],
1877
- [0, radius, 0],
1878
- ]),
1879
- operation: storedOperation('sphere'),
1880
- });
1881
- }
1882
- export function frustum(bottomRadius = 5, topRadius = 3, y = 10) {
1883
- assertPositive('bottomRadius', bottomRadius);
1884
- assertPositive('topRadius', topRadius);
1885
- assertPositive('y', y);
1886
- return ModelObject.create({
1887
- kind: 'solid',
1888
- name: 'Frustum',
1889
- geometry: evaluateSolidGeometry('frustum', [bottomRadius, topRadius, y], [], () => {
1890
- const bottom = sketchCircle(bottomRadius, {
1891
- plane: 'XZ',
1892
- origin: [0, -y / 2, 0],
1893
- });
1894
- const top = sketchCircle(topRadius, {
1895
- plane: 'XZ',
1896
- origin: [0, y / 2, 0],
1897
- });
1898
- return { shape: bottom.loftWith(top, { ruled: true }) };
1899
- }),
1900
- elements: solidElements([
1901
- [0, -y / 2, 0],
1902
- [0, y / 2, 0],
1903
- ]),
1904
- operation: storedOperation('frustum'),
1905
- });
1906
- }
1907
- export function regularPrism(radius = 5, y = 10, sides = 6, rotation = 0) {
1908
- assertPositive('radius', radius);
1909
- assertPositive('y', y);
1910
- if (!Number.isInteger(sides) || sides < 3) {
1911
- throw new Error('sides must be an integer greater than or equal to 3.');
1912
- }
1913
- if (!Number.isFinite(rotation)) {
1914
- throw new Error('rotation must be a finite number.');
1915
- }
1916
- return ModelObject.create({
1917
- kind: 'solid',
1918
- name: `${sides}-sided prism`,
1919
- geometry: evaluateSolidGeometry('regular-prism', [radius, y, sides, rotation], [], () => {
1920
- const sketch = sketchPolysides(radius, sides, 0, {
1921
- plane: 'XZ',
1922
- origin: [0, -y / 2, 0],
1923
- });
1924
- let shape = sketch.extrude(y, {
1925
- extrusionDirection: [0, 1, 0],
1926
- });
1927
- if (rotation !== 0) {
1928
- shape = shape.rotate(rotation, [0, 0, 0], [0, 1, 0]);
1929
- }
1930
- return { shape };
1931
- }),
1932
- elements: solidElements([
1933
- [0, -y / 2, 0],
1934
- [0, y / 2, 0],
1935
- ]),
1936
- operation: storedOperation('regularPrism'),
1937
- });
1938
- }
1939
- /** @internal */
1940
- export function modelFromReplicadSolid(shape) {
1941
- let solid;
1942
- try {
1943
- solid = normalizeReplicadSolid(shape);
1944
- }
1945
- catch (error) {
1946
- shape.delete();
1947
- throw error;
1948
- }
1949
- // The builder still executes on every call. Cache its actual output, not its
1950
- // arguments: captured state may change the geometry between invocations.
1951
- let adopted = false;
1952
- let geometry;
1953
- try {
1954
- geometry = evaluateSolidGeometry('replicad-solid', [solid.serialize()], [], () => {
1955
- adopted = true;
1956
- return { shape: solid };
1957
- });
1958
- }
1959
- finally {
1960
- // A hit returns an independently owned cached copy; discard this output.
1961
- // A miss transfers ownership to evaluateSolidGeometry, including on error.
1962
- if (!adopted)
1963
- solid.delete();
1964
- }
1965
- return ModelObject.create({
1966
- kind: 'solid',
1967
- name: 'Custom primitive',
1968
- geometry,
1969
- operation: storedOperation('primitive'),
1970
- });
1971
- }
1972
- function normalizeReplicadSolid(shape) {
1973
- const oc = getOC();
1974
- const type = shape.wrapped.ShapeType();
1975
- if (type === oc.TopAbs_ShapeEnum.TopAbs_SOLID)
1976
- return shape;
1977
- if (type !== oc.TopAbs_ShapeEnum.TopAbs_COMPSOLID &&
1978
- type !== oc.TopAbs_ShapeEnum.TopAbs_COMPOUND) {
1979
- throw new Error('A primitive builder must return exactly one OpenCascade solid.');
1980
- }
1981
- const containmentLayers = [
1982
- [oc.TopAbs_ShapeEnum.TopAbs_SHELL, oc.TopAbs_ShapeEnum.TopAbs_SOLID],
1983
- [oc.TopAbs_ShapeEnum.TopAbs_FACE, oc.TopAbs_ShapeEnum.TopAbs_SHELL],
1984
- [oc.TopAbs_ShapeEnum.TopAbs_WIRE, oc.TopAbs_ShapeEnum.TopAbs_FACE],
1985
- [oc.TopAbs_ShapeEnum.TopAbs_EDGE, oc.TopAbs_ShapeEnum.TopAbs_WIRE],
1986
- [oc.TopAbs_ShapeEnum.TopAbs_VERTEX, oc.TopAbs_ShapeEnum.TopAbs_EDGE],
1987
- ];
1988
- for (const [find, avoid] of containmentLayers) {
1989
- const outsideSolid = new oc.TopExp_Explorer(shape.wrapped, find, avoid);
1990
- try {
1991
- if (outsideSolid.More()) {
1992
- throw new Error('A primitive builder must return exactly one OpenCascade solid.');
1993
- }
1994
- }
1995
- finally {
1996
- outsideSolid.delete();
1997
- }
1998
- }
1999
- const solids = new oc.TopExp_Explorer(shape.wrapped, oc.TopAbs_ShapeEnum.TopAbs_SOLID);
2000
- let solid;
2001
- try {
2002
- if (!solids.More()) {
2003
- throw new Error('A primitive builder must return exactly one OpenCascade solid.');
2004
- }
2005
- solid = castOwnedShape3D(solids.Current());
2006
- solids.Next();
2007
- if (solids.More()) {
2008
- solid.delete();
2009
- solid = undefined;
2010
- throw new Error('A primitive builder must return exactly one OpenCascade solid.');
2011
- }
2012
- }
2013
- finally {
2014
- solids.delete();
2015
- }
2016
- shape.delete();
2017
- return solid;
2018
- }
2019
- export function group(children, name = 'Group') {
2020
- const runtimeChildren = children.map(child => requireModelObject(child, 'Every group child must be a model.'));
2021
- return ModelObject.create({
2022
- kind: 'group',
2023
- name,
2024
- children: runtimeChildren,
2025
- operation: storedOperation('group', runtimeChildren.map((model, index) => ({
2026
- model,
2027
- role: 'child',
2028
- index,
2029
- }))),
2030
- });
2031
- }
2032
- export function union(operands) {
2033
- const { first, others } = booleanOperands('union', operands);
2034
- return first[combineModels]('fuse', others);
2035
- }
2036
- /**
2037
- * Extrudes a single face; use faces.map(face => extrude(face, distance)) for multiple regions.
2038
- * @code3d.param distance {kind: 'length', label: 'Extrusion distance'}
2039
- */
2040
- export function extrude(face, distance) {
2041
- return requireModelKind(face, 'face', 'extrude requires a single face model.').extrude(distance);
2042
- }
2043
- export function cut(stock, tools) {
2044
- const runtimeStock = requireModelKind(stock, 'solid', 'The cut stock must be a solid model.');
2045
- if (tools.length === 0) {
2046
- throw new Error('cut requires at least one tool.');
2047
- }
2048
- const runtimeTools = tools.map(tool => requireModelKind(tool, 'solid', 'Every cut tool must be a solid model.'));
2049
- return runtimeStock[combineModels]('cut', runtimeTools);
2050
- }
2051
- export function intersect(operands) {
2052
- const { first, others } = booleanOperands('intersect', operands);
2053
- return first[combineModels]('intersect', others);
2054
- }
2055
- export function isModelObject(value) {
2056
- return value instanceof ModelObject;
2057
- }
2058
- export function isConstraint(value) {
2059
- return value instanceof Constraint;
2060
- }
2061
- export function isConstraintExpression(value) {
2062
- return value instanceof ConstraintExpression;
2063
- }
2064
- export function constraintPreview(value) {
2065
- return value.preview();
2066
- }
2067
- export function instrumentConstraint(constraint, sourceRef, parameters) {
2068
- constraint.attachSource(sourceRef);
2069
- constraint.attachParameters(parameters);
2070
- }
2071
- export function instrumentModelOperation(object, instrumentation) {
2072
- object.attachSource(instrumentation.sourceRef);
2073
- object.attachParameters(instrumentation.parameters);
2074
- object.attachOperationTrace(instrumentation.siteId, instrumentation.execution, instrumentation.order, instrumentation.sourceRef);
2075
- }
2076
- export function constraintTraceReference(constraint) {
2077
- return constraint.traceReference();
2078
- }
2079
- export function modelObjectRuntimeInfo(object) {
2080
- return {
2081
- nodeId: object.nodeId,
2082
- name: object.name,
2083
- sourceRefs: object.sourceRefs,
2084
- };
2085
- }
2086
- export function relatedModelObjects(object) {
2087
- return object.relatedObjects();
2088
- }
2089
- function boundsQuery(geometry, transform, selection = { kind: 'solid' }, scale = 1) {
2090
- return {
2091
- inputId: geometry.id,
2092
- geometry: { shape: geometry.value.shape, topology: geometry.value.topology },
2093
- query: {
2094
- kind: 'bounds',
2095
- transform,
2096
- selection,
2097
- scale,
2098
- key: kernelOperationKey('transformed-bounds', [
2099
- transform.position,
2100
- transform.quaternion,
2101
- selection.kind,
2102
- selection.kind === 'solid' ? null : selection.id,
2103
- scale,
2104
- ], [geometry]),
2105
- },
2106
- };
2107
- }
2108
- function meshQuery(artifact, shape, tolerance, topology) {
2109
- return {
2110
- inputId: artifact.id,
2111
- geometry: { shape, topology },
2112
- query: {
2113
- kind: 'mesh',
2114
- tolerance,
2115
- topology: topology !== undefined,
2116
- key: kernelOperationKey('render-mesh', [tolerance, 0.2, topology !== undefined], [artifact]),
2117
- },
2118
- };
2119
- }
2120
- const queryLifecycle = {
2121
- estimateBytes: estimateRetainedBytes,
2122
- retain: value => value,
2123
- instantiate: value => value,
2124
- release: () => { },
2125
- };
2126
- function computeSnapshotQuery(geometry, query) {
2127
- return query.kind === 'bounds'
2128
- ? computeTransformedBounds(geometry, query)
2129
- : computeRenderMesh(geometry.shape, query.tolerance, query.topology ? geometry.topology : undefined);
2130
- }
2131
- function evaluateSnapshotQuery(input) {
2132
- const cached = findKernelOperation(input.query.key, queryLifecycle);
2133
- if (cached)
2134
- return cached.value;
2135
- let value;
2136
- if (input.query.kind === 'mesh') {
2137
- // Mesh from the serialized input in every execution mode. BinTools can
2138
- // renormalize a plane axis by a few ULPs, changing Delaunay diagonal ties.
2139
- // A local query and a remote query must therefore consume the same BREP.
2140
- executeSnapshotQueryBatch(input.inputId, encodeKernelArtifact(input.inputId, input.geometry), [input.query], () => { }, (_, result) => {
2141
- value = result;
2142
- });
2143
- }
2144
- else
2145
- value = computeSnapshotQuery(input.geometry, input.query);
2146
- acceptKernelOperation(input.query.key, queryLifecycle, value);
2147
- return value;
2148
- }
2149
- export function planModelSnapshotQueries(objects) {
2150
- const visited = new Set();
2151
- const keys = new Map();
2152
- const batches = new Map();
2153
- function collect(object) {
2154
- if (visited.has(object))
2155
- return;
2156
- visited.add(object);
2157
- for (const input of object[modelSnapshotQueries]()) {
2158
- const { query, inputId } = input;
2159
- const signature = keys.get(query.key.id);
2160
- if (signature) {
2161
- if (signature !== query.key.signature)
2162
- throw new Error(`Kernel operation cache identity collision: ${query.key.id}`);
2163
- continue;
2164
- }
2165
- keys.set(query.key.id, query.key.signature);
2166
- if (findKernelOperation(query.key, queryLifecycle))
2167
- continue;
2168
- let batch = batches.get(inputId);
2169
- if (!batch) {
2170
- batch = { input, queries: [], sourceRef: object.sourceRefs.at(-1) };
2171
- batches.set(inputId, batch);
2172
- }
2173
- batch.queries.push(query);
2174
- }
2175
- object.children.forEach(collect);
2176
- }
2177
- objects.forEach(collect);
2178
- return [...batches].map(([id, { input, queries, sourceRef }]) => ({
2179
- id,
2180
- queries,
2181
- sourceRef,
2182
- weight: (1 +
2183
- (input.geometry.topology?.edges.ids.length ?? 0) +
2184
- (input.geometry.topology?.surfaces.ids.length ?? 0)) *
2185
- queries.length,
2186
- encode: () => encodeKernelArtifact(id, input.geometry),
2187
- accept: (query, value) => acceptKernelOperation(query.key, queryLifecycle, value),
2188
- }));
2189
- }
2190
- /** Each batch owns its restored shape; completed results leave before cancellation checks. */
2191
- export function executeSnapshotQueryBatch(id, bytes, queries, checkCancelled, onResult, onRestore) {
2192
- const started = performance.now();
2193
- const geometry = decodeKernelArtifact(bytes, id);
2194
- try {
2195
- onRestore?.(performance.now() - started);
2196
- for (const query of queries) {
2197
- checkCancelled();
2198
- onResult(query, computeSnapshotQuery(geometry, query));
2199
- }
2200
- }
2201
- finally {
2202
- geometry.shape.delete();
2203
- }
2204
- }
2205
- export function createModelSnapshotter() {
2206
- const meshCache = new Map();
2207
- return object => object.toSnapshot(meshCache);
2208
- }
2209
- export function disposeModelObjects(objects) {
2210
- const disposed = new Set();
2211
- for (const object of objects) {
2212
- object.disposeShape(disposed);
2213
- }
2214
- }
2215
- export function retainModelGeometry(objects) {
2216
- const retained = new Map();
2217
- const shapes = new Map();
2218
- const topologies = new Map();
2219
- const dispose = () => {
2220
- for (const shape of retained.values())
2221
- shape.delete();
2222
- retained.clear();
2223
- shapes.clear();
2224
- topologies.clear();
2225
- };
2226
- try {
2227
- for (const object of objects) {
2228
- const geometry = object[modelGeometry]()?.value;
2229
- const original = geometry?.shape;
2230
- if (!original)
2231
- continue;
2232
- let shape = retained.get(original);
2233
- if (!shape) {
2234
- shape = original.clone();
2235
- retained.set(original, shape);
2236
- }
2237
- shapes.set(object.nodeId, shape);
2238
- topologies.set(object.nodeId, geometry.topology);
2239
- }
2240
- return {
2241
- shapes,
2242
- dispose,
2243
- inspect(nodeId, options) {
2244
- const shape = shapes.get(nodeId);
2245
- const topology = topologies.get(nodeId);
2246
- if (!shape || !topology)
2247
- throw new Error('The model geometry snapshot is unavailable.');
2248
- return inspectShapeTopology(shape, topology, options);
2249
- },
2250
- };
2251
- }
2252
- catch (error) {
2253
- dispose();
2254
- throw error;
2255
- }
2256
- }
2257
- export const authoringApi = Object.freeze({
2258
- sketch,
2259
- circle,
2260
- ellipse,
2261
- extrude,
2262
- rectangle,
2263
- regularPolygon,
2264
- point,
2265
- line,
2266
- arc,
2267
- bezier,
2268
- spline,
2269
- loft,
2270
- box,
2271
- cylinder,
2272
- tube,
2273
- coil,
2274
- sphere,
2275
- frustum,
2276
- regularPrism,
2277
- group,
2278
- union,
2279
- cut,
2280
- intersect,
2281
- });
2282
- function storedOperation(kind, inputs = [], options = {}) {
2283
- return {
2284
- runtimeId: `operation-${nextOperationId++}`,
2285
- kind,
2286
- inputs: [...inputs],
2287
- regions: [...(options.regions ?? [])],
2288
- selections: [...(options.selections ?? [])],
2289
- dimensions: options.dimensions,
2290
- };
2291
- }
2292
- function storedOperationId(operation) {
2293
- const trace = operationTraces.get(operation);
2294
- return trace
2295
- ? `${trace.siteId}:execution:${trace.execution}`
2296
- : operation.runtimeId;
2297
- }
2298
- const shapeLifecycle = {
2299
- estimateBytes: () => 64,
2300
- retain: shape => shape.clone(),
2301
- instantiate: shape => shape.clone(),
2302
- release: shape => shape.delete(),
2303
- };
2304
- const modelGeometryLifecycle = {
2305
- estimateBytes: geometry => 128 +
2306
- estimateRetainedBytes([
2307
- geometry.topology,
2308
- geometry.localBounds,
2309
- geometry.referenceBasis?.topology,
2310
- geometry.referenceBasis?.transform,
2311
- ]),
2312
- retain: cloneModelGeometryValue,
2313
- instantiate: cloneModelGeometryValue,
2314
- release: disposeModelGeometryValue,
2315
- };
2316
- function cloneModelGeometryValue(geometry) {
2317
- return {
2318
- ...geometry,
2319
- shape: geometry.shape.clone(),
2320
- referenceBasis: geometry.referenceBasis
2321
- ? {
2322
- ...geometry.referenceBasis,
2323
- shape: geometry.referenceBasis.shape.clone(),
2324
- }
2325
- : undefined,
2326
- };
2327
- }
2328
- function disposeModelGeometryValue(geometry) {
2329
- geometry.shape.delete();
2330
- geometry.referenceBasis?.shape.delete();
2331
- }
2332
- function evaluateKernelShape(operation, arguments_, inputs, compute) {
2333
- return evaluateKernelOperation(operation, arguments_, inputs, shapeLifecycle, compute);
2334
- }
2335
- function evaluateModelGeometry(operation, arguments_, inputs, compute) {
2336
- return evaluateKernelOperation(operation, arguments_, inputs, modelGeometryLifecycle, () => {
2337
- const result = compute();
2338
- return {
2339
- ...createModelGeometryValue(result.shape, result.topology),
2340
- referenceBasis: result.referenceBasis,
2341
- };
2342
- });
2343
- }
2344
- function createModelGeometryValue(shape, topology) {
2345
- try {
2346
- return {
2347
- shape,
2348
- topology: topology ?? initialShapeTopology(shape),
2349
- localBounds: shapeBounds(shape),
2350
- };
2351
- }
2352
- catch (error) {
2353
- shape.delete();
2354
- throw error;
2355
- }
2356
- }
2357
- function evaluateSolidGeometry(operation, arguments_, inputs, compute) {
2358
- return evaluateModelGeometry(operation, arguments_, inputs, compute);
2359
- }
2360
- function renderMesh(artifact, shape, cache, tolerance, topology) {
2361
- const cached = cache.get(shape);
2362
- if (cached) {
2363
- return cached;
2364
- }
2365
- const mesh = evaluateSnapshotQuery(meshQuery(artifact, shape, tolerance, topology));
2366
- cache.set(shape, mesh);
2367
- return mesh;
2368
- }
2369
- function computeRenderMesh(shape, tolerance, topology) {
2370
- const surface = shape.mesh({ tolerance, angularTolerance: 0.2 });
2371
- const wire = shape.meshEdges({ tolerance, angularTolerance: 0.2 });
2372
- const vertexData = topology
2373
- ? stableVertexData(shape, topology.vertices)
2374
- : { positions: new Float32Array(), ids: [] };
2375
- return {
2376
- vertices: new Float32Array(surface.vertices),
2377
- normals: new Float32Array(surface.normals),
2378
- uvs: meshUVs(shape, surface.vertices.length / 3),
2379
- triangles: new Uint32Array(surface.triangles),
2380
- edges: new Float32Array(wire.lines),
2381
- topologyVertices: vertexData.positions,
2382
- vertexIds: vertexData.ids,
2383
- surfaceGroups: topology
2384
- ? stableSurfaceGroups(shape, topology.surfaces, surface.faceGroups)
2385
- : surface.faceGroups.map((group, index) => ({
2386
- start: group.start,
2387
- count: group.count,
2388
- surfaceId: index + 1,
2389
- })),
2390
- edgeGroups: topology
2391
- ? stableEdgeGroups(shape, topology.edges, wire.edgeGroups)
2392
- : wire.edgeGroups.map((group, index) => ({
2393
- start: group.start,
2394
- count: group.count,
2395
- // Context-region edges have their own traversal namespace, just
2396
- // like the surfaces above. Native handle hashes change on restore.
2397
- edgeId: index + 1,
2398
- })),
2399
- };
2400
- }
2401
- function meshUVs(shape, vertexCount) {
2402
- if (!vertexCount)
2403
- return undefined;
2404
- const faces = shape.faces;
2405
- const location = new (getOC().TopLoc_Location)();
2406
- const uvs = new Float32Array(vertexCount * 2);
2407
- let offset = 0;
2408
- try {
2409
- // Match the native mesh extractor's face/node traversal exactly.
2410
- for (const face of faces) {
2411
- const triangulation = getOC().BRep_Tool.Triangulation(face.wrapped, location, 0);
2412
- try {
2413
- if (triangulation.isNull())
2414
- continue;
2415
- if (!triangulation.HasUVNodes())
2416
- return undefined;
2417
- const count = triangulation.NbNodes();
2418
- let minU = Infinity, minV = Infinity, maxU = -Infinity, maxV = -Infinity;
2419
- for (let index = 0; index < count; index++) {
2420
- const uv = triangulation.UVNode(index + 1);
2421
- try {
2422
- const u = uv.X(), v = uv.Y();
2423
- uvs[(offset + index) * 2] = u;
2424
- uvs[(offset + index) * 2 + 1] = v;
2425
- minU = Math.min(minU, u);
2426
- maxU = Math.max(maxU, u);
2427
- minV = Math.min(minV, v);
2428
- maxV = Math.max(maxV, v);
2429
- }
2430
- finally {
2431
- uv.delete();
2432
- }
2433
- }
2434
- for (let index = 0; index < count; index++) {
2435
- const base = (offset + index) * 2;
2436
- uvs[base] = maxU === minU ? 0 : (uvs[base] - minU) / (maxU - minU);
2437
- uvs[base + 1] =
2438
- maxV === minV ? 0 : (uvs[base + 1] - minV) / (maxV - minV);
2439
- }
2440
- offset += count;
2441
- }
2442
- finally {
2443
- triangulation.delete();
2444
- }
2445
- }
2446
- return uvs;
2447
- }
2448
- finally {
2449
- location.delete();
2450
- faces.forEach(face => face.delete());
2451
- }
2452
- }
2453
- /** Internal bridge from the kernel-independent sketch definition to model geometry. */
2454
- export function sketchFaceModel(region) {
2455
- const curves = [region.outer, ...region.holes].map(loop => loop.map(curve => curve.kind === 'line'
2456
- ? ['line', curve.points]
2457
- : curve.kind === 'circle'
2458
- ? ['circle', curve.center, curve.radius]
2459
- : ['arc', curve.center, curve.radius, curve.start, curve.sweep]));
2460
- const geometry = evaluateModelGeometry('sketchFace', curves, [], () => ({
2461
- shape: sketchRegionFace(region),
2462
- }));
2463
- const plane = {
2464
- kind: 'face',
2465
- transform: identityRigidTransform,
2466
- };
2467
- return ModelObject.create({
2468
- kind: 'face',
2469
- name: 'Sketch face',
2470
- geometry,
2471
- geometryAnchor: plane,
2472
- elements: { plane },
2473
- operation: storedOperation('sketchFace'),
2474
- });
2475
- }
2476
- function planarFaceModel(operation, name, arguments_, buildSketch, transform) {
2477
- const geometry = evaluateModelGeometry(operation, arguments_, [], () => {
2478
- const sketch = buildSketch();
2479
- try {
2480
- const face = sketch.face();
2481
- return { shape: transform?.(face) ?? face };
2482
- }
2483
- finally {
2484
- sketch.delete();
2485
- }
2486
- });
2487
- const plane = {
2488
- kind: 'face',
2489
- transform: identityRigidTransform,
2490
- };
2491
- return ModelObject.create({
2492
- kind: 'face',
2493
- name,
2494
- geometry,
2495
- geometryAnchor: plane,
2496
- elements: {
2497
- plane,
2498
- },
2499
- operation: storedOperation(operation),
2500
- });
2501
- }
2502
- function curveModel(operation, name, arguments_, build) {
2503
- const geometry = evaluateModelGeometry(operation, arguments_, [], () => ({
2504
- shape: build(),
2505
- }));
2506
- const elements = curveElements(geometry.value.shape);
2507
- return ModelObject.create({
2508
- kind: 'edge',
2509
- name,
2510
- geometry,
2511
- geometryAnchor: { ...elements.start, kind: 'line' },
2512
- elements,
2513
- operation: storedOperation(operation),
2514
- });
2515
- }
2516
- function curveElements(curve) {
2517
- return {
2518
- start: curveAnchor(curve, 0),
2519
- midpoint: curveAnchor(curve, 0.5),
2520
- end: curveAnchor(curve, 1),
2521
- };
2522
- }
2523
- function curveAnchor(curve, position) {
2524
- const point = curve.pointAt(position);
2525
- const tangent = curve.tangentAt(position);
2526
- try {
2527
- return {
2528
- kind: 'point',
2529
- transform: frameFromYAxis(point.toTuple(), tangent.toTuple()),
2530
- };
2531
- }
2532
- finally {
2533
- point.delete();
2534
- tangent.delete();
2535
- }
2536
- }
2537
- function buildLoftGeometry(sections, spine, ruled) {
2538
- const inputs = [];
2539
- let spineWire;
2540
- try {
2541
- for (const [index, section] of sections.entries()) {
2542
- inputs.push({
2543
- shape: shapeWithTransform(section.geometry.value.shape, section.transform),
2544
- topology: section.geometry.value.topology,
2545
- index: index + 1,
2546
- });
2547
- }
2548
- if (spine) {
2549
- const edge = shapeWithTransform(spine.geometry.value.shape, spine.transform);
2550
- try {
2551
- spineWire = assembleWire([edge]);
2552
- }
2553
- finally {
2554
- edge.delete();
2555
- }
2556
- }
2557
- return loftWithTopology(inputs, spineWire, ruled);
2558
- }
2559
- finally {
2560
- inputs.forEach(input => input.shape.delete());
2561
- spineWire?.delete();
2562
- }
2563
- }
2564
- function shapeBounds(shape) {
2565
- const boundingBox = new BoundingBox();
2566
- try {
2567
- // Use analytic geometry, independent of whether a viewport has meshed it.
2568
- getOC().BRepBndLib.AddOptimal(shape.wrapped, boundingBox.wrapped, false, false);
2569
- const [minimum, maximum] = boundingBox.bounds;
2570
- return [
2571
- [minimum[0], minimum[1], minimum[2]],
2572
- [maximum[0], maximum[1], maximum[2]],
2573
- ];
2574
- }
2575
- finally {
2576
- boundingBox.delete();
2577
- }
2578
- }
2579
- function boundsCenter(bounds) {
2580
- const [[minX, minY, minZ], [maxX, maxY, maxZ]] = bounds;
2581
- return [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2];
2582
- }
2583
- function solidElements(bounds) {
2584
- return { axis: { kind: 'line', transform: translation(boundsCenter(bounds)) } };
2585
- }
2586
- function constraintReferences(constraint) {
2587
- return [
2588
- constraint.source.model,
2589
- constraint.target.model,
2590
- ...constraint.rotations.map(action => action.pivot.kind === 'around' ? action.pivot.axis.model : undefined),
2591
- ].filter((model) => !!model);
2592
- }
2593
- function computeTransformedBounds(geometry, query) {
2594
- const { transform, selection, scale } = query;
2595
- return withTopologyShape(geometry.shape, geometry.topology, selection, shape => {
2596
- const scaled = scale === 1 ? shape : shapeWithScale(shape, scale);
2597
- try {
2598
- const moved = shapeWithTransform(scaled, transform);
2599
- try {
2600
- return shapeBounds(moved);
2601
- }
2602
- finally {
2603
- moved.delete();
2604
- }
2605
- }
2606
- finally {
2607
- if (scaled !== shape)
2608
- scaled.delete();
2609
- }
2610
- });
2611
- }
2612
- function pointBounds(points) {
2613
- if (!points.length)
2614
- throw new Error('Empty geometry has no directional bounds.');
2615
- return [
2616
- [0, 1, 2].map(axis => Math.min(...points.map(point => point[axis]))),
2617
- [0, 1, 2].map(axis => Math.max(...points.map(point => point[axis]))),
2618
- ];
2619
- }
2620
- function combineBounds(bounds) {
2621
- return pointBounds(bounds.flat());
2622
- }
2623
- const boundDirections = {
2624
- up: [0, 1, 0],
2625
- down: [0, -1, 0],
2626
- left: [-1, 0, 0],
2627
- right: [1, 0, 0],
2628
- front: [0, 0, 1],
2629
- back: [0, 0, -1],
2630
- };
2631
- function directionalBound(reference, direction) {
2632
- const quaternion = frameFromYAxis(origin, boundDirections[direction]).quaternion;
2633
- const bounds = reference.model[referenceBounds](reference, invertTransform(rotation(quaternion)));
2634
- const center = [
2635
- (bounds[0][0] + bounds[1][0]) / 2,
2636
- bounds[1][1],
2637
- (bounds[0][2] + bounds[1][2]) / 2,
2638
- ];
2639
- return modelAnchor(reference.model, reference.whole ? direction : `${reference.name}.${direction}`, {
2640
- kind: 'face',
2641
- transform: composeTransforms(rotation(quaternion), translation(center)),
2642
- bound: {
2643
- size: [bounds[1][0] - bounds[0][0], bounds[1][2] - bounds[0][2]],
2644
- facing: 1,
2645
- },
2646
- });
2647
- }
2648
- function modelAnchor(model, name, element) {
2649
- const reference = { ...element, model, name };
2650
- return (element.topology
2651
- ? new ModelTopologyElement(reference)
2652
- : new ModelAnchor(reference));
2653
- }
2654
- function snapshotElements(elements, prefix = '') {
2655
- return Object.entries(elements).flatMap(([key, element]) => {
2656
- const name = `${prefix}${key}`;
2657
- const topology = element.topology;
2658
- return [
2659
- {
2660
- name,
2661
- kind: element.kind,
2662
- transform: toTransform(element.transform),
2663
- bound: element.bound,
2664
- facing: element.facing,
2665
- direction: element.direction,
2666
- topology: topology
2667
- ? {
2668
- geometryNodeId: topology.source.nodeId,
2669
- transform: {
2670
- ...topology.transform,
2671
- scale: [topology.scale, topology.scale, topology.scale],
2672
- },
2673
- ...topology.selection,
2674
- }
2675
- : undefined,
2676
- },
2677
- ...snapshotElements(element.members ?? {}, `${name}.`),
2678
- ];
2679
- });
2680
- }
2681
- function scaleElements(elements, factor) {
2682
- return Object.fromEntries(Object.entries(elements).map(([name, element]) => [
2683
- name,
2684
- scaleElement(element, factor),
2685
- ]));
2686
- }
2687
- function transformElement(element, transform) {
2688
- return {
2689
- ...element,
2690
- transform: composeTransforms(transform, element.transform),
2691
- topology: element.topology
2692
- ? {
2693
- ...element.topology,
2694
- transform: composeTransforms(transform, element.topology.transform),
2695
- }
2696
- : undefined,
2697
- members: element.members
2698
- ? Object.fromEntries(Object.entries(element.members).map(([name, member]) => [
2699
- name,
2700
- transformElement(member, transform),
2701
- ]))
2702
- : undefined,
2703
- };
2704
- }
2705
- function scaleElement(element, factor) {
2706
- return {
2707
- ...element,
2708
- bound: element.bound
2709
- ? {
2710
- ...element.bound,
2711
- size: [
2712
- element.bound.size[0] * factor,
2713
- element.bound.size[1] * factor,
2714
- ],
2715
- }
2716
- : undefined,
2717
- topology: element.topology
2718
- ? {
2719
- ...element.topology,
2720
- scale: element.topology.scale * factor,
2721
- transform: scaleFrame(element.topology.transform, factor),
2722
- }
2723
- : undefined,
2724
- members: element.members
2725
- ? scaleElements(element.members, factor)
2726
- : undefined,
2727
- transform: {
2728
- ...element.transform,
2729
- position: [
2730
- element.transform.position[0] * factor,
2731
- element.transform.position[1] * factor,
2732
- element.transform.position[2] * factor,
2733
- ],
2734
- },
2735
- };
2736
- }
2737
- function scaleFrame(transform, factor) {
2738
- return {
2739
- ...transform,
2740
- position: transform.position.map(value => value * factor),
2741
- };
2742
- }
2743
- function topologyTransform(topology, frame) {
2744
- return composeTransforms(topology.transform, scaleFrame(frame, topology.scale));
2745
- }
2746
- function topologyCenter(geometry, selection) {
2747
- const { referenceBasis, shape, topology } = geometry.value;
2748
- if (referenceBasis)
2749
- return withTopologyShape(referenceBasis.shape, referenceBasis.topology, selection, selected => topologyTransform(referenceBasis, translation(boundsCenter(shapeBounds(selected)))));
2750
- return withTopologyShape(shape, topology, selection, selected => translation(boundsCenter(shapeBounds(selected))));
2751
- }
2752
- function transformedReferenceBasis(source, transform, scale) {
2753
- const basis = source.value.referenceBasis;
2754
- return {
2755
- shape: (basis?.shape ?? source.value.shape).clone(),
2756
- topology: basis?.topology ?? source.value.topology,
2757
- transform: composeTransforms(transform, scaleFrame(basis?.transform ?? identityRigidTransform, scale)),
2758
- scale: (basis?.scale ?? 1) * scale,
2759
- };
2760
- }
2761
- function topologyName(selection) {
2762
- return selection.kind === 'solid'
2763
- ? 'solid'
2764
- : formatTopologyId(selection.kind, selection.id);
2765
- }
2766
- function topologyReferences(model, prefix, context, kind, ids) {
2767
- const geometry = context.source[modelGeometry]().value;
2768
- const basis = geometry.referenceBasis;
2769
- const { shape, topology } = basis ?? geometry;
2770
- const frames = kind === 'vertex'
2771
- ? topologyVertexPoints(shape, topology.vertices, ids).map(point => translation(point.position))
2772
- : (kind === 'edge'
2773
- ? topologyEdgeDirections(shape, topology.edges, ids)
2774
- : topologySurfaceDirections(shape, topology.surfaces, ids)).map(({ position, direction }) => frameFromYAxis(position, direction));
2775
- return ids.map((id, index) => {
2776
- const selection = { kind, id };
2777
- return new ModelTopologyElement({
2778
- model,
2779
- name: `${prefix}${topologyName(selection)}`,
2780
- kind: topologyElementKinds[kind],
2781
- transform: topologyTransform(context, basis ? topologyTransform(basis, frames[index]) : frames[index]),
2782
- topology: { ...context, selection },
2783
- });
2784
- });
2785
- }
2786
- function unionSectionShape(left, right) {
2787
- const section = new (getOC().BRepAlgoAPI_Section)(left.wrapped, right.wrapped, false);
2788
- try {
2789
- section.Build();
2790
- const sectionShape = castOwnedShape(section.Shape());
2791
- const edges = shapeSubshapes(sectionShape, 'edge');
2792
- if (edges.length === 0) {
2793
- return sectionShape;
2794
- }
2795
- let wire;
2796
- try {
2797
- wire = assembleWire(edges);
2798
- const face = makeFace(wire);
2799
- sectionShape.delete();
2800
- return face;
2801
- }
2802
- catch {
2803
- return sectionShape;
2804
- }
2805
- finally {
2806
- edges.forEach(edge => edge.delete());
2807
- wire?.delete();
2808
- }
2809
- }
2810
- finally {
2811
- section.delete();
2812
- }
2813
- }
2814
- function boundReference(target) {
2815
- const reference = target instanceof ModelAnchor ? target[anchorReferenceValue] : undefined;
2816
- if (!reference?.bound)
2817
- throw new Error('on() requires a directional bound: up, down, left, right, front, or back.');
2818
- return reference;
2819
- }
2820
- function anchorReference(anchor) {
2821
- if (anchor instanceof ModelObject) {
2822
- return anchor.relationAnchorReference();
2823
- }
2824
- return anchor[anchorReferenceValue];
2825
- }
2826
- function anchorSnapshot(modelOrReference, stored) {
2827
- const model = stored ? modelOrReference : undefined;
2828
- const reference = stored ?? modelOrReference;
2829
- return {
2830
- nodeId: (model ?? modelOrReference.model).nodeId,
2831
- name: reference.name,
2832
- kind: reference.kind,
2833
- };
2834
- }
2835
- function uniqueModels(models) {
2836
- return [...new Set(models)];
2837
- }
2838
- function toTransform(transform) {
2839
- return { ...transform, scale: unitScale };
2840
- }
2841
- function shapeWithTransform(source, transform) {
2842
- const oc = getOC();
2843
- let shape = source.clone();
2844
- const { axis, angleDegrees } = quaternionAxisAngle(transform.quaternion);
2845
- try {
2846
- if (Math.abs(angleDegrees) > 1e-9) {
2847
- const rotated = transformShape(shape, value => {
2848
- const point = new oc.gp_Pnt(0, 0, 0);
2849
- const direction = new oc.gp_Dir(...axis);
2850
- const rotationAxis = new oc.gp_Ax1(point, direction);
2851
- try {
2852
- value.SetRotation(rotationAxis, angleDegrees * (Math.PI / 180));
2853
- }
2854
- finally {
2855
- rotationAxis.delete();
2856
- direction.delete();
2857
- point.delete();
2858
- }
2859
- });
2860
- shape.delete();
2861
- shape = rotated;
2862
- }
2863
- return transformShape(shape, value => {
2864
- const vector = new oc.gp_Vec(...transform.position);
2865
- try {
2866
- value.SetTranslation(vector);
2867
- }
2868
- finally {
2869
- vector.delete();
2870
- }
2871
- });
2872
- }
2873
- finally {
2874
- shape.delete();
2875
- }
2876
- }
2877
- function shapeWithScale(source, scale) {
2878
- return transformShape(source, value => {
2879
- const point = new (getOC().gp_Pnt)(0, 0, 0);
2880
- try {
2881
- value.SetScale(point, scale);
2882
- }
2883
- finally {
2884
- point.delete();
2885
- }
2886
- });
2887
- }
2888
- function requireModelObject(value, message) {
2889
- if (!isModelObject(value)) {
2890
- throw new Error(message);
2891
- }
2892
- return value;
2893
- }
2894
- function requireModelKind(value, kind, message) {
2895
- const object = requireModelObject(value, message);
2896
- if (object.kind !== kind) {
2897
- throw new Error(message);
2898
- }
2899
- return object;
2900
- }
2901
- function booleanOperands(operation, operands) {
2902
- if (operands.length < 2) {
2903
- throw new Error(`${operation} requires at least two model operands.`);
2904
- }
2905
- const runtimeOperands = operands.map(operand => requireModelKind(operand, 'solid', `Every ${operation} operand must be a solid model.`));
2906
- return { first: runtimeOperands[0], others: runtimeOperands.slice(1) };
2907
- }
2908
- function assertPositive(label, value) {
2909
- if (!Number.isFinite(value) || value <= 0) {
2910
- throw new Error(`${label} must be a positive finite number.`);
2911
- }
2912
- }
2913
- function assertFiniteVector(label, value) {
2914
- if (value.some(component => !Number.isFinite(component))) {
2915
- throw new Error(`${label} must be a finite number.`);
2916
- }
2917
- }
2918
- function assertCurvePoints(label, points, minimum) {
2919
- if (points.length < minimum) {
2920
- throw new Error(`${label} requires at least ${minimum} points.`);
2921
- }
2922
- points.forEach((point, index) => assertFiniteVector(`${label} point ${index + 1}`, point));
2923
- const [first, ...rest] = points;
2924
- if (rest.every(point => point.every((component, index) => component === first[index]))) {
2925
- throw new Error(`${label} requires at least two distinct points.`);
2926
- }
2927
- }
2928
- function toPoint(vector) {
2929
- return [vector[0], vector[1], vector[2]];
2930
- }
2931
- function appendUniqueParameters(destination, parameters) {
2932
- for (const parameter of parameters) {
2933
- if (!hasParameter(destination, parameter)) {
2934
- destination.push(parameter);
2935
- }
2936
- }
2937
- }
2938
- function uniqueParameters(parameters) {
2939
- const unique = [];
2940
- appendUniqueParameters(unique, parameters);
2941
- return unique;
2942
- }
2943
- function hasParameter(parameters, parameter) {
2944
- return parameters.some(candidate => candidate.operation === parameter.operation &&
2945
- candidate.argument === parameter.argument &&
2946
- candidate.operationRef.file === parameter.operationRef.file &&
2947
- candidate.operationRef.start === parameter.operationRef.start &&
2948
- candidate.operationRef.end === parameter.operationRef.end &&
2949
- candidate.expressionRef.file === parameter.expressionRef.file &&
2950
- candidate.expressionRef.start === parameter.expressionRef.start &&
2951
- candidate.expressionRef.end === parameter.expressionRef.end &&
2952
- candidate.target.id === parameter.target.id);
2953
- }
2954
- //# sourceMappingURL=runtime.js.map