@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.
- package/README.md +53 -3
- package/bld/chunks/{chunk-3RIMOCPP.js → chunk-EWPRWRLV.js} +2 -2
- package/bld/chunks/{chunk-T6COR2SG.js → chunk-HSU2FH3L.js} +3 -3
- package/bld/chunks/{chunk-T3RAUNSF.js → chunk-MICNLFBF.js} +452 -107
- package/bld/chunks/chunk-MICNLFBF.js.map +7 -0
- package/bld/chunks/{chunk-KYW4AWKO.js → chunk-OAVABPL5.js} +2 -2
- package/bld/library/cached.d.ts +5 -3
- package/bld/library/cached.d.ts.map +1 -1
- package/bld/library/index.d.ts +4 -4
- package/bld/library/index.d.ts.map +1 -1
- package/bld/library/index.js +5 -3
- package/bld/library/kernel-cache.d.ts +5 -3
- package/bld/library/kernel-cache.d.ts.map +1 -1
- package/bld/library/loft.d.ts.map +1 -1
- package/bld/library/replicad.js +2 -2
- package/bld/library/runtime.d.ts +61 -6
- package/bld/library/runtime.d.ts.map +1 -1
- package/bld/library/topology.d.ts +4 -2
- package/bld/library/topology.d.ts.map +1 -1
- package/bld/node/index.js +7 -5
- package/bld/node/replicad.js +4 -4
- package/bld/tooling/index.d.ts +2 -2
- package/bld/tooling/index.d.ts.map +1 -1
- package/bld/tooling/index.js +4 -2
- package/package.json +1 -1
- package/src/library/cached.ts +29 -8
- package/src/library/index.ts +5 -2
- package/src/library/kernel-cache.ts +29 -5
- package/src/library/loft.ts +5 -1
- package/src/library/runtime.ts +508 -43
- package/src/library/shell.ts +3 -3
- package/src/library/topology.ts +24 -15
- package/src/tooling/index.ts +2 -0
- package/bld/chunks/chunk-T3RAUNSF.js.map +0 -7
- /package/bld/chunks/{chunk-3RIMOCPP.js.map → chunk-EWPRWRLV.js.map} +0 -0
- /package/bld/chunks/{chunk-T6COR2SG.js.map → chunk-HSU2FH3L.js.map} +0 -0
- /package/bld/chunks/{chunk-KYW4AWKO.js.map → chunk-OAVABPL5.js.map} +0 -0
package/src/library/runtime.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
transformGeometry,
|
|
33
33
|
type AlignmentGeometry,
|
|
34
34
|
} from './alignment-geometry.js';
|
|
35
|
-
import {
|
|
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(
|
|
637
|
+
export function beginModelEvaluation(
|
|
638
|
+
checkCancelled?: () => void,
|
|
639
|
+
distanceObserver?: DistanceObserver,
|
|
640
|
+
): () => void {
|
|
615
641
|
valueTraces = new WeakMap();
|
|
616
642
|
operationTraces = new WeakMap();
|
|
617
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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:
|
|
1704
|
+
axis: straightAxisReference(axis),
|
|
1656
1705
|
});
|
|
1657
1706
|
}
|
|
1658
1707
|
|
|
@@ -2821,39 +2870,78 @@ export class ModelObject<
|
|
|
2821
2870
|
>;
|
|
2822
2871
|
}
|
|
2823
2872
|
|
|
2824
|
-
/**
|
|
2825
|
-
|
|
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
|
|
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
|
-
|
|
2924
|
+
return (
|
|
2849
2925
|
memberPose ??
|
|
2850
|
-
relativeTransform(
|
|
2851
|
-
|
|
2852
|
-
|
|
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
|
|
2856
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
3756
|
+
namespace: otherIndex === 0 ? 1 : 'intermediate',
|
|
3665
3757
|
},
|
|
3666
3758
|
{
|
|
3667
3759
|
shape: operand.value,
|
|
3668
3760
|
topology: otherGeometry.value.topology,
|
|
3669
|
-
|
|
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 {...
|
|
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(
|
|
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) =>
|
|
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: (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|