okgeometry-api 1.14.0 → 1.16.0
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/dist/Brep.d.ts +83 -0
- package/dist/Brep.d.ts.map +1 -1
- package/dist/Brep.js +148 -0
- package/dist/Brep.js.map +1 -1
- package/dist/Mesh.d.ts +69 -0
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +145 -0
- package/dist/Mesh.js.map +1 -1
- package/dist/NurbsCurve.d.ts +78 -0
- package/dist/NurbsCurve.d.ts.map +1 -1
- package/dist/NurbsCurve.js +118 -0
- package/dist/NurbsCurve.js.map +1 -1
- package/dist/NurbsSurface.d.ts +23 -0
- package/dist/NurbsSurface.d.ts.map +1 -1
- package/dist/NurbsSurface.js +40 -0
- package/dist/NurbsSurface.js.map +1 -1
- package/dist/Polyline.d.ts +47 -0
- package/dist/Polyline.d.ts.map +1 -1
- package/dist/Polyline.js +45 -0
- package/dist/Polyline.js.map +1 -1
- package/dist/wasm-base64.d.ts +1 -1
- package/dist/wasm-base64.d.ts.map +1 -1
- package/dist/wasm-base64.js +1 -1
- package/dist/wasm-base64.js.map +1 -1
- package/dist/wasm-bindings.d.ts +228 -0
- package/dist/wasm-bindings.d.ts.map +1 -1
- package/dist/wasm-bindings.js +336 -0
- package/dist/wasm-bindings.js.map +1 -1
- package/package.json +5 -3
- package/src/Brep.ts +164 -0
- package/src/Mesh.ts +173 -0
- package/src/NurbsCurve.ts +129 -0
- package/src/NurbsSurface.ts +41 -0
- package/src/Polyline.ts +145 -95
- package/src/wasm-base64.ts +1 -1
- package/src/wasm-bindings.d.ts +204 -0
- package/src/wasm-bindings.js +348 -0
package/src/Brep.ts
CHANGED
|
@@ -604,6 +604,170 @@ export class Brep {
|
|
|
604
604
|
return lhs.intersectMesh(rhs);
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
+
// ── Exact intersections (parametric) ──
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* TRIM-EXACT intersection curves with another Brep or a NurbsSurface,
|
|
611
|
+
* returned as exact fitted NURBS curves.
|
|
612
|
+
*
|
|
613
|
+
* Unlike {@link intersectCurves} (tessellation-based, returns polylines at
|
|
614
|
+
* the tessellation density), this runs the parametric boolean's SSI →
|
|
615
|
+
* clip → convergence stages on the EXACT surfaces and clips each
|
|
616
|
+
* intersection curve to BOTH faces' trimmed regions — no classification or
|
|
617
|
+
* assembly, just the fitted intersection edges.
|
|
618
|
+
*
|
|
619
|
+
* Operands need NOT be closed: sheet Breps are valid on either side, and a
|
|
620
|
+
* NurbsSurface operand is converted via {@link Brep.fromSurface}. A closed
|
|
621
|
+
* intersection loop may come back as several contiguous pieces (split at
|
|
622
|
+
* surface seams and face-boundary crossings); the pieces chain end-to-end
|
|
623
|
+
* exactly. Coincident (overlapping) face pairs contribute no curves.
|
|
624
|
+
*
|
|
625
|
+
* `options.tolerance` is the absolute geometric tolerance of the fitted
|
|
626
|
+
* intersection curves (default 1e-6).
|
|
627
|
+
*
|
|
628
|
+
* `options.join` (default false) JOINS the chained pieces into maximal
|
|
629
|
+
* curves: greedy reversal-aware endpoint chaining, exact degree elevation
|
|
630
|
+
* to the chain maximum, exact knot-vector concatenation (junction
|
|
631
|
+
* multiplicity = degree, no re-parameterization), and a per-curve
|
|
632
|
+
* verification gate — every joined curve is sampled densely and must stay
|
|
633
|
+
* within 5·tolerance of BOTH operands' surfaces, otherwise that chain's
|
|
634
|
+
* pieces come back unjoined (never a corrupted joined curve). A closed
|
|
635
|
+
* intersection loop (e.g. a plane through a cylinder) comes back as ONE
|
|
636
|
+
* closed curve with `pointAt(0)` equal to `pointAt(1)`.
|
|
637
|
+
*/
|
|
638
|
+
intersectionCurves(
|
|
639
|
+
other: Brep | NurbsSurface,
|
|
640
|
+
options?: { tolerance?: number; join?: boolean },
|
|
641
|
+
): NurbsCurve[] {
|
|
642
|
+
ensureInit();
|
|
643
|
+
const rhs = other instanceof Brep ? other : Brep.fromSurface(other);
|
|
644
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
645
|
+
const buf = options?.join
|
|
646
|
+
? wasm.brep_intersection_curves_joined_op(this.json, rhs.json, tolerance)
|
|
647
|
+
: wasm.brep_intersection_curves_op(this.json, rhs.json, tolerance);
|
|
648
|
+
// An EMPTY buffer signals a kernel error (invalid BREP / pipeline
|
|
649
|
+
// failure); "no intersections" is the non-empty `[0]` packing.
|
|
650
|
+
if (buf.length < 1) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
"Brep.intersectionCurves failed — invalid BREP operands or kernel error",
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
return Brep.parseCurveList(buf);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* All points where a NURBS curve pierces this BREP's faces.
|
|
660
|
+
*
|
|
661
|
+
* Per face, the curve is intersected with the face's EXACT surface
|
|
662
|
+
* (analytic + bisection for planes, full Newton for NURBS faces) and a hit
|
|
663
|
+
* is kept only when it lies INSIDE the face's trimmed region. Crossings on
|
|
664
|
+
* an edge shared by two faces are deduplicated (smallest face index kept).
|
|
665
|
+
*
|
|
666
|
+
* Hits are sorted by `curveParam` (normalized [0, 1] —
|
|
667
|
+
* `curve.pointAt(curveParam)` reproduces `point` to convergence accuracy);
|
|
668
|
+
* (u, v) are in the pierced face's OWN parameter space (knot domain for
|
|
669
|
+
* NURBS faces, plane-frame coordinates for planar faces). Curve segments
|
|
670
|
+
* lying ON a face (coincident runs) yield no hits.
|
|
671
|
+
*/
|
|
672
|
+
intersectCurve(
|
|
673
|
+
curve: NurbsCurve,
|
|
674
|
+
): { point: Point; curveParam: number; faceIndex: number; u: number; v: number }[] {
|
|
675
|
+
ensureInit();
|
|
676
|
+
const buf = wasm.brep_curve_intersection_op(this.json, curve.data);
|
|
677
|
+
if (buf.length < 1) return [];
|
|
678
|
+
const count = Number(buf[0]);
|
|
679
|
+
const hits: { point: Point; curveParam: number; faceIndex: number; u: number; v: number }[] =
|
|
680
|
+
[];
|
|
681
|
+
for (let i = 0; i < count; i++) {
|
|
682
|
+
const off = 1 + i * 7;
|
|
683
|
+
hits.push({
|
|
684
|
+
point: new Point(buf[off], buf[off + 1], buf[off + 2]),
|
|
685
|
+
curveParam: buf[off + 3],
|
|
686
|
+
faceIndex: Number(buf[off + 4]),
|
|
687
|
+
u: buf[off + 5],
|
|
688
|
+
v: buf[off + 6],
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
return hits;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Boolean-intersection clip of an exact NURBS curve against this CLOSED
|
|
696
|
+
* solid: the curve is split EXACTLY (knot insertion) at every boundary
|
|
697
|
+
* crossing and each piece is classified by containment, so every returned
|
|
698
|
+
* piece lies exactly on the input curve.
|
|
699
|
+
*
|
|
700
|
+
* Closed curves merge the run spanning the parameter seam (a closed curve
|
|
701
|
+
* that never crosses comes back as ONE closed piece on the containing
|
|
702
|
+
* side). Throws when this BREP is not a closed solid — sheet operands have
|
|
703
|
+
* no interior (use {@link intersectCurve} for the crossing points instead).
|
|
704
|
+
*/
|
|
705
|
+
clipCurve(curve: NurbsCurve): { inside: NurbsCurve[]; outside: NurbsCurve[] } {
|
|
706
|
+
ensureInit();
|
|
707
|
+
if (!this.isClosed()) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
"Brep.clipCurve requires a CLOSED solid BREP (sheet operands have no interior)",
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
const buf = wasm.brep_clip_curve_op(curve.data, this.json);
|
|
713
|
+
if (buf.length < 2) throw new Error("Brep.clipCurve failed");
|
|
714
|
+
// Packing: two concatenated multi-curve lists — inside then outside.
|
|
715
|
+
const insideCount = Number(buf[0]);
|
|
716
|
+
let offset = 1;
|
|
717
|
+
const readGroup = (count: number): NurbsCurve[] => {
|
|
718
|
+
const out: NurbsCurve[] = [];
|
|
719
|
+
for (let k = 0; k < count && offset < buf.length; k++) {
|
|
720
|
+
const len = Number(buf[offset]);
|
|
721
|
+
offset += 1;
|
|
722
|
+
out.push(NurbsCurve.fromData(buf.subarray(offset, offset + len)));
|
|
723
|
+
offset += len;
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
};
|
|
727
|
+
const inside = readGroup(insideCount);
|
|
728
|
+
const outsideCount = Number(buf[offset]);
|
|
729
|
+
offset += 1;
|
|
730
|
+
const outside = readGroup(outsideCount);
|
|
731
|
+
return { inside, outside };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Intersection curves with a triangle mesh.
|
|
736
|
+
*
|
|
737
|
+
* TESSELLATION-BASED (documented approximation): the BREP is tessellated at
|
|
738
|
+
* `tolerance` and intersected with the mesh via BVH-pruned tri-tri crossing
|
|
739
|
+
* + chaining, so the polylines approximate the true intersection to the
|
|
740
|
+
* chordal tessellation tolerance on the BREP side — exact where both sides
|
|
741
|
+
* are planar.
|
|
742
|
+
*
|
|
743
|
+
* @param mesh - Triangle mesh operand
|
|
744
|
+
* @param tolerance - BREP tessellation tolerance; `0` (default) selects the
|
|
745
|
+
* deterministic auto display tolerance from the BREP's own extent
|
|
746
|
+
*/
|
|
747
|
+
intersectMesh(mesh: Mesh, tolerance = 0): Polyline[] {
|
|
748
|
+
ensureInit();
|
|
749
|
+
const buf = wasm.brep_mesh_intersection_curves_op(
|
|
750
|
+
this.json,
|
|
751
|
+
mesh.vertexCount,
|
|
752
|
+
mesh.rawBuffer,
|
|
753
|
+
tolerance,
|
|
754
|
+
);
|
|
755
|
+
if (buf.length < 1) return [];
|
|
756
|
+
const out: Polyline[] = [];
|
|
757
|
+
let idx = 1;
|
|
758
|
+
const count = Number(buf[0]);
|
|
759
|
+
for (let i = 0; i < count; i++) {
|
|
760
|
+
const n = Number(buf[idx++]);
|
|
761
|
+
const pts: Point[] = [];
|
|
762
|
+
for (let k = 0; k < n; k++) {
|
|
763
|
+
pts.push(new Point(buf[idx], buf[idx + 1], buf[idx + 2]));
|
|
764
|
+
idx += 3;
|
|
765
|
+
}
|
|
766
|
+
if (pts.length >= 2) out.push(new Polyline(pts));
|
|
767
|
+
}
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
|
|
607
771
|
// ── Parametric booleans (BREP → BREP) ──
|
|
608
772
|
|
|
609
773
|
private runParametricBoolean(
|
package/src/Mesh.ts
CHANGED
|
@@ -2995,6 +2995,179 @@ export class Mesh {
|
|
|
2995
2995
|
return { inside, outside };
|
|
2996
2996
|
}
|
|
2997
2997
|
|
|
2998
|
+
/**
|
|
2999
|
+
* Compute all transversal crossing points of a polyline with this mesh.
|
|
3000
|
+
* Works on OPEN meshes (sheets) — unlike `clipPolyline`, the mesh is NOT
|
|
3001
|
+
* required to be a closed volume.
|
|
3002
|
+
*
|
|
3003
|
+
* Hits are deduplicated (shared-edge / shared-vertex crossings merge into
|
|
3004
|
+
* one, keeping the smallest triangle index) and sorted by
|
|
3005
|
+
* (segmentIndex, segmentT). Coplanar sliding overlaps yield no hits.
|
|
3006
|
+
* Segment `i` runs from point `i` to point `(i + 1) % n`; open polylines
|
|
3007
|
+
* have n − 1 segments, closed ones n (the wrap segment).
|
|
3008
|
+
*
|
|
3009
|
+
* @param polyline - Polyline (closedness inferred) or raw point list
|
|
3010
|
+
* @param closed - Treat the points as a closed loop (defaults to the
|
|
3011
|
+
* polyline's own closedness; required when passing a raw point list)
|
|
3012
|
+
* @returns Crossings with the 3D point, segment index/parameter, and
|
|
3013
|
+
* triangle index
|
|
3014
|
+
*/
|
|
3015
|
+
intersectPolyline(
|
|
3016
|
+
polyline: Polyline | Point[],
|
|
3017
|
+
closed?: boolean,
|
|
3018
|
+
): { point: Point; segmentIndex: number; segmentT: number; triangleIndex: number }[] {
|
|
3019
|
+
ensureInit();
|
|
3020
|
+
const { flat, isClosed } = Mesh.flattenPolylineArg(polyline, closed, "Mesh.intersectPolyline");
|
|
3021
|
+
const buf = wasm.mesh_polyline_intersection_points(
|
|
3022
|
+
this._vertexCount,
|
|
3023
|
+
this._buffer,
|
|
3024
|
+
flat,
|
|
3025
|
+
isClosed,
|
|
3026
|
+
);
|
|
3027
|
+
if (buf.length === 0) {
|
|
3028
|
+
throw new Error(
|
|
3029
|
+
"Mesh.intersectPolyline failed — the mesh and polyline must be non-degenerate.",
|
|
3030
|
+
);
|
|
3031
|
+
}
|
|
3032
|
+
const count = buf[0];
|
|
3033
|
+
const hits: { point: Point; segmentIndex: number; segmentT: number; triangleIndex: number }[] = [];
|
|
3034
|
+
for (let i = 0; i < count; i++) {
|
|
3035
|
+
const off = 1 + i * 6;
|
|
3036
|
+
hits.push({
|
|
3037
|
+
point: new Point(buf[off], buf[off + 1], buf[off + 2]),
|
|
3038
|
+
segmentIndex: buf[off + 3],
|
|
3039
|
+
segmentT: buf[off + 4],
|
|
3040
|
+
triangleIndex: buf[off + 5],
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
return hits;
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
/**
|
|
3047
|
+
* Split a polyline into pieces at every crossing with this mesh (open
|
|
3048
|
+
* sheets allowed). Hit points become the shared endpoints of adjacent
|
|
3049
|
+
* pieces; a closed loop is opened at the first crossing (N crossings → N
|
|
3050
|
+
* pieces), an open polyline yields N + 1 pieces. With no crossings the
|
|
3051
|
+
* original polyline comes back as one piece. Pieces are NOT classified —
|
|
3052
|
+
* use `clipPolyline` (closed volume, containment) or
|
|
3053
|
+
* `clipPolylineBySheet` (oriented sheet, signed side) for classification.
|
|
3054
|
+
*
|
|
3055
|
+
* @param polyline - Polyline (closedness inferred) or raw point list
|
|
3056
|
+
* @param closed - Treat the points as a closed loop (defaults to the
|
|
3057
|
+
* polyline's own closedness; required when passing a raw point list)
|
|
3058
|
+
* @returns Polyline pieces in order along the input
|
|
3059
|
+
*/
|
|
3060
|
+
splitPolyline(polyline: Polyline | Point[], closed?: boolean): Polyline[] {
|
|
3061
|
+
ensureInit();
|
|
3062
|
+
const { flat, isClosed } = Mesh.flattenPolylineArg(polyline, closed, "Mesh.splitPolyline");
|
|
3063
|
+
const buf = wasm.mesh_split_polyline(this._vertexCount, this._buffer, flat, isClosed);
|
|
3064
|
+
if (buf.length === 0) {
|
|
3065
|
+
throw new Error(
|
|
3066
|
+
"Mesh.splitPolyline failed — the mesh and polyline must be non-degenerate.",
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
return parsePolylineBuf(buf).map(pts => new Polyline(pts));
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
/**
|
|
3073
|
+
* Clip a polyline by this mesh treated as an OPEN oriented sheet: the
|
|
3074
|
+
* polyline is split at every crossing and each piece is classified by the
|
|
3075
|
+
* SIGNED SIDE of the sheet — `inside` = in FRONT of the sheet normal,
|
|
3076
|
+
* `outside` = behind it (the mesh's stored winding defines the normals).
|
|
3077
|
+
*
|
|
3078
|
+
* This is side classification, NOT containment — unlike `clipPolyline`,
|
|
3079
|
+
* which requires a CLOSED volume: the sheet may be open, and pieces far
|
|
3080
|
+
* from it still classify deterministically by the nearest facet's normal.
|
|
3081
|
+
* Pieces lying ON the sheet default to `inside`. Adjacent same-side pieces
|
|
3082
|
+
* (e.g. from a touch, not a crossing) merge back into one run; closed
|
|
3083
|
+
* loops merge the run spanning the seam.
|
|
3084
|
+
*
|
|
3085
|
+
* @param polyline - Polyline (closedness inferred) or raw point list
|
|
3086
|
+
* @param closed - Treat the points as a closed loop (defaults to the
|
|
3087
|
+
* polyline's own closedness; required when passing a raw point list)
|
|
3088
|
+
*/
|
|
3089
|
+
clipPolylineBySheet(
|
|
3090
|
+
polyline: Polyline | Point[],
|
|
3091
|
+
closed?: boolean,
|
|
3092
|
+
): { inside: Polyline[]; outside: Polyline[] } {
|
|
3093
|
+
ensureInit();
|
|
3094
|
+
const { flat, isClosed } = Mesh.flattenPolylineArg(
|
|
3095
|
+
polyline,
|
|
3096
|
+
closed,
|
|
3097
|
+
"Mesh.clipPolylineBySheet",
|
|
3098
|
+
);
|
|
3099
|
+
const buf = wasm.mesh_clip_polyline_by_sheet(this._vertexCount, this._buffer, flat, isClosed);
|
|
3100
|
+
if (buf.length === 0) {
|
|
3101
|
+
throw new Error(
|
|
3102
|
+
"Mesh.clipPolylineBySheet failed — the sheet and polyline must be non-degenerate.",
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
// Two concatenated polyline groups: inside first, then outside — same
|
|
3106
|
+
// packing as clipPolyline.
|
|
3107
|
+
let idx = 0;
|
|
3108
|
+
const readGroup = (): Polyline[] => {
|
|
3109
|
+
const group: Polyline[] = [];
|
|
3110
|
+
const count = buf[idx++];
|
|
3111
|
+
for (let i = 0; i < count; i++) {
|
|
3112
|
+
const n = buf[idx++];
|
|
3113
|
+
const points: Point[] = [];
|
|
3114
|
+
for (let k = 0; k < n; k++) {
|
|
3115
|
+
points.push(new Point(buf[idx], buf[idx + 1], buf[idx + 2]));
|
|
3116
|
+
idx += 3;
|
|
3117
|
+
}
|
|
3118
|
+
if (points.length >= 2) group.push(new Polyline(points));
|
|
3119
|
+
}
|
|
3120
|
+
return group;
|
|
3121
|
+
};
|
|
3122
|
+
const inside = readGroup();
|
|
3123
|
+
const outside = readGroup();
|
|
3124
|
+
return { inside, outside };
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
/**
|
|
3128
|
+
* Flatten a Polyline-or-points argument into WASM coords + closed flag.
|
|
3129
|
+
*
|
|
3130
|
+
* Closed inputs following the duplicated-seam convention (terminal point
|
|
3131
|
+
* repeating the first) have the duplicate STRIPPED before crossing the
|
|
3132
|
+
* boundary — the kernel's closed-polyline encoding is seam-free (the wrap
|
|
3133
|
+
* segment is implicit), and a duplicated seam would add a zero-length wrap
|
|
3134
|
+
* segment / doubled seam vertex to split pieces.
|
|
3135
|
+
*/
|
|
3136
|
+
private static flattenPolylineArg(
|
|
3137
|
+
polyline: Polyline | Point[],
|
|
3138
|
+
closed: boolean | undefined,
|
|
3139
|
+
context: string,
|
|
3140
|
+
): { flat: Float64Array; isClosed: boolean } {
|
|
3141
|
+
let pts = Array.isArray(polyline) ? polyline : polyline.points;
|
|
3142
|
+
if (pts.length < 2) {
|
|
3143
|
+
throw new Error(`${context} needs at least 2 polyline points`);
|
|
3144
|
+
}
|
|
3145
|
+
const isClosed = closed ?? (!Array.isArray(polyline) && polyline.isClosed());
|
|
3146
|
+
if (isClosed && pts.length > 2) {
|
|
3147
|
+
// Scale-relative epsilon for the duplicate-seam test.
|
|
3148
|
+
let maxAbs = 0;
|
|
3149
|
+
for (const p of pts) {
|
|
3150
|
+
maxAbs = Math.max(maxAbs, Math.abs(p.x), Math.abs(p.y), Math.abs(p.z));
|
|
3151
|
+
}
|
|
3152
|
+
const eps = (1 + maxAbs) * 1e-12;
|
|
3153
|
+
const first = pts[0];
|
|
3154
|
+
const last = pts[pts.length - 1];
|
|
3155
|
+
const dx = first.x - last.x;
|
|
3156
|
+
const dy = first.y - last.y;
|
|
3157
|
+
const dz = first.z - last.z;
|
|
3158
|
+
if (Math.sqrt(dx * dx + dy * dy + dz * dz) <= eps) {
|
|
3159
|
+
pts = pts.slice(0, -1);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
const flat = new Float64Array(pts.length * 3);
|
|
3163
|
+
for (let i = 0; i < pts.length; i++) {
|
|
3164
|
+
flat[i * 3] = pts[i].x;
|
|
3165
|
+
flat[i * 3 + 1] = pts[i].y;
|
|
3166
|
+
flat[i * 3 + 2] = pts[i].z;
|
|
3167
|
+
}
|
|
3168
|
+
return { flat, isClosed };
|
|
3169
|
+
}
|
|
3170
|
+
|
|
2998
3171
|
/**
|
|
2999
3172
|
* Apply a 4x4 transformation matrix.
|
|
3000
3173
|
* @param matrix - Row-major flat array of 16 numbers
|
package/src/NurbsCurve.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { Polyline } from "./Polyline.js";
|
|
|
7
7
|
import { PolyCurve } from "./PolyCurve.js";
|
|
8
8
|
import type { Line } from "./Line.js";
|
|
9
9
|
import type { Arc } from "./Arc.js";
|
|
10
|
+
import type { NurbsSurface } from "./NurbsSurface.js";
|
|
11
|
+
import type { Brep } from "./Brep.js";
|
|
10
12
|
import type { CurveOffsetOptions, RotationAxis } from "./types.js";
|
|
11
13
|
import * as wasm from "./wasm-bindings.js";
|
|
12
14
|
|
|
@@ -110,6 +112,133 @@ export class NurbsCurve {
|
|
|
110
112
|
return pts;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Find all intersections with another NURBS curve, with exact parameters.
|
|
117
|
+
*
|
|
118
|
+
* Parameters are normalized to [0, 1] (the same parameterization as
|
|
119
|
+
* `pointAt`), so `this.pointAt(paramA)` and `other.pointAt(paramB)` both
|
|
120
|
+
* reproduce `point` up to the convergence tolerance. Results are sorted by
|
|
121
|
+
* `paramA`. Coincident (overlapping) curve runs yield NO points — identical
|
|
122
|
+
* curves return an empty array.
|
|
123
|
+
*
|
|
124
|
+
* @param other - Other curve to intersect with
|
|
125
|
+
* @returns Intersections with the 3D point and the parameter on each curve
|
|
126
|
+
*/
|
|
127
|
+
intersectCurveParams(other: NurbsCurve): { point: Point; paramA: number; paramB: number }[] {
|
|
128
|
+
ensureInit();
|
|
129
|
+
const buf = wasm.nurbs_curve_curve_intersect_params(this._data, other._data);
|
|
130
|
+
if (buf.length === 0) return [];
|
|
131
|
+
const count = buf[0];
|
|
132
|
+
const hits: { point: Point; paramA: number; paramB: number }[] = [];
|
|
133
|
+
for (let i = 0; i < count; i++) {
|
|
134
|
+
const off = 1 + i * 5;
|
|
135
|
+
hits.push({
|
|
136
|
+
point: new Point(buf[off], buf[off + 1], buf[off + 2]),
|
|
137
|
+
paramA: buf[off + 3],
|
|
138
|
+
paramB: buf[off + 4],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return hits;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Find all isolated intersections with a NURBS surface, with exact
|
|
146
|
+
* parameters.
|
|
147
|
+
*
|
|
148
|
+
* All parameters are normalized to [0, 1]: `this.pointAt(curveParam)` and
|
|
149
|
+
* `surface.evaluate(u, v)` both reproduce `point` up to the convergence
|
|
150
|
+
* tolerance. Results are sorted by `curveParam`. Curve segments lying ON
|
|
151
|
+
* the surface (coincident runs) yield no points — a line inside a planar
|
|
152
|
+
* surface returns an empty array.
|
|
153
|
+
*
|
|
154
|
+
* @param surface - Surface to intersect with
|
|
155
|
+
* @returns Intersections with the 3D point, curve parameter, and surface (u, v)
|
|
156
|
+
*/
|
|
157
|
+
intersectSurfaceExact(
|
|
158
|
+
surface: NurbsSurface,
|
|
159
|
+
): { point: Point; curveParam: number; u: number; v: number }[] {
|
|
160
|
+
ensureInit();
|
|
161
|
+
const buf = wasm.nurbs_curve_surface_intersect_exact(this._data, surface.data);
|
|
162
|
+
if (buf.length === 0) return [];
|
|
163
|
+
const count = buf[0];
|
|
164
|
+
const hits: { point: Point; curveParam: number; u: number; v: number }[] = [];
|
|
165
|
+
for (let i = 0; i < count; i++) {
|
|
166
|
+
const off = 1 + i * 6;
|
|
167
|
+
hits.push({
|
|
168
|
+
point: new Point(buf[off], buf[off + 1], buf[off + 2]),
|
|
169
|
+
curveParam: buf[off + 3],
|
|
170
|
+
u: buf[off + 4],
|
|
171
|
+
v: buf[off + 5],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return hits;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Find intersection points with a NURBS surface.
|
|
179
|
+
* Point-only convenience wrapper around `intersectSurfaceExact`.
|
|
180
|
+
* @param surface - Surface to intersect with
|
|
181
|
+
* @returns Array of intersection points, sorted along the curve
|
|
182
|
+
*/
|
|
183
|
+
intersectSurface(surface: NurbsSurface): Point[] {
|
|
184
|
+
return this.intersectSurfaceExact(surface).map(hit => hit.point);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Boolean-intersection clip of this curve against a CLOSED solid Brep —
|
|
189
|
+
* delegate for {@link Brep.clipCurve}: the curve is split EXACTLY (knot
|
|
190
|
+
* insertion) at every boundary crossing and each piece is classified by
|
|
191
|
+
* containment, so every returned piece lies exactly on this curve. Throws
|
|
192
|
+
* when the Brep is not a closed solid.
|
|
193
|
+
*
|
|
194
|
+
* @param brep - Closed solid BREP to clip against
|
|
195
|
+
* @returns Exact sub-curves inside and outside the solid
|
|
196
|
+
*/
|
|
197
|
+
clipByBrep(brep: Brep): { inside: NurbsCurve[]; outside: NurbsCurve[] } {
|
|
198
|
+
return brep.clipCurve(this);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Split this curve at multiple normalized parameters t in [0, 1].
|
|
203
|
+
*
|
|
204
|
+
* Splitting is exact (knot insertion), so every returned piece lies exactly
|
|
205
|
+
* on this curve. Parameters are sorted, deduplicated, and clamped
|
|
206
|
+
* internally. Closed curves are opened at the FIRST parameter (N interior
|
|
207
|
+
* parameters yield N pieces, the seam-spanning piece rejoined); open curves
|
|
208
|
+
* yield N + 1 pieces. With no valid parameters the (clamped) curve comes
|
|
209
|
+
* back as the single piece.
|
|
210
|
+
*
|
|
211
|
+
* @param params - Normalized split parameters in [0, 1]
|
|
212
|
+
* @param closed - Explicit topology override. `true` forces closed
|
|
213
|
+
* (seam-rejoin) semantics; `false` forces open semantics — an open curve
|
|
214
|
+
* whose endpoints merely coincide (a C-shape drawn back onto its start)
|
|
215
|
+
* keeps its N + 1 pieces. Omit to infer closedness geometrically from the
|
|
216
|
+
* endpoint distance (the historical behavior).
|
|
217
|
+
* @returns Curve pieces in parameter order
|
|
218
|
+
*/
|
|
219
|
+
splitAtParams(params: number[], closed?: boolean): NurbsCurve[] {
|
|
220
|
+
ensureInit();
|
|
221
|
+
const closedHint = closed === undefined ? -1 : closed ? 1 : 0;
|
|
222
|
+
const buf = wasm.nurbs_curve_split_at_params(
|
|
223
|
+
this._data,
|
|
224
|
+
new Float64Array(params),
|
|
225
|
+
closedHint
|
|
226
|
+
);
|
|
227
|
+
if (buf.length === 0) {
|
|
228
|
+
throw new Error("NurbsCurve.splitAtParams failed — malformed curve data");
|
|
229
|
+
}
|
|
230
|
+
// Packing: [curveCount, (bufLen, curveData…)·curveCount]
|
|
231
|
+
const count = buf[0];
|
|
232
|
+
const pieces: NurbsCurve[] = [];
|
|
233
|
+
let idx = 1;
|
|
234
|
+
for (let i = 0; i < count; i++) {
|
|
235
|
+
const len = buf[idx++];
|
|
236
|
+
pieces.push(NurbsCurve.fromData(buf.subarray(idx, idx + len)));
|
|
237
|
+
idx += len;
|
|
238
|
+
}
|
|
239
|
+
return pieces;
|
|
240
|
+
}
|
|
241
|
+
|
|
113
242
|
/**
|
|
114
243
|
* Translate this curve by an offset vector.
|
|
115
244
|
* @param offset - Translation vector
|
package/src/NurbsSurface.ts
CHANGED
|
@@ -147,6 +147,47 @@ export class NurbsSurface implements Surface {
|
|
|
147
147
|
return parsePolylineBuf(buf).map(pts => new Polyline(pts));
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* EXACT intersection curves with another NURBS surface.
|
|
152
|
+
*
|
|
153
|
+
* Unlike {@link intersectSurface} (tessellation-based, returns polylines at
|
|
154
|
+
* a fixed sampling resolution), this runs the parametric boolean's
|
|
155
|
+
* surface–surface intersection pipeline — seeded tri-tri detection,
|
|
156
|
+
* chaining, and Newton/tangent-plane relaxation of every point onto the
|
|
157
|
+
* true intersection — and fits each chain to an interpolating NURBS curve,
|
|
158
|
+
* densified until the fitted curve stays within `tolerance` of the exact
|
|
159
|
+
* intersection.
|
|
160
|
+
*
|
|
161
|
+
* Closed intersection loops (e.g. cylinder × plane) come back as ONE closed
|
|
162
|
+
* curve. Coincident (overlapping) surfaces return an empty array — their
|
|
163
|
+
* overlap is a 2D region, not a curve set.
|
|
164
|
+
*
|
|
165
|
+
* @param other - Other surface to intersect with
|
|
166
|
+
* @param tolerance - Absolute geometric tolerance of the fitted curves
|
|
167
|
+
* (model units; floored relative to the combined model scale)
|
|
168
|
+
* @returns Exact fitted NURBS intersection curves
|
|
169
|
+
*/
|
|
170
|
+
intersectSurfaceCurves(other: NurbsSurface, tolerance = 1e-6): NurbsCurve[] {
|
|
171
|
+
ensureInit();
|
|
172
|
+
const buf = wasm.nurbs_surface_surface_intersect_curves(this._data, other._data, tolerance);
|
|
173
|
+
return NurbsSurface.parseCurveList(buf);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Parse [count, (bufLen, curveData…)·count] into NurbsCurves. */
|
|
177
|
+
private static parseCurveList(buf: Float64Array): NurbsCurve[] {
|
|
178
|
+
if (buf.length < 1) return [];
|
|
179
|
+
const out: NurbsCurve[] = [];
|
|
180
|
+
const count = Number(buf[0]);
|
|
181
|
+
let offset = 1;
|
|
182
|
+
for (let k = 0; k < count && offset < buf.length; k++) {
|
|
183
|
+
const len = Number(buf[offset]);
|
|
184
|
+
offset += 1;
|
|
185
|
+
out.push(NurbsCurve.fromData(buf.subarray(offset, offset + len)));
|
|
186
|
+
offset += len;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
150
191
|
/**
|
|
151
192
|
* Convert any supported curve type to a NurbsCurve.
|
|
152
193
|
* @param c - Curve to convert
|