okgeometry-api 1.26.1 → 1.27.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.
Files changed (99) hide show
  1. package/dist/Arc.js +1 -1
  2. package/dist/Arc.js.map +1 -1
  3. package/dist/Brep.js +1 -1
  4. package/dist/Brep.js.map +1 -1
  5. package/dist/Circle.js +1 -1
  6. package/dist/Circle.js.map +1 -1
  7. package/dist/Line.js +1 -1
  8. package/dist/Line.js.map +1 -1
  9. package/dist/Mesh.d.ts +41 -0
  10. package/dist/Mesh.d.ts.map +1 -1
  11. package/dist/Mesh.js +235 -21
  12. package/dist/Mesh.js.map +1 -1
  13. package/dist/MeshSurface.js +1 -1
  14. package/dist/MeshSurface.js.map +1 -1
  15. package/dist/NurbsCurve.js +1 -1
  16. package/dist/NurbsCurve.js.map +1 -1
  17. package/dist/NurbsSurface.js +1 -1
  18. package/dist/NurbsSurface.js.map +1 -1
  19. package/dist/PolyCurve.js +1 -1
  20. package/dist/PolyCurve.js.map +1 -1
  21. package/dist/Polyline.js +1 -1
  22. package/dist/Polyline.js.map +1 -1
  23. package/dist/Ray.js +1 -1
  24. package/dist/Ray.js.map +1 -1
  25. package/dist/SculptSession.js +1 -1
  26. package/dist/SculptSession.js.map +1 -1
  27. package/dist/engine-threaded.d.ts +2 -0
  28. package/dist/engine-threaded.d.ts.map +1 -0
  29. package/dist/engine-threaded.js +33 -0
  30. package/dist/engine-threaded.js.map +1 -0
  31. package/dist/engine.d.ts +21 -0
  32. package/dist/engine.d.ts.map +1 -1
  33. package/dist/engine.js +42 -0
  34. package/dist/engine.js.map +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/mesh-boolean.pool.d.ts +23 -1
  40. package/dist/mesh-boolean.pool.d.ts.map +1 -1
  41. package/dist/mesh-boolean.pool.js +97 -12
  42. package/dist/mesh-boolean.pool.js.map +1 -1
  43. package/dist/mesh-boolean.protocol.d.ts +21 -1
  44. package/dist/mesh-boolean.protocol.d.ts.map +1 -1
  45. package/dist/mesh-boolean.protocol.js.map +1 -1
  46. package/dist/mesh-boolean.worker.d.ts.map +1 -1
  47. package/dist/mesh-boolean.worker.js +94 -1
  48. package/dist/mesh-boolean.worker.js.map +1 -1
  49. package/dist/wasm-active.d.ts +20 -0
  50. package/dist/wasm-active.d.ts.map +1 -0
  51. package/dist/wasm-active.js +26 -0
  52. package/dist/wasm-active.js.map +1 -0
  53. package/dist/wasm-base64-threaded.d.ts +2 -0
  54. package/dist/wasm-base64-threaded.d.ts.map +1 -0
  55. package/dist/wasm-base64-threaded.js +3 -0
  56. package/dist/wasm-base64-threaded.js.map +1 -0
  57. package/dist/wasm-base64.d.ts +1 -1
  58. package/dist/wasm-base64.d.ts.map +1 -1
  59. package/dist/wasm-base64.js +1 -1
  60. package/dist/wasm-base64.js.map +1 -1
  61. package/dist/wasm-bindings-threaded.d.ts +2664 -0
  62. package/dist/wasm-bindings-threaded.d.ts.map +1 -0
  63. package/dist/wasm-bindings-threaded.js +5008 -0
  64. package/dist/wasm-bindings-threaded.js.map +1 -0
  65. package/dist/wasm-bindings.d.ts +130 -0
  66. package/dist/wasm-bindings.d.ts.map +1 -1
  67. package/dist/wasm-bindings.js +208 -0
  68. package/dist/wasm-bindings.js.map +1 -1
  69. package/dist/workerHelpers-threaded.d.ts +2 -0
  70. package/dist/workerHelpers-threaded.d.ts.map +1 -0
  71. package/dist/workerHelpers-threaded.js +100 -0
  72. package/dist/workerHelpers-threaded.js.map +1 -0
  73. package/package.json +8 -3
  74. package/src/Arc.ts +117 -117
  75. package/src/Brep.ts +1 -1
  76. package/src/Circle.ts +166 -166
  77. package/src/Line.ts +171 -171
  78. package/src/Mesh.ts +300 -45
  79. package/src/MeshSurface.ts +1 -1
  80. package/src/NurbsCurve.ts +655 -655
  81. package/src/NurbsSurface.ts +721 -721
  82. package/src/PolyCurve.ts +203 -203
  83. package/src/Polyline.ts +316 -316
  84. package/src/Ray.ts +90 -90
  85. package/src/SculptSession.ts +377 -377
  86. package/src/engine-threaded.ts +39 -0
  87. package/src/engine.ts +46 -0
  88. package/src/index.ts +1 -1
  89. package/src/mesh-boolean.pool.ts +208 -86
  90. package/src/mesh-boolean.protocol.ts +44 -1
  91. package/src/mesh-boolean.worker.ts +170 -56
  92. package/src/wasm-active.ts +31 -0
  93. package/src/wasm-base64-threaded.ts +2 -0
  94. package/src/wasm-base64.ts +1 -1
  95. package/src/wasm-bindings-threaded.d.ts +1989 -0
  96. package/src/wasm-bindings-threaded.js +5235 -0
  97. package/src/wasm-bindings.d.ts +104 -0
  98. package/src/wasm-bindings.js +225 -0
  99. package/src/workerHelpers-threaded.js +107 -0
