@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,18 +1,11 @@
1
- import type {Material} from './three.js';
2
- import {captureModelMaterial, type ModelMaterialSnapshot} from './material.js';
3
- import {
4
- inspectShapeTopology,
5
- type TopologyInspection,
6
- type TopologyInspectionOptions,
7
- } from './topology-inspection.js';
8
1
  import {
9
2
  assembleWire,
10
3
  basicFaceExtrusion,
11
4
  BoundingBox,
12
5
  genericSweep,
13
6
  getOC,
14
- makeBSplineApproximation,
15
7
  makeBezierCurve,
8
+ makeBSplineApproximation,
16
9
  makeCircle,
17
10
  makeCylinder,
18
11
  makeFace,
@@ -30,9 +23,26 @@ import {
30
23
  type Edge as ReplicadEdge,
31
24
  type Face as ReplicadFace,
32
25
  type Vertex as ReplicadVertex,
33
- type Shape3D,
34
26
  type Wire as ReplicadWire,
27
+ type Shape3D,
35
28
  } from 'replicad';
29
+ import {
30
+ edgeGeometry,
31
+ faceGeometry,
32
+ transformGeometry,
33
+ type AlignmentGeometry,
34
+ } from './alignment-geometry.js';
35
+ import {cached, cachedArtifact} from './cached.js';
36
+ import {extrudeWithTopology} from './extrude.js';
37
+ import {font, googleFont, type Font} from './font.js';
38
+ import {
39
+ beginKernelOperationEvaluation,
40
+ kernelOperationKey,
41
+ type KernelArtifact,
42
+ type KernelKeyPart,
43
+ type KernelOperationKey,
44
+ type KernelValueLifecycle,
45
+ } from './kernel-cache.js';
36
46
  import {
37
47
  castOwnedShape,
38
48
  castOwnedShape3D,
@@ -40,17 +50,29 @@ import {
40
50
  shapeSubshapes,
41
51
  transformShape,
42
52
  } from './kernel-shapes.js';
53
+ import {loftWithTopology} from './loft.js';
54
+ import {captureModelMaterial, type ModelMaterialSnapshot} from './material.js';
55
+ import {
56
+ axisRotation,
57
+ solveBodies,
58
+ type Body,
59
+ type BodyRotation,
60
+ } from './relation-solver.js';
61
+ import {estimateRetainedBytes} from './retained-memory.js';
62
+ import {shellWithTopology} from './shell.js';
63
+ import {sketchRegionFace} from './sketch-face.js';
64
+ import type {SketchRegion} from './sketch-regions.js';
43
65
  import {
44
66
  addVectors,
45
67
  composeTransforms,
46
68
  frameFromYAxis,
47
69
  identityRigidTransform,
48
70
  invertTransform,
49
- rotateVector,
50
- origin,
51
71
  negateVector,
72
+ origin,
52
73
  quaternionAxisAngle,
53
74
  relativeTransform,
75
+ rotateVector,
54
76
  rotation,
55
77
  rotationAround,
56
78
  translation,
@@ -58,36 +80,14 @@ import {
58
80
  type RigidTransform,
59
81
  type Vec3,
60
82
  } from './spatial.js';
61
- import {
62
- axisRotation,
63
- solveBodies,
64
- type Body,
65
- type BodyRotation,
66
- } from './relation-solver.js';
67
- import {
68
- edgeGeometry,
69
- faceGeometry,
70
- transformGeometry,
71
- type AlignmentGeometry,
72
- } from './alignment-geometry.js';
73
- import {shellWithTopology} from './shell.js';
74
- import {
75
- beginKernelOperationEvaluation,
76
- evaluateKernelOperation,
77
- kernelOperationKey,
78
- findKernelOperation,
79
- acceptKernelOperation,
80
- type KernelOperationKey,
81
- type KernelArtifact,
82
- type KernelKeyPart,
83
- type KernelValueLifecycle,
84
- } from './kernel-cache.js';
85
- import {loftWithTopology} from './loft.js';
86
- import {estimateRetainedBytes} from './retained-memory.js';
87
- import {extrudeWithTopology} from './extrude.js';
88
- import {sketchRegionFace} from './sketch-face.js';
89
- import type {SketchRegion} from './sketch-regions.js';
83
+ import {textGlyphs, textRegionFace, type TextOptions} from './text.js';
84
+ import type {Material} from 'three';
90
85
  import {formatTopologyId, type TopologyId} from './topology-id.js';
86
+ import {
87
+ inspectShapeTopology,
88
+ type TopologyInspection,
89
+ type TopologyInspectionOptions,
90
+ } from './topology-inspection.js';
91
91
  import {
92
92
  booleanWithTopology,
93
93
  chamferEdges,
@@ -99,10 +99,10 @@ import {
99
99
  stableEdgeGroups,
100
100
  stableSurfaceGroups,
101
101
  stableVertexData,
102
+ topologyChildren,
102
103
  topologyEdgeDirections,
103
104
  topologySurfaceDirections,
104
105
  topologyVertexPoints,
105
- topologyChildren,
106
106
  withTopologyShape,
107
107
  type EdgeId,
108
108
  type ShapeTopology,
@@ -112,11 +112,11 @@ import {
112
112
  type VertexId,
113
113
  } from './topology.js';
114
114
 
115
- import {sketch} from './sketch.js';
116
115
  import {
117
- encodeKernelArtifact,
118
116
  decodeKernelArtifact,
117
+ encodeKernelArtifact,
119
118
  } from './kernel-artifact-codec.js';
119
+ import {sketch} from './sketch.js';
120
120
 
121
121
  export type {Quaternion, Vec3} from './spatial.js';
122
122
  export type {EdgeId, SurfaceId, TopologyKind, VertexId} from './topology.js';
@@ -195,6 +195,7 @@ export type ConstraintSnapshot = Readonly<{
195
195
  );
196
196
 
197
197
  export type ModelOperationKind =
198
+ | 'sketch'
198
199
  | 'box'
199
200
  | 'cylinder'
200
201
  | 'tube'
@@ -213,6 +214,7 @@ export type ModelOperationKind =
213
214
  | 'spline'
214
215
  | 'loft'
215
216
  | 'sketchFace'
217
+ | 'text'
216
218
  | 'extrude'
217
219
  | 'primitive'
218
220
  | 'material'
@@ -337,7 +339,6 @@ export type RenderMesh = Readonly<{
337
339
 
338
340
  export type ModelSnapshotObject = Readonly<{
339
341
  nodeId: string;
340
- kind: ModelKind;
341
342
  name: string;
342
343
  /** Effective material, including recursive overrides from enclosing groups. */
343
344
  material?: ModelMaterialSnapshot;
@@ -352,8 +353,11 @@ export type ModelSnapshotObject = Readonly<{
352
353
  sourceRefs: readonly SourceRef[];
353
354
  parameters: readonly ParameterUsage[];
354
355
  operation: ModelOperationSnapshot;
355
- mesh?: RenderMesh;
356
- }>;
356
+ }> &
357
+ (
358
+ | Readonly<{kind: ModelKind; mesh?: RenderMesh}>
359
+ | Readonly<{kind: 'reference'; mesh?: never}>
360
+ );
357
361
 
358
362
  type StoredElement = Readonly<{
359
363
  kind: ElementKind;
@@ -377,10 +381,10 @@ type StoredElements = Readonly<Record<string, StoredElement>>;
377
381
 
378
382
  type StoredAnchor = StoredElement & Readonly<{name: string}>;
379
383
 
380
- type AnchorReference = StoredAnchor & Readonly<{model: ModelObject}>;
384
+ type AnchorReference = StoredAnchor & Readonly<{model: RelationObject}>;
381
385
 
382
386
  export type ModelElementReference = Readonly<{
383
- model: ModelObject;
387
+ model: RelationObject;
384
388
  name: string;
385
389
  kind: ElementKind;
386
390
  transform: Transform;
@@ -390,7 +394,7 @@ export type ModelElementReference = Readonly<{
390
394
  }>;
391
395
 
392
396
  export type ModelTopologyReference = Readonly<{
393
- model: ModelObject;
397
+ model: RelationObject;
394
398
  geometry: ModelObject;
395
399
  transform: Transform;
396
400
  }> &
@@ -398,14 +402,16 @@ export type ModelTopologyReference = Readonly<{
398
402
 
399
403
  export type ConstraintTraceReference = Readonly<{
400
404
  constraintId: string;
401
- source: ModelObject;
402
- target: ModelObject;
403
- self?: ModelObject;
405
+ source: RelationObject;
406
+ target: RelationObject;
407
+ self?: RelationObject;
404
408
  }>;
405
409
 
406
410
  export type ModelOperationInstrumentation = Readonly<{
407
411
  siteId: string;
408
412
  execution: number;
413
+ /** Distinguishes operation results produced by the same call execution. */
414
+ outputIndex?: number;
409
415
  order: number;
410
416
  sourceRef: SourceRef;
411
417
  parameters: readonly ParameterUsage[];
@@ -427,7 +433,7 @@ type StoredConstraint = Readonly<{
427
433
  }>;
428
434
 
429
435
  // An omitted model refers to relate's self, including after immutable copies.
430
- type RelationReference = StoredAnchor & Readonly<{model?: ModelObject}>;
436
+ type RelationReference = StoredAnchor & Readonly<{model?: RelationObject}>;
431
437
  type PivotSelection =
432
438
  | Readonly<{kind: 'pivot'; point: Vec3}>
433
439
  | Readonly<{kind: 'pivotVertex'; id: VertexId}>
@@ -440,11 +446,11 @@ type ConstraintSpatialSelection = Readonly<{
440
446
  kind: ConstraintSpatialReference['kind'];
441
447
  pivot: PivotSelection;
442
448
  }>;
443
- type RelateContext = Readonly<{self: ModelObject; original: ModelObject}>;
449
+ type RelateContext = Readonly<{self: RelationObject; original: RelationObject}>;
444
450
  let activeRelate: RelateContext | undefined;
445
451
 
446
452
  type StoredOperationInput = Readonly<{
447
- model: ModelObject;
453
+ model: RelationObject;
448
454
  role: ModelOperationInputRole;
449
455
  index: number;
450
456
  }>;
@@ -551,7 +557,7 @@ type SolidGeometry = KernelArtifact<
551
557
  >;
552
558
 
553
559
  type SolveContext = {
554
- poses: ReadonlyMap<ModelObject, RigidTransform>;
560
+ poses: ReadonlyMap<RelationObject, RigidTransform>;
555
561
  };
556
562
 
557
563
  function transformSolveContext(
@@ -593,7 +599,7 @@ const defaultModelNames = {
593
599
 
594
600
  const anchorKind = Symbol('anchorKind');
595
601
  const anchorReferenceValue = Symbol('anchorReference');
596
- const modelFamily = Symbol('modelFamily');
602
+ const modelKind = Symbol('modelKind');
597
603
  const modelNamedElements = Symbol('modelNamedElements');
598
604
 
599
605
  export interface Anchor<Kind extends ElementKind = ElementKind> {
@@ -671,18 +677,18 @@ export type ElementSources = Readonly<Record<string, Anchor>>;
671
677
  export type NamedElements = Readonly<Record<string, Anchor>>;
672
678
 
673
679
  export type ExposedValue<Value> = Value extends {
674
- readonly [modelFamily]: infer Family;
680
+ readonly [modelKind]: infer Kind;
675
681
  readonly [modelNamedElements]: infer Elements;
676
682
  }
677
- ? (Family extends 'solid'
683
+ ? (Kind extends 'solid'
678
684
  ? Solid
679
- : Family extends 'face'
685
+ : Kind extends 'face'
680
686
  ? Surface
681
- : Family extends 'edge'
687
+ : Kind extends 'edge'
682
688
  ? Edge
683
- : Family extends 'vertex'
689
+ : Kind extends 'vertex'
684
690
  ? Vertex
685
- : Family extends 'group'
691
+ : Kind extends 'group'
686
692
  ? Anchor<'frame'>
687
693
  : Anchor) &
688
694
  Elements
@@ -707,81 +713,76 @@ export type ModelElementKind<Kind extends ModelKind> = Kind extends 'face'
707
713
  ? 'point'
708
714
  : 'frame';
709
715
 
710
- export type ModelFamily = ModelKind | 'model';
711
-
712
- export type ModelFamilyElementKind<Family extends ModelFamily> =
713
- Family extends ModelKind ? ModelElementKind<Family> : ElementKind;
714
-
715
- export type ModelForFamily<
716
+ export type ModelForKind<
716
717
  Elements extends NamedElements,
717
- Family extends ModelFamily,
718
- > = Family extends 'solid'
718
+ Kind extends ModelKind,
719
+ > = Kind extends 'solid'
719
720
  ? SolidModel<Elements>
720
- : Family extends 'face'
721
+ : Kind extends 'face'
721
722
  ? FaceModel<Elements>
722
- : Family extends 'edge'
723
+ : Kind extends 'edge'
723
724
  ? EdgeModel<Elements>
724
- : Family extends 'vertex'
725
+ : Kind extends 'vertex'
725
726
  ? VertexModel<Elements>
726
- : Family extends 'group'
727
+ : Kind extends 'group'
727
728
  ? GroupModel<Elements>
728
- : Model<Elements>;
729
+ : never;
729
730
 
730
731
  export interface ModelCapabilities<
731
732
  Elements extends NamedElements,
732
- Family extends ModelFamily,
733
+ Kind extends ModelKind,
733
734
  >
734
- extends Anchor<ModelFamilyElementKind<Family>>, DirectionalBounds {
735
- readonly [modelFamily]: Family extends ModelKind ? Family : ModelKind;
735
+ extends Anchor<ModelElementKind<Kind>>, DirectionalBounds {
736
+ readonly [modelKind]: Kind;
736
737
  readonly [modelNamedElements]: Elements;
737
738
  relate(
738
739
  build: (
739
- self: ModelForFamily<Elements, Family>,
740
+ self: ModelForKind<Elements, Kind>,
740
741
  ) => Constraint | readonly Constraint[],
741
- ): ModelForFamily<Elements, Family>;
742
+ ): ModelForKind<Elements, Kind>;
742
743
  expose<const Sources extends ElementSources>(
743
744
  sources: Sources,
744
- ): ModelForFamily<MergedElements<Elements, ExposedElements<Sources>>, Family>;
745
+ ): ModelForKind<MergedElements<Elements, ExposedElements<Sources>>, Kind>;
745
746
  /**
746
747
  * Shift the origin by this displacement: every local point becomes p - d.
747
- * @code3d.param dx {kind: 'length', label: 'Origin ΔX'}
748
- * @code3d.param dy {kind: 'length', label: 'Origin ΔY'}
749
- * @code3d.param dz {kind: 'length', label: 'Origin ΔZ'}
748
+ * @code3d.param dx {kind: 'length', default: 0, label: 'Origin ΔX'}
749
+ * @code3d.param dy {kind: 'length', default: 0, label: 'Origin ΔY'}
750
+ * @code3d.param dz {kind: 'length', default: 0, label: 'Origin ΔZ'}
750
751
  */
751
752
  originOffset(
752
753
  dx: number,
753
754
  dy: number,
754
755
  dz: number,
755
- ): ModelForFamily<Elements, Family>;
756
+ ): ModelForKind<Elements, Kind>;
756
757
  /** Re-express the model with this point reference at local zero. */
757
- originPoint(point: PointAnchor): ModelForFamily<Elements, Family>;
758
+ originPoint(point: PointAnchor): ModelForKind<Elements, Kind>;
758
759
  /**
759
760
  * Rotate about the current origin, in degrees, about fixed local X, Y, then Z axes.
760
- * @code3d.param x {kind: 'angle', label: 'Rotate X'}
761
- * @code3d.param y {kind: 'angle', label: 'Rotate Y'}
762
- * @code3d.param z {kind: 'angle', label: 'Rotate Z'}
761
+ * @code3d.param x {kind: 'angle', default: 0, label: 'Rotate X'}
762
+ * @code3d.param y {kind: 'angle', default: 0, label: 'Rotate Y'}
763
+ * @code3d.param z {kind: 'angle', default: 0, label: 'Rotate Z'}
763
764
  */
764
- rotate(x: number, y: number, z: number): ModelForFamily<Elements, Family>;
765
+ rotate(x: number, y: number, z: number): ModelForKind<Elements, Kind>;
765
766
  /**
766
767
  * Return a value with its entire material replaced, including every group descendant.
767
768
  * Capture a Three.js material at assignment, or use a CSS color for the default material.
768
769
  */
769
- material(material: Material | string): ModelForFamily<Elements, Family>;
770
+ material(material: Material | string): ModelForKind<Elements, Kind>;
770
771
  }
771
772
 
772
773
  export interface GeometryCapabilities<
773
774
  Elements extends NamedElements,
774
- Family extends ModelGeometryKind,
775
+ Kind extends ModelGeometryKind,
775
776
  > extends GeometryQueryCapabilities {
776
777
  /** Re-express the model with its geometric center at local zero. */
777
- originCenter(): ModelForFamily<Elements, Family>;
778
+ originCenter(): ModelForKind<Elements, Kind>;
778
779
  /**
779
780
  * Re-express the model with the selected vertex at local zero.
780
781
  * @code3d.param id {kind: 'vertex', label: 'Origin vertex'}
781
782
  */
782
- originVertex(id: VertexId): ModelForFamily<Elements, Family>;
783
- /** @code3d.param factor {kind: 'ratio', label: 'Scale'} */
784
- scaled(factor: number): ModelForFamily<Elements, Family>;
783
+ originVertex(id: VertexId): ModelForKind<Elements, Kind>;
784
+ /** @code3d.param factor {kind: 'ratio', default: 1, label: 'Scale'} */
785
+ scaled(factor: number): ModelForKind<Elements, Kind>;
785
786
  }
786
787
 
787
788
  export interface VertexTopologyCapabilities {
@@ -809,12 +810,12 @@ export interface SolidModificationCapabilities<Elements extends NamedElements> {
809
810
  /** Subtracts all tools in one boolean operation, equivalent to cut(stock, tools). */
810
811
  cut(tools: readonly SolidModel<{}>[]): SolidModel;
811
812
  /**
812
- * @code3d.param radius {kind: 'length', label: 'Fillet radius', constraints: {exclusiveMin: 0}}
813
+ * @code3d.param radius {kind: 'length', default: 1, label: 'Fillet radius', constraints: {exclusiveMin: 0}}
813
814
  * @code3d.param edgeIds {kind: 'edge', actions: [{label: 'Use all', action: 'remove-argument'}]}
814
815
  */
815
816
  fillet(radius: number, edgeIds?: readonly EdgeId[]): SolidModel<Elements>;
816
817
  /**
817
- * @code3d.param distance {kind: 'length', label: 'Chamfer distance', constraints: {exclusiveMin: 0}}
818
+ * @code3d.param distance {kind: 'length', default: 1, label: 'Chamfer distance', constraints: {exclusiveMin: 0}}
818
819
  * @code3d.param edgeIds {kind: 'edge', actions: [{label: 'Use all', action: 'remove-argument'}]}
819
820
  */
820
821
  chamfer(distance: number, edgeIds?: readonly EdgeId[]): SolidModel<Elements>;
@@ -822,7 +823,7 @@ export interface SolidModificationCapabilities<Elements extends NamedElements> {
822
823
  * Hollow a solid with uniform walls. Positive thickness offsets inward;
823
824
  * negative thickness offsets outward. Selected surfaces become openings.
824
825
  * Omit the selection, or use [], for a fully enclosed cavity.
825
- * @code3d.param thickness {kind: 'length', label: 'Wall thickness'}
826
+ * @code3d.param thickness {kind: 'length', default: 1, label: 'Wall thickness'}
826
827
  * @code3d.param removedSurfaceIds {kind: 'surface', label: 'Openings', actions: [{label: 'Close all openings', action: 'remove-argument'}]}
827
828
  */
828
829
  shell(
@@ -833,7 +834,7 @@ export interface SolidModificationCapabilities<Elements extends NamedElements> {
833
834
 
834
835
  export type Model<Elements extends NamedElements = {}> = ModelCapabilities<
835
836
  Elements,
836
- 'model'
837
+ ModelKind
837
838
  > &
838
839
  Elements;
839
840
 
@@ -861,7 +862,7 @@ export type FaceModel<Elements extends NamedElements = PlanarElements> =
861
862
  flip(): Surface;
862
863
  /**
863
864
  * Extrudes along the face's local plane normal. Signed distance; no recentering.
864
- * @code3d.param distance {kind: 'length', label: 'Extrusion distance'}
865
+ * @code3d.param distance {kind: 'length', default: 10, label: 'Extrusion distance'}
865
866
  */
866
867
  extrude(distance: number): SolidModel;
867
868
  } & Elements;
@@ -1127,7 +1128,9 @@ export function modelTopologyIds(
1127
1128
  : value instanceof ConstraintExpression
1128
1129
  ? value.self()
1129
1130
  : topology?.source;
1130
- const geometry = source?.[modelGeometry]()?.value;
1131
+ const geometry = isModelObject(source)
1132
+ ? source[modelGeometry]()?.value
1133
+ : undefined;
1131
1134
  if (!geometry) return undefined;
1132
1135
  return topologyChildren(
1133
1136
  geometry.shape,
@@ -1179,12 +1182,12 @@ export abstract class ConstraintExpression {
1179
1182
  appendUniqueParameters(this.parameters, parameters);
1180
1183
  }
1181
1184
  /** @internal */
1182
- self(): ModelObject | undefined {
1185
+ self(): RelationObject | undefined {
1183
1186
  return this.context?.self;
1184
1187
  }
1185
1188
  /** @internal */
1186
1189
  traceReference(): ConstraintTraceReference {
1187
- const rebind = (model: ModelObject) =>
1190
+ const rebind = (model: RelationObject) =>
1188
1191
  model === this.context?.original ? this.context.self : model;
1189
1192
  return {
1190
1193
  constraintId: this.constraintId,
@@ -1194,8 +1197,8 @@ export abstract class ConstraintExpression {
1194
1197
  };
1195
1198
  }
1196
1199
  /** @internal */
1197
- storeFor(model: ModelObject, original: ModelObject): StoredConstraint {
1198
- const isSelf = (candidate: ModelObject | undefined) =>
1200
+ storeFor(model: RelationObject, original: RelationObject): StoredConstraint {
1201
+ const isSelf = (candidate: RelationObject | undefined) =>
1199
1202
  candidate === model || candidate === original;
1200
1203
  if (!isSelf(this.source.model) && !isSelf(this.target.model))
1201
1204
  throw new Error(
@@ -1296,11 +1299,12 @@ export class Constraint extends ConstraintExpression {
1296
1299
  * In the target reference axes, on() pins matching bound centers; align()
1297
1300
  * translates self after alignment, retaining the relation's free modes.
1298
1301
  * Explicit zero pins tangential coordinates for on() only.
1299
- * @code3d.param x {kind: 'length', label: 'ΔX'}
1300
- * @code3d.param y {kind: 'length', label: 'ΔY'}
1301
- * @code3d.param z {kind: 'length', label: 'ΔZ'}
1302
+ * @code3d.param x {kind: 'length', default: 0, label: 'ΔX'}
1303
+ * @code3d.param y {kind: 'length', default: 0, label: 'ΔY'}
1304
+ * @code3d.param z {kind: 'length', default: 0, label: 'ΔZ'}
1302
1305
  */
1303
- offset(x: number, y: number, z: number): Constraint {
1306
+ offset(x: number, y: number, z: number): Constraint;
1307
+ offset(x = 0, y = 0, z = 0): Constraint {
1304
1308
  assertFiniteVector('offset', [x, y, z]);
1305
1309
  return new Constraint(
1306
1310
  this.kind,
@@ -1315,11 +1319,12 @@ export class Constraint extends ConstraintExpression {
1315
1319
  }
1316
1320
  /**
1317
1321
  * Select this rotation's pivot in self's local coordinates.
1318
- * @code3d.param x {kind: 'length', label: 'Pivot X'}
1319
- * @code3d.param y {kind: 'length', label: 'Pivot Y'}
1320
- * @code3d.param z {kind: 'length', label: 'Pivot Z'}
1322
+ * @code3d.param x {kind: 'length', default: 0, label: 'Pivot X'}
1323
+ * @code3d.param y {kind: 'length', default: 0, label: 'Pivot Y'}
1324
+ * @code3d.param z {kind: 'length', default: 0, label: 'Pivot Z'}
1321
1325
  */
1322
- pivot([x, y, z]: Vec3): ConstraintPivotChain {
1326
+ pivot([x, y, z]: Vec3): ConstraintPivotChain;
1327
+ pivot([x = 0, y = 0, z = 0]: Vec3 = [0, 0, 0]): ConstraintPivotChain {
1323
1328
  assertFiniteVector('pivot', [x, y, z]);
1324
1329
  return new ConstraintPivotChain(this, {kind: 'pivot', point: [x, y, z]});
1325
1330
  }
@@ -1334,7 +1339,9 @@ export class Constraint extends ConstraintExpression {
1334
1339
  throw new Error('around() requires an axis or straight edge.');
1335
1340
  const geometry =
1336
1341
  reference.topology?.source[modelGeometry]()?.value ??
1337
- (reference.whole ? reference.model[modelGeometry]()?.value : undefined);
1342
+ (reference.whole && isModelObject(reference.model)
1343
+ ? reference.model[modelGeometry]()?.value
1344
+ : undefined);
1338
1345
  if (geometry) {
1339
1346
  const straight = reference.topology
1340
1347
  ? withTopologyShape(
@@ -1353,11 +1360,12 @@ export class Constraint extends ConstraintExpression {
1353
1360
  }
1354
1361
  /**
1355
1362
  * Rotate about self's origin and local X, Y, then Z axes, in degrees.
1356
- * @code3d.param x {kind: 'angle', label: 'Rotate X'}
1357
- * @code3d.param y {kind: 'angle', label: 'Rotate Y'}
1358
- * @code3d.param z {kind: 'angle', label: 'Rotate Z'}
1363
+ * @code3d.param x {kind: 'angle', default: 0, label: 'Rotate X'}
1364
+ * @code3d.param y {kind: 'angle', default: 0, label: 'Rotate Y'}
1365
+ * @code3d.param z {kind: 'angle', default: 0, label: 'Rotate Z'}
1359
1366
  */
1360
- rotate(x: number, y: number, z: number): Constraint {
1367
+ rotate(x: number, y: number, z: number): Constraint;
1368
+ rotate(x = 0, y = 0, z = 0): Constraint {
1361
1369
  return this.withRotation({kind: 'pivot', point: origin}, [x, y, z], this);
1362
1370
  }
1363
1371
  /** @internal */
@@ -1416,11 +1424,12 @@ export class ConstraintPivotChain extends ConstraintExpression {
1416
1424
  this.spatialOperation = {kind: selection.kind, pivot: selection};
1417
1425
  }
1418
1426
  /**
1419
- * @code3d.param x {kind: 'angle', label: 'Rotate X'}
1420
- * @code3d.param y {kind: 'angle', label: 'Rotate Y'}
1421
- * @code3d.param z {kind: 'angle', label: 'Rotate Z'}
1427
+ * @code3d.param x {kind: 'angle', default: 0, label: 'Rotate X'}
1428
+ * @code3d.param y {kind: 'angle', default: 0, label: 'Rotate Y'}
1429
+ * @code3d.param z {kind: 'angle', default: 0, label: 'Rotate Z'}
1422
1430
  */
1423
- rotate(x: number, y: number, z: number): Constraint {
1431
+ rotate(x: number, y: number, z: number): Constraint;
1432
+ rotate(x = 0, y = 0, z = 0): Constraint {
1424
1433
  return this.constraint.withRotation(this.selection, [x, y, z], this);
1425
1434
  }
1426
1435
  }
@@ -1433,8 +1442,9 @@ export class ConstraintAroundChain extends ConstraintExpression {
1433
1442
  super(...constraint.chainArguments());
1434
1443
  this.spatialOperation = {kind: selection.kind, pivot: selection};
1435
1444
  }
1436
- /** @code3d.param angle {kind: 'angle', label: 'Rotate'} */
1437
- rotate(angle: number): Constraint {
1445
+ /** @code3d.param angle {kind: 'angle', default: 0, label: 'Rotate'} */
1446
+ rotate(angle: number): Constraint;
1447
+ rotate(angle = 0): Constraint {
1438
1448
  return this.constraint.withRotation(this.selection, angle, this);
1439
1449
  }
1440
1450
  }
@@ -1445,173 +1455,70 @@ const referenceBoundsParts = Symbol('referenceBoundsParts');
1445
1455
  const modelSnapshotQueries = Symbol('modelSnapshotQueries');
1446
1456
  const relationPreview = Symbol('relationPreview');
1447
1457
 
1448
- export class ModelObject<
1449
- Elements extends NamedElements = {},
1450
- Kind extends ModelKind = ModelKind,
1451
- > implements Anchor<ModelElementKind<Kind>> {
1452
- declare readonly [anchorKind]: ModelElementKind<Kind>;
1453
- declare readonly [modelFamily]: Kind;
1454
- declare readonly [modelNamedElements]: Elements;
1455
- /** @internal */
1456
- readonly elementKind: ModelElementKind<Kind>;
1457
- /** @internal */
1458
- readonly nodeId: string;
1459
- /** @internal */
1460
- readonly kind: Kind;
1461
- /** @internal */
1462
- readonly name: string;
1463
- /** @internal */
1464
- private readonly materialSnapshot?: ModelMaterialSnapshot;
1465
- /** @internal */
1466
- readonly children: readonly ModelObject[];
1467
- /** @internal */
1468
- get sourceRefs(): SourceRef[] {
1469
- return valueTrace(this).sourceRefs;
1470
- }
1471
- /** @internal */
1472
- get parameters(): ParameterUsage[] {
1473
- return valueTrace(this).parameters;
1474
- }
1475
- private readonly geometry?: ModelGeometry;
1476
- private readonly assembly?: SolveContext;
1477
- private readonly meshTolerance: number;
1478
- private readonly geometryAnchor: StoredElement;
1479
- private readonly elements: StoredElements;
1480
- private constraints: StoredConstraint[];
1481
- private readonly operation: StoredOperation;
1458
+ type RelationObjectInit = Readonly<{
1459
+ nodeId?: string;
1460
+ constraints?: readonly StoredConstraint[];
1461
+ sourceRefs?: readonly SourceRef[];
1462
+ parameters?: readonly ParameterUsage[];
1463
+ }>;
1482
1464
 
1465
+ /** A local reference frame and its relations, independent of finite geometry. */
1466
+ export abstract class RelationObject {
1467
+ readonly nodeId: string;
1483
1468
  /** @internal */
1484
- [modelGeometry](): ModelGeometry | undefined {
1485
- return this.geometry;
1486
- }
1469
+ abstract readonly name: string;
1470
+ protected constraints: StoredConstraint[];
1487
1471
 
1488
- private constructor(init: ModelObjectInit<Kind>) {
1489
- if (init.kind !== 'group' && !init.geometry) {
1490
- throw new Error(
1491
- 'A geometric model object must contain an OpenCascade shape.',
1492
- );
1493
- }
1472
+ protected constructor(init: RelationObjectInit = {}) {
1494
1473
  this.nodeId = init.nodeId ?? `node-${nextNodeId++}`;
1495
- this.kind = init.kind;
1496
- this.elementKind = modelElementKinds[
1497
- init.kind
1498
- ] as unknown as ModelElementKind<Kind>;
1499
- this.geometry = init.geometry;
1500
- this.geometryAnchor = init.geometryAnchor ?? {
1501
- kind: this.elementKind,
1502
- transform: identityRigidTransform,
1503
- };
1504
- this.meshTolerance = init.meshTolerance ?? 0.2;
1505
- this.name = init.name ?? defaultModelNames[init.kind];
1506
- this.materialSnapshot = init.material;
1507
- this.children = init.children ?? [];
1508
- this.assembly =
1509
- init.assembly ??
1510
- (this.kind === 'group'
1511
- ? ModelObject.createAssembly(this.children)
1512
- : undefined);
1513
1474
  this.constraints = [...(init.constraints ?? [])];
1514
1475
  valueTraces.set(this, {
1515
1476
  sourceRefs: [...(init.sourceRefs ?? [])],
1516
1477
  parameters: [...(init.parameters ?? [])],
1517
1478
  });
1518
- this.operation = init.operation;
1519
- const elements =
1520
- init.elements ??
1521
- (this.kind === 'solid' && this.geometry
1522
- ? solidElements(this.geometry.value.localBounds)
1523
- : {});
1524
- this.elements = this.geometry
1525
- ? {
1526
- center: {
1527
- kind: 'point',
1528
- transform: translation(
1529
- boundsCenter(this.geometry.value.localBounds),
1530
- ),
1531
- },
1532
- ...elements,
1533
- }
1534
- : elements;
1535
- for (const [name, element] of Object.entries(this.elements)) {
1536
- if (name in this) {
1537
- throw new Error(
1538
- `The element name ${name} conflicts with the model API.`,
1539
- );
1540
- }
1541
- Object.defineProperty(this, name, {
1542
- value: modelAnchor(this, name, element),
1543
- });
1544
- }
1545
- }
1546
-
1547
- /** @internal */
1548
- static create<
1549
- Elements extends NamedElements = {},
1550
- Kind extends ModelKind = ModelKind,
1551
- >(init: ModelObjectInit<Kind>): ModelObject<Elements, Kind> {
1552
- return new ModelObject<Elements, Kind>(init);
1553
1479
  }
1554
1480
 
1555
- get up(): Bound {
1556
- return directionalBound(this.relationAnchorReference(), 'up');
1557
- }
1558
- get down(): Bound {
1559
- return directionalBound(this.relationAnchorReference(), 'down');
1560
- }
1561
- get left(): Bound {
1562
- return directionalBound(this.relationAnchorReference(), 'left');
1563
- }
1564
- get right(): Bound {
1565
- return directionalBound(this.relationAnchorReference(), 'right');
1566
- }
1567
- get front(): Bound {
1568
- return directionalBound(this.relationAnchorReference(), 'front');
1569
- }
1570
- get back(): Bound {
1571
- return directionalBound(this.relationAnchorReference(), 'back');
1481
+ get sourceRefs(): SourceRef[] {
1482
+ return valueTrace(this).sourceRefs;
1572
1483
  }
1573
1484
 
1574
- on(target: Bound): Constraint {
1575
- return Constraint.create(
1576
- this.relationAnchorReference(),
1577
- boundReference(target),
1578
- );
1485
+ get parameters(): ParameterUsage[] {
1486
+ return valueTrace(this).parameters;
1579
1487
  }
1580
1488
 
1581
- /** @internal */
1582
- relationAnchorReference(): AnchorReference {
1583
- return {model: this, name: 'geometry', ...this.geometryAnchor, whole: true};
1584
- }
1489
+ protected abstract copyRelations(init: RelationObjectInit): RelationObject;
1585
1490
 
1586
- align(target: Anchor<'point' | 'line' | 'face'>): Constraint {
1587
- return Constraint.create(
1588
- this.relationAnchorReference(),
1589
- anchorReference(target),
1590
- 'align',
1491
+ protected vertexPosition(_id: VertexId): Vec3 {
1492
+ throw new Error(
1493
+ 'pivotVertex() requires model topology, not a sketch reference frame. Use pivot([x, y, z]) instead.',
1591
1494
  );
1592
1495
  }
1593
1496
 
1594
- reverse(): Edge {
1595
- return this.edges()[0].reverse();
1596
- }
1597
- flip(): Surface {
1598
- return this.surfaces()[0].flip();
1599
- }
1600
-
1601
- relate(
1602
- build: (
1603
- self: RuntimeModel<Elements, Kind>,
1604
- ) => Constraint | readonly Constraint[],
1605
- ): RuntimeModel<Elements, Kind> {
1606
- const operation = storedOperation('relate', [
1607
- {model: this, role: 'source', index: 0},
1608
- ]);
1609
- const related = this.copy({}, operation);
1497
+ /** @internal */
1498
+ abstract relatedObjects(): readonly RelationObject[];
1499
+ /** @internal */
1500
+ abstract toSnapshot(
1501
+ meshCache?: Map<AnyShape, RenderMesh>,
1502
+ ): ModelSnapshotObject;
1503
+ /** @internal */
1504
+ abstract attachOperationTrace(
1505
+ siteId: string,
1506
+ execution: number,
1507
+ order: number,
1508
+ sourceRef: SourceRef,
1509
+ outputIndex?: number,
1510
+ ): void;
1511
+
1512
+ /** Store relations only after the complete callback has returned. */
1513
+ protected addRelations(
1514
+ original: RelationObject,
1515
+ build: () => Constraint | readonly Constraint[],
1516
+ ): readonly RelationObject[] {
1610
1517
  const previous = activeRelate;
1611
- activeRelate = {self: related, original: this};
1518
+ activeRelate = {self: this, original};
1612
1519
  let built: Constraint | readonly Constraint[];
1613
1520
  try {
1614
- built = build(related);
1521
+ built = build();
1615
1522
  } finally {
1616
1523
  activeRelate = previous;
1617
1524
  }
@@ -1623,1459 +1530,1787 @@ export class ModelObject<
1623
1530
  throw new Error(
1624
1531
  'relate() requires a completed Constraint; finish pivot/around with rotate().',
1625
1532
  );
1626
- return constraint.storeFor(related, this);
1533
+ return constraint.storeFor(this, original);
1627
1534
  });
1628
- related.constraints.push(...stored);
1629
- operation.inputs.push(
1630
- ...uniqueModels(stored.flatMap(constraintReferences)).map(
1631
- (model, index) => ({
1632
- model,
1633
- role: 'reference' as const,
1634
- index,
1635
- }),
1636
- ),
1637
- );
1638
- return related;
1535
+ this.constraints.push(...stored);
1536
+ return uniqueModels(stored.flatMap(constraintReferences));
1639
1537
  }
1640
1538
 
1641
- expose<const Sources extends ElementSources>(
1642
- sources: Sources,
1643
- ): RuntimeModel<MergedElements<Elements, ExposedElements<Sources>>, Kind> {
1644
- const entries = Object.entries(sources);
1645
- const elements = this.localElements(entries.map(([, source]) => source));
1646
- const exposed = Object.fromEntries(
1647
- entries.map(([name], index) => [name, elements[index]]),
1648
- );
1649
- const references = entries.map(
1650
- ([, source]) => anchorReference(source).model,
1539
+ [referenceBounds](
1540
+ reference: StoredAnchor,
1541
+ transform: RigidTransform,
1542
+ ): LocalBounds {
1543
+ if (reference.bound) {
1544
+ const frame = composeTransforms(transform, reference.transform);
1545
+ const [x, z] = reference.bound.size;
1546
+ return pointBounds(
1547
+ [-1, 1].flatMap(a =>
1548
+ [-1, 1].map(
1549
+ b =>
1550
+ composeTransforms(
1551
+ frame,
1552
+ translation([(a * x) / 2, 0, (b * z) / 2]),
1553
+ ).position,
1554
+ ),
1555
+ ),
1556
+ );
1557
+ }
1558
+ if (reference.kind === 'point') {
1559
+ const point = composeTransforms(transform, reference.transform).position;
1560
+ return [point, point];
1561
+ }
1562
+ throw new Error(
1563
+ `The reference ${reference.name} has no finite geometry. Select a model, vertex, edge, or surface for on().`,
1651
1564
  );
1652
- const operation = storedOperation('expose', [
1653
- {model: this, role: 'source', index: 0},
1654
- ...uniqueModels(references)
1655
- .filter(model => model !== this)
1656
- .map((model, index) => ({
1657
- model,
1658
- role: 'reference' as const,
1659
- index,
1660
- })),
1661
- ]);
1662
- return this.copy(
1663
- {elements: {...this.elements, ...exposed}},
1664
- operation,
1665
- ) as unknown as RuntimeModel<
1666
- MergedElements<Elements, ExposedElements<Sources>>,
1667
- Kind
1668
- >;
1669
1565
  }
1670
1566
 
1671
- /** Resolve both own and occurrence references into this model's local frame. */
1672
- private localElements(sources: readonly Anchor[]): StoredElement[] {
1673
- const members = this.memberPoses();
1674
- const external = sources
1675
- .map(source => anchorReference(source).model)
1676
- .filter(model => !members.has(model));
1677
- const context = external.length
1678
- ? ModelObject.createSolveContext([this, ...external])
1679
- : undefined;
1680
- return sources.map(source => {
1681
- const reference = anchorReference(source);
1682
- const {
1683
- kind,
1684
- transform: frame,
1685
- topology,
1686
- members: nested,
1687
- bound,
1688
- facing,
1689
- } = source instanceof ModelObject ? source.exposedElement() : reference;
1690
- const memberPose = members.get(reference.model);
1691
- if (memberPose === null)
1692
- throw new Error(
1693
- "The point or element belongs to multiple occurrences. Expose it through the intended child model's named reference.",
1694
- );
1695
- const transform =
1696
- memberPose ??
1697
- relativeTransform(
1698
- reference.model.solvePose(context!),
1699
- this.solvePose(context!),
1700
- );
1701
- return transformElement(
1702
- {kind, transform: frame, topology, members: nested, bound, facing},
1703
- transform,
1704
- );
1705
- });
1567
+ protected alignmentGeometry(reference: StoredAnchor): AlignmentGeometry {
1568
+ if (reference.kind === 'point')
1569
+ return {kind: 'point', point: reference.transform.position};
1570
+ const direction = rotateVector(
1571
+ [
1572
+ 0,
1573
+ reference.kind === 'line'
1574
+ ? (reference.direction ?? 1)
1575
+ : (reference.facing ?? reference.bound?.facing ?? 1),
1576
+ 0,
1577
+ ],
1578
+ reference.transform.quaternion,
1579
+ );
1580
+ return reference.kind === 'line'
1581
+ ? {kind: 'line', point: reference.transform.position, direction}
1582
+ : {kind: 'plane', point: reference.transform.position, normal: direction};
1706
1583
  }
1707
1584
 
1708
- vertex(id: VertexId): Vertex {
1709
- return this.vertices([id])[0];
1710
- }
1711
- vertices(ids?: readonly VertexId[]): readonly Vertex[] {
1712
- return this.selectTopology('vertex', ids) as unknown as readonly Vertex[];
1713
- }
1714
- edge(id: EdgeId): Edge {
1715
- return this.edges([id])[0];
1716
- }
1717
- edges(ids?: readonly EdgeId[]): readonly Edge[] {
1718
- return this.selectTopology('edge', ids) as unknown as readonly Edge[];
1719
- }
1720
- surface(id: SurfaceId): Surface {
1721
- return this.surfaces([id])[0];
1585
+ /** @internal */
1586
+ attachSource(sourceRef: SourceRef): void {
1587
+ const previous = this.sourceRefs.at(-1);
1588
+ if (
1589
+ previous?.file !== sourceRef.file ||
1590
+ previous.start !== sourceRef.start ||
1591
+ previous.end !== sourceRef.end
1592
+ ) {
1593
+ this.sourceRefs.push(sourceRef);
1594
+ }
1722
1595
  }
1723
- surfaces(ids?: readonly SurfaceId[]): readonly Surface[] {
1724
- return this.selectTopology('surface', ids) as unknown as readonly Surface[];
1596
+
1597
+ /** @internal */
1598
+ attachParameters(parameters: readonly ParameterUsage[]): void {
1599
+ appendUniqueParameters(this.parameters, parameters);
1725
1600
  }
1726
1601
 
1727
- private selectTopology(
1728
- kind: TopologyKind,
1729
- ids?: readonly TopologyId[],
1730
- ): readonly ModelTopologyElement[] {
1731
- const geometry = this.requireGeometry().value;
1732
- const selected = topologyChildren(
1733
- geometry.shape,
1734
- geometry.topology,
1735
- {kind: 'solid'},
1736
- kind,
1737
- ids,
1738
- );
1739
- return topologyReferences(
1740
- this,
1741
- '',
1742
- {source: this, transform: identityRigidTransform, scale: 1},
1743
- kind,
1744
- selected,
1745
- );
1602
+ solvePose(context: SolveContext): RigidTransform {
1603
+ return context.poses.get(this)!;
1746
1604
  }
1747
1605
 
1748
- /** @internal */
1749
- exposedElement(): StoredElement {
1750
- if (this.kind === 'group')
1751
- return {...this.geometryAnchor, members: this.elements};
1752
- const geometry = this.requireGeometry().value;
1753
- const kind = (
1754
- this.kind === 'face' ? 'surface' : this.kind
1755
- ) as TopologySelection['kind'];
1756
- const selection: TopologySelection =
1757
- kind === 'solid'
1758
- ? {kind}
1759
- : {
1760
- kind,
1761
- id: geometry.topology[
1762
- kind === 'surface'
1763
- ? 'surfaces'
1764
- : kind === 'edge'
1765
- ? 'edges'
1766
- : 'vertices'
1767
- ].ids[0],
1768
- };
1769
- const context = {
1770
- source: this,
1771
- transform: identityRigidTransform,
1772
- scale: 1,
1773
- };
1774
- const anchor =
1775
- selection.kind === 'solid'
1776
- ? this.geometryAnchor
1777
- : topologyReferences(this, '', context, selection.kind, [
1778
- selection.id,
1779
- ])[0][anchorReferenceValue];
1606
+ protected relationElement(reference: StoredAnchor): ElementSnapshot {
1607
+ const element = reference;
1608
+ const topology = element.topology;
1609
+ let arrow: RigidTransform | undefined;
1610
+ if (reference.kind === 'line' && topology) {
1611
+ const geometry = topology.source[modelGeometry]()!.value;
1612
+ arrow = withTopologyShape(
1613
+ geometry.shape,
1614
+ geometry.topology,
1615
+ topology.selection,
1616
+ shape => {
1617
+ const edge = shape as ReplicadEdge;
1618
+ const position = (reference.direction ?? 1) === 1 ? 1 : 0;
1619
+ const point = edge.pointAt(position),
1620
+ tangent = edge.tangentAt(position);
1621
+ try {
1622
+ return topologyTransform(
1623
+ topology,
1624
+ frameFromYAxis(
1625
+ point.toTuple(),
1626
+ tangent
1627
+ .toTuple()
1628
+ .map(v => v * (reference.direction ?? 1)) as unknown as Vec3,
1629
+ ),
1630
+ );
1631
+ } finally {
1632
+ point.delete();
1633
+ tangent.delete();
1634
+ }
1635
+ },
1636
+ );
1637
+ }
1780
1638
  return {
1781
- kind: anchor.kind,
1782
- transform: anchor.transform,
1783
- members: this.elements,
1784
- topology: {
1785
- ...context,
1786
- selection,
1787
- },
1639
+ ...snapshotElements({[reference.name]: element})[0],
1640
+ arrow,
1788
1641
  };
1789
1642
  }
1790
1643
 
1791
- material(material: Material | string): RuntimeModel<Elements, Kind> {
1792
- return this.copy(
1793
- {material: captureModelMaterial(material)},
1794
- storedOperation('material', [{model: this, role: 'source', index: 0}]),
1795
- );
1796
- }
1797
-
1798
- originOffset(
1799
- dx: number,
1800
- dy: number,
1801
- dz: number,
1802
- ): RuntimeModel<Elements, Kind> {
1803
- const offset: Vec3 = [dx, dy, dz];
1804
- assertFiniteVector('originOffset', offset);
1805
- return this.withOrigin(offset, {kind: 'originOffset'});
1644
+ private bodyRotations(
1645
+ constraint: StoredConstraint,
1646
+ indices: Map<RelationObject, number>,
1647
+ ): BodyRotation[] {
1648
+ return constraint.rotations.map(({pivot, angles}) => {
1649
+ if (pivot.kind === 'around') {
1650
+ return pivot.axis.model
1651
+ ? {
1652
+ body: indices.get(pivot.axis.model)!,
1653
+ axis: pivot.axis.transform,
1654
+ angle: (angles as number) * (pivot.axis.direction ?? 1),
1655
+ }
1656
+ : {
1657
+ local: axisRotation(
1658
+ pivot.axis.transform,
1659
+ (angles as number) * (pivot.axis.direction ?? 1),
1660
+ ),
1661
+ };
1662
+ }
1663
+ const frame = translation(
1664
+ pivot.kind === 'pivot' ? pivot.point : this.vertexPosition(pivot.id),
1665
+ );
1666
+ return {
1667
+ local: composeTransforms(
1668
+ composeTransforms(frame, rotationAround(origin, angles as Vec3)),
1669
+ invertTransform(frame),
1670
+ ),
1671
+ };
1672
+ });
1806
1673
  }
1807
1674
 
1808
- originPoint(point: PointAnchor): RuntimeModel<Elements, Kind> {
1809
- const reference = anchorReference(point);
1810
- if (reference.kind !== 'point')
1811
- throw new Error('originPoint() requires a point reference.');
1812
- const [element] = this.localElements([point]);
1813
- return this.withOrigin(element.transform.position, {
1814
- kind: 'originPoint',
1815
- model: reference.model,
1675
+ /** @internal */
1676
+ [relationPreview](
1677
+ nodeId: string,
1678
+ constraint: StoredConstraint,
1679
+ selection: ConstraintSpatialSelection | undefined,
1680
+ ): ConstraintPreview {
1681
+ // This expression starts from the relate receiver, before sibling constraints
1682
+ // are returned and committed to self. Geometry and node identity stay shared.
1683
+ const owner = this.copyRelations({
1684
+ nodeId,
1685
+ constraints: [...this.constraints, constraint],
1816
1686
  });
1687
+ const context = RelationObject.createSolveContext([
1688
+ owner,
1689
+ ...(selection?.pivot.kind === 'around' && selection.pivot.axis.model
1690
+ ? [selection.pivot.axis.model]
1691
+ : []),
1692
+ ]);
1693
+ return {
1694
+ object: {
1695
+ nodeId,
1696
+ compositionTransform: toTransform(owner.solvePose(context)),
1697
+ constraints: owner.constraints.map(value =>
1698
+ owner.constraintSnapshot(value, context),
1699
+ ),
1700
+ },
1701
+ spatial: selection
1702
+ ? owner.constraintSpatial(constraint, selection, context)
1703
+ : undefined,
1704
+ };
1817
1705
  }
1818
1706
 
1819
- originVertex(id: VertexId): RuntimeModel<Elements, Kind> {
1820
- const geometry = this.requireGeometry().value;
1821
- const [{position}] = topologyVertexPoints(
1822
- geometry.shape,
1823
- geometry.topology.vertices,
1824
- [id],
1707
+ private constraintSpatial(
1708
+ constraint: StoredConstraint,
1709
+ selection: ConstraintSpatialSelection,
1710
+ context: SolveContext,
1711
+ ): ConstraintSpatialReference {
1712
+ const models = [...context.poses.keys()];
1713
+ const actions = this.bodyRotations(
1714
+ constraint,
1715
+ new Map(models.map((model, i) => [model, i])),
1825
1716
  );
1826
- return this.withOrigin(position, {kind: 'originVertex', id});
1827
- }
1828
-
1829
- originCenter(): RuntimeModel<Elements, Kind> {
1830
- this.requireGeometry();
1831
- const position = this.elements.center.transform.position;
1832
- return this.withOrigin(position, {kind: 'originCenter'});
1833
- }
1834
-
1835
- private withOrigin(
1836
- offset: Vec3,
1837
- selection:
1838
- | {kind: 'originOffset' | 'originCenter'}
1839
- | {kind: 'originVertex'; id: VertexId}
1840
- | {kind: 'originPoint'; model: ModelObject},
1841
- ): RuntimeModel<Elements, Kind> {
1842
- const transform = translation(negateVector(offset));
1843
- const operation = storedOperation(selection.kind, [
1844
- {model: this, role: 'source', index: 0},
1845
- ]);
1846
- if (selection.kind === 'originPoint' && selection.model !== this)
1847
- operation.inputs.push({
1848
- model: selection.model,
1849
- role: 'reference',
1850
- index: 0,
1851
- });
1852
- operation.spatial = {origin, vector: offset};
1853
- if (selection.kind === 'originVertex') {
1854
- operation.selections.push({
1855
- kind: 'vertex',
1856
- input: this,
1857
- ids: [selection.id],
1858
- transform,
1859
- });
1717
+ const stage = actions.length - (selection.kind === 'rotate' ? 1 : 0);
1718
+ const finalPose = this.solvePose(context);
1719
+ let before = finalPose;
1720
+ for (let i = actions.length - 1; i >= stage; i--) {
1721
+ const action = actions[i];
1722
+ before =
1723
+ 'local' in action
1724
+ ? composeTransforms(before, invertTransform(action.local))
1725
+ : composeTransforms(
1726
+ axisRotation(
1727
+ composeTransforms(
1728
+ models[action.body].solvePose(context),
1729
+ action.axis,
1730
+ ),
1731
+ -action.angle,
1732
+ ),
1733
+ before,
1734
+ );
1860
1735
  }
1861
- return this.transformed(transform, operation);
1862
- }
1863
-
1864
- rotate(x: number, y: number, z: number): RuntimeModel<Elements, Kind> {
1865
- const angles: Vec3 = [x, y, z];
1866
- assertFiniteVector('rotate', angles);
1867
- const operation = storedOperation('rotate', [
1868
- {model: this, role: 'source', index: 0},
1869
- ]);
1870
- operation.spatial = {origin, vector: angles};
1871
- return this.transformed(rotationAround(origin, angles), operation);
1736
+ const pivot = selection.pivot;
1737
+ let frame: RigidTransform;
1738
+ if (pivot.kind === 'around') {
1739
+ frame = composeTransforms(
1740
+ pivot.axis.model ? pivot.axis.model.solvePose(context) : before,
1741
+ (pivot.axis.direction ?? 1) === 1
1742
+ ? pivot.axis.transform
1743
+ : composeTransforms(pivot.axis.transform, rotation([1, 0, 0, 0])),
1744
+ );
1745
+ } else {
1746
+ const local = translation(
1747
+ pivot.kind === 'pivot' ? pivot.point : this.vertexPosition(pivot.id),
1748
+ );
1749
+ frame = composeTransforms(before, local);
1750
+ }
1751
+ const angles = constraint.rotations[stage]?.angles ?? origin;
1752
+ const rotationVector: Vec3 =
1753
+ typeof angles === 'number' ? [0, angles, 0] : angles;
1754
+ const localFrame = relativeTransform(frame, finalPose);
1755
+ return {
1756
+ nodeId: this.nodeId,
1757
+ kind: selection.kind,
1758
+ spatial: {
1759
+ origin: localFrame.position,
1760
+ vector:
1761
+ selection.kind === 'rotate'
1762
+ ? rotationVector
1763
+ : pivot.kind === 'pivot'
1764
+ ? pivot.point
1765
+ : origin,
1766
+ frame: localFrame,
1767
+ rotation: rotationVector,
1768
+ axisOnly: pivot.kind === 'around',
1769
+ },
1770
+ };
1872
1771
  }
1873
1772
 
1874
- private transformed(
1875
- transform: RigidTransform,
1876
- operation: StoredOperation,
1877
- ): RuntimeModel<Elements, Kind> {
1878
- const overrides: Partial<ModelObjectInit<Kind>> = {
1879
- geometryAnchor: transformElement(this.geometryAnchor, transform),
1880
- elements: Object.fromEntries(
1881
- Object.entries(this.elements).map(([name, element]) => [
1882
- name,
1883
- transformElement(element, transform),
1884
- ]),
1885
- ),
1886
- constraints: this.mapConstraintGeometry(
1887
- element => transformElement(element, transform),
1888
- point => composeTransforms(transform, translation(point)).position,
1889
- ),
1773
+ static createSolveContext(roots: readonly RelationObject[]): SolveContext {
1774
+ const models: RelationObject[] = [];
1775
+ const indices = new Map<RelationObject, number>();
1776
+ const collect = (model: RelationObject): void => {
1777
+ if (indices.has(model)) return;
1778
+ indices.set(model, models.length);
1779
+ models.push(model);
1780
+ model.constraints.flatMap(constraintReferences).forEach(collect);
1890
1781
  };
1891
- if (this.assembly)
1892
- return this.copy(
1893
- {
1894
- ...overrides,
1895
- assembly: transformSolveContext(this.assembly, transform),
1896
- },
1897
- operation,
1898
- );
1899
- const source = this.requireGeometry();
1900
- const geometry = evaluateModelGeometry(
1901
- 'transform',
1902
- [transform.position, transform.quaternion],
1903
- [source],
1904
- () => {
1905
- const shape = shapeWithTransform(source.value.shape, transform);
1906
- try {
1907
- return {
1908
- shape,
1909
- topology: preserveShapeTopology(shape, source.value.topology),
1910
- referenceBasis: transformedReferenceBasis(source, transform, 1),
1911
- };
1912
- } catch (error) {
1913
- shape.delete();
1914
- throw error;
1915
- }
1916
- },
1782
+ roots.forEach(collect);
1783
+ const poses = solveBodies(
1784
+ models.map((model): Body => ({
1785
+ name: model.name,
1786
+ relations: model.constraints.map(constraint =>
1787
+ constraint.kind === 'align'
1788
+ ? {
1789
+ kind: 'align',
1790
+ id: constraint.id,
1791
+ source: {
1792
+ body: indices.get(constraint.source.model ?? model)!,
1793
+ geometry: (
1794
+ constraint.source.model ?? model
1795
+ ).alignmentGeometry(constraint.source),
1796
+ transform: constraint.source.transform,
1797
+ },
1798
+ target: {
1799
+ body: indices.get(constraint.target.model ?? model)!,
1800
+ geometry: (
1801
+ constraint.target.model ?? model
1802
+ ).alignmentGeometry(constraint.target),
1803
+ transform: constraint.target.transform,
1804
+ },
1805
+ offset: constraint.offset,
1806
+ rotations: model.bodyRotations(constraint, indices),
1807
+ }
1808
+ : {
1809
+ kind: 'on',
1810
+ id: constraint.id,
1811
+ source: {
1812
+ body: indices.get(constraint.source.model ?? model)!,
1813
+ key: constraint.source.name,
1814
+ bounds: (orientation: Quaternion) =>
1815
+ (constraint.source.model ?? model)[referenceBounds](
1816
+ constraint.source,
1817
+ rotation(orientation),
1818
+ ),
1819
+ },
1820
+ target: {
1821
+ body: indices.get(constraint.target.model ?? model)!,
1822
+ transform: constraint.target.transform,
1823
+ facing: constraint.target.bound!.facing,
1824
+ },
1825
+ offset: constraint.offset,
1826
+ rotations: model.bodyRotations(constraint, indices),
1827
+ },
1828
+ ),
1829
+ })),
1917
1830
  );
1918
- return this.copyWithGeometry(geometry, overrides, operation);
1831
+ return {
1832
+ poses: new Map(models.map((model, index) => [model, poses[index]])),
1833
+ };
1919
1834
  }
1920
1835
 
1921
- private mapConstraintGeometry(
1922
- mapElement: (element: StoredElement) => StoredElement,
1923
- mapPoint: (point: Vec3) => Vec3,
1924
- ): StoredConstraint[] {
1925
- const mapReference = (reference: RelationReference): RelationReference =>
1926
- reference.model ? reference : {...reference, ...mapElement(reference)};
1927
- return this.constraints.map(constraint => {
1928
- const mapped: StoredConstraint = {
1929
- ...constraint,
1930
- source: mapReference(constraint.source),
1931
- target: mapReference(constraint.target),
1932
- rotations: constraint.rotations.map(action => ({
1933
- ...action,
1934
- pivot:
1935
- action.pivot.kind === 'around'
1936
- ? {...action.pivot, axis: mapReference(action.pivot.axis)}
1937
- : action.pivot.kind === 'pivot'
1938
- ? {...action.pivot, point: mapPoint(action.pivot.point)}
1939
- : action.pivot,
1940
- })),
1836
+ protected constraintSnapshot(
1837
+ constraint: StoredConstraint,
1838
+ context: SolveContext,
1839
+ ): ConstraintSnapshot {
1840
+ const source = constraint.source.model ?? this;
1841
+ const target = constraint.target.model ?? this;
1842
+ const models = [...context.poses.keys()];
1843
+ const actions = this.bodyRotations(
1844
+ constraint,
1845
+ new Map(models.map((model, i) => [model, i])),
1846
+ );
1847
+ let before = this.solvePose(context);
1848
+ for (const action of [...actions].reverse()) {
1849
+ before =
1850
+ 'local' in action
1851
+ ? composeTransforms(before, invertTransform(action.local))
1852
+ : composeTransforms(
1853
+ axisRotation(
1854
+ composeTransforms(
1855
+ models[action.body].solvePose(context),
1856
+ action.axis,
1857
+ ),
1858
+ -action.angle,
1859
+ ),
1860
+ before,
1861
+ );
1862
+ }
1863
+ const sourcePose = source === this ? before : source.solvePose(context);
1864
+ const targetPose = target === this ? before : target.solvePose(context);
1865
+ let offsetFrame = composeTransforms(
1866
+ targetPose,
1867
+ constraint.target.transform,
1868
+ );
1869
+ if (constraint.kind === 'align') {
1870
+ for (const action of actions)
1871
+ if ('body' in action)
1872
+ offsetFrame = composeTransforms(
1873
+ axisRotation(
1874
+ composeTransforms(
1875
+ models[action.body].solvePose(context),
1876
+ action.axis,
1877
+ ),
1878
+ action.angle,
1879
+ ),
1880
+ offsetFrame,
1881
+ );
1882
+ return {
1883
+ id: constraint.id,
1884
+ kind: 'align',
1885
+ source: anchorSnapshot(source, constraint.source),
1886
+ target: anchorSnapshot(target, constraint.target),
1887
+ sourceElement: source.relationElement(constraint.source),
1888
+ targetElement: target.relationElement(constraint.target),
1889
+ offsetDirection: 1,
1890
+ offset: constraint.offset ?? origin,
1891
+ offsetFrame: toTransform(offsetFrame),
1892
+ sourceRefs: [...valueTrace(constraint).sourceRefs],
1893
+ parameters: [...valueTrace(constraint).parameters],
1941
1894
  };
1942
- valueTraces.set(mapped, valueTrace(constraint));
1943
- return mapped;
1944
- });
1945
- }
1946
-
1947
- scaled(factor: number): RuntimeModel<Elements, Kind> {
1948
- assertPositive('scale', factor);
1949
- const source = this.requireGeometry();
1950
- const geometry = evaluateModelGeometry('scaled', [factor], [source], () => {
1951
- const shape = shapeWithScale(source.value.shape, factor);
1952
- try {
1953
- return {
1954
- shape,
1955
- topology: preserveShapeTopology(shape, source.value.topology),
1956
- referenceBasis: transformedReferenceBasis(
1957
- source,
1958
- identityRigidTransform,
1959
- factor,
1960
- ),
1961
- };
1962
- } catch (error) {
1963
- shape.delete();
1964
- throw error;
1965
- }
1966
- });
1967
- return this.copyWithGeometry(
1968
- geometry,
1895
+ }
1896
+ const orientation = composeTransforms(
1897
+ invertTransform(rotation(offsetFrame.quaternion)),
1898
+ rotation(sourcePose.quaternion),
1899
+ ).quaternion;
1900
+ const bounds = source[referenceBounds](
1901
+ constraint.source,
1902
+ rotation(orientation),
1903
+ );
1904
+ const center: Vec3 = [
1905
+ (bounds[0][0] + bounds[1][0]) / 2,
1906
+ bounds[constraint.target.bound!.facing === 1 ? 0 : 1][1],
1907
+ (bounds[0][2] + bounds[1][2]) / 2,
1908
+ ];
1909
+ const sourceFrame = relativeTransform(
1969
1910
  {
1970
- geometryAnchor: scaleElement(this.geometryAnchor, factor),
1971
- elements: scaleElements(this.elements, factor),
1972
- constraints: this.mapConstraintGeometry(
1973
- element => scaleElement(element, factor),
1974
- point => scaleFrame(translation(point), factor).position,
1911
+ position: addVectors(
1912
+ sourcePose.position,
1913
+ rotateVector(center, offsetFrame.quaternion),
1975
1914
  ),
1915
+ quaternion: offsetFrame.quaternion,
1976
1916
  },
1977
- storedOperation('scaled', [{model: this, role: 'source', index: 0}]),
1978
- );
1979
- }
1980
-
1981
- extrude(this: ModelObject<Elements, 'face'>, distance: number): SolidModel {
1982
- if (this.kind !== 'face')
1983
- throw new Error('extrude requires a single face model.');
1984
- if (!Number.isFinite(distance) || distance === 0)
1985
- throw new Error('Extrusion distance must be finite and non-zero.');
1986
- const source = this.requireGeometry();
1987
- const direction = rotateVector(
1988
- [0, distance, 0],
1989
- this.geometryAnchor.transform.quaternion,
1917
+ sourcePose,
1990
1918
  );
1991
- const geometry = evaluateSolidGeometry(
1992
- 'extrude',
1993
- [direction],
1994
- [source],
1995
- () =>
1996
- extrudeWithTopology(
1997
- {
1998
- shape: source.value.shape,
1999
- topology: source.value.topology,
2000
- index: 1,
2001
- },
2002
- direction,
1919
+ const sourceSize: Vec3 = [
1920
+ bounds[1][0] - bounds[0][0],
1921
+ bounds[1][1] - bounds[0][1],
1922
+ bounds[1][2] - bounds[0][2],
1923
+ ];
1924
+ for (const action of actions) {
1925
+ if ('body' in action)
1926
+ offsetFrame = composeTransforms(
1927
+ axisRotation(
1928
+ composeTransforms(
1929
+ models[action.body].solvePose(context),
1930
+ action.axis,
1931
+ ),
1932
+ action.angle,
1933
+ ),
1934
+ offsetFrame,
1935
+ );
1936
+ }
1937
+ return {
1938
+ id: constraint.id,
1939
+ kind: constraint.kind,
1940
+ source: anchorSnapshot(source, constraint.source),
1941
+ target: anchorSnapshot(target, constraint.target),
1942
+ sourceBounds: {
1943
+ size: sourceSize,
1944
+ transform: toTransform(
1945
+ composeTransforms(sourceFrame, {
1946
+ position: [
1947
+ 0,
1948
+ (constraint.target.bound!.facing * sourceSize[1]) / 2,
1949
+ 0,
1950
+ ],
1951
+ quaternion: [0, 0, 0, 1],
1952
+ }),
2003
1953
  ),
2004
- );
2005
- return ModelObject.create<CanonicalElements, 'solid'>({
2006
- kind: 'solid',
2007
- name: 'Extrude',
2008
- geometry,
2009
- material: this.materialSnapshot,
2010
- constraints: this.constraints,
2011
- sourceRefs: this.sourceRefs,
2012
- parameters: this.allParameters(),
2013
- meshTolerance: this.meshTolerance,
2014
- operation: storedOperation(
2015
- 'extrude',
2016
- [{model: this, role: 'receiver', index: 0}],
2017
- {
2018
- dimensions: {
2019
- distance: {
2020
- origin: this.elements.center.transform.position,
2021
- vector: direction,
2022
- },
2023
- },
1954
+ },
1955
+ sourceElement: {
1956
+ name: `${constraint.source.name}:bound`,
1957
+ kind: 'face',
1958
+ transform: toTransform(sourceFrame),
1959
+ bound: {
1960
+ size: [sourceSize[0], sourceSize[2]],
1961
+ facing: constraint.target.bound!.facing === 1 ? -1 : 1,
2024
1962
  },
2025
- ),
2026
- }) as unknown as SolidModel;
1963
+ },
1964
+ targetElement: {
1965
+ name: constraint.target.name,
1966
+ kind: 'face',
1967
+ transform: toTransform(constraint.target.transform),
1968
+ bound: constraint.target.bound,
1969
+ },
1970
+ offsetDirection: source === this ? 1 : -1,
1971
+ offset: constraint.offset ?? origin,
1972
+ offsetFrame: toTransform(offsetFrame),
1973
+ sourceRefs: [...valueTrace(constraint).sourceRefs],
1974
+ parameters: [...valueTrace(constraint).parameters],
1975
+ };
2027
1976
  }
1977
+ }
2028
1978
 
2029
- cut(
2030
- this: ModelObject<Elements, 'solid'>,
2031
- tools: readonly SolidModel<{}>[],
2032
- ): SolidModel {
2033
- return cut(this as unknown as SolidModel<{}>, tools);
1979
+ /** Plane-only spatial value used by sketches before a B-Rep face exists. */
1980
+ export class SketchFrame extends RelationObject {
1981
+ readonly name = 'Sketch';
1982
+ readonly plane: FaceAnchor;
1983
+ private readonly source?: SketchFrame;
1984
+ private readonly operation = storedOperation('sketch');
1985
+
1986
+ constructor(source?: SketchFrame, init: RelationObjectInit = {}) {
1987
+ super({
1988
+ constraints: source?.constraints,
1989
+ sourceRefs: source?.sourceRefs,
1990
+ parameters: source?.parameters,
1991
+ ...init,
1992
+ });
1993
+ this.source = source;
1994
+ this.plane = modelAnchor(this, 'plane', {
1995
+ kind: 'face',
1996
+ transform: identityRigidTransform,
1997
+ }) as FaceAnchor;
2034
1998
  }
2035
1999
 
2036
- fillet(
2037
- this: ModelObject<Elements, 'solid'>,
2038
- radius: number,
2039
- edgeIds?: readonly EdgeId[],
2040
- ): SolidModel<Elements> {
2041
- assertPositive('radius', radius);
2042
- const source = this.requireSolidGeometry();
2043
- const selectedEdgeIds = resolveEdgeSelection(
2044
- source.value.topology.edges,
2045
- edgeIds,
2046
- );
2047
- const geometry = evaluateSolidGeometry(
2048
- 'fillet',
2049
- [radius, selectedEdgeIds],
2050
- [source],
2051
- () => {
2052
- const result = filletEdges(
2053
- source.value.shape,
2054
- source.value.topology,
2055
- radius,
2056
- selectedEdgeIds,
2057
- );
2058
- return {shape: result.shape, topology: result.topology};
2059
- },
2060
- );
2061
- return this.copyWithGeometry(
2062
- geometry,
2063
- {},
2064
- storedOperation('fillet', [{model: this, role: 'source', index: 0}], {
2065
- selections: [
2066
- {
2067
- kind: 'edge',
2068
- input: this,
2069
- ids: selectedEdgeIds,
2070
- transform: identityRigidTransform,
2071
- },
2072
- ],
2073
- }),
2074
- ) as unknown as SolidModel<Elements>;
2075
- }
2076
-
2077
- chamfer(
2078
- this: ModelObject<Elements, 'solid'>,
2079
- distance: number,
2080
- edgeIds?: readonly EdgeId[],
2081
- ): SolidModel<Elements> {
2082
- assertPositive('distance', distance);
2083
- const source = this.requireSolidGeometry();
2084
- const selectedEdgeIds = resolveEdgeSelection(
2085
- source.value.topology.edges,
2086
- edgeIds,
2087
- );
2088
- const geometry = evaluateSolidGeometry(
2089
- 'chamfer',
2090
- [distance, selectedEdgeIds],
2091
- [source],
2092
- () => {
2093
- const result = chamferEdges(
2094
- source.value.shape,
2095
- source.value.topology,
2096
- distance,
2097
- selectedEdgeIds,
2098
- );
2099
- return {shape: result.shape, topology: result.topology};
2100
- },
2101
- );
2102
- return this.copyWithGeometry(
2103
- geometry,
2104
- {},
2105
- storedOperation('chamfer', [{model: this, role: 'source', index: 0}], {
2106
- selections: [
2107
- {
2108
- kind: 'edge',
2109
- input: this,
2110
- ids: selectedEdgeIds,
2111
- transform: identityRigidTransform,
2112
- },
2113
- ],
2114
- }),
2115
- ) as unknown as SolidModel<Elements>;
2116
- }
2117
-
2118
- shell(
2119
- this: ModelObject<Elements, 'solid'>,
2120
- thickness: number,
2121
- removedSurfaceIds: readonly SurfaceId[] = [],
2122
- ): SolidModel<Elements> {
2123
- if (!Number.isFinite(thickness) || thickness === 0) {
2124
- throw new Error('thickness must be a nonzero finite number.');
2125
- }
2126
- const source = this.requireSolidGeometry();
2127
- const selectedIds = resolveTopologySelection(
2128
- 'surface',
2129
- source.value.topology.surfaces,
2130
- removedSurfaceIds,
2131
- );
2132
- const geometry = evaluateSolidGeometry(
2133
- 'shell',
2134
- [thickness, selectedIds],
2135
- [source],
2136
- () =>
2137
- shellWithTopology(
2138
- source.value.shape,
2139
- source.value.topology,
2140
- thickness,
2141
- selectedIds,
2142
- ),
2143
- );
2144
- return this.copyWithGeometry(
2145
- geometry,
2146
- {},
2147
- storedOperation('shell', [{model: this, role: 'source', index: 0}], {
2148
- selections: [
2149
- {
2150
- kind: 'surface',
2151
- input: this,
2152
- ids: selectedIds,
2153
- transform: identityRigidTransform,
2154
- },
2155
- ],
2156
- }),
2157
- ) as unknown as SolidModel<Elements>;
2158
- }
2159
-
2160
- /** @internal */
2161
- attachSource(sourceRef: SourceRef): void {
2162
- const previous = this.sourceRefs.at(-1);
2163
- if (
2164
- previous?.file !== sourceRef.file ||
2165
- previous.start !== sourceRef.start ||
2166
- previous.end !== sourceRef.end
2167
- ) {
2168
- this.sourceRefs.push(sourceRef);
2169
- }
2000
+ protected copyRelations(init: RelationObjectInit): SketchFrame {
2001
+ return new SketchFrame(this, init);
2170
2002
  }
2171
2003
 
2172
- /** @internal */
2173
- attachParameters(parameters: readonly ParameterUsage[]): void {
2174
- appendUniqueParameters(this.parameters, parameters);
2004
+ relatedObjects(): readonly RelationObject[] {
2005
+ return [
2006
+ ...(this.source ? [this.source] : []),
2007
+ ...this.constraints.flatMap(constraintReferences),
2008
+ ];
2175
2009
  }
2176
2010
 
2177
- /** @internal */
2178
2011
  attachOperationTrace(
2179
2012
  siteId: string,
2180
2013
  execution: number,
2181
2014
  order: number,
2182
2015
  sourceRef: SourceRef,
2016
+ outputIndex = 0,
2183
2017
  ): void {
2184
- if (operationTraces.has(this.operation)) {
2185
- return;
2186
- }
2187
- operationTraces.set(this.operation, {siteId, execution, order, sourceRef});
2188
- }
2189
-
2190
- /** @internal */
2191
- relatedObjects(): readonly ModelObject[] {
2192
- return [
2193
- ...this.children,
2194
- ...this.operation.inputs.map(input => input.model),
2195
- ...this.constraints.flatMap(constraintReferences),
2196
- ];
2197
- }
2198
-
2199
- /** @internal */
2200
- toSnapshot(
2201
- meshCache: Map<AnyShape, RenderMesh> = new Map(),
2202
- ): ModelSnapshotObject {
2203
- const solveContext = ModelObject.createSolveContext([this]);
2204
- return this.snapshotNode(meshCache, solveContext);
2018
+ if (!operationTraces.has(this.operation))
2019
+ operationTraces.set(this.operation, {
2020
+ siteId,
2021
+ execution,
2022
+ order,
2023
+ sourceRef,
2024
+ outputIndex,
2025
+ });
2205
2026
  }
2206
2027
 
2207
- private snapshotNode(
2208
- meshCache: Map<AnyShape, RenderMesh>,
2209
- solveContext: SolveContext,
2210
- inComposition = false,
2211
- overrideMaterial?: ModelMaterialSnapshot,
2212
- ): ModelSnapshotObject {
2213
- const material = overrideMaterial ?? this.materialSnapshot;
2214
- const pose = this.solvePose(solveContext);
2215
- const constraints = this.constraints.map(constraint =>
2216
- this.constraintSnapshot(constraint, solveContext),
2217
- );
2218
- const parameters = uniqueParameters([
2219
- ...this.parameters,
2220
- ...this.constraints.flatMap(
2221
- constraint => valueTrace(constraint).parameters,
2222
- ),
2223
- ]);
2224
- const common = {
2225
- nodeId: this.nodeId,
2226
- kind: this.kind,
2028
+ toSnapshot(): ModelSnapshotObject {
2029
+ const {siteId, execution, order, sourceRef} =
2030
+ operationTraces.get(this.operation) ?? {};
2031
+ return {
2032
+ ...this.snapshot(),
2033
+ kind: 'reference',
2227
2034
  name: this.name,
2228
- material,
2229
- compositionTransform: toTransform(pose),
2230
- transform: toTransform(inComposition ? pose : identityRigidTransform),
2231
- constraints,
2232
- origin,
2035
+ children: [],
2036
+ transform: toTransform(identityRigidTransform),
2233
2037
  elements: snapshotElements({
2234
- ...this.elements,
2235
- ...(this.geometry || this.children.length
2236
- ? Object.fromEntries(
2237
- Object.keys(boundDirections).map(direction => [
2238
- direction,
2239
- anchorReference(this[direction as keyof DirectionalBounds]),
2240
- ]),
2241
- )
2242
- : {}),
2038
+ plane: {kind: 'face', transform: identityRigidTransform},
2243
2039
  }),
2040
+ origin,
2244
2041
  sourceRefs: [...this.sourceRefs],
2245
- parameters,
2246
- operation: this.operationSnapshot(meshCache),
2247
- } as const;
2042
+ parameters: [...this.parameters],
2043
+ operation: {
2044
+ id: storedOperationId(this.operation),
2045
+ siteId,
2046
+ execution,
2047
+ order,
2048
+ sourceRef,
2049
+ kind: 'sketch',
2050
+ outputNodeId: this.nodeId,
2051
+ inputs: this.relatedObjects().map((model, index) => ({
2052
+ nodeId: model.nodeId,
2053
+ role: 'reference',
2054
+ index,
2055
+ })),
2056
+ regions: [],
2057
+ selections: [],
2058
+ },
2059
+ };
2060
+ }
2248
2061
 
2249
- if (this.kind === 'group') {
2250
- const childContext = this.assembly!;
2251
- return {
2252
- ...common,
2253
- children: this.children.map(child =>
2254
- child.snapshotNode(meshCache, childContext, true, material),
2255
- ),
2256
- };
2257
- }
2062
+ relate(
2063
+ build: (frame: SketchFrame) => Constraint | readonly Constraint[],
2064
+ ): SketchFrame {
2065
+ const related = new SketchFrame(this);
2066
+ related.addRelations(this, () => build(related));
2067
+ return related;
2068
+ }
2258
2069
 
2259
- const geometry = this.requireGeometry();
2070
+ face(region: SketchRegion): FaceModel {
2071
+ return sketchFaceModel(region, this.constraints, this);
2072
+ }
2073
+
2074
+ snapshot(): ConstraintPreview['object'] {
2075
+ const context = RelationObject.createSolveContext([this]);
2260
2076
  return {
2261
- ...common,
2262
- children: [],
2263
- mesh: renderMesh(
2264
- geometry,
2265
- geometry.value.shape,
2266
- meshCache,
2267
- this.meshTolerance,
2268
- geometry.value.topology,
2077
+ nodeId: this.nodeId,
2078
+ compositionTransform: toTransform(this.solvePose(context)),
2079
+ constraints: this.constraints.map(constraint =>
2080
+ this.constraintSnapshot(constraint, context),
2269
2081
  ),
2270
2082
  };
2271
2083
  }
2272
2084
 
2273
- /** @internal */
2274
- disposeShape(disposed: Set<AnyShape>): void {
2275
- const shapes = [
2276
- ...(this.geometry ? [this.geometry.value.shape] : []),
2277
- ...(this.geometry?.value.referenceBasis
2278
- ? [this.geometry.value.referenceBasis.shape]
2279
- : []),
2280
- ...this.operation.regions.map(region => region.artifact.value),
2281
- ];
2282
- for (const shape of shapes) {
2283
- if (!disposed.has(shape)) {
2284
- disposed.add(shape);
2285
- shape.delete();
2286
- }
2287
- }
2288
- this.children.forEach(child => child.disposeShape(disposed));
2085
+ /** Related finite models, expressed in this sketch's local editing plane. */
2086
+ context(): readonly Readonly<{nodeId: string; transform: Transform}>[] {
2087
+ const context = RelationObject.createSolveContext([this]);
2088
+ const frame = this.solvePose(context);
2089
+ return [...context.poses].flatMap(([object, pose]) =>
2090
+ object instanceof ModelObject
2091
+ ? [
2092
+ {
2093
+ nodeId: object.nodeId,
2094
+ transform: toTransform(relativeTransform(pose, frame)),
2095
+ },
2096
+ ]
2097
+ : [],
2098
+ );
2289
2099
  }
2100
+ }
2290
2101
 
2102
+ export class ModelObject<
2103
+ Elements extends NamedElements = {},
2104
+ Kind extends ModelKind = ModelKind,
2105
+ >
2106
+ extends RelationObject
2107
+ implements Anchor<ModelElementKind<Kind>>
2108
+ {
2109
+ declare readonly [anchorKind]: ModelElementKind<Kind>;
2110
+ declare readonly [modelKind]: Kind;
2111
+ declare readonly [modelNamedElements]: Elements;
2291
2112
  /** @internal */
2292
- [loftModels](
2293
- this: ModelObject<Elements, 'face'>,
2294
- others: readonly ModelObject<{}, 'face'>[],
2295
- spine: ModelObject<{}, 'edge'> | undefined,
2296
- ruled: boolean,
2297
- ): SolidModel {
2298
- const sections: readonly ModelObject<{}, 'face'>[] = [this, ...others];
2299
- const solveContext = ModelObject.createSolveContext([
2300
- ...sections,
2301
- ...(spine ? [spine] : []),
2302
- ]);
2303
- const resultPose = this.solvePose(solveContext);
2304
- const sectionInputs = sections.map(section => ({
2305
- model: section,
2306
- geometry: section.requireGeometry(),
2307
- transform: relativeTransform(section.solvePose(solveContext), resultPose),
2308
- }));
2309
- const spineInput = spine
2310
- ? {
2311
- model: spine,
2312
- geometry: spine.requireGeometry(),
2313
- transform: relativeTransform(
2314
- spine.solvePose(solveContext),
2315
- resultPose,
2316
- ),
2317
- }
2318
- : undefined;
2319
- const geometry = evaluateSolidGeometry(
2320
- spine ? 'spine-loft' : 'loft',
2321
- [
2322
- ruled,
2323
- sectionInputs.map(input => [
2324
- input.transform.position,
2325
- input.transform.quaternion,
2326
- ]),
2327
- spineInput
2328
- ? [spineInput.transform.position, spineInput.transform.quaternion]
2329
- : null,
2330
- ],
2331
- [
2332
- ...sectionInputs.map(input => input.geometry),
2333
- ...(spineInput ? [spineInput.geometry] : []),
2334
- ],
2335
- () => buildLoftGeometry(sectionInputs, spineInput, ruled),
2336
- );
2337
- const inputs = [...sections, ...(spine ? [spine] : [])];
2338
- return ModelObject.create<CanonicalElements, 'solid'>({
2339
- kind: 'solid',
2340
- name: 'Loft',
2341
- geometry,
2342
- material: this.materialSnapshot,
2343
- constraints: this.constraints,
2344
- sourceRefs: inputs.flatMap(input => input.sourceRefs),
2345
- parameters: uniqueParameters(
2346
- inputs.flatMap(input => input.allParameters()),
2347
- ),
2348
- meshTolerance: Math.min(...inputs.map(input => input.meshTolerance)),
2349
- operation: storedOperation('loft', [
2350
- {model: this, role: 'receiver', index: 0},
2351
- ...others.map((model, index) => ({
2352
- model,
2353
- role: 'section' as const,
2354
- index: index + 1,
2355
- })),
2356
- ...(spine
2357
- ? [{model: spine, role: 'spine' as const, index: sections.length}]
2358
- : []),
2359
- ]),
2360
- }) as unknown as SolidModel;
2361
- }
2113
+ readonly elementKind: ModelElementKind<Kind>;
2114
+ /** @internal */
2115
+ readonly kind: Kind;
2116
+ /** @internal */
2117
+ readonly name: string;
2118
+ /** @internal */
2119
+ private readonly materialSnapshot?: ModelMaterialSnapshot;
2120
+ /** @internal */
2121
+ readonly children: readonly ModelObject[];
2122
+ private readonly geometry?: ModelGeometry;
2123
+ private readonly assembly?: SolveContext;
2124
+ private readonly meshTolerance: number;
2125
+ private readonly geometryAnchor: StoredElement;
2126
+ private readonly elements: StoredElements;
2127
+ private readonly operation: StoredOperation;
2362
2128
 
2363
2129
  /** @internal */
2364
- [combineModels](
2365
- this: ModelObject<Elements, 'solid'>,
2366
- operation: BooleanOperation,
2367
- others: readonly ModelObject<{}, 'solid'>[],
2368
- ): SolidModel {
2369
- const evaluation = this.evaluateBoolean(operation, others);
2370
- let transferred = false;
2371
- try {
2372
- const combined = ModelObject.create<CanonicalElements, 'solid'>({
2373
- kind: 'solid',
2374
- geometryAnchor: this.geometryAnchor,
2375
- geometry: evaluation.geometry,
2376
- name: this.name,
2377
- material: this.materialSnapshot,
2378
- constraints: this.constraints,
2379
- sourceRefs: [this, ...others].flatMap(model => model.sourceRefs),
2380
- parameters: uniqueParameters(
2381
- [this, ...others].flatMap(model => model.allParameters()),
2382
- ),
2383
- meshTolerance: Math.min(
2384
- this.meshTolerance,
2385
- ...others.map(model => model.meshTolerance),
2386
- ),
2387
- operation: storedOperation(
2388
- operation === 'fuse' ? 'union' : operation,
2389
- [
2390
- {model: this, role: 'receiver', index: 0},
2391
- ...others.map((model, index) => ({
2392
- model,
2393
- role:
2394
- operation === 'cut' ? ('tool' as const) : ('operand' as const),
2395
- index: index + 1,
2396
- })),
2397
- ],
2398
- {regions: evaluation.regions},
2399
- ),
2400
- });
2401
- transferred = true;
2402
- return combined as unknown as SolidModel;
2403
- } finally {
2404
- if (!transferred) {
2405
- disposeModelGeometryValue(evaluation.geometry.value);
2406
- evaluation.regions.forEach(region => region.artifact.value.delete());
2407
- }
2408
- }
2130
+ [modelGeometry](): ModelGeometry | undefined {
2131
+ return this.geometry;
2409
2132
  }
2410
2133
 
2411
- private evaluateBoolean(
2412
- this: ModelObject<Elements, 'solid'>,
2413
- operation: BooleanOperation,
2414
- others: readonly ModelObject<{}, 'solid'>[],
2415
- ): BooleanEvaluation {
2416
- const solveContext = ModelObject.createSolveContext([this, ...others]);
2417
- const targetPose = this.solvePose(solveContext);
2418
- let geometry = this.requireSolidGeometry();
2419
- let temporaryGeometry: SolidGeometry | undefined;
2420
- const regions: StoredOperationRegion[] = [];
2421
- let evaluated = false;
2422
- try {
2423
- for (const [otherIndex, other] of others.entries()) {
2424
- const otherGeometry = other.requireSolidGeometry();
2425
- const transform = relativeTransform(
2426
- other.solvePose(solveContext),
2427
- targetPose,
2428
- );
2429
- const operand = evaluateKernelShape(
2430
- 'transform',
2431
- [transform.position, transform.quaternion],
2432
- [otherGeometry],
2433
- () => shapeWithTransform(otherGeometry.value.shape, transform),
2434
- );
2435
- try {
2436
- if (operation === 'cut' || operation === 'fuse') {
2437
- regions.push({
2438
- kind: 'intersection',
2439
- input: other,
2440
- frame: this,
2441
- artifact: evaluateKernelShape(
2442
- 'boolean-intersection-region',
2443
- [],
2444
- [geometry, operand],
2445
- () => geometry.value.shape.intersect(operand.value),
2446
- ),
2447
- });
2448
- }
2449
- if (operation === 'fuse') {
2450
- regions.push({
2451
- kind: 'section',
2452
- input: other,
2453
- frame: this,
2454
- artifact: evaluateKernelShape(
2455
- 'boolean-section-region',
2456
- [],
2457
- [geometry, operand],
2458
- () => unionSectionShape(geometry.value.shape, operand.value),
2459
- ),
2460
- });
2461
- }
2462
- const nextGeometry = evaluateSolidGeometry(
2463
- `boolean-${operation}`,
2464
- [otherIndex],
2465
- [geometry, operand],
2466
- () => {
2467
- const result = booleanWithTopology(
2468
- {
2469
- shape: geometry.value.shape,
2470
- topology: geometry.value.topology,
2471
- index: otherIndex === 0 ? 1 : undefined,
2472
- },
2473
- {
2474
- shape: operand.value,
2475
- topology: otherGeometry.value.topology,
2476
- index: otherIndex + 2,
2477
- },
2478
- operation,
2479
- );
2480
- return {shape: result.shape, topology: result.topology};
2481
- },
2482
- );
2483
- temporaryGeometry?.value.shape.delete();
2484
- temporaryGeometry = nextGeometry;
2485
- geometry = nextGeometry;
2486
- } finally {
2487
- operand.value.delete();
2134
+ private constructor(init: ModelObjectInit<Kind>) {
2135
+ super(init);
2136
+ if (init.kind !== 'group' && !init.geometry) {
2137
+ throw new Error(
2138
+ 'A geometric model object must contain an OpenCascade shape.',
2139
+ );
2140
+ }
2141
+ this.kind = init.kind;
2142
+ this.elementKind = modelElementKinds[
2143
+ init.kind
2144
+ ] as unknown as ModelElementKind<Kind>;
2145
+ this.geometry = init.geometry;
2146
+ this.geometryAnchor = init.geometryAnchor ?? {
2147
+ kind: this.elementKind,
2148
+ transform: identityRigidTransform,
2149
+ };
2150
+ this.meshTolerance = init.meshTolerance ?? 0.2;
2151
+ this.name = init.name ?? defaultModelNames[init.kind];
2152
+ this.materialSnapshot = init.material;
2153
+ this.children = init.children ?? [];
2154
+ this.assembly =
2155
+ init.assembly ??
2156
+ (this.kind === 'group'
2157
+ ? ModelObject.createAssembly(this.children)
2158
+ : undefined);
2159
+ this.operation = init.operation;
2160
+ const elements =
2161
+ init.elements ??
2162
+ (this.kind === 'solid' && this.geometry
2163
+ ? solidElements(this.geometry.value.localBounds)
2164
+ : {});
2165
+ this.elements = this.geometry
2166
+ ? {
2167
+ center: {
2168
+ kind: 'point',
2169
+ transform: translation(
2170
+ boundsCenter(this.geometry.value.localBounds),
2171
+ ),
2172
+ },
2173
+ ...elements,
2488
2174
  }
2175
+ : elements;
2176
+ for (const [name, element] of Object.entries(this.elements)) {
2177
+ if (name in this) {
2178
+ throw new Error(
2179
+ `The element name ${name} conflicts with the model API.`,
2180
+ );
2489
2181
  }
2490
- evaluated = true;
2491
- return {geometry, regions};
2492
- } finally {
2493
- if (!evaluated) {
2494
- temporaryGeometry?.value.shape.delete();
2495
- regions.forEach(region => region.artifact.value.delete());
2496
- }
2182
+ Object.defineProperty(this, name, {
2183
+ value: modelAnchor(this, name, element),
2184
+ });
2497
2185
  }
2498
2186
  }
2499
2187
 
2500
- private allParameters(): ParameterUsage[] {
2501
- return uniqueParameters([
2502
- ...this.parameters,
2503
- ...this.constraints.flatMap(
2504
- constraint => valueTrace(constraint).parameters,
2505
- ),
2506
- ]);
2188
+ /** @internal */
2189
+ static create<
2190
+ Elements extends NamedElements = {},
2191
+ Kind extends ModelKind = ModelKind,
2192
+ >(init: ModelObjectInit<Kind>): ModelObject<Elements, Kind> {
2193
+ return new ModelObject<Elements, Kind>(init);
2507
2194
  }
2508
2195
 
2509
- private solvePose(context: SolveContext): RigidTransform {
2510
- return context.poses.get(this)!;
2196
+ get up(): Bound {
2197
+ return directionalBound(this.relationAnchorReference(), 'up');
2511
2198
  }
2512
-
2513
- private memberPoses(): Map<ModelObject, RigidTransform | null> {
2514
- const members = new Map<ModelObject, RigidTransform | null>([
2515
- [this, identityRigidTransform],
2516
- ]);
2517
- if (this.children.length) {
2518
- const context = this.assembly!;
2519
- for (const child of this.children) {
2520
- const pose = child.solvePose(context);
2521
- for (const [member, localPose] of child.memberPoses()) {
2522
- members.set(
2523
- member,
2524
- members.has(member) || localPose === null
2525
- ? null
2526
- : composeTransforms(pose, localPose),
2527
- );
2528
- }
2529
- }
2530
- }
2531
- return members;
2199
+ get down(): Bound {
2200
+ return directionalBound(this.relationAnchorReference(), 'down');
2532
2201
  }
2533
-
2534
- /** Bounds of the selected finite geometry after a rigid transform. */
2535
- private [referenceBoundsParts](
2536
- reference: StoredAnchor,
2537
- transform: RigidTransform,
2538
- ): ({bounds: LocalBounds} | SnapshotQueryInput)[] {
2539
- if (reference.bound) {
2540
- const frame = composeTransforms(transform, reference.transform);
2541
- const [x, z] = reference.bound.size;
2542
- return [
2543
- {
2544
- bounds: pointBounds(
2545
- [-1, 1].flatMap(a =>
2546
- [-1, 1].map(
2547
- b =>
2548
- composeTransforms(
2549
- frame,
2550
- translation([(a * x) / 2, 0, (b * z) / 2]),
2551
- ).position,
2552
- ),
2553
- ),
2554
- ),
2555
- },
2556
- ];
2557
- }
2558
- if (reference.whole) {
2559
- if (this.geometry) return [boundsQuery(this.geometry, transform)];
2560
- const context = this.assembly!;
2561
- return this.children.flatMap(child =>
2562
- child[referenceBoundsParts](
2563
- child.relationAnchorReference(),
2564
- composeTransforms(transform, child.solvePose(context)),
2565
- ),
2566
- );
2567
- }
2568
- if (reference.topology) {
2569
- const topology = reference.topology;
2570
- return [
2571
- boundsQuery(
2572
- topology.source.requireGeometry(),
2573
- composeTransforms(transform, topology.transform),
2574
- topology.selection,
2575
- topology.scale,
2576
- ),
2577
- ];
2578
- }
2579
- if (reference.kind === 'point') {
2580
- const point = composeTransforms(transform, reference.transform).position;
2581
- return [{bounds: [point, point]}];
2582
- }
2583
- throw new Error(
2584
- `The reference ${reference.name} has no finite geometry. Select a model, vertex, edge, or surface for on().`,
2585
- );
2202
+ get left(): Bound {
2203
+ return directionalBound(this.relationAnchorReference(), 'left');
2204
+ }
2205
+ get right(): Bound {
2206
+ return directionalBound(this.relationAnchorReference(), 'right');
2207
+ }
2208
+ get front(): Bound {
2209
+ return directionalBound(this.relationAnchorReference(), 'front');
2210
+ }
2211
+ get back(): Bound {
2212
+ return directionalBound(this.relationAnchorReference(), 'back');
2586
2213
  }
2587
2214
 
2588
- /** Bounds of the selected finite geometry after a rigid transform. */
2589
- [referenceBounds](
2590
- reference: StoredAnchor,
2591
- transform: RigidTransform,
2592
- ): LocalBounds {
2593
- const bounds = this[referenceBoundsParts](reference, transform).map(part =>
2594
- 'bounds' in part
2595
- ? part.bounds
2596
- : (evaluateSnapshotQuery(part) as LocalBounds),
2215
+ on(target: Bound): Constraint {
2216
+ return Constraint.create(
2217
+ this.relationAnchorReference(),
2218
+ boundReference(target),
2597
2219
  );
2598
- return bounds.length === 1 ? bounds[0] : combineBounds(bounds);
2599
2220
  }
2600
2221
 
2601
- /** Pure query collection: it never substitutes geometry or runs author code. */
2602
- [modelSnapshotQueries](): SnapshotQueryInput[] {
2603
- const result: SnapshotQueryInput[] = [];
2604
- if (this.geometry || this.children.length) {
2605
- for (const direction of Object.values(boundDirections)) {
2606
- const transform = invertTransform(
2607
- rotation(frameFromYAxis(origin, direction).quaternion),
2608
- );
2609
- for (const part of this[referenceBoundsParts](
2610
- this.relationAnchorReference(),
2611
- transform,
2612
- ))
2613
- if (!('bounds' in part)) result.push(part);
2614
- }
2615
- }
2616
- if (this.geometry)
2617
- result.push(
2618
- meshQuery(
2619
- this.geometry,
2620
- this.geometry.value.shape,
2621
- this.meshTolerance,
2622
- this.geometry.value.topology,
2623
- ),
2624
- );
2625
- for (const region of this.operation.regions)
2626
- result.push(
2627
- meshQuery(region.artifact, region.artifact.value, this.meshTolerance),
2628
- );
2629
- return result;
2222
+ /** @internal */
2223
+ relationAnchorReference(): AnchorReference {
2224
+ return {model: this, name: 'geometry', ...this.geometryAnchor, whole: true};
2630
2225
  }
2631
2226
 
2632
- private alignmentGeometry(reference: StoredAnchor): AlignmentGeometry {
2633
- if (reference.kind === 'point')
2634
- return {
2635
- kind: 'point',
2636
- point: reference.whole
2637
- ? (this.requireGeometry().value.shape as ReplicadVertex).asTuple()
2638
- : reference.transform.position,
2639
- };
2640
- const read = (shape: AnyShape) =>
2641
- reference.kind === 'line'
2642
- ? edgeGeometry(shape as ReplicadEdge, reference.direction ?? 1)
2643
- : faceGeometry(
2644
- shape as ReplicadFace,
2645
- reference.facing ?? reference.bound?.facing ?? 1,
2646
- );
2647
- if (reference.topology) {
2648
- const topology = reference.topology,
2649
- geometry = topology.source.requireGeometry().value;
2650
- return withTopologyShape(
2651
- geometry.shape,
2652
- geometry.topology,
2653
- topology.selection,
2654
- shape => {
2655
- const scaled =
2656
- topology.scale === 1
2657
- ? shape
2658
- : shapeWithScale(shape, topology.scale);
2659
- try {
2660
- return transformGeometry(read(scaled), topology.transform);
2661
- } finally {
2662
- if (scaled !== shape) scaled.delete();
2663
- }
2664
- },
2665
- );
2666
- }
2667
- if (reference.whole) return read(this.requireGeometry().value.shape);
2668
- const direction = rotateVector(
2669
- [
2670
- 0,
2671
- reference.kind === 'line'
2672
- ? (reference.direction ?? 1)
2673
- : (reference.facing ?? reference.bound?.facing ?? 1),
2674
- 0,
2675
- ],
2676
- reference.transform.quaternion,
2227
+ align(target: Anchor<'point' | 'line' | 'face'>): Constraint {
2228
+ return Constraint.create(
2229
+ this.relationAnchorReference(),
2230
+ anchorReference(target),
2231
+ 'align',
2677
2232
  );
2678
- return reference.kind === 'line'
2679
- ? {kind: 'line', point: reference.transform.position, direction}
2680
- : {kind: 'plane', point: reference.transform.position, normal: direction};
2681
2233
  }
2682
2234
 
2683
- private relationElement(reference: StoredAnchor): ElementSnapshot {
2684
- const element = reference.whole
2685
- ? {
2686
- ...this.exposedElement(),
2687
- name: reference.name,
2688
- direction: reference.direction,
2689
- facing: reference.facing,
2690
- }
2691
- : reference;
2692
- const topology = element.topology;
2693
- let arrow: RigidTransform | undefined;
2694
- if (reference.kind === 'line' && topology) {
2695
- const geometry = topology.source.requireGeometry().value;
2696
- arrow = withTopologyShape(
2697
- geometry.shape,
2698
- geometry.topology,
2699
- topology.selection,
2700
- shape => {
2701
- const edge = shape as ReplicadEdge;
2702
- const position = (reference.direction ?? 1) === 1 ? 1 : 0;
2703
- const point = edge.pointAt(position),
2704
- tangent = edge.tangentAt(position);
2705
- try {
2706
- return topologyTransform(
2707
- topology,
2708
- frameFromYAxis(
2709
- point.toTuple(),
2710
- tangent
2711
- .toTuple()
2712
- .map(v => v * (reference.direction ?? 1)) as unknown as Vec3,
2713
- ),
2714
- );
2715
- } finally {
2716
- point.delete();
2717
- tangent.delete();
2718
- }
2719
- },
2720
- );
2721
- }
2722
- return {
2723
- ...snapshotElements({[reference.name]: element})[0],
2724
- arrow,
2725
- };
2235
+ reverse(): Edge {
2236
+ return this.edges()[0].reverse();
2726
2237
  }
2727
-
2728
- private bodyRotations(
2729
- constraint: StoredConstraint,
2730
- indices: Map<ModelObject, number>,
2731
- ): BodyRotation[] {
2732
- return constraint.rotations.map(({pivot, angles}) => {
2733
- if (pivot.kind === 'around') {
2734
- return pivot.axis.model
2735
- ? {
2736
- body: indices.get(pivot.axis.model)!,
2737
- axis: pivot.axis.transform,
2738
- angle: (angles as number) * (pivot.axis.direction ?? 1),
2739
- }
2740
- : {
2741
- local: axisRotation(
2742
- pivot.axis.transform,
2743
- (angles as number) * (pivot.axis.direction ?? 1),
2744
- ),
2745
- };
2746
- }
2747
- const frame = translation(
2748
- pivot.kind === 'pivot'
2749
- ? pivot.point
2750
- : anchorReference(this.vertex(pivot.id)).transform.position,
2751
- );
2752
- return {
2753
- local: composeTransforms(
2754
- composeTransforms(frame, rotationAround(origin, angles as Vec3)),
2755
- invertTransform(frame),
2756
- ),
2757
- };
2758
- });
2238
+ flip(): Surface {
2239
+ return this.surfaces()[0].flip();
2759
2240
  }
2760
2241
 
2761
- /** @internal */
2762
- [relationPreview](
2763
- nodeId: string,
2764
- constraint: StoredConstraint,
2765
- selection: ConstraintSpatialSelection | undefined,
2766
- ): ConstraintPreview {
2767
- // This expression starts from the relate receiver, before sibling constraints
2768
- // are returned and committed to self. Geometry and node identity stay shared.
2769
- const owner = this.copy(
2770
- {nodeId, constraints: [...this.constraints, constraint]},
2771
- this.operation,
2772
- );
2773
- const context = ModelObject.createSolveContext([
2774
- owner,
2775
- ...(selection?.pivot.kind === 'around' && selection.pivot.axis.model
2776
- ? [selection.pivot.axis.model]
2777
- : []),
2242
+ relate(
2243
+ build: (
2244
+ self: RuntimeModel<Elements, Kind>,
2245
+ ) => Constraint | readonly Constraint[],
2246
+ ): RuntimeModel<Elements, Kind> {
2247
+ const operation = storedOperation('relate', [
2248
+ {model: this, role: 'source', index: 0},
2778
2249
  ]);
2779
- return {
2780
- object: {
2781
- nodeId,
2782
- compositionTransform: toTransform(owner.solvePose(context)),
2783
- constraints: owner.constraints.map(value =>
2784
- owner.constraintSnapshot(value, context),
2785
- ),
2786
- },
2787
- spatial: selection
2788
- ? owner.constraintSpatial(constraint, selection, context)
2789
- : undefined,
2790
- };
2250
+ const related = this.copy({}, operation);
2251
+ const references = related.addRelations(this, () => build(related));
2252
+ operation.inputs.push(
2253
+ ...references.map((model, index) => ({
2254
+ model,
2255
+ role: 'reference' as const,
2256
+ index,
2257
+ })),
2258
+ );
2259
+ return related;
2791
2260
  }
2792
2261
 
2793
- private constraintSpatial(
2794
- constraint: StoredConstraint,
2795
- selection: ConstraintSpatialSelection,
2796
- context: SolveContext,
2797
- ): ConstraintSpatialReference {
2798
- const models = [...context.poses.keys()];
2799
- const actions = this.bodyRotations(
2800
- constraint,
2801
- new Map(models.map((model, i) => [model, i])),
2262
+ expose<const Sources extends ElementSources>(
2263
+ sources: Sources,
2264
+ ): RuntimeModel<MergedElements<Elements, ExposedElements<Sources>>, Kind> {
2265
+ const entries = Object.entries(sources);
2266
+ const elements = this.localElements(entries.map(([, source]) => source));
2267
+ const exposed = Object.fromEntries(
2268
+ entries.map(([name], index) => [name, elements[index]]),
2802
2269
  );
2803
- const stage = actions.length - (selection.kind === 'rotate' ? 1 : 0);
2804
- const finalPose = this.solvePose(context);
2805
- let before = finalPose;
2806
- for (let i = actions.length - 1; i >= stage; i--) {
2807
- const action = actions[i];
2808
- before =
2809
- 'local' in action
2810
- ? composeTransforms(before, invertTransform(action.local))
2811
- : composeTransforms(
2812
- axisRotation(
2813
- composeTransforms(
2814
- models[action.body].solvePose(context),
2815
- action.axis,
2816
- ),
2817
- -action.angle,
2818
- ),
2819
- before,
2820
- );
2821
- }
2822
- const pivot = selection.pivot;
2823
- let frame: RigidTransform;
2824
- if (pivot.kind === 'around') {
2825
- frame = composeTransforms(
2826
- pivot.axis.model ? pivot.axis.model.solvePose(context) : before,
2827
- (pivot.axis.direction ?? 1) === 1
2828
- ? pivot.axis.transform
2829
- : composeTransforms(pivot.axis.transform, rotation([1, 0, 0, 0])),
2830
- );
2831
- } else {
2832
- const local = translation(
2833
- pivot.kind === 'pivot'
2834
- ? pivot.point
2835
- : anchorReference(this.vertex(pivot.id)).transform.position,
2270
+ const references = entries.map(
2271
+ ([, source]) => anchorReference(source).model,
2272
+ );
2273
+ const operation = storedOperation('expose', [
2274
+ {model: this, role: 'source', index: 0},
2275
+ ...uniqueModels(references)
2276
+ .filter(model => model !== this)
2277
+ .map((model, index) => ({
2278
+ model,
2279
+ role: 'reference' as const,
2280
+ index,
2281
+ })),
2282
+ ]);
2283
+ return this.copy(
2284
+ {elements: {...this.elements, ...exposed}},
2285
+ operation,
2286
+ ) as unknown as RuntimeModel<
2287
+ MergedElements<Elements, ExposedElements<Sources>>,
2288
+ Kind
2289
+ >;
2290
+ }
2291
+
2292
+ /** Resolve both own and occurrence references into this model's local frame. */
2293
+ private localElements(sources: readonly Anchor[]): StoredElement[] {
2294
+ const members = this.memberPoses();
2295
+ const external = sources
2296
+ .map(source => anchorReference(source).model)
2297
+ .filter(model => !members.has(model));
2298
+ const context = external.length
2299
+ ? ModelObject.createSolveContext([this, ...external])
2300
+ : undefined;
2301
+ return sources.map(source => {
2302
+ const reference = anchorReference(source);
2303
+ const {
2304
+ kind,
2305
+ transform: frame,
2306
+ topology,
2307
+ members: nested,
2308
+ bound,
2309
+ facing,
2310
+ } = source instanceof ModelObject ? source.exposedElement() : reference;
2311
+ const memberPose = members.get(reference.model);
2312
+ if (memberPose === null)
2313
+ throw new Error(
2314
+ "The point or element belongs to multiple occurrences. Expose it through the intended child model's named reference.",
2315
+ );
2316
+ const transform =
2317
+ memberPose ??
2318
+ relativeTransform(
2319
+ reference.model.solvePose(context!),
2320
+ this.solvePose(context!),
2321
+ );
2322
+ return transformElement(
2323
+ {kind, transform: frame, topology, members: nested, bound, facing},
2324
+ transform,
2836
2325
  );
2837
- frame = composeTransforms(before, local);
2838
- }
2839
- const angles = constraint.rotations[stage]?.angles ?? origin;
2840
- const rotationVector: Vec3 =
2841
- typeof angles === 'number' ? [0, angles, 0] : angles;
2842
- const localFrame = relativeTransform(frame, finalPose);
2843
- return {
2844
- nodeId: this.nodeId,
2845
- kind: selection.kind,
2846
- spatial: {
2847
- origin: localFrame.position,
2848
- vector:
2849
- selection.kind === 'rotate'
2850
- ? rotationVector
2851
- : pivot.kind === 'pivot'
2852
- ? pivot.point
2853
- : origin,
2854
- frame: localFrame,
2855
- rotation: rotationVector,
2856
- axisOnly: pivot.kind === 'around',
2857
- },
2858
- };
2326
+ });
2859
2327
  }
2860
2328
 
2861
- /** Fix the assembly frame once, using only its direct member origins. */
2862
- private static createAssembly(
2863
- children: readonly ModelObject[],
2864
- ): SolveContext {
2865
- const context = ModelObject.createSolveContext(children);
2866
- const center = children.length
2867
- ? boundsCenter(
2868
- pointBounds(children.map(child => child.solvePose(context).position)),
2869
- )
2870
- : origin;
2871
- return transformSolveContext(context, translation(negateVector(center)));
2329
+ vertex(id: VertexId): Vertex {
2330
+ return this.vertices([id])[0];
2331
+ }
2332
+ vertices(ids?: readonly VertexId[]): readonly Vertex[] {
2333
+ return this.selectTopology('vertex', ids) as unknown as readonly Vertex[];
2334
+ }
2335
+ edge(id: EdgeId): Edge {
2336
+ return this.edges([id])[0];
2337
+ }
2338
+ edges(ids?: readonly EdgeId[]): readonly Edge[] {
2339
+ return this.selectTopology('edge', ids) as unknown as readonly Edge[];
2340
+ }
2341
+ surface(id: SurfaceId): Surface {
2342
+ return this.surfaces([id])[0];
2343
+ }
2344
+ surfaces(ids?: readonly SurfaceId[]): readonly Surface[] {
2345
+ return this.selectTopology('surface', ids) as unknown as readonly Surface[];
2872
2346
  }
2873
2347
 
2874
- private static createSolveContext(
2875
- roots: readonly ModelObject[],
2876
- ): SolveContext {
2877
- const models: ModelObject[] = [];
2878
- const indices = new Map<ModelObject, number>();
2879
- const collect = (model: ModelObject): void => {
2880
- if (indices.has(model)) return;
2881
- indices.set(model, models.length);
2882
- models.push(model);
2883
- model.constraints.flatMap(constraintReferences).forEach(collect);
2884
- };
2885
- roots.forEach(collect);
2886
- const poses = solveBodies(
2887
- models.map((model): Body => ({
2888
- name: model.name,
2889
- relations: model.constraints.map(constraint =>
2890
- constraint.kind === 'align'
2891
- ? {
2892
- kind: 'align',
2893
- id: constraint.id,
2894
- source: {
2895
- body: indices.get(constraint.source.model ?? model)!,
2896
- geometry: (
2897
- constraint.source.model ?? model
2898
- ).alignmentGeometry(constraint.source),
2899
- transform: constraint.source.transform,
2900
- },
2901
- target: {
2902
- body: indices.get(constraint.target.model ?? model)!,
2903
- geometry: (
2904
- constraint.target.model ?? model
2905
- ).alignmentGeometry(constraint.target),
2906
- transform: constraint.target.transform,
2907
- },
2908
- offset: constraint.offset,
2909
- rotations: model.bodyRotations(constraint, indices),
2910
- }
2911
- : {
2912
- kind: 'on',
2913
- id: constraint.id,
2914
- source: {
2915
- body: indices.get(constraint.source.model ?? model)!,
2916
- key: constraint.source.name,
2917
- bounds: (orientation: Quaternion) =>
2918
- (constraint.source.model ?? model)[referenceBounds](
2919
- constraint.source,
2920
- rotation(orientation),
2921
- ),
2922
- },
2923
- target: {
2924
- body: indices.get(constraint.target.model ?? model)!,
2925
- transform: constraint.target.transform,
2926
- facing: constraint.target.bound!.facing,
2927
- },
2928
- offset: constraint.offset,
2929
- rotations: model.bodyRotations(constraint, indices),
2930
- },
2931
- ),
2932
- })),
2348
+ private selectTopology(
2349
+ kind: TopologyKind,
2350
+ ids?: readonly TopologyId[],
2351
+ ): readonly ModelTopologyElement[] {
2352
+ const geometry = this.requireGeometry().value;
2353
+ const selected = topologyChildren(
2354
+ geometry.shape,
2355
+ geometry.topology,
2356
+ {kind: 'solid'},
2357
+ kind,
2358
+ ids,
2359
+ );
2360
+ return topologyReferences(
2361
+ this,
2362
+ '',
2363
+ {source: this, transform: identityRigidTransform, scale: 1},
2364
+ kind,
2365
+ selected,
2933
2366
  );
2934
- return {
2935
- poses: new Map(models.map((model, index) => [model, poses[index]])),
2936
- };
2937
2367
  }
2938
2368
 
2939
- private constraintSnapshot(
2940
- constraint: StoredConstraint,
2941
- context: SolveContext,
2942
- ): ConstraintSnapshot {
2943
- const source = constraint.source.model ?? this;
2944
- const target = constraint.target.model ?? this;
2945
- const models = [...context.poses.keys()];
2946
- const actions = this.bodyRotations(
2947
- constraint,
2948
- new Map(models.map((model, i) => [model, i])),
2949
- );
2950
- let before = this.solvePose(context);
2951
- for (const action of [...actions].reverse()) {
2952
- before =
2953
- 'local' in action
2954
- ? composeTransforms(before, invertTransform(action.local))
2955
- : composeTransforms(
2956
- axisRotation(
2957
- composeTransforms(
2958
- models[action.body].solvePose(context),
2959
- action.axis,
2960
- ),
2961
- -action.angle,
2962
- ),
2963
- before,
2964
- );
2369
+ /** @internal */
2370
+ exposedElement(): StoredElement {
2371
+ if (this.kind === 'group')
2372
+ return {...this.geometryAnchor, members: this.elements};
2373
+ const geometry = this.requireGeometry().value;
2374
+ const kind = (
2375
+ this.kind === 'face' ? 'surface' : this.kind
2376
+ ) as TopologySelection['kind'];
2377
+ const selection: TopologySelection =
2378
+ kind === 'solid'
2379
+ ? {kind}
2380
+ : {
2381
+ kind,
2382
+ id: geometry.topology[
2383
+ kind === 'surface'
2384
+ ? 'surfaces'
2385
+ : kind === 'edge'
2386
+ ? 'edges'
2387
+ : 'vertices'
2388
+ ].ids[0],
2389
+ };
2390
+ const context = {
2391
+ source: this,
2392
+ transform: identityRigidTransform,
2393
+ scale: 1,
2394
+ };
2395
+ const anchor =
2396
+ selection.kind === 'solid'
2397
+ ? this.geometryAnchor
2398
+ : topologyReferences(this, '', context, selection.kind, [
2399
+ selection.id,
2400
+ ])[0][anchorReferenceValue];
2401
+ return {
2402
+ kind: anchor.kind,
2403
+ transform: anchor.transform,
2404
+ members: this.elements,
2405
+ topology: {
2406
+ ...context,
2407
+ selection,
2408
+ },
2409
+ };
2410
+ }
2411
+
2412
+ material(material: Material | string): RuntimeModel<Elements, Kind> {
2413
+ return this.copy(
2414
+ {material: captureModelMaterial(material)},
2415
+ storedOperation('material', [{model: this, role: 'source', index: 0}]),
2416
+ );
2417
+ }
2418
+
2419
+ originOffset(
2420
+ dx: number,
2421
+ dy: number,
2422
+ dz: number,
2423
+ ): RuntimeModel<Elements, Kind>;
2424
+ originOffset(dx = 0, dy = 0, dz = 0): RuntimeModel<Elements, Kind> {
2425
+ const offset: Vec3 = [dx, dy, dz];
2426
+ assertFiniteVector('originOffset', offset);
2427
+ return this.withOrigin(offset, {kind: 'originOffset'});
2428
+ }
2429
+
2430
+ originPoint(point: PointAnchor): RuntimeModel<Elements, Kind> {
2431
+ const reference = anchorReference(point);
2432
+ if (reference.kind !== 'point')
2433
+ throw new Error('originPoint() requires a point reference.');
2434
+ const [element] = this.localElements([point]);
2435
+ return this.withOrigin(element.transform.position, {
2436
+ kind: 'originPoint',
2437
+ model: reference.model,
2438
+ });
2439
+ }
2440
+
2441
+ originVertex(id: VertexId): RuntimeModel<Elements, Kind> {
2442
+ const geometry = this.requireGeometry().value;
2443
+ const [{position}] = topologyVertexPoints(
2444
+ geometry.shape,
2445
+ geometry.topology.vertices,
2446
+ [id],
2447
+ );
2448
+ return this.withOrigin(position, {kind: 'originVertex', id});
2449
+ }
2450
+
2451
+ originCenter(): RuntimeModel<Elements, Kind> {
2452
+ this.requireGeometry();
2453
+ const position = this.elements.center.transform.position;
2454
+ return this.withOrigin(position, {kind: 'originCenter'});
2455
+ }
2456
+
2457
+ private withOrigin(
2458
+ offset: Vec3,
2459
+ selection:
2460
+ | {kind: 'originOffset' | 'originCenter'}
2461
+ | {kind: 'originVertex'; id: VertexId}
2462
+ | {kind: 'originPoint'; model: RelationObject},
2463
+ ): RuntimeModel<Elements, Kind> {
2464
+ const transform = translation(negateVector(offset));
2465
+ const operation = storedOperation(selection.kind, [
2466
+ {model: this, role: 'source', index: 0},
2467
+ ]);
2468
+ if (selection.kind === 'originPoint' && selection.model !== this)
2469
+ operation.inputs.push({
2470
+ model: selection.model,
2471
+ role: 'reference',
2472
+ index: 0,
2473
+ });
2474
+ operation.spatial = {origin, vector: offset};
2475
+ if (selection.kind === 'originVertex') {
2476
+ operation.selections.push({
2477
+ kind: 'vertex',
2478
+ input: this,
2479
+ ids: [selection.id],
2480
+ transform,
2481
+ });
2482
+ }
2483
+ return this.transformed(transform, operation);
2484
+ }
2485
+
2486
+ rotate(x: number, y: number, z: number): RuntimeModel<Elements, Kind>;
2487
+ rotate(x = 0, y = 0, z = 0): RuntimeModel<Elements, Kind> {
2488
+ const angles: Vec3 = [x, y, z];
2489
+ assertFiniteVector('rotate', angles);
2490
+ const operation = storedOperation('rotate', [
2491
+ {model: this, role: 'source', index: 0},
2492
+ ]);
2493
+ operation.spatial = {origin, vector: angles};
2494
+ return this.transformed(rotationAround(origin, angles), operation);
2495
+ }
2496
+
2497
+ private transformed(
2498
+ transform: RigidTransform,
2499
+ operation: StoredOperation,
2500
+ ): RuntimeModel<Elements, Kind> {
2501
+ const overrides: Partial<ModelObjectInit<Kind>> = {
2502
+ geometryAnchor: transformElement(this.geometryAnchor, transform),
2503
+ elements: Object.fromEntries(
2504
+ Object.entries(this.elements).map(([name, element]) => [
2505
+ name,
2506
+ transformElement(element, transform),
2507
+ ]),
2508
+ ),
2509
+ constraints: this.mapConstraintGeometry(
2510
+ element => transformElement(element, transform),
2511
+ point => composeTransforms(transform, translation(point)).position,
2512
+ ),
2513
+ };
2514
+ if (this.assembly)
2515
+ return this.copy(
2516
+ {
2517
+ ...overrides,
2518
+ assembly: transformSolveContext(this.assembly, transform),
2519
+ },
2520
+ operation,
2521
+ );
2522
+ const source = this.requireGeometry();
2523
+ const geometry = evaluateModelGeometry(
2524
+ 'transform',
2525
+ [transform.position, transform.quaternion],
2526
+ [source],
2527
+ () => {
2528
+ const shape = shapeWithTransform(source.value.shape, transform);
2529
+ try {
2530
+ return {
2531
+ shape,
2532
+ topology: preserveShapeTopology(shape, source.value.topology),
2533
+ referenceBasis: transformedReferenceBasis(source, transform, 1),
2534
+ };
2535
+ } catch (error) {
2536
+ shape.delete();
2537
+ throw error;
2538
+ }
2539
+ },
2540
+ );
2541
+ return this.copyWithGeometry(geometry, overrides, operation);
2542
+ }
2543
+
2544
+ private mapConstraintGeometry(
2545
+ mapElement: (element: StoredElement) => StoredElement,
2546
+ mapPoint: (point: Vec3) => Vec3,
2547
+ ): StoredConstraint[] {
2548
+ const mapReference = (reference: RelationReference): RelationReference =>
2549
+ reference.model ? reference : {...reference, ...mapElement(reference)};
2550
+ return this.constraints.map(constraint => {
2551
+ const mapped: StoredConstraint = {
2552
+ ...constraint,
2553
+ source: mapReference(constraint.source),
2554
+ target: mapReference(constraint.target),
2555
+ rotations: constraint.rotations.map(action => ({
2556
+ ...action,
2557
+ pivot:
2558
+ action.pivot.kind === 'around'
2559
+ ? {...action.pivot, axis: mapReference(action.pivot.axis)}
2560
+ : action.pivot.kind === 'pivot'
2561
+ ? {...action.pivot, point: mapPoint(action.pivot.point)}
2562
+ : action.pivot,
2563
+ })),
2564
+ };
2565
+ valueTraces.set(mapped, valueTrace(constraint));
2566
+ return mapped;
2567
+ });
2568
+ }
2569
+
2570
+ scaled(factor: number): RuntimeModel<Elements, Kind>;
2571
+ scaled(factor = 1): RuntimeModel<Elements, Kind> {
2572
+ assertPositive('scale', factor);
2573
+ const source = this.requireGeometry();
2574
+ const geometry = evaluateModelGeometry('scaled', [factor], [source], () => {
2575
+ const shape = shapeWithScale(source.value.shape, factor);
2576
+ try {
2577
+ return {
2578
+ shape,
2579
+ topology: preserveShapeTopology(shape, source.value.topology),
2580
+ referenceBasis: transformedReferenceBasis(
2581
+ source,
2582
+ identityRigidTransform,
2583
+ factor,
2584
+ ),
2585
+ };
2586
+ } catch (error) {
2587
+ shape.delete();
2588
+ throw error;
2589
+ }
2590
+ });
2591
+ return this.copyWithGeometry(
2592
+ geometry,
2593
+ {
2594
+ geometryAnchor: scaleElement(this.geometryAnchor, factor),
2595
+ elements: scaleElements(this.elements, factor),
2596
+ constraints: this.mapConstraintGeometry(
2597
+ element => scaleElement(element, factor),
2598
+ point => scaleFrame(translation(point), factor).position,
2599
+ ),
2600
+ },
2601
+ storedOperation('scaled', [{model: this, role: 'source', index: 0}]),
2602
+ );
2603
+ }
2604
+
2605
+ extrude(this: ModelObject<Elements, 'face'>, distance: number): SolidModel;
2606
+ extrude(this: ModelObject<Elements, 'face'>, distance = 10): SolidModel {
2607
+ if (this.kind !== 'face')
2608
+ throw new Error('extrude requires a single face model.');
2609
+ if (!Number.isFinite(distance) || distance === 0)
2610
+ throw new Error('Extrusion distance must be finite and non-zero.');
2611
+ const source = this.requireGeometry();
2612
+ const direction = rotateVector(
2613
+ [0, distance, 0],
2614
+ this.geometryAnchor.transform.quaternion,
2615
+ );
2616
+ const geometry = evaluateSolidGeometry(
2617
+ 'extrude',
2618
+ [direction],
2619
+ [source],
2620
+ () =>
2621
+ extrudeWithTopology(
2622
+ {
2623
+ shape: source.value.shape,
2624
+ topology: source.value.topology,
2625
+ index: 1,
2626
+ },
2627
+ direction,
2628
+ ),
2629
+ );
2630
+ return ModelObject.create<CanonicalElements, 'solid'>({
2631
+ kind: 'solid',
2632
+ name: 'Extrude',
2633
+ geometry,
2634
+ material: this.materialSnapshot,
2635
+ constraints: this.constraints,
2636
+ sourceRefs: this.sourceRefs,
2637
+ parameters: this.allParameters(),
2638
+ meshTolerance: this.meshTolerance,
2639
+ operation: storedOperation(
2640
+ 'extrude',
2641
+ [{model: this, role: 'receiver', index: 0}],
2642
+ {
2643
+ dimensions: {
2644
+ distance: {
2645
+ origin: this.elements.center.transform.position,
2646
+ vector: direction,
2647
+ },
2648
+ },
2649
+ },
2650
+ ),
2651
+ }) as unknown as SolidModel;
2652
+ }
2653
+
2654
+ cut(
2655
+ this: ModelObject<Elements, 'solid'>,
2656
+ tools: readonly SolidModel<{}>[],
2657
+ ): SolidModel {
2658
+ return cut(this as unknown as SolidModel<{}>, tools);
2659
+ }
2660
+
2661
+ fillet(
2662
+ this: ModelObject<Elements, 'solid'>,
2663
+ radius: number,
2664
+ edgeIds?: readonly EdgeId[],
2665
+ ): SolidModel<Elements>;
2666
+ fillet(
2667
+ this: ModelObject<Elements, 'solid'>,
2668
+ radius = 1,
2669
+ edgeIds?: readonly EdgeId[],
2670
+ ): SolidModel<Elements> {
2671
+ assertPositive('radius', radius);
2672
+ const source = this.requireSolidGeometry();
2673
+ const selectedEdgeIds = resolveEdgeSelection(
2674
+ source.value.topology.edges,
2675
+ edgeIds,
2676
+ );
2677
+ const geometry = evaluateSolidGeometry(
2678
+ 'fillet',
2679
+ [radius, selectedEdgeIds],
2680
+ [source],
2681
+ () => {
2682
+ const result = filletEdges(
2683
+ source.value.shape,
2684
+ source.value.topology,
2685
+ radius,
2686
+ selectedEdgeIds,
2687
+ );
2688
+ return {shape: result.shape, topology: result.topology};
2689
+ },
2690
+ );
2691
+ return this.copyWithGeometry(
2692
+ geometry,
2693
+ {},
2694
+ storedOperation('fillet', [{model: this, role: 'source', index: 0}], {
2695
+ selections: [
2696
+ {
2697
+ kind: 'edge',
2698
+ input: this,
2699
+ ids: selectedEdgeIds,
2700
+ transform: identityRigidTransform,
2701
+ },
2702
+ ],
2703
+ }),
2704
+ ) as unknown as SolidModel<Elements>;
2705
+ }
2706
+
2707
+ chamfer(
2708
+ this: ModelObject<Elements, 'solid'>,
2709
+ distance: number,
2710
+ edgeIds?: readonly EdgeId[],
2711
+ ): SolidModel<Elements>;
2712
+ chamfer(
2713
+ this: ModelObject<Elements, 'solid'>,
2714
+ distance = 1,
2715
+ edgeIds?: readonly EdgeId[],
2716
+ ): SolidModel<Elements> {
2717
+ assertPositive('distance', distance);
2718
+ const source = this.requireSolidGeometry();
2719
+ const selectedEdgeIds = resolveEdgeSelection(
2720
+ source.value.topology.edges,
2721
+ edgeIds,
2722
+ );
2723
+ const geometry = evaluateSolidGeometry(
2724
+ 'chamfer',
2725
+ [distance, selectedEdgeIds],
2726
+ [source],
2727
+ () => {
2728
+ const result = chamferEdges(
2729
+ source.value.shape,
2730
+ source.value.topology,
2731
+ distance,
2732
+ selectedEdgeIds,
2733
+ );
2734
+ return {shape: result.shape, topology: result.topology};
2735
+ },
2736
+ );
2737
+ return this.copyWithGeometry(
2738
+ geometry,
2739
+ {},
2740
+ storedOperation('chamfer', [{model: this, role: 'source', index: 0}], {
2741
+ selections: [
2742
+ {
2743
+ kind: 'edge',
2744
+ input: this,
2745
+ ids: selectedEdgeIds,
2746
+ transform: identityRigidTransform,
2747
+ },
2748
+ ],
2749
+ }),
2750
+ ) as unknown as SolidModel<Elements>;
2751
+ }
2752
+
2753
+ shell(
2754
+ this: ModelObject<Elements, 'solid'>,
2755
+ thickness: number,
2756
+ removedSurfaceIds?: readonly SurfaceId[],
2757
+ ): SolidModel<Elements>;
2758
+ shell(
2759
+ this: ModelObject<Elements, 'solid'>,
2760
+ thickness = 1,
2761
+ removedSurfaceIds: readonly SurfaceId[] = [],
2762
+ ): SolidModel<Elements> {
2763
+ if (!Number.isFinite(thickness) || thickness === 0) {
2764
+ throw new Error('thickness must be a nonzero finite number.');
2965
2765
  }
2966
- const sourcePose = source === this ? before : source.solvePose(context);
2967
- const targetPose = target === this ? before : target.solvePose(context);
2968
- let offsetFrame = composeTransforms(
2969
- targetPose,
2970
- constraint.target.transform,
2766
+ const source = this.requireSolidGeometry();
2767
+ const selectedIds = resolveTopologySelection(
2768
+ 'surface',
2769
+ source.value.topology.surfaces,
2770
+ removedSurfaceIds,
2771
+ );
2772
+ const geometry = evaluateSolidGeometry(
2773
+ 'shell',
2774
+ [thickness, selectedIds],
2775
+ [source],
2776
+ () =>
2777
+ shellWithTopology(
2778
+ source.value.shape,
2779
+ source.value.topology,
2780
+ thickness,
2781
+ selectedIds,
2782
+ ),
2783
+ );
2784
+ return this.copyWithGeometry(
2785
+ geometry,
2786
+ {},
2787
+ storedOperation('shell', [{model: this, role: 'source', index: 0}], {
2788
+ selections: [
2789
+ {
2790
+ kind: 'surface',
2791
+ input: this,
2792
+ ids: selectedIds,
2793
+ transform: identityRigidTransform,
2794
+ },
2795
+ ],
2796
+ }),
2797
+ ) as unknown as SolidModel<Elements>;
2798
+ }
2799
+
2800
+ /** @internal */
2801
+ attachOperationTrace(
2802
+ siteId: string,
2803
+ execution: number,
2804
+ order: number,
2805
+ sourceRef: SourceRef,
2806
+ outputIndex = 0,
2807
+ ): void {
2808
+ if (operationTraces.has(this.operation)) {
2809
+ return;
2810
+ }
2811
+ operationTraces.set(this.operation, {
2812
+ siteId,
2813
+ execution,
2814
+ order,
2815
+ sourceRef,
2816
+ outputIndex,
2817
+ });
2818
+ }
2819
+
2820
+ /** @internal */
2821
+ relatedObjects(): readonly RelationObject[] {
2822
+ return [
2823
+ ...this.children,
2824
+ ...this.operation.inputs.map(input => input.model),
2825
+ ...this.constraints.flatMap(constraintReferences),
2826
+ ];
2827
+ }
2828
+
2829
+ /** @internal */
2830
+ toSnapshot(
2831
+ meshCache: Map<AnyShape, RenderMesh> = new Map(),
2832
+ ): ModelSnapshotObject {
2833
+ const solveContext = ModelObject.createSolveContext([this]);
2834
+ return this.snapshotNode(meshCache, solveContext);
2835
+ }
2836
+
2837
+ private snapshotNode(
2838
+ meshCache: Map<AnyShape, RenderMesh>,
2839
+ solveContext: SolveContext,
2840
+ inComposition = false,
2841
+ overrideMaterial?: ModelMaterialSnapshot,
2842
+ ): ModelSnapshotObject {
2843
+ const material = overrideMaterial ?? this.materialSnapshot;
2844
+ const pose = this.solvePose(solveContext);
2845
+ const constraints = this.constraints.map(constraint =>
2846
+ this.constraintSnapshot(constraint, solveContext),
2847
+ );
2848
+ const parameters = uniqueParameters([
2849
+ ...this.parameters,
2850
+ ...this.constraints.flatMap(
2851
+ constraint => valueTrace(constraint).parameters,
2852
+ ),
2853
+ ]);
2854
+ const common = {
2855
+ nodeId: this.nodeId,
2856
+ kind: this.kind,
2857
+ name: this.name,
2858
+ material,
2859
+ compositionTransform: toTransform(pose),
2860
+ transform: toTransform(inComposition ? pose : identityRigidTransform),
2861
+ constraints,
2862
+ origin,
2863
+ elements: snapshotElements({
2864
+ ...this.elements,
2865
+ ...(this.geometry || this.children.length
2866
+ ? Object.fromEntries(
2867
+ Object.keys(boundDirections).map(direction => [
2868
+ direction,
2869
+ anchorReference(this[direction as keyof DirectionalBounds]),
2870
+ ]),
2871
+ )
2872
+ : {}),
2873
+ }),
2874
+ sourceRefs: [...this.sourceRefs],
2875
+ parameters,
2876
+ operation: this.operationSnapshot(meshCache),
2877
+ } as const;
2878
+
2879
+ if (this.kind === 'group') {
2880
+ const childContext = this.assembly!;
2881
+ return {
2882
+ ...common,
2883
+ children: this.children.map(child =>
2884
+ child.snapshotNode(meshCache, childContext, true, material),
2885
+ ),
2886
+ };
2887
+ }
2888
+
2889
+ const geometry = this.requireGeometry();
2890
+ return {
2891
+ ...common,
2892
+ children: [],
2893
+ mesh: renderMesh(
2894
+ geometry,
2895
+ geometry.value.shape,
2896
+ meshCache,
2897
+ this.meshTolerance,
2898
+ geometry.value.topology,
2899
+ ),
2900
+ };
2901
+ }
2902
+
2903
+ /** @internal */
2904
+ disposeShape(disposed: Set<AnyShape>): void {
2905
+ const shapes = [
2906
+ ...(this.geometry ? [this.geometry.value.shape] : []),
2907
+ ...(this.geometry?.value.referenceBasis
2908
+ ? [this.geometry.value.referenceBasis.shape]
2909
+ : []),
2910
+ ...this.operation.regions.map(region => region.artifact.value),
2911
+ ];
2912
+ for (const shape of shapes) {
2913
+ if (!disposed.has(shape)) {
2914
+ disposed.add(shape);
2915
+ shape.delete();
2916
+ }
2917
+ }
2918
+ this.children.forEach(child => child.disposeShape(disposed));
2919
+ }
2920
+
2921
+ /** @internal */
2922
+ [loftModels](
2923
+ this: ModelObject<Elements, 'face'>,
2924
+ others: readonly ModelObject<{}, 'face'>[],
2925
+ spine: ModelObject<{}, 'edge'> | undefined,
2926
+ ruled: boolean,
2927
+ ): SolidModel {
2928
+ const sections: readonly ModelObject<{}, 'face'>[] = [this, ...others];
2929
+ const solveContext = ModelObject.createSolveContext([
2930
+ ...sections,
2931
+ ...(spine ? [spine] : []),
2932
+ ]);
2933
+ const resultPose = this.solvePose(solveContext);
2934
+ const sectionInputs = sections.map(section => ({
2935
+ model: section,
2936
+ geometry: section.requireGeometry(),
2937
+ transform: relativeTransform(section.solvePose(solveContext), resultPose),
2938
+ }));
2939
+ const spineInput = spine
2940
+ ? {
2941
+ model: spine,
2942
+ geometry: spine.requireGeometry(),
2943
+ transform: relativeTransform(
2944
+ spine.solvePose(solveContext),
2945
+ resultPose,
2946
+ ),
2947
+ }
2948
+ : undefined;
2949
+ const geometry = evaluateSolidGeometry(
2950
+ spine ? 'spine-loft' : 'loft',
2951
+ [
2952
+ ruled,
2953
+ sectionInputs.map(input => [
2954
+ input.transform.position,
2955
+ input.transform.quaternion,
2956
+ ]),
2957
+ spineInput
2958
+ ? [spineInput.transform.position, spineInput.transform.quaternion]
2959
+ : null,
2960
+ ],
2961
+ [
2962
+ ...sectionInputs.map(input => input.geometry),
2963
+ ...(spineInput ? [spineInput.geometry] : []),
2964
+ ],
2965
+ () => buildLoftGeometry(sectionInputs, spineInput, ruled),
2971
2966
  );
2972
- if (constraint.kind === 'align') {
2973
- for (const action of actions)
2974
- if ('body' in action)
2975
- offsetFrame = composeTransforms(
2976
- axisRotation(
2977
- composeTransforms(
2978
- models[action.body].solvePose(context),
2979
- action.axis,
2967
+ const inputs = [...sections, ...(spine ? [spine] : [])];
2968
+ return ModelObject.create<CanonicalElements, 'solid'>({
2969
+ kind: 'solid',
2970
+ name: 'Loft',
2971
+ geometry,
2972
+ material: this.materialSnapshot,
2973
+ constraints: this.constraints,
2974
+ sourceRefs: inputs.flatMap(input => input.sourceRefs),
2975
+ parameters: uniqueParameters(
2976
+ inputs.flatMap(input => input.allParameters()),
2977
+ ),
2978
+ meshTolerance: Math.min(...inputs.map(input => input.meshTolerance)),
2979
+ operation: storedOperation('loft', [
2980
+ {model: this, role: 'receiver', index: 0},
2981
+ ...others.map((model, index) => ({
2982
+ model,
2983
+ role: 'section' as const,
2984
+ index: index + 1,
2985
+ })),
2986
+ ...(spine
2987
+ ? [{model: spine, role: 'spine' as const, index: sections.length}]
2988
+ : []),
2989
+ ]),
2990
+ }) as unknown as SolidModel;
2991
+ }
2992
+
2993
+ /** @internal */
2994
+ [combineModels](
2995
+ this: ModelObject<Elements, 'solid'>,
2996
+ operation: BooleanOperation,
2997
+ others: readonly ModelObject<{}, 'solid'>[],
2998
+ ): SolidModel {
2999
+ const evaluation = this.evaluateBoolean(operation, others);
3000
+ let transferred = false;
3001
+ try {
3002
+ const combined = ModelObject.create<CanonicalElements, 'solid'>({
3003
+ kind: 'solid',
3004
+ geometryAnchor: this.geometryAnchor,
3005
+ geometry: evaluation.geometry,
3006
+ name: this.name,
3007
+ material: this.materialSnapshot,
3008
+ constraints: this.constraints,
3009
+ sourceRefs: [this, ...others].flatMap(model => model.sourceRefs),
3010
+ parameters: uniqueParameters(
3011
+ [this, ...others].flatMap(model => model.allParameters()),
3012
+ ),
3013
+ meshTolerance: Math.min(
3014
+ this.meshTolerance,
3015
+ ...others.map(model => model.meshTolerance),
3016
+ ),
3017
+ operation: storedOperation(
3018
+ operation === 'fuse' ? 'union' : operation,
3019
+ [
3020
+ {model: this, role: 'receiver', index: 0},
3021
+ ...others.map((model, index) => ({
3022
+ model,
3023
+ role:
3024
+ operation === 'cut' ? ('tool' as const) : ('operand' as const),
3025
+ index: index + 1,
3026
+ })),
3027
+ ],
3028
+ {regions: evaluation.regions},
3029
+ ),
3030
+ });
3031
+ transferred = true;
3032
+ return combined as unknown as SolidModel;
3033
+ } finally {
3034
+ if (!transferred) {
3035
+ disposeModelGeometryValue(evaluation.geometry.value);
3036
+ evaluation.regions.forEach(region => region.artifact.value.delete());
3037
+ }
3038
+ }
3039
+ }
3040
+
3041
+ private evaluateBoolean(
3042
+ this: ModelObject<Elements, 'solid'>,
3043
+ operation: BooleanOperation,
3044
+ others: readonly ModelObject<{}, 'solid'>[],
3045
+ ): BooleanEvaluation {
3046
+ const solveContext = ModelObject.createSolveContext([this, ...others]);
3047
+ const targetPose = this.solvePose(solveContext);
3048
+ let geometry = this.requireSolidGeometry();
3049
+ let temporaryGeometry: SolidGeometry | undefined;
3050
+ const regions: StoredOperationRegion[] = [];
3051
+ let evaluated = false;
3052
+ try {
3053
+ for (const [otherIndex, other] of others.entries()) {
3054
+ const otherGeometry = other.requireSolidGeometry();
3055
+ const transform = relativeTransform(
3056
+ other.solvePose(solveContext),
3057
+ targetPose,
3058
+ );
3059
+ const operand = evaluateKernelShape(
3060
+ 'transform',
3061
+ [transform.position, transform.quaternion],
3062
+ [otherGeometry],
3063
+ () => shapeWithTransform(otherGeometry.value.shape, transform),
3064
+ );
3065
+ try {
3066
+ if (operation === 'cut' || operation === 'fuse') {
3067
+ regions.push({
3068
+ kind: 'intersection',
3069
+ input: other,
3070
+ frame: this,
3071
+ artifact: evaluateKernelShape(
3072
+ 'boolean-intersection-region',
3073
+ [],
3074
+ [geometry, operand],
3075
+ () => geometry.value.shape.intersect(operand.value),
2980
3076
  ),
2981
- action.angle,
2982
- ),
2983
- offsetFrame,
3077
+ });
3078
+ }
3079
+ if (operation === 'fuse') {
3080
+ regions.push({
3081
+ kind: 'section',
3082
+ input: other,
3083
+ frame: this,
3084
+ artifact: evaluateKernelShape(
3085
+ 'boolean-section-region',
3086
+ [],
3087
+ [geometry, operand],
3088
+ () => unionSectionShape(geometry.value.shape, operand.value),
3089
+ ),
3090
+ });
3091
+ }
3092
+ const nextGeometry = evaluateSolidGeometry(
3093
+ `boolean-${operation}`,
3094
+ [otherIndex],
3095
+ [geometry, operand],
3096
+ () => {
3097
+ const result = booleanWithTopology(
3098
+ {
3099
+ shape: geometry.value.shape,
3100
+ topology: geometry.value.topology,
3101
+ index: otherIndex === 0 ? 1 : undefined,
3102
+ },
3103
+ {
3104
+ shape: operand.value,
3105
+ topology: otherGeometry.value.topology,
3106
+ index: otherIndex + 2,
3107
+ },
3108
+ operation,
3109
+ );
3110
+ return {shape: result.shape, topology: result.topology};
3111
+ },
2984
3112
  );
2985
- return {
2986
- id: constraint.id,
2987
- kind: 'align',
2988
- source: anchorSnapshot(source, constraint.source),
2989
- target: anchorSnapshot(target, constraint.target),
2990
- sourceElement: source.relationElement(constraint.source),
2991
- targetElement: target.relationElement(constraint.target),
2992
- offsetDirection: 1,
2993
- offset: constraint.offset ?? origin,
2994
- offsetFrame: toTransform(offsetFrame),
2995
- sourceRefs: [...valueTrace(constraint).sourceRefs],
2996
- parameters: [...valueTrace(constraint).parameters],
2997
- };
3113
+ temporaryGeometry?.value.shape.delete();
3114
+ temporaryGeometry = nextGeometry;
3115
+ geometry = nextGeometry;
3116
+ } finally {
3117
+ operand.value.delete();
3118
+ }
3119
+ }
3120
+ evaluated = true;
3121
+ return {geometry, regions};
3122
+ } finally {
3123
+ if (!evaluated) {
3124
+ temporaryGeometry?.value.shape.delete();
3125
+ regions.forEach(region => region.artifact.value.delete());
3126
+ }
2998
3127
  }
2999
- const orientation = composeTransforms(
3000
- invertTransform(rotation(offsetFrame.quaternion)),
3001
- rotation(sourcePose.quaternion),
3002
- ).quaternion;
3003
- const bounds = source[referenceBounds](
3004
- constraint.source,
3005
- rotation(orientation),
3006
- );
3007
- const center: Vec3 = [
3008
- (bounds[0][0] + bounds[1][0]) / 2,
3009
- bounds[constraint.target.bound!.facing === 1 ? 0 : 1][1],
3010
- (bounds[0][2] + bounds[1][2]) / 2,
3011
- ];
3012
- const sourceFrame = relativeTransform(
3013
- {
3014
- position: addVectors(
3015
- sourcePose.position,
3016
- rotateVector(center, offsetFrame.quaternion),
3128
+ }
3129
+
3130
+ private allParameters(): ParameterUsage[] {
3131
+ return uniqueParameters([
3132
+ ...this.parameters,
3133
+ ...this.constraints.flatMap(
3134
+ constraint => valueTrace(constraint).parameters,
3135
+ ),
3136
+ ]);
3137
+ }
3138
+
3139
+ private memberPoses(): Map<RelationObject, RigidTransform | null> {
3140
+ const members = new Map<RelationObject, RigidTransform | null>([
3141
+ [this, identityRigidTransform],
3142
+ ]);
3143
+ if (this.children.length) {
3144
+ const context = this.assembly!;
3145
+ for (const child of this.children) {
3146
+ const pose = child.solvePose(context);
3147
+ for (const [member, localPose] of child.memberPoses()) {
3148
+ members.set(
3149
+ member,
3150
+ members.has(member) || localPose === null
3151
+ ? null
3152
+ : composeTransforms(pose, localPose),
3153
+ );
3154
+ }
3155
+ }
3156
+ }
3157
+ return members;
3158
+ }
3159
+
3160
+ /** Bounds of the selected finite geometry after a rigid transform. */
3161
+ private [referenceBoundsParts](
3162
+ reference: StoredAnchor,
3163
+ transform: RigidTransform,
3164
+ ): ({bounds: LocalBounds} | SnapshotQueryInput)[] {
3165
+ if (reference.bound)
3166
+ return [{bounds: super[referenceBounds](reference, transform)}];
3167
+ if (reference.whole) {
3168
+ if (this.geometry) {
3169
+ const bounds = axisAlignedBounds(
3170
+ this.geometry.value.localBounds,
3171
+ transform,
3172
+ );
3173
+ return [bounds ? {bounds} : boundsQuery(this.geometry, transform)];
3174
+ }
3175
+ const context = this.assembly!;
3176
+ return this.children.flatMap(child =>
3177
+ child[referenceBoundsParts](
3178
+ child.relationAnchorReference(),
3179
+ composeTransforms(transform, child.solvePose(context)),
3017
3180
  ),
3018
- quaternion: offsetFrame.quaternion,
3019
- },
3020
- sourcePose,
3181
+ );
3182
+ }
3183
+ if (reference.topology) {
3184
+ const topology = reference.topology;
3185
+ return [
3186
+ boundsQuery(
3187
+ topology.source.requireGeometry(),
3188
+ composeTransforms(transform, topology.transform),
3189
+ topology.selection,
3190
+ topology.scale,
3191
+ ),
3192
+ ];
3193
+ }
3194
+ return [{bounds: super[referenceBounds](reference, transform)}];
3195
+ }
3196
+
3197
+ /** Bounds of the selected finite geometry after a rigid transform. */
3198
+ override [referenceBounds](
3199
+ reference: StoredAnchor,
3200
+ transform: RigidTransform,
3201
+ ): LocalBounds {
3202
+ const bounds = this[referenceBoundsParts](reference, transform).map(part =>
3203
+ 'bounds' in part
3204
+ ? part.bounds
3205
+ : (evaluateSnapshotQuery(part) as LocalBounds),
3021
3206
  );
3022
- const sourceSize: Vec3 = [
3023
- bounds[1][0] - bounds[0][0],
3024
- bounds[1][1] - bounds[0][1],
3025
- bounds[1][2] - bounds[0][2],
3026
- ];
3027
- for (const action of actions) {
3028
- if ('body' in action)
3029
- offsetFrame = composeTransforms(
3030
- axisRotation(
3031
- composeTransforms(
3032
- models[action.body].solvePose(context),
3033
- action.axis,
3034
- ),
3035
- action.angle,
3036
- ),
3037
- offsetFrame,
3207
+ return bounds.length === 1 ? bounds[0] : combineBounds(bounds);
3208
+ }
3209
+
3210
+ /** Pure query collection: it never substitutes geometry or runs author code. */
3211
+ [modelSnapshotQueries](): SnapshotQueryInput[] {
3212
+ const result: SnapshotQueryInput[] = [];
3213
+ if (this.geometry || this.children.length) {
3214
+ for (const direction of Object.values(boundDirections)) {
3215
+ const transform = invertTransform(
3216
+ rotation(frameFromYAxis(origin, direction).quaternion),
3038
3217
  );
3218
+ for (const part of this[referenceBoundsParts](
3219
+ this.relationAnchorReference(),
3220
+ transform,
3221
+ ))
3222
+ if (!('bounds' in part)) result.push(part);
3223
+ }
3039
3224
  }
3040
- return {
3041
- id: constraint.id,
3042
- kind: constraint.kind,
3043
- source: anchorSnapshot(source, constraint.source),
3044
- target: anchorSnapshot(target, constraint.target),
3045
- sourceBounds: {
3046
- size: sourceSize,
3047
- transform: toTransform(
3048
- composeTransforms(sourceFrame, {
3049
- position: [
3050
- 0,
3051
- (constraint.target.bound!.facing * sourceSize[1]) / 2,
3052
- 0,
3053
- ],
3054
- quaternion: [0, 0, 0, 1],
3055
- }),
3225
+ if (this.geometry)
3226
+ result.push(
3227
+ meshQuery(
3228
+ this.geometry,
3229
+ this.geometry.value.shape,
3230
+ this.meshTolerance,
3231
+ this.geometry.value.topology,
3056
3232
  ),
3057
- },
3058
- sourceElement: {
3059
- name: `${constraint.source.name}:bound`,
3060
- kind: 'face',
3061
- transform: toTransform(sourceFrame),
3062
- bound: {
3063
- size: [sourceSize[0], sourceSize[2]],
3064
- facing: constraint.target.bound!.facing === 1 ? -1 : 1,
3233
+ );
3234
+ for (const region of this.operation.regions)
3235
+ result.push(
3236
+ meshQuery(region.artifact, region.artifact.value, this.meshTolerance),
3237
+ );
3238
+ return result;
3239
+ }
3240
+
3241
+ protected override alignmentGeometry(
3242
+ reference: StoredAnchor,
3243
+ ): AlignmentGeometry {
3244
+ if (reference.kind === 'point')
3245
+ return {
3246
+ kind: 'point',
3247
+ point: reference.whole
3248
+ ? (this.requireGeometry().value.shape as ReplicadVertex).asTuple()
3249
+ : reference.transform.position,
3250
+ };
3251
+ const read = (shape: AnyShape) =>
3252
+ reference.kind === 'line'
3253
+ ? edgeGeometry(shape as ReplicadEdge, reference.direction ?? 1)
3254
+ : faceGeometry(
3255
+ shape as ReplicadFace,
3256
+ reference.facing ?? reference.bound?.facing ?? 1,
3257
+ );
3258
+ if (reference.topology) {
3259
+ const topology = reference.topology,
3260
+ geometry = topology.source.requireGeometry().value;
3261
+ return withTopologyShape(
3262
+ geometry.shape,
3263
+ geometry.topology,
3264
+ topology.selection,
3265
+ shape => {
3266
+ const scaled =
3267
+ topology.scale === 1
3268
+ ? shape
3269
+ : shapeWithScale(shape, topology.scale);
3270
+ try {
3271
+ return transformGeometry(read(scaled), topology.transform);
3272
+ } finally {
3273
+ if (scaled !== shape) scaled.delete();
3274
+ }
3065
3275
  },
3066
- },
3067
- targetElement: {
3068
- name: constraint.target.name,
3069
- kind: 'face',
3070
- transform: toTransform(constraint.target.transform),
3071
- bound: constraint.target.bound,
3072
- },
3073
- offsetDirection: source === this ? 1 : -1,
3074
- offset: constraint.offset ?? origin,
3075
- offsetFrame: toTransform(offsetFrame),
3076
- sourceRefs: [...valueTrace(constraint).sourceRefs],
3077
- parameters: [...valueTrace(constraint).parameters],
3078
- };
3276
+ );
3277
+ }
3278
+ if (reference.whole) return read(this.requireGeometry().value.shape);
3279
+ return super.alignmentGeometry(reference);
3280
+ }
3281
+
3282
+ protected override relationElement(reference: StoredAnchor): ElementSnapshot {
3283
+ return super.relationElement(
3284
+ reference.whole
3285
+ ? {
3286
+ ...this.exposedElement(),
3287
+ name: reference.name,
3288
+ direction: reference.direction,
3289
+ facing: reference.facing,
3290
+ }
3291
+ : reference,
3292
+ );
3293
+ }
3294
+
3295
+ protected override vertexPosition(id: VertexId): Vec3 {
3296
+ return anchorReference(this.vertex(id)).transform.position;
3297
+ }
3298
+
3299
+ protected copyRelations(init: RelationObjectInit): ModelObject {
3300
+ return this.copy(init, this.operation);
3301
+ }
3302
+
3303
+ /** Fix the assembly frame once, using only its direct member origins. */
3304
+ private static createAssembly(
3305
+ children: readonly ModelObject[],
3306
+ ): SolveContext {
3307
+ const context = ModelObject.createSolveContext(children);
3308
+ const center = children.length
3309
+ ? boundsCenter(
3310
+ pointBounds(children.map(child => child.solvePose(context).position)),
3311
+ )
3312
+ : origin;
3313
+ return transformSolveContext(context, translation(negateVector(center)));
3079
3314
  }
3080
3315
 
3081
3316
  private requireGeometry(): ModelGeometry {
@@ -3659,39 +3894,34 @@ export function regularPrism(
3659
3894
  }
3660
3895
 
3661
3896
  /** @internal */
3662
- export function modelFromReplicadSolid(shape: Shape3D): SolidModel {
3663
- let solid: Shape3D;
3664
- try {
3665
- solid = normalizeReplicadSolid(shape);
3666
- } catch (error) {
3667
- shape.delete();
3668
- throw error;
3669
- }
3670
- // The builder still executes on every call. Cache its actual output, not its
3671
- // arguments: captured state may change the geometry between invocations.
3672
- let adopted = false;
3673
- let geometry: SolidGeometry;
3674
- try {
3675
- geometry = evaluateSolidGeometry(
3676
- 'replicad-solid',
3677
- [solid.serialize()],
3678
- [],
3679
- () => {
3680
- adopted = true;
3681
- return {shape: solid};
3682
- },
3683
- );
3684
- } finally {
3685
- // A hit returns an independently owned cached copy; discard this output.
3686
- // A miss transfers ownership to evaluateSolidGeometry, including on error.
3687
- if (!adopted) solid.delete();
3688
- }
3689
- return ModelObject.create<CanonicalElements, 'solid'>({
3690
- kind: 'solid',
3691
- name: 'Custom primitive',
3692
- geometry,
3693
- operation: storedOperation('primitive'),
3694
- }) as unknown as SolidModel;
3897
+ export function primitiveConstructor<Args extends unknown[]>(
3898
+ build: (...args: Args) => Shape3D,
3899
+ ): (...args: Args) => SolidModel {
3900
+ const geometry = cachedArtifact(
3901
+ (...args: Args) => {
3902
+ const shape = build(...args);
3903
+ let solid: Shape3D;
3904
+ try {
3905
+ solid = normalizeReplicadSolid(shape);
3906
+ } catch (error) {
3907
+ shape.delete();
3908
+ throw error;
3909
+ }
3910
+ return createModelGeometryValue(solid);
3911
+ },
3912
+ {
3913
+ identity: build,
3914
+ namespace: 'primitive',
3915
+ lifecycle: modelGeometryLifecycle,
3916
+ },
3917
+ );
3918
+ return (...args) =>
3919
+ ModelObject.create<CanonicalElements, 'solid'>({
3920
+ kind: 'solid',
3921
+ name: 'Custom primitive',
3922
+ geometry: geometry(...args) as SolidGeometry,
3923
+ operation: storedOperation('primitive'),
3924
+ }) as unknown as SolidModel;
3695
3925
  }
3696
3926
 
3697
3927
  function normalizeReplicadSolid(shape: Shape3D): Shape3D {
@@ -3780,15 +4010,55 @@ export function union(operands: readonly SolidModel<{}>[]): SolidModel {
3780
4010
  }
3781
4011
 
3782
4012
  /**
3783
- * Extrudes a single face; use faces.map(face => extrude(face, distance)) for multiple regions.
3784
- * @code3d.param distance {kind: 'length', label: 'Extrusion distance'}
4013
+ * Extrudes each face independently, preserving array order and placement.
4014
+ * @code3d.param distance {kind: 'length', default: 10, label: 'Extrusion distance'}
4015
+ */
4016
+ export function extrude(face: FaceModel<{}>, distance: number): SolidModel;
4017
+ /**
4018
+ * Extrudes each face independently.
4019
+ * @code3d.param distance {kind: 'length', default: 10, label: 'Extrusion distance'}
4020
+ */
4021
+ export function extrude(
4022
+ faces: readonly FaceModel<{}>[],
4023
+ distance: number,
4024
+ ): readonly SolidModel[];
4025
+ export function extrude(
4026
+ face: FaceModel<{}> | readonly FaceModel<{}>[],
4027
+ distance = 10,
4028
+ ): SolidModel | readonly SolidModel[] {
4029
+ const faces = (Array.isArray(face) ? face : [face]).map(value =>
4030
+ requireModelKind(
4031
+ value,
4032
+ 'face',
4033
+ 'extrude requires a face model or an array of face models.',
4034
+ ),
4035
+ );
4036
+ const solids = faces.map(value => value.extrude(distance));
4037
+ return Array.isArray(face) ? solids : solids[0];
4038
+ }
4039
+
4040
+ /**
4041
+ * Creates connected text faces on the XZ plane: +X right, -Z up, normal +Y.
4042
+ * All faces share the baseline origin. Size is the font em in model units.
4043
+ * @code3d.param size {kind: 'length', label: 'Text size'}
3785
4044
  */
3786
- export function extrude(face: FaceModel<{}>, distance: number): SolidModel {
3787
- return requireModelKind(
3788
- face,
3789
- 'face',
3790
- 'extrude requires a single face model.',
3791
- ).extrude(distance);
4045
+ export function text(
4046
+ content: string,
4047
+ font: Font,
4048
+ size: number,
4049
+ options?: TextOptions,
4050
+ ): readonly FaceModel[] {
4051
+ return textGlyphs(content, font, size, options).flatMap(({regions, x, y}) =>
4052
+ regions.value.map((region, index) => {
4053
+ const geometry = evaluateModelGeometry(
4054
+ 'text',
4055
+ [x, y, index],
4056
+ [regions],
4057
+ () => ({shape: textRegionFace(region, x, y)}),
4058
+ );
4059
+ return faceModel('text', 'Text face', geometry);
4060
+ }),
4061
+ );
3792
4062
  }
3793
4063
 
3794
4064
  export function cut(
@@ -3844,7 +4114,7 @@ export function instrumentConstraint(
3844
4114
  }
3845
4115
 
3846
4116
  export function instrumentModelOperation(
3847
- object: ModelObject,
4117
+ object: RelationObject,
3848
4118
  instrumentation: ModelOperationInstrumentation,
3849
4119
  ): void {
3850
4120
  object.attachSource(instrumentation.sourceRef);
@@ -3854,6 +4124,7 @@ export function instrumentModelOperation(
3854
4124
  instrumentation.execution,
3855
4125
  instrumentation.order,
3856
4126
  instrumentation.sourceRef,
4127
+ instrumentation.outputIndex,
3857
4128
  );
3858
4129
  }
3859
4130
 
@@ -3864,7 +4135,7 @@ export function constraintTraceReference(
3864
4135
  }
3865
4136
 
3866
4137
  export function modelObjectRuntimeInfo(
3867
- object: ModelObject,
4138
+ object: RelationObject,
3868
4139
  ): ModelObjectRuntimeInfo {
3869
4140
  return {
3870
4141
  nodeId: object.nodeId,
@@ -3874,8 +4145,8 @@ export function modelObjectRuntimeInfo(
3874
4145
  }
3875
4146
 
3876
4147
  export function relatedModelObjects(
3877
- object: ModelObject,
3878
- ): readonly ModelObject[] {
4148
+ object: RelationObject,
4149
+ ): readonly RelationObject[] {
3879
4150
  return object.relatedObjects();
3880
4151
  }
3881
4152
 
@@ -3947,13 +4218,6 @@ function meshQuery(
3947
4218
  };
3948
4219
  }
3949
4220
 
3950
- const queryLifecycle: KernelValueLifecycle<SnapshotQueryResult> = {
3951
- estimateBytes: estimateRetainedBytes,
3952
- retain: value => value,
3953
- instantiate: value => value,
3954
- release: () => {},
3955
- };
3956
-
3957
4221
  function computeSnapshotQuery(
3958
4222
  geometry: SnapshotQueryInput['geometry'],
3959
4223
  query: SnapshotQuery,
@@ -3967,14 +4231,13 @@ function computeSnapshotQuery(
3967
4231
  );
3968
4232
  }
3969
4233
 
3970
- function evaluateSnapshotQuery(input: SnapshotQueryInput): SnapshotQueryResult {
3971
- const cached = findKernelOperation(input.query.key, queryLifecycle);
3972
- if (cached) return cached.value;
3973
- let value: SnapshotQueryResult;
3974
- if (input.query.kind === 'mesh') {
3975
- // Mesh from the serialized input in every execution mode. BinTools can
3976
- // renormalize a plane axis by a few ULPs, changing Delaunay diagonal ties.
3977
- // A local query and a remote query must therefore consume the same BREP.
4234
+ const snapshotQuery = cachedArtifact(
4235
+ (input: SnapshotQueryInput): SnapshotQueryResult => {
4236
+ if (input.query.kind !== 'mesh')
4237
+ return computeSnapshotQuery(input.geometry, input.query);
4238
+ // Local and remote meshes consume the same binary input: BinTools can
4239
+ // renormalize plane axes by a few ULPs and change Delaunay diagonal ties.
4240
+ let value!: SnapshotQueryResult;
3978
4241
  executeSnapshotQueryBatch(
3979
4242
  input.inputId,
3980
4243
  encodeKernelArtifact(input.inputId, input.geometry),
@@ -3984,9 +4247,13 @@ function evaluateSnapshotQuery(input: SnapshotQueryInput): SnapshotQueryResult {
3984
4247
  value = result;
3985
4248
  },
3986
4249
  );
3987
- } else value = computeSnapshotQuery(input.geometry, input.query);
3988
- acceptKernelOperation(input.query.key, queryLifecycle, value!);
3989
- return value!;
4250
+ return value;
4251
+ },
4252
+ {key: input => input.query.key},
4253
+ );
4254
+
4255
+ function evaluateSnapshotQuery(input: SnapshotQueryInput): SnapshotQueryResult {
4256
+ return snapshotQuery(input).value;
3990
4257
  }
3991
4258
 
3992
4259
  /** Host-owned native input and result admission. Only encode() bytes cross runtimes. */
@@ -4000,10 +4267,13 @@ export type SnapshotQueryBatch = Readonly<{
4000
4267
  }>;
4001
4268
 
4002
4269
  export function planModelSnapshotQueries(
4003
- objects: readonly ModelObject[],
4270
+ objects: readonly RelationObject[],
4004
4271
  ): SnapshotQueryBatch[] {
4005
4272
  const visited = new Set<ModelObject>();
4006
- const keys = new Map<string, string>();
4273
+ const inputs = new Map<
4274
+ string,
4275
+ {input: SnapshotQueryInput; sourceRef?: SourceRef}
4276
+ >();
4007
4277
  const batches = new Map<
4008
4278
  string,
4009
4279
  {input: SnapshotQueryInput; queries: SnapshotQuery[]; sourceRef?: SourceRef}
@@ -4012,27 +4282,33 @@ export function planModelSnapshotQueries(
4012
4282
  if (visited.has(object)) return;
4013
4283
  visited.add(object);
4014
4284
  for (const input of object[modelSnapshotQueries]()) {
4015
- const {query, inputId} = input;
4016
- const signature = keys.get(query.key.id);
4017
- if (signature) {
4018
- if (signature !== query.key.signature)
4285
+ const {query} = input;
4286
+ const existing = inputs.get(query.key.id);
4287
+ if (existing) {
4288
+ if (existing.input.query.key.signature !== query.key.signature)
4019
4289
  throw new Error(
4020
4290
  `Kernel operation cache identity collision: ${query.key.id}`,
4021
4291
  );
4022
4292
  continue;
4023
4293
  }
4024
- keys.set(query.key.id, query.key.signature);
4025
- if (findKernelOperation(query.key, queryLifecycle)) continue;
4026
- let batch = batches.get(inputId);
4027
- if (!batch) {
4028
- batch = {input, queries: [], sourceRef: object.sourceRefs.at(-1)};
4029
- batches.set(inputId, batch);
4030
- }
4031
- batch.queries.push(query);
4294
+ inputs.set(query.key.id, {input, sourceRef: object.sourceRefs.at(-1)});
4032
4295
  }
4033
4296
  object.children.forEach(collect);
4034
4297
  }
4035
- objects.forEach(collect);
4298
+ objects.filter(isModelObject).forEach(collect);
4299
+ const pending = [...inputs.values()];
4300
+ const hits = snapshotQuery.findMany(
4301
+ pending.map(({input}) => input.query.key),
4302
+ );
4303
+ pending.forEach(({input, sourceRef}, index) => {
4304
+ if (hits[index]) return;
4305
+ let batch = batches.get(input.inputId);
4306
+ if (!batch) {
4307
+ batch = {input, queries: [], sourceRef};
4308
+ batches.set(input.inputId, batch);
4309
+ }
4310
+ batch.queries.push(input.query);
4311
+ });
4036
4312
  return [...batches].map(([id, {input, queries, sourceRef}]) => ({
4037
4313
  id,
4038
4314
  queries,
@@ -4043,8 +4319,7 @@ export function planModelSnapshotQueries(
4043
4319
  (input.geometry.topology?.surfaces.ids.length ?? 0)) *
4044
4320
  queries.length,
4045
4321
  encode: () => encodeKernelArtifact(id, input.geometry),
4046
- accept: (query, value) =>
4047
- acceptKernelOperation(query.key, queryLifecycle, value),
4322
+ accept: (query, value) => snapshotQuery.accept(query.key, value),
4048
4323
  }));
4049
4324
  }
4050
4325
 
@@ -4074,16 +4349,16 @@ export function executeSnapshotQueryBatch(
4074
4349
  }
4075
4350
 
4076
4351
  export function createModelSnapshotter(): (
4077
- object: ModelObject,
4352
+ object: RelationObject,
4078
4353
  ) => ModelSnapshotObject {
4079
4354
  const meshCache = new Map<AnyShape, RenderMesh>();
4080
4355
  return object => object.toSnapshot(meshCache);
4081
4356
  }
4082
4357
 
4083
- export function disposeModelObjects(objects: Iterable<ModelObject>): void {
4358
+ export function disposeModelObjects(objects: Iterable<RelationObject>): void {
4084
4359
  const disposed = new Set<AnyShape>();
4085
4360
  for (const object of objects) {
4086
- object.disposeShape(disposed);
4361
+ if (isModelObject(object)) object.disposeShape(disposed);
4087
4362
  }
4088
4363
  }
4089
4364
 
@@ -4099,7 +4374,7 @@ export type ModelGeometrySnapshot = Readonly<{
4099
4374
  }>;
4100
4375
 
4101
4376
  export function retainModelGeometry(
4102
- objects: Iterable<ModelObject>,
4377
+ objects: Iterable<RelationObject>,
4103
4378
  ): ModelGeometrySnapshot {
4104
4379
  const retained = new Map<AnyShape, AnyShape>();
4105
4380
  const shapes = new Map<string, AnyShape>();
@@ -4112,6 +4387,7 @@ export function retainModelGeometry(
4112
4387
  };
4113
4388
  try {
4114
4389
  for (const object of objects) {
4390
+ if (!isModelObject(object)) continue;
4115
4391
  const geometry = object[modelGeometry]()?.value;
4116
4392
  const original = geometry?.shape;
4117
4393
  if (!original) continue;
@@ -4141,6 +4417,10 @@ export function retainModelGeometry(
4141
4417
  }
4142
4418
 
4143
4419
  export const authoringApi = Object.freeze({
4420
+ cached,
4421
+ font,
4422
+ googleFont,
4423
+ text,
4144
4424
  sketch,
4145
4425
  circle,
4146
4426
  ellipse,
@@ -4188,7 +4468,7 @@ function storedOperation(
4188
4468
  function storedOperationId(operation: StoredOperation): string {
4189
4469
  const trace = operationTraces.get(operation);
4190
4470
  return trace
4191
- ? `${trace.siteId}:execution:${trace.execution}`
4471
+ ? `${trace.siteId}:execution:${trace.execution}:output:${trace.outputIndex ?? 0}`
4192
4472
  : operation.runtimeId;
4193
4473
  }
4194
4474
 
@@ -4233,45 +4513,49 @@ function disposeModelGeometryValue(geometry: ModelGeometryValue): void {
4233
4513
  geometry.referenceBasis?.shape.delete();
4234
4514
  }
4235
4515
 
4516
+ const kernelShape = cachedArtifact(
4517
+ (
4518
+ _operation: string,
4519
+ _arguments: readonly KernelKeyPart[],
4520
+ _inputs: readonly KernelArtifact<unknown>[],
4521
+ compute: () => AnyShape,
4522
+ ) => compute(),
4523
+ {key: kernelOperationKey, lifecycle: shapeLifecycle},
4524
+ );
4525
+
4236
4526
  function evaluateKernelShape<Shape extends AnyShape>(
4237
4527
  operation: string,
4238
4528
  arguments_: readonly KernelKeyPart[],
4239
4529
  inputs: readonly KernelArtifact<unknown>[],
4240
4530
  compute: () => Shape,
4241
4531
  ): KernelArtifact<Shape> {
4242
- return evaluateKernelOperation(
4532
+ return kernelShape(
4243
4533
  operation,
4244
4534
  arguments_,
4245
4535
  inputs,
4246
- shapeLifecycle as KernelValueLifecycle<Shape>,
4247
4536
  compute,
4248
- );
4537
+ ) as KernelArtifact<Shape>;
4249
4538
  }
4250
4539
 
4251
- function evaluateModelGeometry(
4252
- operation: string,
4253
- arguments_: readonly KernelKeyPart[],
4254
- inputs: readonly KernelArtifact<unknown>[],
4255
- compute: () => Readonly<{
4256
- shape: AnyShape;
4257
- topology?: ShapeTopology;
4258
- referenceBasis?: ModelGeometryValue['referenceBasis'];
4259
- }>,
4260
- ): ModelGeometry {
4261
- return evaluateKernelOperation(
4262
- operation,
4263
- arguments_,
4264
- inputs,
4265
- modelGeometryLifecycle,
4266
- () => {
4267
- const result = compute();
4268
- return {
4269
- ...createModelGeometryValue(result.shape, result.topology),
4270
- referenceBasis: result.referenceBasis,
4271
- };
4272
- },
4273
- );
4274
- }
4540
+ const evaluateModelGeometry = cachedArtifact(
4541
+ (
4542
+ _operation: string,
4543
+ _arguments: readonly KernelKeyPart[],
4544
+ _inputs: readonly KernelArtifact<unknown>[],
4545
+ compute: () => Readonly<{
4546
+ shape: AnyShape;
4547
+ topology?: ShapeTopology;
4548
+ referenceBasis?: ModelGeometryValue['referenceBasis'];
4549
+ }>,
4550
+ ): ModelGeometryValue => {
4551
+ const result = compute();
4552
+ return {
4553
+ ...createModelGeometryValue(result.shape, result.topology),
4554
+ referenceBasis: result.referenceBasis,
4555
+ };
4556
+ },
4557
+ {key: kernelOperationKey, lifecycle: modelGeometryLifecycle},
4558
+ );
4275
4559
 
4276
4560
  function createModelGeometryValue(
4277
4561
  shape: AnyShape,
@@ -4378,6 +4662,8 @@ function meshUVs(
4378
4662
  location,
4379
4663
  0,
4380
4664
  );
4665
+ // Native meshing can omit a face below its geometric tolerance.
4666
+ if (!triangulation) continue;
4381
4667
  try {
4382
4668
  if (triangulation.isNull()) continue;
4383
4669
  if (!triangulation.HasUVNodes()) return undefined;
@@ -4425,7 +4711,11 @@ type PlanarSketch = Readonly<{
4425
4711
  }>;
4426
4712
 
4427
4713
  /** Internal bridge from the kernel-independent sketch definition to model geometry. */
4428
- export function sketchFaceModel(region: SketchRegion): FaceModel {
4714
+ function sketchFaceModel(
4715
+ region: SketchRegion,
4716
+ constraints: readonly StoredConstraint[],
4717
+ source: SketchFrame,
4718
+ ): FaceModel {
4429
4719
  const curves = [region.outer, ...region.holes].map(loop =>
4430
4720
  loop.map(curve =>
4431
4721
  curve.kind === 'line'
@@ -4445,10 +4735,13 @@ export function sketchFaceModel(region: SketchRegion): FaceModel {
4445
4735
  return ModelObject.create<PlanarElements, 'face'>({
4446
4736
  kind: 'face',
4447
4737
  name: 'Sketch face',
4738
+ constraints,
4448
4739
  geometry,
4449
4740
  geometryAnchor: plane,
4450
4741
  elements: {plane},
4451
- operation: storedOperation('sketchFace'),
4742
+ operation: storedOperation('sketchFace', [
4743
+ {model: source, role: 'source', index: 0},
4744
+ ]),
4452
4745
  }) as unknown as FaceModel;
4453
4746
  }
4454
4747
 
@@ -4471,6 +4764,14 @@ function planarFaceModel(
4471
4764
  sketch.delete();
4472
4765
  }
4473
4766
  });
4767
+ return faceModel(operation, name, geometry);
4768
+ }
4769
+
4770
+ function faceModel(
4771
+ operation: ModelOperationKind,
4772
+ name: string,
4773
+ geometry: ModelGeometry,
4774
+ ): FaceModel {
4474
4775
  const plane: StoredElement = {
4475
4776
  kind: 'face',
4476
4777
  transform: identityRigidTransform,
@@ -4607,14 +4908,47 @@ function solidElements(bounds: LocalBounds): StoredElements {
4607
4908
  return {axis: {kind: 'line', transform: translation(boundsCenter(bounds))}};
4608
4909
  }
4609
4910
 
4610
- function constraintReferences(constraint: StoredConstraint): ModelObject[] {
4911
+ function constraintReferences(constraint: StoredConstraint): RelationObject[] {
4611
4912
  return [
4612
4913
  constraint.source.model,
4613
4914
  constraint.target.model,
4614
4915
  ...constraint.rotations.map(action =>
4615
4916
  action.pivot.kind === 'around' ? action.pivot.axis.model : undefined,
4616
4917
  ),
4617
- ].filter((model): model is ModelObject => !!model);
4918
+ ].filter((model): model is RelationObject => !!model);
4919
+ }
4920
+
4921
+ /** Signed axis permutations preserve tight bounds; arbitrary rotations do not. */
4922
+ function axisAlignedBounds(
4923
+ bounds: LocalBounds,
4924
+ transform: RigidTransform,
4925
+ ): LocalBounds | undefined {
4926
+ const axes = (
4927
+ [
4928
+ [1, 0, 0],
4929
+ [0, 1, 0],
4930
+ [0, 0, 1],
4931
+ ] as const
4932
+ ).map(axis => rotateVector(axis, transform.quaternion));
4933
+ // Account only for floating-point roundoff in quarter-turn quaternion products.
4934
+ if (
4935
+ axes.some(axis =>
4936
+ axis.some(
4937
+ value => Math.abs(value - Math.round(value)) > 8 * Number.EPSILON,
4938
+ ),
4939
+ )
4940
+ )
4941
+ return;
4942
+ return [0, 1].map(side =>
4943
+ axes[0].map((_, output) => {
4944
+ const input = axes.findIndex(axis => Math.abs(axis[output]) > 0.5);
4945
+ const sign = Math.round(axes[input][output]);
4946
+ return (
4947
+ transform.position[output] +
4948
+ sign * bounds[sign > 0 ? side : 1 - side][input]
4949
+ );
4950
+ }),
4951
+ ) as unknown as LocalBounds;
4618
4952
  }
4619
4953
 
4620
4954
  function computeTransformedBounds(
@@ -4699,7 +5033,7 @@ function directionalBound(
4699
5033
  }
4700
5034
 
4701
5035
  function modelAnchor<Kind extends ElementKind>(
4702
- model: ModelObject,
5036
+ model: RelationObject,
4703
5037
  name: string,
4704
5038
  element: StoredElement & Readonly<{kind: Kind}>,
4705
5039
  ): Anchor<Kind> {
@@ -4875,7 +5209,7 @@ function topologyName(selection: TopologySelection): string {
4875
5209
  }
4876
5210
 
4877
5211
  function topologyReferences(
4878
- model: ModelObject,
5212
+ model: RelationObject,
4879
5213
  prefix: string,
4880
5214
  context: Omit<StoredTopology, 'selection'>,
4881
5215
  kind: TopologyKind,
@@ -4957,14 +5291,14 @@ function anchorReference(anchor: Anchor): AnchorReference {
4957
5291
 
4958
5292
  function anchorSnapshot(reference: AnchorReference): ConstraintAnchorSnapshot;
4959
5293
  function anchorSnapshot(
4960
- model: ModelObject,
5294
+ model: RelationObject,
4961
5295
  anchor: StoredAnchor,
4962
5296
  ): ConstraintAnchorSnapshot;
4963
5297
  function anchorSnapshot(
4964
- modelOrReference: ModelObject | AnchorReference,
5298
+ modelOrReference: RelationObject | AnchorReference,
4965
5299
  stored?: StoredAnchor,
4966
5300
  ): ConstraintAnchorSnapshot {
4967
- const model = stored ? (modelOrReference as ModelObject) : undefined;
5301
+ const model = stored ? (modelOrReference as RelationObject) : undefined;
4968
5302
  const reference = stored ?? (modelOrReference as AnchorReference);
4969
5303
  return {
4970
5304
  nodeId: (model ?? (modelOrReference as AnchorReference).model).nodeId,
@@ -4973,7 +5307,7 @@ function anchorSnapshot(
4973
5307
  };
4974
5308
  }
4975
5309
 
4976
- function uniqueModels(models: readonly ModelObject[]): ModelObject[] {
5310
+ function uniqueModels<T extends RelationObject>(models: readonly T[]): T[] {
4977
5311
  return [...new Set(models)];
4978
5312
  }
4979
5313