forgecad 0.9.4 → 0.9.6

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.
Files changed (90) hide show
  1. package/dist/assets/{AdminPage-jwoEgwE_.js → AdminPage-Da6hhpJx.js} +1 -1
  2. package/dist/assets/{BlogPage-Ck7g3ue2.js → BlogPage-Bl_sKeWb.js} +1 -1
  3. package/dist/assets/{DocsPage-9WaRC14b.js → DocsPage-Blz3Tp4j.js} +1 -6
  4. package/dist/assets/EditorApp-CuiPbtn5.js +12754 -0
  5. package/dist/assets/{EmbedViewer-37_PfMwv.js → EmbedViewer-BFG6-Ufm.js} +2 -2
  6. package/dist/assets/{LandingPageProofDriven-CO8WL0CY.js → LandingPageProofDriven-DB9fQd5P.js} +1 -1
  7. package/dist/assets/{PricingPage-DADKGuOa.js → PricingPage-BMxYT_F0.js} +1 -1
  8. package/dist/assets/{SettingsPage-DKKI4W49.js → SettingsPage-VVQNrCAg.js} +1 -1
  9. package/dist/assets/{app-CwI02pTA.js → app-Dl9ymBWC.js} +355 -36
  10. package/dist/assets/cli/{render-Kw5hLEcL.js → render-CFtwKCCY.js} +203 -862
  11. package/dist/assets/{sectionPlaneMath-C8N0w8o3.js → distance-BEC2RjJi.js} +4150 -801
  12. package/dist/assets/{evalWorker-D6ub3kfS.js → evalWorker-CRvbzTXm.js} +2611 -528
  13. package/dist/assets/{manifold-CwDdMKyc.js → manifold-B9QSr-qP.js} +1 -1
  14. package/dist/assets/{manifold-DTvmxSDf.js → manifold-DpBXFS2K.js} +1 -1
  15. package/dist/assets/{manifold-lru0jwVw.js → manifold-DzZ4VRPs.js} +2 -2
  16. package/dist/assets/{renderSceneState-tvtNKNRi.js → renderSceneState-BuAXF2jh.js} +1 -1
  17. package/dist/assets/{reportWorker-DeqktDGt.js → reportWorker-BNWEnRg1.js} +2606 -525
  18. package/dist/cli/render.html +1 -1
  19. package/dist/docs/index.html +2 -2
  20. package/dist/docs-raw/AI/usage.md +0 -1
  21. package/dist/docs-raw/API/core/concepts.md +11 -1
  22. package/dist/docs-raw/CLI.md +64 -13
  23. package/dist/docs-raw/beta-operations.md +4 -0
  24. package/dist/docs-raw/deployment.md +38 -23
  25. package/dist/docs-raw/generated/assembly.md +8 -3
  26. package/dist/docs-raw/generated/concepts.md +126 -46
  27. package/dist/docs-raw/generated/core.md +97 -47
  28. package/dist/docs-raw/generated/curves.md +113 -595
  29. package/dist/docs-raw/generated/lib.md +40 -3
  30. package/dist/docs-raw/generated/output.md +6 -1
  31. package/dist/docs-raw/generated/sdf.md +50 -4
  32. package/dist/docs-raw/generated/sketch.md +9 -1
  33. package/dist/docs-raw/generated/viewport.md +1 -9
  34. package/dist/docs-raw/guides/inspection-bundles.md +40 -9
  35. package/dist/docs-raw/runbook.md +3 -3
  36. package/dist/docs-raw/skills/forgecad-blockout-model.md +1 -0
  37. package/dist/docs-raw/skills/forgecad-image-replicator.md +3 -1
  38. package/dist/docs-raw/skills/forgecad-make-a-model.md +48 -4
  39. package/dist/docs-raw/skills/forgecad-render-inspect.md +3 -1
  40. package/dist/docs-raw/skills/forgecad-visual-spec.md +2 -0
  41. package/dist/docs-raw/skills/forgecad.md +2 -1
  42. package/dist/docs-raw/skills/index.md +0 -1
  43. package/dist/index.html +1 -1
  44. package/dist/sitemap.xml +6 -6
  45. package/dist-cli/blender/render.py +43 -8
  46. package/dist-cli/forgecad.js +5729 -2015
  47. package/dist-cli/forgecad.js.map +1 -1
  48. package/dist-skill/CONTEXT.md +372 -667
  49. package/dist-skill/SKILL-dev.md +2 -1
  50. package/dist-skill/SKILL.md +2 -1
  51. package/dist-skill/docs/API/core/concepts.md +11 -1
  52. package/dist-skill/docs/CLI.md +64 -13
  53. package/dist-skill/docs/generated/assembly.md +8 -3
  54. package/dist-skill/docs/generated/core.md +97 -47
  55. package/dist-skill/docs/generated/curves.md +113 -595
  56. package/dist-skill/docs/generated/lib.md +40 -3
  57. package/dist-skill/docs/generated/output.md +6 -1
  58. package/dist-skill/docs/generated/sdf.md +50 -4
  59. package/dist-skill/docs/generated/sketch.md +9 -1
  60. package/dist-skill/docs/generated/viewport.md +1 -9
  61. package/dist-skill/docs/guides/inspection-bundles.md +40 -9
  62. package/dist-skill/docs-dev/API/core/concepts.md +11 -1
  63. package/dist-skill/docs-dev/CLI.md +64 -13
  64. package/dist-skill/docs-dev/generated/assembly.md +8 -3
  65. package/dist-skill/docs-dev/generated/core.md +97 -47
  66. package/dist-skill/docs-dev/generated/curves.md +113 -595
  67. package/dist-skill/docs-dev/generated/lib.md +40 -3
  68. package/dist-skill/docs-dev/generated/output.md +6 -1
  69. package/dist-skill/docs-dev/generated/sdf.md +50 -4
  70. package/dist-skill/docs-dev/generated/sketch.md +9 -1
  71. package/dist-skill/docs-dev/generated/viewport.md +1 -9
  72. package/dist-skill/docs-dev/guides/inspection-bundles.md +40 -9
  73. package/dist-skill/library/README.md +0 -1
  74. package/dist-skill/library/forgecad-blockout-model/SKILL.md +1 -0
  75. package/dist-skill/library/forgecad-image-replicator/SKILL.md +3 -1
  76. package/dist-skill/library/forgecad-make-a-model/SKILL.md +48 -4
  77. package/dist-skill/library/forgecad-render-inspect/SKILL.md +3 -1
  78. package/dist-skill/library/forgecad-visual-spec/SKILL.md +2 -0
  79. package/examples/api/drive-wheel-regions.forge.js +43 -0
  80. package/examples/api/guided-loft-olive-oil-bottle.forge.js +135 -0
  81. package/examples/api/sdf-circular-array-knurling.forge.js +19 -0
  82. package/examples/api/sdf-pattern2d-ceramic-ripple-set.forge.js +83 -0
  83. package/examples/api/sdf-pattern2d-grip-tread.forge.js +72 -0
  84. package/examples/api/sdf-pattern2d-orbital-jewelry.forge.js +62 -0
  85. package/examples/api/sdf-surface-basket-weave.forge.js +67 -0
  86. package/examples/api/sector-gear-body.forge.js +34 -0
  87. package/package.json +20 -2
  88. package/dist/assets/EditorApp-Dja2jMmW.js +0 -12509
  89. package/dist/docs-raw/skills/forgecad-api-dogfood.md +0 -130
  90. package/dist-skill/library/forgecad-api-dogfood/SKILL.md +0 -125
@@ -542,6 +542,47 @@ function cloneSdfFunctionConstants(constants) {
542
542
  if (!constants) return void 0;
543
543
  return Object.fromEntries(Object.entries(constants).map(([key, value]) => [key, cloneSdfFunctionConstant(value)]));
544
544
  }