package/src/NurbsCurve.ts CHANGED
@@ -1,655 +1,655 @@
1
- import { ensureInit } from "./engine.js";
2
- import { Point } from "./Point.js";
3
- import { Vec3 } from "./Vec3.js";
4
- import { Plane } from "./Plane.js";
5
- import { Mesh } from "./Mesh.js";
6
- import { Polyline } from "./Polyline.js";
7
- import { PolyCurve } from "./PolyCurve.js";
8
- import type { Line } from "./Line.js";
9
- import type { Arc } from "./Arc.js";
10
- import type { NurbsSurface } from "./NurbsSurface.js";
11
- import type { Brep } from "./Brep.js";
12
- import type { CurveOffsetOptions, RotationAxis } from "./types.js";
13
- import * as wasm from "./wasm-bindings.js";
14
-
15
- /**
16
- * Non-Uniform Rational B-Spline (NURBS) curve backed by WASM.
17
- * Provides exact representation for complex curves including conics.
18
- *
19
- * WASM data format: [degree, num_pts, x0,y0,z0, ..., w0,w1,..., k0,k1,...]
20
- */
21
- export class NurbsCurve {
22
- private _data: Float64Array;
23
-
24
- /**
25
- * Create a new NURBS curve.
26
- * @param degree - Polynomial degree (1 = linear, 2 = quadratic, 3 = cubic)
27
- * @param controlPoints - Control points defining the curve shape
28
- * @param weights - Rational weights for each control point (use 1 for non-rational)
29
- * @param knots - Knot vector (length = num_points + degree + 1)
30
- */
31
- constructor(
32
- public readonly degree: number,
33
- public readonly controlPoints: Point[],
34
- public readonly weights: number[],
35
- public readonly knots: number[]
36
- ) {
37
- this._data = NurbsCurve.encode(degree, controlPoints, weights, knots);
38
- }
39
-
40
- /**
41
- * Evaluate a point on the curve at normalized parameter t.
42
- * @param t - Parameter in [0, 1] (0 = start, 1 = end)
43
- * @returns Point on curve at parameter t
44
- */
45
- pointAt(t: number): Point {
46
- ensureInit();
47
- const r = wasm.evaluate_nurbs_curve_at(this._data, t);
48
- return new Point(r[0], r[1], r[2]);
49
- }
50
-
51
- /**
52
- * Sample the curve into evenly-spaced points.
53
- * @param n - Number of points to generate
54
- * @returns Array of n points along the curve
55
- */
56
- sample(n: number): Point[] {
57
- ensureInit();
58
- const buf = wasm.sample_nurbs_curve(this._data, n);
59
- const pts: Point[] = [];
60
- for (let i = 0; i < buf.length; i += 3) {
61
- pts.push(new Point(buf[i], buf[i + 1], buf[i + 2]));
62
- }
63
- return pts;
64
- }
65
-
66
- /**
67
- * Check if this NURBS curve is closed.
68
- * @param eps - Tolerance for comparing start and end points
69
- * @returns True if the curve closes on itself
70
- */
71
- isClosed(eps = 1e-10): boolean {
72
- return this.pointAt(0).equals(this.pointAt(1), eps);
73
- }
74
-
75
- /**
76
- * Find intersection points with a plane.
77
- * @param plane - Cutting plane
78
- * @returns Array of intersection points
79
- */
80
- intersectPlane(plane: Plane): Point[] {
81
- ensureInit();
82
- const buf = wasm.nurbs_curve_plane_intersect(
83
- this._data,
84
- plane.normal.x, plane.normal.y, plane.normal.z,
85
- plane.d
86
- );
87
- if (buf.length === 0) return [];
88
- const numPts = buf[0];
89
- const pts: Point[] = [];
90
- for (let i = 0; i < numPts; i++) {
91
- const off = 1 + i * 3;
92
- pts.push(new Point(buf[off], buf[off + 1], buf[off + 2]));
93
- }
94
- return pts;
95
- }
96
-
97
- /**
98
- * Find intersection points with another NURBS curve.
99
- * @param other - Other curve to intersect with
100
- * @returns Array of intersection points
101
- */
102
- intersectCurve(other: NurbsCurve): Point[] {
103
- ensureInit();
104
- const buf = wasm.nurbs_curve_curve_intersect(this._data, other._data);
105
- if (buf.length === 0) return [];
106
- const numPts = buf[0];
107
- const pts: Point[] = [];
108
- for (let i = 0; i < numPts; i++) {
109
- const off = 1 + i * 3;
110
- pts.push(new Point(buf[off], buf[off + 1], buf[off + 2]));
111
- }
112
- return pts;
113
- }
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
-
242
- /**
243
- * 2D REGION boolean between this curve and another, both CLOSED and
244
- * COPLANAR (Rhino CurveBoolean). Closed planar curves bound regions, so
245
- * union / subtract / intersect are well defined: the curves are projected
246
- * onto a common plane (fitted from this curve, coplanarity verified),
247
- * tessellated to polygons, arranged in 2D, classified by nonzero winding,
248
- * and the result loops emitted.
249
- *
250
- * The result is a list of closed POLYLINE (degree-1 NURBS) curves —
251
- * Rhino-parity fidelity, so curved input boundaries are approximated to the
252
- * tessellation tolerance. Tessellation samples include every distinct
253
- * interior knot value, so degree-1 polygon corners and C0 kinks are
254
- * preserved EXACTLY, and consecutive collinear points are collapsed so
255
- * straight runs come back as single segments. OUTER boundaries are
256
- * counter-clockwise (positive signed area in the fitted plane); HOLE
257
- * boundaries are clockwise. CCW is measured about a DETERMINISTIC plane
258
- * normal: the normal fitted from THIS curve, flipped so its dominant
259
- * world-axis component is positive (for curves in the world XY plane the
260
- * reference normal is +Z, so outer loops are CCW viewed from +Z). The
261
- * emitted orientation is therefore independent of either curve's drawing
262
- * direction. Callers reconstruct the region topology by nesting (a CW loop
263
- * inside a CCW loop is that region's hole).
264
- *
265
- * A valid but empty result (e.g. a disjoint intersect) returns `[]`. Throws
266
- * on non-coplanar or OPEN input (closure is verified kernel-side: the
267
- * curves' endpoints must coincide within a scale-relative tolerance).
268
- *
269
- * @param other - The other closed coplanar curve
270
- * @param op - 0 = union, 1 = subtract (this − other), 2 = intersect
271
- * @returns Result region boundary loops (outer CCW, holes CW)
272
- */
273
- private regionBoolean(other: NurbsCurve, op: number): NurbsCurve[] {
274
- ensureInit();
275
- const buf = wasm.curve_region_boolean(this._data, other._data, op);
276
- // An EMPTY buffer signals an error (non-coplanar / malformed); a valid
277
- // empty result is the non-empty [0] packing.
278
- if (buf.length === 0) {
279
- throw new Error(
280
- "NurbsCurve region boolean failed — curves must be closed and coplanar",
281
- );
282
- }
283
- return NurbsCurve.decodeCurveList(buf);
284
- }
285
-
286
- /**
287
- * Region UNION with another closed coplanar curve. See {@link regionBoolean}.
288
- * @param other - The other closed coplanar curve
289
- * @returns Boundary loops of (inside-this OR inside-other), outer CCW / holes CW
290
- */
291
- regionUnion(other: NurbsCurve): NurbsCurve[] {
292
- return this.regionBoolean(other, 0);
293
- }
294
-
295
- /**
296
- * Region SUBTRACT (this − other) of a closed coplanar curve. Order matters.
297
- * See {@link regionBoolean}.
298
- * @param other - The curve subtracted from this
299
- * @returns Boundary loops of (inside-this AND NOT inside-other)
300
- */
301
- regionSubtract(other: NurbsCurve): NurbsCurve[] {
302
- return this.regionBoolean(other, 1);
303
- }
304
-
305
- /**
306
- * Region INTERSECT with another closed coplanar curve. See
307
- * {@link regionBoolean}.
308
- * @param other - The other closed coplanar curve
309
- * @returns Boundary loops of (inside-this AND inside-other)
310
- */
311
- regionIntersect(other: NurbsCurve): NurbsCurve[] {
312
- return this.regionBoolean(other, 2);
313
- }
314
-
315
- /**
316
- * Region SPLIT: partition this curve and another closed coplanar curve into
317
- * ALL region boundaries (Rhino "region split"). The arrangement partitions
318
- * the plane into the pieces inside-this-only, inside-other-only and
319
- * inside-both; every bounded region's outer and hole loops come back as
320
- * their own closed curves (outer CCW, holes CW). See {@link regionBoolean}
321
- * for the fidelity and orientation contract.
322
- *
323
- * @param other - The other closed coplanar curve
324
- * @returns All region boundary loops
325
- */
326
- regionSplit(other: NurbsCurve): NurbsCurve[] {
327
- ensureInit();
328
- const buf = wasm.curve_region_split(this._data, other._data);
329
- if (buf.length === 0) {
330
- throw new Error(
331
- "NurbsCurve region split failed — curves must be closed and coplanar",
332
- );
333
- }
334
- return NurbsCurve.decodeCurveList(buf);
335
- }
336
-
337
- /** Parse [count, (bufLen, curveData…)·count] into NurbsCurves. */
338
- private static decodeCurveList(buf: Float64Array): NurbsCurve[] {
339
- if (buf.length < 1) return [];
340
- const out: NurbsCurve[] = [];
341
- let offset = 1;
342
- const count = Number(buf[0]);
343
- for (let k = 0; k < count && offset < buf.length; k++) {
344
- const len = Number(buf[offset]);
345
- offset += 1;
346
- out.push(NurbsCurve.fromData(buf.subarray(offset, offset + len)));
347
- offset += len;
348
- }
349
- return out;
350
- }
351
-
352
- /**
353
- * Translate this curve by an offset vector.
354
- * @param offset - Translation vector
355
- * @returns New translated curve
356
- */
357
- translate(offset: Vec3): NurbsCurve {
358
- return new NurbsCurve(
359
- this.degree,
360
- this.controlPoints.map(p => p.add(offset)),
361
- this.weights,
362
- this.knots
363
- );
364
- }
365
-
366
- /**
367
- * Apply a row-major 4x4 AFFINE matrix to this curve. NURBS are invariant
368
- * under affine maps of their control points, so rigid transforms, scaling
369
- * (uniform or not) and shear are EXACT — weights and knots are unchanged.
370
- * @param matrix - Row-major 4x4 matrix (16 entries)
371
- * @returns New transformed curve
372
- */
373
- applyMatrix(matrix: number[] | Float64Array): NurbsCurve {
374
- return new NurbsCurve(
375
- this.degree,
376
- this.controlPoints.map(p => p.applyMatrix(matrix)),
377
- this.weights,
378
- this.knots
379
- );
380
- }
381
-
382
- /**
383
- * Rotate this curve around an axis.
384
- * @param axis - Rotation axis (Vec3 through origin, or Line for arbitrary axis)
385
- * @param angle - Rotation angle in radians
386
- * @returns New rotated curve
387
- */
388
- rotate(axis: RotationAxis, angle: number): NurbsCurve {
389
- return new NurbsCurve(
390
- this.degree,
391
- this.controlPoints.map(p => p.rotate(axis, angle)),
392
- this.weights,
393
- this.knots
394
- );
395
- }
396
-
397
- /**
398
- * Project this curve onto a plane.
399
- * @param plane - Target plane
400
- * @param direction - Optional projection direction (default: perpendicular to plane)
401
- * @returns New projected curve
402
- */
403
- projectOntoPlane(plane: Plane, direction?: Vec3): NurbsCurve {
404
- const proj = (p: Point) => direction ? plane.projectPointAlongDirection(p, direction) : plane.projectPoint(p);
405
- return new NurbsCurve(
406
- this.degree,
407
- this.controlPoints.map(proj),
408
- this.weights,
409
- this.knots
410
- );
411
- }
412
-
413
- // NOTE: `offset()` (exact NURBS offset, kernel-refit to tolerance) lives
414
- // further down next to `data`. The former sampled-polyline `offset()` was
415
- // replaced by it; the sampled multi-result path remains as `offsetAll()`.
416
-
417
- /**
418
- * Offset this curve by a distance and return every sampled result polyline.
419
- * Note: NURBS offset is approximated by sampling and offsetting as polyline.
420
- * @param distance - Offset distance
421
- * @param normal - Reference normal for determining offset direction
422
- * @param samples - Number of sample points used to approximate the source curve
423
- * @param arcSamples - Number of samples per exact arc segment in the returned polylines
424
- * @param options - Offset options such as join style
425
- * @returns Every valid offset polyline approximation
426
- */
427
- offsetAll(
428
- distance: number,
429
- normal?: Vec3,
430
- samples = 64,
431
- arcSamples = 32,
432
- options: CurveOffsetOptions = {},
433
- ): Polyline[] {
434
- const pts = this.sample(samples);
435
- const pl = new Polyline(pts);
436
- return pl.offsetAll(distance, normal, arcSamples, options);
437
- }
438
-
439
- /**
440
- * Thicken this curve into a closed PolyCurve.
441
- * Note: this is approximated by sampling the NURBS curve into a polyline first.
442
- *
443
- * When `bothSides` is false, the sampled source curve forms one side of the
444
- * result. When true, the distance is applied on each side of the sampled curve.
445
- */
446
- thicken(
447
- distance: number,
448
- bothSides = false,
449
- normal?: Vec3,
450
- samples = 64,
451
- options: CurveOffsetOptions = {},
452
- ): PolyCurve {
453
- const pts = this.sample(samples);
454
- return new Polyline(pts).thicken(distance, bothSides, normal, options);
455
- }
456
-
457
- /**
458
- * Extrude this curve along a direction vector.
459
- * Note: this is approximated by sampling the NURBS curve into a polyline
460
- * first. Open curves produce an open sheet; closed curves an uncapped tube
461
- * unless `caps` is true (see extrudeAsSolid for a boolean-ready solid).
462
- * @param direction - Extrusion direction and magnitude
463
- * @param segments - Number of segments along the extrusion (default 1)
464
- * @param caps - Whether to cap the ends of a closed profile (default false)
465
- * @param samples - Number of curve samples used for tessellation (default 64)
466
- * @returns Mesh representing the extruded surface
467
- */
468
- extrude(direction: Vec3, segments = 1, caps = false, samples = 64): Mesh {
469
- const pts = this.sample(samples);
470
- return new Polyline(pts).extrude(direction, segments, caps);
471
- }
472
-
473
- /**
474
- * Extrude this closed NURBS curve into a trusted solid suitable for booleans.
475
- * @param direction - Extrusion direction and magnitude
476
- * @param segments - Number of curve samples used for tessellation (default 64)
477
- * @returns Closed mesh solid ready for boolean operations
478
- */
479
- extrudeAsSolid(direction: Vec3, segments = 64): Mesh {
480
- return Mesh.extrudeCurveAsSolid(this, direction, segments);
481
- }
482
-
483
- /**
484
- * Build a NURBS curve from raw control points with kernel-managed knots and
485
- * uniform weights.
486
- *
487
- * Open curves use a clamped uniform knot vector (curve interpolates the first
488
- * and last control points). Closed curves are periodic (C^{degree-1}
489
- * continuous loops); the first control point must NOT be repeated at the end.
490
- *
491
- * @param degree - Requested degree (clamped by the kernel to the valid range)
492
- * @param controlPoints - Control points (min 2 open / 3 closed)
493
- * @param closed - Build a periodic closed loop (default false)
494
- * @returns New NurbsCurve instance
495
- */
496
- static fromPoints(degree: number, controlPoints: Point[], closed = false): NurbsCurve {
497
- ensureInit();
498
- const coords = new Float64Array(controlPoints.length * 3);
499
- for (let i = 0; i < controlPoints.length; i++) {
500
- coords[i * 3] = controlPoints[i].x;
501
- coords[i * 3 + 1] = controlPoints[i].y;
502
- coords[i * 3 + 2] = controlPoints[i].z;
503
- }
504
- const buf = wasm.build_nurbs_curve(degree, coords, closed);
505
- if (buf.length < 2) {
506
- throw new Error(
507
- `NurbsCurve.fromPoints failed (degree ${degree}, ${controlPoints.length} points, closed=${closed})`,
508
- );
509
- }
510
- return NurbsCurve.fromData(buf);
511
- }
512
-
513
- /**
514
- * Decode a NurbsCurve from WASM flat buffer.
515
- * @param data - Buffer in format [degree, num_cp, xyz..., w..., k...]
516
- * @returns New NurbsCurve instance
517
- */
518
- static fromData(data: Float64Array | number[]): NurbsCurve {
519
- const d = data instanceof Float64Array ? data : new Float64Array(data);
520
- const degree = d[0];
521
- const n = d[1];
522
- let idx = 2;
523
- const pts: Point[] = [];
524
- for (let i = 0; i < n; i++) {
525
- pts.push(new Point(d[idx], d[idx + 1], d[idx + 2]));
526
- idx += 3;
527
- }
528
- const weights = Array.from(d.slice(idx, idx + n)); idx += n;
529
- const numKnots = n + degree + 1;
530
- const knots = Array.from(d.slice(idx, idx + numKnots));
531
- return new NurbsCurve(degree, pts, weights, knots);
532
- }
533
-
534
- /**
535
- * Create exact degree-1 NURBS from a Line.
536
- * @param line - Source line
537
- * @returns Degree-1 NURBS representing the line exactly
538
- */
539
- static fromLine(line: Line): NurbsCurve {
540
- return new NurbsCurve(1, [line.start, line.end], [1, 1], [0, 0, 1, 1]);
541
- }
542
-
543
- /**
544
- * Create exact degree-2 rational NURBS from an Arc.
545
- * Uses WASM circle_arc for exact conic representation.
546
- * @param arc - Source arc
547
- * @returns Degree-2 rational NURBS representing the arc exactly
548
- */
549
- static fromArc(arc: Arc): NurbsCurve {
550
- ensureInit();
551
- const buf = wasm.polycurve_to_nurbs(new Float64Array([
552
- 1, // 1 segment
553
- 1, // type = Arc
554
- arc.center.x, arc.center.y, arc.center.z,
555
- arc.normal.x, arc.normal.y, arc.normal.z,
556
- arc.radius, arc.startAngle, arc.endAngle
557
- ]));
558
- if (buf.length < 2) throw new Error("Failed to convert Arc to NURBS");
559
- return NurbsCurve.fromData(buf);
560
- }
561
-
562
- /**
563
- * Offset this planar curve by `distance` within the plane with the given
564
- * `normal`. Positive distance offsets toward `normal × tangent` (the same
565
- * convention as Polyline/PolyCurve offsets). The result is refit to
566
- * `tolerance` (default 1e-6) against the exact offset locus; rational
567
- * curves (exact arcs) offset exactly. Closed curves stay closed.
568
- *
569
- * Throws for non-planar or kinked curves, or when the offset cannot reach
570
- * tolerance (for example offsetting inward past the curvature radius).
571
- */
572
- offset(
573
- distance: number,
574
- normal: Vec3 = Vec3.Z,
575
- options: { tolerance?: number } = {},
576
- ): NurbsCurve {
577
- ensureInit();
578
- const buf = wasm.nurbs_curve_offset(
579
- this.data,
580
- distance,
581
- normal.x,
582
- normal.y,
583
- normal.z,
584
- options.tolerance ?? 1e-6,
585
- );
586
- if (buf.length < 2) {
587
- throw new Error(
588
- `NurbsCurve.offset() failed: ${wasm.geometry_last_error() || "unknown error"}`,
589
- );
590
- }
591
- return NurbsCurve.fromData(buf);
592
- }
593
-
594
- /**
595
- * Rebuild this curve with the given degree and control point count
596
- * (Rhino-style Rebuild): a global least-squares refit with clean
597
- * approximation knots and chord-length-ish parameterization over [0, 1].
598
- * The result is always NON-RATIONAL; geometrically closed curves stay
599
- * seamlessly closed (periodic basis, C^(degree-1) across the seam); open
600
- * curves keep their exact end points.
601
- *
602
- * Returns the rebuilt curve together with the measured deviation from the
603
- * original (max and mean over dense validation samples).
604
- *
605
- * Throws for invalid degree/count combinations (degree 1..11, pointCount
606
- * >= degree + 1) or degenerate (zero-length) curves.
607
- */
608
- rebuild(
609
- degree: number,
610
- pointCount: number,
611
- ): { curve: NurbsCurve; maxDeviation: number; avgDeviation: number } {
612
- ensureInit();
613
- const buf = wasm.nurbs_curve_rebuild(this.data, degree, pointCount);
614
- if (buf.length < 4) {
615
- throw new Error(
616
- `NurbsCurve.rebuild() failed: ${wasm.geometry_last_error() || "unknown error"}`,
617
- );
618
- }
619
- return {
620
- maxDeviation: Number(buf[0]),
621
- avgDeviation: Number(buf[1]),
622
- curve: NurbsCurve.fromData(buf.subarray(2)),
623
- };
624
- }
625
-
626
- /**
627
- * Get internal WASM data buffer.
628
- * For advanced use cases requiring direct WASM interop.
629
- */
630
- get data(): Float64Array {
631
- return this._data;
632
- }
633
-
634
- /** Encode to WASM format */
635
- private static encode(
636
- degree: number,
637
- controlPoints: Point[],
638
- weights: number[],
639
- knots: number[]
640
- ): Float64Array {
641
- const n = controlPoints.length;
642
- const data = new Float64Array(2 + n * 3 + weights.length + knots.length);
643
- data[0] = degree;
644
- data[1] = n;
645
- let idx = 2;
646
- for (const p of controlPoints) {
647
- data[idx++] = p.x;
648
- data[idx++] = p.y;
649
- data[idx++] = p.z;
650
- }
651
- for (const w of weights) data[idx++] = w;
652
- for (const k of knots) data[idx++] = k;
653
- return data;
654
- }
655
- }
1
+ import { ensureInit } from "./engine.js";
2
+ import { Point } from "./Point.js";
3
+ import { Vec3 } from "./Vec3.js";
4
+ import { Plane } from "./Plane.js";
5
+ import { Mesh } from "./Mesh.js";
6
+ import { Polyline } from "./Polyline.js";
7
+ import { PolyCurve } from "./PolyCurve.js";
8
+ import type { Line } from "./Line.js";
9
+ import type { Arc } from "./Arc.js";
10
+ import type { NurbsSurface } from "./NurbsSurface.js";
11
+ import type { Brep } from "./Brep.js";
12
+ import type { CurveOffsetOptions, RotationAxis } from "./types.js";
13
+ import { wasm } from "./wasm-active.js";
14
+
15
+ /**
16
+ * Non-Uniform Rational B-Spline (NURBS) curve backed by WASM.
17
+ * Provides exact representation for complex curves including conics.
18
+ *
19
+ * WASM data format: [degree, num_pts, x0,y0,z0, ..., w0,w1,..., k0,k1,...]
20
+ */
21
+ export class NurbsCurve {
22
+ private _data: Float64Array;
23
+
24
+ /**
25
+ * Create a new NURBS curve.
26
+ * @param degree - Polynomial degree (1 = linear, 2 = quadratic, 3 = cubic)
27
+ * @param controlPoints - Control points defining the curve shape
28
+ * @param weights - Rational weights for each control point (use 1 for non-rational)
29
+ * @param knots - Knot vector (length = num_points + degree + 1)
30
+ */
31
+ constructor(
32
+ public readonly degree: number,
33
+ public readonly controlPoints: Point[],
34
+ public readonly weights: number[],
35
+ public readonly knots: number[]
36
+ ) {
37
+ this._data = NurbsCurve.encode(degree, controlPoints, weights, knots);
38
+ }
39
+
40
+ /**
41
+ * Evaluate a point on the curve at normalized parameter t.
42
+ * @param t - Parameter in [0, 1] (0 = start, 1 = end)
43
+ * @returns Point on curve at parameter t
44
+ */
45
+ pointAt(t: number): Point {
46
+ ensureInit();
47
+ const r = wasm.evaluate_nurbs_curve_at(this._data, t);
48
+ return new Point(r[0], r[1], r[2]);
49
+ }
50
+
51
+ /**
52
+ * Sample the curve into evenly-spaced points.
53
+ * @param n - Number of points to generate
54
+ * @returns Array of n points along the curve
55
+ */
56
+ sample(n: number): Point[] {
57
+ ensureInit();
58
+ const buf = wasm.sample_nurbs_curve(this._data, n);
59
+ const pts: Point[] = [];
60
+ for (let i = 0; i < buf.length; i += 3) {
61
+ pts.push(new Point(buf[i], buf[i + 1], buf[i + 2]));
62
+ }
63
+ return pts;
64
+ }
65
+
66
+ /**
67
+ * Check if this NURBS curve is closed.
68
+ * @param eps - Tolerance for comparing start and end points
69
+ * @returns True if the curve closes on itself
70
+ */
71
+ isClosed(eps = 1e-10): boolean {
72
+ return this.pointAt(0).equals(this.pointAt(1), eps);
73
+ }
74
+
75
+ /**
76
+ * Find intersection points with a plane.
77
+ * @param plane - Cutting plane
78
+ * @returns Array of intersection points
79
+ */
80
+ intersectPlane(plane: Plane): Point[] {
81
+ ensureInit();
82
+ const buf = wasm.nurbs_curve_plane_intersect(
83
+ this._data,
84
+ plane.normal.x, plane.normal.y, plane.normal.z,
85
+ plane.d
86
+ );
87
+ if (buf.length === 0) return [];
88
+ const numPts = buf[0];
89
+ const pts: Point[] = [];
90
+ for (let i = 0; i < numPts; i++) {
91
+ const off = 1 + i * 3;
92
+ pts.push(new Point(buf[off], buf[off + 1], buf[off + 2]));
93
+ }
94
+ return pts;
95
+ }
96
+
97
+ /**
98
+ * Find intersection points with another NURBS curve.
99
+ * @param other - Other curve to intersect with
100
+ * @returns Array of intersection points
101
+ */
102
+ intersectCurve(other: NurbsCurve): Point[] {
103
+ ensureInit();
104
+ const buf = wasm.nurbs_curve_curve_intersect(this._data, other._data);
105
+ if (buf.length === 0) return [];
106
+ const numPts = buf[0];
107
+ const pts: Point[] = [];
108
+ for (let i = 0; i < numPts; i++) {
109
+ const off = 1 + i * 3;
110
+ pts.push(new Point(buf[off], buf[off + 1], buf[off + 2]));
111
+ }
112
+ return pts;
113
+ }
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
+
242
+ /**
243
+ * 2D REGION boolean between this curve and another, both CLOSED and
244
+ * COPLANAR (Rhino CurveBoolean). Closed planar curves bound regions, so
245
+ * union / subtract / intersect are well defined: the curves are projected
246
+ * onto a common plane (fitted from this curve, coplanarity verified),
247
+ * tessellated to polygons, arranged in 2D, classified by nonzero winding,
248
+ * and the result loops emitted.
249
+ *
250
+ * The result is a list of closed POLYLINE (degree-1 NURBS) curves —
251
+ * Rhino-parity fidelity, so curved input boundaries are approximated to the
252
+ * tessellation tolerance. Tessellation samples include every distinct
253
+ * interior knot value, so degree-1 polygon corners and C0 kinks are
254
+ * preserved EXACTLY, and consecutive collinear points are collapsed so
255
+ * straight runs come back as single segments. OUTER boundaries are
256
+ * counter-clockwise (positive signed area in the fitted plane); HOLE
257
+ * boundaries are clockwise. CCW is measured about a DETERMINISTIC plane
258
+ * normal: the normal fitted from THIS curve, flipped so its dominant
259
+ * world-axis component is positive (for curves in the world XY plane the
260
+ * reference normal is +Z, so outer loops are CCW viewed from +Z). The
261
+ * emitted orientation is therefore independent of either curve's drawing
262
+ * direction. Callers reconstruct the region topology by nesting (a CW loop
263
+ * inside a CCW loop is that region's hole).
264
+ *
265
+ * A valid but empty result (e.g. a disjoint intersect) returns `[]`. Throws
266
+ * on non-coplanar or OPEN input (closure is verified kernel-side: the
267
+ * curves' endpoints must coincide within a scale-relative tolerance).
268
+ *
269
+ * @param other - The other closed coplanar curve
270
+ * @param op - 0 = union, 1 = subtract (this − other), 2 = intersect
271
+ * @returns Result region boundary loops (outer CCW, holes CW)
272
+ */
273
+ private regionBoolean(other: NurbsCurve, op: number): NurbsCurve[] {
274
+ ensureInit();
275
+ const buf = wasm.curve_region_boolean(this._data, other._data, op);
276
+ // An EMPTY buffer signals an error (non-coplanar / malformed); a valid
277
+ // empty result is the non-empty [0] packing.
278
+ if (buf.length === 0) {
279
+ throw new Error(
280
+ "NurbsCurve region boolean failed — curves must be closed and coplanar",
281
+ );
282
+ }
283
+ return NurbsCurve.decodeCurveList(buf);
284
+ }
285
+
286
+ /**
287
+ * Region UNION with another closed coplanar curve. See {@link regionBoolean}.
288
+ * @param other - The other closed coplanar curve
289
+ * @returns Boundary loops of (inside-this OR inside-other), outer CCW / holes CW
290
+ */
291
+ regionUnion(other: NurbsCurve): NurbsCurve[] {
292
+ return this.regionBoolean(other, 0);
293
+ }
294
+
295
+ /**
296
+ * Region SUBTRACT (this − other) of a closed coplanar curve. Order matters.
297
+ * See {@link regionBoolean}.
298
+ * @param other - The curve subtracted from this
299
+ * @returns Boundary loops of (inside-this AND NOT inside-other)
300
+ */
301
+ regionSubtract(other: NurbsCurve): NurbsCurve[] {
302
+ return this.regionBoolean(other, 1);
303
+ }
304
+
305
+ /**
306
+ * Region INTERSECT with another closed coplanar curve. See
307
+ * {@link regionBoolean}.
308
+ * @param other - The other closed coplanar curve
309
+ * @returns Boundary loops of (inside-this AND inside-other)
310
+ */
311
+ regionIntersect(other: NurbsCurve): NurbsCurve[] {
312
+ return this.regionBoolean(other, 2);
313
+ }
314
+
315
+ /**
316
+ * Region SPLIT: partition this curve and another closed coplanar curve into
317
+ * ALL region boundaries (Rhino "region split"). The arrangement partitions
318
+ * the plane into the pieces inside-this-only, inside-other-only and
319
+ * inside-both; every bounded region's outer and hole loops come back as
320
+ * their own closed curves (outer CCW, holes CW). See {@link regionBoolean}
321
+ * for the fidelity and orientation contract.
322
+ *
323
+ * @param other - The other closed coplanar curve
324
+ * @returns All region boundary loops
325
+ */
326
+ regionSplit(other: NurbsCurve): NurbsCurve[] {
327
+ ensureInit();
328
+ const buf = wasm.curve_region_split(this._data, other._data);
329
+ if (buf.length === 0) {
330
+ throw new Error(
331
+ "NurbsCurve region split failed — curves must be closed and coplanar",
332
+ );
333
+ }
334
+ return NurbsCurve.decodeCurveList(buf);
335
+ }
336
+
337
+ /** Parse [count, (bufLen, curveData…)·count] into NurbsCurves. */
338
+ private static decodeCurveList(buf: Float64Array): NurbsCurve[] {
339
+ if (buf.length < 1) return [];
340
+ const out: NurbsCurve[] = [];
341
+ let offset = 1;
342
+ const count = Number(buf[0]);
343
+ for (let k = 0; k < count && offset < buf.length; k++) {
344
+ const len = Number(buf[offset]);
345
+ offset += 1;
346
+ out.push(NurbsCurve.fromData(buf.subarray(offset, offset + len)));
347
+ offset += len;
348
+ }
349
+ return out;
350
+ }
351
+
352
+ /**
353
+ * Translate this curve by an offset vector.
354
+ * @param offset - Translation vector
355
+ * @returns New translated curve
356
+ */
357
+ translate(offset: Vec3): NurbsCurve {
358
+ return new NurbsCurve(
359
+ this.degree,
360
+ this.controlPoints.map(p => p.add(offset)),
361
+ this.weights,
362
+ this.knots
363
+ );
364
+ }
365
+
366
+ /**
367
+ * Apply a row-major 4x4 AFFINE matrix to this curve. NURBS are invariant
368
+ * under affine maps of their control points, so rigid transforms, scaling
369
+ * (uniform or not) and shear are EXACT — weights and knots are unchanged.
370
+ * @param matrix - Row-major 4x4 matrix (16 entries)
371
+ * @returns New transformed curve
372
+ */
373
+ applyMatrix(matrix: number[] | Float64Array): NurbsCurve {
374
+ return new NurbsCurve(
375
+ this.degree,
376
+ this.controlPoints.map(p => p.applyMatrix(matrix)),
377
+ this.weights,
378
+ this.knots
379
+ );
380
+ }
381
+
382
+ /**
383
+ * Rotate this curve around an axis.
384
+ * @param axis - Rotation axis (Vec3 through origin, or Line for arbitrary axis)
385
+ * @param angle - Rotation angle in radians
386
+ * @returns New rotated curve
387
+ */
388
+ rotate(axis: RotationAxis, angle: number): NurbsCurve {
389
+ return new NurbsCurve(
390
+ this.degree,
391
+ this.controlPoints.map(p => p.rotate(axis, angle)),
392
+ this.weights,
393
+ this.knots
394
+ );
395
+ }
396
+
397
+ /**
398
+ * Project this curve onto a plane.
399
+ * @param plane - Target plane
400
+ * @param direction - Optional projection direction (default: perpendicular to plane)
401
+ * @returns New projected curve
402
+ */
403
+ projectOntoPlane(plane: Plane, direction?: Vec3): NurbsCurve {
404
+ const proj = (p: Point) => direction ? plane.projectPointAlongDirection(p, direction) : plane.projectPoint(p);
405
+ return new NurbsCurve(
406
+ this.degree,
407
+ this.controlPoints.map(proj),
408
+ this.weights,
409
+ this.knots
410
+ );
411
+ }
412
+
413
+ // NOTE: `offset()` (exact NURBS offset, kernel-refit to tolerance) lives
414
+ // further down next to `data`. The former sampled-polyline `offset()` was
415
+ // replaced by it; the sampled multi-result path remains as `offsetAll()`.
416
+
417
+ /**
418
+ * Offset this curve by a distance and return every sampled result polyline.
419
+ * Note: NURBS offset is approximated by sampling and offsetting as polyline.
420
+ * @param distance - Offset distance
421
+ * @param normal - Reference normal for determining offset direction
422
+ * @param samples - Number of sample points used to approximate the source curve
423
+ * @param arcSamples - Number of samples per exact arc segment in the returned polylines
424
+ * @param options - Offset options such as join style
425
+ * @returns Every valid offset polyline approximation
426
+ */
427
+ offsetAll(
428
+ distance: number,
429
+ normal?: Vec3,
430
+ samples = 64,
431
+ arcSamples = 32,
432
+ options: CurveOffsetOptions = {},
433
+ ): Polyline[] {
434
+ const pts = this.sample(samples);
435
+ const pl = new Polyline(pts);
436
+ return pl.offsetAll(distance, normal, arcSamples, options);
437
+ }
438
+
439
+ /**
440
+ * Thicken this curve into a closed PolyCurve.
441
+ * Note: this is approximated by sampling the NURBS curve into a polyline first.
442
+ *
443
+ * When `bothSides` is false, the sampled source curve forms one side of the
444
+ * result. When true, the distance is applied on each side of the sampled curve.
445
+ */
446
+ thicken(
447
+ distance: number,
448
+ bothSides = false,
449
+ normal?: Vec3,
450
+ samples = 64,
451
+ options: CurveOffsetOptions = {},
452
+ ): PolyCurve {
453
+ const pts = this.sample(samples);
454
+ return new Polyline(pts).thicken(distance, bothSides, normal, options);
455
+ }
456
+
457
+ /**
458
+ * Extrude this curve along a direction vector.
459
+ * Note: this is approximated by sampling the NURBS curve into a polyline
460
+ * first. Open curves produce an open sheet; closed curves an uncapped tube
461
+ * unless `caps` is true (see extrudeAsSolid for a boolean-ready solid).
462
+ * @param direction - Extrusion direction and magnitude
463
+ * @param segments - Number of segments along the extrusion (default 1)
464
+ * @param caps - Whether to cap the ends of a closed profile (default false)
465
+ * @param samples - Number of curve samples used for tessellation (default 64)
466
+ * @returns Mesh representing the extruded surface
467
+ */
468
+ extrude(direction: Vec3, segments = 1, caps = false, samples = 64): Mesh {
469
+ const pts = this.sample(samples);
470
+ return new Polyline(pts).extrude(direction, segments, caps);
471
+ }
472
+
473
+ /**
474
+ * Extrude this closed NURBS curve into a trusted solid suitable for booleans.
475
+ * @param direction - Extrusion direction and magnitude
476
+ * @param segments - Number of curve samples used for tessellation (default 64)
477
+ * @returns Closed mesh solid ready for boolean operations
478
+ */
479
+ extrudeAsSolid(direction: Vec3, segments = 64): Mesh {
480
+ return Mesh.extrudeCurveAsSolid(this, direction, segments);
481
+ }
482
+
483
+ /**
484
+ * Build a NURBS curve from raw control points with kernel-managed knots and
485
+ * uniform weights.
486
+ *
487
+ * Open curves use a clamped uniform knot vector (curve interpolates the first
488
+ * and last control points). Closed curves are periodic (C^{degree-1}
489
+ * continuous loops); the first control point must NOT be repeated at the end.
490
+ *
491
+ * @param degree - Requested degree (clamped by the kernel to the valid range)
492
+ * @param controlPoints - Control points (min 2 open / 3 closed)
493
+ * @param closed - Build a periodic closed loop (default false)
494
+ * @returns New NurbsCurve instance
495
+ */
496
+ static fromPoints(degree: number, controlPoints: Point[], closed = false): NurbsCurve {
497
+ ensureInit();
498
+ const coords = new Float64Array(controlPoints.length * 3);
499
+ for (let i = 0; i < controlPoints.length; i++) {
500
+ coords[i * 3] = controlPoints[i].x;
501
+ coords[i * 3 + 1] = controlPoints[i].y;
502
+ coords[i * 3 + 2] = controlPoints[i].z;
503
+ }
504
+ const buf = wasm.build_nurbs_curve(degree, coords, closed);
505
+ if (buf.length < 2) {
506
+ throw new Error(
507
+ `NurbsCurve.fromPoints failed (degree ${degree}, ${controlPoints.length} points, closed=${closed})`,
508
+ );
509
+ }
510
+ return NurbsCurve.fromData(buf);
511
+ }
512
+
513
+ /**
514
+ * Decode a NurbsCurve from WASM flat buffer.
515
+ * @param data - Buffer in format [degree, num_cp, xyz..., w..., k...]
516
+ * @returns New NurbsCurve instance
517
+ */
518
+ static fromData(data: Float64Array | number[]): NurbsCurve {
519
+ const d = data instanceof Float64Array ? data : new Float64Array(data);
520
+ const degree = d[0];
521
+ const n = d[1];
522
+ let idx = 2;
523
+ const pts: Point[] = [];
524
+ for (let i = 0; i < n; i++) {
525
+ pts.push(new Point(d[idx], d[idx + 1], d[idx + 2]));
526
+ idx += 3;
527
+ }
528
+ const weights = Array.from(d.slice(idx, idx + n)); idx += n;
529
+ const numKnots = n + degree + 1;
530
+ const knots = Array.from(d.slice(idx, idx + numKnots));
531
+ return new NurbsCurve(degree, pts, weights, knots);
532
+ }
533
+
534
+ /**
535
+ * Create exact degree-1 NURBS from a Line.
536
+ * @param line - Source line
537
+ * @returns Degree-1 NURBS representing the line exactly
538
+ */
539
+ static fromLine(line: Line): NurbsCurve {
540
+ return new NurbsCurve(1, [line.start, line.end], [1, 1], [0, 0, 1, 1]);
541
+ }
542
+
543
+ /**
544
+ * Create exact degree-2 rational NURBS from an Arc.
545
+ * Uses WASM circle_arc for exact conic representation.
546
+ * @param arc - Source arc
547
+ * @returns Degree-2 rational NURBS representing the arc exactly
548
+ */
549
+ static fromArc(arc: Arc): NurbsCurve {
550
+ ensureInit();
551
+ const buf = wasm.polycurve_to_nurbs(new Float64Array([
552
+ 1, // 1 segment
553
+ 1, // type = Arc
554
+ arc.center.x, arc.center.y, arc.center.z,
555
+ arc.normal.x, arc.normal.y, arc.normal.z,
556
+ arc.radius, arc.startAngle, arc.endAngle
557
+ ]));
558
+ if (buf.length < 2) throw new Error("Failed to convert Arc to NURBS");
559
+ return NurbsCurve.fromData(buf);
560
+ }
561
+
562
+ /**
563
+ * Offset this planar curve by `distance` within the plane with the given
564
+ * `normal`. Positive distance offsets toward `normal × tangent` (the same
565
+ * convention as Polyline/PolyCurve offsets). The result is refit to
566
+ * `tolerance` (default 1e-6) against the exact offset locus; rational
567
+ * curves (exact arcs) offset exactly. Closed curves stay closed.
568
+ *
569
+ * Throws for non-planar or kinked curves, or when the offset cannot reach
570
+ * tolerance (for example offsetting inward past the curvature radius).
571
+ */
572
+ offset(
573
+ distance: number,
574
+ normal: Vec3 = Vec3.Z,
575
+ options: { tolerance?: number } = {},
576
+ ): NurbsCurve {
577
+ ensureInit();
578
+ const buf = wasm.nurbs_curve_offset(
579
+ this.data,
580
+ distance,
581
+ normal.x,
582
+ normal.y,
583
+ normal.z,
584
+ options.tolerance ?? 1e-6,
585
+ );
586
+ if (buf.length < 2) {
587
+ throw new Error(
588
+ `NurbsCurve.offset() failed: ${wasm.geometry_last_error() || "unknown error"}`,
589
+ );
590
+ }
591
+ return NurbsCurve.fromData(buf);
592
+ }
593
+
594
+ /**
595
+ * Rebuild this curve with the given degree and control point count
596
+ * (Rhino-style Rebuild): a global least-squares refit with clean
597
+ * approximation knots and chord-length-ish parameterization over [0, 1].
598
+ * The result is always NON-RATIONAL; geometrically closed curves stay
599
+ * seamlessly closed (periodic basis, C^(degree-1) across the seam); open
600
+ * curves keep their exact end points.
601
+ *
602
+ * Returns the rebuilt curve together with the measured deviation from the
603
+ * original (max and mean over dense validation samples).
604
+ *
605
+ * Throws for invalid degree/count combinations (degree 1..11, pointCount
606
+ * >= degree + 1) or degenerate (zero-length) curves.
607
+ */
608
+ rebuild(
609
+ degree: number,
610
+ pointCount: number,
611
+ ): { curve: NurbsCurve; maxDeviation: number; avgDeviation: number } {
612
+ ensureInit();
613
+ const buf = wasm.nurbs_curve_rebuild(this.data, degree, pointCount);
614
+ if (buf.length < 4) {
615
+ throw new Error(
616
+ `NurbsCurve.rebuild() failed: ${wasm.geometry_last_error() || "unknown error"}`,
617
+ );
618
+ }
619
+ return {
620
+ maxDeviation: Number(buf[0]),
621
+ avgDeviation: Number(buf[1]),
622
+ curve: NurbsCurve.fromData(buf.subarray(2)),
623
+ };
624
+ }
625
+
626
+ /**
627
+ * Get internal WASM data buffer.
628
+ * For advanced use cases requiring direct WASM interop.
629
+ */
630
+ get data(): Float64Array {
631
+ return this._data;
632
+ }
633
+
634
+ /** Encode to WASM format */
635
+ private static encode(
636
+ degree: number,
637
+ controlPoints: Point[],
638
+ weights: number[],
639
+ knots: number[]
640
+ ): Float64Array {
641
+ const n = controlPoints.length;
642
+ const data = new Float64Array(2 + n * 3 + weights.length + knots.length);
643
+ data[0] = degree;
644
+ data[1] = n;
645
+ let idx = 2;
646
+ for (const p of controlPoints) {
647
+ data[idx++] = p.x;
648
+ data[idx++] = p.y;
649
+ data[idx++] = p.z;
650
+ }
651
+ for (const w of weights) data[idx++] = w;
652
+ for (const k of knots) data[idx++] = k;
653
+ return data;
654
+ }
655
+ }