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

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