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.
- package/dist/Arc.js +1 -1
- package/dist/Arc.js.map +1 -1
- package/dist/Brep.js +1 -1
- package/dist/Brep.js.map +1 -1
- package/dist/Circle.js +1 -1
- package/dist/Circle.js.map +1 -1
- package/dist/Line.js +1 -1
- package/dist/Line.js.map +1 -1
- package/dist/Mesh.d.ts +41 -0
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +235 -21
- package/dist/Mesh.js.map +1 -1
- package/dist/MeshSurface.js +1 -1
- package/dist/MeshSurface.js.map +1 -1
- package/dist/NurbsCurve.js +1 -1
- package/dist/NurbsCurve.js.map +1 -1
- package/dist/NurbsSurface.js +1 -1
- package/dist/NurbsSurface.js.map +1 -1
- package/dist/PolyCurve.js +1 -1
- package/dist/PolyCurve.js.map +1 -1
- package/dist/Polyline.js +1 -1
- package/dist/Polyline.js.map +1 -1
- package/dist/Ray.js +1 -1
- package/dist/Ray.js.map +1 -1
- package/dist/SculptSession.js +1 -1
- package/dist/SculptSession.js.map +1 -1
- package/dist/engine-threaded.d.ts +2 -0
- package/dist/engine-threaded.d.ts.map +1 -0
- package/dist/engine-threaded.js +33 -0
- package/dist/engine-threaded.js.map +1 -0
- package/dist/engine.d.ts +21 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +42 -0
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mesh-boolean.pool.d.ts +23 -1
- package/dist/mesh-boolean.pool.d.ts.map +1 -1
- package/dist/mesh-boolean.pool.js +97 -12
- package/dist/mesh-boolean.pool.js.map +1 -1
- package/dist/mesh-boolean.protocol.d.ts +21 -1
- package/dist/mesh-boolean.protocol.d.ts.map +1 -1
- package/dist/mesh-boolean.protocol.js.map +1 -1
- package/dist/mesh-boolean.worker.d.ts.map +1 -1
- package/dist/mesh-boolean.worker.js +94 -1
- package/dist/mesh-boolean.worker.js.map +1 -1
- package/dist/wasm-active.d.ts +20 -0
- package/dist/wasm-active.d.ts.map +1 -0
- package/dist/wasm-active.js +26 -0
- package/dist/wasm-active.js.map +1 -0
- package/dist/wasm-base64-threaded.d.ts +2 -0
- package/dist/wasm-base64-threaded.d.ts.map +1 -0
- package/dist/wasm-base64-threaded.js +3 -0
- package/dist/wasm-base64-threaded.js.map +1 -0
- package/dist/wasm-base64.d.ts +1 -1
- package/dist/wasm-base64.d.ts.map +1 -1
- package/dist/wasm-base64.js +1 -1
- package/dist/wasm-base64.js.map +1 -1
- package/dist/wasm-bindings-threaded.d.ts +2664 -0
- package/dist/wasm-bindings-threaded.d.ts.map +1 -0
- package/dist/wasm-bindings-threaded.js +5008 -0
- package/dist/wasm-bindings-threaded.js.map +1 -0
- package/dist/wasm-bindings.d.ts +130 -0
- package/dist/wasm-bindings.d.ts.map +1 -1
- package/dist/wasm-bindings.js +208 -0
- package/dist/wasm-bindings.js.map +1 -1
- package/dist/workerHelpers-threaded.d.ts +2 -0
- package/dist/workerHelpers-threaded.d.ts.map +1 -0
- package/dist/workerHelpers-threaded.js +100 -0
- package/dist/workerHelpers-threaded.js.map +1 -0
- package/package.json +8 -3
- package/src/Arc.ts +117 -117
- package/src/Brep.ts +1 -1
- package/src/Circle.ts +166 -166
- package/src/Line.ts +171 -171
- package/src/Mesh.ts +300 -45
- package/src/MeshSurface.ts +1 -1
- package/src/NurbsCurve.ts +655 -655
- package/src/NurbsSurface.ts +721 -721
- package/src/PolyCurve.ts +203 -203
- package/src/Polyline.ts +316 -316
- package/src/Ray.ts +90 -90
- package/src/SculptSession.ts +377 -377
- package/src/engine-threaded.ts +39 -0
- package/src/engine.ts +46 -0
- package/src/index.ts +1 -1
- package/src/mesh-boolean.pool.ts +208 -86
- package/src/mesh-boolean.protocol.ts +44 -1
- package/src/mesh-boolean.worker.ts +170 -56
- package/src/wasm-active.ts +31 -0
- package/src/wasm-base64-threaded.ts +2 -0
- package/src/wasm-base64.ts +1 -1
- package/src/wasm-bindings-threaded.d.ts +1989 -0
- package/src/wasm-bindings-threaded.js +5235 -0
- package/src/wasm-bindings.d.ts +104 -0
- package/src/wasm-bindings.js +225 -0
- package/src/workerHelpers-threaded.js +107 -0
package/src/NurbsSurface.ts
CHANGED
|
@@ -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
|
|
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
|
+
}
|