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
@@ -1,721 +1,721 @@
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 { Polygon } from "./Polygon.js";
8
- import { Line } from "./Line.js";
9
- import { Arc } from "./Arc.js";
10
- import { PolyCurve } from "./PolyCurve.js";
11
- import { NurbsCurve } from "./NurbsCurve.js";
12
- import type { Surface } from "./Surface.js";
13
- import type { LoftableCurve, RotationAxis } from "./types.js";
14
- import {
15
- decodePointResult,
16
- decodeVec3Result,
17
- parsePolylineBuffer as parsePolylineBuf,
18
- } from "./BufferCodec.js";
19
- import * as wasm from "./wasm-bindings.js";
20
-
21
- /**
22
- * Non-Uniform Rational B-Spline (NURBS) surface backed by WASM.
23
- * Tensor-product surface defined by a 2D grid of control points.
24
- *
25
- * WASM data format: [degree_u, degree_v, num_u, num_v, ...cp(flat)..., ...weights..., ...knots_u..., ...knots_v...]
26
- */
27
- export class NurbsSurface implements Surface {
28
- private _data: Float64Array;
29
-
30
- /**
31
- * Create a new NURBS surface.
32
- * @param degreeU - Polynomial degree in U direction
33
- * @param degreeV - Polynomial degree in V direction
34
- * @param controlPoints - 2D grid of control points [u][v]
35
- * @param weights - 2D grid of weights corresponding to control points
36
- * @param knotsU - Knot vector in U direction
37
- * @param knotsV - Knot vector in V direction
38
- */
39
- constructor(
40
- public readonly degreeU: number,
41
- public readonly degreeV: number,
42
- public readonly controlPoints: Point[][],
43
- public readonly weights: number[][],
44
- public readonly knotsU: number[],
45
- public readonly knotsV: number[]
46
- ) {
47
- this._data = NurbsSurface.encode(degreeU, degreeV, controlPoints, weights, knotsU, knotsV);
48
- }
49
-
50
- /**
51
- * Rotate this surface around an axis.
52
- * @param axis - Rotation axis (Vec3 through origin, or Line for arbitrary axis)
53
- * @param angle - Rotation angle in radians
54
- * @returns New rotated surface
55
- */
56
- rotate(axis: RotationAxis, angle: number): NurbsSurface {
57
- return new NurbsSurface(
58
- this.degreeU,
59
- this.degreeV,
60
- this.controlPoints.map(row => row.map(p => p.rotate(axis, angle))),
61
- this.weights,
62
- this.knotsU,
63
- this.knotsV
64
- );
65
- }
66
-
67
- /**
68
- * Translate this surface by an offset vector.
69
- * @param offset - Translation vector
70
- * @returns New translated surface
71
- */
72
- translate(offset: Vec3): NurbsSurface {
73
- const newCps = this.controlPoints.map(row => row.map(p => p.add(offset)));
74
- return new NurbsSurface(
75
- this.degreeU,
76
- this.degreeV,
77
- newCps,
78
- this.weights,
79
- this.knotsU,
80
- this.knotsV
81
- );
82
- }
83
-
84
- /**
85
- * Apply a row-major 4x4 AFFINE matrix to this surface. NURBS are invariant
86
- * under affine maps of their control net, so rigid transforms, scaling
87
- * (uniform or not) and shear are EXACT — weights and knots are unchanged.
88
- * @param matrix - Row-major 4x4 matrix (16 entries)
89
- * @returns New transformed surface
90
- */
91
- applyMatrix(matrix: number[] | Float64Array): NurbsSurface {
92
- const newCps = this.controlPoints.map(row => row.map(p => p.applyMatrix(matrix)));
93
- return new NurbsSurface(
94
- this.degreeU,
95
- this.degreeV,
96
- newCps,
97
- this.weights,
98
- this.knotsU,
99
- this.knotsV
100
- );
101
- }
102
-
103
- /**
104
- * Evaluate a point on the surface at parameters (u, v).
105
- * @param u - Parameter in U direction [0, 1]
106
- * @param v - Parameter in V direction [0, 1]
107
- * @returns Point on surface at (u, v)
108
- */
109
- evaluate(u: number, v: number): Point {
110
- ensureInit();
111
- return decodePointResult(wasm.nurbs_surface_evaluate(this._data, u, v));
112
- }
113
-
114
- /**
115
- * Evaluate the unit normal on the surface at parameters (u, v).
116
- * @param u - Parameter in U direction [0, 1]
117
- * @param v - Parameter in V direction [0, 1]
118
- * @returns Unit surface normal at (u, v)
119
- */
120
- normal(u: number, v: number): Vec3 {
121
- ensureInit();
122
- return decodeVec3Result(wasm.nurbs_surface_normal(this._data, u, v));
123
- }
124
-
125
- /**
126
- * Tessellate this surface into a triangle mesh.
127
- * @param uSegs - Number of segments in U direction
128
- * @param vSegs - Number of segments in V direction
129
- * @returns Mesh approximating the surface
130
- */
131
- tessellate(uSegs: number, vSegs: number): Mesh {
132
- ensureInit();
133
- const full = new Float64Array(this._data.length + 2);
134
- full.set(this._data);
135
- full[this._data.length] = uSegs;
136
- full[this._data.length + 1] = vSegs;
137
- return Mesh.fromBuffer(wasm.tessellate_nurbs_surface(full));
138
- }
139
-
140
- /**
141
- * Find intersection curves with a plane.
142
- * @param plane - Cutting plane
143
- * @param tess - Tessellation resolution for intersection detection (default 20)
144
- * @returns Array of polylines representing intersection curves
145
- */
146
- intersectPlane(plane: Plane, tess = 20): Polyline[] {
147
- ensureInit();
148
- const buf = wasm.nurbs_surface_plane_intersect(
149
- this._data,
150
- plane.normal.x, plane.normal.y, plane.normal.z,
151
- plane.d,
152
- tess
153
- );
154
- return parsePolylineBuf(buf).map(pts => new Polyline(pts));
155
- }
156
-
157
- /**
158
- * Find intersection curves with another NURBS surface.
159
- * @param other - Other surface to intersect with
160
- * @param tess - Tessellation resolution for intersection detection (default 20)
161
- * @returns Array of polylines representing intersection curves
162
- */
163
- intersectSurface(other: NurbsSurface, tess = 20): Polyline[] {
164
- ensureInit();
165
- const buf = wasm.nurbs_surface_surface_intersect(this._data, other._data, tess);
166
- return parsePolylineBuf(buf).map(pts => new Polyline(pts));
167
- }
168
-
169
- /**
170
- * EXACT intersection curves with another NURBS surface.
171
- *
172
- * Unlike {@link intersectSurface} (tessellation-based, returns polylines at
173
- * a fixed sampling resolution), this runs the parametric boolean's
174
- * surface–surface intersection pipeline — seeded tri-tri detection,
175
- * chaining, and Newton/tangent-plane relaxation of every point onto the
176
- * true intersection — and fits each chain to an interpolating NURBS curve,
177
- * densified until the fitted curve stays within `tolerance` of the exact
178
- * intersection.
179
- *
180
- * Closed intersection loops (e.g. cylinder × plane) come back as ONE closed
181
- * curve. Coincident (overlapping) surfaces return an empty array — their
182
- * overlap is a 2D region, not a curve set.
183
- *
184
- * @param other - Other surface to intersect with
185
- * @param tolerance - Absolute geometric tolerance of the fitted curves
186
- * (model units; floored relative to the combined model scale)
187
- * @returns Exact fitted NURBS intersection curves
188
- */
189
- intersectSurfaceCurves(other: NurbsSurface, tolerance = 1e-6): NurbsCurve[] {
190
- ensureInit();
191
- const buf = wasm.nurbs_surface_surface_intersect_curves(this._data, other._data, tolerance);
192
- return NurbsSurface.parseCurveList(buf);
193
- }
194
-
195
- /** Parse [count, (bufLen, curveData…)·count] into NurbsCurves. */
196
- private static parseCurveList(buf: Float64Array): NurbsCurve[] {
197
- if (buf.length < 1) return [];
198
- const out: NurbsCurve[] = [];
199
- const count = Number(buf[0]);
200
- let offset = 1;
201
- for (let k = 0; k < count && offset < buf.length; k++) {
202
- const len = Number(buf[offset]);
203
- offset += 1;
204
- out.push(NurbsCurve.fromData(buf.subarray(offset, offset + len)));
205
- offset += len;
206
- }
207
- return out;
208
- }
209
-
210
- /**
211
- * Convert any supported curve type to a NurbsCurve.
212
- * @param c - Curve to convert
213
- * @returns NurbsCurve representation
214
- */
215
- private static toNurbs(c: LoftableCurve): NurbsCurve {
216
- if (c instanceof NurbsCurve) return c;
217
- if (c instanceof PolyCurve) return c.toNurbs();
218
- if (c instanceof Polygon || c instanceof Polyline) return PolyCurve.byPoints((c as Polyline).points).toNurbs();
219
- if (c instanceof Arc) return NurbsCurve.fromArc(c);
220
- if (c instanceof Line) return NurbsCurve.fromLine(c);
221
- throw new Error(`Unsupported curve type for loft`);
222
- }
223
-
224
- /**
225
- * Loft a NURBS surface through multiple curves.
226
- * Accepts NurbsCurve, PolyCurve, Polyline, Polygon, Arc, or Line.
227
- * Automatically converts to NURBS and unifies knot vectors.
228
- *
229
- * @param curves - Array of curves to loft through (minimum 2)
230
- * @param degreeV - Polynomial degree in the loft direction (default 3)
231
- * @returns New NurbsSurface passing through all input curves
232
- */
233
- static loftCurves(curves: LoftableCurve[], degreeV = 3): NurbsSurface {
234
- ensureInit();
235
-
236
- // Step 0: Convert all curves to NurbsCurve
237
- const nurbsCurves = curves.map(c => NurbsSurface.toNurbs(c));
238
-
239
- // Step 1: Make curves compatible (same degree, unified knot vectors,
240
- // same CP count). Mixed degrees are fine — the kernel degree-elevates
241
- // lower-degree curves to the max degree exactly (a 2-point line lofts
242
- // with a cubic).
243
- let loftCurves = nurbsCurves;
244
- if (nurbsCurves.length >= 2) {
245
- const ref = nurbsCurves[0];
246
- const matchesRef = (c: NurbsCurve) =>
247
- c.degree === ref.degree &&
248
- c.controlPoints.length === ref.controlPoints.length &&
249
- c.knots.length === ref.knots.length &&
250
- c.knots.every((k, i) => Math.abs(k - (ref.knots[i] as number)) < 1e-12);
251
- const allSame = nurbsCurves.every(matchesRef);
252
- if (!allSame) {
253
- const compatParts: number[] = [nurbsCurves.length];
254
- for (const c of nurbsCurves) {
255
- compatParts.push(c.degree, c.controlPoints.length);
256
- for (const p of c.controlPoints) { compatParts.push(p.x, p.y, p.z); }
257
- for (const w of c.weights) { compatParts.push(w); }
258
- for (const k of c.knots) { compatParts.push(k); }
259
- }
260
- const compatBuf = wasm.make_nurbs_curves_compatible(new Float64Array(compatParts));
261
- if (compatBuf.length === 0) {
262
- throw new Error("NurbsSurface loft: profile curves could not be made compatible");
263
- }
264
- const count = Number(compatBuf[0] ?? 0);
265
- const decoded: NurbsCurve[] = [];
266
- let off = 1;
267
- for (let i = 0; i < count; i++) {
268
- const deg = Number(compatBuf[off] ?? 0);
269
- const n = Number(compatBuf[off + 1] ?? 0);
270
- const curveLen = 2 + n * 3 + n + (n + deg + 1);
271
- decoded.push(NurbsCurve.fromData(compatBuf.slice(off, off + curveLen)));
272
- off += curveLen;
273
- }
274
- loftCurves = decoded;
275
- }
276
- }
277
-
278
- // Step 2: Encode for loft_nurbs_surface
279
- const parts: number[] = [loftCurves.length, degreeV];
280
- for (const c of loftCurves) {
281
- const pts = c.controlPoints;
282
- parts.push(c.degree, pts.length);
283
- for (const p of pts) { parts.push(p.x, p.y, p.z); }
284
- for (const w of c.weights) { parts.push(w); }
285
- for (const k of c.knots) { parts.push(k); }
286
- }
287
- const buf = wasm.loft_nurbs_surface(new Float64Array(parts));
288
- if (buf.length < 4) throw new Error("NurbsSurface loft failed");
289
- return NurbsSurface.fromData(buf);
290
- }
291
-
292
- /**
293
- * Decode a NurbsSurface from the standard WASM surface buffer
294
- * [degU, degV, numU, numV, cps..., weights..., knotsU..., knotsV...].
295
- */
296
- static fromData(buf: Float64Array | number[]): NurbsSurface {
297
- if (buf.length < 4) throw new Error("NurbsSurface.fromData: buffer too short");
298
- const degU = Number(buf[0] ?? 0);
299
- const degV = Number(buf[1] ?? 0);
300
- const numU = Number(buf[2] ?? 0);
301
- const numV = Number(buf[3] ?? 0);
302
- const totalPts = numU * numV;
303
- const expectedLen = 4 + totalPts * 3 + totalPts + (degU + numU + 1) + (degV + numV + 1);
304
- if (buf.length < expectedLen) throw new Error("NurbsSurface.fromData: buffer too short");
305
- let idx = 4;
306
- const cp: Point[][] = [];
307
- for (let u = 0; u < numU; u++) {
308
- const row: Point[] = [];
309
- for (let v = 0; v < numV; v++) {
310
- row.push(new Point(Number(buf[idx]), Number(buf[idx + 1]), Number(buf[idx + 2])));
311
- idx += 3;
312
- }
313
- cp.push(row);
314
- }
315
- const weights: number[][] = [];
316
- for (let u = 0; u < numU; u++) {
317
- const row: number[] = [];
318
- for (let v = 0; v < numV; v++) { row.push(Number(buf[idx++])); }
319
- weights.push(row);
320
- }
321
- const knotsULen = degU + numU + 1;
322
- const knotsVLen = degV + numV + 1;
323
- const knotsU: number[] = Array.from(buf.slice(idx, idx + knotsULen), (value) => Number(value));
324
- idx += knotsULen;
325
- const knotsV: number[] = Array.from(buf.slice(idx, idx + knotsVLen), (value) => Number(value));
326
- return new NurbsSurface(degU, degV, cp, weights, knotsU, knotsV);
327
- }
328
-
329
- /** Raw WASM surface buffer (advanced use). */
330
- get data(): Float64Array {
331
- return this._data;
332
- }
333
-
334
- /**
335
- * Offset this surface by `distance` along its oriented normal field,
336
- * refit to `tolerance` (default 1e-4) against the exact offset. Rational
337
- * surfaces (cylinders, spheres, revolves) offset exactly; closed seams
338
- * stay closed and degenerate poles stay degenerate.
339
- *
340
- * Throws for creased surfaces or when the offset cannot reach tolerance
341
- * (for example offsetting inward past the curvature radius).
342
- */
343
- offset(distance: number, options: { tolerance?: number } = {}): NurbsSurface {
344
- ensureInit();
345
- const buf = wasm.nurbs_surface_offset(this.data, distance, options.tolerance ?? 1e-4);
346
- if (buf.length < 4) {
347
- throw new Error(
348
- `NurbsSurface.offset() failed: ${wasm.geometry_last_error() || "unknown error"}`,
349
- );
350
- }
351
- return NurbsSurface.fromData(buf);
352
- }
353
-
354
- /**
355
- * Rebuild this surface with the given degrees and control point counts
356
- * (Rhino-style Rebuild): a two-stage global least-squares refit with clean
357
- * approximation knots and chord-length-ish parameterization over
358
- * [0, 1] x [0, 1]. The result is always NON-RATIONAL; closed directions
359
- * stay seamlessly closed (periodic basis); degenerate pole rows (spheres,
360
- * cones of revolution) stay exactly degenerate; open boundaries keep their
361
- * exact corner points.
362
- *
363
- * Returns the rebuilt surface together with the measured deviation from
364
- * the original (max and mean over a dense validation grid).
365
- *
366
- * Throws for invalid degree/count combinations (degree 1..11, counts
367
- * >= degree + 1, per-direction cap 512) or degenerate surfaces.
368
- */
369
- rebuild(options: {
370
- degreeU: number;
371
- degreeV: number;
372
- countU: number;
373
- countV: number;
374
- }): { surface: NurbsSurface; maxDeviation: number; avgDeviation: number } {
375
- ensureInit();
376
- const buf = wasm.nurbs_surface_rebuild(
377
- this.data,
378
- options.degreeU,
379
- options.degreeV,
380
- options.countU,
381
- options.countV,
382
- );
383
- if (buf.length < 6) {
384
- throw new Error(
385
- `NurbsSurface.rebuild() failed: ${wasm.geometry_last_error() || "unknown error"}`,
386
- );
387
- }
388
- return {
389
- maxDeviation: Number(buf[0]),
390
- avgDeviation: Number(buf[1]),
391
- surface: NurbsSurface.fromData(buf.subarray(2)),
392
- };
393
- }
394
-
395
- // ── Analytic queries ──
396
-
397
- /**
398
- * Analytic surface derivatives at (u, v) — exact rational derivatives
399
- * (Piegl & Tiller A4.4), with respect to the normalized [0,1] parameters.
400
- */
401
- derivatives(u: number, v: number): {
402
- point: Point;
403
- du: Vec3;
404
- dv: Vec3;
405
- duu: Vec3;
406
- duv: Vec3;
407
- dvv: Vec3;
408
- } {
409
- ensureInit();
410
- const r = wasm.nurbs_surface_derivatives(this._data, u, v);
411
- if (r.length < 18) throw new Error("NurbsSurface.derivatives failed");
412
- const vec = (i: number) => new Vec3(r[i], r[i + 1], r[i + 2]);
413
- return {
414
- point: new Point(r[0], r[1], r[2]),
415
- du: vec(3),
416
- dv: vec(6),
417
- duu: vec(9),
418
- duv: vec(12),
419
- dvv: vec(15),
420
- };
421
- }
422
-
423
- /**
424
- * Principal curvatures and curvature invariants at (u, v), from the exact
425
- * first/second fundamental forms.
426
- */
427
- curvature(u: number, v: number): {
428
- k1: number;
429
- k2: number;
430
- gaussian: number;
431
- mean: number;
432
- normal: Vec3;
433
- } {
434
- ensureInit();
435
- const r = wasm.nurbs_surface_curvature(this._data, u, v);
436
- if (r.length < 7) throw new Error("NurbsSurface.curvature failed");
437
- return {
438
- k1: r[0],
439
- k2: r[1],
440
- gaussian: r[2],
441
- mean: r[3],
442
- normal: new Vec3(r[4], r[5], r[6]),
443
- };
444
- }
445
-
446
- /**
447
- * Closest point on the surface to `p` (Newton point inversion with grid
448
- * seeding). Returns normalized (u, v) and the surface point.
449
- */
450
- closestPoint(p: Point): { u: number; v: number; point: Point } {
451
- ensureInit();
452
- const r = wasm.nurbs_surface_closest_point(this._data, p.x, p.y, p.z);
453
- if (r.length < 5) throw new Error("NurbsSurface.closestPoint failed");
454
- return { u: r[0], v: r[1], point: new Point(r[2], r[3], r[4]) };
455
- }
456
-
457
- // ── Iso-curves (exact rational extraction) ──
458
-
459
- /**
460
- * Exact iso-parametric curve running along the U direction at constant v.
461
- * Rational surfaces yield exact rational curves (weights preserved).
462
- * @param v - Normalized v parameter [0, 1]
463
- */
464
- isoCurveU(v: number): NurbsCurve {
465
- ensureInit();
466
- const buf = wasm.nurbs_surface_iso_curve(this._data, v, true);
467
- if (buf.length < 2) throw new Error("NurbsSurface.isoCurveU failed");
468
- return NurbsCurve.fromData(buf);
469
- }
470
-
471
- /**
472
- * Exact iso-parametric curve running along the V direction at constant u.
473
- * @param u - Normalized u parameter [0, 1]
474
- */
475
- isoCurveV(u: number): NurbsCurve {
476
- ensureInit();
477
- const buf = wasm.nurbs_surface_iso_curve(this._data, u, false);
478
- if (buf.length < 2) throw new Error("NurbsSurface.isoCurveV failed");
479
- return NurbsCurve.fromData(buf);
480
- }
481
-
482
- // ── Exact refinement / editing ──
483
-
484
- /**
485
- * Exact knot insertion at normalized parameter t (shape-preserving; adds a
486
- * row/column of control points for finer editing).
487
- */
488
- insertKnot(direction: "u" | "v", t: number, times = 1): NurbsSurface {
489
- ensureInit();
490
- const buf = wasm.nurbs_surface_insert_knot(this._data, direction === "u", t, times);
491
- if (buf.length < 4) throw new Error("NurbsSurface.insertKnot failed");
492
- return NurbsSurface.fromData(buf);
493
- }
494
-
495
- /**
496
- * Exact degree elevation by `t` in one direction (shape-preserving).
497
- */
498
- elevateDegree(direction: "u" | "v", t = 1): NurbsSurface {
499
- ensureInit();
500
- const buf = wasm.nurbs_surface_elevate_degree(this._data, direction === "u", t);
501
- if (buf.length < 4) throw new Error("NurbsSurface.elevateDegree failed");
502
- return NurbsSurface.fromData(buf);
503
- }
504
-
505
- /**
506
- * Exact split at normalized parameter t. Returns [lower, upper].
507
- */
508
- split(direction: "u" | "v", t: number): [NurbsSurface, NurbsSurface] {
509
- ensureInit();
510
- const buf = wasm.nurbs_surface_split(this._data, direction === "u", t);
511
- if (buf.length < 2) throw new Error("NurbsSurface.split failed");
512
- const lenA = Number(buf[0]);
513
- const a = buf.slice(1, 1 + lenA);
514
- const lenB = Number(buf[1 + lenA]);
515
- const b = buf.slice(2 + lenA, 2 + lenA + lenB);
516
- return [NurbsSurface.fromData(a), NurbsSurface.fromData(b)];
517
- }
518
-
519
- /**
520
- * Exact sub-patch over normalized parameter ranges.
521
- */
522
- extractRegion(u0: number, u1: number, v0: number, v1: number): NurbsSurface {
523
- ensureInit();
524
- const buf = wasm.nurbs_surface_extract_region(this._data, u0, u1, v0, v1);
525
- if (buf.length < 4) throw new Error("NurbsSurface.extractRegion failed");
526
- return NurbsSurface.fromData(buf);
527
- }
528
-
529
- /**
530
- * Return a copy with one control point (and optionally its weight) replaced —
531
- * the primitive for interactive control-point editing.
532
- */
533
- withControlPoint(i: number, j: number, point: Point, weight?: number): NurbsSurface {
534
- if (i < 0 || i >= this.controlPoints.length || j < 0 || j >= (this.controlPoints[0]?.length ?? 0)) {
535
- throw new Error(`Control point index (${i}, ${j}) out of range`);
536
- }
537
- const cps = this.controlPoints.map((row, ri) =>
538
- row.map((p, rj) => (ri === i && rj === j ? point : p))
539
- );
540
- const weights = this.weights.map((row, ri) =>
541
- row.map((w, rj) => (ri === i && rj === j && weight !== undefined ? weight : w))
542
- );
543
- return new NurbsSurface(this.degreeU, this.degreeV, cps, weights, this.knotsU, this.knotsV);
544
- }
545
-
546
- // ── Tessellation ──
547
-
548
- /**
549
- * Curvature-adaptive tessellation: flat regions stay coarse, curved regions
550
- * refine until the chordal `tolerance` is met.
551
- */
552
- tessellateAdaptive(tolerance: number, maxSegmentsPerSpan = 64): Mesh {
553
- ensureInit();
554
- const buf = wasm.nurbs_surface_tessellate_adaptive(this._data, tolerance, maxSegmentsPerSpan);
555
- if (buf.length < 1) throw new Error("NurbsSurface.tessellateAdaptive failed");
556
- return Mesh.fromBuffer(buf);
557
- }
558
-
559
- /**
560
- * Convert to a mesh sheet for boolean/trim operations with meshes
561
- * (adaptive tessellation at the given chordal tolerance). A sheet
562
- * intersected with a closed solid via `Mesh.intersect`/`Mesh.subtract`
563
- * trims to the portion inside/outside that solid.
564
- */
565
- toMesh(tolerance = 0.01): Mesh {
566
- return this.tessellateAdaptive(tolerance);
567
- }
568
-
569
- /**
570
- * Display-ready render tessellation computed in one kernel pass: a
571
- * twist-aware adaptive grid with ANALYTIC surface normals, plus feature
572
- * edges only where the surface genuinely has them (boundary iso-lines,
573
- * kinked C0 knot lines, kinked closed seams — never quad diagonals).
574
- *
575
- * Prefer this over `tessellateAdaptive(...).buildRenderBuffers(...)` for
576
- * displaying a NURBS surface: the mesh path reconstructs normals from
577
- * triangle geometry and flags spurious crease edges on anisotropic grids.
578
- *
579
- * `positions`/`normals` are non-indexed, three floats per vertex;
580
- * `featureEdges` holds two endpoints (six floats) per segment.
581
- */
582
- tessellateRender(
583
- tolerance: number,
584
- maxSegmentsPerSpan = 64,
585
- creaseAngleDeg = 30
586
- ): { positions: Float32Array; normals: Float32Array; featureEdges: Float32Array } {
587
- ensureInit();
588
- const r: Float32Array = wasm.nurbs_surface_tessellate_render(
589
- this._data,
590
- tolerance,
591
- maxSegmentsPerSpan,
592
- creaseAngleDeg
593
- );
594
- const vertexCount = Math.max(0, Math.floor(r[0] ?? 0));
595
- const posStart = 1;
596
- const norStart = posStart + vertexCount * 3;
597
- const edgeCountIndex = norStart + vertexCount * 3;
598
- const edgeCount = Math.max(0, Math.floor(r[edgeCountIndex] ?? 0));
599
- const edgeStart = edgeCountIndex + 1;
600
- return {
601
- positions: r.subarray(posStart, posStart + vertexCount * 3),
602
- normals: r.subarray(norStart, norStart + vertexCount * 3),
603
- featureEdges: r.subarray(edgeStart, edgeStart + edgeCount * 6),
604
- };
605
- }
606
-
607
- // ── Exact constructors ──
608
-
609
- /**
610
- * Exact surface of revolution: revolve a profile curve around an axis.
611
- * The circular direction is exactly rational (true circles, not approximations).
612
- * @param angle - Revolve angle in radians (up to 2π)
613
- */
614
- static revolve(profile: NurbsCurve, origin: Point, axis: Vec3, angle: number): NurbsSurface {
615
- ensureInit();
616
- const buf = wasm.nurbs_surface_revolve(
617
- profile.data,
618
- origin.x, origin.y, origin.z,
619
- axis.x, axis.y, axis.z,
620
- angle
621
- );
622
- if (buf.length < 4) throw new Error("NurbsSurface.revolve failed");
623
- return NurbsSurface.fromData(buf);
624
- }
625
-
626
- /** Exact extruded (translational) surface from a profile curve. */
627
- static extrudeCurve(profile: NurbsCurve, direction: Vec3): NurbsSurface {
628
- ensureInit();
629
- const buf = wasm.nurbs_surface_extrude(profile.data, direction.x, direction.y, direction.z);
630
- if (buf.length < 4) throw new Error("NurbsSurface.extrudeCurve failed");
631
- return NurbsSurface.fromData(buf);
632
- }
633
-
634
- /** Exact ruled surface between two curves (degrees/knots unified automatically). */
635
- static ruled(a: NurbsCurve, b: NurbsCurve): NurbsSurface {
636
- ensureInit();
637
- const buf = wasm.nurbs_surface_ruled(a.data, b.data);
638
- if (buf.length < 4) throw new Error("NurbsSurface.ruled failed");
639
- return NurbsSurface.fromData(buf);
640
- }
641
-
642
- /** Exact rational cylinder side surface. */
643
- static cylinder(base: Point, axis: Vec3, radius: number, height: number): NurbsSurface {
644
- ensureInit();
645
- const buf = wasm.nurbs_surface_primitive(
646
- "cylinder",
647
- new Float64Array([base.x, base.y, base.z, axis.x, axis.y, axis.z, radius, height])
648
- );
649
- if (buf.length < 4) throw new Error("NurbsSurface.cylinder failed");
650
- return NurbsSurface.fromData(buf);
651
- }
652
-
653
- /** Exact rational sphere surface. */
654
- static sphere(center: Point, radius: number): NurbsSurface {
655
- ensureInit();
656
- const buf = wasm.nurbs_surface_primitive(
657
- "sphere",
658
- new Float64Array([center.x, center.y, center.z, radius])
659
- );
660
- if (buf.length < 4) throw new Error("NurbsSurface.sphere failed");
661
- return NurbsSurface.fromData(buf);
662
- }
663
-
664
- /** Exact rational cone/frustum side surface (radius1 may be 0). */
665
- static cone(base: Point, axis: Vec3, radius0: number, radius1: number, height: number): NurbsSurface {
666
- ensureInit();
667
- const buf = wasm.nurbs_surface_primitive(
668
- "cone",
669
- new Float64Array([base.x, base.y, base.z, axis.x, axis.y, axis.z, radius0, radius1, height])
670
- );
671
- if (buf.length < 4) throw new Error("NurbsSurface.cone failed");
672
- return NurbsSurface.fromData(buf);
673
- }
674
-
675
- /** Exact rational torus surface (axis +Z through center). */
676
- static torus(center: Point, major: number, minor: number): NurbsSurface {
677
- ensureInit();
678
- const buf = wasm.nurbs_surface_primitive(
679
- "torus",
680
- new Float64Array([center.x, center.y, center.z, major, minor])
681
- );
682
- if (buf.length < 4) throw new Error("NurbsSurface.torus failed");
683
- return NurbsSurface.fromData(buf);
684
- }
685
-
686
- /** Encode to WASM format */
687
- private static encode(
688
- degU: number,
689
- degV: number,
690
- controlPoints: Point[][],
691
- weights: number[][],
692
- knotsU: number[],
693
- knotsV: number[]
694
- ): Float64Array {
695
- const numU = controlPoints.length;
696
- const numV = controlPoints[0]?.length ?? 0;
697
- const totalPts = numU * numV;
698
- const data = new Float64Array(4 + totalPts * 3 + totalPts + knotsU.length + knotsV.length);
699
- data[0] = degU;
700
- data[1] = degV;
701
- data[2] = numU;
702
- data[3] = numV;
703
- let idx = 4;
704
- for (let u = 0; u < numU; u++) {
705
- for (let v = 0; v < numV; v++) {
706
- const p = controlPoints[u][v];
707
- data[idx++] = p.x;
708
- data[idx++] = p.y;
709
- data[idx++] = p.z;
710
- }
711
- }
712
- for (let u = 0; u < numU; u++) {
713
- for (let v = 0; v < numV; v++) {
714
- data[idx++] = weights[u][v];
715
- }
716
- }
717
- for (const k of knotsU) data[idx++] = k;
718
- for (const k of knotsV) data[idx++] = k;
719
- return data;
720
- }
721
- }
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 { Polygon } from "./Polygon.js";
8
+ import { Line } from "./Line.js";
9
+ import { Arc } from "./Arc.js";
10
+ import { PolyCurve } from "./PolyCurve.js";
11
+ import { NurbsCurve } from "./NurbsCurve.js";
12
+ import type { Surface } from "./Surface.js";
13
+ import type { LoftableCurve, RotationAxis } from "./types.js";
14
+ import {
15
+ decodePointResult,
16
+ decodeVec3Result,
17
+ parsePolylineBuffer as parsePolylineBuf,
18
+ } from "./BufferCodec.js";
19
+ import { wasm } from "./wasm-active.js";
20
+
21
+ /**
22
+ * Non-Uniform Rational B-Spline (NURBS) surface backed by WASM.
23
+ * Tensor-product surface defined by a 2D grid of control points.
24
+ *
25
+ * WASM data format: [degree_u, degree_v, num_u, num_v, ...cp(flat)..., ...weights..., ...knots_u..., ...knots_v...]
26
+ */
27
+ export class NurbsSurface implements Surface {
28
+ private _data: Float64Array;
29
+
30
+ /**
31
+ * Create a new NURBS surface.
32
+ * @param degreeU - Polynomial degree in U direction
33
+ * @param degreeV - Polynomial degree in V direction
34
+ * @param controlPoints - 2D grid of control points [u][v]
35
+ * @param weights - 2D grid of weights corresponding to control points
36
+ * @param knotsU - Knot vector in U direction
37
+ * @param knotsV - Knot vector in V direction
38
+ */
39
+ constructor(
40
+ public readonly degreeU: number,
41
+ public readonly degreeV: number,
42
+ public readonly controlPoints: Point[][],
43
+ public readonly weights: number[][],
44
+ public readonly knotsU: number[],
45
+ public readonly knotsV: number[]
46
+ ) {
47
+ this._data = NurbsSurface.encode(degreeU, degreeV, controlPoints, weights, knotsU, knotsV);
48
+ }
49
+
50
+ /**
51
+ * Rotate this surface around an axis.
52
+ * @param axis - Rotation axis (Vec3 through origin, or Line for arbitrary axis)
53
+ * @param angle - Rotation angle in radians
54
+ * @returns New rotated surface
55
+ */
56
+ rotate(axis: RotationAxis, angle: number): NurbsSurface {
57
+ return new NurbsSurface(
58
+ this.degreeU,
59
+ this.degreeV,
60
+ this.controlPoints.map(row => row.map(p => p.rotate(axis, angle))),
61
+ this.weights,
62
+ this.knotsU,
63
+ this.knotsV
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Translate this surface by an offset vector.
69
+ * @param offset - Translation vector
70
+ * @returns New translated surface
71
+ */
72
+ translate(offset: Vec3): NurbsSurface {
73
+ const newCps = this.controlPoints.map(row => row.map(p => p.add(offset)));
74
+ return new NurbsSurface(
75
+ this.degreeU,
76
+ this.degreeV,
77
+ newCps,
78
+ this.weights,
79
+ this.knotsU,
80
+ this.knotsV
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Apply a row-major 4x4 AFFINE matrix to this surface. NURBS are invariant
86
+ * under affine maps of their control net, so rigid transforms, scaling
87
+ * (uniform or not) and shear are EXACT — weights and knots are unchanged.
88
+ * @param matrix - Row-major 4x4 matrix (16 entries)
89
+ * @returns New transformed surface
90
+ */
91
+ applyMatrix(matrix: number[] | Float64Array): NurbsSurface {
92
+ const newCps = this.controlPoints.map(row => row.map(p => p.applyMatrix(matrix)));
93
+ return new NurbsSurface(
94
+ this.degreeU,
95
+ this.degreeV,
96
+ newCps,
97
+ this.weights,
98
+ this.knotsU,
99
+ this.knotsV
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Evaluate a point on the surface at parameters (u, v).
105
+ * @param u - Parameter in U direction [0, 1]
106
+ * @param v - Parameter in V direction [0, 1]
107
+ * @returns Point on surface at (u, v)
108
+ */
109
+ evaluate(u: number, v: number): Point {
110
+ ensureInit();
111
+ return decodePointResult(wasm.nurbs_surface_evaluate(this._data, u, v));
112
+ }
113
+
114
+ /**
115
+ * Evaluate the unit normal on the surface at parameters (u, v).
116
+ * @param u - Parameter in U direction [0, 1]
117
+ * @param v - Parameter in V direction [0, 1]
118
+ * @returns Unit surface normal at (u, v)
119
+ */
120
+ normal(u: number, v: number): Vec3 {
121
+ ensureInit();
122
+ return decodeVec3Result(wasm.nurbs_surface_normal(this._data, u, v));
123
+ }
124
+
125
+ /**
126
+ * Tessellate this surface into a triangle mesh.
127
+ * @param uSegs - Number of segments in U direction
128
+ * @param vSegs - Number of segments in V direction
129
+ * @returns Mesh approximating the surface
130
+ */
131
+ tessellate(uSegs: number, vSegs: number): Mesh {
132
+ ensureInit();
133
+ const full = new Float64Array(this._data.length + 2);
134
+ full.set(this._data);
135
+ full[this._data.length] = uSegs;
136
+ full[this._data.length + 1] = vSegs;
137
+ return Mesh.fromBuffer(wasm.tessellate_nurbs_surface(full));
138
+ }
139
+
140
+ /**
141
+ * Find intersection curves with a plane.
142
+ * @param plane - Cutting plane
143
+ * @param tess - Tessellation resolution for intersection detection (default 20)
144
+ * @returns Array of polylines representing intersection curves
145
+ */
146
+ intersectPlane(plane: Plane, tess = 20): Polyline[] {
147
+ ensureInit();
148
+ const buf = wasm.nurbs_surface_plane_intersect(
149
+ this._data,
150
+ plane.normal.x, plane.normal.y, plane.normal.z,
151
+ plane.d,
152
+ tess
153
+ );
154
+ return parsePolylineBuf(buf).map(pts => new Polyline(pts));
155
+ }
156
+
157
+ /**
158
+ * Find intersection curves with another NURBS surface.
159
+ * @param other - Other surface to intersect with
160
+ * @param tess - Tessellation resolution for intersection detection (default 20)
161
+ * @returns Array of polylines representing intersection curves
162
+ */
163
+ intersectSurface(other: NurbsSurface, tess = 20): Polyline[] {
164
+ ensureInit();
165
+ const buf = wasm.nurbs_surface_surface_intersect(this._data, other._data, tess);
166
+ return parsePolylineBuf(buf).map(pts => new Polyline(pts));
167
+ }
168
+
169
+ /**
170
+ * EXACT intersection curves with another NURBS surface.
171
+ *
172
+ * Unlike {@link intersectSurface} (tessellation-based, returns polylines at
173
+ * a fixed sampling resolution), this runs the parametric boolean's
174
+ * surface–surface intersection pipeline — seeded tri-tri detection,
175
+ * chaining, and Newton/tangent-plane relaxation of every point onto the
176
+ * true intersection — and fits each chain to an interpolating NURBS curve,
177
+ * densified until the fitted curve stays within `tolerance` of the exact
178
+ * intersection.
179
+ *
180
+ * Closed intersection loops (e.g. cylinder × plane) come back as ONE closed
181
+ * curve. Coincident (overlapping) surfaces return an empty array — their
182
+ * overlap is a 2D region, not a curve set.
183
+ *
184
+ * @param other - Other surface to intersect with
185
+ * @param tolerance - Absolute geometric tolerance of the fitted curves
186
+ * (model units; floored relative to the combined model scale)
187
+ * @returns Exact fitted NURBS intersection curves
188
+ */
189
+ intersectSurfaceCurves(other: NurbsSurface, tolerance = 1e-6): NurbsCurve[] {
190
+ ensureInit();
191
+ const buf = wasm.nurbs_surface_surface_intersect_curves(this._data, other._data, tolerance);
192
+ return NurbsSurface.parseCurveList(buf);
193
+ }
194
+
195
+ /** Parse [count, (bufLen, curveData…)·count] into NurbsCurves. */
196
+ private static parseCurveList(buf: Float64Array): NurbsCurve[] {
197
+ if (buf.length < 1) return [];
198
+ const out: NurbsCurve[] = [];
199
+ const count = Number(buf[0]);
200
+ let offset = 1;
201
+ for (let k = 0; k < count && offset < buf.length; k++) {
202
+ const len = Number(buf[offset]);
203
+ offset += 1;
204
+ out.push(NurbsCurve.fromData(buf.subarray(offset, offset + len)));
205
+ offset += len;
206
+ }
207
+ return out;
208
+ }
209
+
210
+ /**
211
+ * Convert any supported curve type to a NurbsCurve.
212
+ * @param c - Curve to convert
213
+ * @returns NurbsCurve representation
214
+ */
215
+ private static toNurbs(c: LoftableCurve): NurbsCurve {
216
+ if (c instanceof NurbsCurve) return c;
217
+ if (c instanceof PolyCurve) return c.toNurbs();
218
+ if (c instanceof Polygon || c instanceof Polyline) return PolyCurve.byPoints((c as Polyline).points).toNurbs();
219
+ if (c instanceof Arc) return NurbsCurve.fromArc(c);
220
+ if (c instanceof Line) return NurbsCurve.fromLine(c);
221
+ throw new Error(`Unsupported curve type for loft`);
222
+ }
223
+
224
+ /**
225
+ * Loft a NURBS surface through multiple curves.
226
+ * Accepts NurbsCurve, PolyCurve, Polyline, Polygon, Arc, or Line.
227
+ * Automatically converts to NURBS and unifies knot vectors.
228
+ *
229
+ * @param curves - Array of curves to loft through (minimum 2)
230
+ * @param degreeV - Polynomial degree in the loft direction (default 3)
231
+ * @returns New NurbsSurface passing through all input curves
232
+ */
233
+ static loftCurves(curves: LoftableCurve[], degreeV = 3): NurbsSurface {
234
+ ensureInit();
235
+
236
+ // Step 0: Convert all curves to NurbsCurve
237
+ const nurbsCurves = curves.map(c => NurbsSurface.toNurbs(c));
238
+
239
+ // Step 1: Make curves compatible (same degree, unified knot vectors,
240
+ // same CP count). Mixed degrees are fine — the kernel degree-elevates
241
+ // lower-degree curves to the max degree exactly (a 2-point line lofts
242
+ // with a cubic).
243
+ let loftCurves = nurbsCurves;
244
+ if (nurbsCurves.length >= 2) {
245
+ const ref = nurbsCurves[0];
246
+ const matchesRef = (c: NurbsCurve) =>
247
+ c.degree === ref.degree &&
248
+ c.controlPoints.length === ref.controlPoints.length &&
249
+ c.knots.length === ref.knots.length &&
250
+ c.knots.every((k, i) => Math.abs(k - (ref.knots[i] as number)) < 1e-12);
251
+ const allSame = nurbsCurves.every(matchesRef);
252
+ if (!allSame) {
253
+ const compatParts: number[] = [nurbsCurves.length];
254
+ for (const c of nurbsCurves) {
255
+ compatParts.push(c.degree, c.controlPoints.length);
256
+ for (const p of c.controlPoints) { compatParts.push(p.x, p.y, p.z); }
257
+ for (const w of c.weights) { compatParts.push(w); }
258
+ for (const k of c.knots) { compatParts.push(k); }
259
+ }
260
+ const compatBuf = wasm.make_nurbs_curves_compatible(new Float64Array(compatParts));
261
+ if (compatBuf.length === 0) {
262
+ throw new Error("NurbsSurface loft: profile curves could not be made compatible");
263
+ }
264
+ const count = Number(compatBuf[0] ?? 0);
265
+ const decoded: NurbsCurve[] = [];
266
+ let off = 1;
267
+ for (let i = 0; i < count; i++) {
268
+ const deg = Number(compatBuf[off] ?? 0);
269
+ const n = Number(compatBuf[off + 1] ?? 0);
270
+ const curveLen = 2 + n * 3 + n + (n + deg + 1);
271
+ decoded.push(NurbsCurve.fromData(compatBuf.slice(off, off + curveLen)));
272
+ off += curveLen;
273
+ }
274
+ loftCurves = decoded;
275
+ }
276
+ }
277
+
278
+ // Step 2: Encode for loft_nurbs_surface
279
+ const parts: number[] = [loftCurves.length, degreeV];
280
+ for (const c of loftCurves) {
281
+ const pts = c.controlPoints;
282
+ parts.push(c.degree, pts.length);
283
+ for (const p of pts) { parts.push(p.x, p.y, p.z); }
284
+ for (const w of c.weights) { parts.push(w); }
285
+ for (const k of c.knots) { parts.push(k); }
286
+ }
287
+ const buf = wasm.loft_nurbs_surface(new Float64Array(parts));
288
+ if (buf.length < 4) throw new Error("NurbsSurface loft failed");
289
+ return NurbsSurface.fromData(buf);
290
+ }
291
+
292
+ /**
293
+ * Decode a NurbsSurface from the standard WASM surface buffer
294
+ * [degU, degV, numU, numV, cps..., weights..., knotsU..., knotsV...].
295
+ */
296
+ static fromData(buf: Float64Array | number[]): NurbsSurface {
297
+ if (buf.length < 4) throw new Error("NurbsSurface.fromData: buffer too short");
298
+ const degU = Number(buf[0] ?? 0);
299
+ const degV = Number(buf[1] ?? 0);
300
+ const numU = Number(buf[2] ?? 0);
301
+ const numV = Number(buf[3] ?? 0);
302
+ const totalPts = numU * numV;
303
+ const expectedLen = 4 + totalPts * 3 + totalPts + (degU + numU + 1) + (degV + numV + 1);
304
+ if (buf.length < expectedLen) throw new Error("NurbsSurface.fromData: buffer too short");
305
+ let idx = 4;
306
+ const cp: Point[][] = [];
307
+ for (let u = 0; u < numU; u++) {
308
+ const row: Point[] = [];
309
+ for (let v = 0; v < numV; v++) {
310
+ row.push(new Point(Number(buf[idx]), Number(buf[idx + 1]), Number(buf[idx + 2])));
311
+ idx += 3;
312
+ }
313
+ cp.push(row);
314
+ }
315
+ const weights: number[][] = [];
316
+ for (let u = 0; u < numU; u++) {
317
+ const row: number[] = [];
318
+ for (let v = 0; v < numV; v++) { row.push(Number(buf[idx++])); }
319
+ weights.push(row);
320
+ }
321
+ const knotsULen = degU + numU + 1;
322
+ const knotsVLen = degV + numV + 1;
323
+ const knotsU: number[] = Array.from(buf.slice(idx, idx + knotsULen), (value) => Number(value));
324
+ idx += knotsULen;
325
+ const knotsV: number[] = Array.from(buf.slice(idx, idx + knotsVLen), (value) => Number(value));
326
+ return new NurbsSurface(degU, degV, cp, weights, knotsU, knotsV);
327
+ }
328
+
329
+ /** Raw WASM surface buffer (advanced use). */
330
+ get data(): Float64Array {
331
+ return this._data;
332
+ }
333
+
334
+ /**
335
+ * Offset this surface by `distance` along its oriented normal field,
336
+ * refit to `tolerance` (default 1e-4) against the exact offset. Rational
337
+ * surfaces (cylinders, spheres, revolves) offset exactly; closed seams
338
+ * stay closed and degenerate poles stay degenerate.
339
+ *
340
+ * Throws for creased surfaces or when the offset cannot reach tolerance
341
+ * (for example offsetting inward past the curvature radius).
342
+ */
343
+ offset(distance: number, options: { tolerance?: number } = {}): NurbsSurface {
344
+ ensureInit();
345
+ const buf = wasm.nurbs_surface_offset(this.data, distance, options.tolerance ?? 1e-4);
346
+ if (buf.length < 4) {
347
+ throw new Error(
348
+ `NurbsSurface.offset() failed: ${wasm.geometry_last_error() || "unknown error"}`,
349
+ );
350
+ }
351
+ return NurbsSurface.fromData(buf);
352
+ }
353
+
354
+ /**
355
+ * Rebuild this surface with the given degrees and control point counts
356
+ * (Rhino-style Rebuild): a two-stage global least-squares refit with clean
357
+ * approximation knots and chord-length-ish parameterization over
358
+ * [0, 1] x [0, 1]. The result is always NON-RATIONAL; closed directions
359
+ * stay seamlessly closed (periodic basis); degenerate pole rows (spheres,
360
+ * cones of revolution) stay exactly degenerate; open boundaries keep their
361
+ * exact corner points.
362
+ *
363
+ * Returns the rebuilt surface together with the measured deviation from
364
+ * the original (max and mean over a dense validation grid).
365
+ *
366
+ * Throws for invalid degree/count combinations (degree 1..11, counts
367
+ * >= degree + 1, per-direction cap 512) or degenerate surfaces.
368
+ */
369
+ rebuild(options: {
370
+ degreeU: number;
371
+ degreeV: number;
372
+ countU: number;
373
+ countV: number;
374
+ }): { surface: NurbsSurface; maxDeviation: number; avgDeviation: number } {
375
+ ensureInit();
376
+ const buf = wasm.nurbs_surface_rebuild(
377
+ this.data,
378
+ options.degreeU,
379
+ options.degreeV,
380
+ options.countU,
381
+ options.countV,
382
+ );
383
+ if (buf.length < 6) {
384
+ throw new Error(
385
+ `NurbsSurface.rebuild() failed: ${wasm.geometry_last_error() || "unknown error"}`,
386
+ );
387
+ }
388
+ return {
389
+ maxDeviation: Number(buf[0]),
390
+ avgDeviation: Number(buf[1]),
391
+ surface: NurbsSurface.fromData(buf.subarray(2)),
392
+ };
393
+ }
394
+
395
+ // ── Analytic queries ──
396
+
397
+ /**
398
+ * Analytic surface derivatives at (u, v) — exact rational derivatives
399
+ * (Piegl & Tiller A4.4), with respect to the normalized [0,1] parameters.
400
+ */
401
+ derivatives(u: number, v: number): {
402
+ point: Point;
403
+ du: Vec3;
404
+ dv: Vec3;
405
+ duu: Vec3;
406
+ duv: Vec3;
407
+ dvv: Vec3;
408
+ } {
409
+ ensureInit();
410
+ const r = wasm.nurbs_surface_derivatives(this._data, u, v);
411
+ if (r.length < 18) throw new Error("NurbsSurface.derivatives failed");
412
+ const vec = (i: number) => new Vec3(r[i], r[i + 1], r[i + 2]);
413
+ return {
414
+ point: new Point(r[0], r[1], r[2]),
415
+ du: vec(3),
416
+ dv: vec(6),
417
+ duu: vec(9),
418
+ duv: vec(12),
419
+ dvv: vec(15),
420
+ };
421
+ }
422
+
423
+ /**
424
+ * Principal curvatures and curvature invariants at (u, v), from the exact
425
+ * first/second fundamental forms.
426
+ */
427
+ curvature(u: number, v: number): {
428
+ k1: number;
429
+ k2: number;
430
+ gaussian: number;
431
+ mean: number;
432
+ normal: Vec3;
433
+ } {
434
+ ensureInit();
435
+ const r = wasm.nurbs_surface_curvature(this._data, u, v);
436
+ if (r.length < 7) throw new Error("NurbsSurface.curvature failed");
437
+ return {
438
+ k1: r[0],
439
+ k2: r[1],
440
+ gaussian: r[2],
441
+ mean: r[3],
442
+ normal: new Vec3(r[4], r[5], r[6]),
443
+ };
444
+ }
445
+
446
+ /**
447
+ * Closest point on the surface to `p` (Newton point inversion with grid
448
+ * seeding). Returns normalized (u, v) and the surface point.
449
+ */
450
+ closestPoint(p: Point): { u: number; v: number; point: Point } {
451
+ ensureInit();
452
+ const r = wasm.nurbs_surface_closest_point(this._data, p.x, p.y, p.z);
453
+ if (r.length < 5) throw new Error("NurbsSurface.closestPoint failed");
454
+ return { u: r[0], v: r[1], point: new Point(r[2], r[3], r[4]) };
455
+ }
456
+
457
+ // ── Iso-curves (exact rational extraction) ──
458
+
459
+ /**
460
+ * Exact iso-parametric curve running along the U direction at constant v.
461
+ * Rational surfaces yield exact rational curves (weights preserved).
462
+ * @param v - Normalized v parameter [0, 1]
463
+ */
464
+ isoCurveU(v: number): NurbsCurve {
465
+ ensureInit();
466
+ const buf = wasm.nurbs_surface_iso_curve(this._data, v, true);
467
+ if (buf.length < 2) throw new Error("NurbsSurface.isoCurveU failed");
468
+ return NurbsCurve.fromData(buf);
469
+ }
470
+
471
+ /**
472
+ * Exact iso-parametric curve running along the V direction at constant u.
473
+ * @param u - Normalized u parameter [0, 1]
474
+ */
475
+ isoCurveV(u: number): NurbsCurve {
476
+ ensureInit();
477
+ const buf = wasm.nurbs_surface_iso_curve(this._data, u, false);
478
+ if (buf.length < 2) throw new Error("NurbsSurface.isoCurveV failed");
479
+ return NurbsCurve.fromData(buf);
480
+ }
481
+
482
+ // ── Exact refinement / editing ──
483
+
484
+ /**
485
+ * Exact knot insertion at normalized parameter t (shape-preserving; adds a
486
+ * row/column of control points for finer editing).
487
+ */
488
+ insertKnot(direction: "u" | "v", t: number, times = 1): NurbsSurface {
489
+ ensureInit();
490
+ const buf = wasm.nurbs_surface_insert_knot(this._data, direction === "u", t, times);
491
+ if (buf.length < 4) throw new Error("NurbsSurface.insertKnot failed");
492
+ return NurbsSurface.fromData(buf);
493
+ }
494
+
495
+ /**
496
+ * Exact degree elevation by `t` in one direction (shape-preserving).
497
+ */
498
+ elevateDegree(direction: "u" | "v", t = 1): NurbsSurface {
499
+ ensureInit();
500
+ const buf = wasm.nurbs_surface_elevate_degree(this._data, direction === "u", t);
501
+ if (buf.length < 4) throw new Error("NurbsSurface.elevateDegree failed");
502
+ return NurbsSurface.fromData(buf);
503
+ }
504
+
505
+ /**
506
+ * Exact split at normalized parameter t. Returns [lower, upper].
507
+ */
508
+ split(direction: "u" | "v", t: number): [NurbsSurface, NurbsSurface] {
509
+ ensureInit();
510
+ const buf = wasm.nurbs_surface_split(this._data, direction === "u", t);
511
+ if (buf.length < 2) throw new Error("NurbsSurface.split failed");
512
+ const lenA = Number(buf[0]);
513
+ const a = buf.slice(1, 1 + lenA);
514
+ const lenB = Number(buf[1 + lenA]);
515
+ const b = buf.slice(2 + lenA, 2 + lenA + lenB);
516
+ return [NurbsSurface.fromData(a), NurbsSurface.fromData(b)];
517
+ }
518
+
519
+ /**
520
+ * Exact sub-patch over normalized parameter ranges.
521
+ */
522
+ extractRegion(u0: number, u1: number, v0: number, v1: number): NurbsSurface {
523
+ ensureInit();
524
+ const buf = wasm.nurbs_surface_extract_region(this._data, u0, u1, v0, v1);
525
+ if (buf.length < 4) throw new Error("NurbsSurface.extractRegion failed");
526
+ return NurbsSurface.fromData(buf);
527
+ }
528
+
529
+ /**
530
+ * Return a copy with one control point (and optionally its weight) replaced —
531
+ * the primitive for interactive control-point editing.
532
+ */
533
+ withControlPoint(i: number, j: number, point: Point, weight?: number): NurbsSurface {
534
+ if (i < 0 || i >= this.controlPoints.length || j < 0 || j >= (this.controlPoints[0]?.length ?? 0)) {
535
+ throw new Error(`Control point index (${i}, ${j}) out of range`);
536
+ }
537
+ const cps = this.controlPoints.map((row, ri) =>
538
+ row.map((p, rj) => (ri === i && rj === j ? point : p))
539
+ );
540
+ const weights = this.weights.map((row, ri) =>
541
+ row.map((w, rj) => (ri === i && rj === j && weight !== undefined ? weight : w))
542
+ );
543
+ return new NurbsSurface(this.degreeU, this.degreeV, cps, weights, this.knotsU, this.knotsV);
544
+ }
545
+
546
+ // ── Tessellation ──
547
+
548
+ /**
549
+ * Curvature-adaptive tessellation: flat regions stay coarse, curved regions
550
+ * refine until the chordal `tolerance` is met.
551
+ */
552
+ tessellateAdaptive(tolerance: number, maxSegmentsPerSpan = 64): Mesh {
553
+ ensureInit();
554
+ const buf = wasm.nurbs_surface_tessellate_adaptive(this._data, tolerance, maxSegmentsPerSpan);
555
+ if (buf.length < 1) throw new Error("NurbsSurface.tessellateAdaptive failed");
556
+ return Mesh.fromBuffer(buf);
557
+ }
558
+
559
+ /**
560
+ * Convert to a mesh sheet for boolean/trim operations with meshes
561
+ * (adaptive tessellation at the given chordal tolerance). A sheet
562
+ * intersected with a closed solid via `Mesh.intersect`/`Mesh.subtract`
563
+ * trims to the portion inside/outside that solid.
564
+ */
565
+ toMesh(tolerance = 0.01): Mesh {
566
+ return this.tessellateAdaptive(tolerance);
567
+ }
568
+
569
+ /**
570
+ * Display-ready render tessellation computed in one kernel pass: a
571
+ * twist-aware adaptive grid with ANALYTIC surface normals, plus feature
572
+ * edges only where the surface genuinely has them (boundary iso-lines,
573
+ * kinked C0 knot lines, kinked closed seams — never quad diagonals).
574
+ *
575
+ * Prefer this over `tessellateAdaptive(...).buildRenderBuffers(...)` for
576
+ * displaying a NURBS surface: the mesh path reconstructs normals from
577
+ * triangle geometry and flags spurious crease edges on anisotropic grids.
578
+ *
579
+ * `positions`/`normals` are non-indexed, three floats per vertex;
580
+ * `featureEdges` holds two endpoints (six floats) per segment.
581
+ */
582
+ tessellateRender(
583
+ tolerance: number,
584
+ maxSegmentsPerSpan = 64,
585
+ creaseAngleDeg = 30
586
+ ): { positions: Float32Array; normals: Float32Array; featureEdges: Float32Array } {
587
+ ensureInit();
588
+ const r: Float32Array = wasm.nurbs_surface_tessellate_render(
589
+ this._data,
590
+ tolerance,
591
+ maxSegmentsPerSpan,
592
+ creaseAngleDeg
593
+ );
594
+ const vertexCount = Math.max(0, Math.floor(r[0] ?? 0));
595
+ const posStart = 1;
596
+ const norStart = posStart + vertexCount * 3;
597
+ const edgeCountIndex = norStart + vertexCount * 3;
598
+ const edgeCount = Math.max(0, Math.floor(r[edgeCountIndex] ?? 0));
599
+ const edgeStart = edgeCountIndex + 1;
600
+ return {
601
+ positions: r.subarray(posStart, posStart + vertexCount * 3),
602
+ normals: r.subarray(norStart, norStart + vertexCount * 3),
603
+ featureEdges: r.subarray(edgeStart, edgeStart + edgeCount * 6),
604
+ };
605
+ }
606
+
607
+ // ── Exact constructors ──
608
+
609
+ /**
610
+ * Exact surface of revolution: revolve a profile curve around an axis.
611
+ * The circular direction is exactly rational (true circles, not approximations).
612
+ * @param angle - Revolve angle in radians (up to 2π)
613
+ */
614
+ static revolve(profile: NurbsCurve, origin: Point, axis: Vec3, angle: number): NurbsSurface {
615
+ ensureInit();
616
+ const buf = wasm.nurbs_surface_revolve(
617
+ profile.data,
618
+ origin.x, origin.y, origin.z,
619
+ axis.x, axis.y, axis.z,
620
+ angle
621
+ );
622
+ if (buf.length < 4) throw new Error("NurbsSurface.revolve failed");
623
+ return NurbsSurface.fromData(buf);
624
+ }
625
+
626
+ /** Exact extruded (translational) surface from a profile curve. */
627
+ static extrudeCurve(profile: NurbsCurve, direction: Vec3): NurbsSurface {
628
+ ensureInit();
629
+ const buf = wasm.nurbs_surface_extrude(profile.data, direction.x, direction.y, direction.z);
630
+ if (buf.length < 4) throw new Error("NurbsSurface.extrudeCurve failed");
631
+ return NurbsSurface.fromData(buf);
632
+ }
633
+
634
+ /** Exact ruled surface between two curves (degrees/knots unified automatically). */
635
+ static ruled(a: NurbsCurve, b: NurbsCurve): NurbsSurface {
636
+ ensureInit();
637
+ const buf = wasm.nurbs_surface_ruled(a.data, b.data);
638
+ if (buf.length < 4) throw new Error("NurbsSurface.ruled failed");
639
+ return NurbsSurface.fromData(buf);
640
+ }
641
+
642
+ /** Exact rational cylinder side surface. */
643
+ static cylinder(base: Point, axis: Vec3, radius: number, height: number): NurbsSurface {
644
+ ensureInit();
645
+ const buf = wasm.nurbs_surface_primitive(
646
+ "cylinder",
647
+ new Float64Array([base.x, base.y, base.z, axis.x, axis.y, axis.z, radius, height])
648
+ );
649
+ if (buf.length < 4) throw new Error("NurbsSurface.cylinder failed");
650
+ return NurbsSurface.fromData(buf);
651
+ }
652
+
653
+ /** Exact rational sphere surface. */
654
+ static sphere(center: Point, radius: number): NurbsSurface {
655
+ ensureInit();
656
+ const buf = wasm.nurbs_surface_primitive(
657
+ "sphere",
658
+ new Float64Array([center.x, center.y, center.z, radius])
659
+ );
660
+ if (buf.length < 4) throw new Error("NurbsSurface.sphere failed");
661
+ return NurbsSurface.fromData(buf);
662
+ }
663
+
664
+ /** Exact rational cone/frustum side surface (radius1 may be 0). */
665
+ static cone(base: Point, axis: Vec3, radius0: number, radius1: number, height: number): NurbsSurface {
666
+ ensureInit();
667
+ const buf = wasm.nurbs_surface_primitive(
668
+ "cone",
669
+ new Float64Array([base.x, base.y, base.z, axis.x, axis.y, axis.z, radius0, radius1, height])
670
+ );
671
+ if (buf.length < 4) throw new Error("NurbsSurface.cone failed");
672
+ return NurbsSurface.fromData(buf);
673
+ }
674
+
675
+ /** Exact rational torus surface (axis +Z through center). */
676
+ static torus(center: Point, major: number, minor: number): NurbsSurface {
677
+ ensureInit();
678
+ const buf = wasm.nurbs_surface_primitive(
679
+ "torus",
680
+ new Float64Array([center.x, center.y, center.z, major, minor])
681
+ );
682
+ if (buf.length < 4) throw new Error("NurbsSurface.torus failed");
683
+ return NurbsSurface.fromData(buf);
684
+ }
685
+
686
+ /** Encode to WASM format */
687
+ private static encode(
688
+ degU: number,
689
+ degV: number,
690
+ controlPoints: Point[][],
691
+ weights: number[][],
692
+ knotsU: number[],
693
+ knotsV: number[]
694
+ ): Float64Array {
695
+ const numU = controlPoints.length;
696
+ const numV = controlPoints[0]?.length ?? 0;
697
+ const totalPts = numU * numV;
698
+ const data = new Float64Array(4 + totalPts * 3 + totalPts + knotsU.length + knotsV.length);
699
+ data[0] = degU;
700
+ data[1] = degV;
701
+ data[2] = numU;
702
+ data[3] = numV;
703
+ let idx = 4;
704
+ for (let u = 0; u < numU; u++) {
705
+ for (let v = 0; v < numV; v++) {
706
+ const p = controlPoints[u][v];
707
+ data[idx++] = p.x;
708
+ data[idx++] = p.y;
709
+ data[idx++] = p.z;
710
+ }
711
+ }
712
+ for (let u = 0; u < numU; u++) {
713
+ for (let v = 0; v < numV; v++) {
714
+ data[idx++] = weights[u][v];
715
+ }
716
+ }
717
+ for (const k of knotsU) data[idx++] = k;
718
+ for (const k of knotsV) data[idx++] = k;
719
+ return data;
720
+ }
721
+ }