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$8(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 flrm = /* @__PURE__ */ hMap(flt, 9, 1);
4648
4692
  var 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
@@ -5125,8 +5169,8 @@ function parse3mf(data) {
5125
5169
  while ((tMatch = trianglePattern.exec(meshXml)) !== null) {
5126
5170
  const v1 = parseInt(tMatch[1], 10) + vertexOffset;
5127
5171
  const v2 = parseInt(tMatch[2], 10) + vertexOffset;
5128
- const v3 = parseInt(tMatch[3], 10) + vertexOffset;
5129
- allTriIndices.push(v1, v2, v3);
5172
+ const v32 = parseInt(tMatch[3], 10) + vertexOffset;
5173
+ allTriIndices.push(v1, v2, v32);
5130
5174
  }
5131
5175
  for (let i = 0; i < meshVerts.length; i++) {
5132
5176
  allPositions.push(meshVerts[i]);
@@ -5151,13 +5195,13 @@ function parseMeshFile(data, format) {
5151
5195
  return parse3mf(data);
5152
5196
  }
5153
5197
  }
5154
- const EPS$9 = 1e-8;
5198
+ const EPS$a = 1e-8;
5155
5199
  function length$3(v) {
5156
5200
  return Math.hypot(v[0], v[1], v[2]);
5157
5201
  }
5158
5202
  function normalize$7(v) {
5159
5203
  const len2 = length$3(v);
5160
- if (len2 < EPS$9) throw new Error("Plane normal must be non-zero");
5204
+ if (len2 < EPS$a) throw new Error("Plane normal must be non-zero");
5161
5205
  return [v[0] / len2, v[1] / len2, v[2] / len2];
5162
5206
  }
5163
5207
  function resolvePlaneOriginNormal(plane) {
@@ -5179,12 +5223,12 @@ function resolvePlaneOriginNormal(plane) {
5179
5223
  function rotationToPlaneSpace(normal) {
5180
5224
  const n = normalize$7(normal);
5181
5225
  const dot2 = n[2];
5182
- if (dot2 > 1 - EPS$9) {
5226
+ if (dot2 > 1 - EPS$a) {
5183
5227
  return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
5184
5228
  }
5185
5229
  let axis;
5186
5230
  let angle;
5187
- if (dot2 < -1 + EPS$9) {
5231
+ if (dot2 < -1 + EPS$a) {
5188
5232
  axis = [1, 0, 0];
5189
5233
  angle = Math.PI;
5190
5234
  } else {
@@ -5509,6 +5553,287 @@ function lowerShellShapeCompilePlanToConcretePlan(plan) {
5509
5553
  }
5510
5554
  return lowerBaseShellPlanToConcretePlan(plan.base, plan.thickness, normalizeShellOpenFaces(plan.openFaces));
5511
5555
  }
5556
+ const DEFAULT_MAX_GRID_POINTS = 8e6;
5557
+ const DEFAULT_MIN_EDGE_LENGTH = 0.15;
5558
+ function resolveSdfMeshingSettings(tree, bounds, options = {}) {
5559
+ const quality = options.quality ?? "preview";
5560
+ const minEdgeLength = positiveOrDefault(options.minEdgeLength, DEFAULT_MIN_EDGE_LENGTH);
5561
+ const maxGridPoints = positiveOrDefault(options.maxGridPoints, DEFAULT_MAX_GRID_POINTS);
5562
+ const tolerance = options.tolerance !== void 0 ? requirePositiveFinite$3(options.tolerance, "SDF tolerance") : void 0;
5563
+ const minFeatureSize = options.minFeatureSize !== void 0 ? requirePositiveFinite$3(options.minFeatureSize, "SDF minFeatureSize") : void 0;
5564
+ const maxTriangles = options.maxTriangles !== void 0 ? Math.floor(requirePositiveFinite$3(options.maxTriangles, "SDF maxTriangles")) : void 0;
5565
+ const analysis = analyzeSdfTree(tree);
5566
+ const warnings = [];
5567
+ let edgeLength2;
5568
+ if (options.edgeLength !== void 0) {
5569
+ edgeLength2 = requirePositiveFinite$3(options.edgeLength, "SDF edgeLength");
5570
+ if (edgeLength2 < minEdgeLength) {
5571
+ warnings.push(`edgeLength ${formatMm(edgeLength2)} was clamped to minimum ${formatMm(minEdgeLength)}.`);
5572
+ edgeLength2 = minEdgeLength;
5573
+ }
5574
+ } else {
5575
+ edgeLength2 = resolveDefaultEdgeLength(bounds, quality, minEdgeLength, analysis, options);
5576
+ }
5577
+ if (analysis.minWallThickness < Infinity && analysis.minWallThickness < edgeLength2 * 2) {
5578
+ analysis.riskFlags.add("thin-shell");
5579
+ warnings.push(
5580
+ `shell/wall thickness ${formatMm(analysis.minWallThickness)} is below 2 x edgeLength ${formatMm(edgeLength2)}; thin features may be under-sampled.`
5581
+ );
5582
+ }
5583
+ if (!options.bounds && analysis.hasInfiniteRepeat) {
5584
+ warnings.push("infinite repeat bounds are heuristic; pass .toShape({ bounds }) for predictable clipping.");
5585
+ }
5586
+ if (!options.bounds && analysis.riskFlags.has("noise")) {
5587
+ warnings.push("noise field bounds are heuristic; pass .toShape({ bounds }) for predictable clipping.");
5588
+ }
5589
+ if (!options.bounds && (analysis.riskFlags.has("tpms") || analysis.riskFlags.has("voronoi"))) {
5590
+ warnings.push("TPMS/Voronoi bounds are heuristic unless clipped or passed explicitly.");
5591
+ }
5592
+ if (analysis.hasLegacyTpmsThreshold) {
5593
+ warnings.push("TPMS thickness is using legacy field-threshold units; use wallThickness for approximate millimeters.");
5594
+ }
5595
+ return {
5596
+ quality,
5597
+ edgeLength: edgeLength2,
5598
+ tolerance,
5599
+ minFeatureSize,
5600
+ minEdgeLength,
5601
+ simplify: resolveSimplificationMode(options.simplify, quality, analysis.riskFlags),
5602
+ maxTriangles,
5603
+ maxGridPoints,
5604
+ diagnostics: options.diagnostics === true,
5605
+ treeRiskFlags: [...analysis.riskFlags].sort(),
5606
+ warnings
5607
+ };
5608
+ }
5609
+ function withScaledSdfEdgeLength(settings, edgeLength2) {
5610
+ return { ...settings, edgeLength: Math.max(settings.minEdgeLength, edgeLength2) };
5611
+ }
5612
+ function createSdfMeshingDiagnostics(settings, bounds, paddedBounds) {
5613
+ const grid = estimateSdfGridDimensions(paddedBounds, settings.edgeLength);
5614
+ const estimatedSamples = grid[0] * grid[1] * grid[2];
5615
+ return {
5616
+ bounds: cloneBounds$2(bounds),
5617
+ paddedBounds: cloneBounds$2(paddedBounds),
5618
+ edgeLength: settings.edgeLength,
5619
+ grid,
5620
+ estimatedSamples,
5621
+ estimatedMemoryBytes: estimatedSamples * 8,
5622
+ treeRiskFlags: [...settings.treeRiskFlags],
5623
+ simplification: settings.simplify,
5624
+ capMode: "box",
5625
+ capInset: settings.edgeLength,
5626
+ warnings: [...settings.warnings]
5627
+ };
5628
+ }
5629
+ function assertSdfMeshingBudget(diagnostics, maxGridPoints) {
5630
+ if (diagnostics.estimatedSamples <= maxGridPoints) return;
5631
+ const suggestedEdge = suggestEdgeLengthForSampleBudget(diagnostics.paddedBounds, maxGridPoints);
5632
+ throw new Error(
5633
+ `SDF meshing would sample ${formatCount(diagnostics.estimatedSamples)} grid points (~${formatBytes(
5634
+ diagnostics.estimatedMemoryBytes
5635
+ )}). Reduce bounds or use edgeLength >= ${formatMm(suggestedEdge)}.`
5636
+ );
5637
+ }
5638
+ function estimateSdfGridDimensions(bounds, edgeLength2) {
5639
+ const dx = bounds.max[0] - bounds.min[0];
5640
+ const dy = bounds.max[1] - bounds.min[1];
5641
+ const dz = bounds.max[2] - bounds.min[2];
5642
+ return [
5643
+ Math.max(2, Math.ceil(dx / edgeLength2) + 1),
5644
+ Math.max(2, Math.ceil(dy / edgeLength2) + 1),
5645
+ Math.max(2, Math.ceil(dz / edgeLength2) + 1)
5646
+ ];
5647
+ }
5648
+ function logSdfMeshingDiagnostics(prefix, diagnostics) {
5649
+ const warnings = diagnostics.warnings.length > 0 ? `, warnings=${diagnostics.warnings.join(" | ")}` : "";
5650
+ const evaluator = diagnostics.evaluator ? `, evaluator=${diagnostics.evaluator}${diagnostics.evaluatorUnsupportedReason ? ` (${diagnostics.evaluatorUnsupportedReason})` : ""}` : "";
5651
+ console.info(
5652
+ `${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}`
5653
+ );
5654
+ }
5655
+ function resolveDefaultEdgeLength(bounds, quality, minEdgeLength, analysis, options) {
5656
+ const dx = bounds.max[0] - bounds.min[0];
5657
+ const dy = bounds.max[1] - bounds.min[1];
5658
+ const dz = bounds.max[2] - bounds.min[2];
5659
+ const maxDim = Math.max(dx, dy, dz, minEdgeLength);
5660
+ const divisor = quality === "draft" ? 60 : quality === "export" ? 160 : 100;
5661
+ const candidates = [maxDim / divisor];
5662
+ if (options.tolerance !== void 0) candidates.push(requirePositiveFinite$3(options.tolerance, "SDF tolerance") * 2);
5663
+ if (options.minFeatureSize !== void 0) candidates.push(requirePositiveFinite$3(options.minFeatureSize, "SDF minFeatureSize") / 2.5);
5664
+ if (analysis.minMetricTpmsThickness < Infinity) candidates.push(analysis.minMetricTpmsThickness / 2);
5665
+ if (analysis.minTpmsCellSize < Infinity) candidates.push(analysis.minTpmsCellSize / 10);
5666
+ if (analysis.minRepeatSpacing < Infinity) candidates.push(analysis.minRepeatSpacing / 8);
5667
+ if (analysis.minWallThickness < Infinity) candidates.push(analysis.minWallThickness / 2.5);
5668
+ return Math.max(minEdgeLength, Math.min(...candidates.filter((v) => Number.isFinite(v) && v > 0)));
5669
+ }
5670
+ function resolveSimplificationMode(simplify, quality, riskFlags) {
5671
+ if (simplify === false) return "off";
5672
+ if (simplify === true || simplify === "safe") return "safe";
5673
+ if (quality === "export" && riskFlags.size > 0) return "off";
5674
+ return "safe";
5675
+ }
5676
+ function analyzeSdfTree(tree) {
5677
+ const analysis = {
5678
+ riskFlags: /* @__PURE__ */ new Set(),
5679
+ minTpmsCellSize: Infinity,
5680
+ minMetricTpmsThickness: Infinity,
5681
+ minRepeatSpacing: Infinity,
5682
+ minWallThickness: Infinity,
5683
+ hasInfiniteRepeat: false,
5684
+ hasLegacyTpmsThreshold: false
5685
+ };
5686
+ visitSdfNode(tree, analysis);
5687
+ return analysis;
5688
+ }
5689
+ function minPositive(...values) {
5690
+ let result = Infinity;
5691
+ for (const value of values) {
5692
+ if (value !== null && value !== void 0 && Number.isFinite(value) && value > 0) {
5693
+ result = Math.min(result, value);
5694
+ }
5695
+ }
5696
+ return result === Infinity ? null : result;
5697
+ }
5698
+ function estimateSurfacePatternSpacing(pattern) {
5699
+ switch (pattern.kind) {
5700
+ case "surfacePattern:constant":
5701
+ return null;
5702
+ case "surfacePattern:sineWave":
5703
+ return pattern.wavelength;
5704
+ case "surfacePattern:stripes":
5705
+ return Math.min(pattern.spacing, pattern.width);
5706
+ case "surfacePattern:overUnderWeave":
5707
+ return Math.min(...pattern.spacing, ...pattern.threadWidth);
5708
+ case "surfacePattern:abs":
5709
+ case "surfacePattern:negate":
5710
+ return estimateSurfacePatternSpacing(pattern.child);
5711
+ case "surfacePattern:add":
5712
+ case "surfacePattern:multiply":
5713
+ case "surfacePattern:min":
5714
+ case "surfacePattern:max":
5715
+ return minPositive(...pattern.children.map(estimateSurfacePatternSpacing));
5716
+ case "surfacePattern:clamp":
5717
+ return estimateSurfacePatternSpacing(pattern.child);
5718
+ }
5719
+ }
5720
+ function visitSdfNode(node, analysis) {
5721
+ switch (node.kind) {
5722
+ case "sdf:union":
5723
+ case "sdf:difference":
5724
+ case "sdf:intersection":
5725
+ case "sdf:smoothUnion":
5726
+ case "sdf:smoothDifference":
5727
+ case "sdf:smoothIntersection":
5728
+ for (const child of node.children) visitSdfNode(child, analysis);
5729
+ break;
5730
+ case "sdf:morph":
5731
+ case "sdf:spatialBlend":
5732
+ visitSdfNode(node.a, analysis);
5733
+ visitSdfNode(node.b, analysis);
5734
+ break;
5735
+ case "sdf:translate":
5736
+ case "sdf:rotate":
5737
+ case "sdf:scale":
5738
+ case "sdf:twist":
5739
+ case "sdf:bend":
5740
+ case "sdf:onion":
5741
+ visitSdfNode(node.child, analysis);
5742
+ break;
5743
+ case "sdf:repeat":
5744
+ analysis.riskFlags.add("repeat");
5745
+ for (let i = 0; i < 3; i++) {
5746
+ const spacing = node.spacing[i];
5747
+ if (spacing > 0) {
5748
+ analysis.minRepeatSpacing = Math.min(analysis.minRepeatSpacing, spacing);
5749
+ if (node.count[i] <= 0) analysis.hasInfiniteRepeat = true;
5750
+ }
5751
+ }
5752
+ visitSdfNode(node.child, analysis);
5753
+ break;
5754
+ case "sdf:circularArray": {
5755
+ analysis.riskFlags.add("repeat");
5756
+ if (node.offset > 0) {
5757
+ analysis.minRepeatSpacing = Math.min(analysis.minRepeatSpacing, 2 * Math.PI * node.offset / node.count);
5758
+ }
5759
+ visitSdfNode(node.child, analysis);
5760
+ break;
5761
+ }
5762
+ case "sdf:shell":
5763
+ analysis.minWallThickness = Math.min(analysis.minWallThickness, node.thickness);
5764
+ visitSdfNode(node.child, analysis);
5765
+ break;
5766
+ case "sdf:displace":
5767
+ case "sdf:surfaceDisplace":
5768
+ analysis.riskFlags.add("displacement");
5769
+ if (node.kind === "sdf:surfaceDisplace" && node.pattern) {
5770
+ const spacing = estimateSurfacePatternSpacing(node.pattern);
5771
+ if (spacing !== null) analysis.minRepeatSpacing = Math.min(analysis.minRepeatSpacing, spacing);
5772
+ }
5773
+ visitSdfNode(node.child, analysis);
5774
+ break;
5775
+ case "sdf:gyroid":
5776
+ case "sdf:schwarzP":
5777
+ case "sdf:diamond":
5778
+ case "sdf:lidinoid":
5779
+ analysis.riskFlags.add("tpms");
5780
+ analysis.minTpmsCellSize = Math.min(analysis.minTpmsCellSize, node.cellSize);
5781
+ if (node.thicknessMode === "metric-approx") {
5782
+ analysis.minMetricTpmsThickness = Math.min(analysis.minMetricTpmsThickness, node.thickness);
5783
+ analysis.minWallThickness = Math.min(analysis.minWallThickness, node.thickness);
5784
+ } else {
5785
+ analysis.hasLegacyTpmsThreshold = true;
5786
+ }
5787
+ break;
5788
+ case "sdf:noise":
5789
+ analysis.riskFlags.add("noise");
5790
+ break;
5791
+ case "sdf:voronoi":
5792
+ analysis.riskFlags.add("voronoi");
5793
+ analysis.minWallThickness = Math.min(analysis.minWallThickness, node.wallThickness);
5794
+ if (node.surfaceChild) visitSdfNode(node.surfaceChild, analysis);
5795
+ break;
5796
+ case "sdf:custom":
5797
+ analysis.riskFlags.add("custom");
5798
+ break;
5799
+ }
5800
+ }
5801
+ function positiveOrDefault(value, fallback) {
5802
+ if (value === void 0) return fallback;
5803
+ return requirePositiveFinite$3(value, "SDF meshing option");
5804
+ }
5805
+ function requirePositiveFinite$3(value, name) {
5806
+ if (!Number.isFinite(value) || value <= 0) {
5807
+ throw new Error(`${name} must be a positive finite number.`);
5808
+ }
5809
+ return value;
5810
+ }
5811
+ function cloneBounds$2(bounds) {
5812
+ return { min: [...bounds.min], max: [...bounds.max] };
5813
+ }
5814
+ function suggestEdgeLengthForSampleBudget(bounds, maxGridPoints) {
5815
+ const dx = bounds.max[0] - bounds.min[0];
5816
+ const dy = bounds.max[1] - bounds.min[1];
5817
+ const dz = bounds.max[2] - bounds.min[2];
5818
+ const volume = Math.max(dx * dy * dz, 1);
5819
+ return Math.cbrt(volume / Math.max(maxGridPoints, 8));
5820
+ }
5821
+ function formatBounds(bounds) {
5822
+ return `[${bounds.min.map(formatNumber$1).join(",")}]-[${bounds.max.map(formatNumber$1).join(",")}]`;
5823
+ }
5824
+ function formatMm(value) {
5825
+ return `${formatNumber$1(value)}mm`;
5826
+ }
5827
+ function formatNumber$1(value) {
5828
+ return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
5829
+ }
5830
+ function formatCount(value) {
5831
+ return Math.round(value).toLocaleString("en-US");
5832
+ }
5833
+ function formatBytes(bytes) {
5834
+ if (bytes < 1024 * 1024) return `${Math.ceil(bytes / 1024)} KB`;
5835
+ return `${Math.ceil(bytes / (1024 * 1024))} MB`;
5836
+ }
5512
5837
  const grad3 = new Float64Array([
5513
5838
  1,
5514
5839
  1,
@@ -5969,8 +6294,8 @@ function triplanarWeights(nx, ny, nz, sharpness) {
5969
6294
  const inv = 1 / sum2;
5970
6295
  return { wx: wx * inv, wy: wy * inv, wz: wz * inv };
5971
6296
  }
5972
- const { atan2, acos, cos: cos$2, sin: sin$2, sqrt: sqrt$2, PI: PI$2 } = Math;
5973
- const DEG$2 = PI$2 / 180;
6297
+ const { atan2, acos, cos: cos$3, sin: sin$3, sqrt: sqrt$3, PI: PI$3 } = Math;
6298
+ const DEG$3 = PI$3 / 180;
5974
6299
  const IDENTITY = (p2) => p2;
5975
6300
  function analyzeUV(node, override) {
5976
6301
  if (override) {
@@ -5997,10 +6322,10 @@ function analyzeNodeUV(node, toLocal) {
5997
6322
  return analyzeNodeUV(node.child, next);
5998
6323
  }
5999
6324
  case "sdf:rotate": {
6000
- const [rx, ry, rz] = node.degrees.map((d2) => d2 * DEG$2);
6001
- const cx = cos$2(rx), sx = sin$2(rx);
6002
- const cy = cos$2(ry), sy = sin$2(ry);
6003
- const cz = cos$2(rz), sz = sin$2(rz);
6325
+ const [rx, ry, rz] = node.degrees.map((d2) => d2 * DEG$3);
6326
+ const cx = cos$3(rx), sx = sin$3(rx);
6327
+ const cy = cos$3(ry), sy = sin$3(ry);
6328
+ const cz = cos$3(rz), sz = sin$3(rz);
6004
6329
  const prev = toLocal;
6005
6330
  const next = (p2) => {
6006
6331
  const pp = prev(p2);
@@ -6057,7 +6382,7 @@ function compileUVFunction(analysis) {
6057
6382
  return (p2) => {
6058
6383
  const lp = toLocal(p2);
6059
6384
  const u2 = atan2(lp[1], lp[0]) * R;
6060
- const len2 = sqrt$2(lp[0] * lp[0] + lp[1] * lp[1] + lp[2] * lp[2]);
6385
+ const len2 = sqrt$3(lp[0] * lp[0] + lp[1] * lp[1] + lp[2] * lp[2]);
6061
6386
  const v = acos(clampUnit(lp[2] / (len2 || 1))) * R;
6062
6387
  return [u2, v];
6063
6388
  };
@@ -6077,23 +6402,23 @@ function compileUVFunction(analysis) {
6077
6402
  return (p2) => {
6078
6403
  const lp = toLocal(p2);
6079
6404
  const u2 = atan2(lp[1], lp[0]) * R;
6080
- const xyDist = sqrt$2(lp[0] * lp[0] + lp[1] * lp[1]) - R;
6405
+ const xyDist = sqrt$3(lp[0] * lp[0] + lp[1] * lp[1]) - R;
6081
6406
  const v = atan2(lp[2], xyDist) * r;
6082
6407
  return [u2, v];
6083
6408
  };
6084
6409
  }
6085
6410
  }
6086
6411
  }
6087
- const { abs: abs$1, cos: cos$1, sin: sin$1, sqrt: sqrt$1, PI: PI$1 } = Math;
6088
- const TAU = 2 * PI$1;
6089
- const GRAD_EPS = 1e-9;
6412
+ const { abs: abs$1, cos: cos$2, sin: sin$2, sqrt: sqrt$2, PI: PI$2 } = Math;
6413
+ const TAU$1 = 2 * PI$2;
6414
+ const GRAD_EPS$1 = 1e-9;
6090
6415
  function gyroidValueAndGradient(x2, y2, z2, cellSize) {
6091
- const s = TAU / cellSize;
6416
+ const s = TAU$1 / cellSize;
6092
6417
  const xs = x2 * s;
6093
6418
  const ys = y2 * s;
6094
6419
  const zs = z2 * s;
6095
- const sx = sin$1(xs), sy = sin$1(ys), sz = sin$1(zs);
6096
- const cx = cos$1(xs), cy = cos$1(ys), cz = cos$1(zs);
6420
+ const sx = sin$2(xs), sy = sin$2(ys), sz = sin$2(zs);
6421
+ const cx = cos$2(xs), cy = cos$2(ys), cz = cos$2(zs);
6097
6422
  return {
6098
6423
  value: sx * cy + sy * cz + sz * cx,
6099
6424
  gx: s * (cx * cy - sz * sx),
@@ -6102,24 +6427,24 @@ function gyroidValueAndGradient(x2, y2, z2, cellSize) {
6102
6427
  };
6103
6428
  }
6104
6429
  function schwarzPValueAndGradient(x2, y2, z2, cellSize) {
6105
- const s = TAU / cellSize;
6430
+ const s = TAU$1 / cellSize;
6106
6431
  const xs = x2 * s;
6107
6432
  const ys = y2 * s;
6108
6433
  const zs = z2 * s;
6109
6434
  return {
6110
- value: cos$1(xs) + cos$1(ys) + cos$1(zs),
6111
- gx: -s * sin$1(xs),
6112
- gy: -s * sin$1(ys),
6113
- gz: -s * sin$1(zs)
6435
+ value: cos$2(xs) + cos$2(ys) + cos$2(zs),
6436
+ gx: -s * sin$2(xs),
6437
+ gy: -s * sin$2(ys),
6438
+ gz: -s * sin$2(zs)
6114
6439
  };
6115
6440
  }
6116
6441
  function diamondValueAndGradient(x2, y2, z2, cellSize) {
6117
- const s = TAU / cellSize;
6442
+ const s = TAU$1 / cellSize;
6118
6443
  const xs = x2 * s;
6119
6444
  const ys = y2 * s;
6120
6445
  const zs = z2 * s;
6121
- const sx = sin$1(xs), sy = sin$1(ys), sz = sin$1(zs);
6122
- const cx = cos$1(xs), cy = cos$1(ys), cz = cos$1(zs);
6446
+ const sx = sin$2(xs), sy = sin$2(ys), sz = sin$2(zs);
6447
+ const cx = cos$2(xs), cy = cos$2(ys), cz = cos$2(zs);
6123
6448
  return {
6124
6449
  value: sx * sy * sz + sx * cy * cz + cx * sy * cz + cx * cy * sz,
6125
6450
  gx: s * (cx * sy * sz + cx * cy * cz - sx * sy * cz - sx * cy * sz),
@@ -6128,12 +6453,12 @@ function diamondValueAndGradient(x2, y2, z2, cellSize) {
6128
6453
  };
6129
6454
  }
6130
6455
  function lidinoidValueAndGradient(x2, y2, z2, cellSize) {
6131
- const s = TAU / cellSize;
6456
+ const s = TAU$1 / cellSize;
6132
6457
  const sx2 = x2 * s, sy2 = y2 * s, sz2 = z2 * s;
6133
- const sx = sin$1(sx2), sy = sin$1(sy2), sz = sin$1(sz2);
6134
- const cx = cos$1(sx2), cy = cos$1(sy2), cz = cos$1(sz2);
6135
- const s2x = sin$1(2 * sx2), s2y = sin$1(2 * sy2), s2z = sin$1(2 * sz2);
6136
- const c2x = cos$1(2 * sx2), c2y = cos$1(2 * sy2), c2z = cos$1(2 * sz2);
6458
+ const sx = sin$2(sx2), sy = sin$2(sy2), sz = sin$2(sz2);
6459
+ const cx = cos$2(sx2), cy = cos$2(sy2), cz = cos$2(sz2);
6460
+ const s2x = sin$2(2 * sx2), s2y = sin$2(2 * sy2), s2z = sin$2(2 * sz2);
6461
+ const c2x = cos$2(2 * sx2), c2y = cos$2(2 * sy2), c2z = cos$2(2 * sz2);
6137
6462
  const val = s2x * cy * sz + s2y * cz * sx + s2z * cx * sy - c2x * c2y - c2y * c2z - c2z * c2x + 0.3;
6138
6463
  return {
6139
6464
  value: val,
@@ -6156,8 +6481,8 @@ function lidinoid$1(x2, y2, z2, cellSize, thickness, thicknessMode) {
6156
6481
  }
6157
6482
  function tpmsDistance({ value, gx, gy, gz }, thickness, thicknessMode) {
6158
6483
  if (thicknessMode !== "metric-approx") return abs$1(value) - thickness;
6159
- const grad = sqrt$1(gx * gx + gy * gy + gz * gz);
6160
- return abs$1(value) / Math.max(grad, GRAD_EPS) - thickness * 0.5;
6484
+ const grad = sqrt$2(gx * gx + gy * gy + gz * gz);
6485
+ return abs$1(value) / Math.max(grad, GRAD_EPS$1) - thickness * 0.5;
6161
6486
  }
6162
6487
  function mix(h) {
6163
6488
  h = (h ^ h >>> 16) * 2246822507 | 0;
@@ -6253,76 +6578,76 @@ function seededWorley3Surface(seed) {
6253
6578
  const s = seed | 0;
6254
6579
  return (x2, y2, z2, nx, ny, nz, threshold) => worleySurface(x2, y2, z2, s, nx, ny, nz, threshold);
6255
6580
  }
6256
- const { abs, cos, max, min, sin, sqrt, PI } = Math;
6257
- const DEG$1 = PI / 180;
6581
+ const { abs, cos: cos$1, max: max$1, min, sin: sin$1, sqrt: sqrt$1, PI: PI$1 } = Math;
6582
+ const DEG$2 = PI$1 / 180;
6258
6583
  function clamp$b(v, lo, hi) {
6259
6584
  return v < lo ? lo : v > hi ? hi : v;
6260
6585
  }
6261
- function length2(x2, y2) {
6262
- return sqrt(x2 * x2 + y2 * y2);
6586
+ function length2$1(x2, y2) {
6587
+ return sqrt$1(x2 * x2 + y2 * y2);
6263
6588
  }
6264
- function length3(x2, y2, z2) {
6265
- return sqrt(x2 * x2 + y2 * y2 + z2 * z2);
6589
+ function length3$1(x2, y2, z2) {
6590
+ return sqrt$1(x2 * x2 + y2 * y2 + z2 * z2);
6266
6591
  }
6267
- function sdSphere(px, py, pz, r) {
6268
- return length3(px, py, pz) - r;
6592
+ function sdSphere$1(px, py, pz, r) {
6593
+ return length3$1(px, py, pz) - r;
6269
6594
  }
6270
- function sdBox(px, py, pz, hx, hy, hz) {
6595
+ function sdBox$1(px, py, pz, hx, hy, hz) {
6271
6596
  const dx = abs(px) - hx;
6272
6597
  const dy = abs(py) - hy;
6273
6598
  const dz = abs(pz) - hz;
6274
- return length3(max(dx, 0), max(dy, 0), max(dz, 0)) + min(max(dx, dy, dz), 0);
6599
+ return length3$1(max$1(dx, 0), max$1(dy, 0), max$1(dz, 0)) + min(max$1(dx, dy, dz), 0);
6275
6600
  }
6276
- function sdCylinder(px, py, pz, h, r) {
6277
- const dx = length2(px, py) - r;
6601
+ function sdCylinder$1(px, py, pz, h, r) {
6602
+ const dx = length2$1(px, py) - r;
6278
6603
  const dz = abs(pz) - h * 0.5;
6279
- return length2(max(dx, 0), max(dz, 0)) + min(max(dx, dz), 0);
6604
+ return length2$1(max$1(dx, 0), max$1(dz, 0)) + min(max$1(dx, dz), 0);
6280
6605
  }
6281
- function sdTorus(px, py, pz, R, r) {
6282
- const qx = length2(px, py) - R;
6283
- return length2(qx, pz) - r;
6606
+ function sdTorus$1(px, py, pz, R, r) {
6607
+ const qx = length2$1(px, py) - R;
6608
+ return length2$1(qx, pz) - r;
6284
6609
  }
6285
- function sdCapsule(px, py, pz, h, r) {
6610
+ function sdCapsule$1(px, py, pz, h, r) {
6286
6611
  const halfH = h * 0.5;
6287
6612
  const cz = clamp$b(pz, -halfH, halfH);
6288
- return length3(px, py, pz - cz) - r;
6613
+ return length3$1(px, py, pz - cz) - r;
6289
6614
  }
6290
- function sdCone(px, py, pz, h, r) {
6291
- const q = length2(px, py);
6292
- const cLen = length2(h, r);
6615
+ function sdCone$1(px, py, pz, h, r) {
6616
+ const q = length2$1(px, py);
6617
+ const cLen = length2$1(h, r);
6293
6618
  const nx = h / cLen;
6294
6619
  const nz = -r / cLen;
6295
- const d2 = max(nx * q + nz * (pz - h), -pz, pz - h);
6620
+ const d2 = max$1(nx * q + nz * (pz - h), -pz, pz - h);
6296
6621
  return d2;
6297
6622
  }
6298
- function sdTaperedSegment(px, py, pz, ax, ay, az, bx, by, bz, ra, rb) {
6623
+ function sdTaperedSegment$1(px, py, pz, ax, ay, az, bx, by, bz, ra, rb) {
6299
6624
  const vx = bx - ax;
6300
6625
  const vy = by - ay;
6301
6626
  const vz = bz - az;
6302
6627
  const len2 = vx * vx + vy * vy + vz * vz;
6303
- if (len2 <= 1e-12) return sdSphere(px - ax, py - ay, pz - az, max(ra, rb));
6628
+ if (len2 <= 1e-12) return sdSphere$1(px - ax, py - ay, pz - az, max$1(ra, rb));
6304
6629
  const h = clamp$b(((px - ax) * vx + (py - ay) * vy + (pz - az) * vz) / len2, 0, 1);
6305
- return length3(px - (ax + vx * h), py - (ay + vy * h), pz - (az + vz * h)) - (ra + (rb - ra) * h);
6630
+ return length3$1(px - (ax + vx * h), py - (ay + vy * h), pz - (az + vz * h)) - (ra + (rb - ra) * h);
6306
6631
  }
6307
6632
  function sdPolylineSweep3(node, x2, y2, z2) {
6308
6633
  let d2 = 1e20;
6309
6634
  for (let i = 0; i < node.points.length - 1; i++) {
6310
6635
  const a2 = node.points[i];
6311
6636
  const b = node.points[i + 1];
6312
- 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]);
6313
- d2 = i === 0 ? segment : smin(d2, segment, node.blend);
6637
+ 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]);
6638
+ d2 = i === 0 ? segment : smin$1(d2, segment, node.blend);
6314
6639
  }
6315
6640
  return d2;
6316
6641
  }
6317
- function smin(a2, b, k2) {
6642
+ function smin$1(a2, b, k2) {
6318
6643
  if (k2 <= 0) return min(a2, b);
6319
- const h = max(k2 - abs(a2 - b), 0) / k2;
6644
+ const h = max$1(k2 - abs(a2 - b), 0) / k2;
6320
6645
  return min(a2, b) - h * h * h * k2 * (1 / 6);
6321
6646
  }
6322
- function smax(a2, b, k2) {
6323
- return -smin(-a2, -b, k2);
6647
+ function smax$1(a2, b, k2) {
6648
+ return -smin$1(-a2, -b, k2);
6324
6649
  }
6325
- function repeatCoord(v, spacing, count) {
6650
+ function repeatCoord$1(v, spacing, count) {
6326
6651
  if (spacing <= 0) return v;
6327
6652
  if (count > 0) {
6328
6653
  const center = (count - 1) * 0.5;
@@ -6331,31 +6656,138 @@ function repeatCoord(v, spacing, count) {
6331
6656
  }
6332
6657
  return v - spacing * Math.round(v / spacing);
6333
6658
  }
6659
+ function positiveMod(v, period) {
6660
+ return (v % period + period) % period;
6661
+ }
6662
+ function evalStripesPattern(u2, v, directionX, directionY, spacing, width, depth) {
6663
+ const coord = u2 * directionX + v * directionY;
6664
+ const d2 = abs(coord - Math.round(coord / spacing) * spacing);
6665
+ const profile = max$1(0, 1 - d2 / (width * 0.5));
6666
+ return -(profile * profile) * depth;
6667
+ }
6668
+ function evalOverUnderWeavePattern(u2, v, spacingX, spacingY, widthX, widthY, depth, underScale) {
6669
+ const su = u2 / spacingX;
6670
+ const sv = v / spacingY;
6671
+ let pU = max$1(0, 1 - abs(su - Math.round(su)) * spacingX / (widthX * 0.5));
6672
+ let pV = max$1(0, 1 - abs(sv - Math.round(sv)) * spacingY / (widthY * 0.5));
6673
+ pU *= pU;
6674
+ pV *= pV;
6675
+ const checker = (Math.round(su) & 65535) + (Math.round(sv) & 65535) & 1;
6676
+ const top = checker ? pV : pU;
6677
+ const bot = checker ? pU : pV;
6678
+ return -max$1(top, bot * underScale) * depth;
6679
+ }
6680
+ function compileTypedSurfacePattern(pattern) {
6681
+ switch (pattern.kind) {
6682
+ case "surfacePattern:constant":
6683
+ return () => pattern.value;
6684
+ case "surfacePattern:sineWave": {
6685
+ const { direction: direction2, wavelength, amplitude, phase, bias } = pattern;
6686
+ const frequency = 2 * PI$1 / wavelength;
6687
+ return (u2, v) => bias + sin$1((u2 * direction2[0] + v * direction2[1]) * frequency + phase) * amplitude;
6688
+ }
6689
+ case "surfacePattern:stripes": {
6690
+ const { direction: direction2, spacing, width, depth } = pattern;
6691
+ return (u2, v) => evalStripesPattern(u2, v, direction2[0], direction2[1], spacing, width, depth);
6692
+ }
6693
+ case "surfacePattern:overUnderWeave": {
6694
+ const { spacing, threadWidth, depth, underScale } = pattern;
6695
+ return (u2, v) => evalOverUnderWeavePattern(u2, v, spacing[0], spacing[1], threadWidth[0], threadWidth[1], depth, underScale);
6696
+ }
6697
+ case "surfacePattern:abs": {
6698
+ const child = compileTypedSurfacePattern(pattern.child);
6699
+ return (u2, v) => abs(child(u2, v));
6700
+ }
6701
+ case "surfacePattern:negate": {
6702
+ const child = compileTypedSurfacePattern(pattern.child);
6703
+ return (u2, v) => -child(u2, v);
6704
+ }
6705
+ case "surfacePattern:add": {
6706
+ const children = pattern.children.map(compileTypedSurfacePattern);
6707
+ return (u2, v) => children.reduce((sum2, child) => sum2 + child(u2, v), 0);
6708
+ }
6709
+ case "surfacePattern:multiply": {
6710
+ const children = pattern.children.map(compileTypedSurfacePattern);
6711
+ return (u2, v) => children.reduce((product, child) => product * child(u2, v), 1);
6712
+ }
6713
+ case "surfacePattern:min": {
6714
+ const children = pattern.children.map(compileTypedSurfacePattern);
6715
+ if (children.length === 0) return () => 0;
6716
+ return (u2, v) => children.reduce((value, child) => min(value, child(u2, v)), Infinity);
6717
+ }
6718
+ case "surfacePattern:max": {
6719
+ const children = pattern.children.map(compileTypedSurfacePattern);
6720
+ if (children.length === 0) return () => 0;
6721
+ return (u2, v) => children.reduce((value, child) => max$1(value, child(u2, v)), -Infinity);
6722
+ }
6723
+ case "surfacePattern:clamp": {
6724
+ const child = compileTypedSurfacePattern(pattern.child);
6725
+ return (u2, v) => clamp$b(child(u2, v), pattern.min, pattern.max);
6726
+ }
6727
+ }
6728
+ }
6729
+ function estimateSurfacePatternAmplitude(pattern) {
6730
+ switch (pattern.kind) {
6731
+ case "surfacePattern:constant":
6732
+ return abs(pattern.value);
6733
+ case "surfacePattern:sineWave":
6734
+ return abs(pattern.bias) + abs(pattern.amplitude);
6735
+ case "surfacePattern:stripes":
6736
+ case "surfacePattern:overUnderWeave":
6737
+ return pattern.depth;
6738
+ case "surfacePattern:abs":
6739
+ case "surfacePattern:negate":
6740
+ return estimateSurfacePatternAmplitude(pattern.child);
6741
+ case "surfacePattern:add": {
6742
+ const amplitudes = pattern.children.map(estimateSurfacePatternAmplitude);
6743
+ return amplitudes.every((value) => value !== null) ? amplitudes.reduce((sum2, value) => sum2 + value, 0) : null;
6744
+ }
6745
+ case "surfacePattern:multiply": {
6746
+ const amplitudes = pattern.children.map(estimateSurfacePatternAmplitude);
6747
+ return amplitudes.every((value) => value !== null) ? amplitudes.reduce((product, value) => product * value, 1) : null;
6748
+ }
6749
+ case "surfacePattern:min":
6750
+ case "surfacePattern:max": {
6751
+ const amplitudes = pattern.children.map(estimateSurfacePatternAmplitude);
6752
+ return amplitudes.every((value) => value !== null) ? max$1(...amplitudes) : null;
6753
+ }
6754
+ case "surfacePattern:clamp":
6755
+ return max$1(abs(pattern.min), abs(pattern.max));
6756
+ }
6757
+ }
6758
+ function compileSurfacePattern(node) {
6759
+ if (node.pattern) return compileTypedSurfacePattern(node.pattern);
6760
+ const constEntries = Object.entries(node.constants ?? {});
6761
+ const constNames = constEntries.map(([k2]) => k2);
6762
+ const constValues = constEntries.map(([, v]) => v);
6763
+ const patternFn = new Function("u", "v", ...constNames, `return (${node.patternBody});`);
6764
+ return (u2, v) => patternFn(u2, v, ...constValues);
6765
+ }
6334
6766
  function compileSdfNode3(node) {
6335
6767
  switch (node.kind) {
6336
6768
  case "sdf:sphere": {
6337
6769
  const r = node.radius;
6338
- return (x2, y2, z2) => sdSphere(x2, y2, z2, r);
6770
+ return (x2, y2, z2) => sdSphere$1(x2, y2, z2, r);
6339
6771
  }
6340
6772
  case "sdf:box": {
6341
6773
  const [hx, hy, hz] = node.halfExtents;
6342
- return (x2, y2, z2) => sdBox(x2, y2, z2, hx, hy, hz);
6774
+ return (x2, y2, z2) => sdBox$1(x2, y2, z2, hx, hy, hz);
6343
6775
  }
6344
6776
  case "sdf:cylinder": {
6345
6777
  const { height: h, radius: r } = node;
6346
- return (x2, y2, z2) => sdCylinder(x2, y2, z2, h, r);
6778
+ return (x2, y2, z2) => sdCylinder$1(x2, y2, z2, h, r);
6347
6779
  }
6348
6780
  case "sdf:torus": {
6349
6781
  const { majorRadius: R, minorRadius: r } = node;
6350
- return (x2, y2, z2) => sdTorus(x2, y2, z2, R, r);
6782
+ return (x2, y2, z2) => sdTorus$1(x2, y2, z2, R, r);
6351
6783
  }
6352
6784
  case "sdf:capsule": {
6353
6785
  const { height: h, radius: r } = node;
6354
- return (x2, y2, z2) => sdCapsule(x2, y2, z2, h, r);
6786
+ return (x2, y2, z2) => sdCapsule$1(x2, y2, z2, h, r);
6355
6787
  }
6356
6788
  case "sdf:cone": {
6357
6789
  const { height: h, radius: r } = node;
6358
- return (x2, y2, z2) => sdCone(x2, y2, z2, h, r);
6790
+ return (x2, y2, z2) => sdCone$1(x2, y2, z2, h, r);
6359
6791
  }
6360
6792
  case "sdf:polylineSweep": {
6361
6793
  return (x2, y2, z2) => sdPolylineSweep3(node, x2, y2, z2);
@@ -6372,7 +6804,7 @@ function compileSdfNode3(node) {
6372
6804
  const fns = node.children.map(compileSdfNode3);
6373
6805
  return (x2, y2, z2) => {
6374
6806
  let d2 = fns[0](x2, y2, z2);
6375
- for (let i = 1; i < fns.length; i++) d2 = max(d2, -fns[i](x2, y2, z2));
6807
+ for (let i = 1; i < fns.length; i++) d2 = max$1(d2, -fns[i](x2, y2, z2));
6376
6808
  return d2;
6377
6809
  };
6378
6810
  }
@@ -6380,7 +6812,7 @@ function compileSdfNode3(node) {
6380
6812
  const fns = node.children.map(compileSdfNode3);
6381
6813
  return (x2, y2, z2) => {
6382
6814
  let d2 = fns[0](x2, y2, z2);
6383
- for (let i = 1; i < fns.length; i++) d2 = max(d2, fns[i](x2, y2, z2));
6815
+ for (let i = 1; i < fns.length; i++) d2 = max$1(d2, fns[i](x2, y2, z2));
6384
6816
  return d2;
6385
6817
  };
6386
6818
  }
@@ -6389,7 +6821,7 @@ function compileSdfNode3(node) {
6389
6821
  const k2 = node.radius;
6390
6822
  return (x2, y2, z2) => {
6391
6823
  let d2 = fns[0](x2, y2, z2);
6392
- for (let i = 1; i < fns.length; i++) d2 = smin(d2, fns[i](x2, y2, z2), k2);
6824
+ for (let i = 1; i < fns.length; i++) d2 = smin$1(d2, fns[i](x2, y2, z2), k2);
6393
6825
  return d2;
6394
6826
  };
6395
6827
  }
@@ -6398,7 +6830,7 @@ function compileSdfNode3(node) {
6398
6830
  const k2 = node.radius;
6399
6831
  return (x2, y2, z2) => {
6400
6832
  let d2 = fns[0](x2, y2, z2);
6401
- for (let i = 1; i < fns.length; i++) d2 = smax(d2, -fns[i](x2, y2, z2), k2);
6833
+ for (let i = 1; i < fns.length; i++) d2 = smax$1(d2, -fns[i](x2, y2, z2), k2);
6402
6834
  return d2;
6403
6835
  };
6404
6836
  }
@@ -6407,7 +6839,7 @@ function compileSdfNode3(node) {
6407
6839
  const k2 = node.radius;
6408
6840
  return (x2, y2, z2) => {
6409
6841
  let d2 = fns[0](x2, y2, z2);
6410
- for (let i = 1; i < fns.length; i++) d2 = smax(d2, fns[i](x2, y2, z2), k2);
6842
+ for (let i = 1; i < fns.length; i++) d2 = smax$1(d2, fns[i](x2, y2, z2), k2);
6411
6843
  return d2;
6412
6844
  };
6413
6845
  }
@@ -6425,10 +6857,10 @@ function compileSdfNode3(node) {
6425
6857
  }
6426
6858
  case "sdf:rotate": {
6427
6859
  const fn = compileSdfNode3(node.child);
6428
- const [rx, ry, rz] = node.degrees.map((d2) => d2 * DEG$1);
6429
- const cx = cos(rx), sx = sin(rx);
6430
- const cy = cos(ry), sy = sin(ry);
6431
- const cz = cos(rz), sz = sin(rz);
6860
+ const [rx, ry, rz] = node.degrees.map((d2) => d2 * DEG$2);
6861
+ const cx = cos$1(rx), sx = sin$1(rx);
6862
+ const cy = cos$1(ry), sy = sin$1(ry);
6863
+ const cz = cos$1(rz), sz = sin$1(rz);
6432
6864
  return (x2, y2, z2) => {
6433
6865
  const x1 = cz * x2 + sz * y2;
6434
6866
  const y1 = -sz * x2 + cz * y2;
@@ -6447,10 +6879,10 @@ function compileSdfNode3(node) {
6447
6879
  }
6448
6880
  case "sdf:twist": {
6449
6881
  const fn = compileSdfNode3(node.child);
6450
- const k2 = node.degreesPerUnit * DEG$1;
6882
+ const k2 = node.degreesPerUnit * DEG$2;
6451
6883
  return (x2, y2, z2) => {
6452
6884
  const angle = k2 * z2;
6453
- const c2 = cos(angle), s = sin(angle);
6885
+ const c2 = cos$1(angle), s = sin$1(angle);
6454
6886
  return fn(c2 * x2 - s * y2, s * x2 + c2 * y2, z2);
6455
6887
  };
6456
6888
  }
@@ -6459,7 +6891,7 @@ function compileSdfNode3(node) {
6459
6891
  const r = node.radius;
6460
6892
  return (x2, y2, z2) => {
6461
6893
  const angle = x2 / r;
6462
- const c2 = cos(angle), s = sin(angle);
6894
+ const c2 = cos$1(angle), s = sin$1(angle);
6463
6895
  return fn((r + y2) * s, (r + y2) * c2 - r, z2);
6464
6896
  };
6465
6897
  }
@@ -6467,7 +6899,19 @@ function compileSdfNode3(node) {
6467
6899
  const fn = compileSdfNode3(node.child);
6468
6900
  const [sx, sy, sz] = node.spacing;
6469
6901
  const [cx, cy, cz] = node.count;
6470
- return (x2, y2, z2) => fn(repeatCoord(x2, sx, cx), repeatCoord(y2, sy, cy), repeatCoord(z2, sz, cz));
6902
+ return (x2, y2, z2) => fn(repeatCoord$1(x2, sx, cx), repeatCoord$1(y2, sy, cy), repeatCoord$1(z2, sz, cz));
6903
+ }
6904
+ case "sdf:circularArray": {
6905
+ const fn = compileSdfNode3(node.child);
6906
+ const da = 2 * PI$1 / node.count;
6907
+ const offset2 = node.offset;
6908
+ return (x2, y2, z2) => {
6909
+ const r = length2$1(x2, y2);
6910
+ const a2 = positiveMod(Math.atan2(y2, x2), da);
6911
+ const d1 = fn(cos$1(a2 - da) * r - offset2, sin$1(a2 - da) * r, z2);
6912
+ const d2 = fn(cos$1(a2) * r - offset2, sin$1(a2) * r, z2);
6913
+ return min(d1, d2);
6914
+ };
6471
6915
  }
6472
6916
  case "sdf:shell": {
6473
6917
  const fn = compileSdfNode3(node.child);
@@ -6484,10 +6928,7 @@ function compileSdfNode3(node) {
6484
6928
  }
6485
6929
  case "sdf:surfaceDisplace": {
6486
6930
  const childFn = compileSdfNode3(node.child);
6487
- const constEntries = Object.entries(node.constants ?? {});
6488
- const constNames = constEntries.map(([k2]) => k2);
6489
- const constValues = constEntries.map(([, v]) => v);
6490
- const patternFn = new Function("u", "v", ...constNames, `return (${node.patternBody});`);
6931
+ const patternFn = compileSurfacePattern(node);
6491
6932
  const uvMode = node.uvMode && node.uvMode !== "auto" ? node.uvMode : void 0;
6492
6933
  const analysis = analyzeUV(node.child, uvMode);
6493
6934
  const uvFn = compileUVFunction(analysis);
@@ -6499,7 +6940,7 @@ function compileSdfNode3(node) {
6499
6940
  p2[1] = y2;
6500
6941
  p2[2] = z2;
6501
6942
  const [u2, v] = uvFn(p2);
6502
- return d2 + patternFn(u2, v, ...constValues);
6943
+ return d2 + patternFn(u2, v);
6503
6944
  };
6504
6945
  }
6505
6946
  const sharpness = node.triplanarSharpness ?? 4;
@@ -6510,9 +6951,9 @@ function compileSdfNode3(node) {
6510
6951
  const gy = childFn(x2, y2 + eps, z2) - childFn(x2, y2 - eps, z2);
6511
6952
  const gz = childFn(x2, y2, z2 + eps) - childFn(x2, y2, z2 - eps);
6512
6953
  const { wx, wy, wz } = triplanarWeights(gx, gy, gz, sharpness);
6513
- const hX = patternFn(y2, z2, ...constValues);
6514
- const hY = patternFn(x2, z2, ...constValues);
6515
- const hZ = patternFn(x2, y2, ...constValues);
6954
+ const hX = patternFn(y2, z2);
6955
+ const hY = patternFn(x2, z2);
6956
+ const hZ = patternFn(x2, y2);
6516
6957
  return d2 + wx * hX + wy * hY + wz * hZ;
6517
6958
  };
6518
6959
  }
@@ -6582,7 +7023,7 @@ function compileSdfNode3(node) {
6582
7023
  const gx = gradFn(x2 + eps, y2, z2) - gradFn(x2 - eps, y2, z2);
6583
7024
  const gy = gradFn(x2, y2 + eps, z2) - gradFn(x2, y2 - eps, z2);
6584
7025
  const gz = gradFn(x2, y2, z2 + eps) - gradFn(x2, y2, z2 - eps);
6585
- const glen = sqrt(gx * gx + gy * gy + gz * gz);
7026
+ const glen = sqrt$1(gx * gx + gy * gy + gz * gz);
6586
7027
  let nx = 0, ny = 0, nz = 0;
6587
7028
  if (glen > 1e-10) {
6588
7029
  const invG = 1 / glen;
@@ -6644,7 +7085,7 @@ function estimateSdfBounds(node) {
6644
7085
  for (const point2 of node.points) {
6645
7086
  for (let i = 0; i < 3; i++) {
6646
7087
  minPoint[i] = min(minPoint[i], point2[i]);
6647
- maxPoint[i] = max(maxPoint[i], point2[i]);
7088
+ maxPoint[i] = max$1(maxPoint[i], point2[i]);
6648
7089
  }
6649
7090
  }
6650
7091
  return padBounds({ min: minPoint, max: maxPoint }, pad);
@@ -6675,7 +7116,7 @@ function estimateSdfBounds(node) {
6675
7116
  }
6676
7117
  case "sdf:rotate": {
6677
7118
  const b = estimateSdfBounds(node.child);
6678
- 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])));
7119
+ 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])));
6679
7120
  return { min: [-r, -r, -r], max: [r, r, r] };
6680
7121
  }
6681
7122
  case "sdf:scale": {
@@ -6689,7 +7130,7 @@ function estimateSdfBounds(node) {
6689
7130
  case "sdf:twist":
6690
7131
  case "sdf:bend": {
6691
7132
  const b = estimateSdfBounds(node.child);
6692
- 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;
7133
+ 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;
6693
7134
  return { min: [-r, -r, -r], max: [r, r, r] };
6694
7135
  }
6695
7136
  case "sdf:repeat": {
@@ -6710,16 +7151,29 @@ function estimateSdfBounds(node) {
6710
7151
  const [zMin, zMax] = expand(sz, cz, b.min[2], b.max[2]);
6711
7152
  return { min: [xMin, yMin, zMin], max: [xMax, yMax, zMax] };
6712
7153
  }
7154
+ case "sdf:circularArray": {
7155
+ const b = estimateSdfBounds(node.child);
7156
+ const x0 = b.min[0] + node.offset;
7157
+ const x1 = b.max[0] + node.offset;
7158
+ const y0 = b.min[1];
7159
+ const y1 = b.max[1];
7160
+ const r = max$1(length2$1(x0, y0), length2$1(x0, y1), length2$1(x1, y0), length2$1(x1, y1));
7161
+ return { min: [-r, -r, b.min[2]], max: [r, r, b.max[2]] };
7162
+ }
6713
7163
  case "sdf:shell": {
6714
7164
  const b = estimateSdfBounds(node.child);
6715
7165
  const t = node.thickness * 0.5;
6716
7166
  return padBounds(b, t);
6717
7167
  }
6718
- case "sdf:displace":
6719
- case "sdf:surfaceDisplace": {
7168
+ case "sdf:displace": {
6720
7169
  const b = estimateSdfBounds(node.child);
6721
7170
  return padBounds(b, 5);
6722
7171
  }
7172
+ case "sdf:surfaceDisplace": {
7173
+ const b = estimateSdfBounds(node.child);
7174
+ if (!node.pattern) return padBounds(b, 5);
7175
+ return padBounds(b, estimateSurfacePatternAmplitude(node.pattern) ?? 5);
7176
+ }
6723
7177
  case "sdf:onion": {
6724
7178
  const b = estimateSdfBounds(node.child);
6725
7179
  return padBounds(b, node.layers * node.thickness);
@@ -6756,7 +7210,7 @@ function unionBounds(bounds, pad) {
6756
7210
  for (const b of bounds) {
6757
7211
  for (let i = 0; i < 3; i++) {
6758
7212
  result.min[i] = min(result.min[i], b.min[i]);
6759
- result.max[i] = max(result.max[i], b.max[i]);
7213
+ result.max[i] = max$1(result.max[i], b.max[i]);
6760
7214
  }
6761
7215
  }
6762
7216
  if (pad > 0) return padBounds(result, pad);
@@ -6769,7 +7223,7 @@ function intersectBounds(bounds, pad) {
6769
7223
  };
6770
7224
  for (const b of bounds) {
6771
7225
  for (let i = 0; i < 3; i++) {
6772
- result.min[i] = max(result.min[i], b.min[i]);
7226
+ result.min[i] = max$1(result.min[i], b.min[i]);
6773
7227
  result.max[i] = min(result.max[i], b.max[i]);
6774
7228
  }
6775
7229
  }
@@ -6785,242 +7239,544 @@ function padBounds(b, pad) {
6785
7239
  max: [b.max[0] + pad, b.max[1] + pad, b.max[2] + pad]
6786
7240
  };
6787
7241
  }
6788
- const DEFAULT_MAX_GRID_POINTS = 8e6;
6789
- const DEFAULT_MIN_EDGE_LENGTH = 0.15;
6790
- function resolveSdfMeshingSettings(tree, bounds, options = {}) {
6791
- const quality = options.quality ?? "preview";
6792
- const minEdgeLength = positiveOrDefault(options.minEdgeLength, DEFAULT_MIN_EDGE_LENGTH);
6793
- const maxGridPoints = positiveOrDefault(options.maxGridPoints, DEFAULT_MAX_GRID_POINTS);
6794
- const tolerance = options.tolerance !== void 0 ? requirePositiveFinite$2(options.tolerance, "SDF tolerance") : void 0;
6795
- const minFeatureSize = options.minFeatureSize !== void 0 ? requirePositiveFinite$2(options.minFeatureSize, "SDF minFeatureSize") : void 0;
6796
- const maxTriangles = options.maxTriangles !== void 0 ? Math.floor(requirePositiveFinite$2(options.maxTriangles, "SDF maxTriangles")) : void 0;
6797
- const analysis = analyzeSdfTree(tree);
6798
- const warnings = [];
6799
- let edgeLength2;
6800
- if (options.edgeLength !== void 0) {
6801
- edgeLength2 = requirePositiveFinite$2(options.edgeLength, "SDF edgeLength");
6802
- if (edgeLength2 < minEdgeLength) {
6803
- warnings.push(`edgeLength ${formatMm(edgeLength2)} was clamped to minimum ${formatMm(minEdgeLength)}.`);
6804
- edgeLength2 = minEdgeLength;
6805
- }
6806
- } else {
6807
- edgeLength2 = resolveDefaultEdgeLength(bounds, quality, minEdgeLength, analysis, options);
7242
+ const { PI } = Math;
7243
+ const DEG$1 = PI / 180;
7244
+ const TAU = 2 * PI;
7245
+ const GRAD_EPS = 1e-9;
7246
+ const Op = {
7247
+ Const: 0,
7248
+ Neg: 1,
7249
+ Abs: 2,
7250
+ Sqrt: 3,
7251
+ Sin: 4,
7252
+ Cos: 5,
7253
+ Round: 6,
7254
+ Add: 7,
7255
+ Sub: 8,
7256
+ Mul: 9,
7257
+ Div: 10,
7258
+ Min: 11,
7259
+ Max: 12
7260
+ };
7261
+ class UnsupportedSdfProgramNodeError extends Error {
7262
+ constructor(message) {
7263
+ super(message);
7264
+ this.name = "UnsupportedSdfProgramNodeError";
6808
7265
  }
6809
- if (analysis.minWallThickness < Infinity && analysis.minWallThickness < edgeLength2 * 2) {
6810
- analysis.riskFlags.add("thin-shell");
6811
- warnings.push(
6812
- `shell/wall thickness ${formatMm(analysis.minWallThickness)} is below 2 x edgeLength ${formatMm(edgeLength2)}; thin features may be under-sampled.`
6813
- );
7266
+ }
7267
+ class SdfProgramBuilder {
7268
+ constructor() {
7269
+ __publicField(this, "opcodes", []);
7270
+ __publicField(this, "argA", []);
7271
+ __publicField(this, "argB", []);
7272
+ __publicField(this, "argC", []);
7273
+ __publicField(this, "constants", []);
7274
+ __publicField(this, "x", 0);
7275
+ __publicField(this, "y", 1);
7276
+ __publicField(this, "z", 2);
6814
7277
  }
6815
- if (!options.bounds && analysis.hasInfiniteRepeat) {
6816
- warnings.push("infinite repeat bounds are heuristic; pass .toShape({ bounds }) for predictable clipping.");
7278
+ constant(value) {
7279
+ const index2 = this.constants.length;
7280
+ this.constants.push(value);
7281
+ return this.push(Op.Const, 0, 0, index2);
6817
7282
  }
6818
- if (!options.bounds && analysis.riskFlags.has("noise")) {
6819
- warnings.push("noise field bounds are heuristic; pass .toShape({ bounds }) for predictable clipping.");
7283
+ neg(a2) {
7284
+ return this.push(Op.Neg, a2);
6820
7285
  }
6821
- if (!options.bounds && (analysis.riskFlags.has("tpms") || analysis.riskFlags.has("voronoi"))) {
6822
- warnings.push("TPMS/Voronoi bounds are heuristic unless clipped or passed explicitly.");
7286
+ abs(a2) {
7287
+ return this.push(Op.Abs, a2);
6823
7288
  }
6824
- if (analysis.hasLegacyTpmsThreshold) {
6825
- warnings.push("TPMS thickness is using legacy field-threshold units; use wallThickness for approximate millimeters.");
7289
+ sqrt(a2) {
7290
+ return this.push(Op.Sqrt, a2);
7291
+ }
7292
+ sin(a2) {
7293
+ return this.push(Op.Sin, a2);
7294
+ }
7295
+ cos(a2) {
7296
+ return this.push(Op.Cos, a2);
7297
+ }
7298
+ round(a2) {
7299
+ return this.push(Op.Round, a2);
7300
+ }
7301
+ add(a2, b) {
7302
+ return this.push(Op.Add, a2, b);
7303
+ }
7304
+ sub(a2, b) {
7305
+ return this.push(Op.Sub, a2, b);
7306
+ }
7307
+ mul(a2, b) {
7308
+ return this.push(Op.Mul, a2, b);
7309
+ }
7310
+ div(a2, b) {
7311
+ return this.push(Op.Div, a2, b);
7312
+ }
7313
+ min(a2, b) {
7314
+ return this.push(Op.Min, a2, b);
7315
+ }
7316
+ max(a2, b) {
7317
+ return this.push(Op.Max, a2, b);
7318
+ }
7319
+ finalize(output) {
7320
+ return {
7321
+ opcodes: Uint8Array.from(this.opcodes),
7322
+ argA: Int32Array.from(this.argA),
7323
+ argB: Int32Array.from(this.argB),
7324
+ argC: Int32Array.from(this.argC),
7325
+ constants: Float64Array.from(this.constants),
7326
+ output,
7327
+ slotCount: this.opcodes.length + 3
7328
+ };
7329
+ }
7330
+ push(op, a2 = 0, b = 0, c2 = 0) {
7331
+ const slot2 = this.opcodes.length + 3;
7332
+ this.opcodes.push(op);
7333
+ this.argA.push(a2);
7334
+ this.argB.push(b);
7335
+ this.argC.push(c2);
7336
+ return slot2;
7337
+ }
7338
+ }
7339
+ function compileSdfProgramEvaluator3(program) {
7340
+ const lines = ["const { abs, sqrt, sin, cos, round, min, max } = Math;", "let v0 = x;", "let v1 = y;", "let v2 = z;"];
7341
+ const { opcodes, argA, argB, argC, constants, output } = program;
7342
+ for (let i = 0; i < opcodes.length; i++) {
7343
+ const slot2 = `v${i + 3}`;
7344
+ const a2 = `v${argA[i]}`;
7345
+ const b = `v${argB[i]}`;
7346
+ switch (opcodes[i]) {
7347
+ case Op.Const:
7348
+ lines.push(`let ${slot2} = ${numberLiteral(constants[argC[i]])};`);
7349
+ break;
7350
+ case Op.Neg:
7351
+ lines.push(`let ${slot2} = -${a2};`);
7352
+ break;
7353
+ case Op.Abs:
7354
+ lines.push(`let ${slot2} = abs(${a2});`);
7355
+ break;
7356
+ case Op.Sqrt:
7357
+ lines.push(`let ${slot2} = sqrt(${a2});`);
7358
+ break;
7359
+ case Op.Sin:
7360
+ lines.push(`let ${slot2} = sin(${a2});`);
7361
+ break;
7362
+ case Op.Cos:
7363
+ lines.push(`let ${slot2} = cos(${a2});`);
7364
+ break;
7365
+ case Op.Round:
7366
+ lines.push(`let ${slot2} = round(${a2});`);
7367
+ break;
7368
+ case Op.Add:
7369
+ lines.push(`let ${slot2} = ${a2} + ${b};`);
7370
+ break;
7371
+ case Op.Sub:
7372
+ lines.push(`let ${slot2} = ${a2} - ${b};`);
7373
+ break;
7374
+ case Op.Mul:
7375
+ lines.push(`let ${slot2} = ${a2} * ${b};`);
7376
+ break;
7377
+ case Op.Div:
7378
+ lines.push(`let ${slot2} = ${a2} / ${b};`);
7379
+ break;
7380
+ case Op.Min:
7381
+ lines.push(`let ${slot2} = min(${a2}, ${b});`);
7382
+ break;
7383
+ case Op.Max:
7384
+ lines.push(`let ${slot2} = max(${a2}, ${b});`);
7385
+ break;
7386
+ default:
7387
+ throw new Error(`Unknown SdfProgram opcode ${opcodes[i]} at instruction ${i}.`);
7388
+ }
6826
7389
  }
7390
+ lines.push(`return v${output};`);
7391
+ return new Function("Math", `return function sdfProgramEval(x, y, z) {
7392
+ ${lines.join("\n")}
7393
+ };`)(Math);
7394
+ }
7395
+ function numberLiteral(value) {
7396
+ if (Number.isNaN(value)) return "NaN";
7397
+ if (value === Number.POSITIVE_INFINITY) return "Infinity";
7398
+ if (value === Number.NEGATIVE_INFINITY) return "-Infinity";
7399
+ return String(value);
7400
+ }
7401
+ function clampSlot(b, v, lo, hi) {
7402
+ return b.min(b.max(v, b.constant(lo)), b.constant(hi));
7403
+ }
7404
+ function length2(b, x2, y2) {
7405
+ return b.sqrt(b.add(b.mul(x2, x2), b.mul(y2, y2)));
7406
+ }
7407
+ function length3(b, x2, y2, z2) {
7408
+ return b.sqrt(b.add(b.add(b.mul(x2, x2), b.mul(y2, y2)), b.mul(z2, z2)));
7409
+ }
7410
+ function smin(b, a2, child, k2) {
7411
+ if (k2 <= 0) return b.min(a2, child);
7412
+ const h = b.div(b.max(b.sub(b.constant(k2), b.abs(b.sub(a2, child))), b.constant(0)), b.constant(k2));
7413
+ return b.sub(b.min(a2, child), b.mul(b.mul(b.mul(h, h), h), b.constant(k2 / 6)));
7414
+ }
7415
+ function smax(b, a2, child, k2) {
7416
+ return b.neg(smin(b, b.neg(a2), b.neg(child), k2));
7417
+ }
7418
+ function emitScaledTrig(b, x2, y2, z2, cellSize) {
7419
+ const s = b.constant(TAU / cellSize);
7420
+ const xs = b.mul(x2, s);
7421
+ const ys = b.mul(y2, s);
7422
+ const zs = b.mul(z2, s);
6827
7423
  return {
6828
- quality,
6829
- edgeLength: edgeLength2,
6830
- tolerance,
6831
- minFeatureSize,
6832
- minEdgeLength,
6833
- simplify: resolveSimplificationMode(options.simplify, quality, analysis.riskFlags),
6834
- maxTriangles,
6835
- maxGridPoints,
6836
- diagnostics: options.diagnostics === true,
6837
- treeRiskFlags: [...analysis.riskFlags].sort(),
6838
- warnings
7424
+ scale: s,
7425
+ sx: b.sin(xs),
7426
+ sy: b.sin(ys),
7427
+ sz: b.sin(zs),
7428
+ cx: b.cos(xs),
7429
+ cy: b.cos(ys),
7430
+ cz: b.cos(zs),
7431
+ sx2: b.sin(b.mul(b.constant(2), xs)),
7432
+ sy2: b.sin(b.mul(b.constant(2), ys)),
7433
+ sz2: b.sin(b.mul(b.constant(2), zs)),
7434
+ cx2: b.cos(b.mul(b.constant(2), xs)),
7435
+ cy2: b.cos(b.mul(b.constant(2), ys)),
7436
+ cz2: b.cos(b.mul(b.constant(2), zs))
6839
7437
  };
6840
7438
  }
6841
- function withScaledSdfEdgeLength(settings, edgeLength2) {
6842
- return { ...settings, edgeLength: Math.max(settings.minEdgeLength, edgeLength2) };
7439
+ function emitGyroidValueAndGradient(b, x2, y2, z2, cellSize) {
7440
+ const t = emitScaledTrig(b, x2, y2, z2, cellSize);
7441
+ return {
7442
+ value: b.add(b.add(b.mul(t.sx, t.cy), b.mul(t.sy, t.cz)), b.mul(t.sz, t.cx)),
7443
+ gx: b.mul(t.scale, b.sub(b.mul(t.cx, t.cy), b.mul(t.sz, t.sx))),
7444
+ gy: b.mul(t.scale, b.add(b.neg(b.mul(t.sx, t.sy)), b.mul(t.cy, t.cz))),
7445
+ gz: b.mul(t.scale, b.add(b.neg(b.mul(t.sy, t.sz)), b.mul(t.cz, t.cx)))
7446
+ };
6843
7447
  }
6844
- function createSdfMeshingDiagnostics(settings, bounds, paddedBounds) {
6845
- const grid = estimateSdfGridDimensions(paddedBounds, settings.edgeLength);
6846
- const estimatedSamples = grid[0] * grid[1] * grid[2];
7448
+ function emitSchwarzPValueAndGradient(b, x2, y2, z2, cellSize) {
7449
+ const t = emitScaledTrig(b, x2, y2, z2, cellSize);
6847
7450
  return {
6848
- bounds: cloneBounds$2(bounds),
6849
- paddedBounds: cloneBounds$2(paddedBounds),
6850
- edgeLength: settings.edgeLength,
6851
- grid,
6852
- estimatedSamples,
6853
- estimatedMemoryBytes: estimatedSamples * 8,
6854
- treeRiskFlags: [...settings.treeRiskFlags],
6855
- simplification: settings.simplify,
6856
- capMode: "box",
6857
- capInset: settings.edgeLength,
6858
- warnings: [...settings.warnings]
7451
+ value: b.add(b.add(t.cx, t.cy), t.cz),
7452
+ gx: b.neg(b.mul(t.scale, t.sx)),
7453
+ gy: b.neg(b.mul(t.scale, t.sy)),
7454
+ gz: b.neg(b.mul(t.scale, t.sz))
6859
7455
  };
6860
7456
  }
6861
- function assertSdfMeshingBudget(diagnostics, maxGridPoints) {
6862
- if (diagnostics.estimatedSamples <= maxGridPoints) return;
6863
- const suggestedEdge = suggestEdgeLengthForSampleBudget(diagnostics.paddedBounds, maxGridPoints);
6864
- throw new Error(
6865
- `SDF meshing would sample ${formatCount(diagnostics.estimatedSamples)} grid points (~${formatBytes(
6866
- diagnostics.estimatedMemoryBytes
6867
- )}). Reduce bounds or use edgeLength >= ${formatMm(suggestedEdge)}.`
7457
+ function emitDiamondValueAndGradient(b, x2, y2, z2, cellSize) {
7458
+ const t = emitScaledTrig(b, x2, y2, z2, cellSize);
7459
+ return {
7460
+ value: b.add(
7461
+ b.add(b.mul(b.mul(t.sx, t.sy), t.sz), b.mul(b.mul(t.sx, t.cy), t.cz)),
7462
+ b.add(b.mul(b.mul(t.cx, t.sy), t.cz), b.mul(b.mul(t.cx, t.cy), t.sz))
7463
+ ),
7464
+ gx: b.mul(
7465
+ t.scale,
7466
+ b.add(
7467
+ b.add(b.mul(b.mul(t.cx, t.sy), t.sz), b.mul(b.mul(t.cx, t.cy), t.cz)),
7468
+ 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)))
7469
+ )
7470
+ ),
7471
+ gy: b.mul(
7472
+ t.scale,
7473
+ b.add(
7474
+ b.add(b.mul(b.mul(t.sx, t.cy), t.sz), b.neg(b.mul(b.mul(t.sx, t.sy), t.cz))),
7475
+ b.add(b.mul(b.mul(t.cx, t.cy), t.cz), b.neg(b.mul(b.mul(t.cx, t.sy), t.sz)))
7476
+ )
7477
+ ),
7478
+ gz: b.mul(
7479
+ t.scale,
7480
+ b.add(
7481
+ b.add(b.mul(b.mul(t.sx, t.sy), t.cz), b.neg(b.mul(b.mul(t.sx, t.cy), t.sz))),
7482
+ b.add(b.neg(b.mul(b.mul(t.cx, t.sy), t.sz)), b.mul(b.mul(t.cx, t.cy), t.cz))
7483
+ )
7484
+ )
7485
+ };
7486
+ }
7487
+ function emitLidinoidValueAndGradient(b, x2, y2, z2, cellSize) {
7488
+ const t = emitScaledTrig(b, x2, y2, z2, cellSize);
7489
+ const value = b.add(
7490
+ b.sub(
7491
+ 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)),
7492
+ b.add(b.add(b.mul(t.cx2, t.cy2), b.mul(t.cy2, t.cz2)), b.mul(t.cz2, t.cx2))
7493
+ ),
7494
+ b.constant(0.3)
6868
7495
  );
7496
+ return {
7497
+ value,
7498
+ gx: b.mul(
7499
+ t.scale,
7500
+ b.add(
7501
+ b.add(
7502
+ 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)),
7503
+ b.neg(b.mul(b.mul(t.sz2, t.sx), t.sy))
7504
+ ),
7505
+ b.add(b.mul(b.mul(b.constant(2), t.sx2), t.cy2), b.mul(b.mul(b.constant(2), t.cz2), t.sx2))
7506
+ )
7507
+ ),
7508
+ gy: b.mul(
7509
+ t.scale,
7510
+ b.add(
7511
+ b.add(
7512
+ 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))),
7513
+ b.mul(b.mul(t.sz2, t.cx), t.cy)
7514
+ ),
7515
+ b.add(b.mul(b.mul(b.constant(2), t.cx2), t.sy2), b.mul(b.mul(b.constant(2), t.sy2), t.cz2))
7516
+ )
7517
+ ),
7518
+ gz: b.mul(
7519
+ t.scale,
7520
+ b.add(
7521
+ b.add(
7522
+ b.add(b.mul(b.mul(t.sx2, t.cy), t.cz), b.neg(b.mul(b.mul(t.sy2, t.sz), t.sx))),
7523
+ b.mul(b.mul(b.constant(2), t.cz2), b.mul(t.cx, t.sy))
7524
+ ),
7525
+ b.add(b.mul(b.mul(b.constant(2), t.cy2), t.sz2), b.mul(b.mul(b.constant(2), t.sz2), t.cx2))
7526
+ )
7527
+ )
7528
+ };
6869
7529
  }
6870
- function estimateSdfGridDimensions(bounds, edgeLength2) {
6871
- const dx = bounds.max[0] - bounds.min[0];
6872
- const dy = bounds.max[1] - bounds.min[1];
6873
- const dz = bounds.max[2] - bounds.min[2];
6874
- return [
6875
- Math.max(2, Math.ceil(dx / edgeLength2) + 1),
6876
- Math.max(2, Math.ceil(dy / edgeLength2) + 1),
6877
- Math.max(2, Math.ceil(dz / edgeLength2) + 1)
6878
- ];
7530
+ function emitTpmsDistance(b, { value, gx, gy, gz }, thickness, thicknessMode) {
7531
+ if (thicknessMode !== "metric-approx") return b.sub(b.abs(value), b.constant(thickness));
7532
+ const grad = length3(b, gx, gy, gz);
7533
+ return b.sub(b.div(b.abs(value), b.max(grad, b.constant(GRAD_EPS))), b.constant(thickness * 0.5));
6879
7534
  }
6880
- function logSdfMeshingDiagnostics(prefix, diagnostics) {
6881
- const warnings = diagnostics.warnings.length > 0 ? `, warnings=${diagnostics.warnings.join(" | ")}` : "";
6882
- console.info(
6883
- `${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}`
6884
- );
7535
+ const { cos, max, sin, sqrt } = Math;
7536
+ function emitSdfProgramNode(b, node, x2, y2, z2) {
7537
+ switch (node.kind) {
7538
+ case "sdf:sphere":
7539
+ return sdSphere(b, x2, y2, z2, node.radius);
7540
+ case "sdf:box": {
7541
+ const [hx, hy, hz] = node.halfExtents;
7542
+ return sdBox(b, x2, y2, z2, hx, hy, hz);
7543
+ }
7544
+ case "sdf:cylinder":
7545
+ return sdCylinder(b, x2, y2, z2, node.height, node.radius);
7546
+ case "sdf:torus":
7547
+ return sdTorus(b, x2, y2, z2, node.majorRadius, node.minorRadius);
7548
+ case "sdf:capsule":
7549
+ return sdCapsule(b, x2, y2, z2, node.height, node.radius);
7550
+ case "sdf:cone":
7551
+ return sdCone(b, x2, y2, z2, node.height, node.radius);
7552
+ case "sdf:polylineSweep":
7553
+ return sdPolylineSweep(b, node, x2, y2, z2);
7554
+ case "sdf:union":
7555
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => b.min(a2, child));
7556
+ case "sdf:difference":
7557
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => b.max(a2, b.neg(child)));
7558
+ case "sdf:intersection":
7559
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => b.max(a2, child));
7560
+ case "sdf:smoothUnion":
7561
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => smin(b, a2, child, node.radius));
7562
+ case "sdf:smoothDifference":
7563
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => smax(b, a2, b.neg(child), node.radius));
7564
+ case "sdf:smoothIntersection":
7565
+ return foldChildren(b, node.children, x2, y2, z2, (a2, child) => smax(b, a2, child, node.radius));
7566
+ case "sdf:morph": {
7567
+ const a2 = emitSdfProgramNode(b, node.a, x2, y2, z2);
7568
+ const childB = emitSdfProgramNode(b, node.b, x2, y2, z2);
7569
+ return b.add(b.mul(a2, b.constant(1 - node.t)), b.mul(childB, b.constant(node.t)));
7570
+ }
7571
+ case "sdf:translate": {
7572
+ const [ox, oy, oz] = node.offset;
7573
+ return emitSdfProgramNode(b, node.child, b.sub(x2, b.constant(ox)), b.sub(y2, b.constant(oy)), b.sub(z2, b.constant(oz)));
7574
+ }
7575
+ case "sdf:rotate":
7576
+ return emitRotated(b, node, x2, y2, z2);
7577
+ case "sdf:scale": {
7578
+ const inv = 1 / node.factor;
7579
+ const child = emitSdfProgramNode(b, node.child, b.mul(x2, b.constant(inv)), b.mul(y2, b.constant(inv)), b.mul(z2, b.constant(inv)));
7580
+ return b.mul(child, b.constant(node.factor));
7581
+ }
7582
+ case "sdf:twist": {
7583
+ const angle = b.mul(b.constant(node.degreesPerUnit * DEG$1), z2);
7584
+ const c2 = b.cos(angle);
7585
+ const s = b.sin(angle);
7586
+ 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);
7587
+ }
7588
+ case "sdf:bend": {
7589
+ const angle = b.div(x2, b.constant(node.radius));
7590
+ const c2 = b.cos(angle);
7591
+ const s = b.sin(angle);
7592
+ const radiusPlusY = b.add(b.constant(node.radius), y2);
7593
+ return emitSdfProgramNode(b, node.child, b.mul(radiusPlusY, s), b.sub(b.mul(radiusPlusY, c2), b.constant(node.radius)), z2);
7594
+ }
7595
+ case "sdf:repeat": {
7596
+ const [sx, sy, sz] = node.spacing;
7597
+ const [cx, cy, cz] = node.count;
7598
+ return emitSdfProgramNode(b, node.child, repeatCoord(b, x2, sx, cx), repeatCoord(b, y2, sy, cy), repeatCoord(b, z2, sz, cz));
7599
+ }
7600
+ case "sdf:shell": {
7601
+ const child = emitSdfProgramNode(b, node.child, x2, y2, z2);
7602
+ return b.sub(b.abs(child), b.constant(node.thickness * 0.5));
7603
+ }
7604
+ case "sdf:onion": {
7605
+ let d2 = emitSdfProgramNode(b, node.child, x2, y2, z2);
7606
+ for (let i = 0; i < node.layers; i++) d2 = b.sub(b.abs(d2), b.constant(node.thickness));
7607
+ return d2;
7608
+ }
7609
+ case "sdf:gyroid":
7610
+ return emitTpmsDistance(b, emitGyroidValueAndGradient(b, x2, y2, z2, node.cellSize), node.thickness, node.thicknessMode);
7611
+ case "sdf:schwarzP":
7612
+ return emitTpmsDistance(b, emitSchwarzPValueAndGradient(b, x2, y2, z2, node.cellSize), node.thickness, node.thicknessMode);
7613
+ case "sdf:diamond":
7614
+ return emitTpmsDistance(b, emitDiamondValueAndGradient(b, x2, y2, z2, node.cellSize), node.thickness, node.thicknessMode);
7615
+ case "sdf:lidinoid":
7616
+ return emitTpmsDistance(b, emitLidinoidValueAndGradient(b, x2, y2, z2, node.cellSize), node.thickness, node.thicknessMode);
7617
+ default:
7618
+ throw new UnsupportedSdfProgramNodeError(`SdfProgram does not support node kind ${node.kind} yet.`);
7619
+ }
6885
7620
  }
6886
- function resolveDefaultEdgeLength(bounds, quality, minEdgeLength, analysis, options) {
6887
- const dx = bounds.max[0] - bounds.min[0];
6888
- const dy = bounds.max[1] - bounds.min[1];
6889
- const dz = bounds.max[2] - bounds.min[2];
6890
- const maxDim = Math.max(dx, dy, dz, minEdgeLength);
6891
- const divisor = quality === "draft" ? 60 : quality === "export" ? 160 : 100;
6892
- const candidates = [maxDim / divisor];
6893
- if (options.tolerance !== void 0) candidates.push(requirePositiveFinite$2(options.tolerance, "SDF tolerance") * 2);
6894
- if (options.minFeatureSize !== void 0) candidates.push(requirePositiveFinite$2(options.minFeatureSize, "SDF minFeatureSize") / 2.5);
6895
- if (analysis.minMetricTpmsThickness < Infinity) candidates.push(analysis.minMetricTpmsThickness / 2);
6896
- if (analysis.minTpmsCellSize < Infinity) candidates.push(analysis.minTpmsCellSize / 10);
6897
- if (analysis.minRepeatSpacing < Infinity) candidates.push(analysis.minRepeatSpacing / 8);
6898
- if (analysis.minWallThickness < Infinity) candidates.push(analysis.minWallThickness / 2.5);
6899
- return Math.max(minEdgeLength, Math.min(...candidates.filter((v) => Number.isFinite(v) && v > 0)));
7621
+ function foldChildren(b, children, x2, y2, z2, combine2) {
7622
+ let d2 = emitSdfProgramNode(b, children[0], x2, y2, z2);
7623
+ for (let i = 1; i < children.length; i++) d2 = combine2(d2, emitSdfProgramNode(b, children[i], x2, y2, z2));
7624
+ return d2;
6900
7625
  }
6901
- function resolveSimplificationMode(simplify, quality, riskFlags) {
6902
- if (simplify === false) return "off";
6903
- if (simplify === true || simplify === "safe") return "safe";
6904
- if (quality === "export" && riskFlags.size > 0) return "off";
6905
- return "safe";
7626
+ function emitRotated(b, node, x2, y2, z2) {
7627
+ const [rx, ry, rz] = node.degrees.map((d2) => d2 * DEG$1);
7628
+ const cx = cos(rx);
7629
+ const sx = sin(rx);
7630
+ const cy = cos(ry);
7631
+ const sy = sin(ry);
7632
+ const cz = cos(rz);
7633
+ const sz = sin(rz);
7634
+ const x1 = b.add(b.mul(b.constant(cz), x2), b.mul(b.constant(sz), y2));
7635
+ const y1 = b.sub(b.mul(b.constant(cz), y2), b.mul(b.constant(sz), x2));
7636
+ const x22 = b.sub(b.mul(b.constant(cy), x1), b.mul(b.constant(sy), z2));
7637
+ const z22 = b.add(b.mul(b.constant(sy), x1), b.mul(b.constant(cy), z2));
7638
+ const y22 = b.add(b.mul(b.constant(cx), y1), b.mul(b.constant(sx), z22));
7639
+ const z3 = b.sub(b.mul(b.constant(cx), z22), b.mul(b.constant(sx), y1));
7640
+ return emitSdfProgramNode(b, node.child, x22, y22, z3);
7641
+ }
7642
+ function sdSphere(b, x2, y2, z2, r) {
7643
+ return b.sub(length3(b, x2, y2, z2), b.constant(r));
7644
+ }
7645
+ function sdBox(b, x2, y2, z2, hx, hy, hz) {
7646
+ const dx = b.sub(b.abs(x2), b.constant(hx));
7647
+ const dy = b.sub(b.abs(y2), b.constant(hy));
7648
+ const dz = b.sub(b.abs(z2), b.constant(hz));
7649
+ const outside = length3(b, b.max(dx, b.constant(0)), b.max(dy, b.constant(0)), b.max(dz, b.constant(0)));
7650
+ const inside = b.min(b.max(b.max(dx, dy), dz), b.constant(0));
7651
+ return b.add(outside, inside);
7652
+ }
7653
+ function sdCylinder(b, x2, y2, z2, h, r) {
7654
+ const dx = b.sub(length2(b, x2, y2), b.constant(r));
7655
+ const dz = b.sub(b.abs(z2), b.constant(h * 0.5));
7656
+ 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)));
7657
+ }
7658
+ function sdTorus(b, x2, y2, z2, majorRadius, minorRadius) {
7659
+ return b.sub(length2(b, b.sub(length2(b, x2, y2), b.constant(majorRadius)), z2), b.constant(minorRadius));
7660
+ }
7661
+ function sdCapsule(b, x2, y2, z2, h, r) {
7662
+ const cz = clampSlot(b, z2, -h * 0.5, h * 0.5);
7663
+ return b.sub(length3(b, x2, y2, b.sub(z2, cz)), b.constant(r));
7664
+ }
7665
+ function sdCone(b, x2, y2, z2, h, r) {
7666
+ const q = length2(b, x2, y2);
7667
+ const cLen = sqrt(h * h + r * r);
7668
+ const side = b.add(b.mul(b.constant(h / cLen), q), b.mul(b.constant(-r / cLen), b.sub(z2, b.constant(h))));
7669
+ return b.max(b.max(side, b.neg(z2)), b.sub(z2, b.constant(h)));
7670
+ }
7671
+ function sdPolylineSweep(b, node, x2, y2, z2) {
7672
+ let d2 = sdTaperedSegment(b, x2, y2, z2, node.points[0], node.points[1], node.radii[0], node.radii[1]);
7673
+ for (let i = 1; i < node.points.length - 1; i++) {
7674
+ const segment = sdTaperedSegment(b, x2, y2, z2, node.points[i], node.points[i + 1], node.radii[i], node.radii[i + 1]);
7675
+ d2 = smin(b, d2, segment, node.blend);
7676
+ }
7677
+ return d2;
6906
7678
  }
6907
- function analyzeSdfTree(tree) {
6908
- const analysis = {
6909
- riskFlags: /* @__PURE__ */ new Set(),
6910
- minTpmsCellSize: Infinity,
6911
- minMetricTpmsThickness: Infinity,
6912
- minRepeatSpacing: Infinity,
6913
- minWallThickness: Infinity,
6914
- hasInfiniteRepeat: false,
6915
- hasLegacyTpmsThreshold: false
6916
- };
6917
- visitSdfNode(tree, analysis);
6918
- return analysis;
7679
+ function sdTaperedSegment(b, x2, y2, z2, a2, end, ra, rb) {
7680
+ const vx = end[0] - a2[0];
7681
+ const vy = end[1] - a2[1];
7682
+ const vz = end[2] - a2[2];
7683
+ const len2 = vx * vx + vy * vy + vz * vz;
7684
+ 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));
7685
+ const h = clampSlot(
7686
+ b,
7687
+ b.div(
7688
+ b.add(
7689
+ 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))),
7690
+ b.mul(b.sub(z2, b.constant(a2[2])), b.constant(vz))
7691
+ ),
7692
+ b.constant(len2)
7693
+ ),
7694
+ 0,
7695
+ 1
7696
+ );
7697
+ const sx = b.sub(x2, b.add(b.constant(a2[0]), b.mul(b.constant(vx), h)));
7698
+ const sy = b.sub(y2, b.add(b.constant(a2[1]), b.mul(b.constant(vy), h)));
7699
+ const sz = b.sub(z2, b.add(b.constant(a2[2]), b.mul(b.constant(vz), h)));
7700
+ const radius = b.add(b.constant(ra), b.mul(b.constant(rb - ra), h));
7701
+ return b.sub(length3(b, sx, sy, sz), radius);
6919
7702
  }
6920
- function visitSdfNode(node, analysis) {
7703
+ function repeatCoord(b, v, spacing, count) {
7704
+ if (spacing <= 0) return v;
7705
+ if (count > 0) {
7706
+ const center = (count - 1) * 0.5;
7707
+ const index2 = clampSlot(b, b.round(b.add(b.div(v, b.constant(spacing)), b.constant(center))), 0, count - 1);
7708
+ return b.sub(v, b.mul(b.sub(index2, b.constant(center)), b.constant(spacing)));
7709
+ }
7710
+ return b.sub(v, b.mul(b.constant(spacing), b.round(b.div(v, b.constant(spacing)))));
7711
+ }
7712
+ function getUnsupportedSdfProgramReason(node) {
6921
7713
  switch (node.kind) {
7714
+ case "sdf:displace":
7715
+ return "displace uses a dynamic JavaScript function body";
7716
+ case "sdf:surfaceDisplace":
7717
+ return "surfaceDisplace uses dynamic UV/pattern evaluation";
7718
+ case "sdf:spatialBlend":
7719
+ return "spatialBlend uses a dynamic JavaScript blend function";
7720
+ case "sdf:noise":
7721
+ return "noise depends on table-based simplex evaluation";
7722
+ case "sdf:voronoi":
7723
+ return "voronoi depends on table-based Worley evaluation";
7724
+ case "sdf:custom":
7725
+ return "custom uses a dynamic JavaScript function body";
7726
+ case "sdf:polylineSweep":
7727
+ if (node.points.length < 2) return "polylineSweep needs at least two points";
7728
+ if (node.points.length !== node.radii.length) return "polylineSweep point/radius counts differ";
7729
+ return void 0;
6922
7730
  case "sdf:union":
6923
7731
  case "sdf:difference":
6924
7732
  case "sdf:intersection":
6925
7733
  case "sdf:smoothUnion":
6926
7734
  case "sdf:smoothDifference":
6927
7735
  case "sdf:smoothIntersection":
6928
- for (const child of node.children) visitSdfNode(child, analysis);
6929
- break;
7736
+ for (const child of node.children) {
7737
+ const reason = getUnsupportedSdfProgramReason(child);
7738
+ if (reason) return reason;
7739
+ }
7740
+ return void 0;
6930
7741
  case "sdf:morph":
6931
- case "sdf:spatialBlend":
6932
- visitSdfNode(node.a, analysis);
6933
- visitSdfNode(node.b, analysis);
6934
- break;
7742
+ return getUnsupportedSdfProgramReason(node.a) ?? getUnsupportedSdfProgramReason(node.b);
6935
7743
  case "sdf:translate":
6936
7744
  case "sdf:rotate":
6937
7745
  case "sdf:scale":
6938
7746
  case "sdf:twist":
6939
7747
  case "sdf:bend":
6940
- case "sdf:onion":
6941
- visitSdfNode(node.child, analysis);
6942
- break;
6943
7748
  case "sdf:repeat":
6944
- analysis.riskFlags.add("repeat");
6945
- for (let i = 0; i < 3; i++) {
6946
- const spacing = node.spacing[i];
6947
- if (spacing > 0) {
6948
- analysis.minRepeatSpacing = Math.min(analysis.minRepeatSpacing, spacing);
6949
- if (node.count[i] <= 0) analysis.hasInfiniteRepeat = true;
6950
- }
6951
- }
6952
- visitSdfNode(node.child, analysis);
6953
- break;
6954
7749
  case "sdf:shell":
6955
- analysis.minWallThickness = Math.min(analysis.minWallThickness, node.thickness);
6956
- visitSdfNode(node.child, analysis);
6957
- break;
6958
- case "sdf:displace":
6959
- case "sdf:surfaceDisplace":
6960
- analysis.riskFlags.add("displacement");
6961
- visitSdfNode(node.child, analysis);
6962
- break;
6963
- case "sdf:gyroid":
6964
- case "sdf:schwarzP":
6965
- case "sdf:diamond":
6966
- case "sdf:lidinoid":
6967
- analysis.riskFlags.add("tpms");
6968
- analysis.minTpmsCellSize = Math.min(analysis.minTpmsCellSize, node.cellSize);
6969
- if (node.thicknessMode === "metric-approx") {
6970
- analysis.minMetricTpmsThickness = Math.min(analysis.minMetricTpmsThickness, node.thickness);
6971
- analysis.minWallThickness = Math.min(analysis.minWallThickness, node.thickness);
6972
- } else {
6973
- analysis.hasLegacyTpmsThreshold = true;
6974
- }
6975
- break;
6976
- case "sdf:noise":
6977
- analysis.riskFlags.add("noise");
6978
- break;
6979
- case "sdf:voronoi":
6980
- analysis.riskFlags.add("voronoi");
6981
- analysis.minWallThickness = Math.min(analysis.minWallThickness, node.wallThickness);
6982
- if (node.surfaceChild) visitSdfNode(node.surfaceChild, analysis);
6983
- break;
6984
- case "sdf:custom":
6985
- analysis.riskFlags.add("custom");
6986
- break;
7750
+ case "sdf:onion":
7751
+ return getUnsupportedSdfProgramReason(node.child);
7752
+ default:
7753
+ return void 0;
6987
7754
  }
6988
7755
  }
6989
- function positiveOrDefault(value, fallback) {
6990
- if (value === void 0) return fallback;
6991
- return requirePositiveFinite$2(value, "SDF meshing option");
6992
- }
6993
- function requirePositiveFinite$2(value, name) {
6994
- if (!Number.isFinite(value) || value <= 0) {
6995
- throw new Error(`${name} must be a positive finite number.`);
7756
+ function compileSdfProgram(node) {
7757
+ const unsupportedReason = getUnsupportedSdfProgramReason(node);
7758
+ if (unsupportedReason) {
7759
+ throw new UnsupportedSdfProgramNodeError(`SdfProgram does not support this tree yet: ${unsupportedReason}.`);
6996
7760
  }
6997
- return value;
6998
- }
6999
- function cloneBounds$2(bounds) {
7000
- return { min: [...bounds.min], max: [...bounds.max] };
7001
- }
7002
- function suggestEdgeLengthForSampleBudget(bounds, maxGridPoints) {
7003
- const dx = bounds.max[0] - bounds.min[0];
7004
- const dy = bounds.max[1] - bounds.min[1];
7005
- const dz = bounds.max[2] - bounds.min[2];
7006
- const volume = Math.max(dx * dy * dz, 1);
7007
- return Math.cbrt(volume / Math.max(maxGridPoints, 8));
7761
+ const builder = new SdfProgramBuilder();
7762
+ return builder.finalize(emitSdfProgramNode(builder, node, builder.x, builder.y, builder.z));
7008
7763
  }
7009
- function formatBounds(bounds) {
7010
- return `[${bounds.min.map(formatNumber$1).join(",")}]-[${bounds.max.map(formatNumber$1).join(",")}]`;
7011
- }
7012
- function formatMm(value) {
7013
- return `${formatNumber$1(value)}mm`;
7014
- }
7015
- function formatNumber$1(value) {
7016
- return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
7017
- }
7018
- function formatCount(value) {
7019
- return Math.round(value).toLocaleString("en-US");
7764
+ function compileSdfProgram3(node) {
7765
+ return compileSdfProgramEvaluator3(compileSdfProgram(node));
7020
7766
  }
7021
- function formatBytes(bytes) {
7022
- if (bytes < 1024 * 1024) return `${Math.ceil(bytes / 1024)} KB`;
7023
- return `${Math.ceil(bytes / (1024 * 1024))} MB`;
7767
+ function compileSdfMaterializationEvaluator3(node) {
7768
+ const unsupportedReason = getUnsupportedSdfProgramReason(node);
7769
+ if (unsupportedReason) {
7770
+ return {
7771
+ fn: compileSdfNode3(node),
7772
+ engine: "closure",
7773
+ unsupportedReason
7774
+ };
7775
+ }
7776
+ return {
7777
+ fn: compileSdfProgram3(node),
7778
+ engine: "program"
7779
+ };
7024
7780
  }
7025
7781
  function midpoint$3(a2, b) {
7026
7782
  return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
@@ -7034,7 +7790,7 @@ function scale$6(v, s) {
7034
7790
  function sub$7(a2, b) {
7035
7791
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
7036
7792
  }
7037
- function cross$7(a2, b) {
7793
+ function cross$8(a2, b) {
7038
7794
  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]];
7039
7795
  }
7040
7796
  function makeEdge(name, start, end, faceName, curve) {
@@ -7070,7 +7826,7 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
7070
7826
  const center = options.center ?? average$1(corners);
7071
7827
  const uAxis = normalizeAxis$1(sub$7(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
7072
7828
  const vAxis = normalizeAxis$1(sub$7(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
7073
- const normal = normalizeAxis$1(options.normal ?? cross$7(uAxis, vAxis));
7829
+ const normal = normalizeAxis$1(options.normal ?? cross$8(uAxis, vAxis));
7074
7830
  const faces = /* @__PURE__ */ new Map();
7075
7831
  faces.set(faceName, {
7076
7832
  name: faceName,
@@ -7486,7 +8242,7 @@ function buildCircleExtrusionTopology(circ, height, center = false) {
7486
8242
  );
7487
8243
  return { faces, edges };
7488
8244
  }
7489
- function requireFinite$8(v, label) {
8245
+ function requireFinite$9(v, label) {
7490
8246
  if (!Number.isFinite(v)) throw new Error(`nurbsSurface: ${label} must be finite, got ${v}`);
7491
8247
  }
7492
8248
  function normalizeSurfaceTessellation(tessellation) {
@@ -7496,11 +8252,11 @@ function normalizeSurfaceTessellation(tessellation) {
7496
8252
  throw new Error(`nurbsSurface: tessellation.mode must be "uniform" or "adaptive", got ${mode}`);
7497
8253
  }
7498
8254
  if (tessellation.tolerance !== void 0) {
7499
- requireFinite$8(tessellation.tolerance, "tessellation.tolerance");
8255
+ requireFinite$9(tessellation.tolerance, "tessellation.tolerance");
7500
8256
  if (tessellation.tolerance <= 0) throw new Error("nurbsSurface: tessellation.tolerance must be > 0");
7501
8257
  }
7502
- if (tessellation.minResolution !== void 0) requireFinite$8(tessellation.minResolution, "tessellation.minResolution");
7503
- if (tessellation.maxResolution !== void 0) requireFinite$8(tessellation.maxResolution, "tessellation.maxResolution");
8258
+ if (tessellation.minResolution !== void 0) requireFinite$9(tessellation.minResolution, "tessellation.minResolution");
8259
+ if (tessellation.maxResolution !== void 0) requireFinite$9(tessellation.maxResolution, "tessellation.maxResolution");
7504
8260
  const minResolution = tessellation.minResolution === void 0 ? void 0 : Math.max(2, Math.round(tessellation.minResolution));
7505
8261
  const maxResolution = tessellation.maxResolution === void 0 ? void 0 : Math.max(2, Math.round(tessellation.maxResolution));
7506
8262
  if (minResolution !== void 0 && maxResolution !== void 0 && minResolution > maxResolution) {
@@ -7519,10 +8275,10 @@ function normalizeSurfaceDomain(domain) {
7519
8275
  const uMax = domain.uMax ?? 1;
7520
8276
  const vMin = domain.vMin ?? 0;
7521
8277
  const vMax = domain.vMax ?? 1;
7522
- requireFinite$8(uMin, "domain.uMin");
7523
- requireFinite$8(uMax, "domain.uMax");
7524
- requireFinite$8(vMin, "domain.vMin");
7525
- requireFinite$8(vMax, "domain.vMax");
8278
+ requireFinite$9(uMin, "domain.uMin");
8279
+ requireFinite$9(uMax, "domain.uMax");
8280
+ requireFinite$9(vMin, "domain.vMin");
8281
+ requireFinite$9(vMax, "domain.vMax");
7526
8282
  if (uMin < 0 || uMax > 1 || vMin < 0 || vMax > 1) {
7527
8283
  throw new Error("nurbsSurface: domain bounds must stay within [0, 1]");
7528
8284
  }
@@ -7534,8 +8290,8 @@ function normalizeSurfaceDomain(domain) {
7534
8290
  function normalizeTrimLoop(loop, label) {
7535
8291
  if (loop.length < 3) throw new Error(`nurbsSurface: ${label} requires at least 3 points`);
7536
8292
  const normalized = loop.map(([u2, v], idx) => {
7537
- requireFinite$8(u2, `${label}[${idx}][0]`);
7538
- requireFinite$8(v, `${label}[${idx}][1]`);
8293
+ requireFinite$9(u2, `${label}[${idx}][0]`);
8294
+ requireFinite$9(v, `${label}[${idx}][1]`);
7539
8295
  if (u2 < 0 || u2 > 1 || v < 0 || v > 1) throw new Error(`nurbsSurface: ${label}[${idx}] must stay within [0, 1]`);
7540
8296
  return [u2, v];
7541
8297
  });
@@ -7558,8 +8314,8 @@ function normalizeTrimCurve(curve, label) {
7558
8314
  throw new Error(`nurbsSurface: ${label} needs at least ${degree + 1} control points for degree=${degree}`);
7559
8315
  }
7560
8316
  const normalizedControlPoints = controlPoints.map(([u2, v], idx) => {
7561
- requireFinite$8(u2, `${label}.controlPoints[${idx}][0]`);
7562
- requireFinite$8(v, `${label}.controlPoints[${idx}][1]`);
8317
+ requireFinite$9(u2, `${label}.controlPoints[${idx}][0]`);
8318
+ requireFinite$9(v, `${label}.controlPoints[${idx}][1]`);
7563
8319
  if (u2 < 0 || u2 > 1 || v < 0 || v > 1) {
7564
8320
  throw new Error(`nurbsSurface: ${label}.controlPoints[${idx}] must stay within [0, 1]`);
7565
8321
  }
@@ -7570,7 +8326,7 @@ function normalizeTrimCurve(curve, label) {
7570
8326
  throw new Error(`nurbsSurface: ${label}.weights length must match controlPoints length`);
7571
8327
  }
7572
8328
  for (let idx = 0; idx < weights.length; idx += 1) {
7573
- requireFinite$8(weights[idx], `${label}.weights[${idx}]`);
8329
+ requireFinite$9(weights[idx], `${label}.weights[${idx}]`);
7574
8330
  if (weights[idx] <= 0) throw new Error(`nurbsSurface: ${label}.weights[${idx}] must be > 0`);
7575
8331
  }
7576
8332
  const knots = curve.knots ?? generateClampedKnots(controlPoints.length, degree);
@@ -7578,7 +8334,7 @@ function normalizeTrimCurve(curve, label) {
7578
8334
  throw new Error(`nurbsSurface: ${label}.knots.length should be ${controlPoints.length + degree + 1}, got ${knots.length}`);
7579
8335
  }
7580
8336
  for (let idx = 0; idx < knots.length; idx += 1) {
7581
- requireFinite$8(knots[idx], `${label}.knots[${idx}]`);
8337
+ requireFinite$9(knots[idx], `${label}.knots[${idx}]`);
7582
8338
  if (idx > 0 && knots[idx] < knots[idx - 1]) throw new Error(`nurbsSurface: ${label}.knots must be non-decreasing`);
7583
8339
  }
7584
8340
  if (knots[degree] >= knots[controlPoints.length]) {
@@ -7758,16 +8514,16 @@ class NurbsSurface {
7758
8514
  for (let i = 0; i < nU; i++) {
7759
8515
  if (controlGrid[i].length !== nV) throw new Error(`nurbsSurface: row ${i} has ${controlGrid[i].length} points, expected ${nV}`);
7760
8516
  for (let j = 0; j < nV; j++) {
7761
- requireFinite$8(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
7762
- requireFinite$8(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
7763
- requireFinite$8(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
8517
+ requireFinite$9(controlGrid[i][j][0], `controlGrid[${i}][${j}][0]`);
8518
+ requireFinite$9(controlGrid[i][j][1], `controlGrid[${i}][${j}][1]`);
8519
+ requireFinite$9(controlGrid[i][j][2], `controlGrid[${i}][${j}][2]`);
7764
8520
  }
7765
8521
  }
7766
8522
  const weightsGrid = options.weights ?? controlGrid.map((row) => row.map(() => 1));
7767
8523
  for (let i = 0; i < nU; i++) {
7768
8524
  if (weightsGrid[i].length !== nV) throw new Error(`nurbsSurface: weights row ${i} length mismatch`);
7769
8525
  for (let j = 0; j < nV; j++) {
7770
- requireFinite$8(weightsGrid[i][j], `weights[${i}][${j}]`);
8526
+ requireFinite$9(weightsGrid[i][j], `weights[${i}][${j}]`);
7771
8527
  if (weightsGrid[i][j] <= 0) throw new Error(`nurbsSurface: weights[${i}][${j}] must be > 0`);
7772
8528
  }
7773
8529
  }
@@ -8972,6 +9728,7 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
8972
9728
  edgeLength: options.edgeLength
8973
9729
  };
8974
9730
  }
9731
+ const EPS$9 = 1e-9;
8975
9732
  function resamplePolygon(poly, targetCount) {
8976
9733
  if (poly.length < 2) return poly;
8977
9734
  if (targetCount <= 0) return [];
@@ -9009,6 +9766,78 @@ function resamplePolygon(poly, targetCount) {
9009
9766
  }
9010
9767
  return out;
9011
9768
  }
9769
+ function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid$2(poly)) {
9770
+ if (poly.length < 3 || targetCount <= 0) return null;
9771
+ if (!isConvexPolygon(poly)) return null;
9772
+ const out = [];
9773
+ for (let index2 = 0; index2 < targetCount; index2 += 1) {
9774
+ const angle = index2 / targetCount * Math.PI * 2;
9775
+ const point2 = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
9776
+ if (!point2) return null;
9777
+ out.push(point2);
9778
+ }
9779
+ return out;
9780
+ }
9781
+ function rayPolygonIntersection(origin, direction2, poly) {
9782
+ let bestT = Infinity;
9783
+ let best = null;
9784
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
9785
+ const a2 = poly[index2];
9786
+ const b = poly[(index2 + 1) % poly.length];
9787
+ const edge = [b[0] - a2[0], b[1] - a2[1]];
9788
+ const denom = cross$7(direction2, edge);
9789
+ if (Math.abs(denom) < EPS$9) continue;
9790
+ const delta = [a2[0] - origin[0], a2[1] - origin[1]];
9791
+ const rayT = cross$7(delta, edge) / denom;
9792
+ const edgeT = cross$7(delta, direction2) / denom;
9793
+ if (rayT >= -EPS$9 && edgeT >= -EPS$9 && edgeT <= 1 + EPS$9 && rayT < bestT) {
9794
+ bestT = rayT;
9795
+ best = [origin[0] + direction2[0] * rayT, origin[1] + direction2[1] * rayT];
9796
+ }
9797
+ }
9798
+ return best;
9799
+ }
9800
+ function polygonCentroid$2(poly) {
9801
+ let area2 = 0;
9802
+ let cx = 0;
9803
+ let cy = 0;
9804
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
9805
+ const a2 = poly[index2];
9806
+ const b = poly[(index2 + 1) % poly.length];
9807
+ const crossValue = cross$7(a2, b);
9808
+ area2 += crossValue;
9809
+ cx += (a2[0] + b[0]) * crossValue;
9810
+ cy += (a2[1] + b[1]) * crossValue;
9811
+ }
9812
+ if (Math.abs(area2) < EPS$9) return averagePoint(poly);
9813
+ return [cx / (3 * area2), cy / (3 * area2)];
9814
+ }
9815
+ function averagePoint(poly) {
9816
+ let x2 = 0;
9817
+ let y2 = 0;
9818
+ for (const point2 of poly) {
9819
+ x2 += point2[0];
9820
+ y2 += point2[1];
9821
+ }
9822
+ return [x2 / poly.length, y2 / poly.length];
9823
+ }
9824
+ function isConvexPolygon(poly) {
9825
+ let sign2 = 0;
9826
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
9827
+ const a2 = poly[index2];
9828
+ const b = poly[(index2 + 1) % poly.length];
9829
+ const c2 = poly[(index2 + 2) % poly.length];
9830
+ const turn = cross$7([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
9831
+ if (Math.abs(turn) < EPS$9) continue;
9832
+ const currentSign = Math.sign(turn);
9833
+ if (sign2 !== 0 && currentSign !== sign2) return false;
9834
+ sign2 = currentSign;
9835
+ }
9836
+ return sign2 !== 0;
9837
+ }
9838
+ function cross$7(a2, b) {
9839
+ return a2[0] * b[1] - a2[1] * b[0];
9840
+ }
9012
9841
  function loftStitched(profiles2, heights, wasm) {
9013
9842
  if (profiles2.length < 2) return null;
9014
9843
  const classified = profiles2.map((loops) => classifyLoops(loops));
@@ -9137,8 +9966,10 @@ function stitchSingleLoopLoft(loops, heights, wasm) {
9137
9966
  maxPoints = Math.max(maxPoints, loop.length);
9138
9967
  }
9139
9968
  const N = Math.max(maxPoints, 24);
9969
+ const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
9970
+ const useAngularSamples = angularSamples.every((samples) => samples != null);
9140
9971
  const resampled = normalizedLoops.map((loop, i) => {
9141
- const pts2d = resamplePolygon(loop, N);
9972
+ const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
9142
9973
  const z2 = heights[i];
9143
9974
  return pts2d.map(([x2, y2]) => [x2, y2, z2]);
9144
9975
  });
@@ -9157,8 +9988,8 @@ function stitchSingleLoopLoft(loops, heights, wasm) {
9157
9988
  const v0 = baseIdx + j;
9158
9989
  const v1 = nextIdx + j;
9159
9990
  const v2 = nextIdx + j1;
9160
- const v3 = baseIdx + j1;
9161
- triangles.push(v0, v3, v2);
9991
+ const v32 = baseIdx + j1;
9992
+ triangles.push(v0, v32, v2);
9162
9993
  triangles.push(v0, v2, v1);
9163
9994
  }
9164
9995
  }
@@ -9199,7 +10030,7 @@ let _wasm$1 = null;
9199
10030
  async function initManifoldWasm() {
9200
10031
  if (_wasm$1) return _wasm$1;
9201
10032
  performance.mark("manifold:start");
9202
- const Module = (await import("./manifold-DTvmxSDf.js")).default;
10033
+ const Module = (await import("./manifold-DpBXFS2K.js")).default;
9203
10034
  performance.mark("manifold:imported");
9204
10035
  const wasm = await Module();
9205
10036
  wasm.setup();
@@ -9481,8 +10312,8 @@ function stitchLoopAlongPath(loop, _path, frames, wasm) {
9481
10312
  const v0 = baseIdx + j;
9482
10313
  const v1 = nextIdx + j;
9483
10314
  const v2 = nextIdx + j1;
9484
- const v3 = baseIdx + j1;
9485
- triangles.push(v0, v3, v2);
10315
+ const v32 = baseIdx + j1;
10316
+ triangles.push(v0, v32, v2);
9486
10317
  triangles.push(v0, v2, v1);
9487
10318
  }
9488
10319
  }
@@ -10828,8 +11659,16 @@ function lowerShapeCompilePlanToManifold(plan, wasm) {
10828
11659
  case "importedMesh":
10829
11660
  return lowerImportedMeshToManifold(plan.fileData, plan.format, plan.filePath, wasm);
10830
11661
  case "sdf": {
10831
- const evalFn = compileSdfNode3(plan.tree);
10832
- return lowerSdfToManifold(evalFn, plan.bounds, plan.edgeLength, wasm, plan.meshing);
11662
+ const evaluator = compileSdfMaterializationEvaluator3(plan.tree);
11663
+ return lowerSdfToManifold(
11664
+ evaluator.fn,
11665
+ plan.bounds,
11666
+ plan.edgeLength,
11667
+ wasm,
11668
+ plan.meshing,
11669
+ evaluator.engine,
11670
+ evaluator.unsupportedReason
11671
+ );
10833
11672
  }
10834
11673
  case "fromSlices":
10835
11674
  return lowerFromSlicesToManifold(plan, wasm);
@@ -10847,8 +11686,12 @@ function lowerShapeCompilePlanToManifold(plan, wasm) {
10847
11686
  assertExhaustive(plan);
10848
11687
  }
10849
11688
  }
10850
- function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing) {
11689
+ function lowerSdfToManifold(evalFn, bounds, edgeLength2, wasm, meshing, evaluatorEngine, evaluatorUnsupportedReason) {
10851
11690
  const diagnostics = (meshing == null ? void 0 : meshing.diagnostics) ? { ...meshing.diagnostics } : void 0;
11691
+ if (diagnostics && evaluatorEngine) {
11692
+ diagnostics.evaluator = evaluatorEngine;
11693
+ if (evaluatorUnsupportedReason) diagnostics.evaluatorUnsupportedReason = evaluatorUnsupportedReason;
11694
+ }
10852
11695
  const inset = edgeLength2;
10853
11696
  const cappedEvalFn = (x2, y2, z2) => {
10854
11697
  const bx = Math.max(bounds.min[0] + inset - x2, x2 - bounds.max[0] + inset);
@@ -16294,9 +17137,9 @@ function requireClipper() {
16294
17137
  if (ClipperLib2.use_xyz) j.OffPt.Z = OffPt.Z;
16295
17138
  this.m_Joins.push(j);
16296
17139
  };
16297
- ClipperLib2.Clipper.prototype.AddGhostJoin = function(Op, OffPt) {
17140
+ ClipperLib2.Clipper.prototype.AddGhostJoin = function(Op2, OffPt) {
16298
17141
  var j = new ClipperLib2.Join();
16299
- j.OutPt1 = Op;
17142
+ j.OutPt1 = Op2;
16300
17143
  j.OffPt.X = OffPt.X;
16301
17144
  j.OffPt.Y = OffPt.Y;
16302
17145
  if (ClipperLib2.use_xyz) j.OffPt.Z = OffPt.Z;
@@ -19198,7 +20041,7 @@ function requireClipper() {
19198
20041
  }
19199
20042
  var clipperExports = requireClipper();
19200
20043
  var ClipperLib = /* @__PURE__ */ getDefaultExportFromCjs(clipperExports);
19201
- let f$1 = class f {
20044
+ let f$3 = class f {
19202
20045
  constructor(t, e) {
19203
20046
  this.next = null, this.key = t, this.data = e, this.left = null, this.right = null;
19204
20047
  }
@@ -19207,7 +20050,7 @@ function d(n, t) {
19207
20050
  return n > t ? 1 : n < t ? -1 : 0;
19208
20051
  }
19209
20052
  function u$1(n, t, e) {
19210
- const r = new f$1(null, null);
20053
+ const r = new f$3(null, null);
19211
20054
  let l = r, i = r;
19212
20055
  for (; ; ) {
19213
20056
  const o = e(n, t.key);
@@ -19230,7 +20073,7 @@ function u$1(n, t, e) {
19230
20073
  return l.right = t.left, i.left = t.right, t.left = r.right, t.right = r.left, t;
19231
20074
  }
19232
20075
  function c(n, t, e, r) {
19233
- const l = new f$1(n, t);
20076
+ const l = new f$3(n, t);
19234
20077
  if (e === null)
19235
20078
  return l.left = l.right = null, l;
19236
20079
  e = u$1(n, e, r);
@@ -19271,7 +20114,7 @@ class z {
19271
20114
  * Adds a key, if it is not present in the tree
19272
20115
  */
19273
20116
  add(t, e) {
19274
- const r = new f$1(t, e);
20117
+ const r = new f$3(t, e);
19275
20118
  this._root === null && (r.left = r.right = null, this._size++, this._root = r);
19276
20119
  const l = this._comparator, i = u$1(t, this._root, l), o = l(t, i.key);
19277
20120
  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;
@@ -19484,23 +20327,23 @@ class z {
19484
20327
  function a(n, t, e, r) {
19485
20328
  const l = r - e;
19486
20329
  if (l > 0) {
19487
- const i = e + Math.floor(l / 2), o = n[i], s = t[i], h = new f$1(o, s);
20330
+ const i = e + Math.floor(l / 2), o = n[i], s = t[i], h = new f$3(o, s);
19488
20331
  return h.left = a(n, t, e, i), h.right = a(n, t, i + 1, r), h;
19489
20332
  }
19490
20333
  return null;
19491
20334
  }
19492
20335
  function x(n, t) {
19493
- const e = new f$1(null, null);
20336
+ const e = new f$3(null, null);
19494
20337
  let r = e;
19495
20338
  for (let l = 0; l < n.length; l++)
19496
- r = r.next = new f$1(n[l], t[l]);
20339
+ r = r.next = new f$3(n[l], t[l]);
19497
20340
  return r.next = null, e.next;
19498
20341
  }
19499
20342
  function k(n) {
19500
20343
  let t = n;
19501
20344
  const e = [];
19502
20345
  let r = false;
19503
- const l = new f$1(null, null);
20346
+ const l = new f$3(null, null);
19504
20347
  let i = l;
19505
20348
  for (; !r; )
19506
20349
  t ? (e.push(t), t = t.left) : e.length > 0 ? (t = i = i.next = e.pop(), t = t.right) : r = true;
@@ -19515,7 +20358,7 @@ function p(n, t, e) {
19515
20358
  return null;
19516
20359
  }
19517
20360
  function y(n, t, e) {
19518
- const r = new f$1(null, null);
20361
+ const r = new f$3(null, null);
19519
20362
  let l = r, i = n, o = t;
19520
20363
  for (; i !== null && o !== null; )
19521
20364
  e(i.key, o.key) < 0 ? (l.next = i, i = i.next) : (l.next = o, o = o.next), l = l.next;
@@ -24500,7 +25343,13 @@ function normalizeTruckShapeForBooleanInput(shape) {
24500
25343
  return normalized;
24501
25344
  }
24502
25345
  function lowerSdfPlan(plan) {
24503
- const evalFn = compileSdfNode3(plan.tree);
25346
+ var _a3, _b3, _c2;
25347
+ const evaluator = compileSdfMaterializationEvaluator3(plan.tree);
25348
+ if ((_a3 = plan.meshing) == null ? void 0 : _a3.diagnostics) {
25349
+ plan.meshing.diagnostics.evaluator = evaluator.engine;
25350
+ if (evaluator.unsupportedReason) plan.meshing.diagnostics.evaluatorUnsupportedReason = evaluator.unsupportedReason;
25351
+ }
25352
+ const evalFn = evaluator.fn;
24504
25353
  const inset = plan.edgeLength;
24505
25354
  const cappedEvalFn = (x2, y2, z2) => {
24506
25355
  const bx = Math.max(plan.bounds.min[0] + inset - x2, x2 - plan.bounds.max[0] + inset);
@@ -24512,14 +25361,18 @@ function lowerSdfPlan(plan) {
24512
25361
  assertSdfMeshBudget(mesh, plan);
24513
25362
  let surfaceNetsError;
24514
25363
  try {
24515
- return lowerExtractedSdfMesh(mesh, cappedEvalFn, true);
25364
+ const shape = lowerExtractedSdfMesh(mesh, cappedEvalFn, true);
25365
+ if ((_b3 = plan.meshing) == null ? void 0 : _b3.diagnostics) logSdfMeshingDiagnostics("SDF meshing result", plan.meshing.diagnostics);
25366
+ return shape;
24516
25367
  } catch (error) {
24517
25368
  surfaceNetsError = error;
24518
25369
  }
24519
25370
  const tetraMesh = marchingTetrahedra(cappedEvalFn, plan.bounds, plan.edgeLength);
24520
25371
  assertSdfMeshBudget(tetraMesh, plan);
24521
25372
  try {
24522
- return lowerExtractedSdfMesh(tetraMesh, cappedEvalFn, false);
25373
+ const shape = lowerExtractedSdfMesh(tetraMesh, cappedEvalFn, false);
25374
+ if ((_c2 = plan.meshing) == null ? void 0 : _c2.diagnostics) logSdfMeshingDiagnostics("SDF meshing result", plan.meshing.diagnostics);
25375
+ return shape;
24523
25376
  } catch (error) {
24524
25377
  throw new Error(
24525
25378
  `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)}`
@@ -24883,7 +25736,9 @@ function lowerOffsetSolidPlan(plan) {
24883
25736
  if (base.kind === "transform") {
24884
25737
  return lowerTransformedOffsetSolidPlan(base, plan.thickness);
24885
25738
  }
24886
- return truckUnsupported(`compile plan "${plan.kind}" for non-vertical-prism/non-revolved/non-loft/non-straight-sweep/non-straight-variable-sweep solids`);
25739
+ return truckUnsupported(
25740
+ `compile plan "${plan.kind}" for non-vertical-prism/non-revolved/non-loft/non-straight-sweep/non-straight-variable-sweep solids`
25741
+ );
24887
25742
  }
24888
25743
  function lowerLoftPlan(plan) {
24889
25744
  return wrapTruckShapeBackend(
@@ -32185,6 +33040,37 @@ function mergeSketchPlacementModel(sketches) {
32185
33040
  }
32186
33041
  return first;
32187
33042
  }
33043
+ function normalizeSceneTags(value, label = "tags") {
33044
+ if (value == null) return [];
33045
+ const rawTags = typeof value === "string" ? [value] : value;
33046
+ if (!Array.isArray(rawTags)) {
33047
+ throw new Error(`${label} must be a string or array of strings`);
33048
+ }
33049
+ const out = [];
33050
+ const seen = /* @__PURE__ */ new Set();
33051
+ rawTags.forEach((tag, index2) => {
33052
+ if (typeof tag !== "string") {
33053
+ throw new Error(`${label}[${index2}] must be a string`);
33054
+ }
33055
+ const trimmed = tag.trim();
33056
+ if (!trimmed || seen.has(trimmed)) return;
33057
+ seen.add(trimmed);
33058
+ out.push(trimmed);
33059
+ });
33060
+ return out;
33061
+ }
33062
+ function mergeSceneTags(...values) {
33063
+ const out = [];
33064
+ const seen = /* @__PURE__ */ new Set();
33065
+ values.forEach((value) => {
33066
+ normalizeSceneTags(value).forEach((tag) => {
33067
+ if (seen.has(tag)) return;
33068
+ seen.add(tag);
33069
+ out.push(tag);
33070
+ });
33071
+ });
33072
+ return out;
33073
+ }
32188
33074
  const _groupPlacementRefs = /* @__PURE__ */ new WeakMap();
32189
33075
  const _groupExplodeHint = /* @__PURE__ */ new WeakMap();
32190
33076
  function getGroupRefs(g2) {
@@ -32208,7 +33094,7 @@ function transformGroupRefs(source, dest, matrix) {
32208
33094
  }
32209
33095
  return dest;
32210
33096
  }
32211
- function requireFiniteAngle(v, method) {
33097
+ function requireFiniteAngle$1(v, method) {
32212
33098
  if (typeof v !== "number" || !Number.isFinite(v))
32213
33099
  throw new Error(`${method} angleDeg must be a finite number, got ${typeof v === "number" ? v : typeof v}`);
32214
33100
  }
@@ -32269,31 +33155,46 @@ function resolveNamedGroupChild(item) {
32269
33155
  function normalizeGroupInputs(items) {
32270
33156
  const children = [];
32271
33157
  const childNames = [];
33158
+ const childTags = [];
32272
33159
  items.forEach((item) => {
32273
33160
  if (isNamedGroupChild(item)) {
32274
33161
  children.push(resolveNamedGroupChild(item));
32275
33162
  childNames.push(normalizeChildName(item.name));
33163
+ childTags.push(normalizeSceneTags(item.tags, `group(...) named item "${item.name}" tags`));
32276
33164
  return;
32277
33165
  }
32278
33166
  children.push(item);
32279
33167
  childNames.push(void 0);
33168
+ childTags.push([]);
32280
33169
  });
32281
- return { children, childNames };
33170
+ return { children, childNames, childTags };
32282
33171
  }
32283
33172
  class ShapeGroup {
32284
- constructor(children, childNames) {
33173
+ constructor(children, childNames, childTags) {
32285
33174
  __publicField(this, "children");
32286
33175
  __publicField(this, "childNames");
33176
+ __publicField(this, "childTags");
32287
33177
  if (childNames && childNames.length !== children.length) {
32288
33178
  throw new Error("ShapeGroup childNames must match children length");
32289
33179
  }
33180
+ if (childTags && childTags.length !== children.length) {
33181
+ throw new Error("ShapeGroup childTags must match children length");
33182
+ }
32290
33183
  this.children = [...children];
32291
33184
  this.childNames = this.children.map((_2, index2) => normalizeChildName(childNames == null ? void 0 : childNames[index2]));
33185
+ this.childTags = this.children.map((_2, index2) => normalizeSceneTags(childTags == null ? void 0 : childTags[index2], "ShapeGroup childTags"));
32292
33186
  }
32293
33187
  /** Return the optional name of the child at `index`. */
32294
33188
  childName(index2) {
32295
33189
  return this.childNames[index2];
32296
33190
  }
33191
+ /**
33192
+ * Return tags attached to the child at `index`.
33193
+ * @internal
33194
+ */
33195
+ tagsForChild(index2) {
33196
+ return [...this.childTags[index2] ?? []];
33197
+ }
32297
33198
  /**
32298
33199
  * Return the named child by name. Throws if not found.
32299
33200
  * Useful when importing a multipart group and working on components individually.
@@ -32308,13 +33209,13 @@ class ShapeGroup {
32308
33209
  }
32309
33210
  /** Apply fn to all children, producing a new ShapeGroup that also copies placement refs. */
32310
33211
  mapChildren(fn) {
32311
- const next = new ShapeGroup(this.children.map(fn), this.childNames);
33212
+ const next = new ShapeGroup(this.children.map(fn), this.childNames, this.childTags);
32312
33213
  copyGroupPorts(this, next);
32313
33214
  return copyGroupRefs(this, next);
32314
33215
  }
32315
33216
  /** Apply fn to all children and also transform placement refs by the given matrix. */
32316
33217
  mapChildrenTransform(fn, matrix) {
32317
- const next = new ShapeGroup(this.children.map(fn), this.childNames);
33218
+ const next = new ShapeGroup(this.children.map(fn), this.childNames, this.childTags);
32318
33219
  transformGroupPortsHelper(this, next, matrix);
32319
33220
  return transformGroupRefs(this, next, matrix);
32320
33221
  }
@@ -32441,25 +33342,25 @@ class ShapeGroup {
32441
33342
  /** Rotate the group around an arbitrary axis through the origin. */
32442
33343
  rotate(axis, angleDeg, options) {
32443
33344
  requireRotateAxis(axis, "ShapeGroup.rotate()");
32444
- requireFiniteAngle(angleDeg, "ShapeGroup.rotate()");
33345
+ requireFiniteAngle$1(angleDeg, "ShapeGroup.rotate()");
32445
33346
  if (options == null ? void 0 : options.pivot) requireVec3Pivot(options.pivot, "ShapeGroup.rotate()");
32446
33347
  return this.rotateAroundAxis(axis, angleDeg, options == null ? void 0 : options.pivot);
32447
33348
  }
32448
33349
  /** Rotate the group around the X axis. */
32449
33350
  rotateX(angleDeg, options) {
32450
- requireFiniteAngle(angleDeg, "ShapeGroup.rotateX()");
33351
+ requireFiniteAngle$1(angleDeg, "ShapeGroup.rotateX()");
32451
33352
  if (options == null ? void 0 : options.pivot) requireVec3Pivot(options.pivot, "ShapeGroup.rotateX()");
32452
33353
  return this.rotateAroundAxis([1, 0, 0], angleDeg, options == null ? void 0 : options.pivot);
32453
33354
  }
32454
33355
  /** Rotate the group around the Y axis. */
32455
33356
  rotateY(angleDeg, options) {
32456
- requireFiniteAngle(angleDeg, "ShapeGroup.rotateY()");
33357
+ requireFiniteAngle$1(angleDeg, "ShapeGroup.rotateY()");
32457
33358
  if (options == null ? void 0 : options.pivot) requireVec3Pivot(options.pivot, "ShapeGroup.rotateY()");
32458
33359
  return this.rotateAroundAxis([0, 1, 0], angleDeg, options == null ? void 0 : options.pivot);
32459
33360
  }
32460
33361
  /** Rotate the group around the Z axis. */
32461
33362
  rotateZ(angleDeg, options) {
32462
- requireFiniteAngle(angleDeg, "ShapeGroup.rotateZ()");
33363
+ requireFiniteAngle$1(angleDeg, "ShapeGroup.rotateZ()");
32463
33364
  if (options == null ? void 0 : options.pivot) requireVec3Pivot(options.pivot, "ShapeGroup.rotateZ()");
32464
33365
  return this.rotateAroundAxis([0, 0, 1], angleDeg, options == null ? void 0 : options.pivot);
32465
33366
  }
@@ -32502,7 +33403,8 @@ class ShapeGroup {
32502
33403
  "ShapeGroup.transform only supports 3D children (Shape/ShapeGroup). For Sketch children, use 2D transforms (translate/rotate/scale/mirror)."
32503
33404
  );
32504
33405
  }),
32505
- this.childNames
33406
+ this.childNames,
33407
+ this.childTags
32506
33408
  );
32507
33409
  transformGroupPortsHelper(this, next, matrix);
32508
33410
  return transformGroupRefs(this, next, matrix);
@@ -32570,7 +33472,7 @@ class ShapeGroup {
32570
33472
  * ```
32571
33473
  */
32572
33474
  withReferences(refs) {
32573
- const next = new ShapeGroup(this.children, this.childNames);
33475
+ const next = new ShapeGroup(this.children, this.childNames, this.childTags);
32574
33476
  const merged = applyPlacementReferenceInput(getGroupRefs(this), refs);
32575
33477
  return setGroupRefs(next, merged);
32576
33478
  }
@@ -32638,7 +33540,7 @@ class ShapeGroup {
32638
33540
  /** Attach named connectors — attachment points that survive transforms.
32639
33541
  * Connectors can be bare (position + orientation) or typed (with connectorType/gender for compatibility matching). */
32640
33542
  withConnectors(connectors) {
32641
- const next = new ShapeGroup(this.children, this.childNames);
33543
+ const next = new ShapeGroup(this.children, this.childNames, this.childTags);
32642
33544
  copyGroupRefs(this, next);
32643
33545
  const existing = getGroupPorts(this);
32644
33546
  const incoming = normalizeConnectorMapInput(connectors);
@@ -32688,7 +33590,7 @@ class ShapeGroup {
32688
33590
  }
32689
33591
  function group(...items) {
32690
33592
  const normalized = normalizeGroupInputs(items);
32691
- return new ShapeGroup(normalized.children, normalized.childNames);
33593
+ return new ShapeGroup(normalized.children, normalized.childNames, normalized.childTags);
32692
33594
  }
32693
33595
  function getTargetPortsForGroup(target) {
32694
33596
  if (target instanceof Shape) {
@@ -34859,7 +35761,7 @@ function buildSdfFunctionDefinition(source, options) {
34859
35761
  jsExpression: expression,
34860
35762
  ...shader.ok ? { shaderExpression: shader.expression } : { shaderUnsupportedReason: shader.reason },
34861
35763
  raymarchStepLimit: resolveRaymarchStepLimit(options.bounds, options.maxStep),
34862
- ...options.lipschitz !== void 0 ? { raymarchLipschitz: requirePositiveFinite$1(options.lipschitz, "sdf.fromFunction() lipschitz") } : {}
35764
+ ...options.lipschitz !== void 0 ? { raymarchLipschitz: requirePositiveFinite$2(options.lipschitz, "sdf.fromFunction() lipschitz") } : {}
34863
35765
  };
34864
35766
  }
34865
35767
  function extractSdfExpression(source) {
@@ -35027,7 +35929,7 @@ function formatNumericLiteralsForGlsl(source) {
35027
35929
  return result;
35028
35930
  }
35029
35931
  function resolveRaymarchStepLimit(bounds, maxStep) {
35030
- if (maxStep !== void 0) return requirePositiveFinite$1(maxStep, "sdf.fromFunction() maxStep");
35932
+ if (maxStep !== void 0) return requirePositiveFinite$2(maxStep, "sdf.fromFunction() maxStep");
35031
35933
  const dx = bounds.max[0] - bounds.min[0];
35032
35934
  const dy = bounds.max[1] - bounds.min[1];
35033
35935
  const dz = bounds.max[2] - bounds.min[2];
@@ -35035,7 +35937,7 @@ function resolveRaymarchStepLimit(bounds, maxStep) {
35035
35937
  if (!Number.isFinite(diagonal) || diagonal <= 0) return 0.1;
35036
35938
  return Math.max(0.025, Math.min(0.5, diagonal / 240));
35037
35939
  }
35038
- function requirePositiveFinite$1(value, label) {
35940
+ function requirePositiveFinite$2(value, label) {
35039
35941
  if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be a positive finite number.`);
35040
35942
  return value;
35041
35943
  }
@@ -35062,6 +35964,199 @@ class SurfacePattern {
35062
35964
  this.constants = constants;
35063
35965
  }
35064
35966
  }
35967
+ const typedSurfacePatterns = /* @__PURE__ */ new WeakMap();
35968
+ function getTypedSurfacePattern(pattern) {
35969
+ return typedSurfacePatterns.get(pattern);
35970
+ }
35971
+ class Pattern2D extends SurfacePattern {
35972
+ constructor(body) {
35973
+ super(body);
35974
+ }
35975
+ /** Add this pattern to one or more patterns or constant height offsets. */
35976
+ add(...patterns) {
35977
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
35978
+ }
35979
+ /** Subtract another pattern or constant height offset from this pattern. */
35980
+ subtract(pattern) {
35981
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
35982
+ }
35983
+ /** Multiply this pattern by one or more patterns or numeric scale factors. */
35984
+ multiply(...patterns) {
35985
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
35986
+ }
35987
+ /** Keep the lower height between this pattern and one or more other patterns. */
35988
+ min(...patterns) {
35989
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
35990
+ }
35991
+ /** Keep the higher height between this pattern and one or more other patterns. */
35992
+ max(...patterns) {
35993
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
35994
+ }
35995
+ /** Limit pattern height to the inclusive `[min, max]` range in millimeters. */
35996
+ clamp(min2, max2) {
35997
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
35998
+ }
35999
+ /** Convert negative heights to positive heights. */
36000
+ abs() {
36001
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
36002
+ }
36003
+ /** Flip the pattern height sign. */
36004
+ negate() {
36005
+ throw new Error("Pattern2D values are created by sdf.pattern2d().");
36006
+ }
36007
+ }
36008
+ class Pattern2DImpl extends Pattern2D {
36009
+ constructor(node) {
36010
+ super(emitSurfacePatternJsExpression(node));
36011
+ __publicField(this, "node");
36012
+ this.node = node;
36013
+ typedSurfacePatterns.set(this, node);
36014
+ }
36015
+ add(...patterns) {
36016
+ return new Pattern2DImpl({ kind: "surfacePattern:add", children: [this.node, ...patterns.map(patternNodeFromInput)] });
36017
+ }
36018
+ subtract(pattern) {
36019
+ return this.add(new Pattern2DImpl({ kind: "surfacePattern:negate", child: patternNodeFromInput(pattern) }));
36020
+ }
36021
+ multiply(...patterns) {
36022
+ return new Pattern2DImpl({ kind: "surfacePattern:multiply", children: [this.node, ...patterns.map(patternNodeFromInput)] });
36023
+ }
36024
+ min(...patterns) {
36025
+ return new Pattern2DImpl({ kind: "surfacePattern:min", children: [this.node, ...patterns.map(patternNodeFromInput)] });
36026
+ }
36027
+ max(...patterns) {
36028
+ return new Pattern2DImpl({ kind: "surfacePattern:max", children: [this.node, ...patterns.map(patternNodeFromInput)] });
36029
+ }
36030
+ clamp(min2, max2) {
36031
+ const lo = requireFinite$8(min2, "Pattern2D.clamp() min");
36032
+ const hi = requireFinite$8(max2, "Pattern2D.clamp() max");
36033
+ if (lo > hi) throw new Error(`Pattern2D.clamp() min must be <= max. Received: ${lo} > ${hi}`);
36034
+ return new Pattern2DImpl({ kind: "surfacePattern:clamp", child: this.node, min: lo, max: hi });
36035
+ }
36036
+ abs() {
36037
+ return new Pattern2DImpl({ kind: "surfacePattern:abs", child: this.node });
36038
+ }
36039
+ negate() {
36040
+ return new Pattern2DImpl({ kind: "surfacePattern:negate", child: this.node });
36041
+ }
36042
+ }
36043
+ class Pattern2DBuilder {
36044
+ /** Create a constant-height pattern in millimeters. */
36045
+ constant(value = 0) {
36046
+ return new Pattern2DImpl({ kind: "surfacePattern:constant", value: requireFinite$8(value, "sdf.pattern2d().constant() value") });
36047
+ }
36048
+ /** Create a sinusoidal wave pattern in UV space. */
36049
+ sineWave(options) {
36050
+ return new Pattern2DImpl({
36051
+ kind: "surfacePattern:sineWave",
36052
+ direction: normalizeDirection$1(options.direction ?? [1, 0], "sdf.pattern2d().sineWave() direction"),
36053
+ wavelength: requirePositiveFinite$1(options.wavelength, "sdf.pattern2d().sineWave() wavelength"),
36054
+ amplitude: requireFinite$8(options.amplitude ?? 1, "sdf.pattern2d().sineWave() amplitude"),
36055
+ phase: requireFinite$8(options.phase ?? 0, "sdf.pattern2d().sineWave() phase"),
36056
+ bias: requireFinite$8(options.bias ?? 0, "sdf.pattern2d().sineWave() bias")
36057
+ });
36058
+ }
36059
+ /** Create recessed stripe bands in UV space. */
36060
+ stripes(options) {
36061
+ return new Pattern2DImpl({
36062
+ kind: "surfacePattern:stripes",
36063
+ direction: normalizeDirection$1(options.direction ?? [1, 0], "sdf.pattern2d().stripes() direction"),
36064
+ spacing: requirePositiveFinite$1(options.spacing, "sdf.pattern2d().stripes() spacing"),
36065
+ width: requirePositiveFinite$1(options.width, "sdf.pattern2d().stripes() width"),
36066
+ depth: requireNonNegativeFinite$1(options.depth ?? 1, "sdf.pattern2d().stripes() depth")
36067
+ });
36068
+ }
36069
+ /** Create an over-under woven relief pattern in UV space. */
36070
+ overUnderWeave(options) {
36071
+ return new Pattern2DImpl({
36072
+ kind: "surfacePattern:overUnderWeave",
36073
+ spacing: normalizeVec2(options.spacing, "sdf.pattern2d().overUnderWeave() spacing", requirePositiveFinite$1),
36074
+ threadWidth: normalizeVec2(options.threadWidth, "sdf.pattern2d().overUnderWeave() threadWidth", requirePositiveFinite$1),
36075
+ depth: requireNonNegativeFinite$1(options.depth ?? 0.8, "sdf.pattern2d().overUnderWeave() depth"),
36076
+ underScale: requireNonNegativeFinite$1(options.underScale ?? 0.15, "sdf.pattern2d().overUnderWeave() underScale")
36077
+ });
36078
+ }
36079
+ }
36080
+ function pattern2d() {
36081
+ return new Pattern2DBuilder();
36082
+ }
36083
+ function patternNodeFromInput(input) {
36084
+ if (input instanceof SurfacePattern) {
36085
+ const node = getTypedSurfacePattern(input);
36086
+ if (node) return node;
36087
+ }
36088
+ if (typeof input === "number") {
36089
+ return { kind: "surfacePattern:constant", value: requireFinite$8(input, "Pattern2D numeric input") };
36090
+ }
36091
+ throw new Error("Pattern2D composition expects another typed Pattern2D or a number.");
36092
+ }
36093
+ function requireFinite$8(value, label) {
36094
+ if (typeof value !== "number" || !Number.isFinite(value)) {
36095
+ throw new Error(`${label} must be a finite number. Received: ${String(value)}`);
36096
+ }
36097
+ return value;
36098
+ }
36099
+ function requirePositiveFinite$1(value, label) {
36100
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
36101
+ throw new Error(`${label} must be a positive finite number. Received: ${String(value)}`);
36102
+ }
36103
+ return value;
36104
+ }
36105
+ function requireNonNegativeFinite$1(value, label) {
36106
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
36107
+ throw new Error(`${label} must be a non-negative finite number. Received: ${String(value)}`);
36108
+ }
36109
+ return value;
36110
+ }
36111
+ function normalizeVec2(value, label, validate) {
36112
+ if (typeof value === "number") {
36113
+ const n = validate(value, label);
36114
+ return [n, n];
36115
+ }
36116
+ return [validate(value[0], `${label}[0]`), validate(value[1], `${label}[1]`)];
36117
+ }
36118
+ function normalizeDirection$1(value, label) {
36119
+ const x2 = requireFinite$8(value[0], `${label}[0]`);
36120
+ const y2 = requireFinite$8(value[1], `${label}[1]`);
36121
+ const length4 = Math.hypot(x2, y2);
36122
+ if (length4 <= 0) throw new Error(`${label} must not be the zero vector.`);
36123
+ return [x2 / length4, y2 / length4];
36124
+ }
36125
+ function f$2(value) {
36126
+ if (!Number.isFinite(value)) return "0";
36127
+ return Number(value.toPrecision(12)).toString();
36128
+ }
36129
+ function emitSurfacePatternJsExpression(node) {
36130
+ switch (node.kind) {
36131
+ case "surfacePattern:constant":
36132
+ return f$2(node.value);
36133
+ case "surfacePattern:sineWave": {
36134
+ const coord = `(u * ${f$2(node.direction[0])} + v * ${f$2(node.direction[1])})`;
36135
+ const phase = `(${coord} * ${f$2(2 * Math.PI / node.wavelength)} + ${f$2(node.phase)})`;
36136
+ return `(${f$2(node.bias)} + Math.sin(${phase}) * ${f$2(node.amplitude)})`;
36137
+ }
36138
+ case "surfacePattern:stripes": {
36139
+ const coord = `(u * ${f$2(node.direction[0])} + v * ${f$2(node.direction[1])})`;
36140
+ 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)};})()`;
36141
+ }
36142
+ case "surfacePattern:overUnderWeave":
36143
+ 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)};})()`;
36144
+ case "surfacePattern:abs":
36145
+ return `Math.abs(${emitSurfacePatternJsExpression(node.child)})`;
36146
+ case "surfacePattern:negate":
36147
+ return `(-(${emitSurfacePatternJsExpression(node.child)}))`;
36148
+ case "surfacePattern:add":
36149
+ return node.children.length === 0 ? "0" : `(${node.children.map(emitSurfacePatternJsExpression).join(" + ")})`;
36150
+ case "surfacePattern:multiply":
36151
+ return node.children.length === 0 ? "1" : `(${node.children.map(emitSurfacePatternJsExpression).join(" * ")})`;
36152
+ case "surfacePattern:min":
36153
+ return node.children.length === 0 ? "0" : `Math.min(${node.children.map(emitSurfacePatternJsExpression).join(", ")})`;
36154
+ case "surfacePattern:max":
36155
+ return node.children.length === 0 ? "0" : `Math.max(${node.children.map(emitSurfacePatternJsExpression).join(", ")})`;
36156
+ case "surfacePattern:clamp":
36157
+ return `Math.min(${f$2(node.max)}, Math.max(${f$2(node.min)}, ${emitSurfacePatternJsExpression(node.child)}))`;
36158
+ }
36159
+ }
35065
36160
  const SCULPT_MATERIAL_PRESETS = {
35066
36161
  ceramic: {
35067
36162
  color: "#f4f0e6",
@@ -35131,6 +36226,18 @@ function requirePositiveFinite(value, label) {
35131
36226
  }
35132
36227
  return value;
35133
36228
  }
36229
+ function requirePositiveInteger(value, label) {
36230
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1) {
36231
+ throw new Error(`${label} must be a positive integer. Received: ${String(value)}`);
36232
+ }
36233
+ return value;
36234
+ }
36235
+ function requireNonNegativeFinite(value, label) {
36236
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
36237
+ throw new Error(`${label} must be a non-negative finite number. Received: ${String(value)}`);
36238
+ }
36239
+ return value;
36240
+ }
35134
36241
  function resolveBlendRadius(input, label, fallback = 4) {
35135
36242
  if (typeof input === "number") return requirePositiveFinite(input, `${label} radius`);
35136
36243
  if ((input == null ? void 0 : input.radius) !== void 0) return requirePositiveFinite(input.radius, `${label} radius`);
@@ -35377,6 +36484,29 @@ class SdfShape {
35377
36484
  clipBox(x2, y2, z2) {
35378
36485
  return this.intersect(box$1(x2, y2, z2));
35379
36486
  }
36487
+ /** Keep only the material where this shape overlaps another SDF pattern. */
36488
+ fillWith(pattern) {
36489
+ if (!(pattern instanceof SdfShape)) {
36490
+ throw new Error("SdfShape.fillWith() expects an SdfShape pattern, such as sdf.gyroid({ cellSize, wallThickness }).");
36491
+ }
36492
+ return this.intersect(pattern);
36493
+ }
36494
+ /** Keep only the gyroid lattice inside this shape. */
36495
+ fillWithGyroid(options) {
36496
+ return this.fillWith(gyroid(options));
36497
+ }
36498
+ /** Keep only the Schwarz-P lattice inside this shape. */
36499
+ fillWithSchwarzP(options) {
36500
+ return this.fillWith(schwarzP(options));
36501
+ }
36502
+ /** Keep only the diamond TPMS lattice inside this shape. */
36503
+ fillWithDiamond(options) {
36504
+ return this.fillWith(diamond(options));
36505
+ }
36506
+ /** Keep only the lidinoid TPMS lattice inside this shape. */
36507
+ fillWithLidinoid(options) {
36508
+ return this.fillWith(lidinoid(options));
36509
+ }
35380
36510
  /** Smooth union — blends shapes together with a smooth radius. */
35381
36511
  smoothUnion(other, radius) {
35382
36512
  return this.withNode({ kind: "sdf:smoothUnion", children: [this._node, other._node], radius });
@@ -35437,6 +36567,21 @@ class SdfShape {
35437
36567
  repeat(spacing, count) {
35438
36568
  return this.withNode({ kind: "sdf:repeat", child: this._node, spacing, count: count ?? [0, 0, 0] });
35439
36569
  }
36570
+ /**
36571
+ * Arrange this SDF in a circular array around the Z axis.
36572
+ *
36573
+ * The source shape is translated by `offset` in +X before arraying. This uses
36574
+ * angular domain folding, so evaluation stays O(1): the source SDF is sampled
36575
+ * twice no matter how many copies are requested.
36576
+ */
36577
+ circularArray(count, offset2 = 0) {
36578
+ return this.withNode({
36579
+ kind: "sdf:circularArray",
36580
+ child: this._node,
36581
+ count: requirePositiveInteger(count, "SdfShape.circularArray() count"),
36582
+ offset: requireNonNegativeFinite(offset2, "SdfShape.circularArray() offset")
36583
+ });
36584
+ }
35440
36585
  /** Hollow out, keeping only a shell of given thickness. */
35441
36586
  shell(thickness) {
35442
36587
  return this.withNode({ kind: "sdf:shell", child: this._node, thickness });
@@ -35448,8 +36593,8 @@ class SdfShape {
35448
36593
  * // Function displacement
35449
36594
  * shape.displace((x, y, z) => Math.sin(x) * 0.5)
35450
36595
  *
35451
- * // Pattern displacement (e.g. basketWeave)
35452
- * shape.displace(sdf.basketWeave({ threads: 16, spacing: 3 }))
36596
+ * // Pattern displacement from a 3D SDF field
36597
+ * shape.displace(sdf.knurl({ pitch: 2, depth: 0.3 }))
35453
36598
  * ```
35454
36599
  */
35455
36600
  displace(fn, constants) {
@@ -35473,10 +36618,18 @@ class SdfShape {
35473
36618
  * UV coordinates are in **surface millimeters** — patterns defined with `spacing: 3`
35474
36619
  * always produce 3mm spacing, regardless of shape size.
35475
36620
  *
36621
+ * Prefer `sdf.pattern2d()` or built-in surface patterns when the relief should
36622
+ * stay on the native shader and meshing path. Callback functions are supported
36623
+ * for experimentation, but they are opaque to the typed pattern optimizer.
36624
+ *
35476
36625
  * ```js
35477
- * // Surface-following basket weave — auto-detects sphere UV
36626
+ * // Native typed pattern — auto-detects sphere UV
36627
+ * const p = sdf.pattern2d()
36628
+ * const ribs = p.stripes({ spacing: 3, width: 0.8, depth: 0.35 })
36629
+ * .add(p.sineWave({ direction: [0, 1], wavelength: 14, amplitude: 0.08 }))
36630
+ *
35478
36631
  * sdf.sphere(27).shell(3)
35479
- * .surfaceDisplace(sdf.basketWeave({ spacing: 3, depth: 0.8 }))
36632
+ * .surfaceDisplace(ribs)
35480
36633
  * .toShape()
35481
36634
  *
35482
36635
  * // Custom 2D pattern via function
@@ -35486,15 +36639,18 @@ class SdfShape {
35486
36639
  surfaceDisplace(pattern, options) {
35487
36640
  let body;
35488
36641
  let constants;
36642
+ let typedPattern;
35489
36643
  if (pattern instanceof SurfacePattern) {
35490
36644
  body = pattern.body;
35491
36645
  constants = pattern.constants;
36646
+ typedPattern = getTypedSurfacePattern(pattern);
35492
36647
  } else {
35493
36648
  body = extractFunctionBody(pattern);
35494
36649
  }
35495
36650
  return this.withNode({
35496
36651
  kind: "sdf:surfaceDisplace",
35497
36652
  child: this._node,
36653
+ ...typedPattern ? { pattern: typedPattern } : {},
35498
36654
  patternBody: body,
35499
36655
  constants,
35500
36656
  ...(options == null ? void 0 : options.uv) ? { uvMode: options.uv } : {},
@@ -35713,24 +36869,10 @@ function weave(options) {
35713
36869
  });
35714
36870
  }
35715
36871
  function basketWeave(options) {
35716
- const SP = (options == null ? void 0 : options.spacing) ?? 3;
35717
- const TW = (options == null ? void 0 : options.threadWidth) ?? 1.5;
35718
- const D2 = (options == null ? void 0 : options.depth) ?? 0.8;
35719
- const hw = TW * 0.5;
35720
- const body = `(function() {
35721
- var su = u / ${SP};
35722
- var sv = v / ${SP};
35723
- var du = Math.abs(su - Math.round(su)) * ${SP};
35724
- var dv = Math.abs(sv - Math.round(sv)) * ${SP};
35725
- var hw = ${hw};
35726
- var pU = Math.max(0, 1 - du / hw); pU *= pU;
35727
- var pV = Math.max(0, 1 - dv / hw); pV *= pV;
35728
- var checker = ((Math.round(su) & 65535) + (Math.round(sv) & 65535)) & 1;
35729
- var top = checker ? pV : pU;
35730
- var bot = checker ? pU : pV;
35731
- return -(top > bot * 0.15 ? top : bot * 0.15) * ${D2};
35732
- })()`;
35733
- return new SurfacePattern(body);
36872
+ const SP = requirePositiveFinite((options == null ? void 0 : options.spacing) ?? 3, "sdf.basketWeave() spacing");
36873
+ const TW = requirePositiveFinite((options == null ? void 0 : options.threadWidth) ?? 1.5, "sdf.basketWeave() threadWidth");
36874
+ const D2 = requireNonNegativeFinite((options == null ? void 0 : options.depth) ?? 0.8, "sdf.basketWeave() depth");
36875
+ return pattern2d().overUnderWeave({ spacing: SP, threadWidth: TW, depth: D2 });
35734
36876
  }
35735
36877
  function fromFunction(fn, options) {
35736
36878
  if (!options || typeof options !== "object") {
@@ -35763,6 +36905,9 @@ function bend(shape, radius) {
35763
36905
  function repeat(shape, spacing, count) {
35764
36906
  return shape.repeat(spacing, count);
35765
36907
  }
36908
+ function circularArray(shape, count, offset2 = 0) {
36909
+ return shape.circularArray(count, offset2);
36910
+ }
35766
36911
  function resolveTpmsOptions(options) {
35767
36912
  const wallThickness = options.wallThickness;
35768
36913
  const thickness = wallThickness ?? options.thickness;
@@ -36203,12 +37348,16 @@ const sdf = {
36203
37348
  weave,
36204
37349
  /** Basket weave surface pattern — threads with over-under crossings in UV space. Returns a SurfacePattern for use with `.surfaceDisplace()`. */
36205
37350
  basketWeave,
37351
+ /** Create typed, composable 2D surface patterns for `.surfaceDisplace()`. */
37352
+ pattern2d,
36206
37353
  /** Twist an SDF shape around the Z axis. */
36207
37354
  twist,
36208
37355
  /** Bend an SDF shape around the Z axis. */
36209
37356
  bend,
36210
37357
  /** Repeat an SDF shape in space. */
36211
37358
  repeat,
37359
+ /** Arrange an SDF shape in a circular array around the Z axis with O(1) folded-domain evaluation. */
37360
+ circularArray,
36212
37361
  /** A 2D surface pattern — a heightmap function for use with `.surfaceDisplace()`. */
36213
37362
  SurfacePattern,
36214
37363
  /** Create a custom SDF from one expression; shader-safe expressions raymarch directly. */
@@ -41128,13 +42277,16 @@ class SolvedAssembly {
41128
42277
  * @category Assembly
41129
42278
  */
41130
42279
  toGroup() {
42280
+ var _a3;
41131
42281
  const children = [];
41132
42282
  const childNames = [];
41133
- for (const [name] of this.parts) {
42283
+ const childTags = [];
42284
+ for (const [name, rec] of this.parts) {
41134
42285
  children.push(this.getPart(name));
41135
42286
  childNames.push(name);
42287
+ childTags.push(normalizeSceneTags((_a3 = rec.metadata) == null ? void 0 : _a3.tags, `Assembly part "${name}" metadata.tags`));
41136
42288
  }
41137
- return new ShapeGroup(children, childNames);
42289
+ return new ShapeGroup(children, childNames, childTags);
41138
42290
  }
41139
42291
  /**
41140
42292
  * Return an array of named scene objects for the viewport renderer.
@@ -41168,17 +42320,18 @@ class SolvedAssembly {
41168
42320
  const used = usedByPart.get(partName);
41169
42321
  if (used && used.length > 0) markShapePortsUsed(shape, used);
41170
42322
  };
41171
- const appendGroupChildren = (grp, prefix, partName, out2) => {
42323
+ const appendGroupChildren = (grp, prefix, partName, out2, inheritedTags = []) => {
41172
42324
  grp.children.forEach((child, index2) => {
41173
42325
  const childName = grp.childName(index2);
41174
42326
  const label = childName ? `${prefix}.${childName}` : `${prefix}.${index2 + 1}`;
42327
+ const tags = mergeSceneTags(inheritedTags, grp.tagsForChild(index2));
41175
42328
  if (child instanceof ShapeGroup) {
41176
- appendGroupChildren(child, label, partName, out2);
42329
+ appendGroupChildren(child, label, partName, out2, tags);
41177
42330
  return;
41178
42331
  }
41179
42332
  if (child instanceof Shape) {
41180
42333
  markUsedOnShape(child, partName);
41181
- out2.push({ name: label, shape: child });
42334
+ out2.push({ name: label, shape: child, ...tags.length > 0 ? { tags } : {} });
41182
42335
  }
41183
42336
  });
41184
42337
  };
@@ -41544,7 +42697,7 @@ class Assembly {
41544
42697
  *
41545
42698
  * @param name - Unique part name (must not already exist)
41546
42699
  * @param part - The `Shape` or `ShapeGroup` geometry
41547
- * @param options - Optional `{ transform, metadata }` (material, process, qty, etc.)
42700
+ * @param options - Optional `{ transform, metadata }` (material, process, qty, tags, etc.)
41548
42701
  * @returns `this` for chaining
41549
42702
  * @category Assembly
41550
42703
  */
@@ -42498,15 +43651,18 @@ class ImportedAssembly {
42498
43651
  * Any stored placement offset and placement references are forwarded to the group.
42499
43652
  */
42500
43653
  toGroup(state) {
43654
+ var _a3;
42501
43655
  const solved = this._assembly.solve(state);
42502
43656
  const def = this._assembly.describe();
42503
43657
  const children = [];
42504
43658
  const childNames = [];
43659
+ const childTags = [];
42505
43660
  for (const p2 of def.parts) {
42506
43661
  children.push(solved.getPart(p2.name));
42507
43662
  childNames.push(p2.name);
43663
+ childTags.push(normalizeSceneTags((_a3 = p2.metadata) == null ? void 0 : _a3.tags, `Assembly part "${p2.name}" metadata.tags`));
42508
43664
  }
42509
- let result = new ShapeGroup(children, childNames);
43665
+ let result = new ShapeGroup(children, childNames, childTags);
42510
43666
  const [dx, dy, dz] = this._offset;
42511
43667
  if (dx !== 0 || dy !== 0 || dz !== 0) {
42512
43668
  result = result.translate(dx, dy, dz);
@@ -43052,7 +44208,7 @@ class GCodeBuilder {
43052
44208
  this.lines.push("G1 E-0.8 F1800 ; retract");
43053
44209
  this.lines.push("");
43054
44210
  const safeZ = Math.min(maxZ + 5, p2.bedZ - 1);
43055
- this.lines.push(`G1 Z${f2(safeZ)} F900 ; lift nozzle above print`);
44211
+ this.lines.push(`G1 Z${f$1(safeZ)} F900 ; lift nozzle above print`);
43056
44212
  this.lines.push("");
43057
44213
  this.lines.push("M140 S0 ; bed off");
43058
44214
  this.lines.push("M104 S0 ; hotend off");
@@ -43096,7 +44252,7 @@ class GCodeBuilder {
43096
44252
  travelTo(x2, y2, z2) {
43097
44253
  this.retract();
43098
44254
  const from = [...this.pos];
43099
- this.lines.push(`G0 X${f2(x2)} Y${f2(y2)} Z${f2(z2)} F${Math.round(this.profile.travelSpeed)}`);
44255
+ this.lines.push(`G0 X${f$1(x2)} Y${f$1(y2)} Z${f$1(z2)} F${Math.round(this.profile.travelSpeed)}`);
43100
44256
  if (this.posInitialized) {
43101
44257
  this._segments.push({ from, to: [x2, y2, z2], extrude: false, speed: this.profile.travelSpeed });
43102
44258
  }
@@ -43128,7 +44284,7 @@ class GCodeBuilder {
43128
44284
  const beadArea = this.profile.layerHeight * this.profile.nozzle;
43129
44285
  const eIncrement = beadArea * dist4 / this.filamentArea;
43130
44286
  this.e += eIncrement;
43131
- this.lines.push(`G1 X${f2(x2)} Y${f2(y2)} Z${f2(z2)} E${f2(this.e)} F${Math.round(this.currentSpeed)}`);
44287
+ 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)}`);
43132
44288
  if (this.posInitialized) {
43133
44289
  this._segments.push({ from, to: [x2, y2, z2], extrude: true, speed: this.currentSpeed });
43134
44290
  }
@@ -43226,13 +44382,13 @@ class GCodeBuilder {
43226
44382
  retract() {
43227
44383
  if (this.retracted) return;
43228
44384
  this.e -= this.profile.retractionDistance;
43229
- this.lines.push(`G1 E${f2(this.e)} F${Math.round(this.profile.retractionSpeed)}`);
44385
+ this.lines.push(`G1 E${f$1(this.e)} F${Math.round(this.profile.retractionSpeed)}`);
43230
44386
  this.retracted = true;
43231
44387
  }
43232
44388
  unretract() {
43233
44389
  if (!this.retracted) return;
43234
44390
  this.e += this.profile.retractionDistance;
43235
- this.lines.push(`G1 E${f2(this.e)} F${Math.round(this.profile.retractionSpeed)}`);
44391
+ this.lines.push(`G1 E${f$1(this.e)} F${Math.round(this.profile.retractionSpeed)}`);
43236
44392
  this.retracted = false;
43237
44393
  }
43238
44394
  // ---- Bounds tracking ----
@@ -43303,7 +44459,7 @@ class GCodeBuilder {
43303
44459
  return this.lines.join("\n") + "\n";
43304
44460
  }
43305
44461
  }
43306
- function f2(n) {
44462
+ function f$1(n) {
43307
44463
  return n.toFixed(5).replace(/\.?0+$/, "");
43308
44464
  }
43309
44465
  function bambuModelName(preset) {
@@ -45447,10 +46603,8 @@ class PathBuilder {
45447
46603
  if (radius <= 0) throw new Error("fillet: radius must be positive");
45448
46604
  const n = this.segs.length;
45449
46605
  if (n < 2) throw new Error("fillet: need at least 2 segments before a fillet");
45450
- const prev = this.segs[n - 2];
45451
46606
  const curr = this.segs[n - 1];
45452
- curr.kind === "line" || curr.kind === "move" ? prev.kind === "line" || prev.kind === "move" ? 0 : 0 : 0;
45453
- const { trimA, trimB, arcSeg } = this.computeFilletGeom(radius);
46607
+ const { trimA, arcSeg } = this.computeFilletGeom(radius);
45454
46608
  if (!arcSeg) throw new Error("fillet: cannot fillet these segments (parallel or degenerate)");
45455
46609
  this.trimLastSegEnd(n - 2, trimA[0], trimA[1]);
45456
46610
  const trimmedSeg = { ...curr };
@@ -45522,7 +46676,6 @@ class PathBuilder {
45522
46676
  }
45523
46677
  getSegDirAt(seg, which) {
45524
46678
  if (seg.kind === "line" || seg.kind === "move") {
45525
- this.segs.length;
45526
46679
  const idx = this.segs.indexOf(seg);
45527
46680
  if (seg.kind === "line") {
45528
46681
  let sx, sy;
@@ -45764,6 +46917,41 @@ class PathBuilder {
45764
46917
  }
45765
46918
  return pts;
45766
46919
  }
46920
+ /**
46921
+ * Return the open path as a sampled 2D polyline.
46922
+ *
46923
+ * This is for construction geometry such as guide rails, measured centerlines,
46924
+ * and curve-driven helpers where the authored path should stay open instead of
46925
+ * becoming a filled sketch or stroked profile.
46926
+ *
46927
+ * **Example**
46928
+ *
46929
+ * ```ts
46930
+ * const rail = path()
46931
+ * .moveTo(24, 0)
46932
+ * .bezierTo(32, 44, 28, 92, 18, 120)
46933
+ * .toPolyline();
46934
+ * ```
46935
+ *
46936
+ * @returns A sampled open polyline.
46937
+ * @category Path Builder
46938
+ */
46939
+ toPolyline() {
46940
+ const moveCount = this.segs.filter((seg) => seg.kind === "move").length;
46941
+ if (moveCount > 1) {
46942
+ throw new Error("path().toPolyline() supports one continuous open path. Use separate path() builders for separate rails.");
46943
+ }
46944
+ const pts = [];
46945
+ for (const point2 of this.tessellate()) {
46946
+ if (!point2.every(Number.isFinite)) throw new Error("path().toPolyline() produced a non-finite point");
46947
+ const previous = pts[pts.length - 1];
46948
+ if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) {
46949
+ pts.push(point2);
46950
+ }
46951
+ }
46952
+ if (pts.length < 2) throw new Error("path().toPolyline() needs at least 2 points");
46953
+ return pts;
46954
+ }
45767
46955
  // ── Output ────────────────────────────────────────────────────────────────
45768
46956
  /**
45769
46957
  * Close the path and return a filled `Sketch`.
@@ -46814,7 +48002,8 @@ function explode(items, options = {}) {
46814
48002
  if (child instanceof ShapeGroup) return explodeGroup(child, p2, depth + 1, total, groupCenter, motion.branchDirection);
46815
48003
  return explodeLeaf(child, explodeAdd(total, leafMotion(child, p2, depth + 1, groupCenter, motion.branchDirection).offset));
46816
48004
  }),
46817
- grp.childNames
48005
+ grp.childNames,
48006
+ grp.children.map((_2, i) => grp.tagsForChild(i))
46818
48007
  );
46819
48008
  };
46820
48009
  const explodeItemNode = (item, path2, depth, inherited, parentCenter, parentDirection) => {
@@ -46850,7 +48039,8 @@ function explode(items, options = {}) {
46850
48039
  if (child instanceof ShapeGroup) return explodeGroup(child, p2, 1, [0, 0, 0], rootCenter, void 0);
46851
48040
  return explodeLeaf(child, nodeMotion(child, p2, 1, rootCenter, void 0).offset);
46852
48041
  }),
46853
- items.childNames
48042
+ items.childNames,
48043
+ items.children.map((_2, i) => items.tagsForChild(i))
46854
48044
  );
46855
48045
  }
46856
48046
  return items.map((item, i) => {
@@ -47619,6 +48809,398 @@ function spurGear(options) {
47619
48809
  });
47620
48810
  return attachGearMeta(shapeWithConnectors, meta2);
47621
48811
  }
48812
+ function requirePositive$7(scope, name, value) {
48813
+ if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
48814
+ }
48815
+ function requireOptionalBore(scope, boreDiameter, maxDiameter) {
48816
+ const bore = boreDiameter ?? 0;
48817
+ if (!Number.isFinite(bore) || bore < 0) throw new Error(`${scope}: "boreDiameter" must be >= 0`);
48818
+ if (bore > 0 && bore >= maxDiameter) throw new Error(`${scope}: bore is too large for the body`);
48819
+ return bore;
48820
+ }
48821
+ function resolveSegments(segments) {
48822
+ if (segments === void 0) return void 0;
48823
+ if (!Number.isInteger(segments) || segments < 12) throw new Error('gear body: "segments" must be an integer >= 12');
48824
+ return segments;
48825
+ }
48826
+ function cutBore$1(shape, boreDiameter) {
48827
+ if (boreDiameter <= 0) return shape;
48828
+ const bounds = shape.boundingBox();
48829
+ const height = bounds.max[2] - bounds.min[2] + 2;
48830
+ const cutter = cylinder(height, boreDiameter * 0.5, void 0, 64).translate(0, 0, bounds.min[2] - 1);
48831
+ return shape.subtract(cutter);
48832
+ }
48833
+ function gearBodyDisk(options) {
48834
+ requirePositive$7("gearBodyDisk", "outerRadius", options.outerRadius);
48835
+ requirePositive$7("gearBodyDisk", "faceWidth", options.faceWidth);
48836
+ const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
48837
+ const segments = resolveSegments(options.segments);
48838
+ const outer = circle2d(options.outerRadius, segments);
48839
+ const profile = bore > 0 ? difference2d(outer, circle2d(bore * 0.5, segments)) : outer;
48840
+ return sketchExtrude(profile, options.faceWidth);
48841
+ }
48842
+ function gearBodyDiskWithHub(options) {
48843
+ requirePositive$7("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
48844
+ if (options.hubDiameter >= options.outerRadius * 2) {
48845
+ throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
48846
+ }
48847
+ const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
48848
+ const base = gearBodyDisk({ ...options, boreDiameter: 0 });
48849
+ const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
48850
+ requirePositive$7("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
48851
+ const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
48852
+ 0,
48853
+ 0,
48854
+ (options.faceWidth - hubFaceWidth) * 0.5
48855
+ );
48856
+ return cutBore$1(base.add(hub), bore);
48857
+ }
48858
+ function gearBodySpoked(options) {
48859
+ requirePositive$7("gearBodySpoked", "outerRadius", options.outerRadius);
48860
+ requirePositive$7("gearBodySpoked", "faceWidth", options.faceWidth);
48861
+ requirePositive$7("gearBodySpoked", "rimWidth", options.rimWidth);
48862
+ requirePositive$7("gearBodySpoked", "hubDiameter", options.hubDiameter);
48863
+ requirePositive$7("gearBodySpoked", "spokeWidth", options.spokeWidth);
48864
+ if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
48865
+ throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
48866
+ }
48867
+ const hubRadius = options.hubDiameter * 0.5;
48868
+ const rimInnerRadius = options.outerRadius - options.rimWidth;
48869
+ if (rimInnerRadius <= hubRadius) throw new Error("gearBodySpoked: rim overlaps the hub");
48870
+ const bore = requireOptionalBore("gearBodySpoked", options.boreDiameter, options.hubDiameter);
48871
+ const segments = resolveSegments(options.segments);
48872
+ const rim = difference2d(circle2d(options.outerRadius, segments), circle2d(rimInnerRadius, segments));
48873
+ const hub = circle2d(hubRadius, segments);
48874
+ const spokeLength = rimInnerRadius - hubRadius + options.spokeWidth;
48875
+ const spokeCenter = hubRadius + spokeLength * 0.5 - options.spokeWidth * 0.5;
48876
+ const spoke = sketchTranslate(rect(spokeLength, options.spokeWidth), spokeCenter, 0);
48877
+ const spokes = [];
48878
+ for (let i = 0; i < options.spokeCount; i++) {
48879
+ spokes.push(sketchRotateAround(spoke, 360 / options.spokeCount * i, [0, 0]));
48880
+ }
48881
+ const profile = bore > 0 ? difference2d(union2d(rim, hub, ...spokes), circle2d(bore * 0.5, segments)) : union2d(rim, hub, ...spokes);
48882
+ return sketchExtrude(profile, options.faceWidth);
48883
+ }
48884
+ function gearBodyFromProfile(profile, options) {
48885
+ if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
48886
+ requirePositive$7("gearBodyFromProfile", "faceWidth", options.faceWidth);
48887
+ const bore = options.boreDiameter ?? 0;
48888
+ if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
48889
+ return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
48890
+ }
48891
+ function requirePositive$6(scope, name, value) {
48892
+ if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
48893
+ }
48894
+ function requireFiniteAngle(scope, name, value) {
48895
+ if (value !== void 0 && !Number.isFinite(value)) throw new Error(`${scope}: "${name}" must be finite`);
48896
+ }
48897
+ function cutBore(shape, boreDiameter) {
48898
+ if (boreDiameter <= 0) return shape;
48899
+ const bounds = shape.boundingBox();
48900
+ const height = bounds.max[2] - bounds.min[2] + 2;
48901
+ const cutter = cylinder(height, boreDiameter * 0.5, void 0, 64).translate(0, 0, bounds.min[2] - 1);
48902
+ return shape.subtract(cutter);
48903
+ }
48904
+ function bodyOuterRadius(shape) {
48905
+ const bounds = shape.boundingBox();
48906
+ return Math.max(Math.abs(bounds.min[0]), Math.abs(bounds.max[0]), Math.abs(bounds.min[1]), Math.abs(bounds.max[1]));
48907
+ }
48908
+ function buildSpurTeethRegion(options, name, faceWidth) {
48909
+ const scope = "driveWheel.addSpurTeethBetween";
48910
+ const teethOnFullCircle = options.teethOnFullCircle;
48911
+ if (!Number.isInteger(teethOnFullCircle) || teethOnFullCircle < 6) {
48912
+ throw new Error(`${scope}: "teethOnFullCircle" must be an integer >= 6`);
48913
+ }
48914
+ const toothCount = options.toothCount;
48915
+ if (!Number.isInteger(toothCount) || toothCount < 1 || toothCount > teethOnFullCircle) {
48916
+ throw new Error(`${scope}: "toothCount" must be an integer in [1, teethOnFullCircle]`);
48917
+ }
48918
+ const firstTooth = options.firstTooth ?? 0;
48919
+ if (!Number.isInteger(firstTooth) || firstTooth < 0 || firstTooth >= teethOnFullCircle) {
48920
+ throw new Error(`${scope}: "firstTooth" must be an integer in [0, teethOnFullCircle)`);
48921
+ }
48922
+ let normalized;
48923
+ try {
48924
+ normalized = normalizeSpurGearOptions({ ...options, teeth: teethOnFullCircle, faceWidth, boreDiameter: 0 });
48925
+ } catch (error) {
48926
+ remapErrorPrefix(error, "spurGear", scope);
48927
+ }
48928
+ const gearMeta = buildSpurGearMeta(normalized);
48929
+ const pitchStepDeg = 360 / teethOnFullCircle;
48930
+ const fromAngleDeg = firstTooth * pitchStepDeg - pitchStepDeg * 0.5;
48931
+ const toAngleDeg = (firstTooth + toothCount - 1) * pitchStepDeg + pitchStepDeg * 0.5;
48932
+ const profile = buildSpurToothRegionProfile(gearMeta, firstTooth, toothCount, normalized.segmentsPerTooth);
48933
+ return {
48934
+ shape: sketchExtrude(profile, faceWidth),
48935
+ gearMeta,
48936
+ meta: {
48937
+ name,
48938
+ kind: "spurTeeth",
48939
+ fromAngleDeg,
48940
+ toAngleDeg,
48941
+ outerRadius: gearMeta.outerRadius,
48942
+ rootRadius: gearMeta.rootRadius,
48943
+ pitchRadius: gearMeta.pitchRadius,
48944
+ module: normalized.module,
48945
+ teethOnFullCircle,
48946
+ toothCount,
48947
+ faceWidth
48948
+ }
48949
+ };
48950
+ }
48951
+ function buildSolidArcRegion(options, name, faceWidth) {
48952
+ const scope = "driveWheel.addSolidArcBetween";
48953
+ requirePositive$6(scope, "outerRadius", options.outerRadius);
48954
+ const innerRadius = options.innerRadius ?? 0;
48955
+ if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
48956
+ if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
48957
+ const sweepDeg = normalizedSweep(scope, options.fromAngleDeg, options.toAngleDeg);
48958
+ return {
48959
+ shape: sketchExtrude(buildSolidArcProfile(options, sweepDeg), faceWidth),
48960
+ meta: {
48961
+ name,
48962
+ kind: "solidArc",
48963
+ fromAngleDeg: options.fromAngleDeg,
48964
+ toAngleDeg: options.fromAngleDeg + sweepDeg,
48965
+ innerRadius,
48966
+ outerRadius: options.outerRadius,
48967
+ faceWidth
48968
+ }
48969
+ };
48970
+ }
48971
+ function normalizedSweep(scope, fromAngleDeg, toAngleDeg) {
48972
+ if (!Number.isFinite(fromAngleDeg)) throw new Error(`${scope}: "fromAngleDeg" must be finite`);
48973
+ if (!Number.isFinite(toAngleDeg)) throw new Error(`${scope}: "toAngleDeg" must be finite`);
48974
+ let sweep2 = toAngleDeg - fromAngleDeg;
48975
+ while (sweep2 <= 0) sweep2 += 360;
48976
+ if (sweep2 > 360 + EPSILON$1) throw new Error(`${scope}: angular sweep must be <= 360 degrees`);
48977
+ return Math.min(360, sweep2);
48978
+ }
48979
+ function buildSpurToothRegionProfile(meta2, firstTooth, toothCount, segmentsPerTooth) {
48980
+ const tooth = createSpurToothSketch(meta2, segmentsPerTooth);
48981
+ const teeth = [];
48982
+ for (let i = 0; i < toothCount; i++) {
48983
+ teeth.push(sketchRotateAround(tooth, 360 / meta2.teeth * (firstTooth + i), [0, 0]));
48984
+ }
48985
+ return union2d(...teeth);
48986
+ }
48987
+ function buildSolidArcProfile(options, sweepDeg) {
48988
+ const innerRadius = options.innerRadius ?? 0;
48989
+ const segments = options.segments ?? Math.max(16, Math.ceil(sweepDeg / 6));
48990
+ if (!Number.isInteger(segments) || segments < 4) throw new Error('driveWheel.addSolidArcBetween: "segments" must be an integer >= 4');
48991
+ if (Math.abs(sweepDeg - 360) < EPSILON$1) {
48992
+ const outer = circle2d(options.outerRadius, segments);
48993
+ return innerRadius > 0 ? difference2d(outer, circle2d(innerRadius, segments)) : outer;
48994
+ }
48995
+ const start = options.fromAngleDeg * Math.PI / 180;
48996
+ const end = start + sweepDeg * Math.PI / 180;
48997
+ const pts = [];
48998
+ if (innerRadius <= 0) pts.push([0, 0]);
48999
+ addArcPoints(pts, options.outerRadius, start, end, segments, true, true);
49000
+ if (innerRadius > 0) addArcPoints(pts, innerRadius, end, start, segments, true, true);
49001
+ return polygon(pts);
49002
+ }
49003
+ const DRIVE_WHEEL_META_KEY = Symbol.for("forgecad.library.driveWheelMeta");
49004
+ function attachDriveWheelMeta(shape, meta2) {
49005
+ shape[DRIVE_WHEEL_META_KEY] = meta2;
49006
+ return shape;
49007
+ }
49008
+ function readDriveWheelMeta(shape) {
49009
+ const meta2 = shape[DRIVE_WHEEL_META_KEY];
49010
+ return meta2 ?? null;
49011
+ }
49012
+ class DriveWheelBuilder {
49013
+ constructor(options = {}) {
49014
+ __publicField(this, "body");
49015
+ __publicField(this, "faceWidth");
49016
+ __publicField(this, "boreDiameter");
49017
+ __publicField(this, "regions", []);
49018
+ if (options.body !== void 0 && !(options.body instanceof Shape)) throw new Error('driveWheel: "body" must be a Shape');
49019
+ if (options.faceWidth !== void 0) requirePositive$6("driveWheel", "faceWidth", options.faceWidth);
49020
+ const boreDiameter = options.boreDiameter ?? 0;
49021
+ if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
49022
+ this.body = options.body;
49023
+ this.faceWidth = options.faceWidth;
49024
+ this.boreDiameter = boreDiameter;
49025
+ }
49026
+ /**
49027
+ * Add an involute spur-tooth window on part of the pitch circle.
49028
+ */
49029
+ addSpurTeethBetween(options) {
49030
+ const faceWidth = this.resolveFaceWidth("driveWheel.addSpurTeethBetween", options.faceWidth);
49031
+ this.regions.push(buildSpurTeethRegion(options, this.resolveName("teeth", options.name), faceWidth));
49032
+ return this;
49033
+ }
49034
+ /**
49035
+ * Add a constant-radius solid arc region such as a dwell, stop, or pusher.
49036
+ */
49037
+ addSolidArcBetween(options) {
49038
+ const faceWidth = this.resolveFaceWidth("driveWheel.addSolidArcBetween", options.faceWidth);
49039
+ this.regions.push(buildSolidArcRegion(options, this.resolveName("arc", options.name), faceWidth));
49040
+ return this;
49041
+ }
49042
+ /**
49043
+ * Add a fully custom region shape while preserving region metadata.
49044
+ */
49045
+ addShapeRegion(name, shape, options = {}) {
49046
+ const scope = "driveWheel.addShapeRegion";
49047
+ if (typeof name !== "string" || name.trim().length === 0) throw new Error(`${scope}: "name" must be a non-empty string`);
49048
+ if (!(shape instanceof Shape)) throw new Error(`${scope}: "shape" must be a Shape`);
49049
+ requireFiniteAngle(scope, "fromAngleDeg", options.fromAngleDeg);
49050
+ requireFiniteAngle(scope, "toAngleDeg", options.toAngleDeg);
49051
+ if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
49052
+ throw new Error(`${scope}: "innerRadius" must be >= 0`);
49053
+ }
49054
+ if (options.outerRadius !== void 0) requirePositive$6(scope, "outerRadius", options.outerRadius);
49055
+ this.regions.push({
49056
+ shape: shape.clone(),
49057
+ meta: {
49058
+ name: this.resolveName("region", name),
49059
+ kind: "custom",
49060
+ ...options
49061
+ }
49062
+ });
49063
+ return this;
49064
+ }
49065
+ /**
49066
+ * Build the final wheel shape with a bore connector and region metadata.
49067
+ */
49068
+ build() {
49069
+ var _a3, _b3;
49070
+ if (this.regions.length === 0 && this.body === void 0) {
49071
+ throw new Error("driveWheel: add a body or at least one region before build()");
49072
+ }
49073
+ const faceWidth = this.resolveBuildFaceWidth();
49074
+ const firstGearRegion = (_a3 = this.regions.find((region) => region.gearMeta)) == null ? void 0 : _a3.gearMeta;
49075
+ if (firstGearRegion && this.boreDiameter * 0.5 >= firstGearRegion.rootRadius - EPSILON$1) {
49076
+ throw new Error("driveWheel: bore is too large for the first spur-tooth region");
49077
+ }
49078
+ const body = ((_b3 = this.body) == null ? void 0 : _b3.clone()) ?? gearBodyDisk({ outerRadius: (firstGearRegion == null ? void 0 : firstGearRegion.rootRadius) ?? this.defaultBodyRadius(), faceWidth });
49079
+ let combined = body;
49080
+ for (const region of this.regions) combined = combined.add(region.shape);
49081
+ combined = cutBore(combined, this.boreDiameter);
49082
+ const withConnectors = combined.withConnectors({
49083
+ bore: connectorFactory(
49084
+ "drive-wheel-bore",
49085
+ { origin: [0, 0, faceWidth / 2], axis: [0, 0, 1], kind: "revolute" },
49086
+ this.measurements(faceWidth)
49087
+ )
49088
+ });
49089
+ return attachDriveWheelMeta(withConnectors, {
49090
+ kind: "driveWheel",
49091
+ faceWidth,
49092
+ boreDiameter: this.boreDiameter,
49093
+ regions: this.regionMetadata(body, faceWidth)
49094
+ });
49095
+ }
49096
+ measurements(faceWidth) {
49097
+ var _a3;
49098
+ const firstGearRegion = (_a3 = this.regions.find((region) => region.gearMeta)) == null ? void 0 : _a3.gearMeta;
49099
+ return {
49100
+ faceWidth,
49101
+ boreDiameter: this.boreDiameter,
49102
+ regionCount: this.regions.length,
49103
+ ...firstGearRegion ? {
49104
+ module: firstGearRegion.module,
49105
+ teethOnFullCircle: firstGearRegion.teeth,
49106
+ pitchRadius: firstGearRegion.pitchRadius,
49107
+ outerRadius: firstGearRegion.outerRadius
49108
+ } : {}
49109
+ };
49110
+ }
49111
+ regionMetadata(body, faceWidth) {
49112
+ return [
49113
+ { name: "body", kind: "body", outerRadius: bodyOuterRadius(body), faceWidth },
49114
+ ...this.regions.map((region) => ({ ...region.meta }))
49115
+ ];
49116
+ }
49117
+ resolveFaceWidth(scope, localFaceWidth) {
49118
+ const faceWidth = localFaceWidth ?? this.faceWidth;
49119
+ if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
49120
+ requirePositive$6(scope, "faceWidth", faceWidth);
49121
+ if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
49122
+ throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
49123
+ }
49124
+ return faceWidth;
49125
+ }
49126
+ resolveBuildFaceWidth() {
49127
+ var _a3;
49128
+ const faceWidth = this.faceWidth ?? ((_a3 = this.regions.find((region) => region.meta.faceWidth !== void 0)) == null ? void 0 : _a3.meta.faceWidth);
49129
+ if (faceWidth === void 0) throw new Error('driveWheel: "faceWidth" is required before build()');
49130
+ return faceWidth;
49131
+ }
49132
+ defaultBodyRadius() {
49133
+ const outerRadius = this.regions.reduce((max2, region) => Math.max(max2, region.meta.outerRadius ?? 0), 0);
49134
+ if (outerRadius <= 0) throw new Error('driveWheel: "body" is required when regions do not define an outer radius');
49135
+ return outerRadius;
49136
+ }
49137
+ resolveName(prefix, requested) {
49138
+ const base = (requested == null ? void 0 : requested.trim()) || prefix;
49139
+ if (this.regions.every((region) => region.meta.name !== base)) return base;
49140
+ for (let i = 2; ; i++) {
49141
+ const candidate = `${base}${i}`;
49142
+ if (this.regions.every((region) => region.meta.name !== candidate)) return candidate;
49143
+ }
49144
+ }
49145
+ }
49146
+ function driveWheel(options = {}) {
49147
+ return new DriveWheelBuilder(options);
49148
+ }
49149
+ function normalizeSectorGearOptions(options) {
49150
+ const teethOnFullCircle = options.teethOnFullCircle;
49151
+ if (!Number.isInteger(teethOnFullCircle) || teethOnFullCircle < 6) {
49152
+ throw new Error('sectorGear: "teethOnFullCircle" must be an integer >= 6');
49153
+ }
49154
+ const toothCount = options.toothCount;
49155
+ if (!Number.isInteger(toothCount) || toothCount < 1 || toothCount > teethOnFullCircle) {
49156
+ throw new Error('sectorGear: "toothCount" must be an integer in [1, teethOnFullCircle]');
49157
+ }
49158
+ const firstTooth = options.firstTooth ?? 0;
49159
+ if (!Number.isInteger(firstTooth) || firstTooth < 0 || firstTooth >= teethOnFullCircle) {
49160
+ throw new Error('sectorGear: "firstTooth" must be an integer in [0, teethOnFullCircle)');
49161
+ }
49162
+ return {
49163
+ ...normalizeSpurGearOptions({ ...options, teeth: teethOnFullCircle }),
49164
+ teethOnFullCircle,
49165
+ toothCount,
49166
+ firstTooth,
49167
+ boreDiameter: options.boreDiameter ?? 0
49168
+ };
49169
+ }
49170
+ function sectorGear(options) {
49171
+ const normalized = normalizeSectorGearOptions(options);
49172
+ if (options.body !== void 0 && !(options.body instanceof Shape)) {
49173
+ throw new Error('sectorGear: "body" must be a Shape');
49174
+ }
49175
+ const spurMeta = buildSpurGearMeta(normalized);
49176
+ const pitchStepDeg = 360 / normalized.teethOnFullCircle;
49177
+ const activeAngleStartDeg = normalized.firstTooth * pitchStepDeg - pitchStepDeg * 0.5;
49178
+ const activeAngleEndDeg = (normalized.firstTooth + normalized.toothCount - 1) * pitchStepDeg + pitchStepDeg * 0.5;
49179
+ const meta2 = {
49180
+ ...spurMeta,
49181
+ kind: "sector",
49182
+ teethOnFullCircle: normalized.teethOnFullCircle,
49183
+ firstTooth: normalized.firstTooth,
49184
+ toothCount: normalized.toothCount,
49185
+ activeAngleStartDeg,
49186
+ activeAngleEndDeg
49187
+ };
49188
+ const wheel = driveWheel({ body: options.body, faceWidth: normalized.faceWidth, boreDiameter: normalized.boreDiameter }).addSpurTeethBetween({
49189
+ name: "teeth",
49190
+ module: normalized.module,
49191
+ teethOnFullCircle: normalized.teethOnFullCircle,
49192
+ toothCount: normalized.toothCount,
49193
+ firstTooth: normalized.firstTooth,
49194
+ pressureAngleDeg: normalized.pressureAngleDeg,
49195
+ faceWidth: normalized.faceWidth,
49196
+ backlash: normalized.backlash,
49197
+ clearance: normalized.clearance,
49198
+ addendum: normalized.addendum,
49199
+ dedendum: normalized.dedendum,
49200
+ segmentsPerTooth: normalized.segmentsPerTooth
49201
+ }).build();
49202
+ return attachGearMeta(wheel, meta2);
49203
+ }
47622
49204
  function normalizeSideGearOptions(options) {
47623
49205
  let normalizedSpur;
47624
49206
  try {
@@ -48598,6 +50180,12 @@ function boltPattern(options) {
48598
50180
  }
48599
50181
  };
48600
50182
  }
50183
+ const gearBodies = {
50184
+ disk: gearBodyDisk,
50185
+ diskWithHub: gearBodyDiskWithHub,
50186
+ spoked: gearBodySpoked,
50187
+ fromProfile: gearBodyFromProfile
50188
+ };
48601
50189
  function thread(diameter, pitch, length4, options) {
48602
50190
  const r = diameter / 2;
48603
50191
  const depth = (options == null ? void 0 : options.depth) ?? pitch * 0.35;
@@ -48763,7 +50351,23 @@ const partLibrary = {
48763
50351
  gearRatio,
48764
50352
  rackRatio,
48765
50353
  planetaryRatio,
48766
- boltPattern
50354
+ boltPattern,
50355
+ /** Start a composable exceptional gear or drive wheel. */
50356
+ driveWheel,
50357
+ /** Read functional-region metadata from a drive wheel shape. */
50358
+ readDriveWheelMeta,
50359
+ /** Involute sector gear with teeth on only part of the pitch circle. */
50360
+ sectorGear,
50361
+ /** Gear body preset namespace: disk, diskWithHub, spoked, and fromProfile. */
50362
+ gearBodies,
50363
+ /** Solid disk/ring gear body, independent from any tooth geometry. */
50364
+ gearBodyDisk,
50365
+ /** Disk gear body with a raised center hub. */
50366
+ gearBodyDiskWithHub,
50367
+ /** Spoked gear body with an outer rim, center hub, and radial spokes. */
50368
+ gearBodySpoked,
50369
+ /** Extrude a custom 2D profile into a gear body. */
50370
+ gearBodyFromProfile
48767
50371
  };
48768
50372
  /**
48769
50373
  * @license
@@ -57266,7 +58870,7 @@ function requireFinite$7(value, label) {
57266
58870
  }
57267
58871
  return value;
57268
58872
  }
57269
- function requireVec3$2(value, label) {
58873
+ function requireVec3$3(value, label) {
57270
58874
  if (!Array.isArray(value) || value.length !== 3) {
57271
58875
  throw new Error(`${label} must be [x, y, z]`);
57272
58876
  }
@@ -57310,7 +58914,7 @@ function normalizeOptions(options) {
57310
58914
  out.size = requireFinite$7(options.size, "Viewport.label options.size");
57311
58915
  if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
57312
58916
  }
57313
- if (options.offset !== void 0) out.offset = requireVec3$2(options.offset, "Viewport.label options.offset");
58917
+ if (options.offset !== void 0) out.offset = requireVec3$3(options.offset, "Viewport.label options.offset");
57314
58918
  if (options.anchor !== void 0) {
57315
58919
  if (!VALID_ANCHORS.has(options.anchor)) {
57316
58920
  throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
@@ -57327,7 +58931,7 @@ function collectRenderLabel(text, at, options) {
57327
58931
  if (typeof text !== "string" || text.trim().length === 0) {
57328
58932
  throw new Error("Viewport.label text must be a non-empty string");
57329
58933
  }
57330
- const normalizedAt = requireVec3$2(at, "Viewport.label at");
58934
+ const normalizedAt = requireVec3$3(at, "Viewport.label at");
57331
58935
  const normalizedOptions = normalizeOptions(options);
57332
58936
  _collected$4.push({
57333
58937
  id: `render-label-${_nextId++}`,
@@ -57522,7 +59126,7 @@ function requireFinite$6(value, label) {
57522
59126
  }
57523
59127
  return value;
57524
59128
  }
57525
- function requireVec3$1(value, label) {
59129
+ function requireVec3$2(value, label) {
57526
59130
  if (!Array.isArray(value) || value.length !== 3) {
57527
59131
  throw new Error(`${label} must be [x, y, z]`);
57528
59132
  }
@@ -57550,9 +59154,9 @@ const VALID_ENVIRONMENT_PRESETS = /* @__PURE__ */ new Set([
57550
59154
  ]);
57551
59155
  function validateCamera(cam, label) {
57552
59156
  const out = {};
57553
- if (cam.position !== void 0) out.position = requireVec3$1(cam.position, `${label}.position`);
57554
- if (cam.target !== void 0) out.target = requireVec3$1(cam.target, `${label}.target`);
57555
- if (cam.up !== void 0) out.up = requireVec3$1(cam.up, `${label}.up`);
59157
+ if (cam.position !== void 0) out.position = requireVec3$2(cam.position, `${label}.position`);
59158
+ if (cam.target !== void 0) out.target = requireVec3$2(cam.target, `${label}.target`);
59159
+ if (cam.up !== void 0) out.up = requireVec3$2(cam.up, `${label}.up`);
57556
59160
  if (cam.fov !== void 0) {
57557
59161
  out.fov = requireFinite$6(cam.fov, `${label}.fov`);
57558
59162
  if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
@@ -57687,8 +59291,8 @@ function validateLight(light, label) {
57687
59291
  const out = { type: light.type };
57688
59292
  if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
57689
59293
  if (light.intensity !== void 0) out.intensity = requireFinite$6(light.intensity, `${label}.intensity`);
57690
- if (light.position !== void 0) out.position = requireVec3$1(light.position, `${label}.position`);
57691
- if (light.target !== void 0) out.target = requireVec3$1(light.target, `${label}.target`);
59294
+ if (light.position !== void 0) out.position = requireVec3$2(light.position, `${label}.position`);
59295
+ if (light.target !== void 0) out.target = requireVec3$2(light.target, `${label}.target`);
57692
59296
  if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
57693
59297
  if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
57694
59298
  if (light.angle !== void 0) out.angle = requireFinite$6(light.angle, `${label}.angle`);
@@ -58234,7 +59838,7 @@ class ProductStationBuilder {
58234
59838
  this.profileValue = profileFromSketch(sketch, "custom", width, depth);
58235
59839
  return this;
58236
59840
  }
58237
- /** Stores a semantic crown amount for diagnostics and future rail solving. */
59841
+ /** Set the station crown amount for soft product-section intent. */
58238
59842
  crown(amount) {
58239
59843
  if (!Number.isFinite(amount)) throw new Error("station.crown(amount) requires a finite number");
58240
59844
  this.crownValue = amount;
@@ -59220,7 +60824,7 @@ function scale$1(v, s) {
59220
60824
  function dot$2(a2, b) {
59221
60825
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
59222
60826
  }
59223
- function lerp$2(a2, b, t) {
60827
+ function lerp$4(a2, b, t) {
59224
60828
  return a2 + (b - a2) * t;
59225
60829
  }
59226
60830
  function frameMatrix$1(x2, y2, z2, p2) {
@@ -59231,7 +60835,7 @@ function axisVector(axis, sign2 = 1) {
59231
60835
  if (axis === "Y") return [0, sign2, 0];
59232
60836
  return [0, 0, sign2];
59233
60837
  }
59234
- function axisPosition(axis, point2) {
60838
+ function axisPosition$1(axis, point2) {
59235
60839
  return point2[AXIS_INDEX[axis]];
59236
60840
  }
59237
60841
  function crossPointForStation(axis, point2) {
@@ -59239,7 +60843,7 @@ function crossPointForStation(axis, point2) {
59239
60843
  if (axis === "Y") return [point2[0], -point2[2]];
59240
60844
  return [point2[1], point2[2]];
59241
60845
  }
59242
- function orientLoftToAxis(shape, axis) {
60846
+ function orientLoftToAxis$1(shape, axis) {
59243
60847
  if (axis === "Z") return shape;
59244
60848
  if (axis === "Y") return shape.rotateX(-90);
59245
60849
  return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
@@ -59296,9 +60900,9 @@ function interpolateQuery(a2, b, t) {
59296
60900
  }
59297
60901
  return {
59298
60902
  side: sideA,
59299
- u: lerp$2(a2.u ?? 0.5, b.u ?? 0.5, t),
59300
- v: lerp$2(a2.v ?? 0.5, b.v ?? 0.5, t),
59301
- offset: lerp$2(a2.offset ?? 0, b.offset ?? 0, t)
60903
+ u: lerp$4(a2.u ?? 0.5, b.u ?? 0.5, t),
60904
+ v: lerp$4(a2.v ?? 0.5, b.v ?? 0.5, t),
60905
+ offset: lerp$4(a2.offset ?? 0, b.offset ?? 0, t)
59302
60906
  };
59303
60907
  }
59304
60908
  function resolvePathQueries(points) {
@@ -59365,8 +60969,8 @@ class ProductSkin {
59365
60969
  this.stations = stations;
59366
60970
  this.rails = rails;
59367
60971
  for (const [name2, query] of Object.entries(refs)) this.refQueries.set(name2, cloneQuery(query));
59368
- this.axisMin = Math.min(...stations.map((station) => axisPosition(axis, station.center)));
59369
- this.axisMax = Math.max(...stations.map((station) => axisPosition(axis, station.center)));
60972
+ this.axisMin = Math.min(...stations.map((station) => axisPosition$1(axis, station.center)));
60973
+ this.axisMax = Math.max(...stations.map((station) => axisPosition$1(axis, station.center)));
59370
60974
  this.diagnosticsValue = {
59371
60975
  ...diagnostics,
59372
60976
  stationNames: stations.map((station) => station.name),
@@ -59423,24 +61027,24 @@ class ProductSkin {
59423
61027
  }
59424
61028
  /** Interpolate center, width, and depth at a normalized v or absolute axis value. */
59425
61029
  stationAt(vOrAxis) {
59426
- const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$2(this.axisMin, this.axisMax, vOrAxis) : clamp$6(vOrAxis, this.axisMin, this.axisMax);
61030
+ const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$4(this.axisMin, this.axisMax, vOrAxis) : clamp$6(vOrAxis, this.axisMin, this.axisMax);
59427
61031
  const sorted = this.stations;
59428
61032
  for (let index2 = 0; index2 < sorted.length - 1; index2 += 1) {
59429
61033
  const a2 = sorted[index2];
59430
61034
  const b = sorted[index2 + 1];
59431
- const aAxis = axisPosition(this.axis, a2.center);
59432
- const bAxis = axisPosition(this.axis, b.center);
61035
+ const aAxis = axisPosition$1(this.axis, a2.center);
61036
+ const bAxis = axisPosition$1(this.axis, b.center);
59433
61037
  if (axisValue < aAxis - EPS$5 || axisValue > bAxis + EPS$5) continue;
59434
61038
  const span = Math.max(EPS$5, bAxis - aAxis);
59435
61039
  const t = clamp$6((axisValue - aAxis) / span, 0, 1);
59436
61040
  return {
59437
61041
  axisValue,
59438
- 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)],
59439
- width: lerp$2(a2.profile.width, b.profile.width, t),
59440
- depth: lerp$2(a2.profile.depth, b.profile.depth, t),
61042
+ 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)],
61043
+ width: lerp$4(a2.profile.width, b.profile.width, t),
61044
+ depth: lerp$4(a2.profile.depth, b.profile.depth, t),
59441
61045
  dWidth: (b.profile.width - a2.profile.width) / span,
59442
61046
  dDepth: (b.profile.depth - a2.profile.depth) / span,
59443
- exponent: lerp$2(profileExponent(a2), profileExponent(b), t),
61047
+ exponent: lerp$4(profileExponent(a2), profileExponent(b), t),
59444
61048
  kind: a2.profile.kind === b.profile.kind ? a2.profile.kind : "custom"
59445
61049
  };
59446
61050
  }
@@ -59562,10 +61166,10 @@ class ProductSkinBuilder {
59562
61166
  }
59563
61167
  /** Set named cross-section stations for the product skin. */
59564
61168
  stations(stations) {
59565
- this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition(this.axisValue, a2.center) - axisPosition(this.axisValue, b.center));
61169
+ this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition$1(this.axisValue, a2.center) - axisPosition$1(this.axisValue, b.center));
59566
61170
  return this;
59567
61171
  }
59568
- /** Attach guide rails as ProductSkin IR metadata and diagnostics. */
61172
+ /** Attach named guide rails for product-skin construction and downstream surface references. */
59569
61173
  rails(rails) {
59570
61174
  this.railsValue = { ...rails };
59571
61175
  return this;
@@ -59599,7 +61203,7 @@ class ProductSkinBuilder {
59599
61203
  this.edgeLengthValue = value;
59600
61204
  return this;
59601
61205
  }
59602
- /** Records a target wall thickness; v1 keeps exterior skin lowering sampled and reports wall as a diagnostic. */
61206
+ /** Record intended wall thickness for product design metadata. Use explicit shelling when the model needs real inner-wall geometry. */
59603
61207
  wall(thickness) {
59604
61208
  if (!Number.isFinite(thickness) || thickness <= 0) throw new Error("Product.skin().wall(thickness) requires a positive finite number");
59605
61209
  this.wallValue = thickness;
@@ -59612,9 +61216,9 @@ class ProductSkinBuilder {
59612
61216
  const [x2, y2] = crossPointForStation(this.axisValue, station.center);
59613
61217
  return station.profile.sketch.translate(x2, y2);
59614
61218
  });
59615
- const heights = this.stationsValue.map((station) => axisPosition(this.axisValue, station.center));
61219
+ const heights = this.stationsValue.map((station) => axisPosition$1(this.axisValue, station.center));
59616
61220
  let shape = loft(localProfiles, heights, { edgeLength: this.edgeLengthValue });
59617
- shape = orientLoftToAxis(shape, this.axisValue);
61221
+ shape = orientLoftToAxis$1(shape, this.axisValue);
59618
61222
  if (this.colorValue) shape = shape.color(this.colorValue);
59619
61223
  shape = applyMaterial(shape, this.materialValue).as(this.name);
59620
61224
  const warnings = [];
@@ -60273,7 +61877,7 @@ function requirePositive$3(value, label) {
60273
61877
  function clamp$5(value, min2, max2) {
60274
61878
  return Math.max(min2, Math.min(max2, value));
60275
61879
  }
60276
- function lerp$1(a2, b, t) {
61880
+ function lerp$3(a2, b, t) {
60277
61881
  return a2 + (b - a2) * t;
60278
61882
  }
60279
61883
  function add(a2, b) {
@@ -60323,19 +61927,19 @@ function transformLocal(point2, tangentAcross, normal, tangentAlong, x2, y2, z2
60323
61927
  function interpolateCylinder(a2, b, t, mode) {
60324
61928
  let delta = b.angle - a2.angle;
60325
61929
  if (mode === "shortest" && Math.abs(delta) > 180) delta -= Math.sign(delta) * 360;
60326
- 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) };
61930
+ 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) };
60327
61931
  }
60328
61932
  function interpolatePlane(a2, b, t) {
60329
- 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) };
61933
+ 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) };
60330
61934
  }
60331
61935
  function interpolateProductSkin(a2, b, t) {
60332
61936
  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.");
60333
61937
  return {
60334
61938
  kind: "productSkin",
60335
61939
  side: a2.side ?? b.side,
60336
- u: lerp$1(a2.u ?? 0.5, b.u ?? 0.5, t),
60337
- v: lerp$1(a2.v ?? 0.5, b.v ?? 0.5, t),
60338
- offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t)
61940
+ u: lerp$3(a2.u ?? 0.5, b.u ?? 0.5, t),
61941
+ v: lerp$3(a2.v ?? 0.5, b.v ?? 0.5, t),
61942
+ offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t)
60339
61943
  };
60340
61944
  }
60341
61945
  class SurfacePath {
@@ -60658,11 +62262,11 @@ function coordinateOnSide(coordinate, side, label) {
60658
62262
  return { ...coordinate, kind: "productSkin", side };
60659
62263
  }
60660
62264
  class ProductSkinCarrier {
60661
- constructor(skin, name = skin.name, sideValue, offsetValue = 0) {
62265
+ constructor(skin, name = skin.name, sideValue2, offsetValue = 0) {
60662
62266
  __publicField(this, "kind", "productSkin");
60663
62267
  this.skin = skin;
60664
62268
  this.name = name;
60665
- this.sideValue = sideValue;
62269
+ this.sideValue = sideValue2;
60666
62270
  this.offsetValue = offsetValue;
60667
62271
  }
60668
62272
  surface(side) {
@@ -61433,7 +63037,7 @@ function counterboresForPlate(spec2, width, height, thickness, diagnostics) {
61433
63037
  function minWidthAcrossAlongRange(widthAtT, length4, minAlong, maxAlong) {
61434
63038
  let minWidth = Number.POSITIVE_INFINITY;
61435
63039
  for (let index2 = 0; index2 <= 8; index2 += 1) {
61436
- const along = lerp$1(minAlong, maxAlong, index2 / 8);
63040
+ const along = lerp$3(minAlong, maxAlong, index2 / 8);
61437
63041
  const t = Math.max(0, Math.min(1, (along + length4 / 2) / Math.max(length4, 1e-8)));
61438
63042
  minWidth = Math.min(minWidth, widthAtT(t));
61439
63043
  }
@@ -61733,7 +63337,7 @@ function pathParameterAtDistance(samples, distance2) {
61733
63337
  const segmentLength = Math.hypot(b.point[0] - a2.point[0], b.point[1] - a2.point[1], b.point[2] - a2.point[2]);
61734
63338
  if (traveled + segmentLength >= distance2) {
61735
63339
  const localT = segmentLength <= 1e-8 ? 0 : (distance2 - traveled) / segmentLength;
61736
- return lerp$1(a2.t, b.t, localT);
63340
+ return lerp$3(a2.t, b.t, localT);
61737
63341
  }
61738
63342
  traveled += segmentLength;
61739
63343
  }
@@ -61786,7 +63390,7 @@ function compileBandFootprintMesh(path2, input) {
61786
63390
  const width = input.widthAt(t);
61787
63391
  const along = distance2 - length4 / 2;
61788
63392
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
61789
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
63393
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
61790
63394
  mesh.vertices.push(pointAtProfile([across, along], false));
61791
63395
  }
61792
63396
  }
@@ -61796,7 +63400,7 @@ function compileBandFootprintMesh(path2, input) {
61796
63400
  const width = input.widthAt(t);
61797
63401
  const along = distance2 - length4 / 2;
61798
63402
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
61799
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
63403
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
61800
63404
  mesh.vertices.push(pointAtProfile([across, along], true));
61801
63405
  }
61802
63406
  }
@@ -61808,7 +63412,7 @@ function compileBandFootprintMesh(path2, input) {
61808
63412
  const width = input.widthAt(t);
61809
63413
  const along = distance2 - length4 / 2;
61810
63414
  for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
61811
- const across = lerp$1(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
63415
+ const across = lerp$3(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
61812
63416
  filled[alongIndex][acrossIndex] = !holes.some((hole2) => pointInProfileLoop([across, along], hole2));
61813
63417
  }
61814
63418
  }
@@ -62741,7 +64345,7 @@ class SurfaceMemberBuilder {
62741
64345
  this.record.features.push({ ...normalizeFeature(name, feature), type: "counterbore" });
62742
64346
  return this;
62743
64347
  }
62744
- /** Add a named anchor at a carrier surface coordinate for diagnostics, debug views, and future named-anchor joins. */
64348
+ /** Add a named anchor at a carrier surface coordinate for explicit member joins. */
62745
64349
  anchorAt(name, coordinate) {
62746
64350
  if (!name.trim()) throw new Error("SurfaceMemberBuilder.anchorAt(name, coordinate) requires a non-empty name");
62747
64351
  const explicitAnchors = this.record.spec.explicitAnchors ?? [];
@@ -65704,7 +67308,7 @@ const Constraint = {
65704
67308
  return builder.constrain({ type: "length", line: resolveLineId(builder, line2), value });
65705
67309
  }
65706
67310
  };
65707
- function requireVec3(v, label) {
67311
+ function requireVec3$1(v, label) {
65708
67312
  if (!Array.isArray(v) || v.length !== 3 || !Number.isFinite(v[0]) || !Number.isFinite(v[1]) || !Number.isFinite(v[2])) {
65709
67313
  throw new Error(`${label} must be a [number, number, number] with finite values, got ${JSON.stringify(v)}`);
65710
67314
  }
@@ -65717,24 +67321,24 @@ function requireFiniteNumber(n, label) {
65717
67321
  return n;
65718
67322
  }
65719
67323
  function distance$1(a2, b) {
65720
- requireVec3(a2, "a");
65721
- requireVec3(b, "b");
67324
+ requireVec3$1(a2, "a");
67325
+ requireVec3$1(b, "b");
65722
67326
  return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
65723
67327
  }
65724
67328
  function midpoint$1(a2, b) {
65725
- requireVec3(a2, "a");
65726
- requireVec3(b, "b");
67329
+ requireVec3$1(a2, "a");
67330
+ requireVec3$1(b, "b");
65727
67331
  return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
65728
67332
  }
65729
- function lerp(a2, b, t) {
65730
- requireVec3(a2, "a");
65731
- requireVec3(b, "b");
67333
+ function lerp$2(a2, b, t) {
67334
+ requireVec3$1(a2, "a");
67335
+ requireVec3$1(b, "b");
65732
67336
  requireFiniteNumber(t, "t");
65733
67337
  return [a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t];
65734
67338
  }
65735
67339
  function direction(a2, b) {
65736
- requireVec3(a2, "a");
65737
- requireVec3(b, "b");
67340
+ requireVec3$1(a2, "a");
67341
+ requireVec3$1(b, "b");
65738
67342
  const dx = b[0] - a2[0];
65739
67343
  const dy = b[1] - a2[1];
65740
67344
  const dz = b[2] - a2[2];
@@ -65745,8 +67349,8 @@ function direction(a2, b) {
65745
67349
  return [dx / len2, dy / len2, dz / len2];
65746
67350
  }
65747
67351
  function offset(point2, dir, amount) {
65748
- requireVec3(point2, "point");
65749
- requireVec3(dir, "dir");
67352
+ requireVec3$1(point2, "point");
67353
+ requireVec3$1(dir, "dir");
65750
67354
  requireFiniteNumber(amount, "amount");
65751
67355
  return [point2[0] + dir[0] * amount, point2[1] + dir[1] * amount, point2[2] + dir[2] * amount];
65752
67356
  }
@@ -65756,7 +67360,7 @@ const Points = {
65756
67360
  /** Center point between two 3D points. */
65757
67361
  midpoint: midpoint$1,
65758
67362
  /** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */
65759
- lerp,
67363
+ lerp: lerp$2,
65760
67364
  /** Unit direction vector from a to b. Throws if a and b are the same point. */
65761
67365
  direction,
65762
67366
  /** Move a point along a direction vector by a given amount. */
@@ -70888,9 +72492,84 @@ class ConstraintSketch extends Sketch {
70888
72492
  * Select the single arrangement region that contains the given seed point.
70889
72493
  * Throws if no region contains the seed.
70890
72494
  */
70891
- detectArrangementRegion(seed) {
72495
+ detectArrangementRegion(_seed) {
70892
72496
  throw new Error("Not implemented");
70893
72497
  }
72498
+ /**
72499
+ * Return the solved constrained path as a sampled 2D polyline.
72500
+ *
72501
+ * Use this when a construction rail was authored with `constrainedSketch()`
72502
+ * and should feed another operation such as `Loft.pathOnXz(...)`.
72503
+ * The sketch must contain exactly one profile path.
72504
+ *
72505
+ * @param samples - Samples per curved segment. Default 32.
72506
+ * @returns The solved path as an open polyline.
72507
+ */
72508
+ toPolyline(samples = 32) {
72509
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("ConstraintSketch.toPolyline() samples must be at least 2");
72510
+ const profileLoops = this.definition.loops.filter((loop) => loop.type === "profile");
72511
+ if (profileLoops.length !== 1) {
72512
+ throw new Error("ConstraintSketch.toPolyline() requires exactly one profile path");
72513
+ }
72514
+ const sampleCount = Math.max(2, Math.round(samples));
72515
+ const pointMap = new Map(this.definition.points.map((point2) => [point2.id, point2]));
72516
+ const lineMap = new Map(this.definition.lines.map((line2) => [line2.id, line2]));
72517
+ const arcMap = new Map(this.definition.arcs.map((arc) => [arc.id, arc]));
72518
+ const bezierMap = new Map(this.definition.beziers.map((bezier) => [bezier.id, bezier]));
72519
+ const points = [];
72520
+ const appendStart = (point2, label) => {
72521
+ const previous = points[points.length - 1];
72522
+ if (!previous) {
72523
+ points.push(point2);
72524
+ return;
72525
+ }
72526
+ if (Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-6) {
72527
+ throw new Error(`ConstraintSketch.toPolyline() profile path is not continuous at ${label}`);
72528
+ }
72529
+ };
72530
+ const appendPoint = (point2) => {
72531
+ const previous = points[points.length - 1];
72532
+ if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) points.push(point2);
72533
+ };
72534
+ const requirePoint = (id, label) => {
72535
+ const point2 = pointMap.get(id);
72536
+ if (!point2) throw new Error(`ConstraintSketch.toPolyline() missing ${label}`);
72537
+ return [point2.x, point2.y];
72538
+ };
72539
+ for (const segment of profileLoops[0].segments) {
72540
+ if (segment.kind === "line") {
72541
+ const line2 = lineMap.get(segment.line);
72542
+ if (!line2) throw new Error(`ConstraintSketch.toPolyline() missing line "${segment.line}"`);
72543
+ appendStart(requirePoint(line2.a, `line "${segment.line}" start point`), `line "${segment.line}"`);
72544
+ appendPoint(requirePoint(line2.b, `line "${segment.line}" end point`));
72545
+ } else if (segment.kind === "arc") {
72546
+ const arc = arcMap.get(segment.arc);
72547
+ if (!arc) throw new Error(`ConstraintSketch.toPolyline() missing arc "${segment.arc}"`);
72548
+ const center = requirePoint(arc.center, `arc "${segment.arc}" center point`);
72549
+ const start = requirePoint(arc.start, `arc "${segment.arc}" start point`);
72550
+ const end = requirePoint(arc.end, `arc "${segment.arc}" end point`);
72551
+ appendStart(start, `arc "${segment.arc}"`);
72552
+ const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
72553
+ const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
72554
+ for (const point2 of tessellateArc(center[0], center[1], arc.radius, startAngle, endAngle, arc.clockwise, sampleCount)) {
72555
+ appendPoint(point2);
72556
+ }
72557
+ } else {
72558
+ const bezier = bezierMap.get(segment.bezier);
72559
+ if (!bezier) throw new Error(`ConstraintSketch.toPolyline() missing bezier "${segment.bezier}"`);
72560
+ const p0 = requirePoint(bezier.p0, `bezier "${segment.bezier}" start point`);
72561
+ const p1 = requirePoint(bezier.p1, `bezier "${segment.bezier}" first control point`);
72562
+ const p2 = requirePoint(bezier.p2, `bezier "${segment.bezier}" second control point`);
72563
+ const p3 = requirePoint(bezier.p3, `bezier "${segment.bezier}" end point`);
72564
+ appendStart(p0, `bezier "${segment.bezier}"`);
72565
+ for (const point2 of tessellateBezier(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], p3[0], p3[1], sampleCount)) {
72566
+ appendPoint(point2);
72567
+ }
72568
+ }
72569
+ }
72570
+ if (points.length < 2) throw new Error("ConstraintSketch.toPolyline() needs at least 2 points");
72571
+ return points;
72572
+ }
70894
72573
  /**
70895
72574
  * Re-solve the sketch after changing the value of one existing constraint.
70896
72575
  *
@@ -74017,8 +75696,8 @@ tinf_build_bits_base(dist_bits, dist_base, 2, 1);
74017
75696
  length_bits[28] = 0;
74018
75697
  length_base[28] = 258;
74019
75698
  var tinyInflate = tinf_uncompress;
74020
- function derive(v0, v1, v2, v3, t) {
74021
- 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;
75699
+ function derive(v0, v1, v2, v32, t) {
75700
+ 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;
74022
75701
  }
74023
75702
  function BoundingBox() {
74024
75703
  this.x1 = Number.NaN;
@@ -86175,6 +87854,295 @@ function polygonVertices(sides, radius, options) {
86175
87854
  centerY: options == null ? void 0 : options.centerY
86176
87855
  });
86177
87856
  }
87857
+ const LOFT_GUIDE_EPS = 1e-8;
87858
+ function orientLoftToAxis(shape, axis) {
87859
+ if (axis === "Z") return shape;
87860
+ if (axis === "Y") return shape.rotateX(-90);
87861
+ return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
87862
+ }
87863
+ function buildRailEvaluators(rails, axis, start, end, railSamples) {
87864
+ const seen = /* @__PURE__ */ new Set();
87865
+ return rails.map((rail2) => {
87866
+ if (seen.has(rail2.side)) throw new Error(`Loft.withGuideRails() received more than one ${rail2.side} rail`);
87867
+ seen.add(rail2.side);
87868
+ const sampled = sampleRailPath(rail2.path, railSamples);
87869
+ if (sampled.length < 2) throw new Error("Loft guide rails require at least two points");
87870
+ const points = sampled.map((point2) => ({ position: axisPosition(axis, point2), cross: crossPointForAxis(axis, point2) }));
87871
+ const ordered = points[points.length - 1].position >= points[0].position ? points : [...points].reverse();
87872
+ validateRailCoverage(ordered, start, end);
87873
+ return { side: rail2.side, points: ordered };
87874
+ });
87875
+ }
87876
+ function railCrossAt(rail2, position) {
87877
+ const points = rail2.points;
87878
+ if (position <= points[0].position + LOFT_GUIDE_EPS) return points[0].cross;
87879
+ const last = points[points.length - 1];
87880
+ if (position >= last.position - LOFT_GUIDE_EPS) return last.cross;
87881
+ for (let index2 = 0; index2 < points.length - 1; index2 += 1) {
87882
+ const a2 = points[index2];
87883
+ const b = points[index2 + 1];
87884
+ if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
87885
+ const t = (position - a2.position) / (b.position - a2.position);
87886
+ return [lerp$1(a2.cross[0], b.cross[0], t), lerp$1(a2.cross[1], b.cross[1], t)];
87887
+ }
87888
+ }
87889
+ throw new Error("Loft guide rail does not cover requested station position");
87890
+ }
87891
+ function validateRailCoverage(points, start, end) {
87892
+ for (let index2 = 1; index2 < points.length; index2 += 1) {
87893
+ if (points[index2].position - points[index2 - 1].position < LOFT_GUIDE_EPS) {
87894
+ throw new Error("Loft guide rails must be monotone along the loft axis");
87895
+ }
87896
+ }
87897
+ if (points[0].position - start > LOFT_GUIDE_EPS || end - points[points.length - 1].position > LOFT_GUIDE_EPS) {
87898
+ throw new Error("Loft guide rails must cover the full station range");
87899
+ }
87900
+ }
87901
+ function sampleRailPath(path2, samples) {
87902
+ if (Array.isArray(path2)) return path2.map((point2, index2) => requireVec3(point2, `Loft guide rail point ${index2}`));
87903
+ if (path2 instanceof Curve3D || path2 instanceof HermiteCurve3D || path2 instanceof QuinticHermiteCurve3D || path2 instanceof NurbsCurve3D) {
87904
+ return path2.sample(Math.max(2, Math.round(samples))).map((point2, index2) => requireVec3(point2, `Loft guide rail sample ${index2}`));
87905
+ }
87906
+ throw new Error("Loft guide rail path must be a Vec3[] or ForgeCAD 3D curve");
87907
+ }
87908
+ function requireVec3(point2, label) {
87909
+ if (!Array.isArray(point2) || point2.length !== 3 || !point2.every(Number.isFinite)) {
87910
+ throw new Error(`${label} must be a finite [x, y, z] point`);
87911
+ }
87912
+ return [point2[0], point2[1], point2[2]];
87913
+ }
87914
+ function axisPosition(axis, point2) {
87915
+ if (axis === "X") return point2[0];
87916
+ if (axis === "Y") return point2[1];
87917
+ return point2[2];
87918
+ }
87919
+ function crossPointForAxis(axis, point2) {
87920
+ if (axis === "X") return [point2[1], point2[2]];
87921
+ if (axis === "Y") return [point2[0], -point2[2]];
87922
+ return [point2[0], point2[1]];
87923
+ }
87924
+ function lerp$1(a2, b, t) {
87925
+ return a2 + (b - a2) * t;
87926
+ }
87927
+ function loftWithGuideRails(stations, rails, options = {}) {
87928
+ if (stations.length < 2) throw new Error("Loft.withGuideRails() requires at least two stations");
87929
+ if (rails.length === 0) throw new Error("Loft.withGuideRails() requires at least one guide rail");
87930
+ const sortedStations = sortedValidStations(stations);
87931
+ const axis = options.axis ?? "Z";
87932
+ const start = sortedStations[0].position;
87933
+ const end = sortedStations[sortedStations.length - 1].position;
87934
+ const railEvaluators = buildRailEvaluators(rails, axis, start, end, options.railSamples ?? 64);
87935
+ const positions = generatedPositions(sortedStations, options.samples);
87936
+ const profiles2 = positions.map((position) => {
87937
+ const source = profileForPosition(sortedStations, position);
87938
+ const bounds = boundsForPosition(sortedStations, position);
87939
+ return fitProfileToBounds(source, applyRailsToBounds(bounds, railEvaluators, position));
87940
+ });
87941
+ const shape = loft(profiles2, positions, {
87942
+ edgeLength: options.edgeLength,
87943
+ boundsPadding: options.boundsPadding
87944
+ });
87945
+ return orientLoftToAxis(shape, axis);
87946
+ }
87947
+ function sortedValidStations(stations) {
87948
+ const sorted = [...stations].sort((a2, b) => a2.position - b.position);
87949
+ for (let index2 = 0; index2 < sorted.length; index2 += 1) {
87950
+ if (!Number.isFinite(sorted[index2].position)) throw new Error("Loft.withGuideRails station position must be finite");
87951
+ if (!(sorted[index2].profile instanceof Sketch)) throw new Error("Loft.withGuideRails() stations must use Sketch profiles");
87952
+ if (index2 > 0 && sorted[index2].position - sorted[index2 - 1].position < LOFT_GUIDE_EPS) {
87953
+ throw new Error("Loft.withGuideRails() requires unique, strictly increasing station positions");
87954
+ }
87955
+ }
87956
+ return sorted;
87957
+ }
87958
+ function generatedPositions(stations, samples) {
87959
+ const count = Math.max(2, Math.round(samples ?? Math.max(9, (stations.length - 1) * 8 + 1)));
87960
+ const start = stations[0].position;
87961
+ const end = stations[stations.length - 1].position;
87962
+ const values = /* @__PURE__ */ new Set();
87963
+ const positions = [];
87964
+ const addPosition = (position) => {
87965
+ const key = position.toFixed(9);
87966
+ if (!values.has(key)) {
87967
+ values.add(key);
87968
+ positions.push(position);
87969
+ }
87970
+ };
87971
+ for (let index2 = 0; index2 < count; index2 += 1) addPosition(start + (end - start) * index2 / (count - 1));
87972
+ for (const station of stations) addPosition(station.position);
87973
+ return positions.sort((a2, b) => a2 - b);
87974
+ }
87975
+ function profileForPosition(stations, position) {
87976
+ for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
87977
+ if (position <= stations[index2 + 1].position + LOFT_GUIDE_EPS) return stations[index2].profile;
87978
+ }
87979
+ return stations[stations.length - 1].profile;
87980
+ }
87981
+ function boundsForPosition(stations, position) {
87982
+ if (position <= stations[0].position + LOFT_GUIDE_EPS) return sketchBounds(stations[0].profile);
87983
+ const last = stations[stations.length - 1];
87984
+ if (position >= last.position - LOFT_GUIDE_EPS) return sketchBounds(last.profile);
87985
+ for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
87986
+ const a2 = stations[index2];
87987
+ const b = stations[index2 + 1];
87988
+ if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
87989
+ return lerpBounds(sketchBounds(a2.profile), sketchBounds(b.profile), (position - a2.position) / (b.position - a2.position));
87990
+ }
87991
+ }
87992
+ return sketchBounds(last.profile);
87993
+ }
87994
+ function applyRailsToBounds(bounds, rails, position) {
87995
+ const centerRail = rails.find((rail2) => rail2.side === "center");
87996
+ const center = centerRail ? railCrossAt(centerRail, position) : void 0;
87997
+ const next = { ...bounds };
87998
+ applyAxisRail(next, "X", sideValue(rails, "left", position, 0), sideValue(rails, "right", position, 0), center == null ? void 0 : center[0]);
87999
+ applyAxisRail(next, "Y", sideValue(rails, "back", position, 1), sideValue(rails, "front", position, 1), center == null ? void 0 : center[1]);
88000
+ if (next.maxX - next.minX < LOFT_GUIDE_EPS || next.maxY - next.minY < LOFT_GUIDE_EPS) {
88001
+ throw new Error("Loft.withGuideRails() guide rails produced a non-positive section size");
88002
+ }
88003
+ return next;
88004
+ }
88005
+ function sideValue(rails, side, position, crossIndex) {
88006
+ const rail2 = rails.find((entry) => entry.side === side);
88007
+ return rail2 ? railCrossAt(rail2, position)[crossIndex] : void 0;
88008
+ }
88009
+ function applyAxisRail(bounds, axis, minRail, maxRail, center) {
88010
+ const minKey = axis === "X" ? "minX" : "minY";
88011
+ const maxKey = axis === "X" ? "maxX" : "maxY";
88012
+ const width = bounds[maxKey] - bounds[minKey];
88013
+ if (minRail != null && maxRail != null) {
88014
+ if (maxRail - minRail < LOFT_GUIDE_EPS) throw new Error("Loft.withGuideRails() opposite guide rails crossed");
88015
+ if (center != null && Math.abs((minRail + maxRail) / 2 - center) > 1e-5) {
88016
+ throw new Error("Loft.withGuideRails() center rail conflicts with opposite side rails");
88017
+ }
88018
+ bounds[minKey] = minRail;
88019
+ bounds[maxKey] = maxRail;
88020
+ } else if (maxRail != null) {
88021
+ bounds[maxKey] = maxRail;
88022
+ bounds[minKey] = center != null ? 2 * center - maxRail : maxRail - width;
88023
+ } else if (minRail != null) {
88024
+ bounds[minKey] = minRail;
88025
+ bounds[maxKey] = center != null ? 2 * center - minRail : minRail + width;
88026
+ } else if (center != null) {
88027
+ bounds[minKey] = center - width / 2;
88028
+ bounds[maxKey] = center + width / 2;
88029
+ }
88030
+ }
88031
+ function fitProfileToBounds(profile, target) {
88032
+ const source = sketchBounds(profile);
88033
+ const sourceWidth = source.maxX - source.minX;
88034
+ const sourceDepth = source.maxY - source.minY;
88035
+ if (sourceWidth < LOFT_GUIDE_EPS || sourceDepth < LOFT_GUIDE_EPS) {
88036
+ throw new Error("Loft.withGuideRails() station profiles must have positive bounds");
88037
+ }
88038
+ const sourceCenter = [(source.minX + source.maxX) / 2, (source.minY + source.maxY) / 2];
88039
+ const targetCenter = [(target.minX + target.maxX) / 2, (target.minY + target.maxY) / 2];
88040
+ return profile.scaleAround(sourceCenter, [(target.maxX - target.minX) / sourceWidth, (target.maxY - target.minY) / sourceDepth]).translate(targetCenter[0] - sourceCenter[0], targetCenter[1] - sourceCenter[1]);
88041
+ }
88042
+ function sketchBounds(profile) {
88043
+ const bounds = profile.bounds();
88044
+ return { minX: bounds.min[0], maxX: bounds.max[0], minY: bounds.min[1], maxY: bounds.max[1] };
88045
+ }
88046
+ function lerpBounds(a2, b, t) {
88047
+ return {
88048
+ minX: lerp(a2.minX, b.minX, t),
88049
+ maxX: lerp(a2.maxX, b.maxX, t),
88050
+ minY: lerp(a2.minY, b.minY, t),
88051
+ maxY: lerp(a2.maxY, b.maxY, t)
88052
+ };
88053
+ }
88054
+ function lerp(a2, b, t) {
88055
+ return a2 + (b - a2) * t;
88056
+ }
88057
+ function mapLoftPath2D(path2, label, mapper) {
88058
+ const points = sampleLoftPath2D(path2, label);
88059
+ return points.map((point2, index2) => {
88060
+ if (!Array.isArray(point2) || point2.length !== 2 || !point2.every(Number.isFinite)) {
88061
+ throw new Error(`${label} point ${index2} must be a finite [x, y] point`);
88062
+ }
88063
+ return mapper([point2[0], point2[1]]);
88064
+ });
88065
+ }
88066
+ function sampleLoftPath2D(path2, label) {
88067
+ if (Array.isArray(path2)) {
88068
+ if (path2.length < 2) throw new Error(`${label} requires at least two [x, y] points`);
88069
+ return path2;
88070
+ }
88071
+ if (!path2 || typeof path2 !== "object" || typeof path2.toPolyline !== "function") {
88072
+ throw new Error(`${label} requires a 2D path, solved constrained path, or [x, y] point array`);
88073
+ }
88074
+ const points = path2.toPolyline();
88075
+ if (!Array.isArray(points) || points.length < 2) throw new Error(`${label} path must produce at least two [x, y] points`);
88076
+ return points;
88077
+ }
88078
+ const Loft = {
88079
+ /** Create a loft station from a 2D profile and an axis position. */
88080
+ station(profile, position) {
88081
+ if (!Number.isFinite(position)) throw new Error("Loft.station position must be finite");
88082
+ return { profile, position };
88083
+ },
88084
+ /** Create a guide rail that constrains the section-local negative-X side. */
88085
+ leftRail(path2) {
88086
+ return { side: "left", path: path2 };
88087
+ },
88088
+ /** Create a guide rail that constrains the section-local positive-X side. */
88089
+ rightRail(path2) {
88090
+ return { side: "right", path: path2 };
88091
+ },
88092
+ /** Create a guide rail that constrains the section-local positive-Y side. */
88093
+ frontRail(path2) {
88094
+ return { side: "front", path: path2 };
88095
+ },
88096
+ /** Create a guide rail that constrains the section-local negative-Y side. */
88097
+ backRail(path2) {
88098
+ return { side: "back", path: path2 };
88099
+ },
88100
+ /** Create a guide rail that moves section centers along the loft. */
88101
+ centerRail(path2) {
88102
+ return { side: "center", path: path2 };
88103
+ },
88104
+ /**
88105
+ * Place a 2D guide path onto the XZ plane.
88106
+ *
88107
+ * The path's first coordinate becomes X and its second coordinate becomes Z.
88108
+ * Use this for left/right silhouette rails authored with `path()` or `constrainedSketch()`.
88109
+ */
88110
+ pathOnXz(path2, y2 = 0) {
88111
+ if (!Number.isFinite(y2)) throw new Error("Loft.pathOnXz y must be finite");
88112
+ return mapLoftPath2D(path2, "Loft.pathOnXz", ([x2, z2]) => [x2, y2, z2]);
88113
+ },
88114
+ /**
88115
+ * Place a 2D guide path onto the YZ plane.
88116
+ *
88117
+ * The path's first coordinate becomes Y and its second coordinate becomes Z.
88118
+ * Use this for front/back crown rails authored with `path()` or `constrainedSketch()`.
88119
+ */
88120
+ pathOnYz(path2, x2 = 0) {
88121
+ if (!Number.isFinite(x2)) throw new Error("Loft.pathOnYz x must be finite");
88122
+ return mapLoftPath2D(path2, "Loft.pathOnYz", ([y2, z2]) => [x2, y2, z2]);
88123
+ },
88124
+ /**
88125
+ * Place a 2D guide path onto the XY plane.
88126
+ *
88127
+ * The path's first coordinate becomes X and its second coordinate becomes Y.
88128
+ * Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
88129
+ */
88130
+ pathOnXy(path2, z2 = 0) {
88131
+ if (!Number.isFinite(z2)) throw new Error("Loft.pathOnXy z must be finite");
88132
+ return mapLoftPath2D(path2, "Loft.pathOnXy", ([x2, y2]) => [x2, y2, z2]);
88133
+ },
88134
+ /**
88135
+ * Loft through profile stations while forcing generated sections to follow guide rails.
88136
+ *
88137
+ * Stations define the cross-section family. Guide rails define the side or center
88138
+ * paths the loft must pass through. With opposite side rails, the section is scaled
88139
+ * to touch both rails. With one side rail, the section keeps its interpolated size
88140
+ * unless a center rail is also present.
88141
+ */
88142
+ withGuideRails(stations, rails, options = {}) {
88143
+ return loftWithGuideRails(stations, rails, options);
88144
+ }
88145
+ };
86178
88146
  let collectedHighlights = [];
86179
88147
  function resetHighlights() {
86180
88148
  collectedHighlights = [];
@@ -302829,6 +304797,7 @@ function classifySdfPreviewNode(node) {
302829
304797
  case "sdf:twist":
302830
304798
  case "sdf:bend":
302831
304799
  case "sdf:repeat":
304800
+ case "sdf:circularArray":
302832
304801
  case "sdf:shell":
302833
304802
  case "sdf:onion":
302834
304803
  return classifySdfPreviewNode(node.child);
@@ -302838,10 +304807,7 @@ function classifySdfPreviewNode(node) {
302838
304807
  reason: "This SDF uses a custom JavaScript displacement function that cannot be compiled for raymarch preview."
302839
304808
  };
302840
304809
  case "sdf:surfaceDisplace":
302841
- return {
302842
- mode: "unsupported",
302843
- reason: "This SDF uses surface displacement that is not yet available in the raymarch shader."
302844
- };
304810
+ return classifySurfaceDisplacePreviewNode(node);
302845
304811
  case "sdf:spatialBlend":
302846
304812
  return {
302847
304813
  mode: "unsupported",
@@ -302867,6 +304833,79 @@ ${node.shaderUnsupportedReason}` : ""}`
302867
304833
  };
302868
304834
  }
302869
304835
  }
304836
+ function classifySurfaceDisplacePreviewNode(node) {
304837
+ if (!node.pattern) {
304838
+ return {
304839
+ mode: "unsupported",
304840
+ reason: "This SDF uses a custom JavaScript surface pattern that cannot be compiled for raymarch preview."
304841
+ };
304842
+ }
304843
+ const childResult = classifySdfPreviewNode(node.child);
304844
+ if (childResult.mode !== "raymarch") return childResult;
304845
+ const uv = analyzeShaderSurfaceUv(node.child, "p", node.uvMode);
304846
+ if (uv.mode === "triplanar") {
304847
+ return {
304848
+ mode: "unsupported",
304849
+ reason: "Typed surface displacement raymarch preview currently supports sphere, cylinder, and torus UV mappings."
304850
+ };
304851
+ }
304852
+ return { mode: "raymarch" };
304853
+ }
304854
+ function f2(value) {
304855
+ if (!Number.isFinite(value)) return "0.0";
304856
+ const text = Number(value.toPrecision(9)).toString();
304857
+ return text.includes(".") || text.includes("e") ? text : `${text}.0`;
304858
+ }
304859
+ function v3(value) {
304860
+ return `vec3(${f2(value[0])}, ${f2(value[1])}, ${f2(value[2])})`;
304861
+ }
304862
+ function analyzeShaderSurfaceUv(node, p2, override) {
304863
+ const analysis = analyzeShaderSurfaceUvNode(node, p2);
304864
+ if (!override || override === "auto") return analysis;
304865
+ if (override === "triplanar") return { mode: "triplanar" };
304866
+ if (analysis.mode === "triplanar") return analysis;
304867
+ if (override === analysis.mode) return analysis;
304868
+ if (override === "sphere" || override === "cylinder") {
304869
+ return { mode: override, localPoint: analysis.localPoint, radius: analysis.radius };
304870
+ }
304871
+ return analysis.mode === "torus" ? analysis : { mode: "triplanar" };
304872
+ }
304873
+ function analyzeShaderSurfaceUvNode(node, p2) {
304874
+ switch (node.kind) {
304875
+ case "sdf:sphere":
304876
+ return { mode: "sphere", localPoint: p2, radius: node.radius };
304877
+ case "sdf:cylinder":
304878
+ return { mode: "cylinder", localPoint: p2, radius: node.radius };
304879
+ case "sdf:torus":
304880
+ return { mode: "torus", localPoint: p2, radius: node.minorRadius, majorRadius: node.majorRadius };
304881
+ case "sdf:translate":
304882
+ return analyzeShaderSurfaceUvNode(node.child, `(${p2} - ${v3(node.offset)})`);
304883
+ case "sdf:rotate":
304884
+ return analyzeShaderSurfaceUvNode(node.child, `rotateInvEuler(${p2}, ${v3(node.degrees)})`);
304885
+ case "sdf:scale": {
304886
+ const result = analyzeShaderSurfaceUvNode(node.child, `(${p2} / ${f2(node.factor)})`);
304887
+ if (result.mode === "triplanar") return result;
304888
+ return {
304889
+ ...result,
304890
+ radius: result.radius * node.factor,
304891
+ ...result.mode === "torus" ? { majorRadius: result.majorRadius * node.factor } : {}
304892
+ };
304893
+ }
304894
+ case "sdf:shell":
304895
+ return analyzeShaderSurfaceUvNode(node.child, p2);
304896
+ case "sdf:union":
304897
+ case "sdf:smoothUnion":
304898
+ case "sdf:intersection":
304899
+ case "sdf:smoothIntersection":
304900
+ case "sdf:difference":
304901
+ case "sdf:smoothDifference":
304902
+ return node.children.length > 0 ? analyzeShaderSurfaceUvNode(node.children[0], p2) : { mode: "triplanar" };
304903
+ case "sdf:morph":
304904
+ return analyzeShaderSurfaceUvNode(node.a, p2);
304905
+ default:
304906
+ return { mode: "triplanar" };
304907
+ }
304908
+ }
302870
304909
  function describeScriptResultType(value) {
302871
304910
  var _a3, _b3;
302872
304911
  if (value == null) return String(value);
@@ -302927,7 +304966,7 @@ function mapScriptResultToScene(args) {
302927
304966
  var _a3;
302928
304967
  const objects = [];
302929
304968
  const shapeDimensions = [];
302930
- const pushShape = (shape, name, groupName, color, treePath) => {
304969
+ const pushShape = (shape, name, groupName, color, treePath, tags = []) => {
302931
304970
  const objectId = `obj-${objects.length + 1}`;
302932
304971
  objects.push({
302933
304972
  id: objectId,
@@ -302938,7 +304977,8 @@ function mapScriptResultToScene(args) {
302938
304977
  materialProps: shape.materialProps,
302939
304978
  geometryInfo: shape.geometryInfo(),
302940
304979
  groupName,
302941
- treePath: treePath && treePath.length > 0 ? [...treePath] : [name]
304980
+ treePath: treePath && treePath.length > 0 ? [...treePath] : [name],
304981
+ ...tags.length > 0 ? { tags: [...tags] } : {}
302942
304982
  });
302943
304983
  const dims = getShapeDimensions(shape);
302944
304984
  dims.forEach((dim2) => {
@@ -302962,7 +305002,7 @@ function mapScriptResultToScene(args) {
302962
305002
  });
302963
305003
  }
302964
305004
  };
302965
- const pushSketch = (sketch, name, groupName, treePath) => {
305005
+ const pushSketch = (sketch, name, groupName, treePath, tags = []) => {
302966
305006
  const meta2 = sketch instanceof ConstraintSketch ? sketch.constraintMeta : void 0;
302967
305007
  objects.push({
302968
305008
  id: `obj-${objects.length + 1}`,
@@ -302973,10 +305013,11 @@ function mapScriptResultToScene(args) {
302973
305013
  sketchMeta: meta2,
302974
305014
  color: sketch.colorHex,
302975
305015
  groupName,
302976
- treePath: treePath && treePath.length > 0 ? [...treePath] : [name]
305016
+ treePath: treePath && treePath.length > 0 ? [...treePath] : [name],
305017
+ ...tags.length > 0 ? { tags: [...tags] } : {}
302977
305018
  });
302978
305019
  };
302979
- const pushSdf = (sdfShape, name, groupName, treePath, color) => {
305020
+ const pushSdf = (sdfShape, name, groupName, treePath, color, tags = []) => {
302980
305021
  const preview = classifySdfPreviewNode(sdfShape._node);
302981
305022
  const displayColor = color || sdfShape.colorHex;
302982
305023
  const data = {
@@ -302996,7 +305037,8 @@ function mapScriptResultToScene(args) {
302996
305037
  materialProps: sdfShape.materialProps,
302997
305038
  geometryInfo: null,
302998
305039
  groupName,
302999
- treePath: treePath && treePath.length > 0 ? [...treePath] : [name]
305040
+ treePath: treePath && treePath.length > 0 ? [...treePath] : [name],
305041
+ ...tags.length > 0 ? { tags: [...tags] } : {}
303000
305042
  });
303001
305043
  };
303002
305044
  const isNamedObject = (item) => {
@@ -303013,18 +305055,24 @@ function mapScriptResultToScene(args) {
303013
305055
  const rootGroupChildLabel = (grp, index2) => {
303014
305056
  return shapeGroupChildSegment(grp, index2, true);
303015
305057
  };
303016
- const flattenGroupChild = (child, label, groupName, treePath) => {
305058
+ const flattenGroupChild = (child, label, groupName, treePath, tags = []) => {
303017
305059
  const resolvedTreePath = treePath && treePath.length > 0 ? treePath : [label];
303018
305060
  if (child instanceof ShapeGroup) {
303019
305061
  child.children.forEach((nested, i) => {
303020
- flattenGroupChild(nested, groupChildLabel(child, label, i), groupName, [...resolvedTreePath, shapeGroupChildSegment(child, i)]);
305062
+ flattenGroupChild(
305063
+ nested,
305064
+ groupChildLabel(child, label, i),
305065
+ groupName,
305066
+ [...resolvedTreePath, shapeGroupChildSegment(child, i)],
305067
+ mergeSceneTags(tags, child.tagsForChild(i))
305068
+ );
303021
305069
  });
303022
305070
  return;
303023
305071
  }
303024
305072
  if (child instanceof Shape) {
303025
- pushShape(child, label, groupName, void 0, resolvedTreePath);
305073
+ pushShape(child, label, groupName, void 0, resolvedTreePath, tags);
303026
305074
  } else if (child instanceof Sketch) {
303027
- pushSketch(child, label, groupName, resolvedTreePath);
305075
+ pushSketch(child, label, groupName, resolvedTreePath, tags);
303028
305076
  }
303029
305077
  };
303030
305078
  const isPlainObject2 = (value) => {
@@ -303033,34 +305081,40 @@ function mapScriptResultToScene(args) {
303033
305081
  return proto2 === Object.prototype || proto2 === null;
303034
305082
  };
303035
305083
  const joinName = (path2) => path2.join(".");
303036
- const processRenderableTree = (value, fallbackLabel, fallbackSegment, parentGroup, parentTreePath = [], seen = /* @__PURE__ */ new WeakSet()) => {
305084
+ const processRenderableTree = (value, fallbackLabel, fallbackSegment, parentGroup, parentTreePath = [], inheritedTags = [], seen = /* @__PURE__ */ new WeakSet()) => {
303037
305085
  const segment = fallbackSegment.trim().length > 0 ? fallbackSegment : fallbackLabel;
303038
305086
  const treePath = [...parentTreePath, segment];
303039
305087
  const name = joinName(treePath) || fallbackLabel;
303040
305088
  if (value instanceof Assembly) {
303041
- value.solve().toSceneObjects().forEach((item, index2) => processNamedItem(item, `${name}.${index2 + 1}`, `${index2 + 1}`, name, treePath));
305089
+ value.solve().toSceneObjects().forEach((item, index2) => processNamedItem(item, `${name}.${index2 + 1}`, `${index2 + 1}`, name, treePath, inheritedTags));
303042
305090
  return;
303043
305091
  }
303044
305092
  if (value instanceof SolvedAssembly) {
303045
- value.toSceneObjects().forEach((item, index2) => processNamedItem(item, `${name}.${index2 + 1}`, `${index2 + 1}`, name, treePath));
305093
+ value.toSceneObjects().forEach((item, index2) => processNamedItem(item, `${name}.${index2 + 1}`, `${index2 + 1}`, name, treePath, inheritedTags));
303046
305094
  return;
303047
305095
  }
303048
305096
  if (value instanceof ShapeGroup) {
303049
305097
  value.children.forEach((child, i) => {
303050
- flattenGroupChild(child, groupChildLabel(value, name, i), parentGroup, [...treePath, shapeGroupChildSegment(value, i)]);
305098
+ flattenGroupChild(
305099
+ child,
305100
+ groupChildLabel(value, name, i),
305101
+ parentGroup,
305102
+ [...treePath, shapeGroupChildSegment(value, i)],
305103
+ mergeSceneTags(inheritedTags, value.tagsForChild(i))
305104
+ );
303051
305105
  });
303052
305106
  return;
303053
305107
  }
303054
305108
  if (value instanceof Shape) {
303055
- pushShape(value, name, parentGroup, void 0, treePath);
305109
+ pushShape(value, name, parentGroup, void 0, treePath, inheritedTags);
303056
305110
  return;
303057
305111
  }
303058
305112
  if (value instanceof Sketch) {
303059
- pushSketch(value, name, parentGroup, treePath);
305113
+ pushSketch(value, name, parentGroup, treePath, inheritedTags);
303060
305114
  return;
303061
305115
  }
303062
305116
  if (value instanceof SdfShape) {
303063
- pushSdf(value, name, parentGroup, treePath);
305117
+ pushSdf(value, name, parentGroup, treePath, void 0, inheritedTags);
303064
305118
  return;
303065
305119
  }
303066
305120
  if (value instanceof GCodeBuilder) {
@@ -303071,7 +305125,8 @@ function mapScriptResultToScene(args) {
303071
305125
  sketch: null,
303072
305126
  toolpath: value.build(),
303073
305127
  geometryInfo: null,
303074
- treePath
305128
+ treePath,
305129
+ ...inheritedTags.length > 0 ? { tags: [...inheritedTags] } : {}
303075
305130
  });
303076
305131
  return;
303077
305132
  }
@@ -303081,30 +305136,38 @@ function mapScriptResultToScene(args) {
303081
305136
  value.forEach((item, index2) => {
303082
305137
  const childSegment = `${index2 + 1}`;
303083
305138
  const childLabel = `${name}.${childSegment}`;
303084
- processRenderableTree(item, childLabel, childSegment, parentGroup, treePath, seen);
305139
+ processRenderableTree(item, childLabel, childSegment, parentGroup, treePath, inheritedTags, seen);
303085
305140
  });
303086
305141
  return;
303087
305142
  }
303088
305143
  if (isNamedObject(value)) {
303089
- processNamedItem(value, fallbackLabel, fallbackSegment, parentGroup, parentTreePath);
305144
+ processNamedItem(value, fallbackLabel, fallbackSegment, parentGroup, parentTreePath, inheritedTags);
303090
305145
  return;
303091
305146
  }
303092
305147
  if (isPlainObject2(value)) {
303093
305148
  if (seen.has(value)) return;
303094
305149
  seen.add(value);
303095
305150
  Object.entries(value).forEach(([key, entry]) => {
303096
- processRenderableTree(entry, key, key, parentGroup, treePath, seen);
305151
+ processRenderableTree(entry, key, key, parentGroup, treePath, inheritedTags, seen);
303097
305152
  });
303098
305153
  }
303099
305154
  };
303100
- const processNamedItem = (item, fallbackLabel, fallbackSegment, parentGroup, parentTreePath = []) => {
305155
+ const processNamedItem = (item, fallbackLabel, fallbackSegment, parentGroup, parentTreePath = [], inheritedTags = []) => {
305156
+ var _a4;
303101
305157
  const name = typeof item.name === "string" && item.name.trim().length > 0 ? item.name : fallbackLabel;
303102
305158
  const localSegment = typeof item.name === "string" && item.name.trim().length > 0 ? item.name : fallbackSegment;
303103
305159
  const treePath = [...parentTreePath, localSegment];
303104
305160
  const grp = parentGroup;
305161
+ const tags = mergeSceneTags(inheritedTags, (_a4 = item.metadata) == null ? void 0 : _a4.tags, item.tags);
303105
305162
  if (item.group instanceof ShapeGroup) {
303106
305163
  item.group.children.forEach((child, i) => {
303107
- flattenGroupChild(child, groupChildLabel(item.group, name, i), name, [...treePath, shapeGroupChildSegment(item.group, i)]);
305164
+ flattenGroupChild(
305165
+ child,
305166
+ groupChildLabel(item.group, name, i),
305167
+ name,
305168
+ [...treePath, shapeGroupChildSegment(item.group, i)],
305169
+ mergeSceneTags(tags, item.group.tagsForChild(i))
305170
+ );
303108
305171
  });
303109
305172
  return;
303110
305173
  }
@@ -303114,39 +305177,48 @@ function mapScriptResultToScene(args) {
303114
305177
  const childTreePath = [...treePath, `${i + 1}`];
303115
305178
  if (child instanceof ShapeGroup) {
303116
305179
  child.children.forEach((nested, nestedIndex) => {
303117
- flattenGroupChild(nested, groupChildLabel(child, name, nestedIndex), name, [
303118
- ...treePath,
303119
- shapeGroupChildSegment(child, nestedIndex)
303120
- ]);
305180
+ flattenGroupChild(
305181
+ nested,
305182
+ groupChildLabel(child, name, nestedIndex),
305183
+ name,
305184
+ [...treePath, shapeGroupChildSegment(child, nestedIndex)],
305185
+ mergeSceneTags(tags, child.tagsForChild(nestedIndex))
305186
+ );
303121
305187
  });
303122
305188
  } else if (child instanceof Shape) {
303123
- pushShape(child, childLabel, name, void 0, childTreePath);
305189
+ pushShape(child, childLabel, name, void 0, childTreePath, tags);
303124
305190
  } else if (child instanceof Sketch) {
303125
- pushSketch(child, childLabel, name, childTreePath);
305191
+ pushSketch(child, childLabel, name, childTreePath, tags);
303126
305192
  } else if (child instanceof SdfShape) {
303127
- pushSdf(child, childLabel, name, childTreePath);
305193
+ pushSdf(child, childLabel, name, childTreePath, void 0, tags);
303128
305194
  } else if (isNamedObject(child)) {
303129
- processNamedItem(child, childLabel, `${i + 1}`, name, treePath);
305195
+ processNamedItem(child, childLabel, `${i + 1}`, name, treePath, tags);
303130
305196
  }
303131
305197
  });
303132
305198
  return;
303133
305199
  }
303134
305200
  if (item.shape instanceof ShapeGroup) {
303135
305201
  item.shape.children.forEach(
303136
- (child, i) => flattenGroupChild(child, groupChildLabel(item.shape, name, i), name, [...treePath, shapeGroupChildSegment(item.shape, i)])
305202
+ (child, i) => flattenGroupChild(
305203
+ child,
305204
+ groupChildLabel(item.shape, name, i),
305205
+ name,
305206
+ [...treePath, shapeGroupChildSegment(item.shape, i)],
305207
+ mergeSceneTags(tags, item.shape.tagsForChild(i))
305208
+ )
303137
305209
  );
303138
305210
  return;
303139
305211
  }
303140
305212
  if (item.shape instanceof Shape) {
303141
- pushShape(item.shape, name, grp, item.color, treePath);
305213
+ pushShape(item.shape, name, grp, item.color, treePath, tags);
303142
305214
  return;
303143
305215
  }
303144
305216
  if (item.shape instanceof SdfShape) {
303145
- pushSdf(item.shape, name, grp, treePath, item.color);
305217
+ pushSdf(item.shape, name, grp, treePath, item.color, tags);
303146
305218
  return;
303147
305219
  }
303148
305220
  if (item.sdf instanceof SdfShape) {
303149
- pushSdf(item.sdf, name, grp, treePath, item.color);
305221
+ pushSdf(item.sdf, name, grp, treePath, item.color, tags);
303150
305222
  return;
303151
305223
  }
303152
305224
  if (item.sketch instanceof Sketch) {
@@ -303160,7 +305232,8 @@ function mapScriptResultToScene(args) {
303160
305232
  sketchMeta: meta2,
303161
305233
  color: item.color || item.sketch.colorHex,
303162
305234
  groupName: grp,
303163
- treePath
305235
+ treePath,
305236
+ ...tags.length > 0 ? { tags: [...tags] } : {}
303164
305237
  });
303165
305238
  }
303166
305239
  };
@@ -303174,14 +305247,20 @@ function mapScriptResultToScene(args) {
303174
305247
  } else if (result instanceof ShapeGroup) {
303175
305248
  result.children.forEach((child, i) => {
303176
305249
  const label = rootGroupChildLabel(result, i);
303177
- flattenGroupChild(child, label, void 0, [label]);
305250
+ flattenGroupChild(child, label, void 0, [label], result.tagsForChild(i));
303178
305251
  });
303179
305252
  } else if (Array.isArray(result)) {
303180
305253
  result.forEach((item, index2) => {
303181
305254
  const label = `Object ${index2 + 1}`;
303182
305255
  if (item instanceof ShapeGroup) {
303183
305256
  item.children.forEach((child, i) => {
303184
- flattenGroupChild(child, groupChildLabel(item, label, i), void 0, [label, shapeGroupChildSegment(item, i)]);
305257
+ flattenGroupChild(
305258
+ child,
305259
+ groupChildLabel(item, label, i),
305260
+ void 0,
305261
+ [label, shapeGroupChildSegment(item, i)],
305262
+ item.tagsForChild(i)
305263
+ );
303185
305264
  });
303186
305265
  return;
303187
305266
  }
@@ -303214,7 +305293,7 @@ function mapScriptResultToScene(args) {
303214
305293
  } else if (defaultValue instanceof ShapeGroup) {
303215
305294
  defaultValue.children.forEach((child, i) => {
303216
305295
  const label = rootGroupChildLabel(defaultValue, i);
303217
- flattenGroupChild(child, label, void 0, [label]);
305296
+ flattenGroupChild(child, label, void 0, [label], defaultValue.tagsForChild(i));
303218
305297
  });
303219
305298
  } else if (defaultValue instanceof Shape) {
303220
305299
  pushShape(defaultValue, args.fileName, void 0, void 0, [args.fileName]);
@@ -303260,7 +305339,8 @@ function mapScriptResultToScene(args) {
303260
305339
  name: `${mock2.name} (mock)`,
303261
305340
  shape: mock2.shape,
303262
305341
  sketch: null,
303263
- mock: true
305342
+ mock: true,
305343
+ tags: ["mock"]
303264
305344
  });
303265
305345
  }
303266
305346
  const hasSdfLeaves = objects.some((obj) => obj.sdf);
@@ -303531,6 +305611,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
303531
305611
  nurbsSurface,
303532
305612
  spline2d,
303533
305613
  spline3d,
305614
+ Loft,
303534
305615
  loft,
303535
305616
  loftAlongSpine,
303536
305617
  sweep,