okgeometry-api 1.5.0 → 1.9.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/Brep.d.ts +177 -0
- package/dist/Brep.d.ts.map +1 -0
- package/dist/Brep.js +368 -0
- package/dist/Brep.js.map +1 -0
- package/dist/Mesh.d.ts +21 -1
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +63 -1
- package/dist/Mesh.js.map +1 -1
- package/dist/NurbsSurface.d.ts +121 -0
- package/dist/NurbsSurface.d.ts.map +1 -1
- package/dist/NurbsSurface.js +261 -7
- package/dist/NurbsSurface.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- 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.d.ts +300 -0
- package/dist/wasm-bindings.d.ts.map +1 -1
- package/dist/wasm-bindings.js +589 -0
- package/dist/wasm-bindings.js.map +1 -1
- package/package.json +50 -48
- package/src/Brep.ts +437 -0
- package/src/Mesh.ts +73 -2
- package/src/NurbsSurface.ts +367 -62
- package/src/index.ts +77 -68
- package/src/wasm-base64.ts +1 -1
- package/src/wasm-bindings.d.ts +233 -0
- package/src/wasm-bindings.js +612 -0
package/src/Brep.ts
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
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, type MeshBooleanOptions } from "./Mesh.js";
|
|
6
|
+
import { Polyline } from "./Polyline.js";
|
|
7
|
+
import { NurbsCurve } from "./NurbsCurve.js";
|
|
8
|
+
import { NurbsSurface } from "./NurbsSurface.js";
|
|
9
|
+
import * as wasm from "./wasm-bindings.js";
|
|
10
|
+
|
|
11
|
+
/** Options for Brep boolean operations. */
|
|
12
|
+
export interface BrepBooleanOptions extends MeshBooleanOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Chordal deviation tolerance used to tessellate BREP operands before the
|
|
15
|
+
* boolean runs (model units). Defaults to 0.005, matching `toMesh()`.
|
|
16
|
+
*/
|
|
17
|
+
tolerance?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Options for PARAMETRIC Brep booleans (exact SSI, BREP result). */
|
|
21
|
+
export interface BrepParametricBooleanOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Absolute geometric tolerance for intersection edges and coincident-face
|
|
24
|
+
* classification (model units). Defaults to 1e-6.
|
|
25
|
+
*/
|
|
26
|
+
tolerance?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Validation report for a BREP body. */
|
|
30
|
+
export interface BrepValidationReport {
|
|
31
|
+
isClosed: boolean;
|
|
32
|
+
boundaryEdgeCount: number;
|
|
33
|
+
maxCoherenceError: number;
|
|
34
|
+
issues: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Structure summary of a BREP body. */
|
|
38
|
+
export interface BrepInfo {
|
|
39
|
+
faceCount: number;
|
|
40
|
+
edgeCount: number;
|
|
41
|
+
vertexCount: number;
|
|
42
|
+
isClosed: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Boundary representation (BREP) solid or sheet backed by the WASM kernel.
|
|
47
|
+
*
|
|
48
|
+
* A Brep is a graph of faces (trimmed exact surfaces — planes or NURBS),
|
|
49
|
+
* edges (exact 3D curves shared between faces) and vertices. Unlike a Mesh,
|
|
50
|
+
* the geometry stays EXACT: a cylinder face is a true rational surface, and
|
|
51
|
+
* tessellation density is a display decision made at query time.
|
|
52
|
+
*
|
|
53
|
+
* Topology is carried as JSON (kernel-defined schema) so it can persist in
|
|
54
|
+
* documents; tessellation and queries are binary WASM calls.
|
|
55
|
+
*/
|
|
56
|
+
export class Brep {
|
|
57
|
+
private _info: BrepInfo | null = null;
|
|
58
|
+
|
|
59
|
+
private constructor(public readonly json: string) {
|
|
60
|
+
if (!json) throw new Error("Brep: empty body");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Constructors ──
|
|
64
|
+
|
|
65
|
+
/** Restore a Brep from its JSON representation. */
|
|
66
|
+
static fromJson(json: string): Brep {
|
|
67
|
+
return new Brep(json);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Exact BREP box between two corners (6 planar faces, 12 edges, 8 vertices). */
|
|
71
|
+
static box(min: Point, max: Point): Brep {
|
|
72
|
+
ensureInit();
|
|
73
|
+
return Brep.fromPrimitive("box", [min.x, min.y, min.z, max.x, max.y, max.z]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Exact BREP cylinder (rational side surface + planar caps). */
|
|
77
|
+
static cylinder(base: Point, axis: Vec3, radius: number, height: number): Brep {
|
|
78
|
+
ensureInit();
|
|
79
|
+
return Brep.fromPrimitive("cylinder", [
|
|
80
|
+
base.x, base.y, base.z, axis.x, axis.y, axis.z, radius, height,
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Exact BREP sphere (single rational face, degenerate poles). */
|
|
85
|
+
static sphere(center: Point, radius: number): Brep {
|
|
86
|
+
ensureInit();
|
|
87
|
+
return Brep.fromPrimitive("sphere", [center.x, center.y, center.z, radius]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Exact BREP cone or frustum (radius1 = 0 produces an apex). */
|
|
91
|
+
static cone(base: Point, axis: Vec3, radius0: number, radius1: number, height: number): Brep {
|
|
92
|
+
ensureInit();
|
|
93
|
+
return Brep.fromPrimitive("cone", [
|
|
94
|
+
base.x, base.y, base.z, axis.x, axis.y, axis.z, radius0, radius1, height,
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Exact BREP torus (single rational face, axis +Z through center). */
|
|
99
|
+
static torus(center: Point, major: number, minor: number): Brep {
|
|
100
|
+
ensureInit();
|
|
101
|
+
return Brep.fromPrimitive("torus", [center.x, center.y, center.z, major, minor]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private static fromPrimitive(kind: string, params: number[]): Brep {
|
|
105
|
+
const json = wasm.brep_primitive(kind, new Float64Array(params));
|
|
106
|
+
if (!json) throw new Error(`Brep.${kind} construction failed`);
|
|
107
|
+
return new Brep(json);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extrude a planar profile curve. Closed profiles produce a capped solid
|
|
112
|
+
* with proper shared-edge topology; open profiles produce a sheet.
|
|
113
|
+
*/
|
|
114
|
+
static extrude(profile: NurbsCurve, direction: Vec3, height: number): Brep {
|
|
115
|
+
ensureInit();
|
|
116
|
+
const json = wasm.brep_extrude_curve(profile.data, direction.x, direction.y, direction.z, height);
|
|
117
|
+
if (!json) throw new Error("Brep.extrude failed (profile must be planar)");
|
|
118
|
+
return new Brep(json);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Revolve a profile curve around an axis.
|
|
123
|
+
* Full revolutions of closed profiles produce torus-like single-face solids;
|
|
124
|
+
* open profiles with on-axis endpoints close at the poles (sphere/cone style);
|
|
125
|
+
* partial revolutions of closed planar profiles get planar caps.
|
|
126
|
+
*/
|
|
127
|
+
static revolve(profile: NurbsCurve, origin: Point, axis: Vec3, angle: number): Brep {
|
|
128
|
+
ensureInit();
|
|
129
|
+
const json = wasm.brep_revolve_curve(
|
|
130
|
+
profile.data,
|
|
131
|
+
origin.x, origin.y, origin.z,
|
|
132
|
+
axis.x, axis.y, axis.z,
|
|
133
|
+
angle
|
|
134
|
+
);
|
|
135
|
+
if (!json) throw new Error("Brep.revolve failed");
|
|
136
|
+
return new Brep(json);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Loft through profile curves (compatibility handled automatically). */
|
|
140
|
+
static loft(profiles: NurbsCurve[]): Brep {
|
|
141
|
+
ensureInit();
|
|
142
|
+
if (profiles.length < 2) throw new Error("Brep.loft needs at least 2 profiles");
|
|
143
|
+
const parts: number[] = [profiles.length];
|
|
144
|
+
for (const c of profiles) {
|
|
145
|
+
const d = c.data;
|
|
146
|
+
for (let i = 0; i < d.length; i++) parts.push(d[i]);
|
|
147
|
+
}
|
|
148
|
+
const json = wasm.brep_loft_curves(new Float64Array(parts));
|
|
149
|
+
if (!json) throw new Error("Brep.loft failed");
|
|
150
|
+
return new Brep(json);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Untrimmed sheet BREP from a NURBS surface (4 boundary edges). */
|
|
154
|
+
static fromSurface(surface: NurbsSurface): Brep {
|
|
155
|
+
ensureInit();
|
|
156
|
+
const json = wasm.brep_sheet_from_surface(surface.data);
|
|
157
|
+
if (!json) throw new Error("Brep.fromSurface failed");
|
|
158
|
+
return new Brep(json);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Structure ──
|
|
162
|
+
|
|
163
|
+
/** Face/edge/vertex counts and closedness (cached). */
|
|
164
|
+
get info(): BrepInfo {
|
|
165
|
+
if (!this._info) {
|
|
166
|
+
ensureInit();
|
|
167
|
+
const r = wasm.brep_info(this.json);
|
|
168
|
+
if (r.length < 4) throw new Error("Brep.info failed");
|
|
169
|
+
this._info = {
|
|
170
|
+
faceCount: Number(r[0]),
|
|
171
|
+
edgeCount: Number(r[1]),
|
|
172
|
+
vertexCount: Number(r[2]),
|
|
173
|
+
isClosed: r[3] > 0.5,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return this._info;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
get faceCount(): number {
|
|
180
|
+
return this.info.faceCount;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
get edgeCount(): number {
|
|
184
|
+
return this.info.edgeCount;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Whether every non-degenerate edge is shared by exactly two faces (solid). */
|
|
188
|
+
isClosed(): boolean {
|
|
189
|
+
return this.info.isClosed;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Tessellation / display ──
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Tessellate the whole body into a triangle mesh. Adjacent faces share the
|
|
196
|
+
* same edge samples, so the result is crack-free (watertight for solids).
|
|
197
|
+
* @param tolerance - Chordal deviation tolerance (model units)
|
|
198
|
+
*/
|
|
199
|
+
tessellate(tolerance = 0.01): Mesh {
|
|
200
|
+
ensureInit();
|
|
201
|
+
const buf = wasm.brep_tessellate(this.json, tolerance);
|
|
202
|
+
if (buf.length < 1) throw new Error("Brep.tessellate failed");
|
|
203
|
+
return Mesh.fromBuffer(buf);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Tessellate a single face into a triangle mesh. */
|
|
207
|
+
tessellateFace(faceIndex: number, tolerance = 0.01): Mesh {
|
|
208
|
+
ensureInit();
|
|
209
|
+
const buf = wasm.brep_face_tessellate(this.json, faceIndex, tolerance);
|
|
210
|
+
if (buf.length < 1) throw new Error(`Brep.tessellateFace(${faceIndex}) failed`);
|
|
211
|
+
return Mesh.fromBuffer(buf);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Exact model edges sampled as polylines (feature curves for display/selection). */
|
|
215
|
+
edges(tolerance = 0.01): Polyline[] {
|
|
216
|
+
ensureInit();
|
|
217
|
+
const buf = wasm.brep_edges(this.json, tolerance);
|
|
218
|
+
if (buf.length < 1) return [];
|
|
219
|
+
const out: Polyline[] = [];
|
|
220
|
+
let idx = 1;
|
|
221
|
+
const count = Number(buf[0]);
|
|
222
|
+
for (let i = 0; i < count; i++) {
|
|
223
|
+
const n = Number(buf[idx++]);
|
|
224
|
+
const pts: Point[] = [];
|
|
225
|
+
for (let k = 0; k < n; k++) {
|
|
226
|
+
pts.push(new Point(buf[idx], buf[idx + 1], buf[idx + 2]));
|
|
227
|
+
idx += 3;
|
|
228
|
+
}
|
|
229
|
+
if (pts.length >= 2) out.push(new Polyline(pts));
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Face geometry ──
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Exact backing surface of a face: a NurbsSurface for curved faces, a Plane
|
|
238
|
+
* for planar faces.
|
|
239
|
+
*/
|
|
240
|
+
faceSurface(faceIndex: number): NurbsSurface | Plane {
|
|
241
|
+
ensureInit();
|
|
242
|
+
const buf = wasm.brep_face_surface(this.json, faceIndex);
|
|
243
|
+
if (buf.length < 1) throw new Error(`Brep.faceSurface(${faceIndex}) failed`);
|
|
244
|
+
if (Number(buf[0]) === 0) {
|
|
245
|
+
if (buf.length < 10) throw new Error("Brep.faceSurface: bad plane data");
|
|
246
|
+
const origin = new Point(buf[1], buf[2], buf[3]);
|
|
247
|
+
const uDir = new Vec3(buf[4], buf[5], buf[6]);
|
|
248
|
+
const vDir = new Vec3(buf[7], buf[8], buf[9]);
|
|
249
|
+
const normal = uDir.cross(vDir);
|
|
250
|
+
return new Plane(origin, normal, uDir);
|
|
251
|
+
}
|
|
252
|
+
return NurbsSurface.fromData(buf.slice(1));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Exact iso-parametric curve of a curved face's backing surface.
|
|
257
|
+
* Returns null for planar faces.
|
|
258
|
+
* @param t - Normalized parameter [0, 1]
|
|
259
|
+
* @param direction - "u" extracts along u at constant v=t; "v" the converse
|
|
260
|
+
*/
|
|
261
|
+
faceIsoCurve(faceIndex: number, t: number, direction: "u" | "v"): NurbsCurve | null {
|
|
262
|
+
ensureInit();
|
|
263
|
+
const buf = wasm.brep_face_iso_curve(this.json, faceIndex, t, direction === "u");
|
|
264
|
+
if (buf.length < 2) return null;
|
|
265
|
+
return NurbsCurve.fromData(buf);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Transforms ──
|
|
269
|
+
|
|
270
|
+
/** Apply a row-major 4x4 matrix to all geometry (exact). */
|
|
271
|
+
applyMatrix(matrix: number[] | Float64Array): Brep {
|
|
272
|
+
ensureInit();
|
|
273
|
+
if (matrix.length < 16) throw new Error("Brep.applyMatrix: need 16 entries");
|
|
274
|
+
const json = wasm.brep_transform(this.json, new Float64Array(matrix));
|
|
275
|
+
if (!json) throw new Error("Brep.applyMatrix failed");
|
|
276
|
+
return new Brep(json);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Translate the body. */
|
|
280
|
+
translate(offset: Vec3): Brep {
|
|
281
|
+
return this.applyMatrix([
|
|
282
|
+
1, 0, 0, offset.x,
|
|
283
|
+
0, 1, 0, offset.y,
|
|
284
|
+
0, 0, 1, offset.z,
|
|
285
|
+
0, 0, 0, 1,
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Analysis ──
|
|
290
|
+
|
|
291
|
+
/** Validate topology and trim coherence. */
|
|
292
|
+
validate(tolerance = 1e-7): BrepValidationReport {
|
|
293
|
+
ensureInit();
|
|
294
|
+
const json = wasm.brep_validate(this.json, tolerance);
|
|
295
|
+
if (!json) throw new Error("Brep.validate failed");
|
|
296
|
+
return JSON.parse(json) as BrepValidationReport;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Volume and surface area (from tessellation at the given tolerance). */
|
|
300
|
+
volumeArea(tolerance = 0.001): { volume: number; area: number } {
|
|
301
|
+
ensureInit();
|
|
302
|
+
const r = wasm.brep_volume_area(this.json, tolerance);
|
|
303
|
+
if (r.length < 2) throw new Error("Brep.volumeArea failed");
|
|
304
|
+
return { volume: r[0], area: r[1] };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Convert to a mesh body (e.g. for boolean operations with meshes). */
|
|
308
|
+
toMesh(tolerance = 0.005): Mesh {
|
|
309
|
+
return this.tessellate(tolerance);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Booleans ──
|
|
313
|
+
|
|
314
|
+
private static resolveBooleanOperand(other: Brep | Mesh, tolerance: number): Mesh {
|
|
315
|
+
return other instanceof Mesh ? other : other.toMesh(tolerance);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private runBoolean(
|
|
319
|
+
other: Brep | Mesh,
|
|
320
|
+
op: "union" | "subtract" | "intersect",
|
|
321
|
+
options?: BrepBooleanOptions,
|
|
322
|
+
): Mesh {
|
|
323
|
+
const tolerance = options?.tolerance ?? 0.005;
|
|
324
|
+
// The mesh CSG pipeline is sensitive to specific tessellation densities on
|
|
325
|
+
// curved (especially rational) surfaces; a failed run usually succeeds at
|
|
326
|
+
// a slightly perturbed density. Retry with nudged (coarser — finer risks
|
|
327
|
+
// the face-count safety limits) tolerances before giving up; accuracy
|
|
328
|
+
// stays within the requested band.
|
|
329
|
+
const attempts = [1.0, 1.12, 1.25, 1.4];
|
|
330
|
+
let lastError: unknown;
|
|
331
|
+
for (const factor of attempts) {
|
|
332
|
+
const tol = tolerance * factor;
|
|
333
|
+
try {
|
|
334
|
+
const lhs = this.toMesh(tol);
|
|
335
|
+
const rhs = Brep.resolveBooleanOperand(other, tol);
|
|
336
|
+
return lhs[op](rhs, options);
|
|
337
|
+
} catch (e) {
|
|
338
|
+
lastError = e;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throw lastError;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Boolean union with another Brep or Mesh.
|
|
346
|
+
*
|
|
347
|
+
* Both operands are tessellated at `options.tolerance` (BREP tessellation
|
|
348
|
+
* is crack-free, so closed bodies stay watertight) and combined with the
|
|
349
|
+
* mesh CSG pipeline. The result is a Mesh — exact trimmed-BREP boolean
|
|
350
|
+
* output is not supported. Requires both bodies to be closed solids.
|
|
351
|
+
*/
|
|
352
|
+
union(other: Brep | Mesh, options?: BrepBooleanOptions): Mesh {
|
|
353
|
+
return this.runBoolean(other, "union", options);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Boolean subtraction (this − other) with another Brep or Mesh.
|
|
358
|
+
*
|
|
359
|
+
* Closed − closed runs full CSG; an open sheet Brep minus a closed solid
|
|
360
|
+
* trims the sheet (split, keep outside). The result is a Mesh.
|
|
361
|
+
*/
|
|
362
|
+
subtract(other: Brep | Mesh, options?: BrepBooleanOptions): Mesh {
|
|
363
|
+
return this.runBoolean(other, "subtract", options);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Boolean intersection with another Brep or Mesh.
|
|
368
|
+
*
|
|
369
|
+
* Closed ∩ closed returns the shared volume; an open sheet Brep ∩ a closed
|
|
370
|
+
* solid trims the sheet (split, keep inside). The result is a Mesh.
|
|
371
|
+
*/
|
|
372
|
+
intersect(other: Brep | Mesh, options?: BrepBooleanOptions): Mesh {
|
|
373
|
+
return this.runBoolean(other, "intersect", options);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Intersection CURVES with another Brep or Mesh — the polylines where the
|
|
378
|
+
* two surfaces cross (no volume classification). Tolerance controls the
|
|
379
|
+
* tessellation density of both operands.
|
|
380
|
+
*/
|
|
381
|
+
intersectCurves(other: Brep | Mesh, tolerance = 0.005): Polyline[] {
|
|
382
|
+
const lhs = this.toMesh(tolerance);
|
|
383
|
+
const rhs = Brep.resolveBooleanOperand(other, tolerance);
|
|
384
|
+
return lhs.intersectMesh(rhs);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Parametric booleans (BREP → BREP) ──
|
|
388
|
+
|
|
389
|
+
private runParametricBoolean(
|
|
390
|
+
other: Brep,
|
|
391
|
+
op: "union" | "subtract" | "intersect",
|
|
392
|
+
options?: BrepParametricBooleanOptions,
|
|
393
|
+
): Brep {
|
|
394
|
+
ensureInit();
|
|
395
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
396
|
+
const json = wasm.brep_boolean_op(this.json, other.json, op, tolerance);
|
|
397
|
+
if (!json) throw new Error(`Brep.${op}Brep failed`);
|
|
398
|
+
if (json.startsWith('{"error"')) {
|
|
399
|
+
const parsed = JSON.parse(json) as { error: string };
|
|
400
|
+
throw new Error(`Brep.${op}Brep failed: ${parsed.error}`);
|
|
401
|
+
}
|
|
402
|
+
return new Brep(json);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* PARAMETRIC boolean union: exact surface–surface intersection curves with
|
|
407
|
+
* trimmed-surface output. Result faces keep their parents' exact surfaces
|
|
408
|
+
* (planes/NURBS, untessellated); intersection edges are Newton-refined SSI
|
|
409
|
+
* curves stored as tolerant NURBS edges with matched pcurves on both
|
|
410
|
+
* adjacent faces. Both operands must be CLOSED solids.
|
|
411
|
+
*
|
|
412
|
+
* `options.tolerance` is the absolute geometric tolerance of intersection
|
|
413
|
+
* edges and coincident-face classification (default 1e-6).
|
|
414
|
+
*
|
|
415
|
+
* Known limitation: exactly tangential surface contact (e.g. equal-radius
|
|
416
|
+
* cylinders touching along a line/point) is not resolved into pinch
|
|
417
|
+
* topology; transversal and coincident (coplanar) configurations are exact.
|
|
418
|
+
*/
|
|
419
|
+
unionBrep(other: Brep, options?: BrepParametricBooleanOptions): Brep {
|
|
420
|
+
return this.runParametricBoolean(other, "union", options);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** PARAMETRIC boolean subtraction (this − other). See {@link unionBrep}. */
|
|
424
|
+
subtractBrep(other: Brep, options?: BrepParametricBooleanOptions): Brep {
|
|
425
|
+
return this.runParametricBoolean(other, "subtract", options);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** PARAMETRIC boolean intersection. See {@link unionBrep}. */
|
|
429
|
+
intersectBrep(other: Brep, options?: BrepParametricBooleanOptions): Brep {
|
|
430
|
+
return this.runParametricBoolean(other, "intersect", options);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** JSON representation (persistence). */
|
|
434
|
+
toJson(): string {
|
|
435
|
+
return this.json;
|
|
436
|
+
}
|
|
437
|
+
}
|
package/src/Mesh.ts
CHANGED
|
@@ -1879,7 +1879,12 @@ export class Mesh {
|
|
|
1879
1879
|
data.set(coords, 2);
|
|
1880
1880
|
return data;
|
|
1881
1881
|
}
|
|
1882
|
-
|
|
1882
|
+
/**
|
|
1883
|
+
* Concatenate meshes into one multi-component mesh (buffer merge with
|
|
1884
|
+
* index offsetting — no welding and no CSG). Useful for collecting split
|
|
1885
|
+
* pieces back into a single body.
|
|
1886
|
+
*/
|
|
1887
|
+
static mergeMeshes(meshes: Mesh[]): Mesh {
|
|
1883
1888
|
ensureInit();
|
|
1884
1889
|
const packed = Mesh.packMeshes(meshes);
|
|
1885
1890
|
return Mesh.fromTrustedBuffer(wasm.mesh_merge(packed));
|
|
@@ -2296,7 +2301,8 @@ export class Mesh {
|
|
|
2296
2301
|
if (cutters.length === 0) return Mesh.cloneMesh(host);
|
|
2297
2302
|
if (cutters.length === 1) return host.subtract(cutters[0], options);
|
|
2298
2303
|
|
|
2299
|
-
if (
|
|
2304
|
+
if (cutters.some((mesh) => !mesh.isClosedVolume())) {
|
|
2305
|
+
// Open cutters carry per-pair trim/error semantics — sequential only.
|
|
2300
2306
|
return cutters.reduce((acc: Mesh, cutter) => acc.subtract(cutter, options), host);
|
|
2301
2307
|
}
|
|
2302
2308
|
|
|
@@ -2314,6 +2320,15 @@ export class Mesh {
|
|
|
2314
2320
|
const flags = host._trustedBooleanInput && relevant.every((mesh) => mesh._trustedBooleanInput)
|
|
2315
2321
|
? "trustedInput"
|
|
2316
2322
|
: "";
|
|
2323
|
+
|
|
2324
|
+
if (!host.isClosedVolume()) {
|
|
2325
|
+
// Open host + closed cutters: batched surface trim. One pass over the
|
|
2326
|
+
// host with all cutters at once — replaces the sequential pairwise
|
|
2327
|
+
// fold, which re-split (and degraded) the growing host once per cutter.
|
|
2328
|
+
const result = wasm.mesh_surface_difference_all(Mesh.packMeshes([host, ...relevant]), flags);
|
|
2329
|
+
return Mesh.decodeBatchBooleanResult("subtractAll", result);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2317
2332
|
const result = wasm.mesh_boolean_difference_all(Mesh.packMeshes([host, ...relevant]), flags);
|
|
2318
2333
|
return Mesh.decodeBatchBooleanResult("subtractAll", result);
|
|
2319
2334
|
}
|
|
@@ -2752,6 +2767,62 @@ export class Mesh {
|
|
|
2752
2767
|
return parsePolylineBuf(result).map(pts => new Polyline(pts));
|
|
2753
2768
|
}
|
|
2754
2769
|
|
|
2770
|
+
/**
|
|
2771
|
+
* Clip a polyline against this CLOSED mesh volume — boolean intersection
|
|
2772
|
+
* semantics for curves. The polyline is split at every crossing with the
|
|
2773
|
+
* mesh surface; pieces inside the volume land in `inside`, the rest in
|
|
2774
|
+
* `outside`. Closed loops merge the piece spanning the loop seam, and a
|
|
2775
|
+
* loop that never crosses the mesh comes back as one closed polyline.
|
|
2776
|
+
*
|
|
2777
|
+
* @param polyline - Polyline (closedness inferred) or raw point list
|
|
2778
|
+
* @param closed - Treat the points as a closed loop (defaults to the
|
|
2779
|
+
* polyline's own closedness; required when passing a raw point list)
|
|
2780
|
+
*/
|
|
2781
|
+
clipPolyline(
|
|
2782
|
+
polyline: Polyline | Point[],
|
|
2783
|
+
closed?: boolean,
|
|
2784
|
+
): { inside: Polyline[]; outside: Polyline[] } {
|
|
2785
|
+
ensureInit();
|
|
2786
|
+
const pts = Array.isArray(polyline) ? polyline : polyline.points;
|
|
2787
|
+
if (pts.length < 2) {
|
|
2788
|
+
throw new Error("Mesh.clipPolyline needs at least 2 polyline points");
|
|
2789
|
+
}
|
|
2790
|
+
const isClosed = closed ?? (!Array.isArray(polyline) && polyline.isClosed());
|
|
2791
|
+
const flat = new Float64Array(pts.length * 3);
|
|
2792
|
+
for (let i = 0; i < pts.length; i++) {
|
|
2793
|
+
flat[i * 3] = pts[i].x;
|
|
2794
|
+
flat[i * 3 + 1] = pts[i].y;
|
|
2795
|
+
flat[i * 3 + 2] = pts[i].z;
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
const buf = wasm.mesh_clip_polyline(this._vertexCount, this._buffer, flat, isClosed);
|
|
2799
|
+
if (buf.length === 0) {
|
|
2800
|
+
throw new Error(
|
|
2801
|
+
"Mesh.clipPolyline failed — the mesh must be a closed volume and the polyline non-degenerate.",
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// Two concatenated polyline groups: inside first, then outside.
|
|
2806
|
+
let idx = 0;
|
|
2807
|
+
const readGroup = (): Polyline[] => {
|
|
2808
|
+
const group: Polyline[] = [];
|
|
2809
|
+
const count = buf[idx++];
|
|
2810
|
+
for (let i = 0; i < count; i++) {
|
|
2811
|
+
const n = buf[idx++];
|
|
2812
|
+
const points: Point[] = [];
|
|
2813
|
+
for (let k = 0; k < n; k++) {
|
|
2814
|
+
points.push(new Point(buf[idx], buf[idx + 1], buf[idx + 2]));
|
|
2815
|
+
idx += 3;
|
|
2816
|
+
}
|
|
2817
|
+
if (points.length >= 2) group.push(new Polyline(points));
|
|
2818
|
+
}
|
|
2819
|
+
return group;
|
|
2820
|
+
};
|
|
2821
|
+
const inside = readGroup();
|
|
2822
|
+
const outside = readGroup();
|
|
2823
|
+
return { inside, outside };
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2755
2826
|
/**
|
|
2756
2827
|
* Apply a 4x4 transformation matrix.
|
|
2757
2828
|
* @param matrix - Row-major flat array of 16 numbers
|