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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okgeometry-api",
3
- "version": "1.47.0",
3
+ "version": "1.49.0",
4
4
  "description": "Geometry engine API for AEC applications - NURBS, meshes, booleans, intersections. Powered by Rust/WASM.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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({