okgeometry-api 1.14.0 → 1.15.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/src/Brep.ts CHANGED
@@ -604,6 +604,155 @@ 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
+ intersectionCurves(other: Brep | NurbsSurface, options?: { tolerance?: number }): NurbsCurve[] {
629
+ ensureInit();
630
+ const rhs = other instanceof Brep ? other : Brep.fromSurface(other);
631
+ const tolerance = options?.tolerance ?? 1e-6;
632
+ const buf = wasm.brep_intersection_curves_op(this.json, rhs.json, tolerance);
633
+ // An EMPTY buffer signals a kernel error (invalid BREP / pipeline
634
+ // failure); "no intersections" is the non-empty `[0]` packing.
635
+ if (buf.length < 1) {
636
+ throw new Error(
637
+ "Brep.intersectionCurves failed — invalid BREP operands or kernel error",
638
+ );
639
+ }
640
+ return Brep.parseCurveList(buf);
641
+ }
642
+
643
+ /**
644
+ * All points where a NURBS curve pierces this BREP's faces.
645
+ *
646
+ * Per face, the curve is intersected with the face's EXACT surface
647
+ * (analytic + bisection for planes, full Newton for NURBS faces) and a hit
648
+ * is kept only when it lies INSIDE the face's trimmed region. Crossings on
649
+ * an edge shared by two faces are deduplicated (smallest face index kept).
650
+ *
651
+ * Hits are sorted by `curveParam` (normalized [0, 1] —
652
+ * `curve.pointAt(curveParam)` reproduces `point` to convergence accuracy);
653
+ * (u, v) are in the pierced face's OWN parameter space (knot domain for
654
+ * NURBS faces, plane-frame coordinates for planar faces). Curve segments
655
+ * lying ON a face (coincident runs) yield no hits.
656
+ */
657
+ intersectCurve(
658
+ curve: NurbsCurve,
659
+ ): { point: Point; curveParam: number; faceIndex: number; u: number; v: number }[] {
660
+ ensureInit();
661
+ const buf = wasm.brep_curve_intersection_op(this.json, curve.data);
662
+ if (buf.length < 1) return [];
663
+ const count = Number(buf[0]);
664
+ const hits: { point: Point; curveParam: number; faceIndex: number; u: number; v: number }[] =
665
+ [];
666
+ for (let i = 0; i < count; i++) {
667
+ const off = 1 + i * 7;
668
+ hits.push({
669
+ point: new Point(buf[off], buf[off + 1], buf[off + 2]),
670
+ curveParam: buf[off + 3],
671
+ faceIndex: Number(buf[off + 4]),
672
+ u: buf[off + 5],
673
+ v: buf[off + 6],
674
+ });
675
+ }
676
+ return hits;
677
+ }
678
+
679
+ /**
680
+ * Boolean-intersection clip of an exact NURBS curve against this CLOSED
681
+ * solid: the curve is split EXACTLY (knot insertion) at every boundary
682
+ * crossing and each piece is classified by containment, so every returned
683
+ * piece lies exactly on the input curve.
684
+ *
685
+ * Closed curves merge the run spanning the parameter seam (a closed curve
686
+ * that never crosses comes back as ONE closed piece on the containing
687
+ * side). Throws when this BREP is not a closed solid — sheet operands have
688
+ * no interior (use {@link intersectCurve} for the crossing points instead).
689
+ */
690
+ clipCurve(curve: NurbsCurve): { inside: NurbsCurve[]; outside: NurbsCurve[] } {
691
+ ensureInit();
692
+ if (!this.isClosed()) {
693
+ throw new Error(
694
+ "Brep.clipCurve requires a CLOSED solid BREP (sheet operands have no interior)",
695
+ );
696
+ }
697
+ const buf = wasm.brep_clip_curve_op(curve.data, this.json);
698
+ if (buf.length < 2) throw new Error("Brep.clipCurve failed");
699
+ // Packing: two concatenated multi-curve lists — inside then outside.
700
+ const insideCount = Number(buf[0]);
701
+ let offset = 1;
702
+ const readGroup = (count: number): NurbsCurve[] => {
703
+ const out: NurbsCurve[] = [];
704
+ for (let k = 0; k < count && offset < buf.length; k++) {
705
+ const len = Number(buf[offset]);
706
+ offset += 1;
707
+ out.push(NurbsCurve.fromData(buf.subarray(offset, offset + len)));
708
+ offset += len;
709
+ }
710
+ return out;
711
+ };
712
+ const inside = readGroup(insideCount);
713
+ const outsideCount = Number(buf[offset]);
714
+ offset += 1;
715
+ const outside = readGroup(outsideCount);
716
+ return { inside, outside };
717
+ }
718
+
719
+ /**
720
+ * Intersection curves with a triangle mesh.
721
+ *
722
+ * TESSELLATION-BASED (documented approximation): the BREP is tessellated at
723
+ * `tolerance` and intersected with the mesh via BVH-pruned tri-tri crossing
724
+ * + chaining, so the polylines approximate the true intersection to the
725
+ * chordal tessellation tolerance on the BREP side — exact where both sides
726
+ * are planar.
727
+ *
728
+ * @param mesh - Triangle mesh operand
729
+ * @param tolerance - BREP tessellation tolerance; `0` (default) selects the
730
+ * deterministic auto display tolerance from the BREP's own extent
731
+ */
732
+ intersectMesh(mesh: Mesh, tolerance = 0): Polyline[] {
733
+ ensureInit();
734
+ const buf = wasm.brep_mesh_intersection_curves_op(
735
+ this.json,
736
+ mesh.vertexCount,
737
+ mesh.rawBuffer,
738
+ tolerance,
739
+ );
740
+ if (buf.length < 1) return [];
741
+ const out: Polyline[] = [];
742
+ let idx = 1;
743
+ const count = Number(buf[0]);
744
+ for (let i = 0; i < count; i++) {
745
+ const n = Number(buf[idx++]);
746
+ const pts: Point[] = [];
747
+ for (let k = 0; k < n; k++) {
748
+ pts.push(new Point(buf[idx], buf[idx + 1], buf[idx + 2]));
749
+ idx += 3;
750
+ }
751
+ if (pts.length >= 2) out.push(new Polyline(pts));
752
+ }
753
+ return out;
754
+ }
755
+
607
756
  // ── Parametric booleans (BREP → BREP) ──
608
757
 
609
758
  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
@@ -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