@code3d/core 0.0.1-alpha.6 → 0.0.1-alpha.8

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 (37) hide show
  1. package/README.md +53 -3
  2. package/bld/chunks/{chunk-3RIMOCPP.js → chunk-EWPRWRLV.js} +2 -2
  3. package/bld/chunks/{chunk-T6COR2SG.js → chunk-HSU2FH3L.js} +3 -3
  4. package/bld/chunks/{chunk-T3RAUNSF.js → chunk-MICNLFBF.js} +452 -107
  5. package/bld/chunks/chunk-MICNLFBF.js.map +7 -0
  6. package/bld/chunks/{chunk-KYW4AWKO.js → chunk-OAVABPL5.js} +2 -2
  7. package/bld/library/cached.d.ts +5 -3
  8. package/bld/library/cached.d.ts.map +1 -1
  9. package/bld/library/index.d.ts +4 -4
  10. package/bld/library/index.d.ts.map +1 -1
  11. package/bld/library/index.js +5 -3
  12. package/bld/library/kernel-cache.d.ts +5 -3
  13. package/bld/library/kernel-cache.d.ts.map +1 -1
  14. package/bld/library/loft.d.ts.map +1 -1
  15. package/bld/library/replicad.js +2 -2
  16. package/bld/library/runtime.d.ts +61 -6
  17. package/bld/library/runtime.d.ts.map +1 -1
  18. package/bld/library/topology.d.ts +4 -2
  19. package/bld/library/topology.d.ts.map +1 -1
  20. package/bld/node/index.js +7 -5
  21. package/bld/node/replicad.js +4 -4
  22. package/bld/tooling/index.d.ts +2 -2
  23. package/bld/tooling/index.d.ts.map +1 -1
  24. package/bld/tooling/index.js +4 -2
  25. package/package.json +1 -1
  26. package/src/library/cached.ts +29 -8
  27. package/src/library/index.ts +5 -2
  28. package/src/library/kernel-cache.ts +29 -5
  29. package/src/library/loft.ts +5 -1
  30. package/src/library/runtime.ts +508 -43
  31. package/src/library/shell.ts +3 -3
  32. package/src/library/topology.ts +24 -15
  33. package/src/tooling/index.ts +2 -0
  34. package/bld/chunks/chunk-T3RAUNSF.js.map +0 -7
  35. /package/bld/chunks/{chunk-3RIMOCPP.js.map → chunk-EWPRWRLV.js.map} +0 -0
  36. /package/bld/chunks/{chunk-T6COR2SG.js.map → chunk-HSU2FH3L.js.map} +0 -0
  37. /package/bld/chunks/{chunk-KYW4AWKO.js.map → chunk-OAVABPL5.js.map} +0 -0
@@ -32,7 +32,7 @@ import {
32
32
  transformGeometry,
33
33
  type AlignmentGeometry,
34
34
  } from './alignment-geometry.js';
35
- import {cached, cachedArtifact} from './cached.js';
35
+ import {cache, cachedArtifact} from './cached.js';
36
36
  import {extrudeWithTopology} from './extrude.js';
37
37
  import {font, googleFont, type Font} from './font.js';
