brepjs 18.66.2 → 18.68.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/brepjs.cjs CHANGED
@@ -182,7 +182,7 @@ function validateInputs(mesh, queries) {
182
182
  return null;
183
183
  }
184
184
  /** Resolve a registered voxel engine, mapping an unregistered id to an error. */
185
- function resolveEngine(id) {
185
+ function resolveEngine$1(id) {
186
186
  try {
187
187
  return require_errors.ok(getVoxel(id));
188
188
  } catch (cause) {
@@ -199,7 +199,7 @@ function resolveEngine(id) {
199
199
  function windingNumbers(mesh, queries, id) {
200
200
  const invalid = validateInputs(mesh, queries);
201
201
  if (invalid) return require_errors.err(invalid);
202
- const engine = resolveEngine(id);
202
+ const engine = resolveEngine$1(id);
203
203
  if (require_errors.isErr(engine)) return engine;
204
204
  return require_errors.ok(engine.value.winding_numbers(mesh.vertices, mesh.triangles, queries));
205
205
  }
@@ -211,15 +211,15 @@ function windingNumbers(mesh, queries, id) {
211
211
  function pointsInside(mesh, queries, id) {
212
212
  const invalid = validateInputs(mesh, queries);
213
213
  if (invalid) return require_errors.err(invalid);
214
- const engine = resolveEngine(id);
214
+ const engine = resolveEngine$1(id);
215
215
  if (require_errors.isErr(engine)) return engine;
216
216
  const flags = engine.value.points_inside(mesh.vertices, mesh.triangles, queries);
217
217
  return require_errors.ok(Array.from(flags, (flag) => flag === 1));
218
218
  }
219
219
  //#endregion
220
220
  //#region src/voxel/repairFns.ts
221
- var DEFAULT_RESOLUTION$2 = 48;
222
- var DEFAULT_PADDING$2 = 2;
221
+ var DEFAULT_RESOLUTION$4 = 48;
222
+ var DEFAULT_PADDING$4 = 2;
223
223
  /**
224
224
  * Repair a (possibly non-watertight) triangle-soup mesh into a closed surface:
225
225
  * voxelize an FWN-signed SDF, then Surface-Nets contour it back to triangles.
@@ -232,16 +232,16 @@ function repairMesh(mesh, opts, id) {
232
232
  const invalid = validateMesh(mesh);
233
233
  if (invalid) return require_errors.err(invalid);
234
234
  if (mesh.vertices.length === 0 || mesh.triangles.length === 0) return require_errors.err(require_errors.validationError("VOXEL_EMPTY_MESH", "repairMesh requires a non-empty triangle mesh."));
235
- const resolution = opts?.resolution ?? DEFAULT_RESOLUTION$2;
236
- const padding = opts?.padding ?? DEFAULT_PADDING$2;
235
+ const resolution = opts?.resolution ?? DEFAULT_RESOLUTION$4;
236
+ const padding = opts?.padding ?? DEFAULT_PADDING$4;
237
237
  if (!Number.isInteger(resolution) || resolution < 1) return require_errors.err(require_errors.validationError("VOXEL_INVALID_RESOLUTION", "resolution must be an integer >= 1."));
238
238
  if (!Number.isInteger(padding) || padding < 1) return require_errors.err(require_errors.validationError("VOXEL_INVALID_PADDING", "padding must be an integer >= 1."));
239
- const engine = resolveEngine(id);
239
+ const engine = resolveEngine$1(id);
240
240
  if (require_errors.isErr(engine)) return engine;
241
241
  try {
242
242
  try {
243
- var _usingCtx$6 = require_shapeTypes._usingCtx();
244
- const repaired = _usingCtx$6.u(engine.value.repair_mesh(mesh.vertices, mesh.triangles, resolution, padding));
243
+ var _usingCtx$7 = require_shapeTypes._usingCtx();
244
+ const repaired = _usingCtx$7.u(engine.value.repair_mesh(mesh.vertices, mesh.triangles, resolution, padding));
245
245
  const vertexCount = repaired.positions.length / 3;
246
246
  return require_errors.ok({
247
247
  vertices: repaired.positions,
@@ -255,9 +255,9 @@ function repairMesh(mesh, opts, id) {
255
255
  }]
256
256
  });
257
257
  } catch (_) {
258
- _usingCtx$6.e = _;
258
+ _usingCtx$7.e = _;
259
259
  } finally {
260
- _usingCtx$6.d();
260
+ _usingCtx$7.d();
261
261
  }
262
262
  } catch (cause) {
263
263
  return require_errors.err(require_errors.computationError("VOXEL_REPAIR_FAILED", cause instanceof Error ? cause.message : "voxel repair failed (grid too large?).", cause));
@@ -286,16 +286,16 @@ function shapeToMeshInput(shape, deflection = DEFAULT_DEFLECTION) {
286
286
  }
287
287
  //#endregion
288
288
  //#region src/voxel/meshOpsFns.ts
289
- var DEFAULT_RESOLUTION$1 = 48;
290
- var DEFAULT_PADDING$1 = 2;
291
- var BOOLEAN_OP_CODES = {
289
+ var DEFAULT_RESOLUTION$3 = 48;
290
+ var DEFAULT_PADDING$3 = 2;
291
+ var BOOLEAN_OP_CODES$1 = {
292
292
  union: 0,
293
293
  intersection: 1,
294
294
  difference: 2
295
295
  };
296
- function resolveGridParams(opts) {
297
- const resolution = opts?.resolution ?? DEFAULT_RESOLUTION$1;
298
- const padding = opts?.padding ?? DEFAULT_PADDING$1;
296
+ function resolveGridParams$2(opts) {
297
+ const resolution = opts?.resolution ?? DEFAULT_RESOLUTION$3;
298
+ const padding = opts?.padding ?? DEFAULT_PADDING$3;
299
299
  if (!Number.isInteger(resolution) || resolution < 1) return require_errors.err(require_errors.validationError("VOXEL_INVALID_RESOLUTION", "resolution must be an integer >= 1."));
300
300
  if (!Number.isInteger(padding) || padding < 1) return require_errors.err(require_errors.validationError("VOXEL_INVALID_PADDING", "padding must be an integer >= 1."));
301
301
  return require_errors.ok({
@@ -330,18 +330,18 @@ function offsetMesh(mesh, distance, opts, id) {
330
330
  if (invalid) return require_errors.err(invalid);
331
331
  if (mesh.vertices.length === 0 || mesh.triangles.length === 0) return require_errors.err(require_errors.validationError("VOXEL_EMPTY_MESH", "offsetMesh requires a non-empty triangle mesh."));
332
332
  if (!Number.isFinite(distance)) return require_errors.err(require_errors.validationError("VOXEL_INVALID_DISTANCE", "distance must be a finite number."));
333
- const params = resolveGridParams(opts);
333
+ const params = resolveGridParams$2(opts);
334
334
  if (require_errors.isErr(params)) return params;
335
- const engine = resolveEngine(id);
335
+ const engine = resolveEngine$1(id);
336
336
  if (require_errors.isErr(engine)) return engine;
337
337
  try {
338
338
  try {
339
- var _usingCtx$5 = require_shapeTypes._usingCtx();
340
- return meshFromResult(_usingCtx$5.u(engine.value.offset_mesh(mesh.vertices, mesh.triangles, distance, params.value.resolution, params.value.padding)));
339
+ var _usingCtx$6 = require_shapeTypes._usingCtx();
340
+ return meshFromResult(_usingCtx$6.u(engine.value.offset_mesh(mesh.vertices, mesh.triangles, distance, params.value.resolution, params.value.padding)));
341
341
  } catch (_) {
342
- _usingCtx$5.e = _;
342
+ _usingCtx$6.e = _;
343
343
  } finally {
344
- _usingCtx$5.d();
344
+ _usingCtx$6.d();
345
345
  }
346
346
  } catch (cause) {
347
347
  return require_errors.err(require_errors.computationError("VOXEL_OFFSET_FAILED", cause instanceof Error ? cause.message : "voxel offset failed (grid too large?).", cause));
@@ -359,9 +359,9 @@ function shellMesh(mesh, thickness, opts, id) {
359
359
  if (invalid) return require_errors.err(invalid);
360
360
  if (mesh.vertices.length === 0 || mesh.triangles.length === 0) return require_errors.err(require_errors.validationError("VOXEL_EMPTY_MESH", "shellMesh requires a non-empty triangle mesh."));
361
361
  if (!Number.isFinite(thickness) || thickness <= 0) return require_errors.err(require_errors.validationError("VOXEL_INVALID_THICKNESS", "thickness must be a finite number > 0."));
362
- const params = resolveGridParams(opts);
362
+ const params = resolveGridParams$2(opts);
363
363
  if (require_errors.isErr(params)) return params;
364
- const engine = resolveEngine(id);
364
+ const engine = resolveEngine$1(id);
365
365
  if (require_errors.isErr(engine)) return engine;
366
366
  try {
367
367
  try {
@@ -390,10 +390,10 @@ function voxelBoolean(a, b, op, opts, id) {
390
390
  if (invalidB) return require_errors.err(invalidB);
391
391
  if (a.vertices.length === 0 || a.triangles.length === 0) return require_errors.err(require_errors.validationError("VOXEL_EMPTY_MESH", "voxelBoolean requires a non-empty mesh for operand A."));
392
392
  if (b.vertices.length === 0 || b.triangles.length === 0) return require_errors.err(require_errors.validationError("VOXEL_EMPTY_MESH", "voxelBoolean requires a non-empty mesh for operand B."));
393
- const opCode = BOOLEAN_OP_CODES[op];
394
- const params = resolveGridParams(opts);
393
+ const opCode = BOOLEAN_OP_CODES$1[op];
394
+ const params = resolveGridParams$2(opts);
395
395
  if (require_errors.isErr(params)) return params;
396
- const engine = resolveEngine(id);
396
+ const engine = resolveEngine$1(id);
397
397
  if (require_errors.isErr(engine)) return engine;
398
398
  try {
399
399
  try {
@@ -441,6 +441,423 @@ function voxelBooleanShapes(a, b, op, opts, id) {
441
441
  return voxelBoolean(meshA.value, meshB.value, op, opts, id);
442
442
  }
443
443
  //#endregion
444
+ //#region src/voxel/fieldFns.ts
445
+ var DEFAULT_RESOLUTION$2 = 48;
446
+ var DEFAULT_PADDING$2 = 2;
447
+ var BOOLEAN_OP_CODES = {
448
+ union: 0,
449
+ intersection: 1,
450
+ difference: 2
451
+ };
452
+ function resolveGridParams$1(opts) {
453
+ const resolution = opts?.resolution ?? DEFAULT_RESOLUTION$2;
454
+ const padding = opts?.padding ?? DEFAULT_PADDING$2;
455
+ if (!Number.isInteger(resolution) || resolution < 1) return require_errors.err(require_errors.validationError("VOXEL_INVALID_RESOLUTION", "resolution must be an integer >= 1."));
456
+ if (!Number.isInteger(padding) || padding < 1) return require_errors.err(require_errors.validationError("VOXEL_INVALID_PADDING", "padding must be an integer >= 1."));
457
+ return require_errors.ok({
458
+ resolution,
459
+ padding
460
+ });
461
+ }
462
+ /**
463
+ * Copy the buffers out of a WASM {@link VoxelRepairResult} into a plain
464
+ * {@link KernelMeshResult}, freeing the WASM result via `using` (the getters
465
+ * copy, so the mesh survives the free). An empty contour surfaces as an error.
466
+ */
467
+ function meshFromField(field) {
468
+ try {
469
+ var _usingCtx$5 = require_shapeTypes._usingCtx();
470
+ const rawResult = field.contour();
471
+ const { positions, normals, indices } = _usingCtx$5.u({
472
+ value: rawResult,
473
+ [Symbol.dispose]() {
474
+ rawResult.free();
475
+ }
476
+ }).value;
477
+ if (positions.length === 0 || indices.length === 0) return require_errors.err(require_errors.computationError("VOXEL_DEGENERATE_RESULT", "the voxel field contoured to an empty mesh (over-shrunk offset or disjoint operands?)."));
478
+ const vertexCount = positions.length / 3;
479
+ return require_errors.ok({
480
+ vertices: positions,
481
+ normals,
482
+ triangles: indices,
483
+ uvs: new Float32Array(vertexCount * 2),
484
+ faceGroups: [{
485
+ start: 0,
486
+ count: indices.length / 3,
487
+ faceHash: 0
488
+ }]
489
+ });
490
+ } catch (_) {
491
+ _usingCtx$5.e = _;
492
+ } finally {
493
+ _usingCtx$5.d();
494
+ }
495
+ }
496
+ function fieldDeletable(raw) {
497
+ return {
498
+ raw,
499
+ delete() {
500
+ raw.free();
501
+ }
502
+ };
503
+ }
504
+ function makeFieldHandle(raw) {
505
+ return makeHandle(raw);
506
+ }
507
+ function makeHandle(raw) {
508
+ const inner = require_shapeTypes.createKernelHandle(fieldDeletable(raw));
509
+ const handle = {
510
+ get value() {
511
+ return inner.value.raw;
512
+ },
513
+ get disposed() {
514
+ return inner.disposed;
515
+ },
516
+ [Symbol.dispose]() {
517
+ inner[Symbol.dispose]();
518
+ },
519
+ boolean(other, op) {
520
+ this.value.boolean(other.value, BOOLEAN_OP_CODES[op]);
521
+ return handle;
522
+ },
523
+ offset(distance) {
524
+ this.value.offset(distance);
525
+ return handle;
526
+ },
527
+ shell(thickness) {
528
+ this.value.shell(thickness);
529
+ return handle;
530
+ },
531
+ reinit() {
532
+ this.value.reinit();
533
+ return handle;
534
+ },
535
+ contour() {
536
+ const mesh = meshFromField(this.value);
537
+ if (require_errors.isErr(mesh)) throw new Error(mesh.error.message);
538
+ return mesh.value;
539
+ }
540
+ };
541
+ return handle;
542
+ }
543
+ /** Live-handle guard (the `VoxelFieldHandle` analogue of disposal's `isLive`). */
544
+ function isLive$1(handle) {
545
+ return !handle.disposed;
546
+ }
547
+ function disposedErr() {
548
+ return require_errors.err(require_errors.validationError("VOXEL_FIELD_DISPOSED", "the voxel field handle has been disposed."));
549
+ }
550
+ /**
551
+ * Voxelize a mesh into a persistent dense {@link VoxelFieldHandle}: one grid you
552
+ * can boolean / offset / shell / reinit in place, then contour once. The handle
553
+ * is disposable — free the WASM grid with `using` (or `[Symbol.dispose]()`).
554
+ *
555
+ * `resolution` sizes the longest bbox axis; `padding` is the air-margin ring.
556
+ * Errors on an empty/invalid mesh, or if the grid would exceed the dense budget
557
+ * (the persistent path is dense-only) or the voxel cap.
558
+ */
559
+ function voxelField(mesh, opts, id) {
560
+ const invalid = validateMesh(mesh);
561
+ if (invalid) return require_errors.err(invalid);
562
+ if (mesh.vertices.length === 0 || mesh.triangles.length === 0) return require_errors.err(require_errors.validationError("VOXEL_EMPTY_MESH", "voxelField requires a non-empty triangle mesh."));
563
+ const params = resolveGridParams$1(opts);
564
+ if (require_errors.isErr(params)) return params;
565
+ const engine = resolveEngine$1(id);
566
+ if (require_errors.isErr(engine)) return engine;
567
+ try {
568
+ return require_errors.ok(makeHandle(new engine.value.VoxelField(mesh.vertices, mesh.triangles, params.value.resolution, params.value.padding)));
569
+ } catch (cause) {
570
+ return require_errors.err(require_errors.computationError("VOXEL_FIELD_VOXELIZE_FAILED", cause instanceof Error ? cause.message : "voxel field voxelization failed (grid too large or non-dense?).", cause));
571
+ }
572
+ }
573
+ /**
574
+ * Boolean two meshes into ONE co-registered, chainable {@link VoxelFieldHandle}:
575
+ * voxelize both onto a single shared grid sized to their union bbox, combine by
576
+ * `op`, and keep the field. This is THE correct way to "boolean then chain
577
+ * offset/shell" two independently-described meshes — unlike {@link fieldBoolean},
578
+ * which requires the operands to already share grid geometry. The result is
579
+ * dirty (the blend drifts the gradient), so a subsequent offset/shell
580
+ * auto-reinitializes. The handle is disposable — free it with `using`.
581
+ *
582
+ * `op` is `'difference'` = A − B. Errors on an empty/invalid mesh, or if the
583
+ * shared grid would exceed the dense budget (the persistent path is dense-only).
584
+ */
585
+ function voxelBooleanField(a, b, op, opts, id) {
586
+ const invalidA = validateMesh(a);
587
+ if (invalidA) return require_errors.err(invalidA);
588
+ const invalidB = validateMesh(b);
589
+ if (invalidB) return require_errors.err(invalidB);
590
+ if (a.vertices.length === 0 || a.triangles.length === 0) return require_errors.err(require_errors.validationError("VOXEL_EMPTY_MESH", "voxelBooleanField requires a non-empty mesh for operand A."));
591
+ if (b.vertices.length === 0 || b.triangles.length === 0) return require_errors.err(require_errors.validationError("VOXEL_EMPTY_MESH", "voxelBooleanField requires a non-empty mesh for operand B."));
592
+ const params = resolveGridParams$1(opts);
593
+ if (require_errors.isErr(params)) return params;
594
+ const engine = resolveEngine$1(id);
595
+ if (require_errors.isErr(engine)) return engine;
596
+ try {
597
+ return require_errors.ok(makeHandle(engine.value.VoxelField.boolean_of(a.vertices, a.triangles, b.vertices, b.triangles, BOOLEAN_OP_CODES[op], params.value.resolution, params.value.padding)));
598
+ } catch (cause) {
599
+ return require_errors.err(require_errors.computationError("VOXEL_FIELD_BOOLEAN_FAILED", cause instanceof Error ? cause.message : "voxel field boolean failed (grid too large or non-dense?).", cause));
600
+ }
601
+ }
602
+ /**
603
+ * CSG-combine two fields IN PLACE, returning the SAME `handle` for chaining. The
604
+ * min/max blend keeps the zero set exact but drifts the gradient near the join,
605
+ * so a subsequent {@link fieldOffset}/{@link fieldShell} auto-reinitializes.
606
+ *
607
+ * PRECONDITION: both operands must be CO-REGISTERED — same origin, spacing, AND
608
+ * dims. Two fields built by {@link voxelField} from DIFFERENT meshes generally do
609
+ * NOT share geometry (each sizes its grid to its own bbox), and the WASM guard
610
+ * rejects that mismatch as an `err(...)` rather than silently blending mismatched
611
+ * coordinate frames. For the easy co-registered path, build the field directly
612
+ * from both meshes with {@link voxelBooleanField}.
613
+ */
614
+ function fieldBoolean(handle, other, op) {
615
+ if (!isLive$1(handle) || !isLive$1(other)) return disposedErr();
616
+ try {
617
+ handle.value.boolean(other.value, BOOLEAN_OP_CODES[op]);
618
+ return require_errors.ok(handle);
619
+ } catch (cause) {
620
+ return require_errors.err(require_errors.computationError("VOXEL_FIELD_BOOLEAN_FAILED", cause instanceof Error ? cause.message : "voxel field boolean failed (dim mismatch?).", cause));
621
+ }
622
+ }
623
+ /**
624
+ * Offset the field's surface IN PLACE (>0 outward, <0 inward), returning the
625
+ * SAME `handle`. Auto-reinitializes first if the field is dirty (post-boolean),
626
+ * so the iso-shift always rides a true SDF.
627
+ */
628
+ function fieldOffset(handle, distance) {
629
+ if (!isLive$1(handle)) return disposedErr();
630
+ if (!Number.isFinite(distance)) return require_errors.err(require_errors.validationError("VOXEL_INVALID_DISTANCE", "distance must be a finite number."));
631
+ try {
632
+ handle.value.offset(distance);
633
+ return require_errors.ok(handle);
634
+ } catch (cause) {
635
+ return require_errors.err(require_errors.computationError("VOXEL_FIELD_OFFSET_FAILED", cause instanceof Error ? cause.message : "voxel field offset failed.", cause));
636
+ }
637
+ }
638
+ /**
639
+ * Hollow the field into an inward shell of `thickness` IN PLACE, returning the
640
+ * SAME `handle`. Auto-reinitializes first if dirty; the result is dirty again
641
+ * (the shell re-introduces a kink).
642
+ */
643
+ function fieldShell(handle, thickness) {
644
+ if (!isLive$1(handle)) return disposedErr();
645
+ if (!Number.isFinite(thickness) || thickness <= 0) return require_errors.err(require_errors.validationError("VOXEL_INVALID_THICKNESS", "thickness must be a finite number > 0."));
646
+ try {
647
+ handle.value.shell(thickness);
648
+ return require_errors.ok(handle);
649
+ } catch (cause) {
650
+ return require_errors.err(require_errors.computationError("VOXEL_FIELD_SHELL_FAILED", cause instanceof Error ? cause.message : "voxel field shell failed.", cause));
651
+ }
652
+ }
653
+ /**
654
+ * Explicitly reinitialize φ to a true SDF (|∇φ|=1) while preserving the zero
655
+ * set, returning the SAME `handle`. Idempotent on a clean field. Offset/shell
656
+ * already auto-reinitialize, so this is for advanced control only.
657
+ */
658
+ function fieldReinit(handle) {
659
+ if (!isLive$1(handle)) return disposedErr();
660
+ try {
661
+ handle.value.reinit();
662
+ return require_errors.ok(handle);
663
+ } catch (cause) {
664
+ return require_errors.err(require_errors.computationError("VOXEL_FIELD_REINIT_FAILED", cause instanceof Error ? cause.message : "voxel field reinit failed.", cause));
665
+ }
666
+ }
667
+ /**
668
+ * Surface-Nets contour the current field to a {@link KernelMeshResult}. The
669
+ * field stays alive and chainable afterwards (contour borrows it). An empty
670
+ * contour surfaces as `VOXEL_DEGENERATE_RESULT`.
671
+ */
672
+ function fieldContour(handle) {
673
+ if (!isLive$1(handle)) return disposedErr();
674
+ try {
675
+ return meshFromField(handle.value);
676
+ } catch (cause) {
677
+ return require_errors.err(require_errors.computationError("VOXEL_FIELD_CONTOUR_FAILED", cause instanceof Error ? cause.message : "voxel field contour failed.", cause));
678
+ }
679
+ }
680
+ /**
681
+ * Voxelize a B-rep shape into a persistent {@link VoxelFieldHandle}: tessellate
682
+ * it, then run {@link voxelField}. Threads a meshing failure back as an
683
+ * `err(...)`. The handle is disposable — free it with `using`.
684
+ */
685
+ function voxelFieldFromShape(shape, opts, id) {
686
+ const meshInput = shapeToMeshInput(shape);
687
+ if (require_errors.isErr(meshInput)) return meshInput;
688
+ return voxelField(meshInput.value, opts, id);
689
+ }
690
+ /**
691
+ * Boolean two B-rep shapes into one co-registered, chainable
692
+ * {@link VoxelFieldHandle}: tessellate both, then run {@link voxelBooleanField}.
693
+ * `op` is `'difference'` = A − B. Threads either meshing failure back as an
694
+ * `err(...)`. The handle is disposable — free it with `using`.
695
+ */
696
+ function voxelBooleanFieldShapes(a, b, op, opts, id) {
697
+ const meshA = shapeToMeshInput(a);
698
+ if (require_errors.isErr(meshA)) return meshA;
699
+ const meshB = shapeToMeshInput(b);
700
+ if (require_errors.isErr(meshB)) return meshB;
701
+ return voxelBooleanField(meshA.value, meshB.value, op, opts, id);
702
+ }
703
+ //#endregion
704
+ //#region src/implicit/sdfFns.ts
705
+ var DEFAULT_RESOLUTION$1 = 48;
706
+ var DEFAULT_PADDING$1 = 2;
707
+ function resolveEngine(id) {
708
+ try {
709
+ return require_errors.ok(getVoxel(id));
710
+ } catch (cause) {
711
+ return require_errors.err(require_errors.moduleInitError("VOXEL_NOT_INITIALIZED", cause instanceof Error ? cause.message : "voxel engine not initialized", cause));
712
+ }
713
+ }
714
+ function resolveGridParams(opts) {
715
+ const resolution = opts?.resolution ?? DEFAULT_RESOLUTION$1;
716
+ const padding = opts?.padding ?? DEFAULT_PADDING$1;
717
+ if (!Number.isInteger(resolution) || resolution < 1) return require_errors.err(require_errors.validationError("SDF_INVALID_RESOLUTION", "resolution must be an integer >= 1."));
718
+ if (!Number.isInteger(padding) || padding < 1) return require_errors.err(require_errors.validationError("SDF_INVALID_PADDING", "padding must be an integer >= 1."));
719
+ return require_errors.ok({
720
+ resolution,
721
+ padding
722
+ });
723
+ }
724
+ function sdfDeletable(raw) {
725
+ return {
726
+ raw,
727
+ delete() {
728
+ raw.free();
729
+ }
730
+ };
731
+ }
732
+ function makeSdfHandle(raw) {
733
+ const inner = require_shapeTypes.createKernelHandle(sdfDeletable(raw));
734
+ return {
735
+ get value() {
736
+ return inner.value.raw;
737
+ },
738
+ get disposed() {
739
+ return inner.disposed;
740
+ },
741
+ [Symbol.dispose]() {
742
+ inner[Symbol.dispose]();
743
+ },
744
+ union(other) {
745
+ return makeSdfHandle(this.value.union(other.value));
746
+ },
747
+ intersection(other) {
748
+ return makeSdfHandle(this.value.intersection(other.value));
749
+ },
750
+ difference(other) {
751
+ return makeSdfHandle(this.value.difference(other.value));
752
+ },
753
+ smoothUnion(other, k) {
754
+ return makeSdfHandle(this.value.smooth_union(other.value, k));
755
+ },
756
+ smoothIntersection(other, k) {
757
+ return makeSdfHandle(this.value.smooth_intersection(other.value, k));
758
+ },
759
+ smoothDifference(other, k) {
760
+ return makeSdfHandle(this.value.smooth_difference(other.value, k));
761
+ },
762
+ offset(distance) {
763
+ return makeSdfHandle(this.value.offset(distance));
764
+ },
765
+ round(radius) {
766
+ return makeSdfHandle(this.value.round(radius));
767
+ },
768
+ shell(thickness) {
769
+ return makeSdfHandle(this.value.shell(thickness));
770
+ },
771
+ onion(thickness) {
772
+ return makeSdfHandle(this.value.onion(thickness));
773
+ },
774
+ translate(x, y, z) {
775
+ return makeSdfHandle(this.value.translate(x, y, z));
776
+ },
777
+ rotate(ax, ay, az, angle) {
778
+ return makeSdfHandle(this.value.rotate(ax, ay, az, angle));
779
+ },
780
+ scale(s) {
781
+ return makeSdfHandle(this.value.scale(s));
782
+ },
783
+ rasterize(opts) {
784
+ return rasterizeField(this.value, opts);
785
+ },
786
+ rasterizeIn(bounds, opts) {
787
+ return rasterizeFieldIn(this.value, bounds, opts);
788
+ }
789
+ };
790
+ }
791
+ function rasterizeField(sdf, opts) {
792
+ const params = resolveGridParams(opts);
793
+ if (require_errors.isErr(params)) return params;
794
+ try {
795
+ return require_errors.ok(makeFieldHandle(sdf.rasterize(params.value.resolution, params.value.padding)));
796
+ } catch (cause) {
797
+ return rasterizeError(cause);
798
+ }
799
+ }
800
+ function rasterizeFieldIn(sdf, bounds, opts) {
801
+ const params = resolveGridParams(opts);
802
+ if (require_errors.isErr(params)) return params;
803
+ const invalid = validateBounds(bounds);
804
+ if (invalid) return require_errors.err(invalid);
805
+ try {
806
+ return require_errors.ok(makeFieldHandle(sdf.rasterize_in(bounds.min[0], bounds.min[1], bounds.min[2], bounds.max[0], bounds.max[1], bounds.max[2], params.value.resolution, params.value.padding)));
807
+ } catch (cause) {
808
+ return rasterizeError(cause);
809
+ }
810
+ }
811
+ function rasterizeError(cause) {
812
+ return require_errors.err(require_errors.computationError("SDF_RASTERIZE_FAILED", cause instanceof Error ? cause.message : "SDF rasterization failed (grid too large?).", cause));
813
+ }
814
+ function validateBounds(bounds) {
815
+ if (![...bounds.min, ...bounds.max].every((v) => Number.isFinite(v))) return require_errors.validationError("SDF_INVALID_BOUNDS", "bounds must be finite numbers.");
816
+ for (let axis = 0; axis < 3; axis++) if (bounds.max[axis] <= bounds.min[axis]) return require_errors.validationError("SDF_INVALID_BOUNDS", "bounds max must exceed min on every axis.");
817
+ return null;
818
+ }
819
+ function build(make, id) {
820
+ const engine = resolveEngine(id);
821
+ if (require_errors.isErr(engine)) return engine;
822
+ try {
823
+ return require_errors.ok(makeSdfHandle(make(engine.value)));
824
+ } catch (cause) {
825
+ return require_errors.err(require_errors.computationError("SDF_BUILD_FAILED", cause instanceof Error ? cause.message : "SDF primitive construction failed.", cause));
826
+ }
827
+ }
828
+ /** A sphere of radius `r`, centered at the origin. */
829
+ function sphere(r, id) {
830
+ return build((e) => e.Sdf.sphere(r), id);
831
+ }
832
+ /** An axis-aligned box of half-extents `(hx, hy, hz)`, centered at the origin. */
833
+ function box$1(hx, hy, hz, id) {
834
+ return build((e) => e.Sdf.box_(hx, hy, hz), id);
835
+ }
836
+ /** A box with rounded edges of radius `r`. */
837
+ function roundedBox(hx, hy, hz, r, id) {
838
+ return build((e) => e.Sdf.rounded_box(hx, hy, hz, r), id);
839
+ }
840
+ /** A capped cylinder, axis +Z, radius `r`, total height `h`, centered at origin. */
841
+ function cylinder$1(r, h, id) {
842
+ return build((e) => e.Sdf.cylinder(r, h), id);
843
+ }
844
+ /** A capped cone centered at the origin: base radius `r` at z = −h/2 tapering to an apex at z = +h/2. */
845
+ function cone$1(r, h, id) {
846
+ return build((e) => e.Sdf.cone(r, h), id);
847
+ }
848
+ /** A capsule: a line segment `a`→`b` of radius `r`. */
849
+ function capsule(a, b, r, id) {
850
+ return build((e) => e.Sdf.capsule(a[0], a[1], a[2], b[0], b[1], b[2], r), id);
851
+ }
852
+ /** A torus in the XY plane (axis +Z): a `minor`-radius tube on a `major` circle. */
853
+ function torus(major, minor, id) {
854
+ return build((e) => e.Sdf.torus(major, minor), id);
855
+ }
856
+ /** A half-space: the plane through `h·n` with outward normal `n` (normalized). */
857
+ function plane(n, h, id) {
858
+ return build((e) => e.Sdf.plane(n[0], n[1], n[2], h), id);
859
+ }
860
+ //#endregion
444
861
  //#region src/lattice/latticeFns.ts
445
862
  var LATTICE_TAGS = {
446
863
  gyroid: 0,
@@ -494,7 +911,7 @@ function latticeInfill(mesh, opts, id) {
494
911
  if (mesh.vertices.length === 0 || mesh.triangles.length === 0) return require_errors.err(require_errors.validationError("LATTICE_EMPTY_MESH", "latticeInfill requires a non-empty triangle mesh."));
495
912
  const resolved = validateOptions(opts);
496
913
  if (require_errors.isErr(resolved)) return resolved;
497
- const engine = resolveEngine(id);
914
+ const engine = resolveEngine$1(id);
498
915
  if (require_errors.isErr(engine)) return engine;
499
916
  const { tag, period, thickness, resolution, padding } = resolved.value;
500
917
  try {
@@ -535,7 +952,7 @@ function tpmsLattice(bounds, opts, id) {
535
952
  }
536
953
  const resolved = validateOptions(opts);
537
954
  if (require_errors.isErr(resolved)) return resolved;
538
- const engine = resolveEngine(id);
955
+ const engine = resolveEngine$1(id);
539
956
  if (require_errors.isErr(engine)) return engine;
540
957
  const { tag, period, thickness, resolution, padding } = resolved.value;
541
958
  const [minX, minY, minZ] = bounds.min;
@@ -4204,7 +4621,7 @@ function depsOf(...sources) {
4204
4621
  }
4205
4622
  return acc.size === 0 ? EMPTY_DEPS : acc;
4206
4623
  }
4207
- function box$1(x, y, z) {
4624
+ function box$2(x, y, z) {
4208
4625
  const xe = asScalarExpr(x);
4209
4626
  const ye = asScalarExpr(y);
4210
4627
  const ze = asScalarExpr(z);
@@ -4219,7 +4636,7 @@ function box$1(x, y, z) {
4219
4636
  freeParams: depsOf(xe, ye, ze)
4220
4637
  };
4221
4638
  }
4222
- function sphere$1(radius) {
4639
+ function sphere$2(radius) {
4223
4640
  const re = asScalarExpr(radius);
4224
4641
  return {
4225
4642
  kind: "Sphere",
@@ -4228,7 +4645,7 @@ function sphere$1(radius) {
4228
4645
  freeParams: re.freeParams
4229
4646
  };
4230
4647
  }
4231
- function cylinder$1(radius, height) {
4648
+ function cylinder$2(radius, height) {
4232
4649
  const re = asScalarExpr(radius);
4233
4650
  const he = asScalarExpr(height);
4234
4651
  return {
@@ -4239,7 +4656,7 @@ function cylinder$1(radius, height) {
4239
4656
  freeParams: depsOf(re, he)
4240
4657
  };
4241
4658
  }
4242
- function cone$1(radius1, radius2, height) {
4659
+ function cone$2(radius1, radius2, height) {
4243
4660
  const r1 = asScalarExpr(radius1);
4244
4661
  const r2 = asScalarExpr(radius2);
4245
4662
  const he = asScalarExpr(height);
@@ -4254,7 +4671,7 @@ function cone$1(radius1, radius2, height) {
4254
4671
  freeParams: depsOf(r1, r2, he)
4255
4672
  };
4256
4673
  }
4257
- function torus$1(majorRadius, minorRadius) {
4674
+ function torus$2(majorRadius, minorRadius) {
4258
4675
  const ma = asScalarExpr(majorRadius);
4259
4676
  const mi = asScalarExpr(minorRadius);
4260
4677
  return {
@@ -5159,7 +5576,7 @@ function readSingleExpr(j, key, build) {
5159
5576
  function readPrimitive(kind, j) {
5160
5577
  switch (kind) {
5161
5578
  case "Box": return readBox(j);
5162
- case "Sphere": return readSingleExpr(j, "radius", sphere$1);
5579
+ case "Sphere": return readSingleExpr(j, "radius", sphere$2);
5163
5580
  case "Cylinder": return readCylinder(j);
5164
5581
  case "Cone": return readCone(j);
5165
5582
  case "Torus": return readTorus(j);
@@ -5178,14 +5595,14 @@ function readBox(j) {
5178
5595
  if (!y.ok) return y;
5179
5596
  const z = readExpr(j["z"]);
5180
5597
  if (!z.ok) return z;
5181
- return require_errors.ok(box$1(x.value, y.value, z.value));
5598
+ return require_errors.ok(box$2(x.value, y.value, z.value));
5182
5599
  }
5183
5600
  function readCylinder(j) {
5184
5601
  const r = readExpr(j["radius"]);
5185
5602
  if (!r.ok) return r;
5186
5603
  const h = readExpr(j["height"]);
5187
5604
  if (!h.ok) return h;
5188
- return require_errors.ok(cylinder$1(r.value, h.value));
5605
+ return require_errors.ok(cylinder$2(r.value, h.value));
5189
5606
  }
5190
5607
  function readCone(j) {
5191
5608
  const r1 = readExpr(j["radius1"]);
@@ -5194,14 +5611,14 @@ function readCone(j) {
5194
5611
  if (!r2.ok) return r2;
5195
5612
  const h = readExpr(j["height"]);
5196
5613
  if (!h.ok) return h;
5197
- return require_errors.ok(cone$1(r1.value, r2.value, h.value));
5614
+ return require_errors.ok(cone$2(r1.value, r2.value, h.value));
5198
5615
  }
5199
5616
  function readTorus(j) {
5200
5617
  const ma = readExpr(j["majorRadius"]);
5201
5618
  if (!ma.ok) return ma;
5202
5619
  const mi = readExpr(j["minorRadius"]);
5203
5620
  if (!mi.ok) return mi;
5204
- return require_errors.ok(torus$1(ma.value, mi.value));
5621
+ return require_errors.ok(torus$2(ma.value, mi.value));
5205
5622
  }
5206
5623
  function readPolygon(j) {
5207
5624
  const pts = j["points"];
@@ -5377,11 +5794,11 @@ function foldBuildVec(dim, comps) {
5377
5794
  }
5378
5795
  function optimizeNode(n) {
5379
5796
  switch (n.kind) {
5380
- case "Box": return box$1(foldExpr(n.x), foldExpr(n.y), foldExpr(n.z));
5381
- case "Sphere": return sphere$1(foldExpr(n.radius));
5382
- case "Cylinder": return cylinder$1(foldExpr(n.radius), foldExpr(n.height));
5383
- case "Cone": return cone$1(foldExpr(n.radius1), foldExpr(n.radius2), foldExpr(n.height));
5384
- case "Torus": return torus$1(foldExpr(n.majorRadius), foldExpr(n.minorRadius));
5797
+ case "Box": return box$2(foldExpr(n.x), foldExpr(n.y), foldExpr(n.z));
5798
+ case "Sphere": return sphere$2(foldExpr(n.radius));
5799
+ case "Cylinder": return cylinder$2(foldExpr(n.radius), foldExpr(n.height));
5800
+ case "Cone": return cone$2(foldExpr(n.radius1), foldExpr(n.radius2), foldExpr(n.height));
5801
+ case "Torus": return torus$2(foldExpr(n.majorRadius), foldExpr(n.minorRadius));
5385
5802
  case "Polygon": return polygon$1(n.points.map(foldExpr));
5386
5803
  case "Circle": return circle$1(foldExpr(n.radius));
5387
5804
  case "Line": return line$1(foldExpr(n.from), foldExpr(n.to));
@@ -5539,15 +5956,15 @@ var csg_exports = /* @__PURE__ */ require_textBlueprints.__exportAll({
5539
5956
  asVec2Expr: () => asVec2Expr,
5540
5957
  asVec3Expr: () => asVec3Expr,
5541
5958
  binOp: () => binOp,
5542
- box: () => box$1,
5959
+ box: () => box$2,
5543
5960
  buildVec: () => buildVec,
5544
5961
  circle: () => circle$1,
5545
5962
  component: () => component,
5546
5963
  compound: () => compound$1,
5547
- cone: () => cone$1,
5964
+ cone: () => cone$2,
5548
5965
  cut: () => cut$1,
5549
5966
  cutAll: () => cutAll$1,
5550
- cylinder: () => cylinder$1,
5967
+ cylinder: () => cylinder$2,
5551
5968
  emptyFace: () => emptyFace,
5552
5969
  emptySolid: () => emptySolid,
5553
5970
  emptyWire: () => emptyWire,
@@ -5569,9 +5986,9 @@ var csg_exports = /* @__PURE__ */ require_textBlueprints.__exportAll({
5569
5986
  replaceNode: () => replaceNode,
5570
5987
  rotate: () => rotate$1,
5571
5988
  scale: () => scale$1,
5572
- sphere: () => sphere$1,
5989
+ sphere: () => sphere$2,
5573
5990
  toJSON: () => toJSON,
5574
- torus: () => torus$1,
5991
+ torus: () => torus$2,
5575
5992
  translate: () => translate$1,
5576
5993
  unaryOp: () => unaryOp,
5577
5994
  vec2Lit: () => vec2Lit,
@@ -5776,6 +6193,11 @@ exports.faceFinder = require_helpers.faceFinder;
5776
6193
  exports.faceGeomType = require_faceFns.faceGeomType;
5777
6194
  exports.faceOrientation = require_faceFns.faceOrientation;
5778
6195
  exports.facesOfEdge = require_primitiveFns.facesOfEdge;
6196
+ exports.fieldBoolean = fieldBoolean;
6197
+ exports.fieldContour = fieldContour;
6198
+ exports.fieldOffset = fieldOffset;
6199
+ exports.fieldReinit = fieldReinit;
6200
+ exports.fieldShell = fieldShell;
5779
6201
  exports.fill = require_surfaceBuilders.fill;
5780
6202
  exports.filledFace = require_primitiveFns.filledFace;
5781
6203
  exports.fillet = fillet;
@@ -6046,6 +6468,14 @@ exports.roundedRectangleBlueprint = require_boolean2D.roundedRectangleBlueprint;
6046
6468
  exports.scale = scale;
6047
6469
  exports.scale2D = require_blueprintFns.scale2D;
6048
6470
  exports.scaleDrawing = require_drawFns.scaleDrawing;
6471
+ exports.sdfBox = box$1;
6472
+ exports.sdfCapsule = capsule;
6473
+ exports.sdfCone = cone$1;
6474
+ exports.sdfCylinder = cylinder$1;
6475
+ exports.sdfPlane = plane;
6476
+ exports.sdfRoundedBox = roundedBox;
6477
+ exports.sdfSphere = sphere;
6478
+ exports.sdfTorus = torus;
6049
6479
  exports.section = section;
6050
6480
  exports.sectionToFace = sectionToFace;
6051
6481
  exports.serializeHistory = require_historyFns.serializeHistory;
@@ -6164,7 +6594,11 @@ exports.vertexFinder = vertexFinder;
6164
6594
  exports.vertexPosition = require_topologyQueryFns.vertexPosition;
6165
6595
  exports.verticesOfEdge = require_primitiveFns.verticesOfEdge;
6166
6596
  exports.voxelBoolean = voxelBoolean;
6597
+ exports.voxelBooleanField = voxelBooleanField;
6598
+ exports.voxelBooleanFieldShapes = voxelBooleanFieldShapes;
6167
6599
  exports.voxelBooleanShapes = voxelBooleanShapes;
6600
+ exports.voxelField = voxelField;
6601
+ exports.voxelFieldFromShape = voxelFieldFromShape;
6168
6602
  exports.walkAssembly = require_historyFns.walkAssembly;
6169
6603
  exports.windingNumbers = windingNumbers;
6170
6604
  exports.wire = require_primitiveFns.wire;