forgecad 0.9.5 → 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 (39) hide show
  1. package/dist/assets/{AdminPage-uTtcSXtn.js → AdminPage-Da6hhpJx.js} +1 -1
  2. package/dist/assets/{BlogPage-DYJMjWx3.js → BlogPage-Bl_sKeWb.js} +1 -1
  3. package/dist/assets/{DocsPage-C58f0K5v.js → DocsPage-Blz3Tp4j.js} +1 -1
  4. package/dist/assets/{EditorApp-DNH1TEz1.js → EditorApp-CuiPbtn5.js} +32 -7
  5. package/dist/assets/{EmbedViewer-CMXWA2LX.js → EmbedViewer-BFG6-Ufm.js} +2 -2
  6. package/dist/assets/{LandingPageProofDriven-CAu2OZFn.js → LandingPageProofDriven-DB9fQd5P.js} +1 -1
  7. package/dist/assets/{PricingPage-BIgW7m3X.js → PricingPage-BMxYT_F0.js} +1 -1
  8. package/dist/assets/{SettingsPage-N1l1tMXO.js → SettingsPage-VVQNrCAg.js} +1 -1
  9. package/dist/assets/{app-CFy7g5WP.js → app-Dl9ymBWC.js} +293 -36
  10. package/dist/assets/cli/{render-BrVVdj_T.js → render-CFtwKCCY.js} +10 -1081
  11. package/dist/assets/{sectionPlaneMath-CykEnkvQ.js → distance-BEC2RjJi.js} +1897 -288
  12. package/dist/assets/{evalWorker-c_SB9gg3.js → evalWorker-CRvbzTXm.js} +555 -83
  13. package/dist/assets/{manifold-Cjk7WhRs.js → manifold-B9QSr-qP.js} +1 -1
  14. package/dist/assets/{manifold-Dp6pvFr6.js → manifold-DpBXFS2K.js} +1 -1
  15. package/dist/assets/{manifold-CRoBhJKH.js → manifold-DzZ4VRPs.js} +2 -2
  16. package/dist/assets/{renderSceneState-3DfsSASX.js → renderSceneState-BuAXF2jh.js} +1 -1
  17. package/dist/assets/{reportWorker-BLkuIoS8.js → reportWorker-BNWEnRg1.js} +555 -83
  18. package/dist/cli/render.html +1 -1
  19. package/dist/docs/index.html +1 -1
  20. package/dist/docs-raw/beta-operations.md +4 -0
  21. package/dist/docs-raw/deployment.md +38 -23
  22. package/dist/docs-raw/generated/concepts.md +82 -5
  23. package/dist/docs-raw/generated/curves.md +97 -5
  24. package/dist/docs-raw/generated/sketch.md +9 -1
  25. package/dist/docs-raw/guides/inspection-bundles.md +9 -3
  26. package/dist/docs-raw/runbook.md +3 -3
  27. package/dist/index.html +1 -1
  28. package/dist/sitemap.xml +6 -6
  29. package/dist-cli/forgecad.js +828 -297
  30. package/dist-cli/forgecad.js.map +1 -1
  31. package/dist-skill/CONTEXT.md +115 -9
  32. package/dist-skill/docs/generated/curves.md +97 -5
  33. package/dist-skill/docs/generated/sketch.md +9 -1
  34. package/dist-skill/docs/guides/inspection-bundles.md +9 -3
  35. package/dist-skill/docs-dev/generated/curves.md +97 -5
  36. package/dist-skill/docs-dev/generated/sketch.md +9 -1
  37. package/dist-skill/docs-dev/guides/inspection-bundles.md +9 -3
  38. package/examples/api/guided-loft-olive-oil-bottle.forge.js +135 -0
  39. package/package.json +20 -2
@@ -721,7 +721,7 @@ function cloneSdfNode(node) {
721
721
  }
722
722
  }
723
723
  const SHEET_METAL_EDGES = ["top", "right", "bottom", "left"];
724
- const EPS$c = 1e-9;
724
+ const EPS$d = 1e-9;
725
725
  function isFinitePositive$3(value) {
726
726
  return Number.isFinite(value) && value > 0;
727
727
  }
@@ -762,7 +762,7 @@ function edgeDisplayName(edge) {
762
762
  return `sheetMetal().flange("${edge}", ...)`;
763
763
  }
764
764
  function normalizeAngle(angleDeg) {
765
- return Math.abs(angleDeg) <= EPS$c ? 0 : angleDeg;
765
+ return Math.abs(angleDeg) <= EPS$d ? 0 : angleDeg;
766
766
  }
