okgeometry-api 1.47.0 → 1.49.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/Mesh.d.ts +86 -31
- package/dist/Mesh.d.ts.map +1 -1
- package/dist/Mesh.js +133 -2
- package/dist/Mesh.js.map +1 -1
- package/dist/mesh-boolean.protocol.d.ts +43 -1
- package/dist/mesh-boolean.protocol.d.ts.map +1 -1
- package/dist/mesh-boolean.worker.d.ts.map +1 -1
- package/dist/mesh-boolean.worker.js +63 -0
- package/dist/mesh-boolean.worker.js.map +1 -1
- package/dist/wasm-base64.d.ts +1 -1
- package/dist/wasm-base64.js +1 -1
- package/package.json +1 -1
- package/src/Mesh.ts +185 -17
- package/src/mesh-boolean.protocol.ts +29 -1
- package/src/mesh-boolean.worker.ts +75 -0
- package/src/wasm-base64.ts +1 -1
package/package.json
CHANGED
package/src/Mesh.ts
CHANGED
|
@@ -50,6 +50,23 @@ export interface HlrLine {
|
|
|
50
50
|
b3: Point;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** View parameters for `Mesh.hlrProject` / `hlrProjectAsync`. */
|
|
54
|
+
export interface HlrProjectOptions {
|
|
55
|
+
viewDir: { x: number; y: number; z: number };
|
|
56
|
+
up: { x: number; y: number; z: number };
|
|
57
|
+
section?: {
|
|
58
|
+
origin: { x: number; y: number; z: number };
|
|
59
|
+
normal: { x: number; y: number; z: number };
|
|
60
|
+
};
|
|
61
|
+
featureAngleDeg?: number;
|
|
62
|
+
/**
|
|
63
|
+
* View depth in world units beyond the section plane — geometry farther
|
|
64
|
+
* than this is discarded (drafting far clip). Requires `section`; <= 0
|
|
65
|
+
* or absent = unlimited.
|
|
66
|
+
*/
|
|
67
|
+
depth?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
export interface PlanarRectangle {
|
|
54
71
|
corners: [Point, Point, Point, Point];
|
|
55
72
|
width: number;
|
|
@@ -4306,6 +4323,117 @@ export class Mesh {
|
|
|
4306
4323
|
};
|
|
4307
4324
|
}
|
|
4308
4325
|
|
|
4326
|
+
/**
|
|
4327
|
+
* Batched plane sections on the worker pool: one job cuts every mesh at
|
|
4328
|
+
* its own plane and returns cut LOOPS + the fill triangulation per mesh
|
|
4329
|
+
* (okdraw poché + vector cut linework). The per-mesh synchronous
|
|
4330
|
+
* `planeSection` calls scale with SCENE size on an interactive path
|
|
4331
|
+
* (section drags, geometry commits in a view, sheet vector computes),
|
|
4332
|
+
* which is exactly the shape that must not run on the UI thread. Worker
|
|
4333
|
+
* infrastructure failures fall back to the synchronous loop; a kernel
|
|
4334
|
+
* failure on one mesh yields an empty section for that mesh only.
|
|
4335
|
+
*/
|
|
4336
|
+
static async planeSectionsAsync(
|
|
4337
|
+
meshes: Mesh[],
|
|
4338
|
+
planes: Array<{
|
|
4339
|
+
origin: { x: number; y: number; z: number };
|
|
4340
|
+
normal: { x: number; y: number; z: number };
|
|
4341
|
+
}>,
|
|
4342
|
+
asyncOptions?: MeshBooleanAsyncOptions
|
|
4343
|
+
): Promise<
|
|
4344
|
+
Array<{
|
|
4345
|
+
loops: Array<{ closed: boolean; points: Float64Array }>;
|
|
4346
|
+
fill: { positions: Float64Array; indices: Uint32Array } | null;
|
|
4347
|
+
}>
|
|
4348
|
+
> {
|
|
4349
|
+
if (meshes.length === 0) return [];
|
|
4350
|
+
const syncSections = (): Array<{
|
|
4351
|
+
loops: Array<{ closed: boolean; points: Float64Array }>;
|
|
4352
|
+
fill: { positions: Float64Array; indices: Uint32Array } | null;
|
|
4353
|
+
}> =>
|
|
4354
|
+
meshes.map((mesh, i) => {
|
|
4355
|
+
try {
|
|
4356
|
+
const p = planes[i];
|
|
4357
|
+
const plane = new Plane(
|
|
4358
|
+
new Point(p.origin.x, p.origin.y, p.origin.z),
|
|
4359
|
+
new Vec3(p.normal.x, p.normal.y, p.normal.z)
|
|
4360
|
+
);
|
|
4361
|
+
const section = mesh.planeSection(plane);
|
|
4362
|
+
return {
|
|
4363
|
+
loops: section.loops.map((loop) => {
|
|
4364
|
+
const pts = new Float64Array(loop.points.length * 3);
|
|
4365
|
+
for (let k = 0; k < loop.points.length; k++) {
|
|
4366
|
+
pts[k * 3] = loop.points[k].x;
|
|
4367
|
+
pts[k * 3 + 1] = loop.points[k].y;
|
|
4368
|
+
pts[k * 3 + 2] = loop.points[k].z;
|
|
4369
|
+
}
|
|
4370
|
+
return { closed: loop.isClosed(), points: pts };
|
|
4371
|
+
}),
|
|
4372
|
+
fill: section.fill
|
|
4373
|
+
? { positions: section.fill.positions, indices: section.fill.indices }
|
|
4374
|
+
: null,
|
|
4375
|
+
};
|
|
4376
|
+
} catch {
|
|
4377
|
+
return { loops: [], fill: null };
|
|
4378
|
+
}
|
|
4379
|
+
});
|
|
4380
|
+
try {
|
|
4381
|
+
const { buffer } = await runMeshHeavyInWorkerPool(
|
|
4382
|
+
"planeSectionFills",
|
|
4383
|
+
meshes.map((m) => m._buffer),
|
|
4384
|
+
{
|
|
4385
|
+
...asyncOptions,
|
|
4386
|
+
trusted: meshes.map((m) => m._trustedBooleanInput),
|
|
4387
|
+
residents: meshes,
|
|
4388
|
+
params: { sectionPlanes: planes },
|
|
4389
|
+
}
|
|
4390
|
+
);
|
|
4391
|
+
// Packing per mesh: [loopCount, (closed, pointCount, xyz...)...,
|
|
4392
|
+
// vertFloatLen, idxLen, positions..., indices...]
|
|
4393
|
+
const data = new Float64Array(buffer);
|
|
4394
|
+
const out: Array<{
|
|
4395
|
+
loops: Array<{ closed: boolean; points: Float64Array }>;
|
|
4396
|
+
fill: { positions: Float64Array; indices: Uint32Array } | null;
|
|
4397
|
+
}> = [];
|
|
4398
|
+
let offset = 0;
|
|
4399
|
+
const count = data[offset++] ?? 0;
|
|
4400
|
+
for (let i = 0; i < count; i++) {
|
|
4401
|
+
const loopCount = data[offset++];
|
|
4402
|
+
const loops: Array<{ closed: boolean; points: Float64Array }> = [];
|
|
4403
|
+
for (let l = 0; l < loopCount; l++) {
|
|
4404
|
+
const closed = data[offset++] === 1;
|
|
4405
|
+
const pointCount = data[offset++];
|
|
4406
|
+
const points = new Float64Array(
|
|
4407
|
+
data.subarray(offset, offset + pointCount * 3)
|
|
4408
|
+
);
|
|
4409
|
+
offset += pointCount * 3;
|
|
4410
|
+
loops.push({ closed, points });
|
|
4411
|
+
}
|
|
4412
|
+
const vertLen = data[offset++];
|
|
4413
|
+
const idxLen = data[offset++];
|
|
4414
|
+
let fill: { positions: Float64Array; indices: Uint32Array } | null = null;
|
|
4415
|
+
if (idxLen > 0) {
|
|
4416
|
+
const positions = new Float64Array(data.subarray(offset, offset + vertLen));
|
|
4417
|
+
offset += vertLen;
|
|
4418
|
+
const indices = new Uint32Array(idxLen);
|
|
4419
|
+
for (let k = 0; k < idxLen; k++) indices[k] = data[offset++];
|
|
4420
|
+
fill = { positions, indices };
|
|
4421
|
+
} else {
|
|
4422
|
+
offset += vertLen;
|
|
4423
|
+
}
|
|
4424
|
+
out.push({ loops, fill });
|
|
4425
|
+
}
|
|
4426
|
+
return out;
|
|
4427
|
+
} catch (error) {
|
|
4428
|
+
if (!shouldFallbackFromWorkerFailure(error)) throw error;
|
|
4429
|
+
console.warn(
|
|
4430
|
+
"Mesh.planeSectionsAsync worker failed; falling back to main thread.",
|
|
4431
|
+
error
|
|
4432
|
+
);
|
|
4433
|
+
return syncSections();
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
|
|
4309
4437
|
/**
|
|
4310
4438
|
* Hidden-line removal for orthographic drawing views (okdraw
|
|
4311
4439
|
* presentation sheets): project every input mesh's drawable edges
|
|
@@ -4322,24 +4450,21 @@ export class Mesh {
|
|
|
4322
4450
|
*/
|
|
4323
4451
|
static hlrProject(
|
|
4324
4452
|
meshes: Mesh[],
|
|
4325
|
-
options:
|
|
4326
|
-
viewDir: { x: number; y: number; z: number };
|
|
4327
|
-
up: { x: number; y: number; z: number };
|
|
4328
|
-
section?: {
|
|
4329
|
-
origin: { x: number; y: number; z: number };
|
|
4330
|
-
normal: { x: number; y: number; z: number };
|
|
4331
|
-
};
|
|
4332
|
-
featureAngleDeg?: number;
|
|
4333
|
-
/**
|
|
4334
|
-
* View depth in world units beyond the section plane — geometry
|
|
4335
|
-
* farther than this is discarded (drafting far clip). Requires
|
|
4336
|
-
* `section`; <= 0 or absent = unlimited.
|
|
4337
|
-
*/
|
|
4338
|
-
depth?: number;
|
|
4339
|
-
}
|
|
4453
|
+
options: HlrProjectOptions
|
|
4340
4454
|
): HlrLine[] {
|
|
4455
|
+
return Mesh.parseHlrResult(Mesh.hlrProjectRaw(meshes, options));
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
/**
|
|
4459
|
+
* The raw f64 result of `hlrProject` (wasm layout: [count, (class,
|
|
4460
|
+
* meshIndex, ax, ay, bx, by, a3xyz, b3xyz) x count]). The worker path
|
|
4461
|
+
* transfers this buffer back to the main thread, which parses it with
|
|
4462
|
+
* `parseHlrResult` — HlrLine carries Point instances, which do not
|
|
4463
|
+
* survive structured clone.
|
|
4464
|
+
*/
|
|
4465
|
+
static hlrProjectRaw(meshes: Mesh[], options: HlrProjectOptions): Float64Array {
|
|
4341
4466
|
ensureInit();
|
|
4342
|
-
if (meshes.length === 0) return [];
|
|
4467
|
+
if (meshes.length === 0) return new Float64Array([0]);
|
|
4343
4468
|
const packed = Mesh.packMeshes(meshes);
|
|
4344
4469
|
const s = options.section;
|
|
4345
4470
|
const result = wasm.hlr_project_meshes(
|
|
@@ -4355,9 +4480,14 @@ export class Mesh {
|
|
|
4355
4480
|
if (result.length === 0) {
|
|
4356
4481
|
const error = wasm.geometry_last_error?.();
|
|
4357
4482
|
if (error) throw new Error(`hlrProject failed: ${error}`);
|
|
4358
|
-
return [];
|
|
4483
|
+
return new Float64Array([0]);
|
|
4359
4484
|
}
|
|
4485
|
+
return result;
|
|
4486
|
+
}
|
|
4360
4487
|
|
|
4488
|
+
/** Parse a `hlrProjectRaw` result buffer into classified lines. */
|
|
4489
|
+
static parseHlrResult(result: Float64Array): HlrLine[] {
|
|
4490
|
+
if (result.length === 0) return [];
|
|
4361
4491
|
const lines: HlrLine[] = [];
|
|
4362
4492
|
const count = result[0];
|
|
4363
4493
|
let offset = 1;
|
|
@@ -4376,6 +4506,44 @@ export class Mesh {
|
|
|
4376
4506
|
return lines;
|
|
4377
4507
|
}
|
|
4378
4508
|
|
|
4509
|
+
/**
|
|
4510
|
+
* Async `Mesh.hlrProject` on the worker pool — the whole-scene projection
|
|
4511
|
+
* scales with MODEL size, not edit size, and must not block the UI
|
|
4512
|
+
* thread. `options.transforms` (row-major, aligned with `meshes`) bakes
|
|
4513
|
+
* WORKER-side so the main thread never pays the per-mesh transform pass;
|
|
4514
|
+
* pass LOCAL meshes with their placement transforms. Worker
|
|
4515
|
+
* INFRASTRUCTURE failures fall back to the synchronous main-thread path
|
|
4516
|
+
* (baking transforms first); kernel failures propagate.
|
|
4517
|
+
*/
|
|
4518
|
+
static async hlrProjectAsync(
|
|
4519
|
+
meshes: Mesh[],
|
|
4520
|
+
options: HlrProjectOptions & { transforms?: (number[] | undefined)[] },
|
|
4521
|
+
asyncOptions?: MeshBooleanAsyncOptions
|
|
4522
|
+
): Promise<HlrLine[]> {
|
|
4523
|
+
if (meshes.length === 0) return [];
|
|
4524
|
+
const { transforms, ...view } = options;
|
|
4525
|
+
try {
|
|
4526
|
+
const { buffer } = await runMeshHeavyInWorkerPool(
|
|
4527
|
+
"hlrProject",
|
|
4528
|
+
meshes.map((m) => m._buffer),
|
|
4529
|
+
{
|
|
4530
|
+
...asyncOptions,
|
|
4531
|
+
trusted: meshes.map((m) => m._trustedBooleanInput),
|
|
4532
|
+
residents: meshes,
|
|
4533
|
+
params: { hlr: { ...view, transforms } },
|
|
4534
|
+
}
|
|
4535
|
+
);
|
|
4536
|
+
return Mesh.parseHlrResult(new Float64Array(buffer));
|
|
4537
|
+
} catch (error) {
|
|
4538
|
+
if (!shouldFallbackFromWorkerFailure(error)) throw error;
|
|
4539
|
+
console.warn("Mesh.hlrProjectAsync worker failed; falling back to main thread.", error);
|
|
4540
|
+
const baked = meshes.map((m, i) =>
|
|
4541
|
+
transforms?.[i] ? m.applyMatrix(transforms[i]) : m
|
|
4542
|
+
);
|
|
4543
|
+
return Mesh.hlrProject(baked, view);
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4379
4547
|
/**
|
|
4380
4548
|
* Compute intersection curves with another mesh.
|
|
4381
4549
|
* @param other - Mesh to intersect with
|
|
@@ -205,7 +205,17 @@ export type MeshHeavyOperation =
|
|
|
205
205
|
| "solidSplitBySurface"
|
|
206
206
|
| "diagnostics"
|
|
207
207
|
| "repair"
|
|
208
|
-
| "importStl"
|
|
208
|
+
| "importStl"
|
|
209
|
+
// hlrProject: [mesh0, mesh1, ...] (LOCAL-space; per-mesh transforms
|
|
210
|
+
// ride `params.hlr.transforms` and bake worker-side).
|
|
211
|
+
// Response buffer = the raw f64 hlr result (parse with
|
|
212
|
+
// Mesh.parseHlrResult).
|
|
213
|
+
| "hlrProject"
|
|
214
|
+
// planeSectionFills: [mesh0, mesh1, ...] cut at params.sectionPlanes[i];
|
|
215
|
+
// response per mesh = [loopCount, (closed, pointCount,
|
|
216
|
+
// xyz...)..., vertFloatLen, idxLen, positions...,
|
|
217
|
+
// indices...] — cut loops + fill (idxLen 0 = no fill).
|
|
218
|
+
| "planeSectionFills";
|
|
209
219
|
|
|
210
220
|
export interface MeshHeavyParams {
|
|
211
221
|
resolution?: number;
|
|
@@ -249,6 +259,24 @@ export interface MeshHeavyParams {
|
|
|
249
259
|
preserveFeatures?: boolean;
|
|
250
260
|
/** isotropicRemesh: re-project onto the original surface each iteration. */
|
|
251
261
|
project?: boolean;
|
|
262
|
+
/** planeSectionFills: one cutting plane per request mesh (aligned). */
|
|
263
|
+
sectionPlanes?: Array<{
|
|
264
|
+
origin: { x: number; y: number; z: number };
|
|
265
|
+
normal: { x: number; y: number; z: number };
|
|
266
|
+
}>;
|
|
267
|
+
/** hlrProject: view/section parameters + optional per-mesh row-major
|
|
268
|
+
* transforms (aligned with the request buffers, baked worker-side). */
|
|
269
|
+
hlr?: {
|
|
270
|
+
viewDir: { x: number; y: number; z: number };
|
|
271
|
+
up: { x: number; y: number; z: number };
|
|
272
|
+
section?: {
|
|
273
|
+
origin: { x: number; y: number; z: number };
|
|
274
|
+
normal: { x: number; y: number; z: number };
|
|
275
|
+
};
|
|
276
|
+
featureAngleDeg?: number;
|
|
277
|
+
depth?: number;
|
|
278
|
+
transforms?: (number[] | undefined)[];
|
|
279
|
+
};
|
|
252
280
|
}
|
|
253
281
|
|
|
254
282
|
export interface MeshHeavyWorkerRequest {
|
|
@@ -2,6 +2,8 @@ import { init, initThreads, isInitialized } from "./engine.js";
|
|
|
2
2
|
import { Brep } from "./Brep.js";
|
|
3
3
|
import { Mesh } from "./Mesh.js";
|
|
4
4
|
import { Point } from "./Point.js";
|
|
5
|
+
import { Plane } from "./Plane.js";
|
|
6
|
+
import { Vec3 } from "./Vec3.js";
|
|
5
7
|
import type {
|
|
6
8
|
MeshBooleanErrorPayload,
|
|
7
9
|
MeshBooleanWorkerErrorResponse,
|
|
@@ -268,6 +270,21 @@ function runHeavyOperation(message: {
|
|
|
268
270
|
featureAngleDeg?: number;
|
|
269
271
|
preserveFeatures?: boolean;
|
|
270
272
|
project?: boolean;
|
|
273
|
+
sectionPlanes?: Array<{
|
|
274
|
+
origin: { x: number; y: number; z: number };
|
|
275
|
+
normal: { x: number; y: number; z: number };
|
|
276
|
+
}>;
|
|
277
|
+
hlr?: {
|
|
278
|
+
viewDir: { x: number; y: number; z: number };
|
|
279
|
+
up: { x: number; y: number; z: number };
|
|
280
|
+
section?: {
|
|
281
|
+
origin: { x: number; y: number; z: number };
|
|
282
|
+
normal: { x: number; y: number; z: number };
|
|
283
|
+
};
|
|
284
|
+
featureAngleDeg?: number;
|
|
285
|
+
depth?: number;
|
|
286
|
+
transforms?: (number[] | undefined)[];
|
|
287
|
+
};
|
|
271
288
|
};
|
|
272
289
|
options: { allowUnsafe?: boolean; limits?: unknown; debugForceFaceID?: boolean };
|
|
273
290
|
}): { buffer: ArrayBuffer; meta?: Record<string, string | number | boolean> } {
|
|
@@ -308,6 +325,64 @@ function runHeavyOperation(message: {
|
|
|
308
325
|
const result = mesh.voxelRemesh(params.resolution ?? 0, params.iso ?? 0.5);
|
|
309
326
|
return packSingleResult(result, params);
|
|
310
327
|
}
|
|
328
|
+
case "planeSectionFills": {
|
|
329
|
+
const planes = params.sectionPlanes;
|
|
330
|
+
if (!planes) throw new Error("planeSectionFills requires sectionPlanes params");
|
|
331
|
+
const input = meshes();
|
|
332
|
+
// Per mesh: [loopCount, (closed, pointCount, xyz...)...,
|
|
333
|
+
// vertFloatLen, idxLen, positions..., indices...]
|
|
334
|
+
const parts: number[] = [input.length];
|
|
335
|
+
for (let i = 0; i < input.length; i++) {
|
|
336
|
+
let section: ReturnType<Mesh["planeSection"]> | null = null;
|
|
337
|
+
try {
|
|
338
|
+
const p = planes[i];
|
|
339
|
+
const plane = new Plane(
|
|
340
|
+
new Point(p.origin.x, p.origin.y, p.origin.z),
|
|
341
|
+
new Vec3(p.normal.x, p.normal.y, p.normal.z),
|
|
342
|
+
);
|
|
343
|
+
section = input[i].planeSection(plane);
|
|
344
|
+
} catch {
|
|
345
|
+
section = null; // one bad mesh never blocks the batch
|
|
346
|
+
}
|
|
347
|
+
const loops = section?.loops ?? [];
|
|
348
|
+
parts.push(loops.length);
|
|
349
|
+
for (const loop of loops) {
|
|
350
|
+
parts.push(loop.isClosed() ? 1 : 0, loop.points.length);
|
|
351
|
+
for (const pt of loop.points) parts.push(pt.x, pt.y, pt.z);
|
|
352
|
+
}
|
|
353
|
+
const fill = section?.fill ?? null;
|
|
354
|
+
if (!fill || fill.indices.length === 0) {
|
|
355
|
+
parts.push(0, 0);
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
parts.push(fill.positions.length, fill.indices.length);
|
|
359
|
+
for (let k = 0; k < fill.positions.length; k++) parts.push(fill.positions[k]);
|
|
360
|
+
for (let k = 0; k < fill.indices.length; k++) parts.push(fill.indices[k]);
|
|
361
|
+
}
|
|
362
|
+
return { buffer: new Float64Array(parts).buffer };
|
|
363
|
+
}
|
|
364
|
+
case "hlrProject": {
|
|
365
|
+
const view = params.hlr;
|
|
366
|
+
if (!view) throw new Error("hlrProject requires hlr params");
|
|
367
|
+
const input = meshes();
|
|
368
|
+
// Bake per-mesh transforms HERE — the whole point of the worker job
|
|
369
|
+
// is that the main thread pays neither the projection nor the
|
|
370
|
+
// per-mesh transform pass. applyMatrix returns a NEW mesh, so
|
|
371
|
+
// worker-resident operands are never mutated.
|
|
372
|
+
const baked = input.map((m, i) => {
|
|
373
|
+
const t = view.transforms?.[i];
|
|
374
|
+
return t ? m.applyMatrix(t) : m;
|
|
375
|
+
});
|
|
376
|
+
const raw = Mesh.hlrProjectRaw(baked, {
|
|
377
|
+
viewDir: view.viewDir,
|
|
378
|
+
up: view.up,
|
|
379
|
+
section: view.section,
|
|
380
|
+
featureAngleDeg: view.featureAngleDeg,
|
|
381
|
+
depth: view.depth,
|
|
382
|
+
});
|
|
383
|
+
// Copy before transfer: the wasm result may view the wasm heap.
|
|
384
|
+
return { buffer: new Float64Array(raw).buffer };
|
|
385
|
+
}
|
|
311
386
|
case "isotropicRemesh": {
|
|
312
387
|
const [mesh] = meshes();
|
|
313
388
|
const r = mesh.remesh({
|