545
+ function cloneSdfSurfacePatternNode(pattern) {
546
+ switch (pattern.kind) {
547
+ case "surfacePattern:constant":
548
+ return { kind: "surfacePattern:constant", value: pattern.value };
549
+ case "surfacePattern:sineWave":
550
+ return {
551
+ kind: "surfacePattern:sineWave",
552
+ direction: [...pattern.direction],
553
+ wavelength: pattern.wavelength,
554
+ amplitude: pattern.amplitude,
555
+ phase: pattern.phase,
556
+ bias: pattern.bias
557
+ };
558
+ case "surfacePattern:stripes":
559
+ return {
560
+ kind: "surfacePattern:stripes",
561
+ direction: [...pattern.direction],
562
+ spacing: pattern.spacing,
563
+ width: pattern.width,
564
+ depth: pattern.depth
565
+ };
566
+ case "surfacePattern:overUnderWeave":
567
+ return {
568
+ kind: "surfacePattern:overUnderWeave",
569
+ spacing: [...pattern.spacing],
570
+ threadWidth: [...pattern.threadWidth],
571
+ depth: pattern.depth,
572
+ underScale: pattern.underScale
573
+ };
574
+ case "surfacePattern:abs":
575
+ case "surfacePattern:negate":
576
+ return { kind: pattern.kind, child: cloneSdfSurfacePatternNode(pattern.child) };
577
+ case "surfacePattern:add":
578
+ case "surfacePattern:multiply":
579
+ case "surfacePattern:min":
580
+ case "surfacePattern:max":
581
+ return { kind: pattern.kind, children: pattern.children.map(cloneSdfSurfacePatternNode) };
582
+ case "surfacePattern:clamp":
583
+ return { kind: "surfacePattern:clamp", child: cloneSdfSurfacePatternNode(pattern.child), min: pattern.min, max: pattern.max };
584
+ }
585
+ }
545
586
  function cloneSdfNode(node) {
546
587
  switch (node.kind) {
547
588
  // Primitives — plain value types
@@ -592,6 +633,8 @@ function cloneSdfNode(node) {
592
633
  return { kind: "sdf:bend", child: cloneSdfNode(node.child), radius: node.radius };
593
634
  case "sdf:repeat":
594
635
  return { kind: "sdf:repeat", child: cloneSdfNode(node.child), spacing: [...node.spacing], count: [...node.count] };
636
+ case "sdf:circularArray":
637
+ return { kind: "sdf:circularArray", child: cloneSdfNode(node.child), count: node.count, offset: node.offset };
595
638
  case "sdf:shell":
596
639
  return { kind: "sdf:shell", child: cloneSdfNode(node.child), thickness: node.thickness };
597
640
  case "sdf:displace":
@@ -605,6 +648,7 @@ function cloneSdfNode(node) {
605
648
  return {
606
649
  kind: "sdf:surfaceDisplace",
607
650
  child: cloneSdfNode(node.child),
651
+ ...node.pattern ? { pattern: cloneSdfSurfacePatternNode(node.pattern) } : {},
608
652
  patternBody: node.patternBody,
609
653
  ...node.constants ? { constants: cloneSdfFunctionConstants(node.constants) } : {},
610
654
  ...node.uvMode ? { uvMode: node.uvMode } : {},
@@ -677,7 +721,7 @@ function cloneSdfNode(node) {
677
721
  }
678
722
  }
679
723
  const SHEET_METAL_EDGES = ["top", "right", "bottom", "left"];
680
- const EPS$c = 1e-9;
724
+ const EPS$d = 1e-9;
681
725
  function isFinitePositive$3(value) {
682
726
  return Number.isFinite(value) && value > 0;
683
727
  }
@@ -718,7 +762,7 @@ function edgeDisplayName(edge) {
718
762
  return `sheetMetal().flange("${edge}", ...)`;
719
763
  }
720
764
  function normalizeAngle(angleDeg) {
721
- return Math.abs(angleDeg) <= EPS$c ? 0 : angleDeg;
765
+ return Math.abs(angleDeg) <= EPS$d ? 0 : angleDeg;
722
766
  }
723
767
  function validateSheetMetalModel(model) {
724
768
  if (!isFinitePositive$3(model.panel.width) || !isFinitePositive$3(model.panel.height)) {
@@ -730,7 +774,7 @@ function validateSheetMetalModel(model) {
730
774
  if (!isFiniteNonNegative(model.bendRadius)) {
731
775
  return "sheetMetal() requires a finite non-negative bendRadius.";
732
776
  }
733
- if (model.bendRadius <= EPS$c) {
777
+ if (model.bendRadius <= EPS$d) {
734
778
  return "sheetMetal() v1 requires a positive bendRadius so the bend region stays explicit instead of collapsing into a sharp fold.";
735
779
  }
736
780
  if (model.bendAllowance.kind !== "k-factor") {
@@ -792,7 +836,7 @@ function deriveSheetMetalModel(model) {
792
836
  const trimEnd = flanges.has(adjacent.end) ? model.cornerRelief.size : 0;
793
837
  const fullLength = edge === "top" || edge === "bottom" ? model.panel.width : model.panel.height;
794
838
  const span = fullLength - trimStart - trimEnd;
795
- if (!(span > EPS$c)) {
839
+ if (!(span > EPS$d)) {
796
840
  throw new Error(
797
841
  `${edgeDisplayName(edge)} loses all usable span after applying the defended rectangular corner relief size ${model.cornerRelief.size}.`
798
842
  );
@@ -850,7 +894,7 @@ function transformPlacement(origin, u2, v, normal) {
850
894
  };
851
895
  }
852
896
  function translatePlan(plan, x2, y2, z2) {
853
- if (Math.abs(x2) <= EPS$c && Math.abs(y2) <= EPS$c && Math.abs(z2) <= EPS$c) return cloneShapeCompilePlan(plan);
897
+ if (Math.abs(x2) <= EPS$d && Math.abs(y2) <= EPS$d && Math.abs(z2) <= EPS$d) return cloneShapeCompilePlan(plan);
854
898
  return appendShapeCompileTransform(cloneShapeCompilePlan(plan), {
855
899
  kind: "translate",
856
900
  x: x2,
@@ -1294,7 +1338,7 @@ function cloneShapeWorkplanePlacement(placement) {
1294
1338
  placement: cloneSketchPlacementModel(placement.placement)
1295
1339
  };
1296
1340
  }
1297
- const EPS$b = 1e-10;
1341
+ const EPS$c = 1e-10;
1298
1342
  function subVec3(a2, b) {
1299
1343
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
1300
1344
  }
@@ -1320,7 +1364,7 @@ function projectRadial(v, axis) {
1320
1364
  function signedAngleAroundAxis(from, to, axis) {
1321
1365
  const fromLen = lengthVec3$1(from);
1322
1366
  const toLen = lengthVec3$1(to);
1323
- if (fromLen < EPS$b || toLen < EPS$b) return 0;
1367
+ if (fromLen < EPS$c || toLen < EPS$c) return 0;
1324
1368
  const fn = scaleVec3(from, 1 / fromLen);
1325
1369
  const tn = scaleVec3(to, 1 / toLen);
1326
1370
  const sin2 = dotVec3$4(axis, crossVec3$2(fn, tn));
@@ -1341,19 +1385,19 @@ function solveRotateAroundAngle(axis, pivot, movingPoint, targetPoint, options =
1341
1385
  const targetDecomp = projectRadial(target, unitAxis);
1342
1386
  const movingRadialLen = lengthVec3$1(movingDecomp.radial);
1343
1387
  const targetRadialLen = lengthVec3$1(targetDecomp.radial);
1344
- if (movingRadialLen < EPS$b) {
1345
- if (mode === "line" && targetRadialLen >= EPS$b) {
1388
+ if (movingRadialLen < EPS$c) {
1389
+ if (mode === "line" && targetRadialLen >= EPS$c) {
1346
1390
  throw new Error("rotateAroundTo(...): moving point lies on the rotation axis, so line alignment is impossible");
1347
1391
  }
1348
1392
  return 0;
1349
1393
  }
1350
1394
  if (mode === "plane") {
1351
- if (targetRadialLen < EPS$b) {
1395
+ if (targetRadialLen < EPS$c) {
1352
1396
  throw new Error("rotateAroundTo(...): target point lies on the rotation axis, so the target plane is undefined");
1353
1397
  }
1354
1398
  return signedAngleAroundAxis(movingDecomp.radial, targetDecomp.radial, unitAxis);
1355
1399
  }
1356
- if (targetRadialLen < EPS$b) {
1400
+ if (targetRadialLen < EPS$c) {
1357
1401
  throw new Error("rotateAroundTo(...): target line lies on the rotation axis, but the moving point does not");
1358
1402
  }
1359
1403
  const axialTol = 1e-8 * Math.max(1, Math.abs(movingDecomp.axial), Math.abs(targetDecomp.axial));
@@ -1393,7 +1437,7 @@ function multiplyMat4(a2, b) {
1393
1437
  }
1394
1438
  function normalizeVec3$5(v) {
1395
1439
  const len2 = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
1396
- if (len2 < EPS$b) throw new Error("Axis must be non-zero");
1440
+ if (len2 < EPS$c) throw new Error("Axis must be non-zero");
1397
1441
  return [v[0] / len2, v[1] / len2, v[2] / len2];
1398
1442
  }
1399
1443
  function transformPoint$1(m2, p2, w2) {
@@ -1423,7 +1467,7 @@ function invertMat4(m2) {
1423
1467
  const b10 = a21 * a33 - a23 * a31;
1424
1468
  const b11 = a22 * a33 - a23 * a32;
1425
1469
  const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
1426
- if (Math.abs(det) < EPS$b) throw new Error("Transform matrix is not invertible");
1470
+ if (Math.abs(det) < EPS$c) throw new Error("Transform matrix is not invertible");
1427
1471
  const invDet = 1 / det;
1428
1472
  out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet;
1429
1473
  out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * invDet;
@@ -3180,14 +3224,14 @@ function sweepPathToPolylineAdaptive(path2, baseSamples = 48) {
3180
3224
  pts.push(evalPathAt(path2, 1));
3181
3225
  return pts;
3182
3226
  }
3183
- const EPS$a = 1e-8;
3227
+ const EPS$b = 1e-8;
3184
3228
  const SUPPORTED_VERTICAL_EDGE_NAMES = ["vert-bl", "vert-br", "vert-tr", "vert-tl"];
3185
3229
  function midpoint$4(start, end) {
3186
3230
  return [(start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, (start[2] + end[2]) * 0.5];
3187
3231
  }
3188
3232
  function normalize$7(v) {
3189
3233
  const len2 = Math.hypot(v[0], v[1], v[2]);
3190
- if (len2 <= EPS$a) throw new Error("Edge feature selection requires a non-zero direction vector");
3234
+ if (len2 <= EPS$b) throw new Error("Edge feature selection requires a non-zero direction vector");
3191
3235
  return [v[0] / len2, v[1] / len2, v[2] / len2];
3192
3236
  }
3193
3237
  function subtract(a2, b) {
@@ -3269,7 +3313,7 @@ function rigidTransformForEdgeStep(step) {
3269
3313
  case "mirror": {
3270
3314
  const [nx0, ny0, nz0] = [step.normalX, step.normalY, step.normalZ];
3271
3315
  const len2 = Math.hypot(nx0, ny0, nz0);
3272
- if (len2 <= EPS$a) return Transform.identity();
3316
+ if (len2 <= EPS$b) return Transform.identity();
3273
3317
  const nx = nx0 / len2;
3274
3318
  const ny = ny0 / len2;
3275
3319
  const nz = nz0 / len2;
@@ -3580,7 +3624,7 @@ function isRectangleProfile(points) {
3580
3624
  return [next[0] - point2[0], next[1] - point2[1]];
3581
3625
  });
3582
3626
  const lengths2 = vectors.map(([x2, y2]) => Math.hypot(x2, y2));
3583
- if (lengths2.some((length4) => length4 <= EPS$a)) return false;
3627
+ if (lengths2.some((length4) => length4 <= EPS$b)) return false;
3584
3628
  const dot01 = vectors[0][0] * vectors[1][0] + vectors[0][1] * vectors[1][1];
3585
3629
  const dot12 = vectors[1][0] * vectors[2][0] + vectors[1][1] * vectors[2][1];
3586
3630
  const dot23 = vectors[2][0] * vectors[3][0] + vectors[2][1] * vectors[3][1];
@@ -4646,7 +4690,7 @@ for (var i = 0; i < 32; ++i)
4646
4690
  fdt[i] = 5;
4647
4691
  var flm = /* @__PURE__ */ hMap(flt, 9, 0), flrm = /* @__PURE__ */ hMap(flt, 9, 1);
4648
4692
  var fdm = /* @__PURE__ */ hMap(fdt, 5, 0), fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
4649
- var max$1 = function(a2) {
4693
+ var max$2 = function(a2) {
4650
4694
  var m2 = a2[0];
4651
4695
  for (var i = 1; i < a2.length; ++i) {
4652
4696
  if (a2[i] > m2)
@@ -4746,7 +4790,7 @@ var inflt = function(dat, st, buf, dict) {
4746
4790
  clt[clim[i]] = bits(dat, pos + i * 3, 7);
4747
4791
  }
4748
4792
  pos += hcLen * 3;
4749
- var clb = max$1(clt), clbmsk = (1 << clb) - 1;
4793
+ var clb = max$2(clt), clbmsk = (1 << clb) - 1;
4750
4794
  var clm = hMap(clt, clb, 1);
4751
4795
  for (var i = 0; i < tl; ) {
4752
4796
  var r = clm[bits(dat, pos, clbmsk)];
@@ -4767,8 +4811,8 @@ var inflt = function(dat, st, buf, dict) {
4767
4811
  }
4768
4812
  }
4769
4813
  var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
4770
- lbt = max$1(lt);
4771
- dbt = max$1(dt);
4814
+ lbt = max$2(lt);
4815
+ dbt = max$2(dt);
4772
4816
  lm = hMap(lt, lbt, 1);
4773
4817
  dm = hMap(dt, dbt, 1);
4774
4818
  } else
@@ -5621,8 +5665,8 @@ function parse3mf(data) {
5621
5665
  while ((tMatch = trianglePattern.exec(meshXml)) !== null) {
5622
5666
  const v1 = parseInt(tMatch[1], 10) + vertexOffset;
5623
5667
  const v2 = parseInt(tMatch[2], 10) + vertexOffset;
5624
- const v3 = parseInt(tMatch[3], 10) + vertexOffset;
5625
- allTriIndices.push(v1, v2, v3);
5668
+ const v32 = parseInt(tMatch[3], 10) + vertexOffset;
5669
+ allTriIndices.push(v1, v2, v32);
5626
5670
  }
5627
5671
  for (let i = 0; i < meshVerts.length; i++) {
5628
5672
  allPositions.push(meshVerts[i]);
@@ -5647,13 +5691,13 @@ function parseMeshFile(data, format) {
5647
5691
  return parse3mf(data);
5648
5692
  }
5649
5693
  }
5650
- const EPS$9 = 1e-8;
5694
+ const EPS$a = 1e-8;
5651
5695
  function length$3(v) {
5652
5696
  return Math.hypot(v[0], v[1], v[2]);
5653
5697
  }
5654
5698
  function normalize$6(v) {
5655
5699
  const len2 = length$3(v);
5656
- if (len2 < EPS$9) throw new Error("Plane normal must be non-zero");
5700
+ if (len2 < EPS$a) throw new Error("Plane normal must be non-zero");
5657
5701
  return [v[0] / len2, v[1] / len2, v[2] / len2];
5658
5702
  }
5659
5703
  function resolvePlaneOriginNormal(plane) {
@@ -5675,12 +5719,12 @@ function resolvePlaneOriginNormal(plane) {
5675
5719
  function rotationToPlaneSpace(normal) {
5676
5720
  const n = normalize$6(normal);
5677
5721
  const dot2 = n[2];
5678
- if (dot2 > 1 - EPS$9) {
5722
+ if (dot2 > 1 - EPS$a) {
5679
5723
  return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
5680
5724
  }
5681
5725
  let axis;
5682
5726
  let angle;
5683
- if (dot2 < -1 + EPS$9) {
5727
+ if (dot2 < -1 + EPS$a) {
5684
5728
  axis = [1, 0, 0];
5685
5729
  angle = Math.PI;
5686
5730
  } else {
@@ -6005,6 +6049,287 @@ function lowerShellShapeCompilePlanToConcretePlan(plan) {
6005
6049
  }
6006
6050
  return lowerBaseShellPlanToConcretePlan(plan.base, plan.thickness, normalizeShellOpenFaces(plan.openFaces));
6007
6051
  }
6052
+ const DEFAULT_MAX_GRID_POINTS = 8e6;
6053
+ const DEFAULT_MIN_EDGE_LENGTH = 0.15;
6054
+ function resolveSdfMeshingSettings(tree, bounds, options = {}) {
6055
+ const quality = options.quality ?? "preview";
6056
+ const minEdgeLength = positiveOrDefault(options.minEdgeLength, DEFAULT_MIN_EDGE_LENGTH);
6057
+ const maxGridPoints = positiveOrDefault(options.maxGridPoints, DEFAULT_MAX_GRID_POINTS);
6058
+ const tolerance = options.tolerance !== void 0 ? requirePositiveFinite$3(options.tolerance, "SDF tolerance") : void 0;
6059
+ const minFeatureSize = options.minFeatureSize !== void 0 ? requirePositiveFinite$3(options.minFeatureSize, "SDF minFeatureSize") : void 0;
6060
+ const maxTriangles = options.maxTriangles !== void 0 ? Math.floor(requirePositiveFinite$3(options.maxTriangles, "SDF maxTriangles")) : void 0;
6061
+ const analysis = analyzeSdfTree(tree);
6062
+ const warnings = [];
6063
+ let edgeLength2;
6064
+ if (options.edgeLength !== void 0) {
6065
+ edgeLength2 = requirePositiveFinite$3(options.edgeLength, "SDF edgeLength");
6066
+ if (edgeLength2 < minEdgeLength) {
6067
+ warnings.push(`edgeLength ${formatMm(edgeLength2)} was clamped to minimum ${formatMm(minEdgeLength)}.`);
6068
+ edgeLength2 = minEdgeLength;
6069
+ }
6070
+ } else {
6071
+ edgeLength2 = resolveDefaultEdgeLength(bounds, quality, minEdgeLength, analysis, options);
6072
+ }
6073
+ if (analysis.minWallThickness < Infinity && analysis.minWallThickness < edgeLength2 * 2) {
6074
+ analysis.riskFlags.add("thin-shell");
6075
+ warnings.push(
6076
+ `shell/wall thickness ${formatMm(analysis.minWallThickness)} is below 2 x edgeLength ${formatMm(edgeLength2)}; thin features may be under-sampled.`
6077
+ );
6078
+ }
6079
+ if (!options.bounds && analysis.hasInfiniteRepeat) {
6080
+ warnings.push("infinite repeat bounds are heuristic; pass .toShape({ bounds }) for predictable clipping.");
6081
+ }
6082
+ if (!options.bounds && analysis.riskFlags.has("noise")) {
6083
+ warnings.push("noise field bounds are heuristic; pass .toShape({ bounds }) for predictable clipping.");
6084
+ }
6085
+ if (!options.bounds && (analysis.riskFlags.has("tpms") || analysis.riskFlags.has("voronoi"))) {
6086
+ warnings.push("TPMS/Voronoi bounds are heuristic unless clipped or passed explicitly.");
6087
+ }
6088
+ if (analysis.hasLegacyTpmsThreshold) {
6089
+ warnings.push("TPMS thickness is using legacy field-threshold units; use wallThickness for approximate millimeters.");
6090
+ }
6091
+ return {
6092
+ quality,
6093
+ edgeLength: edgeLength2,
6094
+ tolerance,
6095
+ minFeatureSize,
6096
+ minEdgeLength,
6097
+ simplify: resolveSimplificationMode(options.simplify, quality, analysis.riskFlags),
6098
+ maxTriangles,
6099
+ maxGridPoints,
6100
+ diagnostics: options.diagnostics === true,
6101
+ treeRiskFlags: [...analysis.riskFlags].sort(),
6102
+ warnings
6103
+ };
6104
+ }
6105
+ function withScaledSdfEdgeLength(settings, edgeLength2) {
6106
+ return { ...settings, edgeLength: Math.max(settings.minEdgeLength, edgeLength2) };
6107
+ }
6108
+ function createSdfMeshingDiagnostics(settings, bounds, paddedBounds) {
6109
+ const grid = estimateSdfGridDimensions(paddedBounds, settings.edgeLength);
6110
+ const estimatedSamples = grid[0] * grid[1] * grid[2];
6111
+ return {
6112
+ bounds: cloneBounds$2(bounds),
6113
+ paddedBounds: cloneBounds$2(paddedBounds),
6114
+ edgeLength: settings.edgeLength,
6115
+ grid,
6116
+ estimatedSamples,
6117
+ estimatedMemoryBytes: estimatedSamples * 8,
6118
+ treeRiskFlags: [...settings.treeRiskFlags],
6119
+ simplification: settings.simplify,
6120
+ capMode: "box",
6121
+ capInset: settings.edgeLength,
6122
+ warnings: [...settings.warnings]
6123
+ };
6124
+ }
6125
+ function assertSdfMeshingBudget(diagnostics, maxGridPoints) {
6126
+ if (diagnostics.estimatedSamples <= maxGridPoints) return;
6127
+ const suggestedEdge = suggestEdgeLengthForSampleBudget(diagnostics.paddedBounds, maxGridPoints);
6128
+ throw new Error(
6129
+ `SDF meshing would sample ${formatCount(diagnostics.estimatedSamples)} grid points (~${formatBytes(
6130
+ diagnostics.estimatedMemoryBytes
6131
+ )}). Reduce bounds or use edgeLength >= ${formatMm(suggestedEdge)}.`
6132
+ );
6133
+ }
6134
+ function estimateSdfGridDimensions(bounds, edgeLength2) {
6135
+ const dx = bounds.max[0] - bounds.min[0];
6136
+ const dy = bounds.max[1] - bounds.min[1];
6137
+ const dz = bounds.max[2] - bounds.min[2];
6138
+ return [
6139
+ Math.max(2, Math.ceil(dx / edgeLength2) + 1),
6140
+ Math.max(2, Math.ceil(dy / edgeLength2) + 1),
6141
+ Math.max(2, Math.ceil(dz / edgeLength2) + 1)
6142
+ ];
6143
+ }
6144
+ function logSdfMeshingDiagnostics(prefix, diagnostics) {
6145
+ const warnings = diagnostics.warnings.length > 0 ? `, warnings=${diagnostics.warnings.join(" | ")}` : "";
6146
+ const evaluator = diagnostics.evaluator ? `, evaluator=${diagnostics.evaluator}${diagnostics.evaluatorUnsupportedReason ? ` (${diagnostics.evaluatorUnsupportedReason})` : ""}` : "";
6147
+ console.info(
6148
+ `${prefix}: bounds=${formatBounds(diagnostics.bounds)}, paddedBounds=${formatBounds(diagnostics.paddedBounds)}, edgeLength=${formatMm(diagnostics.edgeLength)}, grid=${diagnostics.grid.join("x")}, estimatedSamples=${formatCount(diagnostics.estimatedSamples)}, treeRisk=${diagnostics.treeRiskFlags.join("+") || "none"}, simplify=${diagnostics.simplification}${evaluator}, capMode=${diagnostics.capMode}, capInset=${formatMm(diagnostics.capInset)}${warnings}`
6149
+ );
6150
+ }
6151
+ function resolveDefaultEdgeLength(bounds, quality, minEdgeLength, analysis, options) {
6152
+ const dx = bounds.max[0] - bounds.min[0];
6153
+ const dy = bounds.max[1] - bounds.min[1];
6154
+ const dz = bounds.max[2] - bounds.min[2];
6155
+ const maxDim = Math.max(dx, dy, dz, minEdgeLength);
6156
+ const divisor = quality === "draft" ? 60 : quality === "export" ? 160 : 100;
6157
+ const candidates = [maxDim / divisor];
6158
+ if (options.tolerance !== void 0) candidates.push(requirePositiveFinite$3(options.tolerance, "SDF tolerance") * 2);
6159
+ if (options.minFeatureSize !== void 0) candidates.push(requirePositiveFinite$3(options.minFeatureSize, "SDF minFeatureSize") / 2.5);
6160
+ if (analysis.minMetricTpmsThickness < Infinity) candidates.push(analysis.minMetricTpmsThickness / 2);
6161
+ if (analysis.minTpmsCellSize < Infinity) candidates.push(analysis.minTpmsCellSize / 10);
6162
+ if (analysis.minRepeatSpacing < Infinity) candidates.push(analysis.minRepeatSpacing / 8);
6163
+ if (analysis.minWallThickness < Infinity) candidates.push(analysis.minWallThickness / 2.5);
6164
+ return Math.max(minEdgeLength, Math.min(...candidates.filter((v) => Number.isFinite(v) && v > 0)));
6165
+ }
6166
+ function resolveSimplificationMode(simplify, quality, riskFlags) {
6167
+ if (simplify === false) return "off";
6168
+ if (simplify === true || simplify === "safe") return "safe";
6169
+ if (quality === "export" && riskFlags.size > 0) return "off";
6170
+ return "safe";
6171
+ }
6172
+ function analyzeSdfTree(tree) {
6173
+ const analysis = {
6174
+ riskFlags: /* @__PURE__ */ new Set(),
6175
+ minTpmsCellSize: Infinity,
6176
+ minMetricTpmsThickness: Infinity,
6177
+ minRepeatSpacing: Infinity,
6178
+ minWallThickness: Infinity,
6179
+ hasInfiniteRepeat: false,
6180
+ hasLegacyTpmsThreshold: false
6181
+ };
6182
+ visitSdfNode(tree, analysis);
6183
+ return analysis;
6184
+ }
6185
+ function minPositive(...values) {
6186
+ let result = Infinity;
6187
+ for (const value of values) {
6188
+ if (value !== null && value !== void 0 && Number.isFinite(value) && value > 0) {
6189
+ result = Math.min(result, value);
6190
+ }
6191
+ }
6192
+ return result === Infinity ? null : result;
6193
+ }
6194
+ function estimateSurfacePatternSpacing(pattern) {
6195
+ switch (pattern.kind) {
6196
+ case "surfacePattern:constant":
6197
+ return null;
6198
+ case "surfacePattern:sineWave":
6199
+ return pattern.wavelength;
6200
+ case "surfacePattern:stripes":
6201
+ return Math.min(pattern.spacing, pattern.width);
6202
+ case "surfacePattern:overUnderWeave":
6203
+ return Math.min(...pattern.spacing, ...pattern.threadWidth);
6204
+ case "surfacePattern:abs":
6205
+ case "surfacePattern:negate":
6206
+ return estimateSurfacePatternSpacing(pattern.child);
6207
+ case "surfacePattern:add":
6208
+ case "surfacePattern:multiply":
6209
+ case "surfacePattern:min":
6210
+ case "surfacePattern:max":
6211
+ return minPositive(...pattern.children.map(estimateSurfacePatternSpacing));
6212
+ case "surfacePattern:clamp":
6213
+ return estimateSurfacePatternSpacing(pattern.child);
6214
+ }
6215
+ }
6216
+ function visitSdfNode(node, analysis) {
6217
+ switch (node.kind) {
6218
+ case "sdf:union":
6219
+ case "sdf:difference":
6220
+ case "sdf:intersection":
6221
+ case "sdf:smoothUnion":
6222
+ case "sdf:smoothDifference":
6223
+ case "sdf:smoothIntersection":
6224
+ for (const child of node.children) visitSdfNode(child, analysis);
6225
+ break;
6226
+ case "sdf:morph":
6227
+ case "sdf:spatialBlend":
6228
+ visitSdfNode(node.a, analysis);
6229
+ visitSdfNode(node.b, analysis);
6230
+ break;
6231
+ case "sdf:translate":
6232
+ case "sdf:rotate":
6233
+ case "sdf:scale":
6234
+ case "sdf:twist":
6235
+ case "sdf:bend":
6236
+ case "sdf:onion":
6237
+ visitSdfNode(node.child, analysis);
6238
+ break;
6239
+ case "sdf:repeat":
6240
+ analysis.riskFlags.add("repeat");
6241
+ for (let i = 0; i < 3; i++) {
6242
+ const spacing = node.spacing[i];
6243
+ if (spacing > 0) {
6244
+ analysis.minRepeatSpacing = Math.min(analysis.minRepeatSpacing, spacing);
6245
+ if (node.count[i] <= 0) analysis.hasInfiniteRepeat = true;
6246
+ }
6247
+ }
6248
+ visitSdfNode(node.child, analysis);
6249
+ break;
6250
+ case "sdf:circularArray": {
6251
+ analysis.riskFlags.add("repeat");
6252
+ if (node.offset > 0) {
6253
+ analysis.minRepeatSpacing = Math.min(analysis.minRepeatSpacing, 2 * Math.PI * node.offset / node.count);
6254
+ }
6255
+ visitSdfNode(node.child, analysis);
6256
+ break;
6257
+ }
6258
+ case "sdf:shell":
6259
+ analysis.minWallThickness = Math.min(analysis.minWallThickness, node.thickness);
6260
+ visitSdfNode(node.child, analysis);
6261
+ break;
6262
+ case "sdf:displace":
6263
+ case "sdf:surfaceDisplace":
6264
+ analysis.riskFlags.add("displacement");
6265
+ if (node.kind === "sdf:surfaceDisplace" && node.pattern) {
6266
+ const spacing = estimateSurfacePatternSpacing(node.pattern);
6267
+ if (spacing !== null) analysis.minRepeatSpacing = Math.min(analysis.minRepeatSpacing, spacing);
6268
+ }
6269
+ visitSdfNode(node.child, analysis);
6270
+ break;
6271
+ case "sdf:gyroid":
6272
+ case "sdf:schwarzP":
6273
+ case "sdf:diamond":
6274
+ case "sdf:lidinoid":
6275
+ analysis.riskFlags.add("tpms");
6276
+ analysis.minTpmsCellSize = Math.min(analysis.minTpmsCellSize, node.cellSize);
6277
+ if (node.thicknessMode === "metric-approx") {
6278
+ analysis.minMetricTpmsThickness = Math.min(analysis.minMetricTpmsThickness, node.thickness);
6279
+ analysis.minWallThickness = Math.min(analysis.minWallThickness, node.thickness);
6280
+ } else {
6281
+ analysis.hasLegacyTpmsThreshold = true;
6282
+ }
6283
+ break;
6284
+ case "sdf:noise":
6285
+ analysis.riskFlags.add("noise");
6286
+ break;
6287
+ case "sdf:voronoi":
6288
+ analysis.riskFlags.add("voronoi");
6289
+ analysis.minWallThickness = Math.min(analysis.minWallThickness, node.wallThickness);
6290
+ if (node.surfaceChild) visitSdfNode(node.surfaceChild, analysis);
6291
+ break;
6292
+ case "sdf:custom":
6293
+ analysis.riskFlags.add("custom");
6294
+ break;
6295
+ }
6296
+ }
6297
+ function positiveOrDefault(value, fallback) {
6298
+ if (value === void 0) return fallback;
6299
+ return requirePositiveFinite$3(value, "SDF meshing option");
6300
+ }
6301
+ function requirePositiveFinite$3(value, name) {
6302
+ if (!Number.isFinite(value) || value <= 0) {
6303
+ throw new Error(`${name} must be a positive finite number.`);
6304
+ }
6305
+ return value;
6306
+ }
6307
+ function cloneBounds$2(bounds) {
6308
+ return { min: [...bounds.min], max: [...bounds.max] };
6309
+ }
6310
+ function suggestEdgeLengthForSampleBudget(bounds, maxGridPoints) {
6311
+ const dx = bounds.max[0] - bounds.min[0];
6312
+ const dy = bounds.max[1] - bounds.min[1];
6313
+ const dz = bounds.max[2] - bounds.min[2];
6314
+ const volume = Math.max(dx * dy * dz, 1);
6315
+ return Math.cbrt(volume / Math.max(maxGridPoints, 8));
6316
+ }
6317
+ function formatBounds(bounds) {
6318
+ return `[${bounds.min.map(formatNumber).join(",")}]-[${bounds.max.map(formatNumber).join(",")}]`;
6319
+ }
6320
+ function formatMm(value) {
6321
+ return `${formatNumber(value)}mm`;
6322
+ }
6323
+ function formatNumber(value) {
6324
+ return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
6325
+ }
6326
+ function formatCount(value) {
6327
+ return Math.round(value).toLocaleString("en-US");
6328
+ }
6329
+ function formatBytes(bytes) {
6330
+ if (bytes < 1024 * 1024) return `${Math.ceil(bytes / 1024)} KB`;
6331
+ return `${Math.ceil(bytes / (1024 * 1024))} MB`;
6332
+ }
6008
6333
  const grad3 = new Float64Array([
6009
6334
  1,
6010
6335
  1,
@@ -6465,8 +6790,8 @@ function triplanarWeights(nx, ny, nz, sharpness) {
6465
6790
  const inv = 1 / sum2;
6466
6791
  return { wx: wx * inv, wy: wy * inv, wz: wz * inv };
6467
6792
  }
6468
- const { atan2, acos, cos: cos$2, sin: sin$2, sqrt: sqrt$2, PI: PI$2 } = Math;
6469
- const DEG$2 = PI$2 / 180;
6793
+ const { atan2, acos, cos: cos$3, sin: sin$3, sqrt: sqrt$3, PI: PI$3 } = Math;
6794
+ const DEG$3 = PI$3 / 180;
6470
6795
  const IDENTITY = (p2) => p2;
6471
6796
  function analyzeUV(node, override) {
6472
6797
  if (override) {
@@ -6493,10 +6818,10 @@ function analyzeNodeUV(node, toLocal) {
6493
6818
  return analyzeNodeUV(node.child, next);
6494
6819
  }
6495
6820
  case "sdf:rotate": {
6496
- const [rx, ry, rz] = node.degrees.map((d2) => d2 * DEG$2);
6497
- const cx = cos$2(rx), sx = sin$2(rx);
6498
- const cy = cos$2(ry), sy = sin$2(ry);
6499
- const cz = cos$2(rz), sz = sin$2(rz);
6821
+ const [rx, ry, rz] = node.degrees.map((d2) => d2 * DEG$3);
6822
+ const cx = cos$3(rx), sx = sin$3(rx);
6823
+ const cy = cos$3(ry), sy = sin$3(ry);
6824
+ const cz = cos$3(rz), sz = sin$3(rz);
6500
6825
  const prev = toLocal;
6501
6826
  const next = (p2) => {
6502
6827
  const pp = prev(p2);
@@ -6553,7 +6878,7 @@ function compileUVFunction(analysis) {
6553
6878
  return (p2) => {
6554
6879
  const lp = toLocal(p2);
6555
6880
  const u2 = atan2(lp[1], lp[0]) * R;
6556
- const len2 = sqrt$2(lp[0] * lp[0] + lp[1] * lp[1] + lp[2] * lp[2]);
6881
+ const len2 = sqrt$3(lp[0] * lp[0] + lp[1] * lp[1] + lp[2] * lp[2]);
6557
6882
  const v = acos(clampUnit(lp[2] / (len2 || 1))) * R;
6558
6883
  return [u2, v];
6559
6884
  };
@@ -6573,23 +6898,23 @@ function compileUVFunction(analysis) {
6573
6898
  return (p2) => {
6574
6899
  const lp = toLocal(p2);
6575
6900
  const u2 = atan2(lp[1], lp[0]) * R;
6576
- const xyDist = sqrt$2(lp[0] * lp[0] + lp[1] * lp[1]) - R;
6901
+ const xyDist = sqrt$3(lp[0] * lp[0] + lp[1] * lp[1]) - R;
6577
6902
  const v = atan2(lp[2], xyDist) * r;
6578
6903
  return [u2, v];
6579
6904
  };
6580
6905
  }
6581
6906
  }
6582
6907
  }
6583
- const { abs: abs$1, cos: cos$1, sin: sin$1, sqrt: sqrt$1, PI: PI$1 } = Math;
6584
- const TAU = 2 * PI$1;
6585
- const GRAD_EPS = 1e-9;
6908
+ const { abs: abs$1, cos: cos$2, sin: sin$2, sqrt: sqrt$2, PI: PI$2 } = Math;
6909
+ const TAU$1 = 2 * PI$2;
6910
+ const GRAD_EPS$1 = 1e-9;
6586
6911
  function gyroidValueAndGradient(x2, y2, z2, cellSize) {
6587
- const s = TAU / cellSize;
6912
+ const s = TAU$1 / cellSize;
6588
6913
  const xs = x2 * s;
6589
6914
  const ys = y2 * s;
6590
6915
  const zs = z2 * s;
6591
- const sx = sin$1(xs), sy = sin$1(ys), sz = sin$1(zs);
6592
- const cx = cos$1(xs), cy = cos$1(ys), cz = cos$1(zs);
6916
+ const sx = sin$2(xs), sy = sin$2(ys), sz = sin$2(zs);
6917
+ const cx = cos$2(xs), cy = cos$2(ys), cz = cos$2(zs);
6593
6918
  return {
6594
6919
  value: sx * cy + sy * cz + sz * cx,
6595
6920
  gx: s * (cx * cy - sz * sx),
@@ -6598,24 +6923,24 @@ function gyroidValueAndGradient(x2, y2, z2, cellSize) {
6598
6923
  };
6599
6924
  }
6600
6925
  function schwarzPValueAndGradient(x2, y2, z2, cellSize) {
6601
- const s = TAU / cellSize;
6926
+ const s = TAU$1 / cellSize;
6602
6927
  const xs = x2 * s;
6603
6928
  const ys = y2 * s;
6604
6929
  const zs = z2 * s;
6605
6930
  return {
6606
- value: cos$1(xs) + cos$1(ys) + cos$1(zs),
6607
- gx: -s * sin$1(xs),
6608
- gy: -s * sin$1(ys),
6609
- gz: -s * sin$1(zs)
6931
+ value: cos$2(xs) + cos$2(ys) + cos$2(zs),
6932
+ gx: -s * sin$2(xs),
6933
+ gy: -s * sin$2(ys),
6934
+ gz: -s * sin$2(zs)
6610
6935
  };
6611
6936
  }
6612
6937
  function diamondValueAndGradient(x2, y2, z2, cellSize) {
6613
- const s = TAU / cellSize;
6938
+ const s = TAU$1 / cellSize;
6614
6939
  const xs = x2 * s;
6615
6940
  const ys = y2 * s;
6616
6941
  const zs = z2 * s;
6617
- const sx = sin$1(xs), sy = sin$1(ys), sz = sin$1(zs);
6618
- const cx = cos$1(xs), cy = cos$1(ys), cz = cos$1(zs);
6942
+ const sx = sin$2(xs), sy = sin$2(ys), sz = sin$2(zs);
6943
+ const cx = cos$2(xs), cy = cos$2(ys), cz = cos$2(zs);
6619
6944
  return {
6620
6945
  value: sx * sy * sz + sx * cy * cz + cx * sy * cz + cx * cy * sz,
6621
6946
  gx: s * (cx * sy * sz + cx * cy * cz - sx * sy * cz - sx * cy * sz),
@@ -6624,12 +6949,12 @@ function diamondValueAndGradient(x2, y2, z2, cellSize) {
6624
6949
  };
6625
6950
  }
6626
6951
  function lidinoidValueAndGradient(x2, y2, z2, cellSize) {
6627
- const s = TAU / cellSize;
6952
+ const s = TAU$1 / cellSize;
6628
6953
  const sx2 = x2 * s, sy2 = y2 * s, sz2 = z2 * s;
6629
- const sx = sin$1(sx2), sy = sin$1(sy2), sz = sin$1(sz2);
6630
- const cx = cos$1(sx2), cy = cos$1(sy2), cz = cos$1(sz2);
6631
- const s2x = sin$1(2 * sx2), s2y = sin$1(2 * sy2), s2z = sin$1(2 * sz2);
6632
- const c2x = cos$1(2 * sx2), c2y = cos$1(2 * sy2), c2z = cos$1(2 * sz2);
6954
+ const sx = sin$2(sx2), sy = sin$2(sy2), sz = sin$2(sz2);
6955
+ const cx = cos$2(sx2), cy = cos$2(sy2), cz = cos$2(sz2);
6956
+ const s2x = sin$2(2 * sx2), s2y = sin$2(2 * sy2), s2z = sin$2(2 * sz2);
6957
+ const c2x = cos$2(2 * sx2), c2y = cos$2(2 * sy2), c2z = cos$2(2 * sz2);
6633
6958
  const val = s2x * cy * sz + s2y * cz * sx + s2z * cx * sy - c2x * c2y - c2y * c2z - c2z * c2x + 0.3;
6634
6959
  return {
6635
6960
  value: val,
@@ -6652,8 +6977,8 @@ function lidinoid$1(x2, y2, z2, cellSize, thickness, thicknessMode) {
6652
6977
  }
6653
6978
  function tpmsDistance({ value, gx, gy, gz }, thickness, thicknessMode) {
6654
6979
  if (thicknessMode !== "metric-approx") return abs$1(value) - thickness;
6655
- const grad = sqrt$1(gx * gx + gy * gy + gz * gz);
6656
- return abs$1(value) / Math.max(grad, GRAD_EPS) - thickness * 0.5;
6980
+ const grad = sqrt$2(gx * gx + gy * gy + gz * gz);
6981
+ return abs$1(value) / Math.max(grad, GRAD_EPS$1) - thickness * 0.5;
6657
6982
  }
6658
6983
  function mix(h) {
6659
6984
  h = (h ^ h >>> 16) * 2246822507 | 0;
@@ -6749,76 +7074,76 @@ function seededWorley3Surface(seed) {
6749
7074
  const s = seed | 0;
6750
7075
  return (x2, y2, z2, nx, ny, nz, threshold) => worleySurface(x2, y2, z2, s, nx, ny, nz, threshold);
6751
7076
  }
6752
- const { abs, cos, max, min, sin, sqrt, PI } = Math;
6753
- const DEG$1 = PI / 180;
7077
+ const { abs, cos: cos$1, max: max$1, min, sin: sin$1, sqrt: sqrt$1, PI: PI$1 } = Math;
7078
+ const DEG$2 = PI$1 / 180;
6754
7079
  function clamp$a(v, lo, hi) {
6755
7080
  return v < lo ? lo : v > hi ? hi : v;
6756
7081
  }
6757
- function length2(x2, y2) {
6758
- return sqrt(x2 * x2 + y2 * y2);
7082
+ function length2$1(x2, y2) {
7083
+ return sqrt$1(x2 * x2 + y2 * y2);
6759
7084
  }
6760
- function length3(x2, y2, z2) {
6761
- return sqrt(x2 * x2 + y2 * y2 + z2 * z2);
7085
+ function length3$1(x2, y2, z2) {
7086
+ return sqrt$1(x2 * x2 + y2 * y2 + z2 * z2);
6762
7087
  }
6763
- function sdSphere(px, py, pz, r) {
6764
- return length3(px, py, pz) - r;
7088
+ function sdSphere$1(px, py, pz, r) {
7089
+ return length3$1(px, py, pz) - r;
6765
7090
  }
6766
- function sdBox(px, py, pz, hx, hy, hz) {
7091
+ function sdBox$1(px, py, pz, hx, hy, hz) {
6767
7092
  const dx = abs(px) - hx;
6768
7093
  const dy = abs(py) - hy;
6769
7094
  const dz = abs(pz) - hz;
6770
- return length3(max(dx, 0), max(dy, 0), max(dz, 0)) + min(max(dx, dy, dz), 0);
7095
+ return length3$1(max$1(dx, 0), max$1(dy, 0), max$1(dz, 0)) + min(max$1(dx, dy, dz), 0);
6771
7096
  }
6772
- function sdCylinder(px, py, pz, h, r) {
6773
- const dx = length2(px, py) - r;
7097
+ function sdCylinder$1(px, py, pz, h, r) {
7098
+ const dx = length2$1(px, py) - r;
6774
7099
  const dz = abs(pz) - h * 0.5;
6775
- return length2(max(dx, 0), max(dz, 0)) + min(max(dx, dz), 0);
7100
+ return length2$1(max$1(dx, 0), max$1(dz, 0)) + min(max$1(dx, dz), 0);
6776
7101
  }
6777
- function sdTorus(px, py, pz, R, r) {
6778
- const qx = length2(px, py) - R;
6779
- return length2(qx, pz) - r;
7102
+ function sdTorus$1(px, py, pz, R, r) {
7103
+ const qx = length2$1(px, py) - R;
7104
+ return length2$1(qx, pz) - r;
6780
7105
  }
6781
- function sdCapsule(px, py, pz, h, r) {
7106
+ function sdCapsule$1(px, py, pz, h, r) {
6782
7107
  const halfH = h * 0.5;
6783
7108
  const cz = clamp$a(pz, -halfH, halfH);
6784
- return length3(px, py, pz - cz) - r;
7109
+ return length3$1(px, py, pz - cz) - r;
6785
7110
  }
6786
- function sdCone(px, py, pz, h, r) {
6787
- const q = length2(px, py);
6788
- const cLen = length2(h, r);
7111
+ function sdCone$1(px, py, pz, h, r) {
7112
+ const q = length2$1(px, py);
7113
+ const cLen = length2$1(h, r);
6789
7114
  const nx = h / cLen;
6790
7115
  const nz = -r / cLen;
6791
- const d2 = max(nx * q + nz * (pz - h), -pz, pz - h);
7116
+ const d2 = max$1(nx * q + nz * (pz - h), -pz, pz - h);
6792
7117
  return d2;
6793
7118
  }
6794
- function sdTaperedSegment(px, py, pz, ax, ay, az, bx, by, bz, ra, rb) {
7119
+ function sdTaperedSegment$1(px, py, pz, ax, ay, az, bx, by, bz, ra, rb) {
6795
7120
  const vx = bx - ax;
6796
7121
  const vy = by - ay;
6797
7122
  const vz = bz - az;
6798
7123
  const len2 = vx * vx + vy * vy + vz * vz;
6799
- if (len2 <= 1e-12) return sdSphere(px - ax, py - ay, pz - az, max(ra, rb));
7124
+ if (len2 <= 1e-12) return sdSphere$1(px - ax, py - ay, pz - az, max$1(ra, rb));
6800
7125
  const h = clamp$a(((px - ax) * vx + (py - ay) * vy + (pz - az) * vz) / len2, 0, 1);
6801
- return length3(px - (ax + vx * h), py - (ay + vy * h), pz - (az + vz * h)) - (ra + (rb - ra) * h);
7126
+ return length3$1(px - (ax + vx * h), py - (ay + vy * h), pz - (az + vz * h)) - (ra + (rb - ra) * h);
6802
7127
  }
6803
7128
  function sdPolylineSweep3(node, x2, y2, z2) {
6804
7129
  let d2 = 1e20;
6805
7130
  for (let i = 0; i < node.points.length - 1; i++) {
6806
7131
  const a2 = node.points[i];
6807
7132
  const b = node.points[i + 1];
6808
- const segment = sdTaperedSegment(x2, y2, z2, a2[0], a2[1], a2[2], b[0], b[1], b[2], node.radii[i], node.radii[i + 1]);
6809
- d2 = i === 0 ? segment : smin(d2, segment, node.blend);
7133
+ const segment = sdTaperedSegment$1(x2, y2, z2, a2[0], a2[1], a2[2], b[0], b[1], b[2], node.radii[i], node.radii[i + 1]);
7134
+ d2 = i === 0 ? segment : smin$1(d2, segment, node.blend);
6810
7135
  }
6811
7136
  return d2;
6812
7137
  }
6813
- function smin(a2, b, k2) {
7138
+ function smin$1(a2, b, k2) {
6814
7139
  if (k2 <= 0) return min(a2, b);
6815
- const h = max(k2 - abs(a2 - b), 0) / k2;
7140
+ const h = max$1(k2 - abs(a2 - b), 0) / k2;
6816
7141
  return min(a2, b) - h * h * h * k2 * (1 / 6);
6817
7142
  }
6818
- function smax(a2, b, k2) {
6819
- return -smin(-a2, -b, k2);
7143
+ function smax$1(a2, b, k2) {
7144
+ return -smin$1(-a2, -b, k2);
6820
7145
  }
6821
- function repeatCoord(v, spacing, count) {
7146
+ function repeatCoord$1(v, spacing, count) {
6822
7147
  if (spacing <= 0) return v;
6823
7148
  if (count > 0) {
6824
7149
  const center = (count - 1) * 0.5;
@@ -6827,31 +7152,138 @@ function repeatCoord(v, spacing, count) {
6827
7152
  }
6828
7153
  return v - spacing * Math.round(v / spacing);
6829
7154
  }
7155
+ function positiveMod(v, period) {
7156
+ return (v % period + period) % period;
7157
+ }
7158
+ function evalStripesPattern(u2, v, directionX, directionY, spacing, width, depth) {
7159
+ const coord = u2 * directionX + v * directionY;
7160
+ const d2 = abs(coord - Math.round(coord / spacing) * spacing);
7161
+ const profile = max$1(0, 1 - d2 / (width * 0.5));
7162
+ return -(profile * profile) * depth;
7163
+ }
7164
+ function evalOverUnderWeavePattern(u2, v, spacingX, spacingY, widthX, widthY, depth, underScale) {
7165
+ const su = u2 / spacingX;
7166
+ const sv = v / spacingY;
7167
+ let pU = max$1(0, 1 - abs(su - Math.round(su)) * spacingX / (widthX * 0.5));
7168
+ let pV = max$1(0, 1 - abs(sv - Math.round(sv)) * spacingY / (widthY * 0.5));
7169
+ pU *= pU;
7170
+ pV *= pV;
7171
+ const checker = (Math.round(su) & 65535) + (Math.round(sv) & 65535) & 1;
7172
+ const top = checker ? pV : pU;
7173
+ const bot = checker ? pU : pV;
7174
+ return -max$1(top, bot * underScale) * depth;
7175
+ }
7176
+ function compileTypedSurfacePattern(pattern) {
7177
+ switch (pattern.kind) {
7178
+ case "surfacePattern:constant":
7179
+ return () => pattern.value;
7180
+ case "surfacePattern:sineWave": {
7181
+ const { direction: direction2, wavelength, amplitude, phase, bias } = pattern;
7182
+ const frequency = 2 * PI$1 / wavelength;
7183
+ return (u2, v) => bias + sin$1((u2 * direction2[0] + v * direction2[1]) * frequency + phase) * amplitude;
7184
+ }
7185
+ case "surfacePattern:stripes": {
7186
+ const { direction: direction2, spacing, width, depth } = pattern;
7187
+ return (u2, v) => evalStripesPattern(u2, v, direction2[0], direction2[1], spacing, width, depth);
7188
+ }
7189
+ case "surfacePattern:overUnderWeave": {
7190
+ const { spacing, threadWidth, depth, underScale } = pattern;
7191
+ return (u2, v) => evalOverUnderWeavePattern(u2, v, spacing[0], spacing[1], threadWidth[0], threadWidth[1], depth, underScale);
7192
+ }
7193
+ case "surfacePattern:abs": {
7194
+ const child = compileTypedSurfacePattern(pattern.child);
7195
+ return (u2, v) => abs(child(u2, v));
7196
+ }
7197
+ case "surfacePattern:negate": {
7198
+ const child = compileTypedSurfacePattern(pattern.child);
7199
+ return (u2, v) => -child(u2, v);
7200
+ }
7201
+ case "surfacePattern:add": {
7202
+ const children = pattern.children.map(compileTypedSurfacePattern);
7203
+ return (u2, v) => children.reduce((sum2, child) => sum2 + child(u2, v), 0);
7204
+ }
7205
+ case "surfacePattern:multiply": {
7206
+ const children = pattern.children.map(compileTypedSurfacePattern);
7207
+ return (u2, v) => children.reduce((product, child) => product * child(u2, v), 1);
7208
+ }
7209
+ case "surfacePattern:min": {
7210
+ const children = pattern.children.map(compileTypedSurfacePattern);
7211
+ if (children.length === 0) return () => 0;
7212
+ return (u2, v) => children.reduce((value, child) => min(value, child(u2, v)), Infinity);
7213
+ }
7214
+ case "surfacePattern:max": {
7215
+ const children = pattern.children.map(compileTypedSurfacePattern);
7216
+ if (children.length === 0) return () => 0;
7217
+ return (u2, v) => children.reduce((value, child) => max$1(value, child(u2, v)), -Infinity);
7218
+ }
7219
+ case "surfacePattern:clamp": {
7220
+ const child = compileTypedSurfacePattern(pattern.child);
7221
+ return (u2, v) => clamp$a(child(u2, v), pattern.min, pattern.max);
7222
+ }
7223
+ }
7224
+ }
7225
+ function estimateSurfacePatternAmplitude(pattern) {
7226
+ switch (pattern.kind) {
7227
+ case "surfacePattern:constant":
7228
+ return abs(pattern.value);
7229
+ case "surfacePattern:sineWave":
7230
+ return abs(pattern.bias) + abs(pattern.amplitude);
7231
+ case "surfacePattern:stripes":
7232
+ case "surfacePattern:overUnderWeave":
7233
+ return pattern.depth;
7234
+ case "surfacePattern:abs":
7235
+ case "surfacePattern:negate":
7236
+ return estimateSurfacePatternAmplitude(pattern.child);
7237
+ case "surfacePattern:add": {
7238
+ const amplitudes = pattern.children.map(estimateSurfacePatternAmplitude);
7239
+ return amplitudes.every((value) => value !== null) ? amplitudes.reduce((sum2, value) => sum2 + value, 0) : null;
7240
+ }
7241
+ case "surfacePattern:multiply": {
7242
+ const amplitudes = pattern.children.map(estimateSurfacePatternAmplitude);
7243
+ return amplitudes.every((value) => value !== null) ? amplitudes.reduce((product, value) => product * value, 1) : null;
7244
+ }
7245
+ case "surfacePattern:min":
7246
+ case "surfacePattern:max": {
7247
+ const amplitudes = pattern.children.map(estimateSurfacePatternAmplitude);
7248
+ return amplitudes.every((value) => value !== null) ? max$1(...amplitudes) : null;
7249
+ }
7250
+ case "surfacePattern:clamp":
7251
+ return max$1(abs(pattern.min), abs(pattern.max));
7252
+ }
7253
+ }
7254
+ function compileSurfacePattern(node) {
7255
+ if (node.pattern) return compileTypedSurfacePattern(node.pattern);
7256
+ const constEntries = Object.entries(node.constants ?? {});
7257
+ const constNames = constEntries.map(([k2]) => k2);
7258
+ const constValues = constEntries.map(([, v]) => v);
7259
+ const patternFn = new Function("u", "v", ...constNames, `return (${node.patternBody});`);
7260
+ return (u2, v) => patternFn(u2, v, ...constValues);
7261
+ }
6830
7262
  function compileSdfNode3(node) {
6831
7263
  switch (node.kind) {
6832
7264
  case "sdf:sphere": {
6833
7265
  const r = node.radius;
6834
- return (x2, y2, z2) => sdSphere(x2, y2, z2, r);
7266
+ return (x2, y2, z2) => sdSphere$1(x2, y2, z2, r);
6835
7267
  }
6836
7268
  case "sdf:box": {
6837
7269
  const [hx, hy, hz] = node.halfExtents;
6838
- return (x2, y2, z2) => sdBox(x2, y2, z2, hx, hy, hz);
7270
+ return (x2, y2, z2) => sdBox$1(x2, y2, z2, hx, hy, hz);
6839
7271
  }
6840
7272
  case "sdf:cylinder": {
6841
7273
  const { height: h, radius: r } = node;
6842
- return (x2, y2, z2) => sdCylinder(x2, y2, z2, h, r);
7274
+ return (x2, y2, z2) => sdCylinder$1(x2, y2, z2, h, r);
6843
7275
  }
6844
7276
  case "sdf:torus": {
6845
7277
  const { majorRadius: R, minorRadius: r } = node;
6846
- return (x2, y2, z2) => sdTorus(x2, y2, z2, R, r);
7278
+ return (x2, y2, z2) => sdTorus$1(x2, y2, z2, R, r);
6847
7279
  }
6848
7280
  case "sdf:capsule": {
6849
7281
  const { height: h, radius: r } = node;
6850
- return (x2, y2, z2) => sdCapsule(x2, y2, z2, h, r);
7282
+ return (x2, y2, z2) => sdCapsule$1(x2, y2, z2, h, r);
6851
7283
  }
6852
7284
  case "sdf:cone": {
6853
7285
  const { height: h, radius: r } = node;
6854
- return (x2, y2, z2) => sdCone(x2, y2, z2, h, r);
7286
+ return (x2, y2, z2) => sdCone$1(x2, y2, z2, h, r);
6855
7287
  }
6856
7288
  case "sdf:polylineSweep": {
6857
7289
  return (x2, y2, z2) => sdPolylineSweep3(node, x2, y2, z2);
@@ -6868,7 +7300,7 @@ function compileSdfNode3(node) {
6868
7300
  const fns = node.children.map(compileSdfNode3);
6869
7301
  return (x2, y2, z2) => {
6870
7302
  let d2 = fns[0](x2, y2, z2);
6871
- for (let i = 1; i < fns.length; i++) d2 = max(d2, -fns[i](x2, y2, z2));
7303
+ for (let i = 1; i < fns.length; i++) d2 = max$1(d2, -fns[i](x2, y2, z2));
6872
7304
  return d2;
6873
7305
  };
6874
7306
  }
@@ -6876,7 +7308,7 @@ function compileSdfNode3(node) {
6876
7308
  const fns = node.children.map(compileSdfNode3);
6877
7309
  return (x2, y2, z2) => {
6878
7310
  let d2 = fns[0](x2, y2, z2);
6879
- for (let i = 1; i < fns.length; i++) d2 = max(d2, fns[i](x2, y2, z2));
7311
+ for (let i = 1; i < fns.length; i++) d2 = max$1(d2, fns[i](x2, y2, z2));
6880
7312
  return d2;
6881
7313
  };
6882
7314
  }
@@ -6885,7 +7317,7 @@ function compileSdfNode3(node) {
6885
7317
  const k2 = node.radius;
6886
7318
  return (x2, y2, z2) => {
6887
7319
  let d2 = fns[0](x2, y2, z2);
6888
- for (let i = 1; i < fns.length; i++) d2 = smin(d2, fns[i](x2, y2, z2), k2);
7320
+ for (let i = 1; i < fns.length; i++) d2 = smin$1(d2, fns[i](x2, y2, z2), k2);
6889
7321
  return d2;
6890
7322
  };
6891
7323
  }
@@ -6894,7 +7326,7 @@ function compileSdfNode3(node) {
6894
7326
  const k2 = node.radius;
6895
7327
  return (x2, y2, z2) => {
6896
7328
  let d2 = fns[0](x2, y2, z2);
6897
- for (let i = 1; i < fns.length; i++) d2 = smax(d2, -fns[i](x2, y2, z2), k2);
7329
+ for (let i = 1; i < fns.length; i++) d2 = smax$1(d2, -fns[i](x2, y2, z2), k2);
6898
7330
  return d2;
6899
7331
  };
6900
7332
  }
@@ -6903,7 +7335,7 @@ function compileSdfNode3(node) {
6903
7335
  const k2 = node.radius;
6904
7336
  return (x2, y2, z2) => {
6905
7337
  let d2 = fns[0](x2, y2, z2);
6906
- for (let i = 1; i < fns.length; i++) d2 = smax(d2, fns[i](x2, y2, z2), k2);
7338
+ for (let i = 1; i < fns.length; i++) d2 = smax$1(d2, fns[i](x2, y2, z2), k2);
6907
7339
  return d2;
6908
7340
  };
6909
7341
  }
@@ -6921,10 +7353,10 @@ function compileSdfNode3(node) {
6921
7353
  }
6922
7354
  case "sdf:rotate": {
6923
7355
  const fn = compileSdfNode3(node.child);
6924
- const [rx, ry, rz] = node.degrees.map((d2) => d2 * DEG$1);
6925
- const cx = cos(rx), sx = sin(rx);
6926
- const cy = cos(ry), sy = sin(ry);
6927
- const cz = cos(rz), sz = sin(rz);
7356
+ const [rx, ry, rz] = node.degrees.map((d2) => d2 * DEG$2);
7357
+ const cx = cos$1(rx), sx = sin$1(rx);
7358
+ const cy = cos$1(ry), sy = sin$1(ry);
7359
+ const cz = cos$1(rz), sz = sin$1(rz);
6928
7360
  return (x2, y2, z2) => {
6929
7361
  const x1 = cz * x2 + sz * y2;
6930
7362
  const y1 = -sz * x2 + cz * y2;
@@ -6943,10 +7375,10 @@ function compileSdfNode3(node) {
6943
7375
  }
6944
7376
  case "sdf:twist": {
6945
7377
  const fn = compileSdfNode3(node.child);
6946
- const k2 = node.degreesPerUnit * DEG$1;
7378
+ const k2 = node.degreesPerUnit * DEG$2;
6947
7379
  return (x2, y2, z2) => {
6948
7380
  const angle = k2 * z2;
6949
- const c2 = cos(angle), s = sin(angle);
7381
+ const c2 = cos$1(angle), s = sin$1(angle);
6950
7382
  return fn(c2 * x2 - s * y2, s * x2 + c2 * y2, z2);
6951
7383
  };
6952
7384
  }
@@ -6955,7 +7387,7 @@ function compileSdfNode3(node) {
6955
7387
  const r = node.radius;
6956
7388
  return (x2, y2, z2) => {
6957
7389
  const angle = x2 / r;
6958
- const c2 = cos(angle), s = sin(angle);
7390
+ const c2 = cos$1(angle), s = sin$1(angle);
6959
7391
  return fn((r + y2) * s, (r + y2) * c2 - r, z2);
6960
7392
  };
6961
7393
  }
@@ -6963,7 +7395,19 @@ function compileSdfNode3(node) {
6963
7395
  const fn = compileSdfNode3(node.child);
6964
7396
  const [sx, sy, sz] = node.spacing;
6965
7397
  const [cx, cy, cz] = node.count;
6966
- return (x2, y2, z2) => fn(repeatCoord(x2, sx, cx), repeatCoord(y2, sy, cy), repeatCoord(z2, sz, cz));
7398
+ return (x2, y2, z2) => fn(repeatCoord$1(x2, sx, cx), repeatCoord$1(y2, sy, cy), repeatCoord$1(z2, sz, cz));
7399
+ }
7400
+ case "sdf:circularArray": {
7401
+ const fn = compileSdfNode3(node.child);
7402
+ const da = 2 * PI$1 / node.count;
7403
+ const offset2 = node.offset;
7404
+ return (x2, y2, z2) => {
7405
+ const r = length2$1(x2, y2);
7406
+ const a2 = positiveMod(Math.atan2(y2, x2), da);
7407
+ const d1 = fn(cos$1(a2 - da) * r - offset2, sin$1(a2 - da) * r, z2);
7408
+ const d2 = fn(cos$1(a2) * r - offset2, sin$1(a2) * r, z2);
7409
+ return min(d1, d2);
7410
+ };
6967
7411
  }
6968
7412
  case "sdf:shell": {
6969
7413
  const fn = compileSdfNode3(node.child);
@@ -6980,10 +7424,7 @@ function compileSdfNode3(node) {
6980
7424
  }
6981
7425
  case "sdf:surfaceDisplace": {
6982
7426
  const childFn = compileSdfNode3(node.child);
6983
- const constEntries = Object.entries(node.constants ?? {});
6984
- const constNames = constEntries.map(([k2]) => k2);
6985
- const constValues = constEntries.map(([, v]) => v);
6986
- const patternFn = new Function("u", "v", ...constNames, `return (${node.patternBody});`);
7427
+ const patternFn = compileSurfacePattern(node);
6987
7428
  const uvMode = node.uvMode && node.uvMode !== "auto" ? node.uvMode : void 0;
6988
7429
  const analysis = analyzeUV(node.child, uvMode);
6989
7430
  const uvFn = compileUVFunction(analysis);
@@ -6995,7 +7436,7 @@ function compileSdfNode3(node) {
6995
7436
  p2[1] = y2;
6996
7437
  p2[2] = z2;
6997
7438
  const [u2, v] = uvFn(p2);
6998
- return d2 + patternFn(u2, v, ...constValues);
7439
+ return d2 + patternFn(u2, v);
6999
7440
  };
7000
7441
  }
7001
7442
  const sharpness = node.triplanarSharpness ?? 4;
@@ -7006,9 +7447,9 @@ function compileSdfNode3(node) {
7006
7447
  const gy = childFn(x2, y2 + eps, z2) - childFn(x2, y2 - eps, z2);
7007
7448
  const gz = childFn(x2, y2, z2 + eps) - childFn(x2, y2, z2 - eps);
7008
7449
  const { wx, wy, wz } = triplanarWeights(gx, gy, gz, sharpness);
7009
- const hX = patternFn(y2, z2, ...constValues);
7010
- const hY = patternFn(x2, z2, ...constValues);
7011
- const hZ = patternFn(x2, y2, ...constValues);
7450
+ const hX = patternFn(y2, z2);
7451
+ const hY = patternFn(x2, z2);
7452
+ const hZ = patternFn(x2, y2);
7012
7453
  return d2 + wx * hX + wy * hY + wz * hZ;
7013
7454
  };
7014
7455
  }
@@ -7078,7 +7519,7 @@ function compileSdfNode3(node) {
7078
7519
  const gx = gradFn(x2 + eps, y2, z2) - gradFn(x2 - eps, y2, z2);
7079
7520
  const gy = gradFn(x2, y2 + eps, z2) - gradFn(x2, y2 - eps, z2);
7080
7521
  const gz = gradFn(x2, y2, z2 + eps) - gradFn(x2, y2, z2 - eps);
7081
- const glen = sqrt(gx * gx + gy * gy + gz * gz);
7522
+ const glen = sqrt$1(gx * gx + gy * gy + gz * gz);
7082
7523
  let nx = 0, ny = 0, nz = 0;
7083
7524
  if (glen > 1e-10) {
7084
7525
  const invG = 1 / glen;
@@ -7140,7 +7581,7 @@ function estimateSdfBounds(node) {
7140
7581
  for (const point2 of node.points) {
7141
7582
  for (let i = 0; i < 3; i++) {
7142
7583
  minPoint[i] = min(minPoint[i], point2[i]);
7143
- maxPoint[i] = max(maxPoint[i], point2[i]);
7584
+ maxPoint[i] = max$1(maxPoint[i], point2[i]);
7144
7585
  }
7145
7586
  }
7146
7587
  return padBounds({ min: minPoint, max: maxPoint }, pad);
@@ -7171,7 +7612,7 @@ function estimateSdfBounds(node) {
7171
7612
  }
7172
7613
  case "sdf:rotate": {
7173
7614
  const b = estimateSdfBounds(node.child);
7174
- const r = length3(max(abs(b.min[0]), abs(b.max[0])), max(abs(b.min[1]), abs(b.max[1])), max(abs(b.min[2]), abs(b.max[2])));
7615
+ const r = length3$1(max$1(abs(b.min[0]), abs(b.max[0])), max$1(abs(b.min[1]), abs(b.max[1])), max$1(abs(b.min[2]), abs(b.max[2])));
7175
7616
  return { min: [-r, -r, -r], max: [r, r, r] };
7176
7617
  }
7177
7618
  case "sdf:scale": {
@@ -7185,7 +7626,7 @@ function estimateSdfBounds(node) {
7185
7626
  case "sdf:twist":
7186
7627
  case "sdf:bend": {
7187
7628
  const b = estimateSdfBounds(node.child);
7188
- const r = length3(max(abs(b.min[0]), abs(b.max[0])), max(abs(b.min[1]), abs(b.max[1])), max(abs(b.min[2]), abs(b.max[2]))) * 1.5;
7629
+ const r = length3$1(max$1(abs(b.min[0]), abs(b.max[0])), max$1(abs(b.min[1]), abs(b.max[1])), max$1(abs(b.min[2]), abs(b.max[2]))) * 1.5;
7189
7630
  return { min: [-r, -r, -r], max: [r, r, r] };
7190
7631
  }
7191
7632
  case "sdf:repeat": {
@@ -7206,16 +7647,29 @@ function estimateSdfBounds(node) {
7206
7647
  const [zMin, zMax] = expand(sz, cz, b.min[2], b.max[2]);
7207
7648
  return { min: [xMin, yMin, zMin], max: [xMax, yMax, zMax] };
7208
7649
  }
7650
+ case "sdf:circularArray": {
7651
+ const b = estimateSdfBounds(node.child);
7652
+ const x0 = b.min[0] + node.offset;
7653
+ const x1 = b.max[0] + node.offset;
7654
+ const y0 = b.min[1];
7655
+ const y1 = b.max[1];
7656
+ const r = max$1(length2$1(x0, y0), length2$1(x0, y1), length2$1(x1, y0), length2$1(x1, y1));
7657
+ return { min: [-r, -r, b.min[2]], max: [r, r, b.max[2]] };
7658
+ }
7209
7659
  case "sdf:shell": {
7210
7660
  const b = estimateSdfBounds(node.child);
7211
7661
  const t = node.thickness * 0.5;
7212
7662
  return padBounds(b, t);
7213
7663
  }
7214
- case "sdf:displace":
7215
- case "sdf:surfaceDisplace": {
7664
+ case "sdf:displace": {
7216
7665
  const b = estimateSdfBounds(node.child);
7217
7666
  return padBounds(b, 5);
7218
7667
  }
7668
+ case "sdf:surfaceDisplace": {
7669
+ const b = estimateSdfBounds(node.child);
7670
+ if (!node.pattern) return padBounds(b, 5);
7671
+ return padBounds(b, estimateSurfacePatternAmplitude(node.pattern) ?? 5);
7672
+ }
7219
7673
  case "sdf:onion": {
7220
7674
  const b = estimateSdfBounds(node.child);
7221
7675
  return padBounds(b, node.layers * node.thickness);
@@ -7252,7 +7706,7 @@ function unionBounds(bounds, pad) {
7252
7706
  for (const b of bounds) {
7253
7707
  for (let i = 0; i < 3; i++) {
7254
7708
  result.min[i] = min(result.min[i], b.min[i]);
7255
- result.max[i] = max(result.max[i], b.max[i]);
7709
+ result.max[i] = max$1(result.max[i], b.max[i]);
7256
7710
  }
7257
7711
  }
7258
7712
  if (pad > 0) return padBounds(result, pad);
@@ -7265,7 +7719,7 @@ function intersectBounds(bounds, pad) {
7265
7719
  };
7266
7720
  for (const b of bounds) {
7267
7721
  for (let i = 0; i < 3; i++) {
7268
- result.min[i] = max(result.min[i], b.min[i]);
7722
+ result.min[i] = max$1(result.min[i], b.min[i]);
7269
7723
  result.max[i] = min(result.max[i], b.max[i]);
7270
7724
  }
7271
7725
  }
@@ -7281,242 +7735,544 @@ function padBounds(b, pad) {
7281
7735
  max: [b.max[0] + pad, b.max[1] + pad, b.max[2] + pad]
7282
7736
  };
7283
7737
  }
7284
- const DEFAULT_MAX_GRID_POINTS = 8e6;
7285
- const DEFAULT_MIN_EDGE_LENGTH = 0.15;
7286
- function resolveSdfMeshingSettings(tree, bounds, options = {}) {
7287
- const quality = options.quality ?? "preview";
7288
- const minEdgeLength = positiveOrDefault(options.minEdgeLength, DEFAULT_MIN_EDGE_LENGTH);
7289
- const maxGridPoints = positiveOrDefault(options.maxGridPoints, DEFAULT_MAX_GRID_POINTS);
7290
- const tolerance = options.tolerance !== void 0 ? requirePositiveFinite$2(options.tolerance, "SDF tolerance") : void 0;
7291
- const minFeatureSize = options.minFeatureSize !== void 0 ? requirePositiveFinite$2(options.minFeatureSize, "SDF minFeatureSize") : void 0;
7292
- const maxTriangles = options.maxTriangles !== void 0 ? Math.floor(requirePositiveFinite$2(options.maxTriangles, "SDF maxTriangles")) : void 0;
7293
- const analysis = analyzeSdfTree(tree);
7294
- const warnings = [];
7295
- let edgeLength2;
7296
- if (options.edgeLength !== void 0) {
7297
- edgeLength2 = requirePositiveFinite$2(options.edgeLength, "SDF edgeLength");
7298
- if (edgeLength2 < minEdgeLength) {
7299
- warnings.push(`edgeLength ${formatMm(edgeLength2)} was clamped to minimum ${formatMm(minEdgeLength)}.`);
7300
- edgeLength2 = minEdgeLength;
7301
- }
7302
- } else {
7303
- edgeLength2 = resolveDefaultEdgeLength(bounds, quality, minEdgeLength, analysis, options);
7738
+ const { PI } = Math;
7739
+ const DEG$1 = PI / 180;
7740
+ const TAU = 2 * PI;
7741
+ const GRAD_EPS = 1e-9;
7742
+ const Op = {
7743
+ Const: 0,
7744
+ Neg: 1,
7745
+ Abs: 2,
7746
+ Sqrt: 3,
7747
+ Sin: 4,
7748
+ Cos: 5,
7749
+ Round: 6,
7750
+ Add: 7,
7751
+ Sub: 8,
7752
+ Mul: 9,
7753
+ Div: 10,
7754
+ Min: 11,
7755
+ Max: 12
7756
+ };
7757
+ class UnsupportedSdfProgramNodeError extends Error {
7758
+ constructor(message) {
7759
+ super(message);
7760
+ this.name = "UnsupportedSdfProgramNodeError";
7304
7761
  }
7305
- if (analysis.minWallThickness < Infinity && analysis.minWallThickness < edgeLength2 * 2) {
7306
- analysis.riskFlags.add("thin-shell");
7307
- warnings.push(
7308
- `shell/wall thickness ${formatMm(analysis.minWallThickness)} is below 2 x edgeLength ${formatMm(edgeLength2)}; thin features may be under-sampled.`
7309
- );
7762
+ }
7763
+ class SdfProgramBuilder {
7764
+ constructor() {
7765
+ __publicField(this, "opcodes", []);
7766
+ __publicField(this, "argA", []);
7767
+ __publicField(this, "argB", []);
7768
+ __publicField(this, "argC", []);
7769
+ __publicField(this, "constants", []);
7770
+ __publicField(this, "x", 0);
7771
+ __publicField(this, "y", 1);
7772
+ __publicField(this, "z", 2);
7310
7773
  }
7311
- if (!options.bounds && analysis.hasInfiniteRepeat) {
7312
- warnings.push("infinite repeat bounds are heuristic; pass .toShape({ bounds }) for predictable clipping.");
7774
+ constant(value) {
7775
+ const index2 = this.constants.length;
7776
+ this.constants.push(value);
7777
+ return this.push(Op.Const, 0, 0, index2);
7313
7778
  }
7314
- if (!options.bounds && analysis.riskFlags.has("noise")) {
7315
- warnings.push("noise field bounds are heuristic; pass .toShape({ bounds }) for predictable clipping.");
7779
+ neg(a2) {
7780
+ return this.push(Op.Neg, a2);
7316
7781
  }
7317
- if (!options.bounds && (analysis.riskFlags.has("tpms") || analysis.riskFlags.has("voronoi"))) {
7318
- warnings.push("TPMS/Voronoi bounds are heuristic unless clipped or passed explicitly.");
7782
+ abs(a2) {
7783
+ return this.push(Op.Abs, a2);
7319
7784
  }
7320
- if (analysis.hasLegacyTpmsThreshold) {
7321
- warnings.push("TPMS thickness is using legacy field-threshold units; use wallThickness for approximate millimeters.");
7785
+ sqrt(a2) {
7786
+ return this.push(Op.Sqrt, a2);
7787
+ }
7788
+ sin(a2) {
7789
+ return this.push(Op.Sin, a2);
7790
+ }
7791
+ cos(a2) {
7792
+ return this.push(Op.Cos, a2);
7793
+ }
7794
+ round(a2) {
7795
+ return this.push(Op.Round, a2);
7796
+ }
7797
+ add(a2, b) {
7798
+ return this.push(Op.Add, a2, b);
7799
+ }
7800
+ sub(a2, b) {
7801
+ return this.push(Op.Sub, a2, b);
7802
+ }
7803
+ mul(a2, b) {
7804
+ return this.push(Op.Mul, a2, b);
7805
+ }
7806
+ div(a2, b) {
7807
+ return this.push(Op.Div, a2, b);
7808
+ }
7809
+ min(a2, b) {
7810
+ return this.push(Op.Min, a2, b);
7811
+ }
7812
+ max(a2, b) {
7813
+ return this.push(Op.Max, a2, b);
7814
+ }
7815
+ finalize(output) {
7816
+ return {
7817
+ opcodes: Uint8Array.from(this.opcodes),
7818
+ argA: Int32Array.from(this.argA),
7819
+ argB: Int32Array.from(this.argB),
7820
+ argC: Int32Array.from(this.argC),
7821
+ constants: Float64Array.from(this.constants),
7822
+ output,
7823
+ slotCount: this.opcodes.length + 3
7824
+ };
7825
+ }
7826
+ push(op, a2 = 0, b = 0, c2 = 0) {
7827
+ const slot2 = this.opcodes.length + 3;
7828
+ this.opcodes.push(op);
7829
+ this.argA.push(a2);
7830
+ this.argB.push(b);
7831
+ this.argC.push(c2);
7832
+ return slot2;
7833
+ }
7834
+ }
7835
+ function compileSdfProgramEvaluator3(program) {
7836
+ const lines = ["const { abs, sqrt, sin, cos, round, min, max } = Math;", "let v0 = x;", "let v1 = y;", "let v2 = z;"];
7837
+ const { opcodes, argA, argB, argC, constants, output } = program;
7838
+ for (let i = 0; i < opcodes.length; i++) {
7839
+ const slot2 = `v${i + 3}`;
7840
+ const a2 = `v${argA[i]}`;
7841
+ const b = `v${argB[i]}`;
7842
+ switch (opcodes[i]) {
7843
+ case Op.Const:
7844
+ lines.push(`let ${slot2} = ${numberLiteral(constants[argC[i]])};`);
7845
+ break;
7846
+ case Op.Neg:
7847
+ lines.push(`let ${slot2} = -${a2};`);
7848
+ break;
7849
+ case Op.Abs:
7850
+ lines.push(`let ${slot2} = abs(${a2});`);
7851
+ break;
7852
+ case Op.Sqrt:
7853
+ lines.push(`let ${slot2} = sqrt(${a2});`);
7854
+ break;
7855
+ case Op.Sin:
7856
+ lines.push(`let ${slot2} = sin(${a2});`);
7857
+ break;
7858
+ case Op.Cos:
7859
+ lines.push(`let ${slot2} = cos(${a2});`);
7860
+ break;
7861
+ case Op.Round:
7862
+ lines.push(`let ${slot2} = round(${a2});`);
7863
+ break;
7864
+ case Op.Add:
7865
+ lines.push(`let ${slot2} = ${a2} + ${b};`);
7866
+ break;
7867
+ case Op.Sub:
7868
+ lines.push(`let ${slot2} = ${a2} - ${b};`);
7869
+ break;
7870
+ case Op.Mul:
7871
+ lines.push(`let ${slot2} = ${a2} * ${b};`);
7872
+ break;
7873
+ case Op.Div:
7874
+ lines.push(`let ${slot2} = ${a2} / ${b};`);
7875
+ break;
7876
+ case Op.Min:
7877
+ lines.push(`let ${slot2} = min(${a2}, ${b});`);
7878
+ break;
7879
+ case Op.Max:
7880
+ lines.push(`let ${slot2} = max(${a2}, ${b});`);
7881
+ break;
7882
+ default:
7883
+ throw new Error(`Unknown SdfProgram opcode ${opcodes[i]} at instruction ${i}.`);
7884
+ }
7322
7885
  }
7886
+ lines.push(`return v${output};`);
7887
+ return new Function("Math", `return function sdfProgramEval(x, y, z) {
7888
+ ${lines.join("\n")}
7889
+ };`)(Math);
7890
+ }
7891
+ function numberLiteral(value) {
7892
+ if (Number.isNaN(value)) return "NaN";
7893
+ if (value === Number.POSITIVE_INFINITY) return "Infinity";
7894
+ if (value === Number.NEGATIVE_INFINITY) return "-Infinity";
7895
+ return String(value);
7896
+ }
7897
+ function clampSlot(b, v, lo, hi) {
7898
+ return b.min(b.max(v, b.constant(lo)), b.constant(hi));
7899
+ }
7900
+ function length2(b, x2, y2) {
7901
+ return b.sqrt(b.add(b.mul(x2, x2), b.mul(y2, y2)));
7902
+ }
7903
+ function length3(b, x2, y2, z2) {
7904
+ return b.sqrt(b.add(b.add(b.mul(x2, x2), b.mul(y2, y2)), b.mul(z2, z2)));
7905
+ }
7906
+ function smin(b, a2, child, k2) {
7907
+ if (k2 <= 0) return b.min(a2, child);
7908
+ const h = b.div(b.max(b.sub(b.constant(k2), b.abs(b.sub(a2, child))), b.constant(0)), b.constant(k2));
7909
+ return b.sub(b.min(a2, child), b.mul(b.mul(b.mul(h, h), h), b.constant(k2 / 6)));
7910
+ }
7911
+ function smax(b, a2, child, k2) {
7912
+ return b.neg(smin(b, b.neg(a2), b.neg(child), k2));
7913
+ }
7914
+ function emitScaledTrig(b, x2, y2, z2, cellSize) {
7915
+ const s = b.constant(TAU / cellSize);
7916
+ const xs = b.mul(x2, s);
7917
+ const ys = b.mul(y2, s);
7918
+ const zs = b.mul(z2, s);
7323
7919
  return {
7324
- quality,
7325
- edgeLength: edgeLength2,
7326
- tolerance,
7327
- minFeatureSize,
7328
- minEdgeLength,
7329
- simplify: resolveSimplificationMode(options.simplify, quality, analysis.riskFlags),
7330
- maxTriangles,
7331
- maxGridPoints,
7332
- diagnostics: options.diagnostics === true,
7333
- treeRiskFlags: [...analysis.riskFlags].sort(),
7334
- warnings
7920
+ scale: s,
7921
+ sx: b.sin(xs),
7922
+ sy: b.sin(ys),
7923
+ sz: b.sin(zs),
7924
+ cx: b.cos(xs),
7925
+ cy: b.cos(ys),
7926
+ cz: b.cos(zs),
7927
+ sx2: b.sin(b.mul(b.constant(2), xs)),
7928
+ sy2: b.sin(b.mul(b.constant(2), ys)),
7929
+ sz2: b.sin(b.mul(b.constant(2), zs)),
7930
+ cx2: b.cos(b.mul(b.constant(2), xs)),
7931
+ cy2: b.cos(b.mul(b.constant(2), ys)),
7932
+ cz2: b.cos(b.mul(b.constant(2), zs))
7335
7933
  };
7336
7934
  }
7337
- function withScaledSdfEdgeLength(settings, edgeLength2) {
7338
- return { ...settings, edgeLength: Math.max(settings.minEdgeLength, edgeLength2) };
7935
+ function emitGyroidValueAndGradient(b, x2, y2, z2, cellSize) {
7936
+ const t = emitScaledTrig(b, x2, y2, z2, cellSize);
7937
+ return {
7938
+ value: b.add(b.add(b.mul(t.sx, t.cy), b.mul(t.sy, t.cz)), b.mul(t.sz, t.cx)),
7939
+ gx: b.mul(t.scale, b.sub(b.mul(t.cx, t.cy), b.mul(t.sz, t.sx))),
7940
+ gy: b.mul(t.scale, b.add(b.neg(b.mul(t.sx, t.sy)), b.mul(t.cy, t.cz))),
7941
+ gz: b.mul(t.scale, b.add(b.neg(b.mul(t.sy, t.sz)), b.mul(t.cz, t.cx)))
7942
+ };
7339
7943
  }
7340
- function createSdfMeshingDiagnostics(settings, bounds, paddedBounds) {
7341
- const grid = estimateSdfGridDimensions(paddedBounds, settings.edgeLength);
7342
- const estimatedSamples = grid[0] * grid[1] * grid[2];
7944
+ function emitSchwarzPValueAndGradient(b, x2, y2, z2, cellSize) {
7945
+ const t = emitScaledTrig(b, x2, y2, z2, cellSize);
7343
7946
  return {
7344
- bounds: cloneBounds$2(bounds),
7345
- paddedBounds: cloneBounds$2(paddedBounds),
7346
- edgeLength: settings.edgeLength,
7347
- grid,
7348
- estimatedSamples,
7349
- estimatedMemoryBytes: estimatedSamples * 8,
7350
- treeRiskFlags: [...settings.treeRiskFlags],
7351
- simplification: settings.simplify,
7352
- capMode: "box",
7353
- capInset: settings.edgeLength,
7354
- warnings: [...settings.warnings]
7947
+ value: b.add(b.add(t.cx, t.cy), t.cz),
7948
+ gx: b.neg(b.mul(t.scale, t.sx)),
7949
+ gy: b.neg(b.mul(t.scale, t.sy)),
7950
+ gz: b.neg(b.mul(t.scale, t.sz))
7355
7951
  };
7356
7952
  }
7357
- function assertSdfMeshingBudget(diagnostics, maxGridPoints) {
7358
- if (diagnostics.estimatedSamples <= maxGridPoints) return;
7359
- const suggestedEdge = suggestEdgeLengthForSampleBudget(diagnostics.paddedBounds, maxGridPoints);
7360
- throw new Error(
7361
- `SDF meshing would sample ${formatCount(diagnostics.estimatedSamples)} grid points (~${formatBytes(
7362
- diagnostics.estimatedMemoryBytes
7363
- )}). Reduce bounds or use edgeLength >= ${formatMm(suggestedEdge)}.`
7953
+ function emitDiamondValueAndGradient(b, x2, y2, z2, cellSize) {
7954
+ const t = emitScaledTrig(b, x2, y2, z2, cellSize);
7955
+ return {
7956
+ value: b.add(
7957
+ b.add(b.mul(b.mul(t.sx, t.sy), t.sz), b.mul(b.mul(t.sx, t.cy), t.cz)),
7958
+ b.add(b.mul(b.mul(t.cx, t.sy), t.cz), b.mul(b.mul(t.cx, t.cy), t.sz))
7959
+ ),
7960
+ gx: b.mul(
7961
+ t.scale,
7962
+ b.add(
7963
+ b.add(b.mul(b.mul(t.cx, t.sy), t.sz), b.mul(b.mul(t.cx, t.cy), t.cz)),
7964
+ b.add(b.neg(b.mul(b.mul(t.sx, t.sy), t.cz)), b.neg(b.mul(b.mul(t.sx, t.cy), t.sz)))
7965
+ )
7966
+ ),
7967
+ gy: b.mul(
7968
+ t.scale,
7969
+ b.add(
7970
+ b.add(b.mul(b.mul(t.sx, t.cy), t.sz), b.neg(b.mul(b.mul(t.sx, t.sy), t.cz))),
7971
+ b.add(b.mul(b.mul(t.cx, t.cy), t.cz), b.neg(b.mul(b.mul(t.cx, t.sy), t.sz)))
7972
+ )
7973
+ ),
7974
+ gz: b.mul(
7975
+ t.scale,
7976
+ b.add(
7977
+ b.add(b.mul(b.mul(t.sx, t.sy), t.cz), b.neg(b.mul(b.mul(t.sx, t.cy), t.sz))),
7978
+ b.add(b.neg(b.mul(b.mul(t.cx, t.sy), t.sz)), b.mul(b.mul(t.cx, t.cy), t.cz))
7979
+ )
7980
+ )
7981
+ };
7982
+ }
7983
+ function emitLidinoidValueAndGradient(b, x2, y2, z2, cellSize) {
7984
+ const t = emitScaledTrig(b, x2, y2, z2, cellSize);
7985
+ const value = b.add(
7986
+ b.sub(
7987
+ b.add(b.add(b.mul(b.mul(t.sx2, t.cy), t.sz), b.mul(b.mul(t.sy2, t.cz), t.sx)), b.mul(b.mul(t.sz2, t.cx), t.sy)),
7988
+ b.add(b.add(b.mul(t.cx2, t.cy2), b.mul(t.cy2, t.cz2)), b.mul(t.cz2, t.cx2))
7989
+ ),
7990
+ b.constant(0.3)
7364
7991
  );
7992
+ return {
7993
+ value,
7994
+ gx: b.mul(
7995
+ t.scale,
7996
+ b.add(
7997
+ b.add(
7998
+ b.add(b.mul(b.mul(b.constant(2), t.cx2), b.mul(t.cy, t.sz)), b.mul(b.mul(t.sy2, t.cz), t.cx)),
7999
+ b.neg(b.mul(b.mul(t.sz2, t.sx), t.sy))
8000
+ ),
8001
+ b.add(b.mul(b.mul(b.constant(2), t.sx2), t.cy2), b.mul(b.mul(b.constant(2), t.cz2), t.sx2))
8002
+ )
8003
+ ),
8004
+ gy: b.mul(
8005
+ t.scale,
8006
+ b.add(
8007
+ b.add(
8008
+ b.add(b.neg(b.mul(b.mul(t.sx2, t.sy), t.sz)), b.mul(b.mul(b.constant(2), t.cy2), b.mul(t.cz, t.sx))),
8009
+ b.mul(b.mul(t.sz2, t.cx), t.cy)
8010
+ ),
8011
+ b.add(b.mul(b.mul(b.constant(2), t.cx2), t.sy2), b.mul(b.mul(b.constant(2), t.sy2), t.cz2))
8012
+ )
8013
+ ),
8014
+ gz: b.mul(
8015
+ t.scale,
8016
+ b.add(
8017
+ b.add(
8018
+ b.add(b.mul(b.mul(t.sx2, t.cy), t.cz), b.neg(b.mul(b.mul(t.sy2, t.sz), t.sx))),
8019
+ b.mul(b.mul(b.constant(2), t.cz2), b.mul(t.cx, t.sy))
8020
+ ),
8021
+ b.add(b.mul(b.mul(b.constant(2), t.cy2), t.sz2), b.mul(b.mul(b.constant(2), t.sz2), t.cx2))
8022
+ )
8023
+ )
8024
+ };
7365
8025
  }
7366
- function estimateSdfGridDimensions(bounds, edgeLength2) {
7367
- const dx = bounds.max[0] - bounds.min[0];
7368
- const dy = bounds.max[1] - bounds.min[1];
7369
- const dz = bounds.max[2] - bounds.min[2];
7370
- return [
7371
- Math.max(2, Math.ceil(dx / edgeLength2) + 1),
7372
- Math.max(2, Math.ceil(dy / edgeLength2) + 1),
7373
- Math.max(2, Math.ceil(dz / edgeLength2) + 1)
7374
- ];
8026
+ function emitTpmsDistance(b, { value, gx, gy, gz }, thickness, thicknessMode) {
8027
+ if (thicknessMode !== "metric-approx") return b.sub(b.abs(value), b.constant(thickness));
8028
+ const grad = length3(b, gx, gy, gz);
8029
+ return b.sub(b.div(b.abs(value), b.max(grad, b.constant(GRAD_EPS))), b.constant(thickness * 0.5));
7375
8030
  }
7376
- function logSdfMeshingDiagnostics(prefix, diagnostics) {
7377
- const warnings = diagnostics.warnings.length > 0 ? `, warnings=${diagnostics.warnings.join(" | ")}` : "";
7378
- console.info(
7379
- `${prefix}: bounds=${formatBounds(diagnostics.bounds)}, paddedBounds=${formatBounds(diagnostics.paddedBounds)}, edgeLength=${formatMm(diagnostics.edgeLength)}, grid=${diagnostics.grid.join("x")}, estimatedSamples=${formatCount(diagnostics.estimatedSamples)}, treeRisk=${diagnostics.treeRiskFlags.join("+") || "none"}, simplify=${diagnostics.simplification}, capMode=${diagnostics.capMode}, capInset=${formatMm(diagnostics.capInset)}${warnings}`
7380
- );
8031
+ const { cos, max, sin, sqrt } = Math;
8032
+ function emitSdfProgramNode(b, node, x2, y2, z2) {
8033
+ switch (node.kind) {
8034
+ case "sdf:sphere":
8035
+ return sdSphere(b, x2, y2, z2, node.radius);
8036
+ case "sdf:box": {
8037
+ const [hx, hy, hz] = node.halfExtents;
8038
+ return sdBox(b, x2, y2, z2, hx, hy, hz);
8039
+ }
8040
+ case "sdf:cylinder":
8041
+ return sdCylinder(b, x2, y2, z2, node.height, node.radius);
8042
+ case "sdf:torus":
8043
+ return sdTorus(b, x2, y2, z2, node.majorRadius, node.minorRadius);
8044
+ case "sdf:capsule":
8045
+ return sdCapsule(b, x2, y2, z2, node.height, node.radius);
8046
+ case "sdf:cone":
8047
+ return sdCone(b, x2, y2, z2, node.height, node.radius);
8048
+ case "sdf:polylineSweep":
8049
+ return sdPolylineSweep(b, node, x2, y2, z2);
8050
+ case "sdf:union":
8051
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => b.min(a2, child));
8052
+ case "sdf:difference":
8053
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => b.max(a2, b.neg(child)));
8054
+ case "sdf:intersection":
8055
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => b.max(a2, child));
8056
+ case "sdf:smoothUnion":
8057
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => smin(b, a2, child, node.radius));
8058
+ case "sdf:smoothDifference":
8059
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => smax(b, a2, b.neg(child), node.radius));
8060
+ case "sdf:smoothIntersection":
8061
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => smax(b, a2, child, node.radius));
8062
+ case "sdf:morph": {
8063
+ const a2 = emitSdfProgramNode(b, node.a, x2, y2, z2);
8064
+ const childB = emitSdfProgramNode(b, node.b, x2, y2, z2);
8065
+ return b.add(b.mul(a2, b.constant(1 - node.t)), b.mul(childB, b.constant(node.t)));
8066
+ }
8067
+ case "sdf:translate": {
8068
+ const [ox, oy, oz] = node.offset;
8069
+ return emitSdfProgramNode(b, node.child, b.sub(x2, b.constant(ox)), b.sub(y2, b.constant(oy)), b.sub(z2, b.constant(oz)));
8070
+ }
8071
+ case "sdf:rotate":
8072
+ return emitRotated(b, node, x2, y2, z2);
8073
+ case "sdf:scale": {
8074
+ const inv = 1 / node.factor;
8075
+ const child = emitSdfProgramNode(b, node.child, b.mul(x2, b.constant(inv)), b.mul(y2, b.constant(inv)), b.mul(z2, b.constant(inv)));
8076
+ return b.mul(child, b.constant(node.factor));
8077
+ }
8078
+ case "sdf:twist": {
8079
+ const angle = b.mul(b.constant(node.degreesPerUnit * DEG$1), z2);
8080
+ const c2 = b.cos(angle);
8081
+ const s = b.sin(angle);
8082
+ return emitSdfProgramNode(b, node.child, b.sub(b.mul(c2, x2), b.mul(s, y2)), b.add(b.mul(s, x2), b.mul(c2, y2)), z2);
8083
+ }
8084
+ case "sdf:bend": {
8085
+ const angle = b.div(x2, b.constant(node.radius));
8086
+ const c2 = b.cos(angle);
8087
+ const s = b.sin(angle);
8088
+ const radiusPlusY = b.add(b.constant(node.radius), y2);
8089
+ return emitSdfProgramNode(b, node.child, b.mul(radiusPlusY, s), b.sub(b.mul(radiusPlusY, c2), b.constant(node.radius)), z2);
8090
+ }
8091
+ case "sdf:repeat": {
8092
+ const [sx, sy, sz] = node.spacing;
8093
+ const [cx, cy, cz] = node.count;
8094
+ return emitSdfProgramNode(b, node.child, repeatCoord(b, x2, sx, cx), repeatCoord(b, y2, sy, cy), repeatCoord(b, z2, sz, cz));
8095
+ }
8096
+ case "sdf:shell": {
8097
+ const child = emitSdfProgramNode(b, node.child, x2, y2, z2);
8098
+ return b.sub(b.abs(child), b.constant(node.thickness * 0.5));
8099
+ }
8100
+ case "sdf:onion": {
8101
+ let d2 = emitSdfProgramNode(b, node.child, x2, y2, z2);
8102
+ for (let i = 0; i < node.layers; i++) d2 = b.sub(b.abs(d2), b.constant(node.thickness));
8103
+ return d2;
8104
+ }
8105
+ case "sdf:gyroid":
8106
+ return emitTpmsDistance(b, emitGyroidValueAndGradient(b, x2, y2, z2, node.cellSize), node.thickness, node.thicknessMode);
8107
+ case "sdf:schwarzP":
8108
+ return emitTpmsDistance(b, emitSchwarzPValueAndGradient(b, x2, y2, z2, node.cellSize), node.thickness, node.thicknessMode);
8109
+ case "sdf:diamond":
8110
+ return emitTpmsDistance(b, emitDiamondValueAndGradient(b, x2, y2, z2, node.cellSize), node.thickness, node.thicknessMode);
8111
+ case "sdf:lidinoid":
8112
+ return emitTpmsDistance(b, emitLidinoidValueAndGradient(b, x2, y2, z2, node.cellSize), node.thickness, node.thicknessMode);
8113
+ default:
8114
+ throw new UnsupportedSdfProgramNodeError(`SdfProgram does not support node kind ${node.kind} yet.`);
8115
+ }
7381
8116
  }
7382
- function resolveDefaultEdgeLength(bounds, quality, minEdgeLength, analysis, options) {
7383
- const dx = bounds.max[0] - bounds.min[0];
7384
- const dy = bounds.max[1] - bounds.min[1];
7385
- const dz = bounds.max[2] - bounds.min[2];
7386
- const maxDim = Math.max(dx, dy, dz, minEdgeLength);
7387
- const divisor = quality === "draft" ? 60 : quality === "export" ? 160 : 100;
7388
- const candidates = [maxDim / divisor];
7389
- if (options.tolerance !== void 0) candidates.push(requirePositiveFinite$2(options.tolerance, "SDF tolerance") * 2);
7390
- if (options.minFeatureSize !== void 0) candidates.push(requirePositiveFinite$2(options.minFeatureSize, "SDF minFeatureSize") / 2.5);
7391
- if (analysis.minMetricTpmsThickness < Infinity) candidates.push(analysis.minMetricTpmsThickness / 2);
7392
- if (analysis.minTpmsCellSize < Infinity) candidates.push(analysis.minTpmsCellSize / 10);
7393
- if (analysis.minRepeatSpacing < Infinity) candidates.push(analysis.minRepeatSpacing / 8);
7394
- if (analysis.minWallThickness < Infinity) candidates.push(analysis.minWallThickness / 2.5);
7395
- return Math.max(minEdgeLength, Math.min(...candidates.filter((v) => Number.isFinite(v) && v > 0)));
8117
+ function foldChildren(b, children, x2, y2, z2, combine2) {
8118
+ let d2 = emitSdfProgramNode(b, children[0], x2, y2, z2);
8119
+ for (let i = 1; i < children.length; i++) d2 = combine2(d2, emitSdfProgramNode(b, children[i], x2, y2, z2));
8120
+ return d2;
7396
8121
  }
7397
- function resolveSimplificationMode(simplify, quality, riskFlags) {
7398
- if (simplify === false) return "off";
7399
- if (simplify === true || simplify === "safe") return "safe";
7400
- if (quality === "export" && riskFlags.size > 0) return "off";
7401
- return "safe";
8122
+ function emitRotated(b, node, x2, y2, z2) {
8123
+ const [rx, ry, rz] = node.degrees.map((d2) => d2 * DEG$1);
8124
+ const cx = cos(rx);
8125
+ const sx = sin(rx);
8126
+ const cy = cos(ry);
8127
+ const sy = sin(ry);
8128
+ const cz = cos(rz);
8129
+ const sz = sin(rz);
8130
+ const x1 = b.add(b.mul(b.constant(cz), x2), b.mul(b.constant(sz), y2));
8131
+ const y1 = b.sub(b.mul(b.constant(cz), y2), b.mul(b.constant(sz), x2));
8132
+ const x22 = b.sub(b.mul(b.constant(cy), x1), b.mul(b.constant(sy), z2));
8133
+ const z22 = b.add(b.mul(b.constant(sy), x1), b.mul(b.constant(cy), z2));
8134
+ const y22 = b.add(b.mul(b.constant(cx), y1), b.mul(b.constant(sx), z22));
8135
+ const z3 = b.sub(b.mul(b.constant(cx), z22), b.mul(b.constant(sx), y1));
8136
+ return emitSdfProgramNode(b, node.child, x22, y22, z3);
8137
+ }
8138
+ function sdSphere(b, x2, y2, z2, r) {
8139
+ return b.sub(length3(b, x2, y2, z2), b.constant(r));
8140
+ }
8141
+ function sdBox(b, x2, y2, z2, hx, hy, hz) {
8142
+ const dx = b.sub(b.abs(x2), b.constant(hx));
8143
+ const dy = b.sub(b.abs(y2), b.constant(hy));
8144
+ const dz = b.sub(b.abs(z2), b.constant(hz));
8145
+ const outside = length3(b, b.max(dx, b.constant(0)), b.max(dy, b.constant(0)), b.max(dz, b.constant(0)));
8146
+ const inside = b.min(b.max(b.max(dx, dy), dz), b.constant(0));
8147
+ return b.add(outside, inside);
8148
+ }
8149
+ function sdCylinder(b, x2, y2, z2, h, r) {
8150
+ const dx = b.sub(length2(b, x2, y2), b.constant(r));
8151
+ const dz = b.sub(b.abs(z2), b.constant(h * 0.5));
8152
+ return b.add(length2(b, b.max(dx, b.constant(0)), b.max(dz, b.constant(0))), b.min(b.max(dx, dz), b.constant(0)));
8153
+ }
8154
+ function sdTorus(b, x2, y2, z2, majorRadius, minorRadius) {
8155
+ return b.sub(length2(b, b.sub(length2(b, x2, y2), b.constant(majorRadius)), z2), b.constant(minorRadius));
8156
+ }
8157
+ function sdCapsule(b, x2, y2, z2, h, r) {
8158
+ const cz = clampSlot(b, z2, -h * 0.5, h * 0.5);
8159
+ return b.sub(length3(b, x2, y2, b.sub(z2, cz)), b.constant(r));
8160
+ }
8161
+ function sdCone(b, x2, y2, z2, h, r) {
8162
+ const q = length2(b, x2, y2);
8163
+ const cLen = sqrt(h * h + r * r);
8164
+ const side = b.add(b.mul(b.constant(h / cLen), q), b.mul(b.constant(-r / cLen), b.sub(z2, b.constant(h))));
8165
+ return b.max(b.max(side, b.neg(z2)), b.sub(z2, b.constant(h)));
8166
+ }
8167
+ function sdPolylineSweep(b, node, x2, y2, z2) {
8168
+ let d2 = sdTaperedSegment(b, x2, y2, z2, node.points[0], node.points[1], node.radii[0], node.radii[1]);
8169
+ for (let i = 1; i < node.points.length - 1; i++) {
8170
+ const segment = sdTaperedSegment(b, x2, y2, z2, node.points[i], node.points[i + 1], node.radii[i], node.radii[i + 1]);
8171
+ d2 = smin(b, d2, segment, node.blend);
8172
+ }
8173
+ return d2;
7402
8174
  }
7403
- function analyzeSdfTree(tree) {
7404
- const analysis = {
7405
- riskFlags: /* @__PURE__ */ new Set(),
7406
- minTpmsCellSize: Infinity,
7407
- minMetricTpmsThickness: Infinity,
7408
- minRepeatSpacing: Infinity,
7409
- minWallThickness: Infinity,
7410
- hasInfiniteRepeat: false,
7411
- hasLegacyTpmsThreshold: false
7412
- };
7413
- visitSdfNode(tree, analysis);
7414
- return analysis;
8175
+ function sdTaperedSegment(b, x2, y2, z2, a2, end, ra, rb) {
8176
+ const vx = end[0] - a2[0];
8177
+ const vy = end[1] - a2[1];
8178
+ const vz = end[2] - a2[2];
8179
+ const len2 = vx * vx + vy * vy + vz * vz;
8180
+ if (len2 <= 1e-12) return sdSphere(b, b.sub(x2, b.constant(a2[0])), b.sub(y2, b.constant(a2[1])), b.sub(z2, b.constant(a2[2])), max(ra, rb));
8181
+ const h = clampSlot(
8182
+ b,
8183
+ b.div(
8184
+ b.add(
8185
+ b.add(b.mul(b.sub(x2, b.constant(a2[0])), b.constant(vx)), b.mul(b.sub(y2, b.constant(a2[1])), b.constant(vy))),
8186
+ b.mul(b.sub(z2, b.constant(a2[2])), b.constant(vz))
8187
+ ),
8188
+ b.constant(len2)
8189
+ ),
8190
+ 0,
8191
+ 1
8192
+ );
8193
+ const sx = b.sub(x2, b.add(b.constant(a2[0]), b.mul(b.constant(vx), h)));
8194
+ const sy = b.sub(y2, b.add(b.constant(a2[1]), b.mul(b.constant(vy), h)));
8195
+ const sz = b.sub(z2, b.add(b.constant(a2[2]), b.mul(b.constant(vz), h)));
8196
+ const radius = b.add(b.constant(ra), b.mul(b.constant(rb - ra), h));
8197
+ return b.sub(length3(b, sx, sy, sz), radius);
7415
8198
  }
7416
- function visitSdfNode(node, analysis) {
8199
+ function repeatCoord(b, v, spacing, count) {
8200
+ if (spacing <= 0) return v;
8201
+ if (count > 0) {
8202
+ const center = (count - 1) * 0.5;
8203
+ const index2 = clampSlot(b, b.round(b.add(b.div(v, b.constant(spacing)), b.constant(center))), 0, count - 1);
8204
+ return b.sub(v, b.mul(b.sub(index2, b.constant(center)), b.constant(spacing)));
8205
+ }
8206
+ return b.sub(v, b.mul(b.constant(spacing), b.round(b.div(v, b.constant(spacing)))));
8207
+ }
8208
+ function getUnsupportedSdfProgramReason(node) {
7417
8209
  switch (node.kind) {
8210
+ case "sdf:displace":
8211
+ return "displace uses a dynamic JavaScript function body";
8212
+ case "sdf:surfaceDisplace":
8213
+ return "surfaceDisplace uses dynamic UV/pattern evaluation";
8214
+ case "sdf:spatialBlend":
8215
+ return "spatialBlend uses a dynamic JavaScript blend function";
8216
+ case "sdf:noise":
8217
+ return "noise depends on table-based simplex evaluation";
8218
+ case "sdf:voronoi":
8219
+ return "voronoi depends on table-based Worley evaluation";
8220
+ case "sdf:custom":
8221
+ return "custom uses a dynamic JavaScript function body";
8222
+ case "sdf:polylineSweep":
8223
+ if (node.points.length < 2) return "polylineSweep needs at least two points";
8224
+ if (node.points.length !== node.radii.length) return "polylineSweep point/radius counts differ";
8225
+ return void 0;
7418
8226
  case "sdf:union":
7419
8227
  case "sdf:difference":
7420
8228
  case "sdf:intersection":
7421
8229
  case "sdf:smoothUnion":
7422
8230
  case "sdf:smoothDifference":
7423
8231
  case "sdf:smoothIntersection":
7424
- for (const child of node.children) visitSdfNode(child, analysis);
7425
- break;
8232
+ for (const child of node.children) {
8233
+ const reason = getUnsupportedSdfProgramReason(child);
8234
+ if (reason) return reason;
8235
+ }
8236
+ return void 0;
7426
8237
  case "sdf:morph":
7427
- case "sdf:spatialBlend":
7428
- visitSdfNode(node.a, analysis);
7429
- visitSdfNode(node.b, analysis);
7430
- break;
8238
+ return getUnsupportedSdfProgramReason(node.a) ?? getUnsupportedSdfProgramReason(node.b);
7431
8239
  case "sdf:translate":
7432
8240
  case "sdf:rotate":
7433
8241
  case "sdf:scale":
7434
8242
  case "sdf:twist":
7435
8243
  case "sdf:bend":
7436
- case "sdf:onion":
7437
- visitSdfNode(node.child, analysis);
7438
- break;
7439
8244
  case "sdf:repeat":
7440
- analysis.riskFlags.add("repeat");
7441
- for (let i = 0; i < 3; i++) {
7442
- const spacing = node.spacing[i];
7443
- if (spacing > 0) {
7444
- analysis.minRepeatSpacing = Math.min(analysis.minRepeatSpacing, spacing);
7445
- if (node.count[i] <= 0) analysis.hasInfiniteRepeat = true;
7446
- }
7447
- }
7448
- visitSdfNode(node.child, analysis);
7449
- break;
7450
8245
  case "sdf:shell":
7451
- analysis.minWallThickness = Math.min(analysis.minWallThickness, node.thickness);
7452
- visitSdfNode(node.child, analysis);
7453
- break;
7454
- case "sdf:displace":
7455
- case "sdf:surfaceDisplace":
7456
- analysis.riskFlags.add("displacement");
7457
- visitSdfNode(node.child, analysis);
7458
- break;
7459
- case "sdf:gyroid":
7460
- case "sdf:schwarzP":
7461
- case "sdf:diamond":
7462
- case "sdf:lidinoid":
7463
- analysis.riskFlags.add("tpms");
7464
- analysis.minTpmsCellSize = Math.min(analysis.minTpmsCellSize, node.cellSize);
7465
- if (node.thicknessMode === "metric-approx") {
7466
- analysis.minMetricTpmsThickness = Math.min(analysis.minMetricTpmsThickness, node.thickness);
7467
- analysis.minWallThickness = Math.min(analysis.minWallThickness, node.thickness);
7468
- } else {
7469
- analysis.hasLegacyTpmsThreshold = true;
7470
- }
7471
- break;
7472
- case "sdf:noise":
7473
- analysis.riskFlags.add("noise");
7474
- break;
7475
- case "sdf:voronoi":
7476
- analysis.riskFlags.add("voronoi");
7477
- analysis.minWallThickness = Math.min(analysis.minWallThickness, node.wallThickness);
7478
- if (node.surfaceChild) visitSdfNode(node.surfaceChild, analysis);
7479
- break;
7480
- case "sdf:custom":
7481
- analysis.riskFlags.add("custom");
7482
- break;
8246
+ case "sdf:onion":
8247
+ return getUnsupportedSdfProgramReason(node.child);
8248
+ default:
8249
+ return void 0;
7483
8250
  }
7484
8251
  }
7485
- function positiveOrDefault(value, fallback) {
7486
- if (value === void 0) return fallback;
7487
- return requirePositiveFinite$2(value, "SDF meshing option");
7488
- }
7489
- function requirePositiveFinite$2(value, name) {
7490
- if (!Number.isFinite(value) || value <= 0) {
7491
- throw new Error(`${name} must be a positive finite number.`);
8252
+ function compileSdfProgram(node) {
8253
+ const unsupportedReason = getUnsupportedSdfProgramReason(node);
8254
+ if (unsupportedReason) {
8255
+ throw new UnsupportedSdfProgramNodeError(`SdfProgram does not support this tree yet: ${unsupportedReason}.`);
7492
8256
  }
7493
- return value;
7494
- }
7495
- function cloneBounds$2(bounds) {
7496
- return { min: [...bounds.min], max: [...bounds.max] };
7497
- }
7498
- function suggestEdgeLengthForSampleBudget(bounds, maxGridPoints) {
7499
- const dx = bounds.max[0] - bounds.min[0];
7500
- const dy = bounds.max[1] - bounds.min[1];
7501
- const dz = bounds.max[2] - bounds.min[2];
7502
- const volume = Math.max(dx * dy * dz, 1);
7503
- return Math.cbrt(volume / Math.max(maxGridPoints, 8));
8257
+ const builder = new SdfProgramBuilder();
8258
+ return builder.finalize(emitSdfProgramNode(builder, node, builder.x, builder.y, builder.z));
7504
8259
  }
7505
- function formatBounds(bounds) {
7506
- return `[${bounds.min.map(formatNumber).join(",")}]-[${bounds.max.map(formatNumber).join(",")}]`;
7507
- }
7508
- function formatMm(value) {
7509
- return `${formatNumber(value)}mm`;
7510
- }
7511
- function formatNumber(value) {
7512
- return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
7513
- }
7514
- function formatCount(value) {
7515
- return Math.round(value).toLocaleString("en-US");
8260
+ function compileSdfProgram3(node) {
8261
+ return compileSdfProgramEvaluator3(compileSdfProgram(node));
7516
8262
  }
7517
- function formatBytes(bytes) {
7518
- if (bytes < 1024 * 1024) return `${Math.ceil(bytes / 1024)} KB`;
7519
- return `${Math.ceil(bytes / (1024 * 1024))} MB`;
8263
+ function compileSdfMaterializationEvaluator3(node) {
8264
+ const unsupportedReason = getUnsupportedSdfProgramReason(node);
8265
+ if (unsupportedReason) {
8266
+ return {
8267
+ fn: compileSdfNode3(node),
8268
+ engine: "closure",
8269
+ unsupportedReason
8270
+ };
8271
+ }
8272
+ return {
8273
+ fn: compileSdfProgram3(node),
8274
+ engine: "program"
8275
+ };
7520
8276
  }
7521
8277
  function midpoint$3(a2, b) {
7522
8278
  return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
@@ -7530,7 +8286,7 @@ function scale$6(v, s) {
7530
8286
  function sub$7(a2, b) {
7531
8287
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
7532
8288
  }
7533
- function cross$7(a2, b) {
8289
+ function cross$8(a2, b) {
7534
8290
  return [a2[1] * b[2] - a2[2] * b[1], a2[2] * b[0] - a2[0] * b[2], a2[0] * b[1] - a2[1] * b[0]];
7535
8291
  }
7536
8292
  function makeEdge(name, start, end, faceName, curve) {
@@ -7566,7 +8322,7 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
7566
8322
  const center = options.center ?? average$1(corners);
7567
8323
  const uAxis = normalizeAxis$1(sub$7(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
7568
8324
  const vAxis = normalizeAxis$1(sub$7(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
7569
- const normal = normalizeAxis$1(options.normal ?? cross$7(uAxis, vAxis));
8325
+ const normal = normalizeAxis$1(options.normal ?? cross$8(uAxis, vAxis));
7570
8326
  const faces = /* @__PURE__ */ new Map();
7571
8327
  faces.set(faceName, {
7572
8328
  name: faceName,
@@ -7982,7 +8738,7 @@ function buildCircleExtrusionTopology(circ, height, center = false) {
7982
8738
  );
7983
8739
  return { faces, edges };
7984
8740
  }
7985
- function requireFinite$8(v, label) {
8741
+ function requireFinite$9(v, label) {
7986
8742
  if (!Number.isFinite(v)) throw new Error(`nurbsSurface: ${label} must be finite, got ${v}`);
7987
8743
  }
7988
8744
  function normalizeSurfaceTessellation(tessellation) {
@@ -7992,11 +8748,11 @@ function normalizeSurfaceTessellation(tessellation) {
7992
8748
  throw new Error(`nurbsSurface: tessellation.mode must be "uniform" or "adaptive", got ${mode}`);
7993
8749
  }
7994
8750
  if (tessellation.tolerance !== void 0) {
7995
- requireFinite$8(tessellation.tolerance, "tessellation.tolerance");
8751
+ requireFinite$9(tessellation.tolerance, "tessellation.tolerance");
7996
8752
  if (tessellation.tolerance <= 0) throw new Error("nurbsSurface: tessellation.tolerance must be > 0");
7997
8753
  }
7998
- if (tessellation.minResolution !== void 0) requireFinite$8(tessellation.minResolution, "tessellation.minResolution");
7999
- if (tessellation.maxResolution !== void 0) requireFinite$8(tessellation.maxResolution, "tessellation.maxResolution");
8754
+ if (tessellation.minResolution !== void 0) requireFinite$9(tessellation.minResolution, "tessellation.minResolution");
8755
+ if (tessellation.maxResolution !== void 0) requireFinite$9(tessellation.maxResolution, "tessellation.maxResolution");
8000
8756
  const minResolution = tessellation.minResolution === void 0 ? void 0 : Math.max(2, Math.round(tessellation.minResolution));
8001
8757
  const maxResolution = tessellation.maxResolution === void 0 ? void 0 : Math.max(2, Math.round(tessellation.maxResolution));
8002
8758
  if (minResolution !== void 0 && maxResolution !== void 0 && minResolution > maxResolution) {
@@ -8015,10 +8771,10 @@ function normalizeSurfaceDomain(domain) {
8015
8771
  const uMax = domain.uMax ?? 1;
8016
8772
  const vMin = domain.vMin ?? 0;
8017
8773
  const vMax = domain.vMax ?? 1;
8018
- requireFinite$8(uMin, "domain.uMin");
8019
- requireFinite$8(uMax, "domain.uMax");
8020
- requireFinite$8(vMin, "domain.vMin");
8021
- requireFinite$8(vMax, "domain.vMax");
8774
+ requireFinite$9(uMin, "domain.uMin");
8775
+ requireFinite$9(uMax, "domain.uMax");
8776
+ requireFinite$9(vMin, "domain.vMin");
8777
+ requireFinite$9(vMax, "domain.vMax");
8022
8778
  if (uMin < 0 || uMax > 1 || vMin < 0 || vMax > 1) {
8023
8779
  throw new Error("nurbsSurface: domain bounds must stay within [0, 1]");
8024
8780
  }
@@ -8030,8 +8786,8 @@ function normalizeSurfaceDomain(domain) {
8030
8786
  function normalizeTrimLoop(loop, label) {
8031
8787
  if (loop.length < 3) throw new Error(`nurbsSurface: ${label} requires at least 3 points`);
8032
8788
  const normalized = loop.map(([u2, v], idx) => {
8033
- requireFinite$8(u2, `${label}[${idx}][0]`);
8034
- requireFinite$8(v, `${label}[${idx}][1]`);
8789
+ requireFinite$9(u2, `${label}[${idx}][0]`);
8790
+ requireFinite$9(v, `${label}[${idx}][1]`);
8035
8791
  if (u2 < 0 || u2 > 1 || v < 0 || v > 1) throw new Error(`nurbsSurface: ${label}[${idx}] must stay within [0, 1]`);
8036
8792
  return [u2, v];
8037
8793
  });
@@ -8054,8 +8810,8 @@ function normalizeTrimCurve(curve, label) {
8054
8810
  throw new Error(`nurbsSurface: ${label} needs at least ${degree + 1} control points for degree=${degree}`);
8055
8811
  }
8056
8812
  const normalizedControlPoints = controlPoints.map(([u2, v], idx) => {
8057
- requireFinite$8(u2, `${label}.controlPoints[${idx}][0]`);
8058
- requireFinite$8(v, `${label}.controlPoints[${idx}][1]`);
8813
+ requireFinite$9(u2, `${label}.controlPoints[${idx}][0]`);
8814
+ requireFinite$9(v, `${label}.controlPoints[${idx}][1]`);
8059
8815
  if (u2 < 0 || u2 > 1 || v < 0 || v > 1) {
8060
8816
  throw new Error(`nurbsSurface: ${label}.controlPoints[${idx}] must stay within [0, 1]`);
8061
8817
  }
@@ -8066,7 +8822,7 @@ function normalizeTrimCurve(curve, label) {
8066
8822
  throw new Error(`nurbsSurface: ${label}.weights length must match controlPoints length`);
8067
8823
  }
8068
8824
  for (let idx = 0; idx < weights.length; idx += 1) {
8069
- requireFinite$8(weights[idx], `${label}.weights[${idx}]`);
8825
+ requireFinite$9(weights[idx], `${label}.weights[${idx}]`);
8070
8826
  if (weights[idx] <= 0) throw new Error(`nurbsSurface: ${label}.weights[${idx}] must be > 0`);
8071
8827
  }
8072
8828
  const knots = curve.knots ?? generateClampedKnots(controlPoints.length, degree);
@@ -8074,7 +8830,7 @@ function normalizeTrimCurve(curve, label) {
8074
8830
  throw new Error(`nurbsSurface: ${label}.knots.length should be ${controlPoints.length + degree + 1}, got ${knots.length}`);
8075
8831
  }
8076
8832
  for (let idx = 0; idx < knots.length; idx += 1) {
8077
- requireFinite$8(knots[idx], `${label}.knots[${idx}]`);
8833
+ requireFinite$9(knots[idx], `${label}.knots[${idx}]`);
8078
8834
  if (idx > 0 && knots[idx] < knots[idx - 1]) throw new Error(`nurbsSurface: ${label}.knots must be non-decreasing`);
8079
8835
  }
8080
8836
  if (knots[degree] >= knots[controlPoints.length]) {
@@ -8254,16 +9010,16 @@ class NurbsSurface {
8254
9010
  for (let i = 0; i < nU; i++) {
8255
9011
  if (controlGrid[i].length !== nV) throw new Error(`nurbsSurface: row ${i} has ${controlGrid[i].length} points, expected ${nV}`);
8256
9012
  for (let j = 0; j < nV; j++) {
8257
- requireFinite$8(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
8258
- requireFinite$8(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
8259
- requireFinite$8(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
9013
+ requireFinite$9(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
9014
+ requireFinite$9(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
9015
+ requireFinite$9(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
8260
9016
  }
8261
9017
  }
8262
9018
  const weightsGrid = options.weights ?? controlGrid.map((row) => row.map(() => 1));
8263
9019
  for (let i = 0; i < nU; i++) {
8264
9020
  if (weightsGrid[i].length !== nV) throw new Error(`nurbsSurface: weights row ${i} length mismatch`);
8265
9021
  for (let j = 0; j < nV; j++) {
8266
- requireFinite$8(weightsGrid[i][j], `weights[${i}][${j}]`);
9022
+ requireFinite$9(weightsGrid[i][j], `weights[${i}][${j}]`);
8267
9023
  if (weightsGrid[i][j] <= 0) throw new Error(`nurbsSurface: weights[${i}][${j}] must be > 0`);
8268
9024
  }
8269
9025
  }
@@ -9468,6 +10224,7 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
9468
10224
  edgeLength: options.edgeLength
9469
10225
  };
9470
10226
  }
10227
+ const EPS$9 = 1e-9;
9471
10228
  function resamplePolygon(poly, targetCount) {
9472
10229
  if (poly.length < 2) return poly;
9473
10230
  if (targetCount <= 0) return [];
@@ -9505,6 +10262,78 @@ function resamplePolygon(poly, targetCount) {
9505
10262
  }
9506
10263
  return out;
9507
10264
  }
10265
+ function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid$2(poly)) {
10266
+ if (poly.length < 3 || targetCount <= 0) return null;
10267
+ if (!isConvexPolygon(poly)) return null;
10268
+ const out = [];
10269
+ for (let index2 = 0; index2 < targetCount; index2 += 1) {
10270
+ const angle = index2 / targetCount * Math.PI * 2;
10271
+ const point2 = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
10272
+ if (!point2) return null;
10273
+ out.push(point2);
10274
+ }
10275
+ return out;
10276
+ }
10277
+ function rayPolygonIntersection(origin, direction2, poly) {
10278
+ let bestT = Infinity;
10279
+ let best = null;
10280
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10281
+ const a2 = poly[index2];
10282
+ const b = poly[(index2 + 1) % poly.length];
10283
+ const edge = [b[0] - a2[0], b[1] - a2[1]];
10284
+ const denom = cross$7(direction2, edge);
10285
+ if (Math.abs(denom) < EPS$9) continue;
10286
+ const delta = [a2[0] - origin[0], a2[1] - origin[1]];
10287
+ const rayT = cross$7(delta, edge) / denom;
10288
+ const edgeT = cross$7(delta, direction2) / denom;
10289
+ if (rayT >= -EPS$9 && edgeT >= -EPS$9 && edgeT <= 1 + EPS$9 && rayT < bestT) {
10290
+ bestT = rayT;
10291
+ best = [origin[0] + direction2[0] * rayT, origin[1] + direction2[1] * rayT];
10292
+ }
10293
+ }
10294
+ return best;
10295
+ }
10296
+ function polygonCentroid$2(poly) {
10297
+ let area2 = 0;
10298
+ let cx = 0;
10299
+ let cy = 0;
10300
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10301
+ const a2 = poly[index2];
10302
+ const b = poly[(index2 + 1) % poly.length];
10303
+ const crossValue = cross$7(a2, b);
10304
+ area2 += crossValue;
10305
+ cx += (a2[0] + b[0]) * crossValue;
10306
+ cy += (a2[1] + b[1]) * crossValue;
10307
+ }
10308
+ if (Math.abs(area2) < EPS$9) return averagePoint(poly);
10309
+ return [cx / (3 * area2), cy / (3 * area2)];
10310
+ }
10311
+ function averagePoint(poly) {
10312
+ let x2 = 0;
10313
+ let y2 = 0;
10314
+ for (const point2 of poly) {
10315
+ x2 += point2[0];
10316
+ y2 += point2[1];
10317
+ }
10318
+ return [x2 / poly.length, y2 / poly.length];
10319
+ }
10320
+ function isConvexPolygon(poly) {
10321
+ let sign2 = 0;
10322
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10323
+ const a2 = poly[index2];
10324
+ const b = poly[(index2 + 1) % poly.length];
10325
+ const c2 = poly[(index2 + 2) % poly.length];
10326
+ const turn = cross$7([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
10327
+ if (Math.abs(turn) < EPS$9) continue;
10328
+ const currentSign = Math.sign(turn);
10329
+ if (sign2 !== 0 && currentSign !== sign2) return false;
10330
+ sign2 = currentSign;
10331
+ }
10332
+ return sign2 !== 0;
10333
+ }
10334
+ function cross$7(a2, b) {
10335
+ return a2[0] * b[1] - a2[1] * b[0];
10336
+ }
9508
10337
  function loftStitched(profiles2, heights, wasm) {
9509
10338
  if (profiles2.length < 2) return null;
9510
10339
  const classified = profiles2.map((loops) => classifyLoops(loops));
@@ -9633,8 +10462,10 @@ function stitchSingleLoopLoft(loops, heights, wasm) {
9633
10462
  maxPoints = Math.max(maxPoints, loop.length);
9634
10463
  }
9635
10464
  const N = Math.max(maxPoints, 24);
10465
+ const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
10466
+ const useAngularSamples = angularSamples.every((samples) => samples != null);
9636
10467
  const resampled = normalizedLoops.map((loop, i) => {
9637
- const pts2d = resamplePolygon(loop, N);
10468
+ const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
9638
10469
  const z2 = heights[i];
9639
10470
  return pts2d.map(([x2, y2]) => [x2, y2, z2]);
9640
10471
  });
@@ -9653,8 +10484,8 @@ function stitchSingleLoopLoft(loops, heights, wasm) {
9653
10484
  const v0 = baseIdx + j;
9654
10485
  const v1 = nextIdx + j;
9655
10486
  const v2 = nextIdx + j1;
9656
- const v3 = baseIdx + j1;
9657
- triangles.push(v0, v3, v2);
10487
+ const v32 = baseIdx + j1;
10488
+ triangles.push(v0, v32, v2);
9658
10489
  triangles.push(v0, v2, v1);
9659
10490
  }
9660
10491
  }
@@ -9695,7 +10526,7 @@ let _wasm$1 = null;
9695
10526
  async function initManifoldWasm() {
9696
10527
  if (_wasm$1) return _wasm$1;
9697
10528
  performance.mark("manifold:start");
9698
- const Module = (await import("./manifold-CwDdMKyc.js")).default;
10529
+ const Module = (await import("./manifold-B9QSr-qP.js")).default;
9699
10530
  performance.mark("manifold:imported");
9700
10531
  const wasm = await Module();
9701
10532
  wasm.setup();
@@ -9965,8 +10796,8 @@ function stitchLoopAlongPath(loop, _path, frames, wasm) {
9965
10796
  const v0 = baseIdx + j;
9966
10797
  const v1 = nextIdx + j;
9967
10798
  const v2 = nextIdx + j1;
9968
- const v3 = baseIdx + j1;
9969
- triangles.push(v0, v3, v2);
10799
+ const v32 = baseIdx + j1;
10800
+ triangles.push(v0, v32, v2);
9970
10801
  triangles.push(v0, v2, v1);
9971
10802
  }
9972
10803
  }
@@ -11312,8 +12143,16 @@ function lowerShapeCompilePlanToManifold(plan, wasm) {
11312
12143
  case "importedMesh":
11313
12144
  return lowerImportedMeshToManifold(plan.fileData, plan.format, plan.filePath, wasm);
11314
12145
  case "sdf": {
11315
- const evalFn = compileSdfNode3(plan.tree);
11316
- return lowerSdfToManifold(evalFn, plan.bounds, plan.edgeLength, wasm, plan.meshing);
12146
+ const evaluator = compileSdfMaterializationEvaluator3(plan.tree);
12147
+ return lowerSdfToManifold(
12148
+ evaluator.fn,
12149
+ plan.bounds,
12150
+ plan.edgeLength,
12151
+ wasm,
12152
+ plan.meshing,
12153
+ evaluator.engine,
12154
+ evaluator.unsupportedReason
12155
+ );
11317
12156
  }
11318
12157
  case "fromSlices":
11319
12158
  return lowerFromSlicesToManifold(plan, wasm);
@@ -11331,8 +12170,12 @@ function lowerShapeCompilePlanToManifold(plan, wasm) {
11331
12170
  assertExhaustive(plan);
11332
12171
  }
11333
12172
  }
11334
- function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing) {
12173
+ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing, evaluatorEngine, evaluatorUnsupportedReason) {
11335
12174
  const diagnostics = (meshing == null ? void 0 : meshing.diagnostics) ? { ...meshing.diagnostics } : void 0;
12175
+ if (diagnostics && evaluatorEngine) {
12176
+ diagnostics.evaluator = evaluatorEngine;
12177
+ if (evaluatorUnsupportedReason) diagnostics.evaluatorUnsupportedReason = evaluatorUnsupportedReason;
12178
+ }
11336
12179
  const inset = edgeLength2;
11337
12180
  const cappedEvalFn = (x2, y2, z2) => {
11338
12181
  const bx = Math.max(bounds.min[0] + inset - x2, x2 - bounds.max[0] + inset);
@@ -16783,9 +17626,9 @@ function requireClipper() {
16783
17626
  if (ClipperLib2.use_xyz) j.OffPt.Z = OffPt.Z;
16784
17627
  this.m_Joins.push(j);
16785
17628
  };
16786
- ClipperLib2.Clipper.prototype.AddGhostJoin = function(Op, OffPt) {
17629
+ ClipperLib2.Clipper.prototype.AddGhostJoin = function(Op2, OffPt) {
16787
17630
  var j = new ClipperLib2.Join();
16788
- j.OutPt1 = Op;
17631
+ j.OutPt1 = Op2;
16789
17632
  j.OffPt.X = OffPt.X;
16790
17633
  j.OffPt.Y = OffPt.Y;
16791
17634
  if (ClipperLib2.use_xyz) j.OffPt.Z = OffPt.Z;
@@ -19687,7 +20530,7 @@ function requireClipper() {
19687
20530
  }
19688
20531
  var clipperExports = requireClipper();
19689
20532
  var ClipperLib = /* @__PURE__ */ getDefaultExportFromCjs(clipperExports);
19690
- let f$1 = class f {
20533
+ let f$3 = class f {
19691
20534
  constructor(t, e) {
19692
20535
  this.next = null, this.key = t, this.data = e, this.left = null, this.right = null;
19693
20536
  }
@@ -19696,7 +20539,7 @@ function d(n, t) {
19696
20539
  return n > t ? 1 : n < t ? -1 : 0;
19697
20540
  }
19698
20541
  function u$1(n, t, e) {
19699
- const r = new f$1(null, null);
20542
+ const r = new f$3(null, null);
19700
20543
  let l = r, i = r;
19701
20544
  for (; ; ) {
19702
20545
  const o = e(n, t.key);
@@ -19719,7 +20562,7 @@ function u$1(n, t, e) {
19719
20562
  return l.right = t.left, i.left = t.right, t.left = r.right, t.right = r.left, t;
19720
20563
  }
19721
20564
  function c(n, t, e, r) {
19722
- const l = new f$1(n, t);
20565
+ const l = new f$3(n, t);
19723
20566
  if (e === null)
19724
20567
  return l.left = l.right = null, l;
19725
20568
  e = u$1(n, e, r);
@@ -19760,7 +20603,7 @@ class z {
19760
20603
  * Adds a key, if it is not present in the tree
19761
20604
  */
19762
20605
  add(t, e) {
19763
- const r = new f$1(t, e);
20606
+ const r = new f$3(t, e);
19764
20607
  this._root === null && (r.left = r.right = null, this._size++, this._root = r);
19765
20608
  const l = this._comparator, i = u$1(t, this._root, l), o = l(t, i.key);
19766
20609
  return o === 0 ? this._root = i : (o < 0 ? (r.left = i.left, r.right = i, i.left = null) : o > 0 && (r.right = i.right, r.left = i, i.right = null), this._size++, this._root = r), this._root;
@@ -19973,23 +20816,23 @@ class z {
19973
20816
  function a(n, t, e, r) {
19974
20817
  const l = r - e;
19975
20818
  if (l > 0) {
19976
- const i = e + Math.floor(l / 2), o = n[i], s = t[i], h = new f$1(o, s);
20819
+ const i = e + Math.floor(l / 2), o = n[i], s = t[i], h = new f$3(o, s);
19977
20820
  return h.left = a(n, t, e, i), h.right = a(n, t, i + 1, r), h;
19978
20821
  }
19979
20822
  return null;
19980
20823
  }
19981
20824
  function x(n, t) {
19982
- const e = new f$1(null, null);
20825
+ const e = new f$3(null, null);
19983
20826
  let r = e;
19984
20827
  for (let l = 0; l < n.length; l++)
19985
- r = r.next = new f$1(n[l], t[l]);
20828
+ r = r.next = new f$3(n[l], t[l]);
19986
20829
  return r.next = null, e.next;
19987
20830
  }
19988
20831
  function k(n) {
19989
20832
  let t = n;
19990
20833
  const e = [];
19991
20834
  let r = false;
19992
- const l = new f$1(null, null);
20835
+ const l = new f$3(null, null);
19993
20836
  let i = l;
19994
20837
  for (; !r; )
19995
20838
  t ? (e.push(t), t = t.left) : e.length > 0 ? (t = i = i.next = e.pop(), t = t.right) : r = true;
@@ -20004,7 +20847,7 @@ function p(n, t, e) {
20004
20847
  return null;
20005
20848
  }
20006
20849
  function y(n, t, e) {
20007
- const r = new f$1(null, null);
20850
+ const r = new f$3(null, null);
20008
20851
  let l = r, i = n, o = t;
20009
20852
  for (; i !== null && o !== null; )
20010
20853
  e(i.key, o.key) < 0 ? (l.next = i, i = i.next) : (l.next = o, o = o.next), l = l.next;
@@ -24992,7 +25835,13 @@ function normalizeTruckShapeForBooleanInput(shape) {
24992
25835
  return normalized;
24993
25836
  }
24994
25837
  function lowerSdfPlan(plan) {
24995
- const evalFn = compileSdfNode3(plan.tree);
25838
+ var _a3, _b3, _c2;
25839
+ const evaluator = compileSdfMaterializationEvaluator3(plan.tree);
25840
+ if ((_a3 = plan.meshing) == null ? void 0 : _a3.diagnostics) {
25841
+ plan.meshing.diagnostics.evaluator = evaluator.engine;
25842
+ if (evaluator.unsupportedReason) plan.meshing.diagnostics.evaluatorUnsupportedReason = evaluator.unsupportedReason;
25843
+ }
25844
+ const evalFn = evaluator.fn;
24996
25845
  const inset = plan.edgeLength;
24997
25846
  const cappedEvalFn = (x2, y2, z2) => {
24998
25847
  const bx = Math.max(plan.bounds.min[0] + inset - x2, x2 - plan.bounds.max[0] + inset);
@@ -25004,14 +25853,18 @@ function lowerSdfPlan(plan) {
25004
25853
  assertSdfMeshBudget(mesh, plan);
25005
25854
  let surfaceNetsError;
25006
25855
  try {
25007
- return lowerExtractedSdfMesh(mesh, cappedEvalFn, true);
25856
+ const shape = lowerExtractedSdfMesh(mesh, cappedEvalFn, true);
25857
+ if ((_b3 = plan.meshing) == null ? void 0 : _b3.diagnostics) logSdfMeshingDiagnostics("SDF meshing result", plan.meshing.diagnostics);
25858
+ return shape;
25008
25859
  } catch (error) {
25009
25860
  surfaceNetsError = error;
25010
25861
  }
25011
25862
  const tetraMesh = marchingTetrahedra(cappedEvalFn, plan.bounds, plan.edgeLength);
25012
25863
  assertSdfMeshBudget(tetraMesh, plan);
25013
25864
  try {
25014
- return lowerExtractedSdfMesh(tetraMesh, cappedEvalFn, false);
25865
+ const shape = lowerExtractedSdfMesh(tetraMesh, cappedEvalFn, false);
25866
+ if ((_c2 = plan.meshing) == null ? void 0 : _c2.diagnostics) logSdfMeshingDiagnostics("SDF meshing result", plan.meshing.diagnostics);
25867
+ return shape;
25015
25868
  } catch (error) {
25016
25869
  throw new Error(
25017
25870
  `Truck backend does not support compile plan "sdf" for this materialized field yet: Surface Nets failed with ${surfaceNetsError instanceof Error ? surfaceNetsError.message : String(surfaceNetsError)}; marching tetrahedra failed with ${error instanceof Error ? error.message : String(error)}`
@@ -25375,7 +26228,9 @@ function lowerOffsetSolidPlan(plan) {
25375
26228
  if (base.kind === "transform") {
25376
26229
  return lowerTransformedOffsetSolidPlan(base, plan.thickness);
25377
26230
  }
25378
- return truckUnsupported(`compile plan "${plan.kind}" for non-vertical-prism/non-revolved/non-loft/non-straight-sweep/non-straight-variable-sweep solids`);
26231
+ return truckUnsupported(
26232
+ `compile plan "${plan.kind}" for non-vertical-prism/non-revolved/non-loft/non-straight-sweep/non-straight-variable-sweep solids`
26233
+ );
25379
26234
  }
25380
26235
  function lowerLoftPlan(plan) {
25381
26236
  return wrapTruckShapeBackend(
@@ -32677,6 +33532,37 @@ function mergeSketchPlacementModel(sketches) {
32677
33532
  }
32678
33533
  return first;
32679
33534
  }
33535
+ function normalizeSceneTags(value, label = "tags") {
33536
+ if (value == null) return [];
33537
+ const rawTags = typeof value === "string" ? [value] : value;
33538
+ if (!Array.isArray(rawTags)) {
33539
+ throw new Error(`${label} must be a string or array of strings`);
33540
+ }
33541
+ const out = [];
33542
+ const seen = /* @__PURE__ */ new Set();
33543
+ rawTags.forEach((tag, index2) => {
33544
+ if (typeof tag !== "string") {
33545
+ throw new Error(`${label}[${index2}] must be a string`);
33546
+ }
33547
+ const trimmed = tag.trim();
33548
+ if (!trimmed || seen.has(trimmed)) return;
33549
+ seen.add(trimmed);
33550
+ out.push(trimmed);
33551
+ });
33552
+ return out;
33553
+ }
33554
+ function mergeSceneTags(...values) {
33555
+ const out = [];
33556
+ const seen = /* @__PURE__ */ new Set();
33557
+ values.forEach((value) => {
33558
+ normalizeSceneTags(value).forEach((tag) => {
33559
+ if (seen.has(tag)) return;
33560
+ seen.add(tag);
33561
+ out.push(tag);
33562
+ });
33563
+ });
33564
+ return out;
33565
+ }
32680
33566
  const _groupPlacementRefs = /* @__PURE__ */ new WeakMap();
32681
33567
  const _groupExplodeHint = /* @__PURE__ */ new WeakMap();
32682
33568
  function getGroupRefs(g2) {
@@ -32700,7 +33586,7 @@ function transformGroupRefs(source, dest, matrix) {
32700
33586
  }
32701
33587
  return dest;
32702
33588
  }
32703
- function requireFiniteAngle(v, method) {
33589
+ function requireFiniteAngle$1(v, method) {
32704
33590
  if (typeof v !== "number" || !Number.isFinite(v))
32705
33591
  throw new Error(`${method} angleDeg must be a finite number, got ${typeof v === "number" ? v : typeof v}`);
32706
33592
  }
@@ -32761,31 +33647,46 @@ function resolveNamedGroupChild(item) {
32761
33647
  function normalizeGroupInputs(items) {
32762
33648
  const children = [];
32763
33649
  const childNames = [];
33650
+ const childTags = [];
32764
33651
  items.forEach((item) => {
32765
33652
  if (isNamedGroupChild(item)) {
32766
33653
  children.push(resolveNamedGroupChild(item));
32767
33654
  childNames.push(normalizeChildName(item.name));
33655
+ childTags.push(normalizeSceneTags(item.tags, `group(...) named item "${item.name}" tags`));
32768
33656
  return;
32769
33657
  }
32770
33658
  children.push(item);
32771
33659
  childNames.push(void 0);
33660
+ childTags.push([]);
32772
33661
  });
32773
- return { children, childNames };
33662
+ return { children, childNames, childTags };
32774
33663
  }
32775
33664
  class ShapeGroup {
32776
- constructor(children, childNames) {
33665
+ constructor(children, childNames, childTags) {
32777
33666
  __publicField(this, "children");
32778
33667
  __publicField(this, "childNames");
33668
+ __publicField(this, "childTags");
32779
33669
  if (childNames && childNames.length !== children.length) {
32780
33670
  throw new Error("ShapeGroup childNames must match children length");
32781
33671
  }
33672
+ if (childTags && childTags.length !== children.length) {
33673
+ throw new Error("ShapeGroup childTags must match children length");
33674
+ }
32782
33675
  this.children = [...children];
32783
33676
  this.childNames = this.children.map((_2, index2) => normalizeChildName(childNames == null ? void 0 : childNames[index2]));
33677
+ this.childTags = this.children.map((_2, index2) => normalizeSceneTags(childTags == null ? void 0 : childTags[index2], "ShapeGroup childTags"));
32784
33678
  }
32785
33679
  /** Return the optional name of the child at `index`. */
32786
33680
  childName(index2) {
32787
33681
  return this.childNames[index2];
32788
33682
  }
33683
+ /**
33684
+ * Return tags attached to the child at `index`.
33685
+ * @internal
33686
+ */
33687
+ tagsForChild(index2) {
33688
+ return [...this.childTags[index2] ?? []];
33689
+ }
32789
33690
  /**
32790
33691
  * Return the named child by name. Throws if not found.
32791
33692
  * Useful when importing a multipart group and working on components individually.
@@ -32800,13 +33701,13 @@ class ShapeGroup {
32800
33701
  }
32801
33702
  /** Apply fn to all children, producing a new ShapeGroup that also copies placement refs. */
32802
33703
  mapChildren(fn) {
32803
- const next = new ShapeGroup(this.children.map(fn), this.childNames);
33704
+ const next = new ShapeGroup(this.children.map(fn), this.childNames, this.childTags);
32804
33705
  copyGroupPorts(this, next);
32805
33706
  return copyGroupRefs(this, next);
32806
33707
  }
32807
33708
  /** Apply fn to all children and also transform placement refs by the given matrix. */
32808
33709
  mapChildrenTransform(fn, matrix) {
32809
- const next = new ShapeGroup(this.children.map(fn), this.childNames);
33710
+ const next = new ShapeGroup(this.children.map(fn), this.childNames, this.childTags);
32810
33711
  transformGroupPortsHelper(this, next, matrix);
32811
33712
  return transformGroupRefs(this, next, matrix);
32812
33713
  }
@@ -32933,25 +33834,25 @@ class ShapeGroup {
32933
33834
  /** Rotate the group around an arbitrary axis through the origin. */
32934
33835
  rotate(axis, angleDeg, options) {
32935
33836
  requireRotateAxis(axis, "ShapeGroup.rotate()");
32936
- requireFiniteAngle(angleDeg, "ShapeGroup.rotate()");
33837
+ requireFiniteAngle$1(angleDeg, "ShapeGroup.rotate()");
32937
33838
  if (options == null ? void 0 : options.pivot) requireVec3Pivot(options.pivot, "ShapeGroup.rotate()");
32938
33839
  return this.rotateAroundAxis(axis, angleDeg, options == null ? void 0 : options.pivot);
32939
33840
  }
32940
33841
  /** Rotate the group around the X axis. */
32941
33842
  rotateX(angleDeg, options) {
32942
- requireFiniteAngle(angleDeg, "ShapeGroup.rotateX()");
33843
+ requireFiniteAngle$1(angleDeg, "ShapeGroup.rotateX()");
32943
33844
  if (options == null ? void 0 : options.pivot) requireVec3Pivot(options.pivot, "ShapeGroup.rotateX()");
32944
33845
  return this.rotateAroundAxis([1, 0, 0], angleDeg, options == null ? void 0 : options.pivot);
32945
33846
  }
32946
33847
  /** Rotate the group around the Y axis. */
32947
33848
  rotateY(angleDeg, options) {
32948
- requireFiniteAngle(angleDeg, "ShapeGroup.rotateY()");
33849
+ requireFiniteAngle$1(angleDeg, "ShapeGroup.rotateY()");
32949
33850
  if (options == null ? void 0 : options.pivot) requireVec3Pivot(options.pivot, "ShapeGroup.rotateY()");
32950
33851
  return this.rotateAroundAxis([0, 1, 0], angleDeg, options == null ? void 0 : options.pivot);
32951
33852
  }
32952
33853
  /** Rotate the group around the Z axis. */
32953
33854
  rotateZ(angleDeg, options) {
32954
- requireFiniteAngle(angleDeg, "ShapeGroup.rotateZ()");
33855
+ requireFiniteAngle$1(angleDeg, "ShapeGroup.rotateZ()");
32955
33856
  if (options == null ? void 0 : options.pivot) requireVec3Pivot(options.pivot, "ShapeGroup.rotateZ()");
32956
33857
  return this.rotateAroundAxis([0, 0, 1], angleDeg, options == null ? void 0 : options.pivot);
32957
33858
  }
@@ -32994,7 +33895,8 @@ class ShapeGroup {
32994
33895
  "ShapeGroup.transform only supports 3D children (Shape/ShapeGroup). For Sketch children, use 2D transforms (translate/rotate/scale/mirror)."
32995
33896
  );
32996
33897
  }),
32997
- this.childNames
33898
+ this.childNames,
33899
+ this.childTags
32998
33900
  );
32999
33901
  transformGroupPortsHelper(this, next, matrix);
33000
33902
  return transformGroupRefs(this, next, matrix);
@@ -33062,7 +33964,7 @@ class ShapeGroup {
33062
33964
  * ```
33063
33965
  */
33064
33966
  withReferences(refs) {
33065
- const next = new ShapeGroup(this.children, this.childNames);
33967
+ const next = new ShapeGroup(this.children, this.childNames, this.childTags);
33066
33968
  const merged = applyPlacementReferenceInput(getGroupRefs(this), refs);
33067
33969
  return setGroupRefs(next, merged);
33068
33970
  }
@@ -33130,7 +34032,7 @@ class ShapeGroup {
33130
34032
  /** Attach named connectors — attachment points that survive transforms.
33131
34033
  * Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching). */
33132
34034
  withConnectors(connectors) {
33133
- const next = new ShapeGroup(this.children, this.childNames);
34035
+ const next = new ShapeGroup(this.children, this.childNames, this.childTags);
33134
34036
  copyGroupRefs(this, next);
33135
34037
  const existing = getGroupPorts(this);
33136
34038
  const incoming = normalizeConnectorMapInput(connectors);
@@ -33180,7 +34082,7 @@ class ShapeGroup {
33180
34082
  }
33181
34083
  function group(...items) {
33182
34084
  const normalized = normalizeGroupInputs(items);
33183
- return new ShapeGroup(normalized.children, normalized.childNames);
34085
+ return new ShapeGroup(normalized.children, normalized.childNames, normalized.childTags);
33184
34086
  }
33185
34087
  function getTargetPortsForGroup(target) {
33186
34088
  if (target instanceof Shape) {
@@ -35355,7 +36257,7 @@ function buildSdfFunctionDefinition(source, options) {
35355
36257
  jsExpression: expression,
35356
36258
  ...shader.ok ? { shaderExpression: shader.expression } : { shaderUnsupportedReason: shader.reason },
35357
36259
  raymarchStepLimit: resolveRaymarchStepLimit(options.bounds, options.maxStep),
35358
- ...options.lipschitz !== void 0 ? { raymarchLipschitz: requirePositiveFinite$1(options.lipschitz, "sdf.fromFunction() lipschitz") } : {}
36260
+ ...options.lipschitz !== void 0 ? { raymarchLipschitz: requirePositiveFinite$2(options.lipschitz, "sdf.fromFunction() lipschitz") } : {}
35359
36261
  };
35360
36262
  }
35361
36263
  function extractSdfExpression(source) {
@@ -35523,7 +36425,7 @@ function formatNumericLiteralsForGlsl(source) {
35523
36425
  return result;
35524
36426
  }
35525
36427
  function resolveRaymarchStepLimit(bounds, maxStep) {
35526
- if (maxStep !== void 0) return requirePositiveFinite$1(maxStep, "sdf.fromFunction() maxStep");
36428
+ if (maxStep !== void 0) return requirePositiveFinite$2(maxStep, "sdf.fromFunction() maxStep");
35527
36429
  const dx = bounds.max[0] - bounds.min[0];
35528
36430
  const dy = bounds.max[1] - bounds.min[1];
35529
36431
  const dz = bounds.max[2] - bounds.min[2];
@@ -35531,7 +36433,7 @@ function resolveRaymarchStepLimit(bounds, maxStep) {
35531
36433
  if (!Number.isFinite(diagonal) || diagonal <= 0) return 0.1;
35532
36434
  return Math.max(0.025, Math.min(0.5, diagonal / 240));
35533
36435
  }
35534
- function requirePositiveFinite$1(value, label) {
36436
+ function requirePositiveFinite$2(value, label) {
35535
36437
  if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive finite number.`);
35536
36438
  return value;
35537
36439
  }
@@ -35558,6 +36460,199 @@ class SurfacePattern {
35558
36460
  this.constants = constants;
35559
36461
  }
35560
36462
  }
36463
+ const typedSurfacePatterns = /* @__PURE__ */ new WeakMap();
36464
+ function getTypedSurfacePattern(pattern) {
36465
+ return typedSurfacePatterns.get(pattern);
36466
+ }
36467
+ class Pattern2D extends SurfacePattern {
36468
+ constructor(body) {
36469
+ super(body);
36470
+ }
36471
+ /** Add this pattern to one or more patterns or constant height offsets. */
36472
+ add(...patterns) {
36473
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
36474
+ }
36475
+ /** Subtract another pattern or constant height offset from this pattern. */
36476
+ subtract(pattern) {
36477
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
36478
+ }
36479
+ /** Multiply this pattern by one or more patterns or numeric scale factors. */
36480
+ multiply(...patterns) {
36481
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
36482
+ }
36483
+ /** Keep the lower height between this pattern and one or more other patterns. */
36484
+ min(...patterns) {
36485
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
36486
+ }
36487
+ /** Keep the higher height between this pattern and one or more other patterns. */
36488
+ max(...patterns) {
36489
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
36490
+ }
36491
+ /** Limit pattern height to the inclusive `[min, max]` range in millimeters. */
36492
+ clamp(min2, max2) {
36493
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
36494
+ }
36495
+ /** Convert negative heights to positive heights. */
36496
+ abs() {
36497
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
36498
+ }
36499
+ /** Flip the pattern height sign. */
36500
+ negate() {
36501
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
36502
+ }
36503
+ }
36504
+ class Pattern2DImpl extends Pattern2D {
36505
+ constructor(node) {
36506
+ super(emitSurfacePatternJsExpression(node));
36507
+ __publicField(this, "node");
36508
+ this.node = node;
36509
+ typedSurfacePatterns.set(this, node);
36510
+ }
36511
+ add(...patterns) {
36512
+ return new Pattern2DImpl({ kind: "surfacePattern:add", children: [this.node, ...patterns.map(patternNodeFromInput)] });
36513
+ }
36514
+ subtract(pattern) {
36515
+ return this.add(new Pattern2DImpl({ kind: "surfacePattern:negate", child: patternNodeFromInput(pattern) }));
36516
+ }
36517
+ multiply(...patterns) {
36518
+ return new Pattern2DImpl({ kind: "surfacePattern:multiply", children: [this.node, ...patterns.map(patternNodeFromInput)] });
36519
+ }
36520
+ min(...patterns) {
36521
+ return new Pattern2DImpl({ kind: "surfacePattern:min", children: [this.node, ...patterns.map(patternNodeFromInput)] });
36522
+ }
36523
+ max(...patterns) {
36524
+ return new Pattern2DImpl({ kind: "surfacePattern:max", children: [this.node, ...patterns.map(patternNodeFromInput)] });
36525
+ }
36526
+ clamp(min2, max2) {
36527
+ const lo = requireFinite$8(min2, "Pattern2D.clamp() min");
36528
+ const hi = requireFinite$8(max2, "Pattern2D.clamp() max");
36529
+ if (lo > hi) throw new Error(`Pattern2D.clamp() min must be <= max. Received: ${lo} > ${hi}`);
36530
+ return new Pattern2DImpl({ kind: "surfacePattern:clamp", child: this.node, min: lo, max: hi });
36531
+ }
36532
+ abs() {
36533
+ return new Pattern2DImpl({ kind: "surfacePattern:abs", child: this.node });
36534
+ }
36535
+ negate() {
36536
+ return new Pattern2DImpl({ kind: "surfacePattern:negate", child: this.node });
36537
+ }
36538
+ }
36539
+ class Pattern2DBuilder {
36540
+ /** Create a constant-height pattern in millimeters. */
36541
+ constant(value = 0) {
36542
+ return new Pattern2DImpl({ kind: "surfacePattern:constant", value: requireFinite$8(value, "sdf.pattern2d().constant() value") });
36543
+ }
36544
+ /** Create a sinusoidal wave pattern in UV space. */
36545
+ sineWave(options) {
36546
+ return new Pattern2DImpl({
36547
+ kind: "surfacePattern:sineWave",
36548
+ direction: normalizeDirection$1(options.direction ?? [1, 0], "sdf.pattern2d().sineWave() direction"),
36549
+ wavelength: requirePositiveFinite$1(options.wavelength, "sdf.pattern2d().sineWave() wavelength"),
36550
+ amplitude: requireFinite$8(options.amplitude ?? 1, "sdf.pattern2d().sineWave() amplitude"),
36551
+ phase: requireFinite$8(options.phase ?? 0, "sdf.pattern2d().sineWave() phase"),
36552
+ bias: requireFinite$8(options.bias ?? 0, "sdf.pattern2d().sineWave() bias")
36553
+ });
36554
+ }
36555
+ /** Create recessed stripe bands in UV space. */
36556
+ stripes(options) {
36557
+ return new Pattern2DImpl({
36558
+ kind: "surfacePattern:stripes",
36559
+ direction: normalizeDirection$1(options.direction ?? [1, 0], "sdf.pattern2d().stripes() direction"),
36560
+ spacing: requirePositiveFinite$1(options.spacing, "sdf.pattern2d().stripes() spacing"),
36561
+ width: requirePositiveFinite$1(options.width, "sdf.pattern2d().stripes() width"),
36562
+ depth: requireNonNegativeFinite$1(options.depth ?? 1, "sdf.pattern2d().stripes() depth")
36563
+ });
36564
+ }
36565
+ /** Create an over-under woven relief pattern in UV space. */
36566
+ overUnderWeave(options) {
36567
+ return new Pattern2DImpl({
36568
+ kind: "surfacePattern:overUnderWeave",
36569
+ spacing: normalizeVec2(options.spacing, "sdf.pattern2d().overUnderWeave() spacing", requirePositiveFinite$1),
36570
+ threadWidth: normalizeVec2(options.threadWidth, "sdf.pattern2d().overUnderWeave() threadWidth", requirePositiveFinite$1),
36571
+ depth: requireNonNegativeFinite$1(options.depth ?? 0.8, "sdf.pattern2d().overUnderWeave() depth"),
36572
+ underScale: requireNonNegativeFinite$1(options.underScale ?? 0.15, "sdf.pattern2d().overUnderWeave() underScale")
36573
+ });
36574
+ }
36575
+ }
36576
+ function pattern2d() {
36577
+ return new Pattern2DBuilder();
36578
+ }
36579
+ function patternNodeFromInput(input) {
36580
+ if (input instanceof SurfacePattern) {
36581
+ const node = getTypedSurfacePattern(input);
36582
+ if (node) return node;
36583
+ }
36584
+ if (typeof input === "number") {
36585
+ return { kind: "surfacePattern:constant", value: requireFinite$8(input, "Pattern2D numeric input") };
36586
+ }
36587
+ throw new Error("Pattern2D composition expects another typed Pattern2D or a number.");
36588
+ }
36589
+ function requireFinite$8(value, label) {
36590
+ if (typeof value !== "number" || !Number.isFinite(value)) {
36591
+ throw new Error(`${label} must be a finite number. Received: ${String(value)}`);
36592
+ }
36593
+ return value;
36594
+ }
36595
+ function requirePositiveFinite$1(value, label) {
36596
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
36597
+ throw new Error(`${label} must be a positive finite number. Received: ${String(value)}`);
36598
+ }
36599
+ return value;
36600
+ }
36601
+ function requireNonNegativeFinite$1(value, label) {
36602
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
36603
+ throw new Error(`${label} must be a non-negative finite number. Received: ${String(value)}`);
36604
+ }
36605
+ return value;
36606
+ }
36607
+ function normalizeVec2(value, label, validate) {
36608
+ if (typeof value === "number") {
36609
+ const n = validate(value, label);
36610
+ return [n, n];
36611
+ }
36612
+ return [validate(value[0], `${label}[0]`), validate(value[1], `${label}[1]`)];
36613
+ }
36614
+ function normalizeDirection$1(value, label) {
36615
+ const x2 = requireFinite$8(value[0], `${label}[0]`);
36616
+ const y2 = requireFinite$8(value[1], `${label}[1]`);
36617
+ const length4 = Math.hypot(x2, y2);
36618
+ if (length4 <= 0) throw new Error(`${label} must not be the zero vector.`);
36619
+ return [x2 / length4, y2 / length4];
36620
+ }
36621
+ function f$2(value) {
36622
+ if (!Number.isFinite(value)) return "0";
36623
+ return Number(value.toPrecision(12)).toString();
36624
+ }
36625
+ function emitSurfacePatternJsExpression(node) {
36626
+ switch (node.kind) {
36627
+ case "surfacePattern:constant":
36628
+ return f$2(node.value);
36629
+ case "surfacePattern:sineWave": {
36630
+ const coord = `(u * ${f$2(node.direction[0])} + v * ${f$2(node.direction[1])})`;
36631
+ const phase = `(${coord} * ${f$2(2 * Math.PI / node.wavelength)} + ${f$2(node.phase)})`;
36632
+ return `(${f$2(node.bias)} + Math.sin(${phase}) * ${f$2(node.amplitude)})`;
36633
+ }
36634
+ case "surfacePattern:stripes": {
36635
+ const coord = `(u * ${f$2(node.direction[0])} + v * ${f$2(node.direction[1])})`;
36636
+ return `(function(){var c=${coord};var d=Math.abs(c - Math.round(c / ${f$2(node.spacing)}) * ${f$2(node.spacing)});var p=Math.max(0, 1 - d / ${f$2(node.width * 0.5)});return -(p * p) * ${f$2(node.depth)};})()`;
36637
+ }
36638
+ case "surfacePattern:overUnderWeave":
36639
+ return `(function(){var su=u/${f$2(node.spacing[0])};var sv=v/${f$2(node.spacing[1])};var du=Math.abs(su - Math.round(su))*${f$2(node.spacing[0])};var dv=Math.abs(sv - Math.round(sv))*${f$2(node.spacing[1])};var pU=Math.max(0,1-du/${f$2(node.threadWidth[0] * 0.5)});pU*=pU;var pV=Math.max(0,1-dv/${f$2(node.threadWidth[1] * 0.5)});pV*=pV;var checker=((Math.round(su)&65535)+(Math.round(sv)&65535))&1;var top=checker?pV:pU;var bot=checker?pU:pV;return -Math.max(top,bot*${f$2(node.underScale)})*${f$2(node.depth)};})()`;
36640
+ case "surfacePattern:abs":
36641
+ return `Math.abs(${emitSurfacePatternJsExpression(node.child)})`;
36642
+ case "surfacePattern:negate":
36643
+ return `(-(${emitSurfacePatternJsExpression(node.child)}))`;
36644
+ case "surfacePattern:add":
36645
+ return node.children.length === 0 ? "0" : `(${node.children.map(emitSurfacePatternJsExpression).join(" + ")})`;
36646
+ case "surfacePattern:multiply":
36647
+ return node.children.length === 0 ? "1" : `(${node.children.map(emitSurfacePatternJsExpression).join(" * ")})`;
36648
+ case "surfacePattern:min":
36649
+ return node.children.length === 0 ? "0" : `Math.min(${node.children.map(emitSurfacePatternJsExpression).join(", ")})`;
36650
+ case "surfacePattern:max":
36651
+ return node.children.length === 0 ? "0" : `Math.max(${node.children.map(emitSurfacePatternJsExpression).join(", ")})`;
36652
+ case "surfacePattern:clamp":
36653
+ return `Math.min(${f$2(node.max)}, Math.max(${f$2(node.min)}, ${emitSurfacePatternJsExpression(node.child)}))`;
36654
+ }
36655
+ }
35561
36656
  const SCULPT_MATERIAL_PRESETS = {
35562
36657
  ceramic: {
35563
36658
  color: "#f4f0e6",
@@ -35627,6 +36722,18 @@ function requirePositiveFinite(value, label) {
35627
36722
  }
35628
36723
  return value;
35629
36724
  }
36725
+ function requirePositiveInteger(value, label) {
36726
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
36727
+ throw new Error(`${label} must be a positive integer. Received: ${String(value)}`);
36728
+ }
36729
+ return value;
36730
+ }
36731
+ function requireNonNegativeFinite(value, label) {
36732
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
36733
+ throw new Error(`${label} must be a non-negative finite number. Received: ${String(value)}`);
36734
+ }
36735
+ return value;
36736
+ }
35630
36737
  function resolveBlendRadius(input, label, fallback = 4) {
35631
36738
  if (typeof input === "number") return requirePositiveFinite(input, `${label} radius`);
35632
36739
  if ((input == null ? void 0 : input.radius) !== void 0) return requirePositiveFinite(input.radius, `${label} radius`);
@@ -35873,6 +36980,29 @@ class SdfShape {
35873
36980
  clipBox(x2, y2, z2) {
35874
36981
  return this.intersect(box$1(x2, y2, z2));
35875
36982
  }
36983
+ /** Keep only the material where this shape overlaps another SDF pattern. */
36984
+ fillWith(pattern) {
36985
+ if (!(pattern instanceof SdfShape)) {
36986
+ throw new Error("SdfShape.fillWith() expects an SdfShape pattern, such as sdf.gyroid({ cellSize, wallThickness }).");
36987
+ }
36988
+ return this.intersect(pattern);
36989
+ }
36990
+ /** Keep only the gyroid lattice inside this shape. */
36991
+ fillWithGyroid(options) {
36992
+ return this.fillWith(gyroid(options));
36993
+ }
36994
+ /** Keep only the Schwarz-P lattice inside this shape. */
36995
+ fillWithSchwarzP(options) {
36996
+ return this.fillWith(schwarzP(options));
36997
+ }
36998
+ /** Keep only the diamond TPMS lattice inside this shape. */
36999
+ fillWithDiamond(options) {
37000
+ return this.fillWith(diamond(options));
37001
+ }
37002
+ /** Keep only the lidinoid TPMS lattice inside this shape. */
37003
+ fillWithLidinoid(options) {
37004
+ return this.fillWith(lidinoid(options));
37005
+ }
35876
37006
  /** Smooth union — blends shapes together with a smooth radius. */
35877
37007
  smoothUnion(other, radius) {
35878
37008
  return this.withNode({ kind: "sdf:smoothUnion", children: [this._node, other._node], radius });
@@ -35933,6 +37063,21 @@ class SdfShape {
35933
37063
  repeat(spacing, count) {
35934
37064
  return this.withNode({ kind: "sdf:repeat", child: this._node, spacing, count: count ?? [0, 0, 0] });
35935
37065
  }
37066
+ /**
37067
+ * Arrange this SDF in a circular array around the Z axis.
37068
+ *
37069
+ * The source shape is translated by `offset` in +X before arraying. This uses
37070
+ * angular domain folding, so evaluation stays O(1): the source SDF is sampled
37071
+ * twice no matter how many copies are requested.
37072
+ */
37073
+ circularArray(count, offset2 = 0) {
37074
+ return this.withNode({
37075
+ kind: "sdf:circularArray",
37076
+ child: this._node,
37077
+ count: requirePositiveInteger(count, "SdfShape.circularArray() count"),
37078
+ offset: requireNonNegativeFinite(offset2, "SdfShape.circularArray() offset")
37079
+ });
37080
+ }
35936
37081
  /** Hollow out, keeping only a shell of given thickness. */
35937
37082
  shell(thickness) {
35938
37083
  return this.withNode({ kind: "sdf:shell", child: this._node, thickness });
@@ -35944,8 +37089,8 @@ class SdfShape {
35944
37089
  * // Function displacement
35945
37090
  * shape.displace((x, y, z) => Math.sin(x) * 0.5)
35946
37091
  *
35947
- * // Pattern displacement (e.g. basketWeave)
35948
- * shape.displace(sdf.basketWeave({ threads: 16, spacing: 3 }))
37092
+ * // Pattern displacement from a 3D SDF field
37093
+ * shape.displace(sdf.knurl({ pitch: 2, depth: 0.3 }))
35949
37094
  * ```
35950
37095
  */
35951
37096
  displace(fn, constants) {
@@ -35969,10 +37114,18 @@ class SdfShape {
35969
37114
  * UV coordinates are in **surface millimeters** — patterns defined with `spacing: 3`
35970
37115
  * always produce 3mm spacing, regardless of shape size.
35971
37116
  *
37117
+ * Prefer `sdf.pattern2d()` or built-in surface patterns when the relief should
37118
+ * stay on the native shader and meshing path. Callback functions are supported
37119
+ * for experimentation, but they are opaque to the typed pattern optimizer.
37120
+ *
35972
37121
  * ```js
35973
- * // Surface-following basket weave — auto-detects sphere UV
37122
+ * // Native typed pattern — auto-detects sphere UV
37123
+ * const p = sdf.pattern2d()
37124
+ * const ribs = p.stripes({ spacing: 3, width: 0.8, depth: 0.35 })
37125
+ * .add(p.sineWave({ direction: [0, 1], wavelength: 14, amplitude: 0.08 }))
37126
+ *
35974
37127
  * sdf.sphere(27).shell(3)
35975
- * .surfaceDisplace(sdf.basketWeave({ spacing: 3, depth: 0.8 }))
37128
+ * .surfaceDisplace(ribs)
35976
37129
  * .toShape()
35977
37130
  *
35978
37131
  * // Custom 2D pattern via function
@@ -35982,15 +37135,18 @@ class SdfShape {
35982
37135
  surfaceDisplace(pattern, options) {
35983
37136
  let body;
35984
37137
  let constants;
37138
+ let typedPattern;
35985
37139
  if (pattern instanceof SurfacePattern) {
35986
37140
  body = pattern.body;
35987
37141
  constants = pattern.constants;
37142
+ typedPattern = getTypedSurfacePattern(pattern);
35988
37143
  } else {
35989
37144
  body = extractFunctionBody(pattern);
35990
37145
  }
35991
37146
  return this.withNode({
35992
37147
  kind: "sdf:surfaceDisplace",
35993
37148
  child: this._node,
37149
+ ...typedPattern ? { pattern: typedPattern } : {},
35994
37150
  patternBody: body,
35995
37151
  constants,
35996
37152
  ...(options == null ? void 0 : options.uv) ? { uvMode: options.uv } : {},
@@ -36209,24 +37365,10 @@ function weave(options) {
36209
37365
  });
36210
37366
  }
36211
37367
  function basketWeave(options) {
36212
- const SP = (options == null ? void 0 : options.spacing) ?? 3;
36213
- const TW = (options == null ? void 0 : options.threadWidth) ?? 1.5;
36214
- const D2 = (options == null ? void 0 : options.depth) ?? 0.8;
36215
- const hw = TW * 0.5;
36216
- const body = `(function() {
36217
- var su = u / ${SP};
36218
- var sv = v / ${SP};
36219
- var du = Math.abs(su - Math.round(su)) * ${SP};
36220
- var dv = Math.abs(sv - Math.round(sv)) * ${SP};
36221
- var hw = ${hw};
36222
- var pU = Math.max(0, 1 - du / hw); pU *= pU;
36223
- var pV = Math.max(0, 1 - dv / hw); pV *= pV;
36224
- var checker = ((Math.round(su) & 65535) + (Math.round(sv) & 65535)) & 1;
36225
- var top = checker ? pV : pU;
36226
- var bot = checker ? pU : pV;
36227
- return -(top > bot * 0.15 ? top : bot * 0.15) * ${D2};
36228
- })()`;
36229
- return new SurfacePattern(body);
37368
+ const SP = requirePositiveFinite((options == null ? void 0 : options.spacing) ?? 3, "sdf.basketWeave() spacing");
37369
+ const TW = requirePositiveFinite((options == null ? void 0 : options.threadWidth) ?? 1.5, "sdf.basketWeave() threadWidth");
37370
+ const D2 = requireNonNegativeFinite((options == null ? void 0 : options.depth) ?? 0.8, "sdf.basketWeave() depth");
37371
+ return pattern2d().overUnderWeave({ spacing: SP, threadWidth: TW, depth: D2 });
36230
37372
  }
36231
37373
  function fromFunction(fn, options) {
36232
37374
  if (!options || typeof options !== "object") {
@@ -36259,6 +37401,9 @@ function bend(shape, radius) {
36259
37401
  function repeat(shape, spacing, count) {
36260
37402
  return shape.repeat(spacing, count);
36261
37403
  }
37404
+ function circularArray(shape, count, offset2 = 0) {
37405
+ return shape.circularArray(count, offset2);
37406
+ }
36262
37407
  function resolveTpmsOptions(options) {
36263
37408
  const wallThickness = options.wallThickness;
36264
37409
  const thickness = wallThickness ?? options.thickness;
@@ -36699,12 +37844,16 @@ const sdf = {
36699
37844
  weave,
36700
37845
  /** Basket weave surface pattern — threads with over-under crossings in UV space. Returns a SurfacePattern for use with `.surfaceDisplace()`. */
36701
37846
  basketWeave,
37847
+ /** Create typed, composable 2D surface patterns for `.surfaceDisplace()`. */
37848
+ pattern2d,
36702
37849
  /** Twist an SDF shape around the Z axis. */
36703
37850
  twist,
36704
37851
  /** Bend an SDF shape around the Z axis. */
36705
37852
  bend,
36706
37853
  /** Repeat an SDF shape in space. */
36707
37854
  repeat,
37855
+ /** Arrange an SDF shape in a circular array around the Z axis with O(1) folded-domain evaluation. */
37856
+ circularArray,
36708
37857
  /** A 2D surface pattern — a heightmap function for use with `.surfaceDisplace()`. */
36709
37858
  SurfacePattern,
36710
37859
  /** Create a custom SDF from one expression; shader-safe expressions raymarch directly. */
@@ -41638,13 +42787,16 @@ class SolvedAssembly {
41638
42787
  * @category Assembly
41639
42788
  */
41640
42789
  toGroup() {
42790
+ var _a3;
41641
42791
  const children = [];
41642
42792
  const childNames = [];
41643
- for (const [name] of this.parts) {
42793
+ const childTags = [];
42794
+ for (const [name, rec] of this.parts) {
41644
42795
  children.push(this.getPart(name));
41645
42796
  childNames.push(name);
42797
+ childTags.push(normalizeSceneTags((_a3 = rec.metadata) == null ? void 0 : _a3.tags, `Assembly part "${name}" metadata.tags`));
41646
42798
  }
41647
- return new ShapeGroup(children, childNames);
42799
+ return new ShapeGroup(children, childNames, childTags);
41648
42800
  }
41649
42801
  /**
41650
42802
  * Return an array of named scene objects for the viewport renderer.
@@ -41678,17 +42830,18 @@ class SolvedAssembly {
41678
42830
  const used = usedByPart.get(partName);
41679
42831
  if (used && used.length > 0) markShapePortsUsed(shape, used);
41680
42832
  };
41681
- const appendGroupChildren = (grp, prefix, partName, out2) => {
42833
+ const appendGroupChildren = (grp, prefix, partName, out2, inheritedTags = []) => {
41682
42834
  grp.children.forEach((child, index2) => {
41683
42835
  const childName = grp.childName(index2);
41684
42836
  const label = childName ? `${prefix}.${childName}` : `${prefix}.${index2 + 1}`;
42837
+ const tags = mergeSceneTags(inheritedTags, grp.tagsForChild(index2));
41685
42838
  if (child instanceof ShapeGroup) {
41686
- appendGroupChildren(child, label, partName, out2);
42839
+ appendGroupChildren(child, label, partName, out2, tags);
41687
42840
  return;
41688
42841
  }
41689
42842
  if (child instanceof Shape) {
41690
42843
  markUsedOnShape(child, partName);
41691
- out2.push({ name: label, shape: child });
42844
+ out2.push({ name: label, shape: child, ...tags.length > 0 ? { tags } : {} });
41692
42845
  }
41693
42846
  });
41694
42847
  };
@@ -42054,7 +43207,7 @@ class Assembly {
42054
43207
  *
42055
43208
  * @param name - Unique part name (must not already exist)
42056
43209
  * @param part - The `Shape` or `ShapeGroup` geometry
42057
- * @param options - Optional `{ transform, metadata }` (material, process, qty, etc.)
43210
+ * @param options - Optional `{ transform, metadata }` (material, process, qty, tags, etc.)
42058
43211
  * @returns `this` for chaining
42059
43212
  * @category Assembly
42060
43213
  */
@@ -43008,15 +44161,18 @@ class ImportedAssembly {
43008
44161
  * Any stored placement offset and placement references are forwarded to the group.
43009
44162
  */
43010
44163
  toGroup(state) {
44164
+ var _a3;
43011
44165
  const solved = this._assembly.solve(state);
43012
44166
  const def = this._assembly.describe();
43013
44167
  const children = [];
43014
44168
  const childNames = [];
44169
+ const childTags = [];
43015
44170
  for (const p2 of def.parts) {
43016
44171
  children.push(solved.getPart(p2.name));
43017
44172
  childNames.push(p2.name);
44173
+ childTags.push(normalizeSceneTags((_a3 = p2.metadata) == null ? void 0 : _a3.tags, `Assembly part "${p2.name}" metadata.tags`));
43018
44174
  }
43019
- let result = new ShapeGroup(children, childNames);
44175
+ let result = new ShapeGroup(children, childNames, childTags);
43020
44176
  const [dx, dy, dz] = this._offset;
43021
44177
  if (dx !== 0 || dy !== 0 || dz !== 0) {
43022
44178
  result = result.translate(dx, dy, dz);
@@ -43386,8 +44542,8 @@ function buildPure3mfBuffer(objects, options = {}) {
43386
44542
  for (let t = 0; t < numTri; t++) {
43387
44543
  const v1 = triVerts[t * 3];
43388
44544
  const v2 = triVerts[t * 3 + 1];
43389
- const v3 = triVerts[t * 3 + 2];
43390
- xmlParts.push(` <triangle v1="${v1}" v2="${v2}" v3="${v3}" />`);
44545
+ const v32 = triVerts[t * 3 + 2];
44546
+ xmlParts.push(` <triangle v1="${v1}" v2="${v2}" v3="${v32}" />`);
43391
44547
  }
43392
44548
  xmlParts.push(" </triangles>");
43393
44549
  xmlParts.push(" </mesh>");
@@ -43813,7 +44969,7 @@ class GCodeBuilder {
43813
44969
  this.lines.push("G1 E-0.8 F1800 ; retract");
43814
44970
  this.lines.push("");
43815
44971
  const safeZ = Math.min(maxZ + 5, p2.bedZ - 1);
43816
- this.lines.push(`G1 Z${f2(safeZ)} F900 ; lift nozzle above print`);
44972
+ this.lines.push(`G1 Z${f$1(safeZ)} F900 ; lift nozzle above print`);
43817
44973
  this.lines.push("");
43818
44974
  this.lines.push("M140 S0 ; bed off");
43819
44975
  this.lines.push("M104 S0 ; hotend off");
@@ -43857,7 +45013,7 @@ class GCodeBuilder {
43857
45013
  travelTo(x2, y2, z2) {
43858
45014
  this.retract();
43859
45015
  const from = [...this.pos];
43860
- this.lines.push(`G0 X${f2(x2)} Y${f2(y2)} Z${f2(z2)} F${Math.round(this.profile.travelSpeed)}`);
45016
+ this.lines.push(`G0 X${f$1(x2)} Y${f$1(y2)} Z${f$1(z2)} F${Math.round(this.profile.travelSpeed)}`);
43861
45017
  if (this.posInitialized) {
43862
45018
  this._segments.push({ from, to: [x2, y2, z2], extrude: false, speed: this.profile.travelSpeed });
43863
45019
  }
@@ -43889,7 +45045,7 @@ class GCodeBuilder {
43889
45045
  const beadArea = this.profile.layerHeight * this.profile.nozzle;
43890
45046
  const eIncrement = beadArea * dist4 / this.filamentArea;
43891
45047
  this.e += eIncrement;
43892
- this.lines.push(`G1 X${f2(x2)} Y${f2(y2)} Z${f2(z2)} E${f2(this.e)} F${Math.round(this.currentSpeed)}`);
45048
+ this.lines.push(`G1 X${f$1(x2)} Y${f$1(y2)} Z${f$1(z2)} E${f$1(this.e)} F${Math.round(this.currentSpeed)}`);
43893
45049
  if (this.posInitialized) {
43894
45050
  this._segments.push({ from, to: [x2, y2, z2], extrude: true, speed: this.currentSpeed });
43895
45051
  }
@@ -43987,13 +45143,13 @@ class GCodeBuilder {
43987
45143
  retract() {
43988
45144
  if (this.retracted) return;
43989
45145
  this.e -= this.profile.retractionDistance;
43990
- this.lines.push(`G1 E${f2(this.e)} F${Math.round(this.profile.retractionSpeed)}`);
45146
+ this.lines.push(`G1 E${f$1(this.e)} F${Math.round(this.profile.retractionSpeed)}`);
43991
45147
  this.retracted = true;
43992
45148
  }
43993
45149
  unretract() {
43994
45150
  if (!this.retracted) return;
43995
45151
  this.e += this.profile.retractionDistance;
43996
- this.lines.push(`G1 E${f2(this.e)} F${Math.round(this.profile.retractionSpeed)}`);
45152
+ this.lines.push(`G1 E${f$1(this.e)} F${Math.round(this.profile.retractionSpeed)}`);
43997
45153
  this.retracted = false;
43998
45154
  }
43999
45155
  // ---- Bounds tracking ----
@@ -44064,7 +45220,7 @@ class GCodeBuilder {
44064
45220
  return this.lines.join("\n") + "\n";
44065
45221
  }
44066
45222
  }
44067
- function f2(n) {
45223
+ function f$1(n) {
44068
45224
  return n.toFixed(5).replace(/\.?0+$/, "");
44069
45225
  }
44070
45226
  function bambuModelName(preset) {
@@ -46208,10 +47364,8 @@ class PathBuilder {
46208
47364
  if (radius <= 0) throw new Error("fillet: radius must be positive");
46209
47365
  const n = this.segs.length;
46210
47366
  if (n < 2) throw new Error("fillet: need at least 2 segments before a fillet");
46211
- const prev = this.segs[n - 2];
46212
47367
  const curr = this.segs[n - 1];
46213
- curr.kind === "line" || curr.kind === "move" ? prev.kind === "line" || prev.kind === "move" ? 0 : 0 : 0;
46214
- const { trimA, trimB, arcSeg } = this.computeFilletGeom(radius);
47368
+ const { trimA, arcSeg } = this.computeFilletGeom(radius);
46215
47369
  if (!arcSeg) throw new Error("fillet: cannot fillet these segments (parallel or degenerate)");
46216
47370
  this.trimLastSegEnd(n - 2, trimA[0], trimA[1]);
46217
47371
  const trimmedSeg = { ...curr };
@@ -46283,7 +47437,6 @@ class PathBuilder {
46283
47437
  }
46284
47438
  getSegDirAt(seg, which) {
46285
47439
  if (seg.kind === "line" || seg.kind === "move") {
46286
- this.segs.length;
46287
47440
  const idx = this.segs.indexOf(seg);
46288
47441
  if (seg.kind === "line") {
46289
47442
  let sx, sy;
@@ -46525,6 +47678,41 @@ class PathBuilder {
46525
47678
  }
46526
47679
  return pts;
46527
47680
  }
47681
+ /**
47682
+ * Return the open path as a sampled 2D polyline.
47683
+ *
47684
+ * This is for construction geometry such as guide rails, measured centerlines,
47685
+ * and curve-driven helpers where the authored path should stay open instead of
47686
+ * becoming a filled sketch or stroked profile.
47687
+ *
47688
+ * **Example**
47689
+ *
47690
+ * ```ts
47691
+ * const rail = path()
47692
+ * .moveTo(24, 0)
47693
+ * .bezierTo(32, 44, 28, 92, 18, 120)
47694
+ * .toPolyline();
47695
+ * ```
47696
+ *
47697
+ * @returns A sampled open polyline.
47698
+ * @category Path Builder
47699
+ */
47700
+ toPolyline() {
47701
+ const moveCount = this.segs.filter((seg) => seg.kind === "move").length;
47702
+ if (moveCount > 1) {
47703
+ throw new Error("path().toPolyline() supports one continuous open path. Use separate path() builders for separate rails.");
47704
+ }
47705
+ const pts = [];
47706
+ for (const point2 of this.tessellate()) {
47707
+ if (!point2.every(Number.isFinite)) throw new Error("path().toPolyline() produced a non-finite point");
47708
+ const previous = pts[pts.length - 1];
47709
+ if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) {
47710
+ pts.push(point2);
47711
+ }
47712
+ }
47713
+ if (pts.length < 2) throw new Error("path().toPolyline() needs at least 2 points");
47714
+ return pts;
47715
+ }
46528
47716
  // ── Output ────────────────────────────────────────────────────────────────
46529
47717
  /**
46530
47718
  * Close the path and return a filled `Sketch`.
@@ -47575,7 +48763,8 @@ function explode(items, options = {}) {
47575
48763
  if (child instanceof ShapeGroup) return explodeGroup(child, p2, depth + 1, total, groupCenter, motion.branchDirection);
47576
48764
  return explodeLeaf(child, explodeAdd(total, leafMotion(child, p2, depth + 1, groupCenter, motion.branchDirection).offset));
47577
48765
  }),
47578
- grp.childNames
48766
+ grp.childNames,
48767
+ grp.children.map((_2, i) => grp.tagsForChild(i))
47579
48768
  );
47580
48769
  };
47581
48770
  const explodeItemNode = (item, path2, depth, inherited, parentCenter, parentDirection) => {
@@ -47611,7 +48800,8 @@ function explode(items, options = {}) {
47611
48800
  if (child instanceof ShapeGroup) return explodeGroup(child, p2, 1, [0, 0, 0], rootCenter, void 0);
47612
48801
  return explodeLeaf(child, nodeMotion(child, p2, 1, rootCenter, void 0).offset);
47613
48802
  }),
47614
- items.childNames
48803
+ items.childNames,
48804
+ items.children.map((_2, i) => items.tagsForChild(i))
47615
48805
  );
47616
48806
  }
47617
48807
  return items.map((item, i) => {
@@ -48380,6 +49570,398 @@ function spurGear(options) {
48380
49570
  });
48381
49571
  return attachGearMeta(shapeWithConnectors, meta2);
48382
49572
  }
49573
+ function requirePositive$7(scope, name, value) {
49574
+ if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
49575
+ }
49576
+ function requireOptionalBore(scope, boreDiameter, maxDiameter) {
49577
+ const bore = boreDiameter ?? 0;
49578
+ if (!Number.isFinite(bore) || bore < 0) throw new Error(`${scope}: "boreDiameter" must be >= 0`);
49579
+ if (bore > 0 && bore >= maxDiameter) throw new Error(`${scope}: bore is too large for the body`);
49580
+ return bore;
49581
+ }
49582
+ function resolveSegments(segments) {
49583
+ if (segments === void 0) return void 0;
49584
+ if (!Number.isInteger(segments) || segments < 12) throw new Error('gear body: "segments" must be an integer >= 12');
49585
+ return segments;
49586
+ }
49587
+ function cutBore$1(shape, boreDiameter) {
49588
+ if (boreDiameter <= 0) return shape;
49589
+ const bounds = shape.boundingBox();
49590
+ const height = bounds.max[2] - bounds.min[2] + 2;
49591
+ const cutter = cylinder(height, boreDiameter * 0.5, void 0, 64).translate(0, 0, bounds.min[2] - 1);
49592
+ return shape.subtract(cutter);
49593
+ }
49594
+ function gearBodyDisk(options) {
49595
+ requirePositive$7("gearBodyDisk", "outerRadius", options.outerRadius);
49596
+ requirePositive$7("gearBodyDisk", "faceWidth", options.faceWidth);
49597
+ const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
49598
+ const segments = resolveSegments(options.segments);
49599
+ const outer = circle2d(options.outerRadius, segments);
49600
+ const profile = bore > 0 ? difference2d(outer, circle2d(bore * 0.5, segments)) : outer;
49601
+ return sketchExtrude(profile, options.faceWidth);
49602
+ }
49603
+ function gearBodyDiskWithHub(options) {
49604
+ requirePositive$7("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
49605
+ if (options.hubDiameter >= options.outerRadius * 2) {
49606
+ throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
49607
+ }
49608
+ const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
49609
+ const base = gearBodyDisk({ ...options, boreDiameter: 0 });
49610
+ const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
49611
+ requirePositive$7("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
49612
+ const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
49613
+ 0,
49614
+ 0,
49615
+ (options.faceWidth - hubFaceWidth) * 0.5
49616
+ );
49617
+ return cutBore$1(base.add(hub), bore);
49618
+ }
49619
+ function gearBodySpoked(options) {
49620
+ requirePositive$7("gearBodySpoked", "outerRadius", options.outerRadius);
49621
+ requirePositive$7("gearBodySpoked", "faceWidth", options.faceWidth);
49622
+ requirePositive$7("gearBodySpoked", "rimWidth", options.rimWidth);
49623
+ requirePositive$7("gearBodySpoked", "hubDiameter", options.hubDiameter);
49624
+ requirePositive$7("gearBodySpoked", "spokeWidth", options.spokeWidth);
49625
+ if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
49626
+ throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
49627
+ }
49628
+ const hubRadius = options.hubDiameter * 0.5;
49629
+ const rimInnerRadius = options.outerRadius - options.rimWidth;
49630
+ if (rimInnerRadius <= hubRadius) throw new Error("gearBodySpoked: rim overlaps the hub");
49631
+ const bore = requireOptionalBore("gearBodySpoked", options.boreDiameter, options.hubDiameter);
49632
+ const segments = resolveSegments(options.segments);
49633
+ const rim = difference2d(circle2d(options.outerRadius, segments), circle2d(rimInnerRadius, segments));
49634
+ const hub = circle2d(hubRadius, segments);
49635
+ const spokeLength = rimInnerRadius - hubRadius + options.spokeWidth;
49636
+ const spokeCenter = hubRadius + spokeLength * 0.5 - options.spokeWidth * 0.5;
49637
+ const spoke = sketchTranslate(rect(spokeLength, options.spokeWidth), spokeCenter, 0);
49638
+ const spokes = [];
49639
+ for (let i = 0; i < options.spokeCount; i++) {
49640
+ spokes.push(sketchRotateAround(spoke, 360 / options.spokeCount * i, [0, 0]));
49641
+ }
49642
+ const profile = bore > 0 ? difference2d(union2d(rim, hub, ...spokes), circle2d(bore * 0.5, segments)) : union2d(rim, hub, ...spokes);
49643
+ return sketchExtrude(profile, options.faceWidth);
49644
+ }
49645
+ function gearBodyFromProfile(profile, options) {
49646
+ if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
49647
+ requirePositive$7("gearBodyFromProfile", "faceWidth", options.faceWidth);
49648
+ const bore = options.boreDiameter ?? 0;
49649
+ if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
49650
+ return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
49651
+ }
49652
+ function requirePositive$6(scope, name, value) {
49653
+ if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
49654
+ }
49655
+ function requireFiniteAngle(scope, name, value) {
49656
+ if (value !== void 0 && !Number.isFinite(value)) throw new Error(`${scope}: "${name}" must be finite`);
49657
+ }
49658
+ function cutBore(shape, boreDiameter) {
49659
+ if (boreDiameter <= 0) return shape;
49660
+ const bounds = shape.boundingBox();
49661
+ const height = bounds.max[2] - bounds.min[2] + 2;
49662
+ const cutter = cylinder(height, boreDiameter * 0.5, void 0, 64).translate(0, 0, bounds.min[2] - 1);
49663
+ return shape.subtract(cutter);
49664
+ }
49665
+ function bodyOuterRadius(shape) {
49666
+ const bounds = shape.boundingBox();
49667
+ return Math.max(Math.abs(bounds.min[0]), Math.abs(bounds.max[0]), Math.abs(bounds.min[1]), Math.abs(bounds.max[1]));
49668
+ }
49669
+ function buildSpurTeethRegion(options, name, faceWidth) {
49670
+ const scope = "driveWheel.addSpurTeethBetween";
49671
+ const teethOnFullCircle = options.teethOnFullCircle;
49672
+ if (!Number.isInteger(teethOnFullCircle) || teethOnFullCircle < 6) {
49673
+ throw new Error(`${scope}: "teethOnFullCircle" must be an integer >= 6`);
49674
+ }
49675
+ const toothCount = options.toothCount;
49676
+ if (!Number.isInteger(toothCount) || toothCount < 1 || toothCount > teethOnFullCircle) {
49677
+ throw new Error(`${scope}: "toothCount" must be an integer in [1, teethOnFullCircle]`);
49678
+ }
49679
+ const firstTooth = options.firstTooth ?? 0;
49680
+ if (!Number.isInteger(firstTooth) || firstTooth < 0 || firstTooth >= teethOnFullCircle) {
49681
+ throw new Error(`${scope}: "firstTooth" must be an integer in [0, teethOnFullCircle)`);
49682
+ }
49683
+ let normalized;
49684
+ try {
49685
+ normalized = normalizeSpurGearOptions({ ...options, teeth: teethOnFullCircle, faceWidth, boreDiameter: 0 });
49686
+ } catch (error) {
49687
+ remapErrorPrefix(error, "spurGear", scope);
49688
+ }
49689
+ const gearMeta = buildSpurGearMeta(normalized);
49690
+ const pitchStepDeg = 360 / teethOnFullCircle;
49691
+ const fromAngleDeg = firstTooth * pitchStepDeg - pitchStepDeg * 0.5;
49692
+ const toAngleDeg = (firstTooth + toothCount - 1) * pitchStepDeg + pitchStepDeg * 0.5;
49693
+ const profile = buildSpurToothRegionProfile(gearMeta, firstTooth, toothCount, normalized.segmentsPerTooth);
49694
+ return {
49695
+ shape: sketchExtrude(profile, faceWidth),
49696
+ gearMeta,
49697
+ meta: {
49698
+ name,
49699
+ kind: "spurTeeth",
49700
+ fromAngleDeg,
49701
+ toAngleDeg,
49702
+ outerRadius: gearMeta.outerRadius,
49703
+ rootRadius: gearMeta.rootRadius,
49704
+ pitchRadius: gearMeta.pitchRadius,
49705
+ module: normalized.module,
49706
+ teethOnFullCircle,
49707
+ toothCount,
49708
+ faceWidth
49709
+ }
49710
+ };
49711
+ }
49712
+ function buildSolidArcRegion(options, name, faceWidth) {
49713
+ const scope = "driveWheel.addSolidArcBetween";
49714
+ requirePositive$6(scope, "outerRadius", options.outerRadius);
49715
+ const innerRadius = options.innerRadius ?? 0;
49716
+ if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
49717
+ if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
49718
+ const sweepDeg = normalizedSweep(scope, options.fromAngleDeg, options.toAngleDeg);
49719
+ return {
49720
+ shape: sketchExtrude(buildSolidArcProfile(options, sweepDeg), faceWidth),
49721
+ meta: {
49722
+ name,
49723
+ kind: "solidArc",
49724
+ fromAngleDeg: options.fromAngleDeg,
49725
+ toAngleDeg: options.fromAngleDeg + sweepDeg,
49726
+ innerRadius,
49727
+ outerRadius: options.outerRadius,
49728
+ faceWidth
49729
+ }
49730
+ };
49731
+ }
49732
+ function normalizedSweep(scope, fromAngleDeg, toAngleDeg) {
49733
+ if (!Number.isFinite(fromAngleDeg)) throw new Error(`${scope}: "fromAngleDeg" must be finite`);
49734
+ if (!Number.isFinite(toAngleDeg)) throw new Error(`${scope}: "toAngleDeg" must be finite`);
49735
+ let sweep2 = toAngleDeg - fromAngleDeg;
49736
+ while (sweep2 <= 0) sweep2 += 360;
49737
+ if (sweep2 > 360 + EPSILON$1) throw new Error(`${scope}: angular sweep must be <= 360 degrees`);
49738
+ return Math.min(360, sweep2);
49739
+ }
49740
+ function buildSpurToothRegionProfile(meta2, firstTooth, toothCount, segmentsPerTooth) {
49741
+ const tooth = createSpurToothSketch(meta2, segmentsPerTooth);
49742
+ const teeth = [];
49743
+ for (let i = 0; i < toothCount; i++) {
49744
+ teeth.push(sketchRotateAround(tooth, 360 / meta2.teeth * (firstTooth + i), [0, 0]));
49745
+ }
49746
+ return union2d(...teeth);
49747
+ }
49748
+ function buildSolidArcProfile(options, sweepDeg) {
49749
+ const innerRadius = options.innerRadius ?? 0;
49750
+ const segments = options.segments ?? Math.max(16, Math.ceil(sweepDeg / 6));
49751
+ if (!Number.isInteger(segments) || segments < 4) throw new Error('driveWheel.addSolidArcBetween: "segments" must be an integer >= 4');
49752
+ if (Math.abs(sweepDeg - 360) < EPSILON$1) {
49753
+ const outer = circle2d(options.outerRadius, segments);
49754
+ return innerRadius > 0 ? difference2d(outer, circle2d(innerRadius, segments)) : outer;
49755
+ }
49756
+ const start = options.fromAngleDeg * Math.PI / 180;
49757
+ const end = start + sweepDeg * Math.PI / 180;
49758
+ const pts = [];
49759
+ if (innerRadius <= 0) pts.push([0, 0]);
49760
+ addArcPoints(pts, options.outerRadius, start, end, segments, true, true);
49761
+ if (innerRadius > 0) addArcPoints(pts, innerRadius, end, start, segments, true, true);
49762
+ return polygon(pts);
49763
+ }
49764
+ const DRIVE_WHEEL_META_KEY = Symbol.for("forgecad.library.driveWheelMeta");
49765
+ function attachDriveWheelMeta(shape, meta2) {
49766
+ shape[DRIVE_WHEEL_META_KEY] = meta2;
49767
+ return shape;
49768
+ }
49769
+ function readDriveWheelMeta(shape) {
49770
+ const meta2 = shape[DRIVE_WHEEL_META_KEY];
49771
+ return meta2 ?? null;
49772
+ }
49773
+ class DriveWheelBuilder {
49774
+ constructor(options = {}) {
49775
+ __publicField(this, "body");
49776
+ __publicField(this, "faceWidth");
49777
+ __publicField(this, "boreDiameter");
49778
+ __publicField(this, "regions", []);
49779
+ if (options.body !== void 0 && !(options.body instanceof Shape)) throw new Error('driveWheel: "body" must be a Shape');
49780
+ if (options.faceWidth !== void 0) requirePositive$6("driveWheel", "faceWidth", options.faceWidth);
49781
+ const boreDiameter = options.boreDiameter ?? 0;
49782
+ if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
49783
+ this.body = options.body;
49784
+ this.faceWidth = options.faceWidth;
49785
+ this.boreDiameter = boreDiameter;
49786
+ }
49787
+ /**
49788
+ * Add an involute spur-tooth window on part of the pitch circle.
49789
+ */
49790
+ addSpurTeethBetween(options) {
49791
+ const faceWidth = this.resolveFaceWidth("driveWheel.addSpurTeethBetween", options.faceWidth);
49792
+ this.regions.push(buildSpurTeethRegion(options, this.resolveName("teeth", options.name), faceWidth));
49793
+ return this;
49794
+ }
49795
+ /**
49796
+ * Add a constant-radius solid arc region such as a dwell, stop, or pusher.
49797
+ */
49798
+ addSolidArcBetween(options) {
49799
+ const faceWidth = this.resolveFaceWidth("driveWheel.addSolidArcBetween", options.faceWidth);
49800
+ this.regions.push(buildSolidArcRegion(options, this.resolveName("arc", options.name), faceWidth));
49801
+ return this;
49802
+ }
49803
+ /**
49804
+ * Add a fully custom region shape while preserving region metadata.
49805
+ */
49806
+ addShapeRegion(name, shape, options = {}) {
49807
+ const scope = "driveWheel.addShapeRegion";
49808
+ if (typeof name !== "string" || name.trim().length === 0) throw new Error(`${scope}: "name" must be a non-empty string`);
49809
+ if (!(shape instanceof Shape)) throw new Error(`${scope}: "shape" must be a Shape`);
49810
+ requireFiniteAngle(scope, "fromAngleDeg", options.fromAngleDeg);
49811
+ requireFiniteAngle(scope, "toAngleDeg", options.toAngleDeg);
49812
+ if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
49813
+ throw new Error(`${scope}: "innerRadius" must be >= 0`);
49814
+ }
49815
+ if (options.outerRadius !== void 0) requirePositive$6(scope, "outerRadius", options.outerRadius);
49816
+ this.regions.push({
49817
+ shape: shape.clone(),
49818
+ meta: {
49819
+ name: this.resolveName("region", name),
49820
+ kind: "custom",
49821
+ ...options
49822
+ }
49823
+ });
49824
+ return this;
49825
+ }
49826
+ /**
49827
+ * Build the final wheel shape with a bore connector and region metadata.
49828
+ */
49829
+ build() {
49830
+ var _a3, _b3;
49831
+ if (this.regions.length === 0 && this.body === void 0) {
49832
+ throw new Error("driveWheel: add a body or at least one region before build()");
49833
+ }
49834
+ const faceWidth = this.resolveBuildFaceWidth();
49835
+ const firstGearRegion = (_a3 = this.regions.find((region) => region.gearMeta)) == null ? void 0 : _a3.gearMeta;
49836
+ if (firstGearRegion && this.boreDiameter * 0.5 >= firstGearRegion.rootRadius - EPSILON$1) {
49837
+ throw new Error("driveWheel: bore is too large for the first spur-tooth region");
49838
+ }
49839
+ const body = ((_b3 = this.body) == null ? void 0 : _b3.clone()) ?? gearBodyDisk({ outerRadius: (firstGearRegion == null ? void 0 : firstGearRegion.rootRadius) ?? this.defaultBodyRadius(), faceWidth });
49840
+ let combined = body;
49841
+ for (const region of this.regions) combined = combined.add(region.shape);
49842
+ combined = cutBore(combined, this.boreDiameter);
49843
+ const withConnectors = combined.withConnectors({
49844
+ bore: connectorFactory(
49845
+ "drive-wheel-bore",
49846
+ { origin: [0, 0, faceWidth / 2], axis: [0, 0, 1], kind: "revolute" },
49847
+ this.measurements(faceWidth)
49848
+ )
49849
+ });
49850
+ return attachDriveWheelMeta(withConnectors, {
49851
+ kind: "driveWheel",
49852
+ faceWidth,
49853
+ boreDiameter: this.boreDiameter,
49854
+ regions: this.regionMetadata(body, faceWidth)
49855
+ });
49856
+ }
49857
+ measurements(faceWidth) {
49858
+ var _a3;
49859
+ const firstGearRegion = (_a3 = this.regions.find((region) => region.gearMeta)) == null ? void 0 : _a3.gearMeta;
49860
+ return {
49861
+ faceWidth,
49862
+ boreDiameter: this.boreDiameter,
49863
+ regionCount: this.regions.length,
49864
+ ...firstGearRegion ? {
49865
+ module: firstGearRegion.module,
49866
+ teethOnFullCircle: firstGearRegion.teeth,
49867
+ pitchRadius: firstGearRegion.pitchRadius,
49868
+ outerRadius: firstGearRegion.outerRadius
49869
+ } : {}
49870
+ };
49871
+ }
49872
+ regionMetadata(body, faceWidth) {
49873
+ return [
49874
+ { name: "body", kind: "body", outerRadius: bodyOuterRadius(body), faceWidth },
49875
+ ...this.regions.map((region) => ({ ...region.meta }))
49876
+ ];
49877
+ }
49878
+ resolveFaceWidth(scope, localFaceWidth) {
49879
+ const faceWidth = localFaceWidth ?? this.faceWidth;
49880
+ if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
49881
+ requirePositive$6(scope, "faceWidth", faceWidth);
49882
+ if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
49883
+ throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
49884
+ }
49885
+ return faceWidth;
49886
+ }
49887
+ resolveBuildFaceWidth() {
49888
+ var _a3;
49889
+ const faceWidth = this.faceWidth ?? ((_a3 = this.regions.find((region) => region.meta.faceWidth !== void 0)) == null ? void 0 : _a3.meta.faceWidth);
49890
+ if (faceWidth === void 0) throw new Error('driveWheel: "faceWidth" is required before build()');
49891
+ return faceWidth;
49892
+ }
49893
+ defaultBodyRadius() {
49894
+ const outerRadius = this.regions.reduce((max2, region) => Math.max(max2, region.meta.outerRadius ?? 0), 0);
49895
+ if (outerRadius <= 0) throw new Error('driveWheel: "body" is required when regions do not define an outer radius');
49896
+ return outerRadius;
49897
+ }
49898
+ resolveName(prefix, requested) {
49899
+ const base = (requested == null ? void 0 : requested.trim()) || prefix;
49900
+ if (this.regions.every((region) => region.meta.name !== base)) return base;
49901
+ for (let i = 2; ; i++) {
49902
+ const candidate = `${base}${i}`;
49903
+ if (this.regions.every((region) => region.meta.name !== candidate)) return candidate;
49904
+ }
49905
+ }
49906
+ }
49907
+ function driveWheel(options = {}) {
49908
+ return new DriveWheelBuilder(options);
49909
+ }
49910
+ function normalizeSectorGearOptions(options) {
49911
+ const teethOnFullCircle = options.teethOnFullCircle;
49912
+ if (!Number.isInteger(teethOnFullCircle) || teethOnFullCircle < 6) {
49913
+ throw new Error('sectorGear: "teethOnFullCircle" must be an integer >= 6');
49914
+ }
49915
+ const toothCount = options.toothCount;
49916
+ if (!Number.isInteger(toothCount) || toothCount < 1 || toothCount > teethOnFullCircle) {
49917
+ throw new Error('sectorGear: "toothCount" must be an integer in [1, teethOnFullCircle]');
49918
+ }
49919
+ const firstTooth = options.firstTooth ?? 0;
49920
+ if (!Number.isInteger(firstTooth) || firstTooth < 0 || firstTooth >= teethOnFullCircle) {
49921
+ throw new Error('sectorGear: "firstTooth" must be an integer in [0, teethOnFullCircle)');
49922
+ }
49923
+ return {
49924
+ ...normalizeSpurGearOptions({ ...options, teeth: teethOnFullCircle }),
49925
+ teethOnFullCircle,
49926
+ toothCount,
49927
+ firstTooth,
49928
+ boreDiameter: options.boreDiameter ?? 0
49929
+ };
49930
+ }
49931
+ function sectorGear(options) {
49932
+ const normalized = normalizeSectorGearOptions(options);
49933
+ if (options.body !== void 0 && !(options.body instanceof Shape)) {
49934
+ throw new Error('sectorGear: "body" must be a Shape');
49935
+ }
49936
+ const spurMeta = buildSpurGearMeta(normalized);
49937
+ const pitchStepDeg = 360 / normalized.teethOnFullCircle;
49938
+ const activeAngleStartDeg = normalized.firstTooth * pitchStepDeg - pitchStepDeg * 0.5;
49939
+ const activeAngleEndDeg = (normalized.firstTooth + normalized.toothCount - 1) * pitchStepDeg + pitchStepDeg * 0.5;
49940
+ const meta2 = {
49941
+ ...spurMeta,
49942
+ kind: "sector",
49943
+ teethOnFullCircle: normalized.teethOnFullCircle,
49944
+ firstTooth: normalized.firstTooth,
49945
+ toothCount: normalized.toothCount,
49946
+ activeAngleStartDeg,
49947
+ activeAngleEndDeg
49948
+ };
49949
+ const wheel = driveWheel({ body: options.body, faceWidth: normalized.faceWidth, boreDiameter: normalized.boreDiameter }).addSpurTeethBetween({
49950
+ name: "teeth",
49951
+ module: normalized.module,
49952
+ teethOnFullCircle: normalized.teethOnFullCircle,
49953
+ toothCount: normalized.toothCount,
49954
+ firstTooth: normalized.firstTooth,
49955
+ pressureAngleDeg: normalized.pressureAngleDeg,
49956
+ faceWidth: normalized.faceWidth,
49957
+ backlash: normalized.backlash,
49958
+ clearance: normalized.clearance,
49959
+ addendum: normalized.addendum,
49960
+ dedendum: normalized.dedendum,
49961
+ segmentsPerTooth: normalized.segmentsPerTooth
49962
+ }).build();
49963
+ return attachGearMeta(wheel, meta2);
49964
+ }
48383
49965
  function normalizeSideGearOptions(options) {
48384
49966
  let normalizedSpur;
48385
49967
  try {
@@ -49359,6 +50941,12 @@ function boltPattern(options) {
49359
50941
  }
49360
50942
  };
49361
50943
  }
50944
+ const gearBodies = {
50945
+ disk: gearBodyDisk,
50946
+ diskWithHub: gearBodyDiskWithHub,
50947
+ spoked: gearBodySpoked,
50948
+ fromProfile: gearBodyFromProfile
50949
+ };
49362
50950
  function thread(diameter, pitch, length4, options) {
49363
50951
  const r = diameter / 2;
49364
50952
  const depth = (options == null ? void 0 : options.depth) ?? pitch * 0.35;
@@ -49524,7 +51112,23 @@ const partLibrary = {
49524
51112
  gearRatio,
49525
51113
  rackRatio,
49526
51114
  planetaryRatio,
49527
- boltPattern
51115
+ boltPattern,
51116
+ /** Start a composable exceptional gear or drive wheel. */
51117
+ driveWheel,
51118
+ /** Read functional-region metadata from a drive wheel shape. */
51119
+ readDriveWheelMeta,
51120
+ /** Involute sector gear with teeth on only part of the pitch circle. */
51121
+ sectorGear,
51122
+ /** Gear body preset namespace: disk, diskWithHub, spoked, and fromProfile. */
51123
+ gearBodies,
51124
+ /** Solid disk/ring gear body, independent from any tooth geometry. */
51125
+ gearBodyDisk,
51126
+ /** Disk gear body with a raised center hub. */
51127
+ gearBodyDiskWithHub,
51128
+ /** Spoked gear body with an outer rim, center hub, and radial spokes. */
51129
+ gearBodySpoked,
51130
+ /** Extrude a custom 2D profile into a gear body. */
51131
+ gearBodyFromProfile
49528
51132
  };
49529
51133
  /**
49530
51134
  * @license
@@ -50881,7 +52485,7 @@ function requireFinite$7(value, label) {
50881
52485
  }
50882
52486
  return value;
50883
52487
  }
50884
- function requireVec3$2(value, label) {
52488
+ function requireVec3$3(value, label) {
50885
52489
  if (!Array.isArray(value) || value.length !== 3) {
50886
52490
  throw new Error(`${label} must be [x, y, z]`);
50887
52491
  }
@@ -50925,7 +52529,7 @@ function normalizeOptions(options) {
50925
52529
  out.size = requireFinite$7(options.size, "Viewport.label options.size");
50926
52530
  if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
50927
52531
  }
50928
- if (options.offset !== void 0) out.offset = requireVec3$2(options.offset, "Viewport.label options.offset");
52532
+ if (options.offset !== void 0) out.offset = requireVec3$3(options.offset, "Viewport.label options.offset");
50929
52533
  if (options.anchor !== void 0) {
50930
52534
  if (!VALID_ANCHORS.has(options.anchor)) {
50931
52535
  throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
@@ -50942,7 +52546,7 @@ function collectRenderLabel(text, at, options) {
50942
52546
  if (typeof text !== "string" || text.trim().length === 0) {
50943
52547
  throw new Error("Viewport.label text must be a non-empty string");
50944
52548
  }
50945
- const normalizedAt = requireVec3$2(at, "Viewport.label at");
52549
+ const normalizedAt = requireVec3$3(at, "Viewport.label at");
50946
52550
  const normalizedOptions = normalizeOptions(options);
50947
52551
  _collected$4.push({
50948
52552
  id: `render-label-${_nextId++}`,
@@ -51137,7 +52741,7 @@ function requireFinite$6(value, label) {
51137
52741
  }
51138
52742
  return value;
51139
52743
  }
51140
- function requireVec3$1(value, label) {
52744
+ function requireVec3$2(value, label) {
51141
52745
  if (!Array.isArray(value) || value.length !== 3) {
51142
52746
  throw new Error(`${label} must be [x, y, z]`);
51143
52747
  }
@@ -51165,9 +52769,9 @@ const VALID_ENVIRONMENT_PRESETS = /* @__PURE__ */ new Set([
51165
52769
  ]);
51166
52770
  function validateCamera(cam, label) {
51167
52771
  const out = {};
51168
- if (cam.position !== void 0) out.position = requireVec3$1(cam.position, `${label}.position`);
51169
- if (cam.target !== void 0) out.target = requireVec3$1(cam.target, `${label}.target`);
51170
- if (cam.up !== void 0) out.up = requireVec3$1(cam.up, `${label}.up`);
52772
+ if (cam.position !== void 0) out.position = requireVec3$2(cam.position, `${label}.position`);
52773
+ if (cam.target !== void 0) out.target = requireVec3$2(cam.target, `${label}.target`);
52774
+ if (cam.up !== void 0) out.up = requireVec3$2(cam.up, `${label}.up`);
51171
52775
  if (cam.fov !== void 0) {
51172
52776
  out.fov = requireFinite$6(cam.fov, `${label}.fov`);
51173
52777
  if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
@@ -51302,8 +52906,8 @@ function validateLight(light, label) {
51302
52906
  const out = { type: light.type };
51303
52907
  if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
51304
52908
  if (light.intensity !== void 0) out.intensity = requireFinite$6(light.intensity, `${label}.intensity`);
51305
- if (light.position !== void 0) out.position = requireVec3$1(light.position, `${label}.position`);
51306
- if (light.target !== void 0) out.target = requireVec3$1(light.target, `${label}.target`);
52909
+ if (light.position !== void 0) out.position = requireVec3$2(light.position, `${label}.position`);
52910
+ if (light.target !== void 0) out.target = requireVec3$2(light.target, `${label}.target`);
51307
52911
  if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
51308
52912
  if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
51309
52913
  if (light.angle !== void 0) out.angle = requireFinite$6(light.angle, `${label}.angle`);
@@ -51849,7 +53453,7 @@ class ProductStationBuilder {
51849
53453
  this.profileValue = profileFromSketch(sketch, "custom", width, depth);
51850
53454
  return this;
51851
53455
  }
51852
- /** Stores a semantic crown amount for diagnostics and future rail solving. */
53456
+ /** Set the station crown amount for soft product-section intent. */
51853
53457
  crown(amount) {
51854
53458
  if (!Number.isFinite(amount)) throw new Error("station.crown(amount) requires a finite number");
51855
53459
  this.crownValue = amount;
@@ -52835,7 +54439,7 @@ function scale$1(v, s) {
52835
54439
  function dot$2(a2, b) {
52836
54440
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
52837
54441
  }
52838
- function lerp$2(a2, b, t) {
54442
+ function lerp$4(a2, b, t) {
52839
54443
  return a2 + (b - a2) * t;
52840
54444
  }
52841
54445
  function frameMatrix$1(x2, y2, z2, p2) {
@@ -52846,7 +54450,7 @@ function axisVector(axis, sign2 = 1) {
52846
54450
  if (axis === "Y") return [0, sign2, 0];
52847
54451
  return [0, 0, sign2];
52848
54452
  }
52849
- function axisPosition(axis, point2) {
54453
+ function axisPosition$1(axis, point2) {
52850
54454
  return point2[AXIS_INDEX[axis]];
52851
54455
  }
52852
54456
  function crossPointForStation(axis, point2) {
@@ -52854,7 +54458,7 @@ function crossPointForStation(axis, point2) {
52854
54458
  if (axis === "Y") return [point2[0], -point2[2]];
52855
54459
  return [point2[1], point2[2]];
52856
54460
  }
52857
- function orientLoftToAxis(shape, axis) {
54461
+ function orientLoftToAxis$1(shape, axis) {
52858
54462
  if (axis === "Z") return shape;
52859
54463
  if (axis === "Y") return shape.rotateX(-90);
52860
54464
  return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
@@ -52911,9 +54515,9 @@ function interpolateQuery(a2, b, t) {
52911
54515
  }
52912
54516
  return {
52913
54517
  side: sideA,
52914
- u: lerp$2(a2.u ?? 0.5, b.u ?? 0.5, t),
52915
- v: lerp$2(a2.v ?? 0.5, b.v ?? 0.5, t),
52916
- offset: lerp$2(a2.offset ?? 0, b.offset ?? 0, t)
54518
+ u: lerp$4(a2.u ?? 0.5, b.u ?? 0.5, t),
54519
+ v: lerp$4(a2.v ?? 0.5, b.v ?? 0.5, t),
54520
+ offset: lerp$4(a2.offset ?? 0, b.offset ?? 0, t)
52917
54521
  };
52918
54522
  }
52919
54523
  function resolvePathQueries(points) {
@@ -52980,8 +54584,8 @@ class ProductSkin {
52980
54584
  this.stations = stations;
52981
54585
  this.rails = rails;
52982
54586
  for (const [name2, query] of Object.entries(refs)) this.refQueries.set(name2, cloneQuery(query));
52983
- this.axisMin = Math.min(...stations.map((station) => axisPosition(axis, station.center)));
52984
- this.axisMax = Math.max(...stations.map((station) => axisPosition(axis, station.center)));
54587
+ this.axisMin = Math.min(...stations.map((station) => axisPosition$1(axis, station.center)));
54588
+ this.axisMax = Math.max(...stations.map((station) => axisPosition$1(axis, station.center)));
52985
54589
  this.diagnosticsValue = {
52986
54590
  ...diagnostics,
52987
54591
  stationNames: stations.map((station) => station.name),
@@ -53038,24 +54642,24 @@ class ProductSkin {
53038
54642
  }
53039
54643
  /** Interpolate center, width, and depth at a normalized v or absolute axis value. */
53040
54644
  stationAt(vOrAxis) {
53041
- const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$2(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
54645
+ const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$4(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
53042
54646
  const sorted = this.stations;
53043
54647
  for (let index2 = 0; index2 < sorted.length - 1; index2 += 1) {
53044
54648
  const a2 = sorted[index2];
53045
54649
  const b = sorted[index2 + 1];
53046
- const aAxis = axisPosition(this.axis, a2.center);
53047
- const bAxis = axisPosition(this.axis, b.center);
54650
+ const aAxis = axisPosition$1(this.axis, a2.center);
54651
+ const bAxis = axisPosition$1(this.axis, b.center);
53048
54652
  if (axisValue < aAxis - EPS$5 || axisValue > bAxis + EPS$5) continue;
53049
54653
  const span = Math.max(EPS$5, bAxis - aAxis);
53050
54654
  const t = clamp$5((axisValue - aAxis) / span, 0, 1);
53051
54655
  return {
53052
54656
  axisValue,
53053
- center: [lerp$2(a2.center[0], b.center[0], t), lerp$2(a2.center[1], b.center[1], t), lerp$2(a2.center[2], b.center[2], t)],
53054
- width: lerp$2(a2.profile.width, b.profile.width, t),
53055
- depth: lerp$2(a2.profile.depth, b.profile.depth, t),
54657
+ center: [lerp$4(a2.center[0], b.center[0], t), lerp$4(a2.center[1], b.center[1], t), lerp$4(a2.center[2], b.center[2], t)],
54658
+ width: lerp$4(a2.profile.width, b.profile.width, t),
54659
+ depth: lerp$4(a2.profile.depth, b.profile.depth, t),
53056
54660
  dWidth: (b.profile.width - a2.profile.width) / span,
53057
54661
  dDepth: (b.profile.depth - a2.profile.depth) / span,
53058
- exponent: lerp$2(profileExponent(a2), profileExponent(b), t),
54662
+ exponent: lerp$4(profileExponent(a2), profileExponent(b), t),
53059
54663
  kind: a2.profile.kind === b.profile.kind ? a2.profile.kind : "custom"
53060
54664
  };
53061
54665
  }
@@ -53177,10 +54781,10 @@ class ProductSkinBuilder {
53177
54781
  }
53178
54782
  /** Set named cross-section stations for the product skin. */
53179
54783
  stations(stations) {
53180
- this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition(this.axisValue, a2.center) - axisPosition(this.axisValue, b.center));
54784
+ this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition$1(this.axisValue, a2.center) - axisPosition$1(this.axisValue, b.center));
53181
54785
  return this;
53182
54786
  }
53183
- /** Attach guide rails as ProductSkin IR metadata and diagnostics. */
54787
+ /** Attach named guide rails for product-skin construction and downstream surface references. */
53184
54788
  rails(rails) {
53185
54789
  this.railsValue = { ...rails };
53186
54790
  return this;
@@ -53214,7 +54818,7 @@ class ProductSkinBuilder {
53214
54818
  this.edgeLengthValue = value;
53215
54819
  return this;
53216
54820
  }
53217
- /** Records a target wall thickness; v1 keeps exterior skin lowering sampled and reports wall as a diagnostic. */
54821
+ /** Record intended wall thickness for product design metadata. Use explicit shelling when the model needs real inner-wall geometry. */
53218
54822
  wall(thickness) {
53219
54823
  if (!Number.isFinite(thickness) || thickness <= 0) throw new Error("Product.skin().wall(thickness) requires a positive finite number");
53220
54824
  this.wallValue = thickness;
@@ -53227,9 +54831,9 @@ class ProductSkinBuilder {
53227
54831
  const [x2, y2] = crossPointForStation(this.axisValue, station.center);
53228
54832
  return station.profile.sketch.translate(x2, y2);
53229
54833
  });
53230
- const heights = this.stationsValue.map((station) => axisPosition(this.axisValue, station.center));
54834
+ const heights = this.stationsValue.map((station) => axisPosition$1(this.axisValue, station.center));
53231
54835
  let shape = loft(localProfiles, heights, { edgeLength: this.edgeLengthValue });
53232
- shape = orientLoftToAxis(shape, this.axisValue);
54836
+ shape = orientLoftToAxis$1(shape, this.axisValue);
53233
54837
  if (this.colorValue) shape = shape.color(this.colorValue);
53234
54838
  shape = applyMaterial(shape, this.materialValue).as(this.name);
53235
54839
  const warnings = [];
@@ -53888,7 +55492,7 @@ function requirePositive$3(value, label) {
53888
55492
  function clamp$4(value, min2, max2) {
53889
55493
  return Math.max(min2, Math.min(max2, value));
53890
55494
  }
53891
- function lerp$1(a2, b, t) {
55495
+ function lerp$3(a2, b, t) {
53892
55496
  return a2 + (b - a2) * t;
53893
55497
  }
53894
55498
  function add(a2, b) {
@@ -53938,19 +55542,19 @@ function transformLocal(point2, tangentAcross, normal, tangentAlong, x2, y2, z2
53938
55542
  function interpolateCylinder(a2, b, t, mode) {
53939
55543
  let delta = b.angle - a2.angle;
53940
55544
  if (mode === "shortest" && Math.abs(delta) > 180) delta -= Math.sign(delta) * 360;
53941
- return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$1(a2.z, b.z, t), offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t) };
55545
+ return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$3(a2.z, b.z, t), offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t) };
53942
55546
  }
53943
55547
  function interpolatePlane(a2, b, t) {
53944
- return { kind: "plane", x: lerp$1(a2.x, b.x, t), y: lerp$1(a2.y, b.y, t), offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t) };
55548
+ return { kind: "plane", x: lerp$3(a2.x, b.x, t), y: lerp$3(a2.y, b.y, t), offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t) };
53945
55549
  }
53946
55550
  function interpolateProductSkin(a2, b, t) {
53947
55551
  if ((a2.side ?? b.side) !== (b.side ?? a2.side)) throw new Error("SurfacePath on ProductSkin currently supports one side per path; split side transitions into separate members.");
53948
55552
  return {
53949
55553
  kind: "productSkin",
53950
55554
  side: a2.side ?? b.side,
53951
- u: lerp$1(a2.u ?? 0.5, b.u ?? 0.5, t),
53952
- v: lerp$1(a2.v ?? 0.5, b.v ?? 0.5, t),
53953
- offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t)
55555
+ u: lerp$3(a2.u ?? 0.5, b.u ?? 0.5, t),
55556
+ v: lerp$3(a2.v ?? 0.5, b.v ?? 0.5, t),
55557
+ offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t)
53954
55558
  };
53955
55559
  }
53956
55560
  class SurfacePath {
@@ -54273,11 +55877,11 @@ function coordinateOnSide(coordinate, side, label) {
54273
55877
  return { ...coordinate, kind: "productSkin", side };
54274
55878
  }
54275
55879
  class ProductSkinCarrier {
54276
- constructor(skin, name = skin.name, sideValue, offsetValue = 0) {
55880
+ constructor(skin, name = skin.name, sideValue2, offsetValue = 0) {
54277
55881
  __publicField(this, "kind", "productSkin");
54278
55882
  this.skin = skin;
54279
55883
  this.name = name;
54280
- this.sideValue = sideValue;
55884
+ this.sideValue = sideValue2;
54281
55885
  this.offsetValue = offsetValue;
54282
55886
  }
54283
55887
  surface(side) {
@@ -55048,7 +56652,7 @@ function counterboresForPlate(spec2, width, height, thickness, diagnostics) {
55048
56652
  function minWidthAcrossAlongRange(widthAtT, length4, minAlong, maxAlong) {
55049
56653
  let minWidth = Number.POSITIVE_INFINITY;
55050
56654
  for (let index2 = 0; index2 <= 8; index2 += 1) {
55051
- const along = lerp$1(minAlong, maxAlong, index2 / 8);
56655
+ const along = lerp$3(minAlong, maxAlong, index2 / 8);
55052
56656
  const t = Math.max(0, Math.min(1, (along + length4 / 2) / Math.max(length4, 1e-8)));
55053
56657
  minWidth = Math.min(minWidth, widthAtT(t));
55054
56658
  }
@@ -55348,7 +56952,7 @@ function pathParameterAtDistance(samples, distance2) {
55348
56952
  const segmentLength = Math.hypot(b.point[0] - a2.point[0], b.point[1] - a2.point[1], b.point[2] - a2.point[2]);
55349
56953
  if (traveled + segmentLength >= distance2) {
55350
56954
  const localT = segmentLength <= 1e-8 ? 0 : (distance2 - traveled) / segmentLength;
55351
- return lerp$1(a2.t, b.t, localT);
56955
+ return lerp$3(a2.t, b.t, localT);
55352
56956
  }
55353
56957
  traveled += segmentLength;
55354
56958
  }
@@ -55401,7 +57005,7 @@ function compileBandFootprintMesh(path2, input) {
55401
57005
  const width = input.widthAt(t);
55402
57006
  const along = distance2 - length4 / 2;
55403
57007
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
55404
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
57008
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
55405
57009
  mesh.vertices.push(pointAtProfile([across, along], false));
55406
57010
  }
55407
57011
  }
@@ -55411,7 +57015,7 @@ function compileBandFootprintMesh(path2, input) {
55411
57015
  const width = input.widthAt(t);
55412
57016
  const along = distance2 - length4 / 2;
55413
57017
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
55414
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
57018
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
55415
57019
  mesh.vertices.push(pointAtProfile([across, along], true));
55416
57020
  }
55417
57021
  }
@@ -55423,7 +57027,7 @@ function compileBandFootprintMesh(path2, input) {
55423
57027
  const width = input.widthAt(t);
55424
57028
  const along = distance2 - length4 / 2;
55425
57029
  for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
55426
- const across = lerp$1(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
57030
+ const across = lerp$3(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
55427
57031
  filled[alongIndex][acrossIndex] = !holes.some((hole2) => pointInProfileLoop([across, along], hole2));
55428
57032
  }
55429
57033
  }
@@ -56356,7 +57960,7 @@ class SurfaceMemberBuilder {
56356
57960
  this.record.features.push({ ...normalizeFeature(name, feature), type: "counterbore" });
56357
57961
  return this;
56358
57962
  }
56359
- /** Add a named anchor at a carrier surface coordinate for diagnostics, debug views, and future named-anchor joins. */
57963
+ /** Add a named anchor at a carrier surface coordinate for explicit member joins. */
56360
57964
  anchorAt(name, coordinate) {
56361
57965
  if (!name.trim()) throw new Error("SurfaceMemberBuilder.anchorAt(name, coordinate) requires a non-empty name");
56362
57966
  const explicitAnchors = this.record.spec.explicitAnchors ?? [];
@@ -57505,7 +59109,7 @@ const Constraint = {
57505
59109
  return builder.constrain({ type: "length", line: resolveLineId(builder, line2), value });
57506
59110
  }
57507
59111
  };
57508
- function requireVec3(v, label) {
59112
+ function requireVec3$1(v, label) {
57509
59113
  if (!Array.isArray(v) || v.length !== 3 || !Number.isFinite(v[0]) || !Number.isFinite(v[1]) || !Number.isFinite(v[2])) {
57510
59114
  throw new Error(`${label} must be a [number, number, number] with finite values, got ${JSON.stringify(v)}`);
57511
59115
  }
@@ -57518,24 +59122,24 @@ function requireFiniteNumber(n, label) {
57518
59122
  return n;
57519
59123
  }
57520
59124
  function distance$1(a2, b) {
57521
- requireVec3(a2, "a");
57522
- requireVec3(b, "b");
59125
+ requireVec3$1(a2, "a");
59126
+ requireVec3$1(b, "b");
57523
59127
  return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
57524
59128
  }
57525
59129
  function midpoint$1(a2, b) {
57526
- requireVec3(a2, "a");
57527
- requireVec3(b, "b");
59130
+ requireVec3$1(a2, "a");
59131
+ requireVec3$1(b, "b");
57528
59132
  return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
57529
59133
  }
57530
- function lerp(a2, b, t) {
57531
- requireVec3(a2, "a");
57532
- requireVec3(b, "b");
59134
+ function lerp$2(a2, b, t) {
59135
+ requireVec3$1(a2, "a");
59136
+ requireVec3$1(b, "b");
57533
59137
  requireFiniteNumber(t, "t");
57534
59138
  return [a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t];
57535
59139
  }
57536
59140
  function direction(a2, b) {
57537
- requireVec3(a2, "a");
57538
- requireVec3(b, "b");
59141
+ requireVec3$1(a2, "a");
59142
+ requireVec3$1(b, "b");
57539
59143
  const dx = b[0] - a2[0];
57540
59144
  const dy = b[1] - a2[1];
57541
59145
  const dz = b[2] - a2[2];
@@ -57546,8 +59150,8 @@ function direction(a2, b) {
57546
59150
  return [dx / len2, dy / len2, dz / len2];
57547
59151
  }
57548
59152
  function offset(point2, dir, amount) {
57549
- requireVec3(point2, "point");
57550
- requireVec3(dir, "dir");
59153
+ requireVec3$1(point2, "point");
59154
+ requireVec3$1(dir, "dir");
57551
59155
  requireFiniteNumber(amount, "amount");
57552
59156
  return [point2[0] + dir[0] * amount, point2[1] + dir[1] * amount, point2[2] + dir[2] * amount];
57553
59157
  }
@@ -57557,7 +59161,7 @@ const Points = {
57557
59161
  /** Center point between two 3D points. */
57558
59162
  midpoint: midpoint$1,
57559
59163
  /** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */
57560
- lerp,
59164
+ lerp: lerp$2,
57561
59165
  /** Unit direction vector from a to b. Throws if a and b are the same point. */
57562
59166
  direction,
57563
59167
  /** Move a point along a direction vector by a given amount. */
@@ -62689,9 +64293,84 @@ class ConstraintSketch extends Sketch {
62689
64293
  * Select the single arrangement region that contains the given seed point.
62690
64294
  * Throws if no region contains the seed.
62691
64295
  */
62692
- detectArrangementRegion(seed) {
64296
+ detectArrangementRegion(_seed) {
62693
64297
  throw new Error("Not implemented");
62694
64298
  }
64299
+ /**
64300
+ * Return the solved constrained path as a sampled 2D polyline.
64301
+ *
64302
+ * Use this when a construction rail was authored with `constrainedSketch()`
64303
+ * and should feed another operation such as `Loft.pathOnXz(...)`.
64304
+ * The sketch must contain exactly one profile path.
64305
+ *
64306
+ * @param samples - Samples per curved segment. Default 32.
64307
+ * @returns The solved path as an open polyline.
64308
+ */
64309
+ toPolyline(samples = 32) {
64310
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("ConstraintSketch.toPolyline() samples must be at least 2");
64311
+ const profileLoops = this.definition.loops.filter((loop) => loop.type === "profile");
64312
+ if (profileLoops.length !== 1) {
64313
+ throw new Error("ConstraintSketch.toPolyline() requires exactly one profile path");
64314
+ }
64315
+ const sampleCount = Math.max(2, Math.round(samples));
64316
+ const pointMap = new Map(this.definition.points.map((point2) => [point2.id, point2]));
64317
+ const lineMap = new Map(this.definition.lines.map((line2) => [line2.id, line2]));
64318
+ const arcMap = new Map(this.definition.arcs.map((arc) => [arc.id, arc]));
64319
+ const bezierMap = new Map(this.definition.beziers.map((bezier) => [bezier.id, bezier]));
64320
+ const points = [];
64321
+ const appendStart = (point2, label) => {
64322
+ const previous = points[points.length - 1];
64323
+ if (!previous) {
64324
+ points.push(point2);
64325
+ return;
64326
+ }
64327
+ if (Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-6) {
64328
+ throw new Error(`ConstraintSketch.toPolyline() profile path is not continuous at ${label}`);
64329
+ }
64330
+ };
64331
+ const appendPoint = (point2) => {
64332
+ const previous = points[points.length - 1];
64333
+ if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) points.push(point2);
64334
+ };
64335
+ const requirePoint = (id, label) => {
64336
+ const point2 = pointMap.get(id);
64337
+ if (!point2) throw new Error(`ConstraintSketch.toPolyline() missing ${label}`);
64338
+ return [point2.x, point2.y];
64339
+ };
64340
+ for (const segment of profileLoops[0].segments) {
64341
+ if (segment.kind === "line") {
64342
+ const line2 = lineMap.get(segment.line);
64343
+ if (!line2) throw new Error(`ConstraintSketch.toPolyline() missing line "${segment.line}"`);
64344
+ appendStart(requirePoint(line2.a, `line "${segment.line}" start point`), `line "${segment.line}"`);
64345
+ appendPoint(requirePoint(line2.b, `line "${segment.line}" end point`));
64346
+ } else if (segment.kind === "arc") {
64347
+ const arc = arcMap.get(segment.arc);
64348
+ if (!arc) throw new Error(`ConstraintSketch.toPolyline() missing arc "${segment.arc}"`);
64349
+ const center = requirePoint(arc.center, `arc "${segment.arc}" center point`);
64350
+ const start = requirePoint(arc.start, `arc "${segment.arc}" start point`);
64351
+ const end = requirePoint(arc.end, `arc "${segment.arc}" end point`);
64352
+ appendStart(start, `arc "${segment.arc}"`);
64353
+ const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
64354
+ const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
64355
+ for (const point2 of tessellateArc(center[0], center[1], arc.radius, startAngle, endAngle, arc.clockwise, sampleCount)) {
64356
+ appendPoint(point2);
64357
+ }
64358
+ } else {
64359
+ const bezier = bezierMap.get(segment.bezier);
64360
+ if (!bezier) throw new Error(`ConstraintSketch.toPolyline() missing bezier "${segment.bezier}"`);
64361
+ const p0 = requirePoint(bezier.p0, `bezier "${segment.bezier}" start point`);
64362
+ const p1 = requirePoint(bezier.p1, `bezier "${segment.bezier}" first control point`);
64363
+ const p2 = requirePoint(bezier.p2, `bezier "${segment.bezier}" second control point`);
64364
+ const p3 = requirePoint(bezier.p3, `bezier "${segment.bezier}" end point`);
64365
+ appendStart(p0, `bezier "${segment.bezier}"`);
64366
+ for (const point2 of tessellateBezier(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], p3[0], p3[1], sampleCount)) {
64367
+ appendPoint(point2);
64368
+ }
64369
+ }
64370
+ }
64371
+ if (points.length < 2) throw new Error("ConstraintSketch.toPolyline() needs at least 2 points");
64372
+ return points;
64373
+ }
62695
64374
  /**
62696
64375
  * Re-solve the sketch after changing the value of one existing constraint.
62697
64376
  *
@@ -65818,8 +67497,8 @@ tinf_build_bits_base(dist_bits, dist_base, 2, 1);
65818
67497
  length_bits[28] = 0;
65819
67498
  length_base[28] = 258;
65820
67499
  var tinyInflate = tinf_uncompress;
65821
- function derive(v0, v1, v2, v3, t) {
65822
- return Math.pow(1 - t, 3) * v0 + 3 * Math.pow(1 - t, 2) * t * v1 + 3 * (1 - t) * Math.pow(t, 2) * v2 + Math.pow(t, 3) * v3;
67500
+ function derive(v0, v1, v2, v32, t) {
67501
+ return Math.pow(1 - t, 3) * v0 + 3 * Math.pow(1 - t, 2) * t * v1 + 3 * (1 - t) * Math.pow(t, 2) * v2 + Math.pow(t, 3) * v32;
65823
67502
  }
65824
67503
  function BoundingBox() {
65825
67504
  this.x1 = Number.NaN;
@@ -77976,6 +79655,295 @@ function polygonVertices(sides, radius, options) {
77976
79655
  centerY: options == null ? void 0 : options.centerY
77977
79656
  });
77978
79657
  }
79658
+ const LOFT_GUIDE_EPS = 1e-8;
79659
+ function orientLoftToAxis(shape, axis) {
79660
+ if (axis === "Z") return shape;
79661
+ if (axis === "Y") return shape.rotateX(-90);
79662
+ return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
79663
+ }
79664
+ function buildRailEvaluators(rails, axis, start, end, railSamples) {
79665
+ const seen = /* @__PURE__ */ new Set();
79666
+ return rails.map((rail2) => {
79667
+ if (seen.has(rail2.side)) throw new Error(`Loft.withGuideRails() received more than one ${rail2.side} rail`);
79668
+ seen.add(rail2.side);
79669
+ const sampled = sampleRailPath(rail2.path, railSamples);
79670
+ if (sampled.length < 2) throw new Error("Loft guide rails require at least two points");
79671
+ const points = sampled.map((point2) => ({ position: axisPosition(axis, point2), cross: crossPointForAxis(axis, point2) }));
79672
+ const ordered = points[points.length - 1].position >= points[0].position ? points : [...points].reverse();
79673
+ validateRailCoverage(ordered, start, end);
79674
+ return { side: rail2.side, points: ordered };
79675
+ });
79676
+ }
79677
+ function railCrossAt(rail2, position) {
79678
+ const points = rail2.points;
79679
+ if (position <= points[0].position + LOFT_GUIDE_EPS) return points[0].cross;
79680
+ const last = points[points.length - 1];
79681
+ if (position >= last.position - LOFT_GUIDE_EPS) return last.cross;
79682
+ for (let index2 = 0; index2 < points.length - 1; index2 += 1) {
79683
+ const a2 = points[index2];
79684
+ const b = points[index2 + 1];
79685
+ if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
79686
+ const t = (position - a2.position) / (b.position - a2.position);
79687
+ return [lerp$1(a2.cross[0], b.cross[0], t), lerp$1(a2.cross[1], b.cross[1], t)];
79688
+ }
79689
+ }
79690
+ throw new Error("Loft guide rail does not cover requested station position");
79691
+ }
79692
+ function validateRailCoverage(points, start, end) {
79693
+ for (let index2 = 1; index2 < points.length; index2 += 1) {
79694
+ if (points[index2].position - points[index2 - 1].position < LOFT_GUIDE_EPS) {
79695
+ throw new Error("Loft guide rails must be monotone along the loft axis");
79696
+ }
79697
+ }
79698
+ if (points[0].position - start > LOFT_GUIDE_EPS || end - points[points.length - 1].position > LOFT_GUIDE_EPS) {
79699
+ throw new Error("Loft guide rails must cover the full station range");
79700
+ }
79701
+ }
79702
+ function sampleRailPath(path2, samples) {
79703
+ if (Array.isArray(path2)) return path2.map((point2, index2) => requireVec3(point2, `Loft guide rail point ${index2}`));
79704
+ if (path2 instanceof Curve3D || path2 instanceof HermiteCurve3D || path2 instanceof QuinticHermiteCurve3D || path2 instanceof NurbsCurve3D) {
79705
+ return path2.sample(Math.max(2, Math.round(samples))).map((point2, index2) => requireVec3(point2, `Loft guide rail sample ${index2}`));
79706
+ }
79707
+ throw new Error("Loft guide rail path must be a Vec3[] or ForgeCAD 3D curve");
79708
+ }
79709
+ function requireVec3(point2, label) {
79710
+ if (!Array.isArray(point2) || point2.length !== 3 || !point2.every(Number.isFinite)) {
79711
+ throw new Error(`${label} must be a finite [x, y, z] point`);
79712
+ }
79713
+ return [point2[0], point2[1], point2[2]];
79714
+ }
79715
+ function axisPosition(axis, point2) {
79716
+ if (axis === "X") return point2[0];
79717
+ if (axis === "Y") return point2[1];
79718
+ return point2[2];
79719
+ }
79720
+ function crossPointForAxis(axis, point2) {
79721
+ if (axis === "X") return [point2[1], point2[2]];
79722
+ if (axis === "Y") return [point2[0], -point2[2]];
79723
+ return [point2[0], point2[1]];
79724
+ }
79725
+ function lerp$1(a2, b, t) {
79726
+ return a2 + (b - a2) * t;
79727
+ }
79728
+ function loftWithGuideRails(stations, rails, options = {}) {
79729
+ if (stations.length < 2) throw new Error("Loft.withGuideRails() requires at least two stations");
79730
+ if (rails.length === 0) throw new Error("Loft.withGuideRails() requires at least one guide rail");
79731
+ const sortedStations = sortedValidStations(stations);
79732
+ const axis = options.axis ?? "Z";
79733
+ const start = sortedStations[0].position;
79734
+ const end = sortedStations[sortedStations.length - 1].position;
79735
+ const railEvaluators = buildRailEvaluators(rails, axis, start, end, options.railSamples ?? 64);
79736
+ const positions = generatedPositions(sortedStations, options.samples);
79737
+ const profiles2 = positions.map((position) => {
79738
+ const source = profileForPosition(sortedStations, position);
79739
+ const bounds = boundsForPosition(sortedStations, position);
79740
+ return fitProfileToBounds(source, applyRailsToBounds(bounds, railEvaluators, position));
79741
+ });
79742
+ const shape = loft(profiles2, positions, {
79743
+ edgeLength: options.edgeLength,
79744
+ boundsPadding: options.boundsPadding
79745
+ });
79746
+ return orientLoftToAxis(shape, axis);
79747
+ }
79748
+ function sortedValidStations(stations) {
79749
+ const sorted = [...stations].sort((a2, b) => a2.position - b.position);
79750
+ for (let index2 = 0; index2 < sorted.length; index2 += 1) {
79751
+ if (!Number.isFinite(sorted[index2].position)) throw new Error("Loft.withGuideRails station position must be finite");
79752
+ if (!(sorted[index2].profile instanceof Sketch)) throw new Error("Loft.withGuideRails() stations must use Sketch profiles");
79753
+ if (index2 > 0 && sorted[index2].position - sorted[index2 - 1].position < LOFT_GUIDE_EPS) {
79754
+ throw new Error("Loft.withGuideRails() requires unique, strictly increasing station positions");
79755
+ }
79756
+ }
79757
+ return sorted;
79758
+ }
79759
+ function generatedPositions(stations, samples) {
79760
+ const count = Math.max(2, Math.round(samples ?? Math.max(9, (stations.length - 1) * 8 + 1)));
79761
+ const start = stations[0].position;
79762
+ const end = stations[stations.length - 1].position;
79763
+ const values = /* @__PURE__ */ new Set();
79764
+ const positions = [];
79765
+ const addPosition = (position) => {
79766
+ const key = position.toFixed(9);
79767
+ if (!values.has(key)) {
79768
+ values.add(key);
79769
+ positions.push(position);
79770
+ }
79771
+ };
79772
+ for (let index2 = 0; index2 < count; index2 += 1) addPosition(start + (end - start) * index2 / (count - 1));
79773
+ for (const station of stations) addPosition(station.position);
79774
+ return positions.sort((a2, b) => a2 - b);
79775
+ }
79776
+ function profileForPosition(stations, position) {
79777
+ for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
79778
+ if (position <= stations[index2 + 1].position + LOFT_GUIDE_EPS) return stations[index2].profile;
79779
+ }
79780
+ return stations[stations.length - 1].profile;
79781
+ }
79782
+ function boundsForPosition(stations, position) {
79783
+ if (position <= stations[0].position + LOFT_GUIDE_EPS) return sketchBounds(stations[0].profile);
79784
+ const last = stations[stations.length - 1];
79785
+ if (position >= last.position - LOFT_GUIDE_EPS) return sketchBounds(last.profile);
79786
+ for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
79787
+ const a2 = stations[index2];
79788
+ const b = stations[index2 + 1];
79789
+ if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
79790
+ return lerpBounds(sketchBounds(a2.profile), sketchBounds(b.profile), (position - a2.position) / (b.position - a2.position));
79791
+ }
79792
+ }
79793
+ return sketchBounds(last.profile);
79794
+ }
79795
+ function applyRailsToBounds(bounds, rails, position) {
79796
+ const centerRail = rails.find((rail2) => rail2.side === "center");
79797
+ const center = centerRail ? railCrossAt(centerRail, position) : void 0;
79798
+ const next = { ...bounds };
79799
+ applyAxisRail(next, "X", sideValue(rails, "left", position, 0), sideValue(rails, "right", position, 0), center == null ? void 0 : center[0]);
79800
+ applyAxisRail(next, "Y", sideValue(rails, "back", position, 1), sideValue(rails, "front", position, 1), center == null ? void 0 : center[1]);
79801
+ if (next.maxX - next.minX < LOFT_GUIDE_EPS || next.maxY - next.minY < LOFT_GUIDE_EPS) {
79802
+ throw new Error("Loft.withGuideRails() guide rails produced a non-positive section size");
79803
+ }
79804
+ return next;
79805
+ }
79806
+ function sideValue(rails, side, position, crossIndex) {
79807
+ const rail2 = rails.find((entry) => entry.side === side);
79808
+ return rail2 ? railCrossAt(rail2, position)[crossIndex] : void 0;
79809
+ }
79810
+ function applyAxisRail(bounds, axis, minRail, maxRail, center) {
79811
+ const minKey = axis === "X" ? "minX" : "minY";
79812
+ const maxKey = axis === "X" ? "maxX" : "maxY";
79813
+ const width = bounds[maxKey] - bounds[minKey];
79814
+ if (minRail != null && maxRail != null) {
79815
+ if (maxRail - minRail < LOFT_GUIDE_EPS) throw new Error("Loft.withGuideRails() opposite guide rails crossed");
79816
+ if (center != null && Math.abs((minRail + maxRail) / 2 - center) > 1e-5) {
79817
+ throw new Error("Loft.withGuideRails() center rail conflicts with opposite side rails");
79818
+ }
79819
+ bounds[minKey] = minRail;
79820
+ bounds[maxKey] = maxRail;
79821
+ } else if (maxRail != null) {
79822
+ bounds[maxKey] = maxRail;
79823
+ bounds[minKey] = center != null ? 2 * center - maxRail : maxRail - width;
79824
+ } else if (minRail != null) {
79825
+ bounds[minKey] = minRail;
79826
+ bounds[maxKey] = center != null ? 2 * center - minRail : minRail + width;
79827
+ } else if (center != null) {
79828
+ bounds[minKey] = center - width / 2;
79829
+ bounds[maxKey] = center + width / 2;
79830
+ }
79831
+ }
79832
+ function fitProfileToBounds(profile, target) {
79833
+ const source = sketchBounds(profile);
79834
+ const sourceWidth = source.maxX - source.minX;
79835
+ const sourceDepth = source.maxY - source.minY;
79836
+ if (sourceWidth < LOFT_GUIDE_EPS || sourceDepth < LOFT_GUIDE_EPS) {
79837
+ throw new Error("Loft.withGuideRails() station profiles must have positive bounds");
79838
+ }
79839
+ const sourceCenter = [(source.minX + source.maxX) / 2, (source.minY + source.maxY) / 2];
79840
+ const targetCenter = [(target.minX + target.maxX) / 2, (target.minY + target.maxY) / 2];
79841
+ return profile.scaleAround(sourceCenter, [(target.maxX - target.minX) / sourceWidth, (target.maxY - target.minY) / sourceDepth]).translate(targetCenter[0] - sourceCenter[0], targetCenter[1] - sourceCenter[1]);
79842
+ }
79843
+ function sketchBounds(profile) {
79844
+ const bounds = profile.bounds();
79845
+ return { minX: bounds.min[0], maxX: bounds.max[0], minY: bounds.min[1], maxY: bounds.max[1] };
79846
+ }
79847
+ function lerpBounds(a2, b, t) {
79848
+ return {
79849
+ minX: lerp(a2.minX, b.minX, t),
79850
+ maxX: lerp(a2.maxX, b.maxX, t),
79851
+ minY: lerp(a2.minY, b.minY, t),
79852
+ maxY: lerp(a2.maxY, b.maxY, t)
79853
+ };
79854
+ }
79855
+ function lerp(a2, b, t) {
79856
+ return a2 + (b - a2) * t;
79857
+ }
79858
+ function mapLoftPath2D(path2, label, mapper) {
79859
+ const points = sampleLoftPath2D(path2, label);
79860
+ return points.map((point2, index2) => {
79861
+ if (!Array.isArray(point2) || point2.length !== 2 || !point2.every(Number.isFinite)) {
79862
+ throw new Error(`${label} point ${index2} must be a finite [x, y] point`);
79863
+ }
79864
+ return mapper([point2[0], point2[1]]);
79865
+ });
79866
+ }
79867
+ function sampleLoftPath2D(path2, label) {
79868
+ if (Array.isArray(path2)) {
79869
+ if (path2.length < 2) throw new Error(`${label} requires at least two [x, y] points`);
79870
+ return path2;
79871
+ }
79872
+ if (!path2 || typeof path2 !== "object" || typeof path2.toPolyline !== "function") {
79873
+ throw new Error(`${label} requires a 2D path, solved constrained path, or [x, y] point array`);
79874
+ }
79875
+ const points = path2.toPolyline();
79876
+ if (!Array.isArray(points) || points.length < 2) throw new Error(`${label} path must produce at least two [x, y] points`);
79877
+ return points;
79878
+ }
79879
+ const Loft = {
79880
+ /** Create a loft station from a 2D profile and an axis position. */
79881
+ station(profile, position) {
79882
+ if (!Number.isFinite(position)) throw new Error("Loft.station position must be finite");
79883
+ return { profile, position };
79884
+ },
79885
+ /** Create a guide rail that constrains the section-local negative-X side. */
79886
+ leftRail(path2) {
79887
+ return { side: "left", path: path2 };
79888
+ },
79889
+ /** Create a guide rail that constrains the section-local positive-X side. */
79890
+ rightRail(path2) {
79891
+ return { side: "right", path: path2 };
79892
+ },
79893
+ /** Create a guide rail that constrains the section-local positive-Y side. */
79894
+ frontRail(path2) {
79895
+ return { side: "front", path: path2 };
79896
+ },
79897
+ /** Create a guide rail that constrains the section-local negative-Y side. */
79898
+ backRail(path2) {
79899
+ return { side: "back", path: path2 };
79900
+ },
79901
+ /** Create a guide rail that moves section centers along the loft. */
79902
+ centerRail(path2) {
79903
+ return { side: "center", path: path2 };
79904
+ },
79905
+ /**
79906
+ * Place a 2D guide path onto the XZ plane.
79907
+ *
79908
+ * The path's first coordinate becomes X and its second coordinate becomes Z.
79909
+ * Use this for left/right silhouette rails authored with `path()` or `constrainedSketch()`.
79910
+ */
79911
+ pathOnXz(path2, y2 = 0) {
79912
+ if (!Number.isFinite(y2)) throw new Error("Loft.pathOnXz y must be finite");
79913
+ return mapLoftPath2D(path2, "Loft.pathOnXz", ([x2, z2]) => [x2, y2, z2]);
79914
+ },
79915
+ /**
79916
+ * Place a 2D guide path onto the YZ plane.
79917
+ *
79918
+ * The path's first coordinate becomes Y and its second coordinate becomes Z.
79919
+ * Use this for front/back crown rails authored with `path()` or `constrainedSketch()`.
79920
+ */
79921
+ pathOnYz(path2, x2 = 0) {
79922
+ if (!Number.isFinite(x2)) throw new Error("Loft.pathOnYz x must be finite");
79923
+ return mapLoftPath2D(path2, "Loft.pathOnYz", ([y2, z2]) => [x2, y2, z2]);
79924
+ },
79925
+ /**
79926
+ * Place a 2D guide path onto the XY plane.
79927
+ *
79928
+ * The path's first coordinate becomes X and its second coordinate becomes Y.
79929
+ * Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
79930
+ */
79931
+ pathOnXy(path2, z2 = 0) {
79932
+ if (!Number.isFinite(z2)) throw new Error("Loft.pathOnXy z must be finite");
79933
+ return mapLoftPath2D(path2, "Loft.pathOnXy", ([x2, y2]) => [x2, y2, z2]);
79934
+ },
79935
+ /**
79936
+ * Loft through profile stations while forcing generated sections to follow guide rails.
79937
+ *
79938
+ * Stations define the cross-section family. Guide rails define the side or center
79939
+ * paths the loft must pass through. With opposite side rails, the section is scaled
79940
+ * to touch both rails. With one side rail, the section keeps its interpolated size
79941
+ * unless a center rail is also present.
79942
+ */
79943
+ withGuideRails(stations, rails, options = {}) {
79944
+ return loftWithGuideRails(stations, rails, options);
79945
+ }
79946
+ };
77979
79947
  let collectedHighlights = [];
77980
79948
  function resetHighlights() {
77981
79949
  collectedHighlights = [];
@@ -294630,6 +296598,7 @@ function classifySdfPreviewNode(node) {
294630
296598
  case "sdf:twist":
294631
296599
  case "sdf:bend":
294632
296600
  case "sdf:repeat":
296601
+ case "sdf:circularArray":
294633
296602
  case "sdf:shell":
294634
296603
  case "sdf:onion":
294635
296604
  return classifySdfPreviewNode(node.child);
@@ -294639,10 +296608,7 @@ function classifySdfPreviewNode(node) {
294639
296608
  reason: "This SDF uses a custom JavaScript displacement function that cannot be compiled for raymarch preview."
294640
296609
  };
294641
296610
  case "sdf:surfaceDisplace":
294642
- return {
294643
- mode: "unsupported",
294644
- reason: "This SDF uses surface displacement that is not yet available in the raymarch shader."
294645
- };
296611
+ return classifySurfaceDisplacePreviewNode(node);
294646
296612
  case "sdf:spatialBlend":
294647
296613
  return {
294648
296614
  mode: "unsupported",
@@ -294668,6 +296634,79 @@ ${node.shaderUnsupportedReason}` : ""}`
294668
296634
  };
294669
296635
  }
294670
296636
  }
296637
+ function classifySurfaceDisplacePreviewNode(node) {
296638
+ if (!node.pattern) {
296639
+ return {
296640
+ mode: "unsupported",
296641
+ reason: "This SDF uses a custom JavaScript surface pattern that cannot be compiled for raymarch preview."
296642
+ };
296643
+ }
296644
+ const childResult = classifySdfPreviewNode(node.child);
296645
+ if (childResult.mode !== "raymarch") return childResult;
296646
+ const uv = analyzeShaderSurfaceUv(node.child, "p", node.uvMode);
296647
+ if (uv.mode === "triplanar") {
296648
+ return {
296649
+ mode: "unsupported",
296650
+ reason: "Typed surface displacement raymarch preview currently supports sphere, cylinder, and torus UV mappings."
296651
+ };
296652
+ }
296653
+ return { mode: "raymarch" };
296654
+ }
296655
+ function f2(value) {
296656
+ if (!Number.isFinite(value)) return "0.0";
296657
+ const text = Number(value.toPrecision(9)).toString();
296658
+ return text.includes(".") || text.includes("e") ? text : `${text}.0`;
296659
+ }
296660
+ function v3(value) {
296661
+ return `vec3(${f2(value[0])}, ${f2(value[1])}, ${f2(value[2])})`;
296662
+ }
296663
+ function analyzeShaderSurfaceUv(node, p2, override) {
296664
+ const analysis = analyzeShaderSurfaceUvNode(node, p2);
296665
+ if (!override || override === "auto") return analysis;
296666
+ if (override === "triplanar") return { mode: "triplanar" };
296667
+ if (analysis.mode === "triplanar") return analysis;
296668
+ if (override === analysis.mode) return analysis;
296669
+ if (override === "sphere" || override === "cylinder") {
296670
+ return { mode: override, localPoint: analysis.localPoint, radius: analysis.radius };
296671
+ }
296672
+ return analysis.mode === "torus" ? analysis : { mode: "triplanar" };
296673
+ }
296674
+ function analyzeShaderSurfaceUvNode(node, p2) {
296675
+ switch (node.kind) {
296676
+ case "sdf:sphere":
296677
+ return { mode: "sphere", localPoint: p2, radius: node.radius };
296678
+ case "sdf:cylinder":
296679
+ return { mode: "cylinder", localPoint: p2, radius: node.radius };
296680
+ case "sdf:torus":
296681
+ return { mode: "torus", localPoint: p2, radius: node.minorRadius, majorRadius: node.majorRadius };
296682
+ case "sdf:translate":
296683
+ return analyzeShaderSurfaceUvNode(node.child, `(${p2} - ${v3(node.offset)})`);
296684
+ case "sdf:rotate":
296685
+ return analyzeShaderSurfaceUvNode(node.child, `rotateInvEuler(${p2}, ${v3(node.degrees)})`);
296686
+ case "sdf:scale": {
296687
+ const result = analyzeShaderSurfaceUvNode(node.child, `(${p2} / ${f2(node.factor)})`);
296688
+ if (result.mode === "triplanar") return result;
296689
+ return {
296690
+ ...result,
296691
+ radius: result.radius * node.factor,
296692
+ ...result.mode === "torus" ? { majorRadius: result.majorRadius * node.factor } : {}
296693
+ };
296694
+ }
296695
+ case "sdf:shell":
296696
+ return analyzeShaderSurfaceUvNode(node.child, p2);
296697
+ case "sdf:union":
296698
+ case "sdf:smoothUnion":
296699
+ case "sdf:intersection":
296700
+ case "sdf:smoothIntersection":
296701
+ case "sdf:difference":
296702
+ case "sdf:smoothDifference":
296703
+ return node.children.length > 0 ? analyzeShaderSurfaceUvNode(node.children[0], p2) : { mode: "triplanar" };
296704
+ case "sdf:morph":
296705
+ return analyzeShaderSurfaceUvNode(node.a, p2);
296706
+ default:
296707
+ return { mode: "triplanar" };
296708
+ }
296709
+ }
294671
296710
  function describeScriptResultType(value) {
294672
296711
  var _a3, _b3;
294673
296712
  if (value == null) return String(value);
@@ -294728,7 +296767,7 @@ function mapScriptResultToScene(args) {
294728
296767
  var _a3;
294729
296768
  const objects = [];
294730
296769
  const shapeDimensions = [];
294731
- const pushShape = (shape, name, groupName, color, treePath) => {
296770
+ const pushShape = (shape, name, groupName, color, treePath, tags = []) => {
294732
296771
  const objectId = `obj-${objects.length + 1}`;
294733
296772
  objects.push({
294734
296773
  id: objectId,
@@ -294739,7 +296778,8 @@ function mapScriptResultToScene(args) {
294739
296778
  materialProps: shape.materialProps,
294740
296779
  geometryInfo: shape.geometryInfo(),
294741
296780
  groupName,
294742
- treePath: treePath && treePath.length > 0 ? [...treePath] : [name]
296781
+ treePath: treePath && treePath.length > 0 ? [...treePath] : [name],
296782
+ ...tags.length > 0 ? { tags: [...tags] } : {}
294743
296783
  });
294744
296784
  const dims = getShapeDimensions(shape);
294745
296785
  dims.forEach((dim2) => {
@@ -294763,7 +296803,7 @@ function mapScriptResultToScene(args) {
294763
296803
  });
294764
296804
  }
294765
296805
  };
294766
- const pushSketch = (sketch, name, groupName, treePath) => {
296806
+ const pushSketch = (sketch, name, groupName, treePath, tags = []) => {
294767
296807
  const meta2 = sketch instanceof ConstraintSketch ? sketch.constraintMeta : void 0;
294768
296808
  objects.push({
294769
296809
  id: `obj-${objects.length + 1}`,
@@ -294774,10 +296814,11 @@ function mapScriptResultToScene(args) {
294774
296814
  sketchMeta: meta2,
294775
296815
  color: sketch.colorHex,
294776
296816
  groupName,
294777
- treePath: treePath && treePath.length > 0 ? [...treePath] : [name]
296817
+ treePath: treePath && treePath.length > 0 ? [...treePath] : [name],
296818
+ ...tags.length > 0 ? { tags: [...tags] } : {}
294778
296819
  });
294779
296820
  };
294780
- const pushSdf = (sdfShape, name, groupName, treePath, color) => {
296821
+ const pushSdf = (sdfShape, name, groupName, treePath, color, tags = []) => {
294781
296822
  const preview = classifySdfPreviewNode(sdfShape._node);
294782
296823
  const displayColor = color || sdfShape.colorHex;
294783
296824
  const data = {
@@ -294797,7 +296838,8 @@ function mapScriptResultToScene(args) {
294797
296838
  materialProps: sdfShape.materialProps,
294798
296839
  geometryInfo: null,
294799
296840
  groupName,
294800
- treePath: treePath && treePath.length > 0 ? [...treePath] : [name]
296841
+ treePath: treePath && treePath.length > 0 ? [...treePath] : [name],
296842
+ ...tags.length > 0 ? { tags: [...tags] } : {}
294801
296843
  });
294802
296844
  };
294803
296845
  const isNamedObject = (item) => {
@@ -294814,18 +296856,24 @@ function mapScriptResultToScene(args) {
294814
296856
  const rootGroupChildLabel = (grp, index2) => {
294815
296857
  return shapeGroupChildSegment(grp, index2, true);
294816
296858
  };
294817
- const flattenGroupChild = (child, label, groupName, treePath) => {
296859
+ const flattenGroupChild = (child, label, groupName, treePath, tags = []) => {
294818
296860
  const resolvedTreePath = treePath && treePath.length > 0 ? treePath : [label];
294819
296861
  if (child instanceof ShapeGroup) {
294820
296862
  child.children.forEach((nested, i) => {
294821
- flattenGroupChild(nested, groupChildLabel(child, label, i), groupName, [...resolvedTreePath, shapeGroupChildSegment(child, i)]);
296863
+ flattenGroupChild(
296864
+ nested,
296865
+ groupChildLabel(child, label, i),
296866
+ groupName,
296867
+ [...resolvedTreePath, shapeGroupChildSegment(child, i)],
296868
+ mergeSceneTags(tags, child.tagsForChild(i))
296869
+ );
294822
296870
  });
294823
296871
  return;
294824
296872
  }
294825
296873
  if (child instanceof Shape) {
294826
- pushShape(child, label, groupName, void 0, resolvedTreePath);
296874
+ pushShape(child, label, groupName, void 0, resolvedTreePath, tags);
294827
296875
  } else if (child instanceof Sketch) {
294828
- pushSketch(child, label, groupName, resolvedTreePath);
296876
+ pushSketch(child, label, groupName, resolvedTreePath, tags);
294829
296877
  }
294830
296878
  };
294831
296879
  const isPlainObject2 = (value) => {
@@ -294834,34 +296882,40 @@ function mapScriptResultToScene(args) {
294834
296882
  return proto2 === Object.prototype || proto2 === null;
294835
296883
  };
294836
296884
  const joinName = (path2) => path2.join(".");
294837
- const processRenderableTree = (value, fallbackLabel, fallbackSegment, parentGroup, parentTreePath = [], seen = /* @__PURE__ */ new WeakSet()) => {
296885
+ const processRenderableTree = (value, fallbackLabel, fallbackSegment, parentGroup, parentTreePath = [], inheritedTags = [], seen = /* @__PURE__ */ new WeakSet()) => {
294838
296886
  const segment = fallbackSegment.trim().length > 0 ? fallbackSegment : fallbackLabel;
294839
296887
  const treePath = [...parentTreePath, segment];
294840
296888
  const name = joinName(treePath) || fallbackLabel;
294841
296889
  if (value instanceof Assembly) {
294842
- value.solve().toSceneObjects().forEach((item, index2) => processNamedItem(item, `${name}.${index2 + 1}`, `${index2 + 1}`, name, treePath));
296890
+ value.solve().toSceneObjects().forEach((item, index2) => processNamedItem(item, `${name}.${index2 + 1}`, `${index2 + 1}`, name, treePath, inheritedTags));
294843
296891
  return;
294844
296892
  }
294845
296893
  if (value instanceof SolvedAssembly) {
294846
- value.toSceneObjects().forEach((item, index2) => processNamedItem(item, `${name}.${index2 + 1}`, `${index2 + 1}`, name, treePath));
296894
+ value.toSceneObjects().forEach((item, index2) => processNamedItem(item, `${name}.${index2 + 1}`, `${index2 + 1}`, name, treePath, inheritedTags));
294847
296895
  return;
294848
296896
  }
294849
296897
  if (value instanceof ShapeGroup) {
294850
296898
  value.children.forEach((child, i) => {
294851
- flattenGroupChild(child, groupChildLabel(value, name, i), parentGroup, [...treePath, shapeGroupChildSegment(value, i)]);
296899
+ flattenGroupChild(
296900
+ child,
296901
+ groupChildLabel(value, name, i),
296902
+ parentGroup,
296903
+ [...treePath, shapeGroupChildSegment(value, i)],
296904
+ mergeSceneTags(inheritedTags, value.tagsForChild(i))
296905
+ );
294852
296906
  });
294853
296907
  return;
294854
296908
  }
294855
296909
  if (value instanceof Shape) {
294856
- pushShape(value, name, parentGroup, void 0, treePath);
296910
+ pushShape(value, name, parentGroup, void 0, treePath, inheritedTags);
294857
296911
  return;
294858
296912
  }
294859
296913
  if (value instanceof Sketch) {
294860
- pushSketch(value, name, parentGroup, treePath);
296914
+ pushSketch(value, name, parentGroup, treePath, inheritedTags);
294861
296915
  return;
294862
296916
  }
294863
296917
  if (value instanceof SdfShape) {
294864
- pushSdf(value, name, parentGroup, treePath);
296918
+ pushSdf(value, name, parentGroup, treePath, void 0, inheritedTags);
294865
296919
  return;
294866
296920
  }
294867
296921
  if (value instanceof GCodeBuilder) {
@@ -294872,7 +296926,8 @@ function mapScriptResultToScene(args) {
294872
296926
  sketch: null,
294873
296927
  toolpath: value.build(),
294874
296928
  geometryInfo: null,
294875
- treePath
296929
+ treePath,
296930
+ ...inheritedTags.length > 0 ? { tags: [...inheritedTags] } : {}
294876
296931
  });
294877
296932
  return;
294878
296933
  }
@@ -294882,30 +296937,38 @@ function mapScriptResultToScene(args) {
294882
296937
  value.forEach((item, index2) => {
294883
296938
  const childSegment = `${index2 + 1}`;
294884
296939
  const childLabel = `${name}.${childSegment}`;
294885
- processRenderableTree(item, childLabel, childSegment, parentGroup, treePath, seen);
296940
+ processRenderableTree(item, childLabel, childSegment, parentGroup, treePath, inheritedTags, seen);
294886
296941
  });
294887
296942
  return;
294888
296943
  }
294889
296944
  if (isNamedObject(value)) {
294890
- processNamedItem(value, fallbackLabel, fallbackSegment, parentGroup, parentTreePath);
296945
+ processNamedItem(value, fallbackLabel, fallbackSegment, parentGroup, parentTreePath, inheritedTags);
294891
296946
  return;
294892
296947
  }
294893
296948
  if (isPlainObject2(value)) {
294894
296949
  if (seen.has(value)) return;
294895
296950
  seen.add(value);
294896
296951
  Object.entries(value).forEach(([key, entry]) => {
294897
- processRenderableTree(entry, key, key, parentGroup, treePath, seen);
296952
+ processRenderableTree(entry, key, key, parentGroup, treePath, inheritedTags, seen);
294898
296953
  });
294899
296954
  }
294900
296955
  };
294901
- const processNamedItem = (item, fallbackLabel, fallbackSegment, parentGroup, parentTreePath = []) => {
296956
+ const processNamedItem = (item, fallbackLabel, fallbackSegment, parentGroup, parentTreePath = [], inheritedTags = []) => {
296957
+ var _a4;
294902
296958
  const name = typeof item.name === "string" && item.name.trim().length > 0 ? item.name : fallbackLabel;
294903
296959
  const localSegment = typeof item.name === "string" && item.name.trim().length > 0 ? item.name : fallbackSegment;
294904
296960
  const treePath = [...parentTreePath, localSegment];
294905
296961
  const grp = parentGroup;
296962
+ const tags = mergeSceneTags(inheritedTags, (_a4 = item.metadata) == null ? void 0 : _a4.tags, item.tags);
294906
296963
  if (item.group instanceof ShapeGroup) {
294907
296964
  item.group.children.forEach((child, i) => {
294908
- flattenGroupChild(child, groupChildLabel(item.group, name, i), name, [...treePath, shapeGroupChildSegment(item.group, i)]);
296965
+ flattenGroupChild(
296966
+ child,
296967
+ groupChildLabel(item.group, name, i),
296968
+ name,
296969
+ [...treePath, shapeGroupChildSegment(item.group, i)],
296970
+ mergeSceneTags(tags, item.group.tagsForChild(i))
296971
+ );
294909
296972
  });
294910
296973
  return;
294911
296974
  }
@@ -294915,39 +296978,48 @@ function mapScriptResultToScene(args) {
294915
296978
  const childTreePath = [...treePath, `${i + 1}`];
294916
296979
  if (child instanceof ShapeGroup) {
294917
296980
  child.children.forEach((nested, nestedIndex) => {
294918
- flattenGroupChild(nested, groupChildLabel(child, name, nestedIndex), name, [
294919
- ...treePath,
294920
- shapeGroupChildSegment(child, nestedIndex)
294921
- ]);
296981
+ flattenGroupChild(
296982
+ nested,
296983
+ groupChildLabel(child, name, nestedIndex),
296984
+ name,
296985
+ [...treePath, shapeGroupChildSegment(child, nestedIndex)],
296986
+ mergeSceneTags(tags, child.tagsForChild(nestedIndex))
296987
+ );
294922
296988
  });
294923
296989
  } else if (child instanceof Shape) {
294924
- pushShape(child, childLabel, name, void 0, childTreePath);
296990
+ pushShape(child, childLabel, name, void 0, childTreePath, tags);
294925
296991
  } else if (child instanceof Sketch) {
294926
- pushSketch(child, childLabel, name, childTreePath);
296992
+ pushSketch(child, childLabel, name, childTreePath, tags);
294927
296993
  } else if (child instanceof SdfShape) {
294928
- pushSdf(child, childLabel, name, childTreePath);
296994
+ pushSdf(child, childLabel, name, childTreePath, void 0, tags);
294929
296995
  } else if (isNamedObject(child)) {
294930
- processNamedItem(child, childLabel, `${i + 1}`, name, treePath);
296996
+ processNamedItem(child, childLabel, `${i + 1}`, name, treePath, tags);
294931
296997
  }
294932
296998
  });
294933
296999
  return;
294934
297000
  }
294935
297001
  if (item.shape instanceof ShapeGroup) {
294936
297002
  item.shape.children.forEach(
294937
- (child, i) => flattenGroupChild(child, groupChildLabel(item.shape, name, i), name, [...treePath, shapeGroupChildSegment(item.shape, i)])
297003
+ (child, i) => flattenGroupChild(
297004
+ child,
297005
+ groupChildLabel(item.shape, name, i),
297006
+ name,
297007
+ [...treePath, shapeGroupChildSegment(item.shape, i)],
297008
+ mergeSceneTags(tags, item.shape.tagsForChild(i))
297009
+ )
294938
297010
  );
294939
297011
  return;
294940
297012
  }
294941
297013
  if (item.shape instanceof Shape) {
294942
- pushShape(item.shape, name, grp, item.color, treePath);
297014
+ pushShape(item.shape, name, grp, item.color, treePath, tags);
294943
297015
  return;
294944
297016
  }
294945
297017
  if (item.shape instanceof SdfShape) {
294946
- pushSdf(item.shape, name, grp, treePath, item.color);
297018
+ pushSdf(item.shape, name, grp, treePath, item.color, tags);
294947
297019
  return;
294948
297020
  }
294949
297021
  if (item.sdf instanceof SdfShape) {
294950
- pushSdf(item.sdf, name, grp, treePath, item.color);
297022
+ pushSdf(item.sdf, name, grp, treePath, item.color, tags);
294951
297023
  return;
294952
297024
  }
294953
297025
  if (item.sketch instanceof Sketch) {
@@ -294961,7 +297033,8 @@ function mapScriptResultToScene(args) {
294961
297033
  sketchMeta: meta2,
294962
297034
  color: item.color || item.sketch.colorHex,
294963
297035
  groupName: grp,
294964
- treePath
297036
+ treePath,
297037
+ ...tags.length > 0 ? { tags: [...tags] } : {}
294965
297038
  });
294966
297039
  }
294967
297040
  };
@@ -294975,14 +297048,20 @@ function mapScriptResultToScene(args) {
294975
297048
  } else if (result instanceof ShapeGroup) {
294976
297049
  result.children.forEach((child, i) => {
294977
297050
  const label = rootGroupChildLabel(result, i);
294978
- flattenGroupChild(child, label, void 0, [label]);
297051
+ flattenGroupChild(child, label, void 0, [label], result.tagsForChild(i));
294979
297052
  });
294980
297053
  } else if (Array.isArray(result)) {
294981
297054
  result.forEach((item, index2) => {
294982
297055
  const label = `Object ${index2 + 1}`;
294983
297056
  if (item instanceof ShapeGroup) {
294984
297057
  item.children.forEach((child, i) => {
294985
- flattenGroupChild(child, groupChildLabel(item, label, i), void 0, [label, shapeGroupChildSegment(item, i)]);
297058
+ flattenGroupChild(
297059
+ child,
297060
+ groupChildLabel(item, label, i),
297061
+ void 0,
297062
+ [label, shapeGroupChildSegment(item, i)],
297063
+ item.tagsForChild(i)
297064
+ );
294986
297065
  });
294987
297066
  return;
294988
297067
  }
@@ -295015,7 +297094,7 @@ function mapScriptResultToScene(args) {
295015
297094
  } else if (defaultValue instanceof ShapeGroup) {
295016
297095
  defaultValue.children.forEach((child, i) => {
295017
297096
  const label = rootGroupChildLabel(defaultValue, i);
295018
- flattenGroupChild(child, label, void 0, [label]);
297097
+ flattenGroupChild(child, label, void 0, [label], defaultValue.tagsForChild(i));
295019
297098
  });
295020
297099
  } else if (defaultValue instanceof Shape) {
295021
297100
  pushShape(defaultValue, args.fileName, void 0, void 0, [args.fileName]);
@@ -295061,7 +297140,8 @@ function mapScriptResultToScene(args) {
295061
297140
  name: `${mock2.name} (mock)`,
295062
297141
  shape: mock2.shape,
295063
297142
  sketch: null,
295064
- mock: true
297143
+ mock: true,
297144
+ tags: ["mock"]
295065
297145
  });
295066
297146
  }
295067
297147
  const hasSdfLeaves = objects.some((obj) => obj.sdf);
@@ -295332,6 +297412,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
295332
297412
  nurbsSurface,
295333
297413
  spline2d,
295334
297414
  spline3d,
297415
+ Loft,
295335
297416
  loft,
295336
297417
  loftAlongSpine,
295337
297418
  sweep,
@@ -296189,7 +298270,8 @@ function serializeRunResult(result, solverDebug = null) {
296189
298270
  geometryInfo: obj.geometryInfo,
296190
298271
  sketchMeta: obj.sketchMeta,
296191
298272
  groupName: obj.groupName,
296192
- treePath: obj.treePath
298273
+ treePath: obj.treePath,
298274
+ tags: obj.tags
296193
298275
  };
296194
298276
  if (serialized2.shapeData) {
296195
298277
  transferables.push(
@@ -296235,6 +298317,7 @@ function serializeRunMetadata(result) {
296235
298317
  sketchMeta: obj.sketchMeta,
296236
298318
  groupName: obj.groupName,
296237
298319
  treePath: obj.treePath,
298320
+ tags: obj.tags,
296238
298321
  mock: obj.mock,
296239
298322
  serverShapeRef: obj.serverShapeRef,
296240
298323
  exactState: obj.exactState