767
767
  function validateSheetMetalModel(model) {
768
768
  if (!isFinitePositive$3(model.panel.width) || !isFinitePositive$3(model.panel.height)) {
@@ -774,7 +774,7 @@ function validateSheetMetalModel(model) {
774
774
  if (!isFiniteNonNegative(model.bendRadius)) {
775
775
  return "sheetMetal() requires a finite non-negative bendRadius.";
776
776
  }
777
- if (model.bendRadius <= EPS$c) {
777
+ if (model.bendRadius <= EPS$d) {
778
778
  return "sheetMetal() v1 requires a positive bendRadius so the bend region stays explicit instead of collapsing into a sharp fold.";
779
779
  }
780
780
  if (model.bendAllowance.kind !== "k-factor") {
@@ -836,7 +836,7 @@ function deriveSheetMetalModel(model) {
836
836
  const trimEnd = flanges.has(adjacent.end) ? model.cornerRelief.size : 0;
837
837
  const fullLength = edge === "top" || edge === "bottom" ? model.panel.width : model.panel.height;
838
838
  const span = fullLength - trimStart - trimEnd;
839
- if (!(span > EPS$c)) {
839
+ if (!(span > EPS$d)) {
840
840
  throw new Error(
841
841
  `${edgeDisplayName(edge)} loses all usable span after applying the defended rectangular corner relief size ${model.cornerRelief.size}.`
842
842
  );
@@ -894,7 +894,7 @@ function transformPlacement(origin, u2, v, normal) {
894
894
  };
895
895
  }
896
896
  function translatePlan(plan, x2, y2, z2) {
897
- 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);
898
898
  return appendShapeCompileTransform(cloneShapeCompilePlan(plan), {
899
899
  kind: "translate",
900
900
  x: x2,
@@ -1338,7 +1338,7 @@ function cloneShapeWorkplanePlacement(placement) {
1338
1338
  placement: cloneSketchPlacementModel(placement.placement)
1339
1339
  };
1340
1340
  }
1341
- const EPS$b = 1e-10;
1341
+ const EPS$c = 1e-10;
1342
1342
  function subVec3(a2, b) {
1343
1343
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
1344
1344
  }
@@ -1364,7 +1364,7 @@ function projectRadial(v, axis) {
1364
1364
  function signedAngleAroundAxis(from, to, axis) {
1365
1365
  const fromLen = lengthVec3$1(from);
1366
1366
  const toLen = lengthVec3$1(to);
1367
- if (fromLen < EPS$b || toLen < EPS$b) return 0;
1367
+ if (fromLen < EPS$c || toLen < EPS$c) return 0;
1368
1368
  const fn = scaleVec3(from, 1 / fromLen);
1369
1369
  const tn = scaleVec3(to, 1 / toLen);
1370
1370
  const sin2 = dotVec3$4(axis, crossVec3$2(fn, tn));
@@ -1385,19 +1385,19 @@ function solveRotateAroundAngle(axis, pivot, movingPoint, targetPoint, options =
1385
1385
  const targetDecomp = projectRadial(target, unitAxis);
1386
1386
  const movingRadialLen = lengthVec3$1(movingDecomp.radial);
1387
1387
  const targetRadialLen = lengthVec3$1(targetDecomp.radial);
1388
- if (movingRadialLen < EPS$b) {
1389
- if (mode === "line" && targetRadialLen >= EPS$b) {
1388
+ if (movingRadialLen < EPS$c) {
1389
+ if (mode === "line" && targetRadialLen >= EPS$c) {
1390
1390
  throw new Error("rotateAroundTo(...): moving point lies on the rotation axis, so line alignment is impossible");
1391
1391
  }
1392
1392
  return 0;
1393
1393
  }
1394
1394
  if (mode === "plane") {
1395
- if (targetRadialLen < EPS$b) {
1395
+ if (targetRadialLen < EPS$c) {
1396
1396
  throw new Error("rotateAroundTo(...): target point lies on the rotation axis, so the target plane is undefined");
1397
1397
  }
1398
1398
  return signedAngleAroundAxis(movingDecomp.radial, targetDecomp.radial, unitAxis);
1399
1399
  }
1400
- if (targetRadialLen < EPS$b) {
1400
+ if (targetRadialLen < EPS$c) {
1401
1401
  throw new Error("rotateAroundTo(...): target line lies on the rotation axis, but the moving point does not");
1402
1402
  }
1403
1403
  const axialTol = 1e-8 * Math.max(1, Math.abs(movingDecomp.axial), Math.abs(targetDecomp.axial));
@@ -1437,7 +1437,7 @@ function multiplyMat4(a2, b) {
1437
1437
  }
1438
1438
  function normalizeVec3$5(v) {
1439
1439
  const len2 = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
1440
- 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");
1441
1441
  return [v[0] / len2, v[1] / len2, v[2] / len2];
1442
1442
  }
1443
1443
  function transformPoint$1(m2, p2, w2) {
@@ -1467,7 +1467,7 @@ function invertMat4(m2) {
1467
1467
  const b10 = a21 * a33 - a23 * a31;
1468
1468
  const b11 = a22 * a33 - a23 * a32;
1469
1469
  const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
1470
- 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");
1471
1471
  const invDet = 1 / det;
1472
1472
  out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet;
1473
1473
  out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * invDet;
@@ -3224,14 +3224,14 @@ function sweepPathToPolylineAdaptive(path2, baseSamples = 48) {
3224
3224
  pts.push(evalPathAt(path2, 1));
3225
3225
  return pts;
3226
3226
  }
3227
- const EPS$a = 1e-8;
3227
+ const EPS$b = 1e-8;
3228
3228
  const SUPPORTED_VERTICAL_EDGE_NAMES = ["vert-bl", "vert-br", "vert-tr", "vert-tl"];
3229
3229
  function midpoint$4(start, end) {
3230
3230
  return [(start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, (start[2] + end[2]) * 0.5];
3231
3231
  }
3232
3232
  function normalize$8(v) {
3233
3233
  const len2 = Math.hypot(v[0], v[1], v[2]);
3234
- 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");
3235
3235
  return [v[0] / len2, v[1] / len2, v[2] / len2];
3236
3236
  }
3237
3237
  function subtract(a2, b) {
@@ -3313,7 +3313,7 @@ function rigidTransformForEdgeStep(step) {
3313
3313
  case "mirror": {
3314
3314
  const [nx0, ny0, nz0] = [step.normalX, step.normalY, step.normalZ];
3315
3315
  const len2 = Math.hypot(nx0, ny0, nz0);
3316
- if (len2 <= EPS$a) return Transform.identity();
3316
+ if (len2 <= EPS$b) return Transform.identity();
3317
3317
  const nx = nx0 / len2;
3318
3318
  const ny = ny0 / len2;
3319
3319
  const nz = nz0 / len2;
@@ -3624,7 +3624,7 @@ function isRectangleProfile(points) {
3624
3624
  return [next[0] - point2[0], next[1] - point2[1]];
3625
3625
  });
3626
3626
  const lengths2 = vectors.map(([x2, y2]) => Math.hypot(x2, y2));
3627
- if (lengths2.some((length4) => length4 <= EPS$a)) return false;
3627
+ if (lengths2.some((length4) => length4 <= EPS$b)) return false;
3628
3628
  const dot01 = vectors[0][0] * vectors[1][0] + vectors[0][1] * vectors[1][1];
3629
3629
  const dot12 = vectors[1][0] * vectors[2][0] + vectors[1][1] * vectors[2][1];
3630
3630
  const dot23 = vectors[2][0] * vectors[3][0] + vectors[2][1] * vectors[3][1];
@@ -5195,13 +5195,13 @@ function parseMeshFile(data, format) {
5195
5195
  return parse3mf(data);
5196
5196
  }
5197
5197
  }
5198
- const EPS$9 = 1e-8;
5198
+ const EPS$a = 1e-8;
5199
5199
  function length$3(v) {
5200
5200
  return Math.hypot(v[0], v[1], v[2]);
5201
5201
  }
5202
5202
  function normalize$7(v) {
5203
5203
  const len2 = length$3(v);
5204
- 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");
5205
5205
  return [v[0] / len2, v[1] / len2, v[2] / len2];
5206
5206
  }
5207
5207
  function resolvePlaneOriginNormal(plane) {
@@ -5223,12 +5223,12 @@ function resolvePlaneOriginNormal(plane) {
5223
5223
  function rotationToPlaneSpace(normal) {
5224
5224
  const n = normalize$7(normal);
5225
5225
  const dot2 = n[2];
5226
- if (dot2 > 1 - EPS$9) {
5226
+ if (dot2 > 1 - EPS$a) {
5227
5227
  return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
5228
5228
  }
5229
5229
  let axis;
5230
5230
  let angle;
5231
- if (dot2 < -1 + EPS$9) {
5231
+ if (dot2 < -1 + EPS$a) {
5232
5232
  axis = [1, 0, 0];
5233
5233
  angle = Math.PI;
5234
5234
  } else {
@@ -7790,7 +7790,7 @@ function scale$6(v, s) {
7790
7790
  function sub$7(a2, b) {
7791
7791
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
7792
7792
  }
7793
- function cross$7(a2, b) {
7793
+ function cross$8(a2, b) {
7794
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]];
7795
7795
  }
7796
7796
  function makeEdge(name, start, end, faceName, curve) {
@@ -7826,7 +7826,7 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
7826
7826
  const center = options.center ?? average$1(corners);
7827
7827
  const uAxis = normalizeAxis$1(sub$7(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
7828
7828
  const vAxis = normalizeAxis$1(sub$7(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
7829
- const normal = normalizeAxis$1(options.normal ?? cross$7(uAxis, vAxis));
7829
+ const normal = normalizeAxis$1(options.normal ?? cross$8(uAxis, vAxis));
7830
7830
  const faces = /* @__PURE__ */ new Map();
7831
7831
  faces.set(faceName, {
7832
7832
  name: faceName,
@@ -9728,6 +9728,7 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
9728
9728
  edgeLength: options.edgeLength
9729
9729
  };
9730
9730
  }
9731
+ const EPS$9 = 1e-9;
9731
9732
  function resamplePolygon(poly, targetCount) {
9732
9733
  if (poly.length < 2) return poly;
9733
9734
  if (targetCount <= 0) return [];
@@ -9765,6 +9766,78 @@ function resamplePolygon(poly, targetCount) {
9765
9766
  }
9766
9767
  return out;
9767
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
+ }
9768
9841
  function loftStitched(profiles2, heights, wasm) {
9769
9842
  if (profiles2.length < 2) return null;
9770
9843
  const classified = profiles2.map((loops) => classifyLoops(loops));
@@ -9893,8 +9966,10 @@ function stitchSingleLoopLoft(loops, heights, wasm) {
9893
9966
  maxPoints = Math.max(maxPoints, loop.length);
9894
9967
  }
9895
9968
  const N = Math.max(maxPoints, 24);
9969
+ const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
9970
+ const useAngularSamples = angularSamples.every((samples) => samples != null);
9896
9971
  const resampled = normalizedLoops.map((loop, i) => {
9897
- const pts2d = resamplePolygon(loop, N);
9972
+ const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
9898
9973
  const z2 = heights[i];
9899
9974
  return pts2d.map(([x2, y2]) => [x2, y2, z2]);
9900
9975
  });
@@ -9955,7 +10030,7 @@ let _wasm$1 = null;
9955
10030
  async function initManifoldWasm() {
9956
10031
  if (_wasm$1) return _wasm$1;
9957
10032
  performance.mark("manifold:start");
9958
- const Module = (await import("./manifold-Dp6pvFr6.js")).default;
10033
+ const Module = (await import("./manifold-DpBXFS2K.js")).default;
9959
10034
  performance.mark("manifold:imported");
9960
10035
  const wasm = await Module();
9961
10036
  wasm.setup();
@@ -46528,10 +46603,8 @@ class PathBuilder {
46528
46603
  if (radius <= 0) throw new Error("fillet: radius must be positive");
46529
46604
  const n = this.segs.length;
46530
46605
  if (n < 2) throw new Error("fillet: need at least 2 segments before a fillet");
46531
- const prev = this.segs[n - 2];
46532
46606
  const curr = this.segs[n - 1];
46533
- curr.kind === "line" || curr.kind === "move" ? prev.kind === "line" || prev.kind === "move" ? 0 : 0 : 0;
46534
- const { trimA, trimB, arcSeg } = this.computeFilletGeom(radius);
46607
+ const { trimA, arcSeg } = this.computeFilletGeom(radius);
46535
46608
  if (!arcSeg) throw new Error("fillet: cannot fillet these segments (parallel or degenerate)");
46536
46609
  this.trimLastSegEnd(n - 2, trimA[0], trimA[1]);
46537
46610
  const trimmedSeg = { ...curr };
@@ -46603,7 +46676,6 @@ class PathBuilder {
46603
46676
  }
46604
46677
  getSegDirAt(seg, which) {
46605
46678
  if (seg.kind === "line" || seg.kind === "move") {
46606
- this.segs.length;
46607
46679
  const idx = this.segs.indexOf(seg);
46608
46680
  if (seg.kind === "line") {
46609
46681
  let sx, sy;
@@ -46845,6 +46917,41 @@ class PathBuilder {
46845
46917
  }
46846
46918
  return pts;
46847
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
+ }
46848
46955
  // ── Output ────────────────────────────────────────────────────────────────
46849
46956
  /**
46850
46957
  * Close the path and return a filled `Sketch`.
@@ -58763,7 +58870,7 @@ function requireFinite$7(value, label) {
58763
58870
  }
58764
58871
  return value;
58765
58872
  }
58766
- function requireVec3$2(value, label) {
58873
+ function requireVec3$3(value, label) {
58767
58874
  if (!Array.isArray(value) || value.length !== 3) {
58768
58875
  throw new Error(`${label} must be [x, y, z]`);
58769
58876
  }
@@ -58807,7 +58914,7 @@ function normalizeOptions(options) {
58807
58914
  out.size = requireFinite$7(options.size, "Viewport.label options.size");
58808
58915
  if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
58809
58916
  }
58810
- 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");
58811
58918
  if (options.anchor !== void 0) {
58812
58919
  if (!VALID_ANCHORS.has(options.anchor)) {
58813
58920
  throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
@@ -58824,7 +58931,7 @@ function collectRenderLabel(text, at, options) {
58824
58931
  if (typeof text !== "string" || text.trim().length === 0) {
58825
58932
  throw new Error("Viewport.label text must be a non-empty string");
58826
58933
  }
58827
- const normalizedAt = requireVec3$2(at, "Viewport.label at");
58934
+ const normalizedAt = requireVec3$3(at, "Viewport.label at");
58828
58935
  const normalizedOptions = normalizeOptions(options);
58829
58936
  _collected$4.push({
58830
58937
  id: `render-label-${_nextId++}`,
@@ -59019,7 +59126,7 @@ function requireFinite$6(value, label) {
59019
59126
  }
59020
59127
  return value;
59021
59128
  }
59022
- function requireVec3$1(value, label) {
59129
+ function requireVec3$2(value, label) {
59023
59130
  if (!Array.isArray(value) || value.length !== 3) {
59024
59131
  throw new Error(`${label} must be [x, y, z]`);
59025
59132
  }
@@ -59047,9 +59154,9 @@ const VALID_ENVIRONMENT_PRESETS = /* @__PURE__ */ new Set([
59047
59154
  ]);
59048
59155
  function validateCamera(cam, label) {
59049
59156
  const out = {};
59050
- if (cam.position !== void 0) out.position = requireVec3$1(cam.position, `${label}.position`);
59051
- if (cam.target !== void 0) out.target = requireVec3$1(cam.target, `${label}.target`);
59052
- 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`);
59053
59160
  if (cam.fov !== void 0) {
59054
59161
  out.fov = requireFinite$6(cam.fov, `${label}.fov`);
59055
59162
  if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
@@ -59184,8 +59291,8 @@ function validateLight(light, label) {
59184
59291
  const out = { type: light.type };
59185
59292
  if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
59186
59293
  if (light.intensity !== void 0) out.intensity = requireFinite$6(light.intensity, `${label}.intensity`);
59187
- if (light.position !== void 0) out.position = requireVec3$1(light.position, `${label}.position`);
59188
- 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`);
59189
59296
  if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
59190
59297
  if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
59191
59298
  if (light.angle !== void 0) out.angle = requireFinite$6(light.angle, `${label}.angle`);
@@ -60717,7 +60824,7 @@ function scale$1(v, s) {
60717
60824
  function dot$2(a2, b) {
60718
60825
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
60719
60826
  }
60720
- function lerp$2(a2, b, t) {
60827
+ function lerp$4(a2, b, t) {
60721
60828
  return a2 + (b - a2) * t;
60722
60829
  }
60723
60830
  function frameMatrix$1(x2, y2, z2, p2) {
@@ -60728,7 +60835,7 @@ function axisVector(axis, sign2 = 1) {
60728
60835
  if (axis === "Y") return [0, sign2, 0];
60729
60836
  return [0, 0, sign2];
60730
60837
  }
60731
- function axisPosition(axis, point2) {
60838
+ function axisPosition$1(axis, point2) {
60732
60839
  return point2[AXIS_INDEX[axis]];
60733
60840
  }
60734
60841
  function crossPointForStation(axis, point2) {
@@ -60736,7 +60843,7 @@ function crossPointForStation(axis, point2) {
60736
60843
  if (axis === "Y") return [point2[0], -point2[2]];
60737
60844
  return [point2[1], point2[2]];
60738
60845
  }
60739
- function orientLoftToAxis(shape, axis) {
60846
+ function orientLoftToAxis$1(shape, axis) {
60740
60847
  if (axis === "Z") return shape;
60741
60848
  if (axis === "Y") return shape.rotateX(-90);
60742
60849
  return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
@@ -60793,9 +60900,9 @@ function interpolateQuery(a2, b, t) {
60793
60900
  }
60794
60901
  return {
60795
60902
  side: sideA,
60796
- u: lerp$2(a2.u ?? 0.5, b.u ?? 0.5, t),
60797
- v: lerp$2(a2.v ?? 0.5, b.v ?? 0.5, t),
60798
- 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)
60799
60906
  };
60800
60907
  }
60801
60908
  function resolvePathQueries(points) {
@@ -60862,8 +60969,8 @@ class ProductSkin {
60862
60969
  this.stations = stations;
60863
60970
  this.rails = rails;
60864
60971
  for (const [name2, query] of Object.entries(refs)) this.refQueries.set(name2, cloneQuery(query));
60865
- this.axisMin = Math.min(...stations.map((station) => axisPosition(axis, station.center)));
60866
- 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)));
60867
60974
  this.diagnosticsValue = {
60868
60975
  ...diagnostics,
60869
60976
  stationNames: stations.map((station) => station.name),
@@ -60920,24 +61027,24 @@ class ProductSkin {
60920
61027
  }
60921
61028
  /** Interpolate center, width, and depth at a normalized v or absolute axis value. */
60922
61029
  stationAt(vOrAxis) {
60923
- 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);
60924
61031
  const sorted = this.stations;
60925
61032
  for (let index2 = 0; index2 < sorted.length - 1; index2 += 1) {
60926
61033
  const a2 = sorted[index2];
60927
61034
  const b = sorted[index2 + 1];
60928
- const aAxis = axisPosition(this.axis, a2.center);
60929
- 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);
60930
61037
  if (axisValue < aAxis - EPS$5 || axisValue > bAxis + EPS$5) continue;
60931
61038
  const span = Math.max(EPS$5, bAxis - aAxis);
60932
61039
  const t = clamp$6((axisValue - aAxis) / span, 0, 1);
60933
61040
  return {
60934
61041
  axisValue,
60935
- 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)],
60936
- width: lerp$2(a2.profile.width, b.profile.width, t),
60937
- 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),
60938
61045
  dWidth: (b.profile.width - a2.profile.width) / span,
60939
61046
  dDepth: (b.profile.depth - a2.profile.depth) / span,
60940
- exponent: lerp$2(profileExponent(a2), profileExponent(b), t),
61047
+ exponent: lerp$4(profileExponent(a2), profileExponent(b), t),
60941
61048
  kind: a2.profile.kind === b.profile.kind ? a2.profile.kind : "custom"
60942
61049
  };
60943
61050
  }
@@ -61059,7 +61166,7 @@ class ProductSkinBuilder {
61059
61166
  }
61060
61167
  /** Set named cross-section stations for the product skin. */
61061
61168
  stations(stations) {
61062
- 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));
61063
61170
  return this;
61064
61171
  }
61065
61172
  /** Attach named guide rails for product-skin construction and downstream surface references. */
@@ -61109,9 +61216,9 @@ class ProductSkinBuilder {
61109
61216
  const [x2, y2] = crossPointForStation(this.axisValue, station.center);
61110
61217
  return station.profile.sketch.translate(x2, y2);
61111
61218
  });
61112
- const heights = this.stationsValue.map((station) => axisPosition(this.axisValue, station.center));
61219
+ const heights = this.stationsValue.map((station) => axisPosition$1(this.axisValue, station.center));
61113
61220
  let shape = loft(localProfiles, heights, { edgeLength: this.edgeLengthValue });
61114
- shape = orientLoftToAxis(shape, this.axisValue);
61221
+ shape = orientLoftToAxis$1(shape, this.axisValue);
61115
61222
  if (this.colorValue) shape = shape.color(this.colorValue);
61116
61223
  shape = applyMaterial(shape, this.materialValue).as(this.name);
61117
61224
  const warnings = [];
@@ -61770,7 +61877,7 @@ function requirePositive$3(value, label) {
61770
61877
  function clamp$5(value, min2, max2) {
61771
61878
  return Math.max(min2, Math.min(max2, value));
61772
61879
  }
61773
- function lerp$1(a2, b, t) {
61880
+ function lerp$3(a2, b, t) {
61774
61881
  return a2 + (b - a2) * t;
61775
61882
  }
61776
61883
  function add(a2, b) {
@@ -61820,19 +61927,19 @@ function transformLocal(point2, tangentAcross, normal, tangentAlong, x2, y2, z2
61820
61927
  function interpolateCylinder(a2, b, t, mode) {
61821
61928
  let delta = b.angle - a2.angle;
61822
61929
  if (mode === "shortest" && Math.abs(delta) > 180) delta -= Math.sign(delta) * 360;
61823
- 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) };
61824
61931
  }
61825
61932
  function interpolatePlane(a2, b, t) {
61826
- 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) };
61827
61934
  }
61828
61935
  function interpolateProductSkin(a2, b, t) {
61829
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.");
61830
61937
  return {
61831
61938
  kind: "productSkin",
61832
61939
  side: a2.side ?? b.side,
61833
- u: lerp$1(a2.u ?? 0.5, b.u ?? 0.5, t),
61834
- v: lerp$1(a2.v ?? 0.5, b.v ?? 0.5, t),
61835
- 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)
61836
61943
  };
61837
61944
  }
61838
61945
  class SurfacePath {
@@ -62155,11 +62262,11 @@ function coordinateOnSide(coordinate, side, label) {
62155
62262
  return { ...coordinate, kind: "productSkin", side };
62156
62263
  }
62157
62264
  class ProductSkinCarrier {
62158
- constructor(skin, name = skin.name, sideValue, offsetValue = 0) {
62265
+ constructor(skin, name = skin.name, sideValue2, offsetValue = 0) {
62159
62266
  __publicField(this, "kind", "productSkin");
62160
62267
  this.skin = skin;
62161
62268
  this.name = name;
62162
- this.sideValue = sideValue;
62269
+ this.sideValue = sideValue2;
62163
62270
  this.offsetValue = offsetValue;
62164
62271
  }
62165
62272
  surface(side) {
@@ -62930,7 +63037,7 @@ function counterboresForPlate(spec2, width, height, thickness, diagnostics) {
62930
63037
  function minWidthAcrossAlongRange(widthAtT, length4, minAlong, maxAlong) {
62931
63038
  let minWidth = Number.POSITIVE_INFINITY;
62932
63039
  for (let index2 = 0; index2 <= 8; index2 += 1) {
62933
- const along = lerp$1(minAlong, maxAlong, index2 / 8);
63040
+ const along = lerp$3(minAlong, maxAlong, index2 / 8);
62934
63041
  const t = Math.max(0, Math.min(1, (along + length4 / 2) / Math.max(length4, 1e-8)));
62935
63042
  minWidth = Math.min(minWidth, widthAtT(t));
62936
63043
  }
@@ -63230,7 +63337,7 @@ function pathParameterAtDistance(samples, distance2) {
63230
63337
  const segmentLength = Math.hypot(b.point[0] - a2.point[0], b.point[1] - a2.point[1], b.point[2] - a2.point[2]);
63231
63338
  if (traveled + segmentLength >= distance2) {
63232
63339
  const localT = segmentLength <= 1e-8 ? 0 : (distance2 - traveled) / segmentLength;
63233
- return lerp$1(a2.t, b.t, localT);
63340
+ return lerp$3(a2.t, b.t, localT);
63234
63341
  }
63235
63342
  traveled += segmentLength;
63236
63343
  }
@@ -63283,7 +63390,7 @@ function compileBandFootprintMesh(path2, input) {
63283
63390
  const width = input.widthAt(t);
63284
63391
  const along = distance2 - length4 / 2;
63285
63392
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
63286
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
63393
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
63287
63394
  mesh.vertices.push(pointAtProfile([across, along], false));
63288
63395
  }
63289
63396
  }
@@ -63293,7 +63400,7 @@ function compileBandFootprintMesh(path2, input) {
63293
63400
  const width = input.widthAt(t);
63294
63401
  const along = distance2 - length4 / 2;
63295
63402
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
63296
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
63403
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
63297
63404
  mesh.vertices.push(pointAtProfile([across, along], true));
63298
63405
  }
63299
63406
  }
@@ -63305,7 +63412,7 @@ function compileBandFootprintMesh(path2, input) {
63305
63412
  const width = input.widthAt(t);
63306
63413
  const along = distance2 - length4 / 2;
63307
63414
  for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
63308
- 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);
63309
63416
  filled[alongIndex][acrossIndex] = !holes.some((hole2) => pointInProfileLoop([across, along], hole2));
63310
63417
  }
63311
63418
  }
@@ -67201,7 +67308,7 @@ const Constraint = {
67201
67308
  return builder.constrain({ type: "length", line: resolveLineId(builder, line2), value });
67202
67309
  }
67203
67310
  };
67204
- function requireVec3(v, label) {
67311
+ function requireVec3$1(v, label) {
67205
67312
  if (!Array.isArray(v) || v.length !== 3 || !Number.isFinite(v[0]) || !Number.isFinite(v[1]) || !Number.isFinite(v[2])) {
67206
67313
  throw new Error(`${label} must be a [number, number, number] with finite values, got ${JSON.stringify(v)}`);
67207
67314
  }
@@ -67214,24 +67321,24 @@ function requireFiniteNumber(n, label) {
67214
67321
  return n;
67215
67322
  }
67216
67323
  function distance$1(a2, b) {
67217
- requireVec3(a2, "a");
67218
- requireVec3(b, "b");
67324
+ requireVec3$1(a2, "a");
67325
+ requireVec3$1(b, "b");
67219
67326
  return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
67220
67327
  }
67221
67328
  function midpoint$1(a2, b) {
67222
- requireVec3(a2, "a");
67223
- requireVec3(b, "b");
67329
+ requireVec3$1(a2, "a");
67330
+ requireVec3$1(b, "b");
67224
67331
  return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
67225
67332
  }
67226
- function lerp(a2, b, t) {
67227
- requireVec3(a2, "a");
67228
- requireVec3(b, "b");
67333
+ function lerp$2(a2, b, t) {
67334
+ requireVec3$1(a2, "a");
67335
+ requireVec3$1(b, "b");
67229
67336
  requireFiniteNumber(t, "t");
67230
67337
  return [a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t];
67231
67338
  }
67232
67339
  function direction(a2, b) {
67233
- requireVec3(a2, "a");
67234
- requireVec3(b, "b");
67340
+ requireVec3$1(a2, "a");
67341
+ requireVec3$1(b, "b");
67235
67342
  const dx = b[0] - a2[0];
67236
67343
  const dy = b[1] - a2[1];
67237
67344
  const dz = b[2] - a2[2];
@@ -67242,8 +67349,8 @@ function direction(a2, b) {
67242
67349
  return [dx / len2, dy / len2, dz / len2];
67243
67350
  }
67244
67351
  function offset(point2, dir, amount) {
67245
- requireVec3(point2, "point");
67246
- requireVec3(dir, "dir");
67352
+ requireVec3$1(point2, "point");
67353
+ requireVec3$1(dir, "dir");
67247
67354
  requireFiniteNumber(amount, "amount");
67248
67355
  return [point2[0] + dir[0] * amount, point2[1] + dir[1] * amount, point2[2] + dir[2] * amount];
67249
67356
  }
@@ -67253,7 +67360,7 @@ const Points = {
67253
67360
  /** Center point between two 3D points. */
67254
67361
  midpoint: midpoint$1,
67255
67362
  /** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */
67256
- lerp,
67363
+ lerp: lerp$2,
67257
67364
  /** Unit direction vector from a to b. Throws if a and b are the same point. */
67258
67365
  direction,
67259
67366
  /** Move a point along a direction vector by a given amount. */
@@ -72385,9 +72492,84 @@ class ConstraintSketch extends Sketch {
72385
72492
  * Select the single arrangement region that contains the given seed point.
72386
72493
  * Throws if no region contains the seed.
72387
72494
  */
72388
- detectArrangementRegion(seed) {
72495
+ detectArrangementRegion(_seed) {
72389
72496
  throw new Error("Not implemented");
72390
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
+ }
72391
72573
  /**
72392
72574
  * Re-solve the sketch after changing the value of one existing constraint.
72393
72575
  *
@@ -87672,6 +87854,295 @@ function polygonVertices(sides, radius, options) {
87672
87854
  centerY: options == null ? void 0 : options.centerY
87673
87855
  });
87674
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
+ };
87675
88146
  let collectedHighlights = [];
87676
88147
  function resetHighlights() {
87677
88148
  collectedHighlights = [];
@@ -305140,6 +305611,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
305140
305611
  nurbsSurface,
305141
305612
  spline2d,
305142
305613
  spline3d,
305614
+ Loft,
305143
305615
  loft,
305144
305616
  loftAlongSpine,
305145
305617
  sweep,