38
38
  import {
@@ -171,6 +171,26 @@ export type ElementSnapshot = Readonly<{
171
171
  TopologySelection;
172
172
  }>;
173
173
 
174
+ /** A finite measurement in the common frame solved at the time of the call. */
175
+ export type DistanceSnapshot = Readonly<{
176
+ value: number;
177
+ start: Vec3;
178
+ end: Vec3;
179
+ axis?: Vec3;
180
+ axisName?: 'x' | 'y' | 'z';
181
+ operands: readonly Readonly<{
182
+ nodeId: string;
183
+ elements: readonly ElementSnapshot[];
184
+ }>[];
185
+ placements: readonly Readonly<{nodeId: string; transform: Transform}>[];
186
+ }>;
187
+
188
+ type DistanceObserver = (
189
+ snapshot: DistanceSnapshot,
190
+ objects: readonly RelationObject[],
191
+ ) => void;
192
+ let observeDistance: DistanceObserver | undefined;
193
+
174
194
  export type ConstraintAnchorSnapshot = Readonly<{
175
195
  nodeId: string;
176
196
  name: string;
@@ -406,6 +426,7 @@ type StoredElement = Readonly<{
406
426
  facing?: 1 | -1;
407
427
  direction?: 1 | -1;
408
428
  topology?: StoredTopology;
429
+ parts?: readonly StoredTopology[];
409
430
  members?: StoredElements;
410
431
  }>;
411
432
 
@@ -610,11 +631,22 @@ let operationTraces = new WeakMap<StoredOperation, OperationTrace>();
610
631
  * Finish in finally after snapshotting to retain this evaluation's kernel work.
611
632
  * An optional cancellation check may throw before any kernel operation starts;
612
633
  * complete results survive cancellation, and cleanup removes the check.
634
+ * The optional distance observer receives call-time geometry snapshots; cleanup
635
+ * also releases its evaluation scope, while distance() still returns a number.
613
636
  */
614
- export function beginModelEvaluation(checkCancelled?: () => void): () => void {
637
+ export function beginModelEvaluation(
638
+ checkCancelled?: () => void,
639
+ distanceObserver?: DistanceObserver,
640
+ ): () => void {
615
641
  valueTraces = new WeakMap();
616
642
  operationTraces = new WeakMap();
617
- return beginKernelOperationEvaluation(checkCancelled);
643
+ const previous = observeDistance;
644
+ observeDistance = distanceObserver;
645
+ const finish = beginKernelOperationEvaluation(checkCancelled);
646
+ return () => {
647
+ observeDistance = previous;
648
+ finish();
649
+ };
618
650
  }
619
651
 
620
652
  function valueTrace(value: object): ValueTrace {
@@ -741,6 +773,9 @@ export interface FaceAnchor extends Anchor<'face'> {
741
773
  flip(): this;
742
774
  }
743
775
 
776
+ /** Fixed solve-frame direction, or the solved direction of a straight edge/axis. */
777
+ export type DistanceAxis = 'x' | 'y' | 'z' | Vec3 | LineAnchor;
778
+
744
779
  const boundKind = Symbol('boundKind');
745
780
  export interface Bound extends FaceAnchor {
746
781
  readonly [boundKind]: true;
@@ -805,7 +840,7 @@ export type ExposedValue<Value> = Value extends {
805
840
  : Kind extends 'vertex'
806
841
  ? Vertex
807
842
  : Kind extends 'group'
808
- ? Anchor<'frame'>
843
+ ? Anchor<'frame'> & DirectionalBounds
809
844
  : Anchor) &
810
845
  Elements
811
846
  : Value extends Anchor
@@ -844,6 +879,13 @@ export type ModelForKind<
844
879
  ? GroupModel<Elements>
845
880
  : never;
846
881
 
882
+ /** Tight axis-aligned bounds in the selected model coordinate frame. */
883
+ export type ModelBounds = Readonly<{
884
+ minimum: Vec3;
885
+ maximum: Vec3;
886
+ size: Vec3;
887
+ }>;
888
+
847
889
  export interface ModelCapabilities<
848
890
  Elements extends NamedElements,
849
891
  Kind extends ModelKind,
@@ -851,6 +893,10 @@ export interface ModelCapabilities<
851
893
  extends Anchor<ModelElementKind<Kind>>, DirectionalBounds {
852
894
  readonly [modelKind]: Kind;
853
895
  readonly [modelNamedElements]: Elements;
896
+ /** Measure finite geometry in this model's local frame, or in relativeTo's frame. */
897
+ bounds(relativeTo?: Model): ModelBounds;
898
+ /** Model-origin coordinates in relativeTo's local frame, including placement. */
899
+ position(relativeTo: Model): Vec3;
854
900
  relate(
855
901
  build: (
856
902
  self: ModelForKind<Elements, Kind>,
@@ -1470,10 +1516,13 @@ function rotationPointReference(point: PointAnchor): AnchorReference {
1470
1516
  return reference;
1471
1517
  }
1472
1518
 
1473
- function rotationAxisReference(axis: LineAnchor): AnchorReference {
1519
+ function straightAxisReference(
1520
+ axis: LineAnchor,
1521
+ operation = 'axisLine()',
1522
+ ): AnchorReference {
1474
1523
  const reference = anchorReference(axis);
1475
1524
  if (reference.kind !== 'line')
1476
- throw new Error('axisLine() requires an axis or straight edge.');
1525
+ throw new Error(`${operation} requires an axis or straight edge.`);
1477
1526
  const geometry =
1478
1527
  reference.topology?.source[modelGeometry]()?.value ??
1479
1528
  (reference.whole && isModelObject(reference.model)
@@ -1490,7 +1539,7 @@ function rotationAxisReference(axis: LineAnchor): AnchorReference {
1490
1539
  : (geometry.shape as ReplicadEdge).geomType === 'LINE';
1491
1540
  if (!straight)
1492
1541
  throw new Error(
1493
- 'axisLine() requires a straight axis; curved edges do not define one rotation axis.',
1542
+ `${operation} requires a straight axis; curved edges do not define one axis.`,
1494
1543
  );
1495
1544
  }
1496
1545
  return reference;
@@ -1652,7 +1701,7 @@ export function axisEdge(id: EdgeId): AxisChain {
1652
1701
  export function axisLine(axis: LineAnchor): AxisChain {
1653
1702
  return new AxisChain(new TransformationRotation(), {
1654
1703
  kind: 'axisLine',
1655
- axis: rotationAxisReference(axis),
1704
+ axis: straightAxisReference(axis),
1656
1705
  });
1657
1706
  }
1658
1707
 
@@ -2821,39 +2870,78 @@ export class ModelObject<
2821
2870
  >;
2822
2871
  }
2823
2872
 
2824
- /** Resolve both own and occurrence references into this model's local frame. */
2825
- private localElements(sources: readonly Anchor[]): StoredElement[] {
2873
+ /**
2874
+ * Measure finite geometry in this model's local frame, or in relativeTo's frame.
2875
+ * An explicit reference includes solved placement and nested group occurrences.
2876
+ * Empty groups have no finite bounds.
2877
+ */
2878
+ bounds(relativeTo?: Model): ModelBounds {
2879
+ const target =
2880
+ relativeTo === undefined
2881
+ ? this
2882
+ : requireModelObject(relativeTo, 'bounds requires a reference model.');
2883
+ const transform = target.localFrames([this])[0];
2884
+ const [minimum, maximum] = this[referenceBounds](
2885
+ this.relationAnchorReference(),
2886
+ transform,
2887
+ );
2888
+ return {
2889
+ minimum,
2890
+ maximum,
2891
+ size: [
2892
+ maximum[0] - minimum[0],
2893
+ maximum[1] - minimum[1],
2894
+ maximum[2] - minimum[2],
2895
+ ],
2896
+ };
2897
+ }
2898
+
2899
+ /**
2900
+ * Model-origin coordinates in relativeTo's local frame, including placement.
2901
+ * This model's origin in its own frame is always [0, 0, 0], even for point models.
2902
+ */
2903
+ position(relativeTo: Model): Vec3 {
2904
+ const target = requireModelObject(
2905
+ relativeTo,
2906
+ 'position requires a reference model.',
2907
+ );
2908
+ return target.localFrames([this])[0].position;
2909
+ }
2910
+
2911
+ /** Resolve source frames without losing nested occurrence positions. */
2912
+ private localFrames(sources: readonly RelationObject[]): RigidTransform[] {
2826
2913
  const members = this.memberPoses();
2827
- const external = sources
2828
- .map(source => anchorReference(source).model)
2829
- .filter(model => !members.has(model));
2914
+ const external = sources.filter(model => !members.has(model));
2830
2915
  const context = external.length
2831
2916
  ? ModelObject.createSolveContext([this, ...external])
2832
2917
  : undefined;
2833
2918
  return sources.map(source => {
2834
- const reference = anchorReference(source);
2835
- const {
2836
- kind,
2837
- transform: frame,
2838
- topology,
2839
- members: nested,
2840
- bound,
2841
- facing,
2842
- } = source instanceof ModelObject ? source.exposedElement() : reference;
2843
- const memberPose = members.get(reference.model);
2919
+ const memberPose = members.get(source);
2844
2920
  if (memberPose === null)
2845
2921
  throw new Error(
2846
2922
  "The point or element belongs to multiple occurrences. Expose it through the intended child model's named reference.",
2847
2923
  );
2848
- const transform =
2924
+ return (
2849
2925
  memberPose ??
2850
- relativeTransform(
2851
- reference.model.solvePose(context!),
2852
- this.solvePose(context!),
2853
- );
2926
+ relativeTransform(source.solvePose(context!), this.solvePose(context!))
2927
+ );
2928
+ });
2929
+ }
2930
+
2931
+ /** Resolve both own and occurrence references into this model's local frame. */
2932
+ private localElements(sources: readonly Anchor[]): StoredElement[] {
2933
+ const references = sources.map(anchorReference);
2934
+ const transforms = this.localFrames(
2935
+ references.map(reference => reference.model),
2936
+ );
2937
+ return sources.map((source, index) => {
2938
+ const {kind, transform, topology, parts, members, bound, facing} =
2939
+ source instanceof ModelObject
2940
+ ? source.exposedElement()
2941
+ : references[index];
2854
2942
  return transformElement(
2855
- {kind, transform: frame, topology, members: nested, bound, facing},
2856
- transform,
2943
+ {kind, transform, topology, parts, members, bound, facing},
2944
+ transforms[index],
2857
2945
  );
2858
2946
  });
2859
2947
  }
@@ -2901,7 +2989,11 @@ export class ModelObject<
2901
2989
  /** @internal */
2902
2990
  exposedElement(): StoredElement {
2903
2991
  if (this.kind === 'group')
2904
- return {...this.geometryAnchor, members: this.elements};
2992
+ return {
2993
+ ...this.geometryAnchor,
2994
+ parts: this.geometryParts(),
2995
+ members: this.elements,
2996
+ };
2905
2997
  const geometry = this.requireGeometry().value;
2906
2998
  const kind = (
2907
2999
  this.kind === 'face' ? 'surface' : this.kind
@@ -3183,7 +3275,7 @@ export class ModelObject<
3183
3275
  {
3184
3276
  shape: source.value.shape,
3185
3277
  topology: source.value.topology,
3186
- index: 1,
3278
+ namespace: 1,
3187
3279
  },
3188
3280
  direction,
3189
3281
  ),
@@ -3661,12 +3753,12 @@ export class ModelObject<
3661
3753
  {
3662
3754
  shape: geometry.value.shape,
3663
3755
  topology: geometry.value.topology,
3664
- index: otherIndex === 0 ? 1 : undefined,
3756
+ namespace: otherIndex === 0 ? 1 : 'intermediate',
3665
3757
  },
3666
3758
  {
3667
3759
  shape: operand.value,
3668
3760
  topology: otherGeometry.value.topology,
3669
- index: otherIndex + 2,
3761
+ namespace: otherIndex + 2,
3670
3762
  },
3671
3763
  operation,
3672
3764
  );
@@ -3720,6 +3812,226 @@ export class ModelObject<
3720
3812
  return members;
3721
3813
  }
3722
3814
 
3815
+ /** Finite members in this value's saved local assembly, without re-solving children. */
3816
+ private geometryParts(transform = identityRigidTransform): StoredTopology[] {
3817
+ if (this.geometry)
3818
+ return [{source: this, selection: {kind: 'solid'}, transform, scale: 1}];
3819
+ return this.children.flatMap(child =>
3820
+ child.geometryParts(
3821
+ composeTransforms(transform, child.solvePose(this.assembly!)),
3822
+ ),
3823
+ );
3824
+ }
3825
+
3826
+ /** @internal */
3827
+ static distance(a: Anchor, b: Anchor, axis?: DistanceAxis): number {
3828
+ const left = anchorReference(a),
3829
+ right = anchorReference(b);
3830
+ const axisReference =
3831
+ axis !== undefined && typeof axis !== 'string' && !Array.isArray(axis)
3832
+ ? straightAxisReference(axis as LineAnchor, 'distance() axis')
3833
+ : undefined;
3834
+ const context = ModelObject.createSolveContext([
3835
+ left.model,
3836
+ right.model,
3837
+ ...(axisReference ? [axisReference.model] : []),
3838
+ ]);
3839
+ const topologyParts = (reference: AnchorReference) =>
3840
+ reference.topology
3841
+ ? [reference.topology]
3842
+ : (reference.parts ??
3843
+ (reference.whole && reference.model instanceof ModelObject
3844
+ ? reference.model.geometryParts()
3845
+ : undefined));
3846
+ const parts = (reference: AnchorReference): DistancePart[] => {
3847
+ const pose = reference.model.solvePose(context);
3848
+ const frame = composeTransforms(pose, reference.transform);
3849
+ if (reference.kind === 'point') return [{points: [frame.position]}];
3850
+ const topology = topologyParts(reference);
3851
+ if (topology)
3852
+ return topology.map(part => ({
3853
+ geometry: part.source.requireGeometry(),
3854
+ selection: part.selection,
3855
+ scale: part.scale,
3856
+ transform: composeTransforms(pose, part.transform),
3857
+ }));
3858
+ if (reference.bound) {
3859
+ const [x, z] = reference.bound.size;
3860
+ const corners: Vec3[] =
3861
+ x === 0 && z === 0
3862
+ ? [origin]
3863
+ : x === 0
3864
+ ? [
3865
+ [0, 0, -z / 2],
3866
+ [0, 0, z / 2],
3867
+ ]
3868
+ : z === 0
3869
+ ? [
3870
+ [-x / 2, 0, 0],
3871
+ [x / 2, 0, 0],
3872
+ ]
3873
+ : [
3874
+ [-x / 2, 0, -z / 2],
3875
+ [x / 2, 0, -z / 2],
3876
+ [x / 2, 0, z / 2],
3877
+ [-x / 2, 0, z / 2],
3878
+ ];
3879
+ return [
3880
+ {
3881
+ points: corners.map(
3882
+ point => composeTransforms(frame, translation(point)).position,
3883
+ ),
3884
+ },
3885
+ ];
3886
+ }
3887
+ throw new Error(
3888
+ 'distance() requires finite geometry or a point; select an edge or face instead of an infinite axis or plane.',
3889
+ );
3890
+ };
3891
+ const first = parts(left),
3892
+ second = parts(right);
3893
+ if (!first.length || !second.length)
3894
+ throw new Error('distance() cannot measure an empty group.');
3895
+ const record = (result: DistanceResult, direction?: Vec3): number => {
3896
+ if (observeDistance) {
3897
+ const operands = [left, right].map(reference => {
3898
+ const topology = topologyParts(reference);
3899
+ const elements =
3900
+ topology && reference.kind !== 'point'
3901
+ ? topology.map(
3902
+ part =>
3903
+ snapshotElements({
3904
+ [reference.name]: {
3905
+ ...reference,
3906
+ topology: part,
3907
+ members: undefined,
3908
+ },
3909
+ })[0],
3910
+ )
3911
+ : [
3912
+ snapshotElements({
3913
+ [reference.name]: {...reference, members: undefined},
3914
+ })[0],
3915
+ ];
3916
+ return {nodeId: reference.model.nodeId, elements};
3917
+ });
3918
+ observeDistance(
3919
+ {
3920
+ ...result,
3921
+ axis: direction,
3922
+ axisName: typeof axis === 'string' ? axis : undefined,
3923
+ operands,
3924
+ placements: [...context.poses].map(([model, pose]) => ({
3925
+ nodeId: model.nodeId,
3926
+ transform: toTransform(pose),
3927
+ })),
3928
+ },
3929
+ [...context.poses.keys()],
3930
+ );
3931
+ }
3932
+ return result.value;
3933
+ };
3934
+ if (axis === undefined) {
3935
+ let result: DistanceResult | undefined;
3936
+ for (const a of first)
3937
+ for (const b of second) {
3938
+ const candidate =
3939
+ 'points' in a &&
3940
+ a.points.length === 1 &&
3941
+ 'points' in b &&
3942
+ b.points.length === 1
3943
+ ? {
3944
+ value: Math.hypot(
3945
+ ...a.points[0].map((value, i) => value - b.points[0][i]),
3946
+ ),
3947
+ start: a.points[0],
3948
+ end: b.points[0],
3949
+ }
3950
+ : distanceBetweenParts(a, b).value;
3951
+ if (!result || candidate.value < result.value) result = candidate;
3952
+ if (result.value === 0) return record(result);
3953
+ }
3954
+ return record(result!);
3955
+ }
3956
+ const direction = axisReference
3957
+ ? rotateVector(
3958
+ [0, 1, 0],
3959
+ composeTransforms(
3960
+ axisReference.model.solvePose(context),
3961
+ axisReference.transform,
3962
+ ).quaternion,
3963
+ )
3964
+ : typeof axis === 'string'
3965
+ ? distanceAxes[axis]
3966
+ : (axis as Vec3);
3967
+ assertFiniteVector('distance() axis', direction);
3968
+ const magnitude = Math.hypot(...direction);
3969
+ if (magnitude === 0) throw new Error('distance() axis must be non-zero.');
3970
+ const normalized = direction.map(
3971
+ value => value / magnitude,
3972
+ ) as unknown as Vec3;
3973
+ const project = invertTransform(frameFromYAxis(origin, normalized));
3974
+ const projectedBounds = (parts: DistancePart[]): LocalBounds => {
3975
+ const bounds = parts.map(part => {
3976
+ if ('points' in part)
3977
+ return pointBounds(
3978
+ part.points.map(
3979
+ point => composeTransforms(project, translation(point)).position,
3980
+ ),
3981
+ );
3982
+ return evaluateSnapshotQuery(
3983
+ boundsQuery(
3984
+ part.geometry,
3985
+ composeTransforms(project, part.transform),
3986
+ part.selection,
3987
+ part.scale,
3988
+ ),
3989
+ ) as LocalBounds;
3990
+ });
3991
+ return [
3992
+ [0, 1, 2].map(i =>
3993
+ Math.min(...bounds.map(value => value[0][i])),
3994
+ ) as unknown as Vec3,
3995
+ [0, 1, 2].map(i =>
3996
+ Math.max(...bounds.map(value => value[1][i])),
3997
+ ) as unknown as Vec3,
3998
+ ];
3999
+ };
4000
+ const aBounds = projectedBounds(first),
4001
+ bBounds = projectedBounds(second);
4002
+ const [aMin, aMax] = [aBounds[0][1], aBounds[1][1]],
4003
+ [bMin, bMax] = [bBounds[0][1], bBounds[1][1]];
4004
+ const overlap = (Math.max(aMin, bMin) + Math.min(aMax, bMax)) / 2;
4005
+ const [start, end] =
4006
+ bMin > aMax
4007
+ ? [aMax, bMin]
4008
+ : aMin > bMax
4009
+ ? [aMin, bMax]
4010
+ : [overlap, overlap];
4011
+ // Place the axis-parallel dimension within the shared transverse bounds.
4012
+ // A point inside the other operand's projection then anchors the line at
4013
+ // that point. Disjoint bounds use the midpoint of their nearest limits;
4014
+ // projected endpoints remain reference positions for general geometry.
4015
+ const center = [0, 1, 2].map(
4016
+ i =>
4017
+ (Math.max(aBounds[0][i], bBounds[0][i]) +
4018
+ Math.min(aBounds[1][i], bBounds[1][i])) /
4019
+ 2,
4020
+ );
4021
+ const unproject = invertTransform(project);
4022
+ const at = (y: number) =>
4023
+ composeTransforms(unproject, translation([center[0], y, center[2]]))
4024
+ .position;
4025
+ return record(
4026
+ {
4027
+ value: Math.max(0, bMin - aMax, aMin - bMax),
4028
+ start: at(start),
4029
+ end: at(end),
4030
+ },
4031
+ normalized,
4032
+ );
4033
+ }
4034
+
3723
4035
  /** Bounds of the selected finite geometry after a rigid transform. */
3724
4036
  private [referenceBoundsParts](
3725
4037
  reference: StoredAnchor,
@@ -3754,6 +4066,15 @@ export class ModelObject<
3754
4066
  ),
3755
4067
  ];
3756
4068
  }
4069
+ if (reference.parts)
4070
+ return reference.parts.map(part =>
4071
+ boundsQuery(
4072
+ part.source.requireGeometry(),
4073
+ composeTransforms(transform, part.transform),
4074
+ part.selection,
4075
+ part.scale,
4076
+ ),
4077
+ );
3757
4078
  return [{bounds: super[referenceBounds](reference, transform)}];
3758
4079
  }
3759
4080
 
@@ -3856,7 +4177,7 @@ export class ModelObject<
3856
4177
  }
3857
4178
 
3858
4179
  protected override edgeReference(id: EdgeId): RelationReference {
3859
- return {...rotationAxisReference(this.edge(id)), model: undefined};
4180
+ return {...straightAxisReference(this.edge(id)), model: undefined};
3860
4181
  }
3861
4182
 
3862
4183
  protected override vertexPosition(id: VertexId): Vec3 {
@@ -4572,6 +4893,16 @@ export function group(children: readonly Model[], name = 'Group'): GroupModel {
4572
4893
  }) as unknown as GroupModel;
4573
4894
  }
4574
4895
 
4896
+ /**
4897
+ * Measure finite models, topology, bounds or point references in their solved placement.
4898
+ * Without axis, returns the shortest geometric distance. With axis, returns the
4899
+ * gap between projected intervals (zero when they overlap). The result is a
4900
+ * non-negative number computed now; later relations do not update it.
4901
+ */
4902
+ export function distance(a: Anchor, b: Anchor, axis?: DistanceAxis): number {
4903
+ return ModelObject.distance(a, b, axis);
4904
+ }
4905
+
4575
4906
  export function union(operands: readonly SolidModel<{}>[]): SolidModel {
4576
4907
  const {first, others} = booleanOperands('union', operands);
4577
4908
  return first[combineModels]('fuse', others);
@@ -4755,6 +5086,106 @@ type SnapshotQueryInput = {
4755
5086
  query: SnapshotQuery;
4756
5087
  };
4757
5088
 
5089
+ type DistancePart =
5090
+ | Readonly<{points: readonly Vec3[]}>
5091
+ | Readonly<{
5092
+ geometry: ModelGeometry;
5093
+ selection: TopologySelection;
5094
+ transform: RigidTransform;
5095
+ scale: number;
5096
+ }>;
5097
+
5098
+ type DistanceResult = Readonly<{value: number; start: Vec3; end: Vec3}>;
5099
+
5100
+ const distanceAxes = {x: [1, 0, 0], y: [0, 1, 0], z: [0, 0, 1]} as const;
5101
+
5102
+ function withDistanceShape<T>(
5103
+ part: DistancePart,
5104
+ visit: (shape: AnyShape) => T,
5105
+ ): T {
5106
+ if (!('points' in part))
5107
+ return withTransformedGeometry(part.geometry.value, part, visit);
5108
+ const points = part.points;
5109
+ if (points.length <= 2) {
5110
+ const shape =
5111
+ points.length === 1
5112
+ ? makeVertex(toPoint(points[0]))
5113
+ : makeLine(toPoint(points[0]), toPoint(points[1]));
5114
+ try {
5115
+ return visit(shape);
5116
+ } finally {
5117
+ shape.delete();
5118
+ }
5119
+ }
5120
+ const edges: ReplicadEdge[] = [];
5121
+ let wire: ReplicadWire | undefined;
5122
+ let face: ReplicadFace | undefined;
5123
+ try {
5124
+ points.forEach((point, index) =>
5125
+ edges.push(
5126
+ makeLine(toPoint(point), toPoint(points[(index + 1) % points.length])),
5127
+ ),
5128
+ );
5129
+ wire = assembleWire(edges);
5130
+ face = makeFace(wire);
5131
+ return visit(face);
5132
+ } finally {
5133
+ face?.delete();
5134
+ wire?.delete();
5135
+ edges.forEach(edge => edge.delete());
5136
+ }
5137
+ }
5138
+
5139
+ const distanceBetweenParts = cachedArtifact(
5140
+ (a: DistancePart, b: DistancePart): DistanceResult =>
5141
+ withDistanceShape(a, left =>
5142
+ withDistanceShape(b, right => {
5143
+ const query = new (getOC().BRepExtrema_DistShapeShape)();
5144
+ try {
5145
+ query.LoadS1(left.wrapped);
5146
+ query.LoadS2(right.wrapped);
5147
+ query.Perform();
5148
+ if (!query.IsDone())
5149
+ throw new Error('distance() could not measure these geometries.');
5150
+ const first = query.PointOnShape1(1),
5151
+ second = query.PointOnShape2(1);
5152
+ try {
5153
+ return {
5154
+ value: query.Value(),
5155
+ start: [first.X(), first.Y(), first.Z()],
5156
+ end: [second.X(), second.Y(), second.Z()],
5157
+ };
5158
+ } finally {
5159
+ first.delete();
5160
+ second.delete();
5161
+ }
5162
+ } finally {
5163
+ query.delete();
5164
+ }
5165
+ }),
5166
+ ),
5167
+ {
5168
+ key: (a, b) =>
5169
+ kernelOperationKey(
5170
+ 'distance-witnesses',
5171
+ [a, b].map(part =>
5172
+ 'points' in part
5173
+ ? ['points', part.points]
5174
+ : [
5175
+ 'shape',
5176
+ part.geometry.id,
5177
+ part.selection.kind,
5178
+ part.selection.kind === 'solid' ? null : part.selection.id,
5179
+ part.transform.position,
5180
+ part.transform.quaternion,
5181
+ part.scale,
5182
+ ],
5183
+ ),
5184
+ [a, b].flatMap(part => ('points' in part ? [] : [part.geometry])),
5185
+ ),
5186
+ },
5187
+ );
5188
+
4758
5189
  function boundsQuery(
4759
5190
  geometry: ModelGeometry,
4760
5191
  transform: RigidTransform,
@@ -4851,7 +5282,11 @@ export type SnapshotQueryBatch = Readonly<{
4851
5282
  weight: number;
4852
5283
  sourceRef?: SourceRef;
4853
5284
  encode(): Uint8Array;
4854
- accept(query: SnapshotQuery, value: SnapshotQueryResult): void;
5285
+ accept(
5286
+ query: SnapshotQuery,
5287
+ value: SnapshotQueryResult,
5288
+ milliseconds: number,
5289
+ ): void;
4855
5290
  }>;
4856
5291
 
4857
5292
  export function planModelSnapshotQueries(
@@ -4907,7 +5342,8 @@ export function planModelSnapshotQueries(
4907
5342
  (input.geometry.topology?.surfaces.ids.length ?? 0)) *
4908
5343
  queries.length,
4909
5344
  encode: () => encodeKernelArtifact(id, input.geometry),
4910
- accept: (query, value) => snapshotQuery.accept(query.key, value),
5345
+ accept: (query, value, milliseconds) =>
5346
+ snapshotQuery.accept(query.key, value, milliseconds),
4911
5347
  }));
4912
5348
  }
4913
5349
 
@@ -4917,7 +5353,11 @@ export function executeSnapshotQueryBatch(
4917
5353
  bytes: Uint8Array,
4918
5354
  queries: readonly SnapshotQuery[],
4919
5355
  checkCancelled: () => void,
4920
- onResult: (query: SnapshotQuery, value: SnapshotQueryResult) => void,
5356
+ onResult: (
5357
+ query: SnapshotQuery,
5358
+ value: SnapshotQueryResult,
5359
+ milliseconds: number,
5360
+ ) => void,
4921
5361
  onRestore?: (milliseconds: number) => void,
4922
5362
  ): void {
4923
5363
  const started = performance.now();
@@ -4929,7 +5369,9 @@ export function executeSnapshotQueryBatch(
4929
5369
  onRestore?.(performance.now() - started);
4930
5370
  for (const query of queries) {
4931
5371
  checkCancelled();
4932
- onResult(query, computeSnapshotQuery(geometry, query));
5372
+ const started = performance.now();
5373
+ const value = computeSnapshotQuery(geometry, query);
5374
+ onResult(query, value, performance.now() - started);
4933
5375
  }
4934
5376
  } finally {
4935
5377
  geometry.shape.delete();
@@ -5012,7 +5454,7 @@ export const authoringApi = Object.freeze({
5012
5454
  pivotPoint,
5013
5455
  axisEdge,
5014
5456
  axisLine,
5015
- cached,
5457
+ cache,
5016
5458
  font,
5017
5459
  googleFont,
5018
5460
  text,
@@ -5036,6 +5478,7 @@ export const authoringApi = Object.freeze({
5036
5478
  frustum,
5037
5479
  regularPrism,
5038
5480
  group,
5481
+ distance,
5039
5482
  union,
5040
5483
  cut,
5041
5484
  intersect,
@@ -5450,7 +5893,7 @@ function buildLoftGeometry(
5450
5893
  const inputs: {
5451
5894
  shape: ReplicadFace;
5452
5895
  topology: ShapeTopology;
5453
- index: number;
5896
+ namespace: number;
5454
5897
  }[] = [];
5455
5898
  let spineWire: ReplicadWire | undefined;
5456
5899
  try {
@@ -5461,7 +5904,7 @@ function buildLoftGeometry(
5461
5904
  section.transform,
5462
5905
  ),
5463
5906
  topology: section.geometry.value.topology,
5464
- index: index + 1,
5907
+ namespace: index + 1,
5465
5908
  });
5466
5909
  }
5467
5910
  if (spine) {
@@ -5560,6 +6003,19 @@ function computeTransformedBounds(
5560
6003
  geometry: SnapshotQueryInput['geometry'],
5561
6004
  query: Extract<SnapshotQuery, {kind: 'bounds'}>,
5562
6005
  ): LocalBounds {
6006
+ return withTransformedGeometry(geometry, query, shapeBounds);
6007
+ }
6008
+
6009
+ /** Borrow the input, and release all selected/transformed handles after the query. */
6010
+ function withTransformedGeometry<T>(
6011
+ geometry: SnapshotQueryInput['geometry'],
6012
+ query: Readonly<{
6013
+ transform: RigidTransform;
6014
+ selection: TopologySelection;
6015
+ scale: number;
6016
+ }>,
6017
+ visit: (shape: AnyShape) => T,
6018
+ ): T {
5563
6019
  const {transform, selection, scale} = query;
5564
6020
  return withTopologyShape(
5565
6021
  geometry.shape,
@@ -5570,7 +6026,7 @@ function computeTransformedBounds(
5570
6026
  try {
5571
6027
  const moved = shapeWithTransform(scaled, transform);
5572
6028
  try {
5573
- return shapeBounds(moved);
6029
+ return visit(moved);
5574
6030
  } finally {
5575
6031
  moved.delete();
5576
6032
  }
@@ -5706,6 +6162,10 @@ function transformElement(
5706
6162
  transform: composeTransforms(transform, element.topology.transform),
5707
6163
  }
5708
6164
  : undefined,
6165
+ parts: element.parts?.map(part => ({
6166
+ ...part,
6167
+ transform: composeTransforms(transform, part.transform),
6168
+ })),
5709
6169
  members: element.members
5710
6170
  ? Object.fromEntries(
5711
6171
  Object.entries(element.members).map(([name, member]) => [
@@ -5736,6 +6196,11 @@ function scaleElement(element: StoredElement, factor: number): StoredElement {
5736
6196
  transform: scaleFrame(element.topology.transform, factor),
5737
6197
  }
5738
6198
  : undefined,
6199
+ parts: element.parts?.map(part => ({
6200
+ ...part,
6201
+ scale: part.scale * factor,
6202
+ transform: scaleFrame(part.transform, factor),
6203
+ })),
5739
6204
  members: element.members
5740
6205
  ? scaleElements(element.members, factor)
5741
6206
  : undefined,