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