@code3d/core 0.0.1-alpha.7 → 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 +42 -1
- package/bld/chunks/{chunk-TCMEEKWL.js → chunk-EWPRWRLV.js} +2 -2
- package/bld/chunks/{chunk-VHSPHJ5F.js → chunk-HSU2FH3L.js} +3 -3
- package/bld/chunks/{chunk-4WLVDYUJ.js → chunk-MICNLFBF.js} +401 -76
- package/bld/chunks/chunk-MICNLFBF.js.map +7 -0
- package/bld/chunks/{chunk-C3AJ6WNB.js → chunk-OAVABPL5.js} +2 -2
- package/bld/library/index.d.ts +2 -2
- package/bld/library/index.d.ts.map +1 -1
- package/bld/library/index.js +3 -1
- package/bld/library/loft.d.ts.map +1 -1
- package/bld/library/replicad.js +2 -2
- package/bld/library/runtime.d.ts +57 -2
- 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 +5 -3
- package/bld/node/replicad.js +4 -4
- package/bld/tooling/index.d.ts +1 -1
- package/bld/tooling/index.d.ts.map +1 -1
- package/bld/tooling/index.js +2 -2
- package/package.json +1 -1
- package/src/library/index.ts +3 -0
- package/src/library/loft.ts +5 -1
- package/src/library/runtime.ts +491 -37
- package/src/library/shell.ts +3 -3
- package/src/library/topology.ts +24 -15
- package/src/tooling/index.ts +1 -0
- package/bld/chunks/chunk-4WLVDYUJ.js.map +0 -7
- /package/bld/chunks/{chunk-TCMEEKWL.js.map → chunk-EWPRWRLV.js.map} +0 -0
- /package/bld/chunks/{chunk-VHSPHJ5F.js.map → chunk-HSU2FH3L.js.map} +0 -0
- /package/bld/chunks/{chunk-C3AJ6WNB.js.map → chunk-OAVABPL5.js.map} +0 -0
package/src/library/runtime.ts
CHANGED
|
@@ -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,
|
|
@@ -5047,6 +5478,7 @@ export const authoringApi = Object.freeze({
|
|
|
5047
5478
|
frustum,
|
|
5048
5479
|
regularPrism,
|
|
5049
5480
|
group,
|
|
5481
|
+
distance,
|
|
5050
5482
|
union,
|
|
5051
5483
|
cut,
|
|
5052
5484
|
intersect,
|
|
@@ -5461,7 +5893,7 @@ function buildLoftGeometry(
|
|
|
5461
5893
|
const inputs: {
|
|
5462
5894
|
shape: ReplicadFace;
|
|
5463
5895
|
topology: ShapeTopology;
|
|
5464
|
-
|
|
5896
|
+
namespace: number;
|
|
5465
5897
|
}[] = [];
|
|
5466
5898
|
let spineWire: ReplicadWire | undefined;
|
|
5467
5899
|
try {
|
|
@@ -5472,7 +5904,7 @@ function buildLoftGeometry(
|
|
|
5472
5904
|
section.transform,
|
|
5473
5905
|
),
|
|
5474
5906
|
topology: section.geometry.value.topology,
|
|
5475
|
-
|
|
5907
|
+
namespace: index + 1,
|
|
5476
5908
|
});
|
|
5477
5909
|
}
|
|
5478
5910
|
if (spine) {
|
|
@@ -5571,6 +6003,19 @@ function computeTransformedBounds(
|
|
|
5571
6003
|
geometry: SnapshotQueryInput['geometry'],
|
|
5572
6004
|
query: Extract<SnapshotQuery, {kind: 'bounds'}>,
|
|
5573
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 {
|
|
5574
6019
|
const {transform, selection, scale} = query;
|
|
5575
6020
|
return withTopologyShape(
|
|
5576
6021
|
geometry.shape,
|
|
@@ -5581,7 +6026,7 @@ function computeTransformedBounds(
|
|
|
5581
6026
|
try {
|
|
5582
6027
|
const moved = shapeWithTransform(scaled, transform);
|
|
5583
6028
|
try {
|
|
5584
|
-
return
|
|
6029
|
+
return visit(moved);
|
|
5585
6030
|
} finally {
|
|
5586
6031
|
moved.delete();
|
|
5587
6032
|
}
|
|
@@ -5717,6 +6162,10 @@ function transformElement(
|
|
|
5717
6162
|
transform: composeTransforms(transform, element.topology.transform),
|
|
5718
6163
|
}
|
|
5719
6164
|
: undefined,
|
|
6165
|
+
parts: element.parts?.map(part => ({
|
|
6166
|
+
...part,
|
|
6167
|
+
transform: composeTransforms(transform, part.transform),
|
|
6168
|
+
})),
|
|
5720
6169
|
members: element.members
|
|
5721
6170
|
? Object.fromEntries(
|
|
5722
6171
|
Object.entries(element.members).map(([name, member]) => [
|
|
@@ -5747,6 +6196,11 @@ function scaleElement(element: StoredElement, factor: number): StoredElement {
|
|
|
5747
6196
|
transform: scaleFrame(element.topology.transform, factor),
|
|
5748
6197
|
}
|
|
5749
6198
|
: undefined,
|
|
6199
|
+
parts: element.parts?.map(part => ({
|
|
6200
|
+
...part,
|
|
6201
|
+
scale: part.scale * factor,
|
|
6202
|
+
transform: scaleFrame(part.transform, factor),
|
|
6203
|
+
})),
|
|
5750
6204
|
members: element.members
|
|
5751
6205
|
? scaleElements(element.members, factor)
|
|
5752
6206
|
: undefined,
|