@code3d/core 0.0.1-alpha.7 → 0.0.1-alpha.9
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-C3AJ6WNB.js → chunk-2NWM3BLX.js} +2 -2
- package/bld/chunks/{chunk-4WLVDYUJ.js → chunk-OALDEU3F.js} +405 -76
- package/bld/chunks/chunk-OALDEU3F.js.map +7 -0
- package/bld/chunks/{chunk-TCMEEKWL.js → chunk-SM7V45DO.js} +2 -2
- package/bld/chunks/{chunk-VHSPHJ5F.js → chunk-TAYVRBWE.js} +3 -3
- 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 +59 -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 +497 -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-C3AJ6WNB.js.map → chunk-2NWM3BLX.js.map} +0 -0
- /package/bld/chunks/{chunk-TCMEEKWL.js.map → chunk-SM7V45DO.js.map} +0 -0
- /package/bld/chunks/{chunk-VHSPHJ5F.js.map → chunk-TAYVRBWE.js.map} +0 -0
package/src/library/runtime.ts
CHANGED
|
@@ -171,6 +171,28 @@ 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
|
+
/** The entire model, rather than a named or topology reference. */
|
|
184
|
+
whole: boolean;
|
|
185
|
+
elements: readonly ElementSnapshot[];
|
|
186
|
+
}>[];
|
|
187
|
+
placements: readonly Readonly<{nodeId: string; transform: Transform}>[];
|
|
188
|
+
}>;
|
|
189
|
+
|
|
190
|
+
type DistanceObserver = (
|
|
191
|
+
snapshot: DistanceSnapshot,
|
|
192
|
+
objects: readonly RelationObject[],
|
|
193
|
+
) => void;
|
|
194
|
+
let observeDistance: DistanceObserver | undefined;
|
|
195
|
+
|
|
174
196
|
export type ConstraintAnchorSnapshot = Readonly<{
|
|
175
197
|
nodeId: string;
|
|
176
198
|
name: string;
|
|
@@ -406,6 +428,7 @@ type StoredElement = Readonly<{
|
|
|
406
428
|
facing?: 1 | -1;
|
|
407
429
|
direction?: 1 | -1;
|
|
408
430
|
topology?: StoredTopology;
|
|
431
|
+
parts?: readonly StoredTopology[];
|
|
409
432
|
members?: StoredElements;
|
|
410
433
|
}>;
|
|
411
434
|
|
|
@@ -610,11 +633,22 @@ let operationTraces = new WeakMap<StoredOperation, OperationTrace>();
|
|
|
610
633
|
* Finish in finally after snapshotting to retain this evaluation's kernel work.
|
|
611
634
|
* An optional cancellation check may throw before any kernel operation starts;
|
|
612
635
|
* complete results survive cancellation, and cleanup removes the check.
|
|
636
|
+
* The optional distance observer receives call-time geometry snapshots; cleanup
|
|
637
|
+
* also releases its evaluation scope, while distance() still returns a number.
|
|
613
638
|
*/
|
|
614
|
-
export function beginModelEvaluation(
|
|
639
|
+
export function beginModelEvaluation(
|
|
640
|
+
checkCancelled?: () => void,
|
|
641
|
+
distanceObserver?: DistanceObserver,
|
|
642
|
+
): () => void {
|
|
615
643
|
valueTraces = new WeakMap();
|
|
616
644
|
operationTraces = new WeakMap();
|
|
617
|
-
|
|
645
|
+
const previous = observeDistance;
|
|
646
|
+
observeDistance = distanceObserver;
|
|
647
|
+
const finish = beginKernelOperationEvaluation(checkCancelled);
|
|
648
|
+
return () => {
|
|
649
|
+
observeDistance = previous;
|
|
650
|
+
finish();
|
|
651
|
+
};
|
|
618
652
|
}
|
|
619
653
|
|
|
620
654
|
function valueTrace(value: object): ValueTrace {
|
|
@@ -741,6 +775,9 @@ export interface FaceAnchor extends Anchor<'face'> {
|
|
|
741
775
|
flip(): this;
|
|
742
776
|
}
|
|
743
777
|
|
|
778
|
+
/** Fixed solve-frame direction, or the solved direction of a straight edge/axis. */
|
|
779
|
+
export type DistanceAxis = 'x' | 'y' | 'z' | Vec3 | LineAnchor;
|
|
780
|
+
|
|
744
781
|
const boundKind = Symbol('boundKind');
|
|
745
782
|
export interface Bound extends FaceAnchor {
|
|
746
783
|
readonly [boundKind]: true;
|
|
@@ -805,7 +842,7 @@ export type ExposedValue<Value> = Value extends {
|
|
|
805
842
|
: Kind extends 'vertex'
|
|
806
843
|
? Vertex
|
|
807
844
|
: Kind extends 'group'
|
|
808
|
-
? Anchor<'frame'>
|
|
845
|
+
? Anchor<'frame'> & DirectionalBounds
|
|
809
846
|
: Anchor) &
|
|
810
847
|
Elements
|
|
811
848
|
: Value extends Anchor
|
|
@@ -844,6 +881,13 @@ export type ModelForKind<
|
|
|
844
881
|
? GroupModel<Elements>
|
|
845
882
|
: never;
|
|
846
883
|
|
|
884
|
+
/** Tight axis-aligned bounds in the selected model coordinate frame. */
|
|
885
|
+
export type ModelBounds = Readonly<{
|
|
886
|
+
minimum: Vec3;
|
|
887
|
+
maximum: Vec3;
|
|
888
|
+
size: Vec3;
|
|
889
|
+
}>;
|
|
890
|
+
|
|
847
891
|
export interface ModelCapabilities<
|
|
848
892
|
Elements extends NamedElements,
|
|
849
893
|
Kind extends ModelKind,
|
|
@@ -851,6 +895,10 @@ export interface ModelCapabilities<
|
|
|
851
895
|
extends Anchor<ModelElementKind<Kind>>, DirectionalBounds {
|
|
852
896
|
readonly [modelKind]: Kind;
|
|
853
897
|
readonly [modelNamedElements]: Elements;
|
|
898
|
+
/** Measure finite geometry in this model's local frame, or in relativeTo's frame. */
|
|
899
|
+
bounds(relativeTo?: Model): ModelBounds;
|
|
900
|
+
/** Model-origin coordinates in relativeTo's local frame, including placement. */
|
|
901
|
+
position(relativeTo: Model): Vec3;
|
|
854
902
|
relate(
|
|
855
903
|
build: (
|
|
856
904
|
self: ModelForKind<Elements, Kind>,
|
|
@@ -1470,10 +1518,13 @@ function rotationPointReference(point: PointAnchor): AnchorReference {
|
|
|
1470
1518
|
return reference;
|
|
1471
1519
|
}
|
|
1472
1520
|
|
|
1473
|
-
function
|
|
1521
|
+
function straightAxisReference(
|
|
1522
|
+
axis: LineAnchor,
|
|
1523
|
+
operation = 'axisLine()',
|
|
1524
|
+
): AnchorReference {
|
|
1474
1525
|
const reference = anchorReference(axis);
|
|
1475
1526
|
if (reference.kind !== 'line')
|
|
1476
|
-
throw new Error(
|
|
1527
|
+
throw new Error(`${operation} requires an axis or straight edge.`);
|
|
1477
1528
|
const geometry =
|
|
1478
1529
|
reference.topology?.source[modelGeometry]()?.value ??
|
|
1479
1530
|
(reference.whole && isModelObject(reference.model)
|
|
@@ -1490,7 +1541,7 @@ function rotationAxisReference(axis: LineAnchor): AnchorReference {
|
|
|
1490
1541
|
: (geometry.shape as ReplicadEdge).geomType === 'LINE';
|
|
1491
1542
|
if (!straight)
|
|
1492
1543
|
throw new Error(
|
|
1493
|
-
|
|
1544
|
+
`${operation} requires a straight axis; curved edges do not define one axis.`,
|
|
1494
1545
|
);
|
|
1495
1546
|
}
|
|
1496
1547
|
return reference;
|
|
@@ -1652,7 +1703,7 @@ export function axisEdge(id: EdgeId): AxisChain {
|
|
|
1652
1703
|
export function axisLine(axis: LineAnchor): AxisChain {
|
|
1653
1704
|
return new AxisChain(new TransformationRotation(), {
|
|
1654
1705
|
kind: 'axisLine',
|
|
1655
|
-
axis:
|
|
1706
|
+
axis: straightAxisReference(axis),
|
|
1656
1707
|
});
|
|
1657
1708
|
}
|
|
1658
1709
|
|
|
@@ -2821,39 +2872,78 @@ export class ModelObject<
|
|
|
2821
2872
|
>;
|
|
2822
2873
|
}
|
|
2823
2874
|
|
|
2824
|
-
/**
|
|
2825
|
-
|
|
2875
|
+
/**
|
|
2876
|
+
* Measure finite geometry in this model's local frame, or in relativeTo's frame.
|
|
2877
|
+
* An explicit reference includes solved placement and nested group occurrences.
|
|
2878
|
+
* Empty groups have no finite bounds.
|
|
2879
|
+
*/
|
|
2880
|
+
bounds(relativeTo?: Model): ModelBounds {
|
|
2881
|
+
const target =
|
|
2882
|
+
relativeTo === undefined
|
|
2883
|
+
? this
|
|
2884
|
+
: requireModelObject(relativeTo, 'bounds requires a reference model.');
|
|
2885
|
+
const transform = target.localFrames([this])[0];
|
|
2886
|
+
const [minimum, maximum] = this[referenceBounds](
|
|
2887
|
+
this.relationAnchorReference(),
|
|
2888
|
+
transform,
|
|
2889
|
+
);
|
|
2890
|
+
return {
|
|
2891
|
+
minimum,
|
|
2892
|
+
maximum,
|
|
2893
|
+
size: [
|
|
2894
|
+
maximum[0] - minimum[0],
|
|
2895
|
+
maximum[1] - minimum[1],
|
|
2896
|
+
maximum[2] - minimum[2],
|
|
2897
|
+
],
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
/**
|
|
2902
|
+
* Model-origin coordinates in relativeTo's local frame, including placement.
|
|
2903
|
+
* This model's origin in its own frame is always [0, 0, 0], even for point models.
|
|
2904
|
+
*/
|
|
2905
|
+
position(relativeTo: Model): Vec3 {
|
|
2906
|
+
const target = requireModelObject(
|
|
2907
|
+
relativeTo,
|
|
2908
|
+
'position requires a reference model.',
|
|
2909
|
+
);
|
|
2910
|
+
return target.localFrames([this])[0].position;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
/** Resolve source frames without losing nested occurrence positions. */
|
|
2914
|
+
private localFrames(sources: readonly RelationObject[]): RigidTransform[] {
|
|
2826
2915
|
const members = this.memberPoses();
|
|
2827
|
-
const external = sources
|
|
2828
|
-
.map(source => anchorReference(source).model)
|
|
2829
|
-
.filter(model => !members.has(model));
|
|
2916
|
+
const external = sources.filter(model => !members.has(model));
|
|
2830
2917
|
const context = external.length
|
|
2831
2918
|
? ModelObject.createSolveContext([this, ...external])
|
|
2832
2919
|
: undefined;
|
|
2833
2920
|
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);
|
|
2921
|
+
const memberPose = members.get(source);
|
|
2844
2922
|
if (memberPose === null)
|
|
2845
2923
|
throw new Error(
|
|
2846
2924
|
"The point or element belongs to multiple occurrences. Expose it through the intended child model's named reference.",
|
|
2847
2925
|
);
|
|
2848
|
-
|
|
2926
|
+
return (
|
|
2849
2927
|
memberPose ??
|
|
2850
|
-
relativeTransform(
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2928
|
+
relativeTransform(source.solvePose(context!), this.solvePose(context!))
|
|
2929
|
+
);
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
/** Resolve both own and occurrence references into this model's local frame. */
|
|
2934
|
+
private localElements(sources: readonly Anchor[]): StoredElement[] {
|
|
2935
|
+
const references = sources.map(anchorReference);
|
|
2936
|
+
const transforms = this.localFrames(
|
|
2937
|
+
references.map(reference => reference.model),
|
|
2938
|
+
);
|
|
2939
|
+
return sources.map((source, index) => {
|
|
2940
|
+
const {kind, transform, topology, parts, members, bound, facing} =
|
|
2941
|
+
source instanceof ModelObject
|
|
2942
|
+
? source.exposedElement()
|
|
2943
|
+
: references[index];
|
|
2854
2944
|
return transformElement(
|
|
2855
|
-
{kind, transform
|
|
2856
|
-
|
|
2945
|
+
{kind, transform, topology, parts, members, bound, facing},
|
|
2946
|
+
transforms[index],
|
|
2857
2947
|
);
|
|
2858
2948
|
});
|
|
2859
2949
|
}
|
|
@@ -2901,7 +2991,11 @@ export class ModelObject<
|
|
|
2901
2991
|
/** @internal */
|
|
2902
2992
|
exposedElement(): StoredElement {
|
|
2903
2993
|
if (this.kind === 'group')
|
|
2904
|
-
return {
|
|
2994
|
+
return {
|
|
2995
|
+
...this.geometryAnchor,
|
|
2996
|
+
parts: this.geometryParts(),
|
|
2997
|
+
members: this.elements,
|
|
2998
|
+
};
|
|
2905
2999
|
const geometry = this.requireGeometry().value;
|
|
2906
3000
|
const kind = (
|
|
2907
3001
|
this.kind === 'face' ? 'surface' : this.kind
|
|
@@ -3183,7 +3277,7 @@ export class ModelObject<
|
|
|
3183
3277
|
{
|
|
3184
3278
|
shape: source.value.shape,
|
|
3185
3279
|
topology: source.value.topology,
|
|
3186
|
-
|
|
3280
|
+
namespace: 1,
|
|
3187
3281
|
},
|
|
3188
3282
|
direction,
|
|
3189
3283
|
),
|
|
@@ -3661,12 +3755,12 @@ export class ModelObject<
|
|
|
3661
3755
|
{
|
|
3662
3756
|
shape: geometry.value.shape,
|
|
3663
3757
|
topology: geometry.value.topology,
|
|
3664
|
-
|
|
3758
|
+
namespace: otherIndex === 0 ? 1 : 'intermediate',
|
|
3665
3759
|
},
|
|
3666
3760
|
{
|
|
3667
3761
|
shape: operand.value,
|
|
3668
3762
|
topology: otherGeometry.value.topology,
|
|
3669
|
-
|
|
3763
|
+
namespace: otherIndex + 2,
|
|
3670
3764
|
},
|
|
3671
3765
|
operation,
|
|
3672
3766
|
);
|
|
@@ -3720,6 +3814,230 @@ export class ModelObject<
|
|
|
3720
3814
|
return members;
|
|
3721
3815
|
}
|
|
3722
3816
|
|
|
3817
|
+
/** Finite members in this value's saved local assembly, without re-solving children. */
|
|
3818
|
+
private geometryParts(transform = identityRigidTransform): StoredTopology[] {
|
|
3819
|
+
if (this.geometry)
|
|
3820
|
+
return [{source: this, selection: {kind: 'solid'}, transform, scale: 1}];
|
|
3821
|
+
return this.children.flatMap(child =>
|
|
3822
|
+
child.geometryParts(
|
|
3823
|
+
composeTransforms(transform, child.solvePose(this.assembly!)),
|
|
3824
|
+
),
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
/** @internal */
|
|
3829
|
+
static distance(a: Anchor, b: Anchor, axis?: DistanceAxis): number {
|
|
3830
|
+
const left = anchorReference(a),
|
|
3831
|
+
right = anchorReference(b);
|
|
3832
|
+
const axisReference =
|
|
3833
|
+
axis !== undefined && typeof axis !== 'string' && !Array.isArray(axis)
|
|
3834
|
+
? straightAxisReference(axis as LineAnchor, 'distance() axis')
|
|
3835
|
+
: undefined;
|
|
3836
|
+
const context = ModelObject.createSolveContext([
|
|
3837
|
+
left.model,
|
|
3838
|
+
right.model,
|
|
3839
|
+
...(axisReference ? [axisReference.model] : []),
|
|
3840
|
+
]);
|
|
3841
|
+
const topologyParts = (reference: AnchorReference) =>
|
|
3842
|
+
reference.topology
|
|
3843
|
+
? [reference.topology]
|
|
3844
|
+
: (reference.parts ??
|
|
3845
|
+
(reference.whole && reference.model instanceof ModelObject
|
|
3846
|
+
? reference.model.geometryParts()
|
|
3847
|
+
: undefined));
|
|
3848
|
+
const parts = (reference: AnchorReference): DistancePart[] => {
|
|
3849
|
+
const pose = reference.model.solvePose(context);
|
|
3850
|
+
const frame = composeTransforms(pose, reference.transform);
|
|
3851
|
+
if (reference.kind === 'point') return [{points: [frame.position]}];
|
|
3852
|
+
const topology = topologyParts(reference);
|
|
3853
|
+
if (topology)
|
|
3854
|
+
return topology.map(part => ({
|
|
3855
|
+
geometry: part.source.requireGeometry(),
|
|
3856
|
+
selection: part.selection,
|
|
3857
|
+
scale: part.scale,
|
|
3858
|
+
transform: composeTransforms(pose, part.transform),
|
|
3859
|
+
}));
|
|
3860
|
+
if (reference.bound) {
|
|
3861
|
+
const [x, z] = reference.bound.size;
|
|
3862
|
+
const corners: Vec3[] =
|
|
3863
|
+
x === 0 && z === 0
|
|
3864
|
+
? [origin]
|
|
3865
|
+
: x === 0
|
|
3866
|
+
? [
|
|
3867
|
+
[0, 0, -z / 2],
|
|
3868
|
+
[0, 0, z / 2],
|
|
3869
|
+
]
|
|
3870
|
+
: z === 0
|
|
3871
|
+
? [
|
|
3872
|
+
[-x / 2, 0, 0],
|
|
3873
|
+
[x / 2, 0, 0],
|
|
3874
|
+
]
|
|
3875
|
+
: [
|
|
3876
|
+
[-x / 2, 0, -z / 2],
|
|
3877
|
+
[x / 2, 0, -z / 2],
|
|
3878
|
+
[x / 2, 0, z / 2],
|
|
3879
|
+
[-x / 2, 0, z / 2],
|
|
3880
|
+
];
|
|
3881
|
+
return [
|
|
3882
|
+
{
|
|
3883
|
+
points: corners.map(
|
|
3884
|
+
point => composeTransforms(frame, translation(point)).position,
|
|
3885
|
+
),
|
|
3886
|
+
},
|
|
3887
|
+
];
|
|
3888
|
+
}
|
|
3889
|
+
throw new Error(
|
|
3890
|
+
'distance() requires finite geometry or a point; select an edge or face instead of an infinite axis or plane.',
|
|
3891
|
+
);
|
|
3892
|
+
};
|
|
3893
|
+
const first = parts(left),
|
|
3894
|
+
second = parts(right);
|
|
3895
|
+
if (!first.length || !second.length)
|
|
3896
|
+
throw new Error('distance() cannot measure an empty group.');
|
|
3897
|
+
const record = (result: DistanceResult, direction?: Vec3): number => {
|
|
3898
|
+
if (observeDistance) {
|
|
3899
|
+
const operands = [left, right].map(reference => {
|
|
3900
|
+
const topology = topologyParts(reference);
|
|
3901
|
+
const elements =
|
|
3902
|
+
topology && reference.kind !== 'point'
|
|
3903
|
+
? topology.map(
|
|
3904
|
+
part =>
|
|
3905
|
+
snapshotElements({
|
|
3906
|
+
[reference.name]: {
|
|
3907
|
+
...reference,
|
|
3908
|
+
topology: part,
|
|
3909
|
+
members: undefined,
|
|
3910
|
+
},
|
|
3911
|
+
})[0],
|
|
3912
|
+
)
|
|
3913
|
+
: [
|
|
3914
|
+
snapshotElements({
|
|
3915
|
+
[reference.name]: {...reference, members: undefined},
|
|
3916
|
+
})[0],
|
|
3917
|
+
];
|
|
3918
|
+
return {
|
|
3919
|
+
nodeId: reference.model.nodeId,
|
|
3920
|
+
whole: reference.whole === true,
|
|
3921
|
+
elements,
|
|
3922
|
+
};
|
|
3923
|
+
});
|
|
3924
|
+
observeDistance(
|
|
3925
|
+
{
|
|
3926
|
+
...result,
|
|
3927
|
+
axis: direction,
|
|
3928
|
+
axisName: typeof axis === 'string' ? axis : undefined,
|
|
3929
|
+
operands,
|
|
3930
|
+
placements: [...context.poses].map(([model, pose]) => ({
|
|
3931
|
+
nodeId: model.nodeId,
|
|
3932
|
+
transform: toTransform(pose),
|
|
3933
|
+
})),
|
|
3934
|
+
},
|
|
3935
|
+
[...context.poses.keys()],
|
|
3936
|
+
);
|
|
3937
|
+
}
|
|
3938
|
+
return result.value;
|
|
3939
|
+
};
|
|
3940
|
+
if (axis === undefined) {
|
|
3941
|
+
let result: DistanceResult | undefined;
|
|
3942
|
+
for (const a of first)
|
|
3943
|
+
for (const b of second) {
|
|
3944
|
+
const candidate =
|
|
3945
|
+
'points' in a &&
|
|
3946
|
+
a.points.length === 1 &&
|
|
3947
|
+
'points' in b &&
|
|
3948
|
+
b.points.length === 1
|
|
3949
|
+
? {
|
|
3950
|
+
value: Math.hypot(
|
|
3951
|
+
...a.points[0].map((value, i) => value - b.points[0][i]),
|
|
3952
|
+
),
|
|
3953
|
+
start: a.points[0],
|
|
3954
|
+
end: b.points[0],
|
|
3955
|
+
}
|
|
3956
|
+
: distanceBetweenParts(a, b).value;
|
|
3957
|
+
if (!result || candidate.value < result.value) result = candidate;
|
|
3958
|
+
if (result.value === 0) return record(result);
|
|
3959
|
+
}
|
|
3960
|
+
return record(result!);
|
|
3961
|
+
}
|
|
3962
|
+
const direction = axisReference
|
|
3963
|
+
? rotateVector(
|
|
3964
|
+
[0, 1, 0],
|
|
3965
|
+
composeTransforms(
|
|
3966
|
+
axisReference.model.solvePose(context),
|
|
3967
|
+
axisReference.transform,
|
|
3968
|
+
).quaternion,
|
|
3969
|
+
)
|
|
3970
|
+
: typeof axis === 'string'
|
|
3971
|
+
? distanceAxes[axis]
|
|
3972
|
+
: (axis as Vec3);
|
|
3973
|
+
assertFiniteVector('distance() axis', direction);
|
|
3974
|
+
const magnitude = Math.hypot(...direction);
|
|
3975
|
+
if (magnitude === 0) throw new Error('distance() axis must be non-zero.');
|
|
3976
|
+
const normalized = direction.map(
|
|
3977
|
+
value => value / magnitude,
|
|
3978
|
+
) as unknown as Vec3;
|
|
3979
|
+
const project = invertTransform(frameFromYAxis(origin, normalized));
|
|
3980
|
+
const projectedBounds = (parts: DistancePart[]): LocalBounds => {
|
|
3981
|
+
const bounds = parts.map(part => {
|
|
3982
|
+
if ('points' in part)
|
|
3983
|
+
return pointBounds(
|
|
3984
|
+
part.points.map(
|
|
3985
|
+
point => composeTransforms(project, translation(point)).position,
|
|
3986
|
+
),
|
|
3987
|
+
);
|
|
3988
|
+
return evaluateSnapshotQuery(
|
|
3989
|
+
boundsQuery(
|
|
3990
|
+
part.geometry,
|
|
3991
|
+
composeTransforms(project, part.transform),
|
|
3992
|
+
part.selection,
|
|
3993
|
+
part.scale,
|
|
3994
|
+
),
|
|
3995
|
+
) as LocalBounds;
|
|
3996
|
+
});
|
|
3997
|
+
return [
|
|
3998
|
+
[0, 1, 2].map(i =>
|
|
3999
|
+
Math.min(...bounds.map(value => value[0][i])),
|
|
4000
|
+
) as unknown as Vec3,
|
|
4001
|
+
[0, 1, 2].map(i =>
|
|
4002
|
+
Math.max(...bounds.map(value => value[1][i])),
|
|
4003
|
+
) as unknown as Vec3,
|
|
4004
|
+
];
|
|
4005
|
+
};
|
|
4006
|
+
const aBounds = projectedBounds(first),
|
|
4007
|
+
bBounds = projectedBounds(second);
|
|
4008
|
+
const [aMin, aMax] = [aBounds[0][1], aBounds[1][1]],
|
|
4009
|
+
[bMin, bMax] = [bBounds[0][1], bBounds[1][1]];
|
|
4010
|
+
const overlap = (Math.max(aMin, bMin) + Math.min(aMax, bMax)) / 2;
|
|
4011
|
+
const [start, end] =
|
|
4012
|
+
bMin > aMax
|
|
4013
|
+
? [aMax, bMin]
|
|
4014
|
+
: aMin > bMax
|
|
4015
|
+
? [aMin, bMax]
|
|
4016
|
+
: [overlap, overlap];
|
|
4017
|
+
// Place the axis-parallel dimension within the shared transverse bounds.
|
|
4018
|
+
// A point inside the other operand's projection then anchors the line at
|
|
4019
|
+
// that point. Disjoint bounds use the midpoint of their nearest limits;
|
|
4020
|
+
// projected endpoints remain reference positions for general geometry.
|
|
4021
|
+
const center = [0, 1, 2].map(
|
|
4022
|
+
i =>
|
|
4023
|
+
(Math.max(aBounds[0][i], bBounds[0][i]) +
|
|
4024
|
+
Math.min(aBounds[1][i], bBounds[1][i])) /
|
|
4025
|
+
2,
|
|
4026
|
+
);
|
|
4027
|
+
const unproject = invertTransform(project);
|
|
4028
|
+
const at = (y: number) =>
|
|
4029
|
+
composeTransforms(unproject, translation([center[0], y, center[2]]))
|
|
4030
|
+
.position;
|
|
4031
|
+
return record(
|
|
4032
|
+
{
|
|
4033
|
+
value: Math.max(0, bMin - aMax, aMin - bMax),
|
|
4034
|
+
start: at(start),
|
|
4035
|
+
end: at(end),
|
|
4036
|
+
},
|
|
4037
|
+
normalized,
|
|
4038
|
+
);
|
|
4039
|
+
}
|
|
4040
|
+
|
|
3723
4041
|
/** Bounds of the selected finite geometry after a rigid transform. */
|
|
3724
4042
|
private [referenceBoundsParts](
|
|
3725
4043
|
reference: StoredAnchor,
|
|
@@ -3754,6 +4072,15 @@ export class ModelObject<
|
|
|
3754
4072
|
),
|
|
3755
4073
|
];
|
|
3756
4074
|
}
|
|
4075
|
+
if (reference.parts)
|
|
4076
|
+
return reference.parts.map(part =>
|
|
4077
|
+
boundsQuery(
|
|
4078
|
+
part.source.requireGeometry(),
|
|
4079
|
+
composeTransforms(transform, part.transform),
|
|
4080
|
+
part.selection,
|
|
4081
|
+
part.scale,
|
|
4082
|
+
),
|
|
4083
|
+
);
|
|
3757
4084
|
return [{bounds: super[referenceBounds](reference, transform)}];
|
|
3758
4085
|
}
|
|
3759
4086
|
|
|
@@ -3856,7 +4183,7 @@ export class ModelObject<
|
|
|
3856
4183
|
}
|
|
3857
4184
|
|
|
3858
4185
|
protected override edgeReference(id: EdgeId): RelationReference {
|
|
3859
|
-
return {...
|
|
4186
|
+
return {...straightAxisReference(this.edge(id)), model: undefined};
|
|
3860
4187
|
}
|
|
3861
4188
|
|
|
3862
4189
|
protected override vertexPosition(id: VertexId): Vec3 {
|
|
@@ -4572,6 +4899,16 @@ export function group(children: readonly Model[], name = 'Group'): GroupModel {
|
|
|
4572
4899
|
}) as unknown as GroupModel;
|
|
4573
4900
|
}
|
|
4574
4901
|
|
|
4902
|
+
/**
|
|
4903
|
+
* Measure finite models, topology, bounds or point references in their solved placement.
|
|
4904
|
+
* Without axis, returns the shortest geometric distance. With axis, returns the
|
|
4905
|
+
* gap between projected intervals (zero when they overlap). The result is a
|
|
4906
|
+
* non-negative number computed now; later relations do not update it.
|
|
4907
|
+
*/
|
|
4908
|
+
export function distance(a: Anchor, b: Anchor, axis?: DistanceAxis): number {
|
|
4909
|
+
return ModelObject.distance(a, b, axis);
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4575
4912
|
export function union(operands: readonly SolidModel<{}>[]): SolidModel {
|
|
4576
4913
|
const {first, others} = booleanOperands('union', operands);
|
|
4577
4914
|
return first[combineModels]('fuse', others);
|
|
@@ -4755,6 +5092,106 @@ type SnapshotQueryInput = {
|
|
|
4755
5092
|
query: SnapshotQuery;
|
|
4756
5093
|
};
|
|
4757
5094
|
|
|
5095
|
+
type DistancePart =
|
|
5096
|
+
| Readonly<{points: readonly Vec3[]}>
|
|
5097
|
+
| Readonly<{
|
|
5098
|
+
geometry: ModelGeometry;
|
|
5099
|
+
selection: TopologySelection;
|
|
5100
|
+
transform: RigidTransform;
|
|
5101
|
+
scale: number;
|
|
5102
|
+
}>;
|
|
5103
|
+
|
|
5104
|
+
type DistanceResult = Readonly<{value: number; start: Vec3; end: Vec3}>;
|
|
5105
|
+
|
|
5106
|
+
const distanceAxes = {x: [1, 0, 0], y: [0, 1, 0], z: [0, 0, 1]} as const;
|
|
5107
|
+
|
|
5108
|
+
function withDistanceShape<T>(
|
|
5109
|
+
part: DistancePart,
|
|
5110
|
+
visit: (shape: AnyShape) => T,
|
|
5111
|
+
): T {
|
|
5112
|
+
if (!('points' in part))
|
|
5113
|
+
return withTransformedGeometry(part.geometry.value, part, visit);
|
|
5114
|
+
const points = part.points;
|
|
5115
|
+
if (points.length <= 2) {
|
|
5116
|
+
const shape =
|
|
5117
|
+
points.length === 1
|
|
5118
|
+
? makeVertex(toPoint(points[0]))
|
|
5119
|
+
: makeLine(toPoint(points[0]), toPoint(points[1]));
|
|
5120
|
+
try {
|
|
5121
|
+
return visit(shape);
|
|
5122
|
+
} finally {
|
|
5123
|
+
shape.delete();
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
const edges: ReplicadEdge[] = [];
|
|
5127
|
+
let wire: ReplicadWire | undefined;
|
|
5128
|
+
let face: ReplicadFace | undefined;
|
|
5129
|
+
try {
|
|
5130
|
+
points.forEach((point, index) =>
|
|
5131
|
+
edges.push(
|
|
5132
|
+
makeLine(toPoint(point), toPoint(points[(index + 1) % points.length])),
|
|
5133
|
+
),
|
|
5134
|
+
);
|
|
5135
|
+
wire = assembleWire(edges);
|
|
5136
|
+
face = makeFace(wire);
|
|
5137
|
+
return visit(face);
|
|
5138
|
+
} finally {
|
|
5139
|
+
face?.delete();
|
|
5140
|
+
wire?.delete();
|
|
5141
|
+
edges.forEach(edge => edge.delete());
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
const distanceBetweenParts = cachedArtifact(
|
|
5146
|
+
(a: DistancePart, b: DistancePart): DistanceResult =>
|
|
5147
|
+
withDistanceShape(a, left =>
|
|
5148
|
+
withDistanceShape(b, right => {
|
|
5149
|
+
const query = new (getOC().BRepExtrema_DistShapeShape)();
|
|
5150
|
+
try {
|
|
5151
|
+
query.LoadS1(left.wrapped);
|
|
5152
|
+
query.LoadS2(right.wrapped);
|
|
5153
|
+
query.Perform();
|
|
5154
|
+
if (!query.IsDone())
|
|
5155
|
+
throw new Error('distance() could not measure these geometries.');
|
|
5156
|
+
const first = query.PointOnShape1(1),
|
|
5157
|
+
second = query.PointOnShape2(1);
|
|
5158
|
+
try {
|
|
5159
|
+
return {
|
|
5160
|
+
value: query.Value(),
|
|
5161
|
+
start: [first.X(), first.Y(), first.Z()],
|
|
5162
|
+
end: [second.X(), second.Y(), second.Z()],
|
|
5163
|
+
};
|
|
5164
|
+
} finally {
|
|
5165
|
+
first.delete();
|
|
5166
|
+
second.delete();
|
|
5167
|
+
}
|
|
5168
|
+
} finally {
|
|
5169
|
+
query.delete();
|
|
5170
|
+
}
|
|
5171
|
+
}),
|
|
5172
|
+
),
|
|
5173
|
+
{
|
|
5174
|
+
key: (a, b) =>
|
|
5175
|
+
kernelOperationKey(
|
|
5176
|
+
'distance-witnesses',
|
|
5177
|
+
[a, b].map(part =>
|
|
5178
|
+
'points' in part
|
|
5179
|
+
? ['points', part.points]
|
|
5180
|
+
: [
|
|
5181
|
+
'shape',
|
|
5182
|
+
part.geometry.id,
|
|
5183
|
+
part.selection.kind,
|
|
5184
|
+
part.selection.kind === 'solid' ? null : part.selection.id,
|
|
5185
|
+
part.transform.position,
|
|
5186
|
+
part.transform.quaternion,
|
|
5187
|
+
part.scale,
|
|
5188
|
+
],
|
|
5189
|
+
),
|
|
5190
|
+
[a, b].flatMap(part => ('points' in part ? [] : [part.geometry])),
|
|
5191
|
+
),
|
|
5192
|
+
},
|
|
5193
|
+
);
|
|
5194
|
+
|
|
4758
5195
|
function boundsQuery(
|
|
4759
5196
|
geometry: ModelGeometry,
|
|
4760
5197
|
transform: RigidTransform,
|
|
@@ -5047,6 +5484,7 @@ export const authoringApi = Object.freeze({
|
|
|
5047
5484
|
frustum,
|
|
5048
5485
|
regularPrism,
|
|
5049
5486
|
group,
|
|
5487
|
+
distance,
|
|
5050
5488
|
union,
|
|
5051
5489
|
cut,
|
|
5052
5490
|
intersect,
|
|
@@ -5461,7 +5899,7 @@ function buildLoftGeometry(
|
|
|
5461
5899
|
const inputs: {
|
|
5462
5900
|
shape: ReplicadFace;
|
|
5463
5901
|
topology: ShapeTopology;
|
|
5464
|
-
|
|
5902
|
+
namespace: number;
|
|
5465
5903
|
}[] = [];
|
|
5466
5904
|
let spineWire: ReplicadWire | undefined;
|
|
5467
5905
|
try {
|
|
@@ -5472,7 +5910,7 @@ function buildLoftGeometry(
|
|
|
5472
5910
|
section.transform,
|
|
5473
5911
|
),
|
|
5474
5912
|
topology: section.geometry.value.topology,
|
|
5475
|
-
|
|
5913
|
+
namespace: index + 1,
|
|
5476
5914
|
});
|
|
5477
5915
|
}
|
|
5478
5916
|
if (spine) {
|
|
@@ -5571,6 +6009,19 @@ function computeTransformedBounds(
|
|
|
5571
6009
|
geometry: SnapshotQueryInput['geometry'],
|
|
5572
6010
|
query: Extract<SnapshotQuery, {kind: 'bounds'}>,
|
|
5573
6011
|
): LocalBounds {
|
|
6012
|
+
return withTransformedGeometry(geometry, query, shapeBounds);
|
|
6013
|
+
}
|
|
6014
|
+
|
|
6015
|
+
/** Borrow the input, and release all selected/transformed handles after the query. */
|
|
6016
|
+
function withTransformedGeometry<T>(
|
|
6017
|
+
geometry: SnapshotQueryInput['geometry'],
|
|
6018
|
+
query: Readonly<{
|
|
6019
|
+
transform: RigidTransform;
|
|
6020
|
+
selection: TopologySelection;
|
|
6021
|
+
scale: number;
|
|
6022
|
+
}>,
|
|
6023
|
+
visit: (shape: AnyShape) => T,
|
|
6024
|
+
): T {
|
|
5574
6025
|
const {transform, selection, scale} = query;
|
|
5575
6026
|
return withTopologyShape(
|
|
5576
6027
|
geometry.shape,
|
|
@@ -5581,7 +6032,7 @@ function computeTransformedBounds(
|
|
|
5581
6032
|
try {
|
|
5582
6033
|
const moved = shapeWithTransform(scaled, transform);
|
|
5583
6034
|
try {
|
|
5584
|
-
return
|
|
6035
|
+
return visit(moved);
|
|
5585
6036
|
} finally {
|
|
5586
6037
|
moved.delete();
|
|
5587
6038
|
}
|
|
@@ -5717,6 +6168,10 @@ function transformElement(
|
|
|
5717
6168
|
transform: composeTransforms(transform, element.topology.transform),
|
|
5718
6169
|
}
|
|
5719
6170
|
: undefined,
|
|
6171
|
+
parts: element.parts?.map(part => ({
|
|
6172
|
+
...part,
|
|
6173
|
+
transform: composeTransforms(transform, part.transform),
|
|
6174
|
+
})),
|
|
5720
6175
|
members: element.members
|
|
5721
6176
|
? Object.fromEntries(
|
|
5722
6177
|
Object.entries(element.members).map(([name, member]) => [
|
|
@@ -5747,6 +6202,11 @@ function scaleElement(element: StoredElement, factor: number): StoredElement {
|
|
|
5747
6202
|
transform: scaleFrame(element.topology.transform, factor),
|
|
5748
6203
|
}
|
|
5749
6204
|
: undefined,
|
|
6205
|
+
parts: element.parts?.map(part => ({
|
|
6206
|
+
...part,
|
|
6207
|
+
scale: part.scale * factor,
|
|
6208
|
+
transform: scaleFrame(part.transform, factor),
|
|
6209
|
+
})),
|
|
5750
6210
|
members: element.members
|
|
5751
6211
|
? scaleElements(element.members, factor)
|
|
5752
6212
|
: undefined,
|