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$7(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];
@@ -5691,13 +5691,13 @@ function parseMeshFile(data, format) {
5691
5691
  return parse3mf(data);
5692
5692
  }
5693
5693
  }
5694
- const EPS$9 = 1e-8;
5694
+ const EPS$a = 1e-8;
5695
5695
  function length$3(v) {
5696
5696
  return Math.hypot(v[0], v[1], v[2]);
5697
5697
  }
5698
5698
  function normalize$6(v) {
5699
5699
  const len2 = length$3(v);
5700
- if (len2 < EPS$9) throw new Error("Plane normal must be non-zero");
5700
+ if (len2 < EPS$a) throw new Error("Plane normal must be non-zero");
5701
5701
  return [v[0] / len2, v[1] / len2, v[2] / len2];
5702
5702
  }
5703
5703
  function resolvePlaneOriginNormal(plane) {
@@ -5719,12 +5719,12 @@ function resolvePlaneOriginNormal(plane) {
5719
5719
  function rotationToPlaneSpace(normal) {
5720
5720
  const n = normalize$6(normal);
5721
5721
  const dot2 = n[2];
5722
- if (dot2 > 1 - EPS$9) {
5722
+ if (dot2 > 1 - EPS$a) {
5723
5723
  return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
5724
5724
  }
5725
5725
  let axis;
5726
5726
  let angle;
5727
- if (dot2 < -1 + EPS$9) {
5727
+ if (dot2 < -1 + EPS$a) {
5728
5728
  axis = [1, 0, 0];
5729
5729
  angle = Math.PI;
5730
5730
  } else {
@@ -8286,7 +8286,7 @@ function scale$6(v, s) {
8286
8286
  function sub$7(a2, b) {
8287
8287
  return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
8288
8288
  }
8289
- function cross$7(a2, b) {
8289
+ function cross$8(a2, b) {
8290
8290
  return [a2[1] * b[2] - a2[2] * b[1], a2[2] * b[0] - a2[0] * b[2], a2[0] * b[1] - a2[1] * b[0]];
8291
8291
  }
8292
8292
  function makeEdge(name, start, end, faceName, curve) {
@@ -8322,7 +8322,7 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
8322
8322
  const center = options.center ?? average$1(corners);
8323
8323
  const uAxis = normalizeAxis$1(sub$7(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
8324
8324
  const vAxis = normalizeAxis$1(sub$7(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
8325
- const normal = normalizeAxis$1(options.normal ?? cross$7(uAxis, vAxis));
8325
+ const normal = normalizeAxis$1(options.normal ?? cross$8(uAxis, vAxis));
8326
8326
  const faces = /* @__PURE__ */ new Map();
8327
8327
  faces.set(faceName, {
8328
8328
  name: faceName,
@@ -10224,6 +10224,7 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
10224
10224
  edgeLength: options.edgeLength
10225
10225
  };
10226
10226
  }
10227
+ const EPS$9 = 1e-9;
10227
10228
  function resamplePolygon(poly, targetCount) {
10228
10229
  if (poly.length < 2) return poly;
10229
10230
  if (targetCount <= 0) return [];
@@ -10261,6 +10262,78 @@ function resamplePolygon(poly, targetCount) {
10261
10262
  }
10262
10263
  return out;
10263
10264
  }
10265
+ function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid$2(poly)) {
10266
+ if (poly.length < 3 || targetCount <= 0) return null;
10267
+ if (!isConvexPolygon(poly)) return null;
10268
+ const out = [];
10269
+ for (let index2 = 0; index2 < targetCount; index2 += 1) {
10270
+ const angle = index2 / targetCount * Math.PI * 2;
10271
+ const point2 = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
10272
+ if (!point2) return null;
10273
+ out.push(point2);
10274
+ }
10275
+ return out;
10276
+ }
10277
+ function rayPolygonIntersection(origin, direction2, poly) {
10278
+ let bestT = Infinity;
10279
+ let best = null;
10280
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10281
+ const a2 = poly[index2];
10282
+ const b = poly[(index2 + 1) % poly.length];
10283
+ const edge = [b[0] - a2[0], b[1] - a2[1]];
10284
+ const denom = cross$7(direction2, edge);
10285
+ if (Math.abs(denom) < EPS$9) continue;
10286
+ const delta = [a2[0] - origin[0], a2[1] - origin[1]];
10287
+ const rayT = cross$7(delta, edge) / denom;
10288
+ const edgeT = cross$7(delta, direction2) / denom;
10289
+ if (rayT >= -EPS$9 && edgeT >= -EPS$9 && edgeT <= 1 + EPS$9 && rayT < bestT) {
10290
+ bestT = rayT;
10291
+ best = [origin[0] + direction2[0] * rayT, origin[1] + direction2[1] * rayT];
10292
+ }
10293
+ }
10294
+ return best;
10295
+ }
10296
+ function polygonCentroid$2(poly) {
10297
+ let area2 = 0;
10298
+ let cx = 0;
10299
+ let cy = 0;
10300
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10301
+ const a2 = poly[index2];
10302
+ const b = poly[(index2 + 1) % poly.length];
10303
+ const crossValue = cross$7(a2, b);
10304
+ area2 += crossValue;
10305
+ cx += (a2[0] + b[0]) * crossValue;
10306
+ cy += (a2[1] + b[1]) * crossValue;
10307
+ }
10308
+ if (Math.abs(area2) < EPS$9) return averagePoint(poly);
10309
+ return [cx / (3 * area2), cy / (3 * area2)];
10310
+ }
10311
+ function averagePoint(poly) {
10312
+ let x2 = 0;
10313
+ let y2 = 0;
10314
+ for (const point2 of poly) {
10315
+ x2 += point2[0];
10316
+ y2 += point2[1];
10317
+ }
10318
+ return [x2 / poly.length, y2 / poly.length];
10319
+ }
10320
+ function isConvexPolygon(poly) {
10321
+ let sign2 = 0;
10322
+ for (let index2 = 0; index2 < poly.length; index2 += 1) {
10323
+ const a2 = poly[index2];
10324
+ const b = poly[(index2 + 1) % poly.length];
10325
+ const c2 = poly[(index2 + 2) % poly.length];
10326
+ const turn = cross$7([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
10327
+ if (Math.abs(turn) < EPS$9) continue;
10328
+ const currentSign = Math.sign(turn);
10329
+ if (sign2 !== 0 && currentSign !== sign2) return false;
10330
+ sign2 = currentSign;
10331
+ }
10332
+ return sign2 !== 0;
10333
+ }
10334
+ function cross$7(a2, b) {
10335
+ return a2[0] * b[1] - a2[1] * b[0];
10336
+ }
10264
10337
  function loftStitched(profiles2, heights, wasm) {
10265
10338
  if (profiles2.length < 2) return null;
10266
10339
  const classified = profiles2.map((loops) => classifyLoops(loops));
@@ -10389,8 +10462,10 @@ function stitchSingleLoopLoft(loops, heights, wasm) {
10389
10462
  maxPoints = Math.max(maxPoints, loop.length);
10390
10463
  }
10391
10464
  const N = Math.max(maxPoints, 24);
10465
+ const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
10466
+ const useAngularSamples = angularSamples.every((samples) => samples != null);
10392
10467
  const resampled = normalizedLoops.map((loop, i) => {
10393
- const pts2d = resamplePolygon(loop, N);
10468
+ const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
10394
10469
  const z2 = heights[i];
10395
10470
  return pts2d.map(([x2, y2]) => [x2, y2, z2]);
10396
10471
  });
@@ -10451,7 +10526,7 @@ let _wasm$1 = null;
10451
10526
  async function initManifoldWasm() {
10452
10527
  if (_wasm$1) return _wasm$1;
10453
10528
  performance.mark("manifold:start");
10454
- const Module = (await import("./manifold-Cjk7WhRs.js")).default;
10529
+ const Module = (await import("./manifold-B9QSr-qP.js")).default;
10455
10530
  performance.mark("manifold:imported");
10456
10531
  const wasm = await Module();
10457
10532
  wasm.setup();
@@ -47289,10 +47364,8 @@ class PathBuilder {
47289
47364
  if (radius <= 0) throw new Error("fillet: radius must be positive");
47290
47365
  const n = this.segs.length;
47291
47366
  if (n < 2) throw new Error("fillet: need at least 2 segments before a fillet");
47292
- const prev = this.segs[n - 2];
47293
47367
  const curr = this.segs[n - 1];
47294
- curr.kind === "line" || curr.kind === "move" ? prev.kind === "line" || prev.kind === "move" ? 0 : 0 : 0;
47295
- const { trimA, trimB, arcSeg } = this.computeFilletGeom(radius);
47368
+ const { trimA, arcSeg } = this.computeFilletGeom(radius);
47296
47369
  if (!arcSeg) throw new Error("fillet: cannot fillet these segments (parallel or degenerate)");
47297
47370
  this.trimLastSegEnd(n - 2, trimA[0], trimA[1]);
47298
47371
  const trimmedSeg = { ...curr };
@@ -47364,7 +47437,6 @@ class PathBuilder {
47364
47437
  }
47365
47438
  getSegDirAt(seg, which) {
47366
47439
  if (seg.kind === "line" || seg.kind === "move") {
47367
- this.segs.length;
47368
47440
  const idx = this.segs.indexOf(seg);
47369
47441
  if (seg.kind === "line") {
47370
47442
  let sx, sy;
@@ -47606,6 +47678,41 @@ class PathBuilder {
47606
47678
  }
47607
47679
  return pts;
47608
47680
  }
47681
+ /**
47682
+ * Return the open path as a sampled 2D polyline.
47683
+ *
47684
+ * This is for construction geometry such as guide rails, measured centerlines,
47685
+ * and curve-driven helpers where the authored path should stay open instead of
47686
+ * becoming a filled sketch or stroked profile.
47687
+ *
47688
+ * **Example**
47689
+ *
47690
+ * ```ts
47691
+ * const rail = path()
47692
+ * .moveTo(24, 0)
47693
+ * .bezierTo(32, 44, 28, 92, 18, 120)
47694
+ * .toPolyline();
47695
+ * ```
47696
+ *
47697
+ * @returns A sampled open polyline.
47698
+ * @category Path Builder
47699
+ */
47700
+ toPolyline() {
47701
+ const moveCount = this.segs.filter((seg) => seg.kind === "move").length;
47702
+ if (moveCount > 1) {
47703
+ throw new Error("path().toPolyline() supports one continuous open path. Use separate path() builders for separate rails.");
47704
+ }
47705
+ const pts = [];
47706
+ for (const point2 of this.tessellate()) {
47707
+ if (!point2.every(Number.isFinite)) throw new Error("path().toPolyline() produced a non-finite point");
47708
+ const previous = pts[pts.length - 1];
47709
+ if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) {
47710
+ pts.push(point2);
47711
+ }
47712
+ }
47713
+ if (pts.length < 2) throw new Error("path().toPolyline() needs at least 2 points");
47714
+ return pts;
47715
+ }
47609
47716
  // ── Output ────────────────────────────────────────────────────────────────
47610
47717
  /**
47611
47718
  * Close the path and return a filled `Sketch`.
@@ -52378,7 +52485,7 @@ function requireFinite$7(value, label) {
52378
52485
  }
52379
52486
  return value;
52380
52487
  }
52381
- function requireVec3$2(value, label) {
52488
+ function requireVec3$3(value, label) {
52382
52489
  if (!Array.isArray(value) || value.length !== 3) {
52383
52490
  throw new Error(`${label} must be [x, y, z]`);
52384
52491
  }
@@ -52422,7 +52529,7 @@ function normalizeOptions(options) {
52422
52529
  out.size = requireFinite$7(options.size, "Viewport.label options.size");
52423
52530
  if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
52424
52531
  }
52425
- if (options.offset !== void 0) out.offset = requireVec3$2(options.offset, "Viewport.label options.offset");
52532
+ if (options.offset !== void 0) out.offset = requireVec3$3(options.offset, "Viewport.label options.offset");
52426
52533
  if (options.anchor !== void 0) {
52427
52534
  if (!VALID_ANCHORS.has(options.anchor)) {
52428
52535
  throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
@@ -52439,7 +52546,7 @@ function collectRenderLabel(text, at, options) {
52439
52546
  if (typeof text !== "string" || text.trim().length === 0) {
52440
52547
  throw new Error("Viewport.label text must be a non-empty string");
52441
52548
  }
52442
- const normalizedAt = requireVec3$2(at, "Viewport.label at");
52549
+ const normalizedAt = requireVec3$3(at, "Viewport.label at");
52443
52550
  const normalizedOptions = normalizeOptions(options);
52444
52551
  _collected$4.push({
52445
52552
  id: `render-label-${_nextId++}`,
@@ -52634,7 +52741,7 @@ function requireFinite$6(value, label) {
52634
52741
  }
52635
52742
  return value;
52636
52743
  }
52637
- function requireVec3$1(value, label) {
52744
+ function requireVec3$2(value, label) {
52638
52745
  if (!Array.isArray(value) || value.length !== 3) {
52639
52746
  throw new Error(`${label} must be [x, y, z]`);
52640
52747
  }
@@ -52662,9 +52769,9 @@ const VALID_ENVIRONMENT_PRESETS = /* @__PURE__ */ new Set([
52662
52769
  ]);
52663
52770
  function validateCamera(cam, label) {
52664
52771
  const out = {};
52665
- if (cam.position !== void 0) out.position = requireVec3$1(cam.position, `${label}.position`);
52666
- if (cam.target !== void 0) out.target = requireVec3$1(cam.target, `${label}.target`);
52667
- if (cam.up !== void 0) out.up = requireVec3$1(cam.up, `${label}.up`);
52772
+ if (cam.position !== void 0) out.position = requireVec3$2(cam.position, `${label}.position`);
52773
+ if (cam.target !== void 0) out.target = requireVec3$2(cam.target, `${label}.target`);
52774
+ if (cam.up !== void 0) out.up = requireVec3$2(cam.up, `${label}.up`);
52668
52775
  if (cam.fov !== void 0) {
52669
52776
  out.fov = requireFinite$6(cam.fov, `${label}.fov`);
52670
52777
  if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
@@ -52799,8 +52906,8 @@ function validateLight(light, label) {
52799
52906
  const out = { type: light.type };
52800
52907
  if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
52801
52908
  if (light.intensity !== void 0) out.intensity = requireFinite$6(light.intensity, `${label}.intensity`);
52802
- if (light.position !== void 0) out.position = requireVec3$1(light.position, `${label}.position`);
52803
- if (light.target !== void 0) out.target = requireVec3$1(light.target, `${label}.target`);
52909
+ if (light.position !== void 0) out.position = requireVec3$2(light.position, `${label}.position`);
52910
+ if (light.target !== void 0) out.target = requireVec3$2(light.target, `${label}.target`);
52804
52911
  if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
52805
52912
  if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
52806
52913
  if (light.angle !== void 0) out.angle = requireFinite$6(light.angle, `${label}.angle`);
@@ -54332,7 +54439,7 @@ function scale$1(v, s) {
54332
54439
  function dot$2(a2, b) {
54333
54440
  return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
54334
54441
  }
54335
- function lerp$2(a2, b, t) {
54442
+ function lerp$4(a2, b, t) {
54336
54443
  return a2 + (b - a2) * t;
54337
54444
  }
54338
54445
  function frameMatrix$1(x2, y2, z2, p2) {
@@ -54343,7 +54450,7 @@ function axisVector(axis, sign2 = 1) {
54343
54450
  if (axis === "Y") return [0, sign2, 0];
54344
54451
  return [0, 0, sign2];
54345
54452
  }
54346
- function axisPosition(axis, point2) {
54453
+ function axisPosition$1(axis, point2) {
54347
54454
  return point2[AXIS_INDEX[axis]];
54348
54455
  }
54349
54456
  function crossPointForStation(axis, point2) {
@@ -54351,7 +54458,7 @@ function crossPointForStation(axis, point2) {
54351
54458
  if (axis === "Y") return [point2[0], -point2[2]];
54352
54459
  return [point2[1], point2[2]];
54353
54460
  }
54354
- function orientLoftToAxis(shape, axis) {
54461
+ function orientLoftToAxis$1(shape, axis) {
54355
54462
  if (axis === "Z") return shape;
54356
54463
  if (axis === "Y") return shape.rotateX(-90);
54357
54464
  return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
@@ -54408,9 +54515,9 @@ function interpolateQuery(a2, b, t) {
54408
54515
  }
54409
54516
  return {
54410
54517
  side: sideA,
54411
- u: lerp$2(a2.u ?? 0.5, b.u ?? 0.5, t),
54412
- v: lerp$2(a2.v ?? 0.5, b.v ?? 0.5, t),
54413
- offset: lerp$2(a2.offset ?? 0, b.offset ?? 0, t)
54518
+ u: lerp$4(a2.u ?? 0.5, b.u ?? 0.5, t),
54519
+ v: lerp$4(a2.v ?? 0.5, b.v ?? 0.5, t),
54520
+ offset: lerp$4(a2.offset ?? 0, b.offset ?? 0, t)
54414
54521
  };
54415
54522
  }
54416
54523
  function resolvePathQueries(points) {
@@ -54477,8 +54584,8 @@ class ProductSkin {
54477
54584
  this.stations = stations;
54478
54585
  this.rails = rails;
54479
54586
  for (const [name2, query] of Object.entries(refs)) this.refQueries.set(name2, cloneQuery(query));
54480
- this.axisMin = Math.min(...stations.map((station) => axisPosition(axis, station.center)));
54481
- this.axisMax = Math.max(...stations.map((station) => axisPosition(axis, station.center)));
54587
+ this.axisMin = Math.min(...stations.map((station) => axisPosition$1(axis, station.center)));
54588
+ this.axisMax = Math.max(...stations.map((station) => axisPosition$1(axis, station.center)));
54482
54589
  this.diagnosticsValue = {
54483
54590
  ...diagnostics,
54484
54591
  stationNames: stations.map((station) => station.name),
@@ -54535,24 +54642,24 @@ class ProductSkin {
54535
54642
  }
54536
54643
  /** Interpolate center, width, and depth at a normalized v or absolute axis value. */
54537
54644
  stationAt(vOrAxis) {
54538
- const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$2(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
54645
+ const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$4(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
54539
54646
  const sorted = this.stations;
54540
54647
  for (let index2 = 0; index2 < sorted.length - 1; index2 += 1) {
54541
54648
  const a2 = sorted[index2];
54542
54649
  const b = sorted[index2 + 1];
54543
- const aAxis = axisPosition(this.axis, a2.center);
54544
- const bAxis = axisPosition(this.axis, b.center);
54650
+ const aAxis = axisPosition$1(this.axis, a2.center);
54651
+ const bAxis = axisPosition$1(this.axis, b.center);
54545
54652
  if (axisValue < aAxis - EPS$5 || axisValue > bAxis + EPS$5) continue;
54546
54653
  const span = Math.max(EPS$5, bAxis - aAxis);
54547
54654
  const t = clamp$5((axisValue - aAxis) / span, 0, 1);
54548
54655
  return {
54549
54656
  axisValue,
54550
- 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)],
54551
- width: lerp$2(a2.profile.width, b.profile.width, t),
54552
- depth: lerp$2(a2.profile.depth, b.profile.depth, t),
54657
+ center: [lerp$4(a2.center[0], b.center[0], t), lerp$4(a2.center[1], b.center[1], t), lerp$4(a2.center[2], b.center[2], t)],
54658
+ width: lerp$4(a2.profile.width, b.profile.width, t),
54659
+ depth: lerp$4(a2.profile.depth, b.profile.depth, t),
54553
54660
  dWidth: (b.profile.width - a2.profile.width) / span,
54554
54661
  dDepth: (b.profile.depth - a2.profile.depth) / span,
54555
- exponent: lerp$2(profileExponent(a2), profileExponent(b), t),
54662
+ exponent: lerp$4(profileExponent(a2), profileExponent(b), t),
54556
54663
  kind: a2.profile.kind === b.profile.kind ? a2.profile.kind : "custom"
54557
54664
  };
54558
54665
  }
@@ -54674,7 +54781,7 @@ class ProductSkinBuilder {
54674
54781
  }
54675
54782
  /** Set named cross-section stations for the product skin. */
54676
54783
  stations(stations) {
54677
- this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition(this.axisValue, a2.center) - axisPosition(this.axisValue, b.center));
54784
+ this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition$1(this.axisValue, a2.center) - axisPosition$1(this.axisValue, b.center));
54678
54785
  return this;
54679
54786
  }
54680
54787
  /** Attach named guide rails for product-skin construction and downstream surface references. */
@@ -54724,9 +54831,9 @@ class ProductSkinBuilder {
54724
54831
  const [x2, y2] = crossPointForStation(this.axisValue, station.center);
54725
54832
  return station.profile.sketch.translate(x2, y2);
54726
54833
  });
54727
- const heights = this.stationsValue.map((station) => axisPosition(this.axisValue, station.center));
54834
+ const heights = this.stationsValue.map((station) => axisPosition$1(this.axisValue, station.center));
54728
54835
  let shape = loft(localProfiles, heights, { edgeLength: this.edgeLengthValue });
54729
- shape = orientLoftToAxis(shape, this.axisValue);
54836
+ shape = orientLoftToAxis$1(shape, this.axisValue);
54730
54837
  if (this.colorValue) shape = shape.color(this.colorValue);
54731
54838
  shape = applyMaterial(shape, this.materialValue).as(this.name);
54732
54839
  const warnings = [];
@@ -55385,7 +55492,7 @@ function requirePositive$3(value, label) {
55385
55492
  function clamp$4(value, min2, max2) {
55386
55493
  return Math.max(min2, Math.min(max2, value));
55387
55494
  }
55388
- function lerp$1(a2, b, t) {
55495
+ function lerp$3(a2, b, t) {
55389
55496
  return a2 + (b - a2) * t;
55390
55497
  }
55391
55498
  function add(a2, b) {
@@ -55435,19 +55542,19 @@ function transformLocal(point2, tangentAcross, normal, tangentAlong, x2, y2, z2
55435
55542
  function interpolateCylinder(a2, b, t, mode) {
55436
55543
  let delta = b.angle - a2.angle;
55437
55544
  if (mode === "shortest" && Math.abs(delta) > 180) delta -= Math.sign(delta) * 360;
55438
- return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$1(a2.z, b.z, t), offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t) };
55545
+ return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$3(a2.z, b.z, t), offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t) };
55439
55546
  }
55440
55547
  function interpolatePlane(a2, b, t) {
55441
- return { kind: "plane", x: lerp$1(a2.x, b.x, t), y: lerp$1(a2.y, b.y, t), offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t) };
55548
+ return { kind: "plane", x: lerp$3(a2.x, b.x, t), y: lerp$3(a2.y, b.y, t), offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t) };
55442
55549
  }
55443
55550
  function interpolateProductSkin(a2, b, t) {
55444
55551
  if ((a2.side ?? b.side) !== (b.side ?? a2.side)) throw new Error("SurfacePath on ProductSkin currently supports one side per path; split side transitions into separate members.");
55445
55552
  return {
55446
55553
  kind: "productSkin",
55447
55554
  side: a2.side ?? b.side,
55448
- u: lerp$1(a2.u ?? 0.5, b.u ?? 0.5, t),
55449
- v: lerp$1(a2.v ?? 0.5, b.v ?? 0.5, t),
55450
- offset: lerp$1(a2.offset ?? 0, b.offset ?? 0, t)
55555
+ u: lerp$3(a2.u ?? 0.5, b.u ?? 0.5, t),
55556
+ v: lerp$3(a2.v ?? 0.5, b.v ?? 0.5, t),
55557
+ offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t)
55451
55558
  };
55452
55559
  }
55453
55560
  class SurfacePath {
@@ -55770,11 +55877,11 @@ function coordinateOnSide(coordinate, side, label) {
55770
55877
  return { ...coordinate, kind: "productSkin", side };
55771
55878
  }
55772
55879
  class ProductSkinCarrier {
55773
- constructor(skin, name = skin.name, sideValue, offsetValue = 0) {
55880
+ constructor(skin, name = skin.name, sideValue2, offsetValue = 0) {
55774
55881
  __publicField(this, "kind", "productSkin");
55775
55882
  this.skin = skin;
55776
55883
  this.name = name;
55777
- this.sideValue = sideValue;
55884
+ this.sideValue = sideValue2;
55778
55885
  this.offsetValue = offsetValue;
55779
55886
  }
55780
55887
  surface(side) {
@@ -56545,7 +56652,7 @@ function counterboresForPlate(spec2, width, height, thickness, diagnostics) {
56545
56652
  function minWidthAcrossAlongRange(widthAtT, length4, minAlong, maxAlong) {
56546
56653
  let minWidth = Number.POSITIVE_INFINITY;
56547
56654
  for (let index2 = 0; index2 <= 8; index2 += 1) {
56548
- const along = lerp$1(minAlong, maxAlong, index2 / 8);
56655
+ const along = lerp$3(minAlong, maxAlong, index2 / 8);
56549
56656
  const t = Math.max(0, Math.min(1, (along + length4 / 2) / Math.max(length4, 1e-8)));
56550
56657
  minWidth = Math.min(minWidth, widthAtT(t));
56551
56658
  }
@@ -56845,7 +56952,7 @@ function pathParameterAtDistance(samples, distance2) {
56845
56952
  const segmentLength = Math.hypot(b.point[0] - a2.point[0], b.point[1] - a2.point[1], b.point[2] - a2.point[2]);
56846
56953
  if (traveled + segmentLength >= distance2) {
56847
56954
  const localT = segmentLength <= 1e-8 ? 0 : (distance2 - traveled) / segmentLength;
56848
- return lerp$1(a2.t, b.t, localT);
56955
+ return lerp$3(a2.t, b.t, localT);
56849
56956
  }
56850
56957
  traveled += segmentLength;
56851
56958
  }
@@ -56898,7 +57005,7 @@ function compileBandFootprintMesh(path2, input) {
56898
57005
  const width = input.widthAt(t);
56899
57006
  const along = distance2 - length4 / 2;
56900
57007
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
56901
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
57008
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
56902
57009
  mesh.vertices.push(pointAtProfile([across, along], false));
56903
57010
  }
56904
57011
  }
@@ -56908,7 +57015,7 @@ function compileBandFootprintMesh(path2, input) {
56908
57015
  const width = input.widthAt(t);
56909
57016
  const along = distance2 - length4 / 2;
56910
57017
  for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
56911
- const across = lerp$1(-width / 2, width / 2, acrossIndex / acrossSegments);
57018
+ const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
56912
57019
  mesh.vertices.push(pointAtProfile([across, along], true));
56913
57020
  }
56914
57021
  }
@@ -56920,7 +57027,7 @@ function compileBandFootprintMesh(path2, input) {
56920
57027
  const width = input.widthAt(t);
56921
57028
  const along = distance2 - length4 / 2;
56922
57029
  for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
56923
- const across = lerp$1(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
57030
+ const across = lerp$3(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
56924
57031
  filled[alongIndex][acrossIndex] = !holes.some((hole2) => pointInProfileLoop([across, along], hole2));
56925
57032
  }
56926
57033
  }
@@ -59002,7 +59109,7 @@ const Constraint = {
59002
59109
  return builder.constrain({ type: "length", line: resolveLineId(builder, line2), value });
59003
59110
  }
59004
59111
  };
59005
- function requireVec3(v, label) {
59112
+ function requireVec3$1(v, label) {
59006
59113
  if (!Array.isArray(v) || v.length !== 3 || !Number.isFinite(v[0]) || !Number.isFinite(v[1]) || !Number.isFinite(v[2])) {
59007
59114
  throw new Error(`${label} must be a [number, number, number] with finite values, got ${JSON.stringify(v)}`);
59008
59115
  }
@@ -59015,24 +59122,24 @@ function requireFiniteNumber(n, label) {
59015
59122
  return n;
59016
59123
  }
59017
59124
  function distance$1(a2, b) {
59018
- requireVec3(a2, "a");
59019
- requireVec3(b, "b");
59125
+ requireVec3$1(a2, "a");
59126
+ requireVec3$1(b, "b");
59020
59127
  return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
59021
59128
  }
59022
59129
  function midpoint$1(a2, b) {
59023
- requireVec3(a2, "a");
59024
- requireVec3(b, "b");
59130
+ requireVec3$1(a2, "a");
59131
+ requireVec3$1(b, "b");
59025
59132
  return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
59026
59133
  }
59027
- function lerp(a2, b, t) {
59028
- requireVec3(a2, "a");
59029
- requireVec3(b, "b");
59134
+ function lerp$2(a2, b, t) {
59135
+ requireVec3$1(a2, "a");
59136
+ requireVec3$1(b, "b");
59030
59137
  requireFiniteNumber(t, "t");
59031
59138
  return [a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t];
59032
59139
  }
59033
59140
  function direction(a2, b) {
59034
- requireVec3(a2, "a");
59035
- requireVec3(b, "b");
59141
+ requireVec3$1(a2, "a");
59142
+ requireVec3$1(b, "b");
59036
59143
  const dx = b[0] - a2[0];
59037
59144
  const dy = b[1] - a2[1];
59038
59145
  const dz = b[2] - a2[2];
@@ -59043,8 +59150,8 @@ function direction(a2, b) {
59043
59150
  return [dx / len2, dy / len2, dz / len2];
59044
59151
  }
59045
59152
  function offset(point2, dir, amount) {
59046
- requireVec3(point2, "point");
59047
- requireVec3(dir, "dir");
59153
+ requireVec3$1(point2, "point");
59154
+ requireVec3$1(dir, "dir");
59048
59155
  requireFiniteNumber(amount, "amount");
59049
59156
  return [point2[0] + dir[0] * amount, point2[1] + dir[1] * amount, point2[2] + dir[2] * amount];
59050
59157
  }
@@ -59054,7 +59161,7 @@ const Points = {
59054
59161
  /** Center point between two 3D points. */
59055
59162
  midpoint: midpoint$1,
59056
59163
  /** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */
59057
- lerp,
59164
+ lerp: lerp$2,
59058
59165
  /** Unit direction vector from a to b. Throws if a and b are the same point. */
59059
59166
  direction,
59060
59167
  /** Move a point along a direction vector by a given amount. */
@@ -64186,9 +64293,84 @@ class ConstraintSketch extends Sketch {
64186
64293
  * Select the single arrangement region that contains the given seed point.
64187
64294
  * Throws if no region contains the seed.
64188
64295
  */
64189
- detectArrangementRegion(seed) {
64296
+ detectArrangementRegion(_seed) {
64190
64297
  throw new Error("Not implemented");
64191
64298
  }
64299
+ /**
64300
+ * Return the solved constrained path as a sampled 2D polyline.
64301
+ *
64302
+ * Use this when a construction rail was authored with `constrainedSketch()`
64303
+ * and should feed another operation such as `Loft.pathOnXz(...)`.
64304
+ * The sketch must contain exactly one profile path.
64305
+ *
64306
+ * @param samples - Samples per curved segment. Default 32.
64307
+ * @returns The solved path as an open polyline.
64308
+ */
64309
+ toPolyline(samples = 32) {
64310
+ if (!Number.isFinite(samples) || samples < 2) throw new Error("ConstraintSketch.toPolyline() samples must be at least 2");
64311
+ const profileLoops = this.definition.loops.filter((loop) => loop.type === "profile");
64312
+ if (profileLoops.length !== 1) {
64313
+ throw new Error("ConstraintSketch.toPolyline() requires exactly one profile path");
64314
+ }
64315
+ const sampleCount = Math.max(2, Math.round(samples));
64316
+ const pointMap = new Map(this.definition.points.map((point2) => [point2.id, point2]));
64317
+ const lineMap = new Map(this.definition.lines.map((line2) => [line2.id, line2]));
64318
+ const arcMap = new Map(this.definition.arcs.map((arc) => [arc.id, arc]));
64319
+ const bezierMap = new Map(this.definition.beziers.map((bezier) => [bezier.id, bezier]));
64320
+ const points = [];
64321
+ const appendStart = (point2, label) => {
64322
+ const previous = points[points.length - 1];
64323
+ if (!previous) {
64324
+ points.push(point2);
64325
+ return;
64326
+ }
64327
+ if (Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-6) {
64328
+ throw new Error(`ConstraintSketch.toPolyline() profile path is not continuous at ${label}`);
64329
+ }
64330
+ };
64331
+ const appendPoint = (point2) => {
64332
+ const previous = points[points.length - 1];
64333
+ if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) points.push(point2);
64334
+ };
64335
+ const requirePoint = (id, label) => {
64336
+ const point2 = pointMap.get(id);
64337
+ if (!point2) throw new Error(`ConstraintSketch.toPolyline() missing ${label}`);
64338
+ return [point2.x, point2.y];
64339
+ };
64340
+ for (const segment of profileLoops[0].segments) {
64341
+ if (segment.kind === "line") {
64342
+ const line2 = lineMap.get(segment.line);
64343
+ if (!line2) throw new Error(`ConstraintSketch.toPolyline() missing line "${segment.line}"`);
64344
+ appendStart(requirePoint(line2.a, `line "${segment.line}" start point`), `line "${segment.line}"`);
64345
+ appendPoint(requirePoint(line2.b, `line "${segment.line}" end point`));
64346
+ } else if (segment.kind === "arc") {
64347
+ const arc = arcMap.get(segment.arc);
64348
+ if (!arc) throw new Error(`ConstraintSketch.toPolyline() missing arc "${segment.arc}"`);
64349
+ const center = requirePoint(arc.center, `arc "${segment.arc}" center point`);
64350
+ const start = requirePoint(arc.start, `arc "${segment.arc}" start point`);
64351
+ const end = requirePoint(arc.end, `arc "${segment.arc}" end point`);
64352
+ appendStart(start, `arc "${segment.arc}"`);
64353
+ const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
64354
+ const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
64355
+ for (const point2 of tessellateArc(center[0], center[1], arc.radius, startAngle, endAngle, arc.clockwise, sampleCount)) {
64356
+ appendPoint(point2);
64357
+ }
64358
+ } else {
64359
+ const bezier = bezierMap.get(segment.bezier);
64360
+ if (!bezier) throw new Error(`ConstraintSketch.toPolyline() missing bezier "${segment.bezier}"`);
64361
+ const p0 = requirePoint(bezier.p0, `bezier "${segment.bezier}" start point`);
64362
+ const p1 = requirePoint(bezier.p1, `bezier "${segment.bezier}" first control point`);
64363
+ const p2 = requirePoint(bezier.p2, `bezier "${segment.bezier}" second control point`);
64364
+ const p3 = requirePoint(bezier.p3, `bezier "${segment.bezier}" end point`);
64365
+ appendStart(p0, `bezier "${segment.bezier}"`);
64366
+ for (const point2 of tessellateBezier(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], p3[0], p3[1], sampleCount)) {
64367
+ appendPoint(point2);
64368
+ }
64369
+ }
64370
+ }
64371
+ if (points.length < 2) throw new Error("ConstraintSketch.toPolyline() needs at least 2 points");
64372
+ return points;
64373
+ }
64192
64374
  /**
64193
64375
  * Re-solve the sketch after changing the value of one existing constraint.
64194
64376
  *
@@ -79473,6 +79655,295 @@ function polygonVertices(sides, radius, options) {
79473
79655
  centerY: options == null ? void 0 : options.centerY
79474
79656
  });
79475
79657
  }
79658
+ const LOFT_GUIDE_EPS = 1e-8;
79659
+ function orientLoftToAxis(shape, axis) {
79660
+ if (axis === "Z") return shape;
79661
+ if (axis === "Y") return shape.rotateX(-90);
79662
+ return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
79663
+ }
79664
+ function buildRailEvaluators(rails, axis, start, end, railSamples) {
79665
+ const seen = /* @__PURE__ */ new Set();
79666
+ return rails.map((rail2) => {
79667
+ if (seen.has(rail2.side)) throw new Error(`Loft.withGuideRails() received more than one ${rail2.side} rail`);
79668
+ seen.add(rail2.side);
79669
+ const sampled = sampleRailPath(rail2.path, railSamples);
79670
+ if (sampled.length < 2) throw new Error("Loft guide rails require at least two points");
79671
+ const points = sampled.map((point2) => ({ position: axisPosition(axis, point2), cross: crossPointForAxis(axis, point2) }));
79672
+ const ordered = points[points.length - 1].position >= points[0].position ? points : [...points].reverse();
79673
+ validateRailCoverage(ordered, start, end);
79674
+ return { side: rail2.side, points: ordered };
79675
+ });
79676
+ }
79677
+ function railCrossAt(rail2, position) {
79678
+ const points = rail2.points;
79679
+ if (position <= points[0].position + LOFT_GUIDE_EPS) return points[0].cross;
79680
+ const last = points[points.length - 1];
79681
+ if (position >= last.position - LOFT_GUIDE_EPS) return last.cross;
79682
+ for (let index2 = 0; index2 < points.length - 1; index2 += 1) {
79683
+ const a2 = points[index2];
79684
+ const b = points[index2 + 1];
79685
+ if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
79686
+ const t = (position - a2.position) / (b.position - a2.position);
79687
+ return [lerp$1(a2.cross[0], b.cross[0], t), lerp$1(a2.cross[1], b.cross[1], t)];
79688
+ }
79689
+ }
79690
+ throw new Error("Loft guide rail does not cover requested station position");
79691
+ }
79692
+ function validateRailCoverage(points, start, end) {
79693
+ for (let index2 = 1; index2 < points.length; index2 += 1) {
79694
+ if (points[index2].position - points[index2 - 1].position < LOFT_GUIDE_EPS) {
79695
+ throw new Error("Loft guide rails must be monotone along the loft axis");
79696
+ }
79697
+ }
79698
+ if (points[0].position - start > LOFT_GUIDE_EPS || end - points[points.length - 1].position > LOFT_GUIDE_EPS) {
79699
+ throw new Error("Loft guide rails must cover the full station range");
79700
+ }
79701
+ }
79702
+ function sampleRailPath(path2, samples) {
79703
+ if (Array.isArray(path2)) return path2.map((point2, index2) => requireVec3(point2, `Loft guide rail point ${index2}`));
79704
+ if (path2 instanceof Curve3D || path2 instanceof HermiteCurve3D || path2 instanceof QuinticHermiteCurve3D || path2 instanceof NurbsCurve3D) {
79705
+ return path2.sample(Math.max(2, Math.round(samples))).map((point2, index2) => requireVec3(point2, `Loft guide rail sample ${index2}`));
79706
+ }
79707
+ throw new Error("Loft guide rail path must be a Vec3[] or ForgeCAD 3D curve");
79708
+ }
79709
+ function requireVec3(point2, label) {
79710
+ if (!Array.isArray(point2) || point2.length !== 3 || !point2.every(Number.isFinite)) {
79711
+ throw new Error(`${label} must be a finite [x, y, z] point`);
79712
+ }
79713
+ return [point2[0], point2[1], point2[2]];
79714
+ }
79715
+ function axisPosition(axis, point2) {
79716
+ if (axis === "X") return point2[0];
79717
+ if (axis === "Y") return point2[1];
79718
+ return point2[2];
79719
+ }
79720
+ function crossPointForAxis(axis, point2) {
79721
+ if (axis === "X") return [point2[1], point2[2]];
79722
+ if (axis === "Y") return [point2[0], -point2[2]];
79723
+ return [point2[0], point2[1]];
79724
+ }
79725
+ function lerp$1(a2, b, t) {
79726
+ return a2 + (b - a2) * t;
79727
+ }
79728
+ function loftWithGuideRails(stations, rails, options = {}) {
79729
+ if (stations.length < 2) throw new Error("Loft.withGuideRails() requires at least two stations");
79730
+ if (rails.length === 0) throw new Error("Loft.withGuideRails() requires at least one guide rail");
79731
+ const sortedStations = sortedValidStations(stations);
79732
+ const axis = options.axis ?? "Z";
79733
+ const start = sortedStations[0].position;
79734
+ const end = sortedStations[sortedStations.length - 1].position;
79735
+ const railEvaluators = buildRailEvaluators(rails, axis, start, end, options.railSamples ?? 64);
79736
+ const positions = generatedPositions(sortedStations, options.samples);
79737
+ const profiles2 = positions.map((position) => {
79738
+ const source = profileForPosition(sortedStations, position);
79739
+ const bounds = boundsForPosition(sortedStations, position);
79740
+ return fitProfileToBounds(source, applyRailsToBounds(bounds, railEvaluators, position));
79741
+ });
79742
+ const shape = loft(profiles2, positions, {
79743
+ edgeLength: options.edgeLength,
79744
+ boundsPadding: options.boundsPadding
79745
+ });
79746
+ return orientLoftToAxis(shape, axis);
79747
+ }
79748
+ function sortedValidStations(stations) {
79749
+ const sorted = [...stations].sort((a2, b) => a2.position - b.position);
79750
+ for (let index2 = 0; index2 < sorted.length; index2 += 1) {
79751
+ if (!Number.isFinite(sorted[index2].position)) throw new Error("Loft.withGuideRails station position must be finite");
79752
+ if (!(sorted[index2].profile instanceof Sketch)) throw new Error("Loft.withGuideRails() stations must use Sketch profiles");
79753
+ if (index2 > 0 && sorted[index2].position - sorted[index2 - 1].position < LOFT_GUIDE_EPS) {
79754
+ throw new Error("Loft.withGuideRails() requires unique, strictly increasing station positions");
79755
+ }
79756
+ }
79757
+ return sorted;
79758
+ }
79759
+ function generatedPositions(stations, samples) {
79760
+ const count = Math.max(2, Math.round(samples ?? Math.max(9, (stations.length - 1) * 8 + 1)));
79761
+ const start = stations[0].position;
79762
+ const end = stations[stations.length - 1].position;
79763
+ const values = /* @__PURE__ */ new Set();
79764
+ const positions = [];
79765
+ const addPosition = (position) => {
79766
+ const key = position.toFixed(9);
79767
+ if (!values.has(key)) {
79768
+ values.add(key);
79769
+ positions.push(position);
79770
+ }
79771
+ };
79772
+ for (let index2 = 0; index2 < count; index2 += 1) addPosition(start + (end - start) * index2 / (count - 1));
79773
+ for (const station of stations) addPosition(station.position);
79774
+ return positions.sort((a2, b) => a2 - b);
79775
+ }
79776
+ function profileForPosition(stations, position) {
79777
+ for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
79778
+ if (position <= stations[index2 + 1].position + LOFT_GUIDE_EPS) return stations[index2].profile;
79779
+ }
79780
+ return stations[stations.length - 1].profile;
79781
+ }
79782
+ function boundsForPosition(stations, position) {
79783
+ if (position <= stations[0].position + LOFT_GUIDE_EPS) return sketchBounds(stations[0].profile);
79784
+ const last = stations[stations.length - 1];
79785
+ if (position >= last.position - LOFT_GUIDE_EPS) return sketchBounds(last.profile);
79786
+ for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
79787
+ const a2 = stations[index2];
79788
+ const b = stations[index2 + 1];
79789
+ if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
79790
+ return lerpBounds(sketchBounds(a2.profile), sketchBounds(b.profile), (position - a2.position) / (b.position - a2.position));
79791
+ }
79792
+ }
79793
+ return sketchBounds(last.profile);
79794
+ }
79795
+ function applyRailsToBounds(bounds, rails, position) {
79796
+ const centerRail = rails.find((rail2) => rail2.side === "center");
79797
+ const center = centerRail ? railCrossAt(centerRail, position) : void 0;
79798
+ const next = { ...bounds };
79799
+ applyAxisRail(next, "X", sideValue(rails, "left", position, 0), sideValue(rails, "right", position, 0), center == null ? void 0 : center[0]);
79800
+ applyAxisRail(next, "Y", sideValue(rails, "back", position, 1), sideValue(rails, "front", position, 1), center == null ? void 0 : center[1]);
79801
+ if (next.maxX - next.minX < LOFT_GUIDE_EPS || next.maxY - next.minY < LOFT_GUIDE_EPS) {
79802
+ throw new Error("Loft.withGuideRails() guide rails produced a non-positive section size");
79803
+ }
79804
+ return next;
79805
+ }
79806
+ function sideValue(rails, side, position, crossIndex) {
79807
+ const rail2 = rails.find((entry) => entry.side === side);
79808
+ return rail2 ? railCrossAt(rail2, position)[crossIndex] : void 0;
79809
+ }
79810
+ function applyAxisRail(bounds, axis, minRail, maxRail, center) {
79811
+ const minKey = axis === "X" ? "minX" : "minY";
79812
+ const maxKey = axis === "X" ? "maxX" : "maxY";
79813
+ const width = bounds[maxKey] - bounds[minKey];
79814
+ if (minRail != null && maxRail != null) {
79815
+ if (maxRail - minRail < LOFT_GUIDE_EPS) throw new Error("Loft.withGuideRails() opposite guide rails crossed");
79816
+ if (center != null && Math.abs((minRail + maxRail) / 2 - center) > 1e-5) {
79817
+ throw new Error("Loft.withGuideRails() center rail conflicts with opposite side rails");
79818
+ }
79819
+ bounds[minKey] = minRail;
79820
+ bounds[maxKey] = maxRail;
79821
+ } else if (maxRail != null) {
79822
+ bounds[maxKey] = maxRail;
79823
+ bounds[minKey] = center != null ? 2 * center - maxRail : maxRail - width;
79824
+ } else if (minRail != null) {
79825
+ bounds[minKey] = minRail;
79826
+ bounds[maxKey] = center != null ? 2 * center - minRail : minRail + width;
79827
+ } else if (center != null) {
79828
+ bounds[minKey] = center - width / 2;
79829
+ bounds[maxKey] = center + width / 2;
79830
+ }
79831
+ }
79832
+ function fitProfileToBounds(profile, target) {
79833
+ const source = sketchBounds(profile);
79834
+ const sourceWidth = source.maxX - source.minX;
79835
+ const sourceDepth = source.maxY - source.minY;
79836
+ if (sourceWidth < LOFT_GUIDE_EPS || sourceDepth < LOFT_GUIDE_EPS) {
79837
+ throw new Error("Loft.withGuideRails() station profiles must have positive bounds");
79838
+ }
79839
+ const sourceCenter = [(source.minX + source.maxX) / 2, (source.minY + source.maxY) / 2];
79840
+ const targetCenter = [(target.minX + target.maxX) / 2, (target.minY + target.maxY) / 2];
79841
+ return profile.scaleAround(sourceCenter, [(target.maxX - target.minX) / sourceWidth, (target.maxY - target.minY) / sourceDepth]).translate(targetCenter[0] - sourceCenter[0], targetCenter[1] - sourceCenter[1]);
79842
+ }
79843
+ function sketchBounds(profile) {
79844
+ const bounds = profile.bounds();
79845
+ return { minX: bounds.min[0], maxX: bounds.max[0], minY: bounds.min[1], maxY: bounds.max[1] };
79846
+ }
79847
+ function lerpBounds(a2, b, t) {
79848
+ return {
79849
+ minX: lerp(a2.minX, b.minX, t),
79850
+ maxX: lerp(a2.maxX, b.maxX, t),
79851
+ minY: lerp(a2.minY, b.minY, t),
79852
+ maxY: lerp(a2.maxY, b.maxY, t)
79853
+ };
79854
+ }
79855
+ function lerp(a2, b, t) {
79856
+ return a2 + (b - a2) * t;
79857
+ }
79858
+ function mapLoftPath2D(path2, label, mapper) {
79859
+ const points = sampleLoftPath2D(path2, label);
79860
+ return points.map((point2, index2) => {
79861
+ if (!Array.isArray(point2) || point2.length !== 2 || !point2.every(Number.isFinite)) {
79862
+ throw new Error(`${label} point ${index2} must be a finite [x, y] point`);
79863
+ }
79864
+ return mapper([point2[0], point2[1]]);
79865
+ });
79866
+ }
79867
+ function sampleLoftPath2D(path2, label) {
79868
+ if (Array.isArray(path2)) {
79869
+ if (path2.length < 2) throw new Error(`${label} requires at least two [x, y] points`);
79870
+ return path2;
79871
+ }
79872
+ if (!path2 || typeof path2 !== "object" || typeof path2.toPolyline !== "function") {
79873
+ throw new Error(`${label} requires a 2D path, solved constrained path, or [x, y] point array`);
79874
+ }
79875
+ const points = path2.toPolyline();
79876
+ if (!Array.isArray(points) || points.length < 2) throw new Error(`${label} path must produce at least two [x, y] points`);
79877
+ return points;
79878
+ }
79879
+ const Loft = {
79880
+ /** Create a loft station from a 2D profile and an axis position. */
79881
+ station(profile, position) {
79882
+ if (!Number.isFinite(position)) throw new Error("Loft.station position must be finite");
79883
+ return { profile, position };
79884
+ },
79885
+ /** Create a guide rail that constrains the section-local negative-X side. */
79886
+ leftRail(path2) {
79887
+ return { side: "left", path: path2 };
79888
+ },
79889
+ /** Create a guide rail that constrains the section-local positive-X side. */
79890
+ rightRail(path2) {
79891
+ return { side: "right", path: path2 };
79892
+ },
79893
+ /** Create a guide rail that constrains the section-local positive-Y side. */
79894
+ frontRail(path2) {
79895
+ return { side: "front", path: path2 };
79896
+ },
79897
+ /** Create a guide rail that constrains the section-local negative-Y side. */
79898
+ backRail(path2) {
79899
+ return { side: "back", path: path2 };
79900
+ },
79901
+ /** Create a guide rail that moves section centers along the loft. */
79902
+ centerRail(path2) {
79903
+ return { side: "center", path: path2 };
79904
+ },
79905
+ /**
79906
+ * Place a 2D guide path onto the XZ plane.
79907
+ *
79908
+ * The path's first coordinate becomes X and its second coordinate becomes Z.
79909
+ * Use this for left/right silhouette rails authored with `path()` or `constrainedSketch()`.
79910
+ */
79911
+ pathOnXz(path2, y2 = 0) {
79912
+ if (!Number.isFinite(y2)) throw new Error("Loft.pathOnXz y must be finite");
79913
+ return mapLoftPath2D(path2, "Loft.pathOnXz", ([x2, z2]) => [x2, y2, z2]);
79914
+ },
79915
+ /**
79916
+ * Place a 2D guide path onto the YZ plane.
79917
+ *
79918
+ * The path's first coordinate becomes Y and its second coordinate becomes Z.
79919
+ * Use this for front/back crown rails authored with `path()` or `constrainedSketch()`.
79920
+ */
79921
+ pathOnYz(path2, x2 = 0) {
79922
+ if (!Number.isFinite(x2)) throw new Error("Loft.pathOnYz x must be finite");
79923
+ return mapLoftPath2D(path2, "Loft.pathOnYz", ([y2, z2]) => [x2, y2, z2]);
79924
+ },
79925
+ /**
79926
+ * Place a 2D guide path onto the XY plane.
79927
+ *
79928
+ * The path's first coordinate becomes X and its second coordinate becomes Y.
79929
+ * Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
79930
+ */
79931
+ pathOnXy(path2, z2 = 0) {
79932
+ if (!Number.isFinite(z2)) throw new Error("Loft.pathOnXy z must be finite");
79933
+ return mapLoftPath2D(path2, "Loft.pathOnXy", ([x2, y2]) => [x2, y2, z2]);
79934
+ },
79935
+ /**
79936
+ * Loft through profile stations while forcing generated sections to follow guide rails.
79937
+ *
79938
+ * Stations define the cross-section family. Guide rails define the side or center
79939
+ * paths the loft must pass through. With opposite side rails, the section is scaled
79940
+ * to touch both rails. With one side rail, the section keeps its interpolated size
79941
+ * unless a center rail is also present.
79942
+ */
79943
+ withGuideRails(stations, rails, options = {}) {
79944
+ return loftWithGuideRails(stations, rails, options);
79945
+ }
79946
+ };
79476
79947
  let collectedHighlights = [];
79477
79948
  function resetHighlights() {
79478
79949
  collectedHighlights = [];
@@ -296941,6 +297412,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
296941
297412
  nurbsSurface,
296942
297413
  spline2d,
296943
297414
  spline3d,
297415
+ Loft,
296944
297416
  loft,
296945
297417
  loftAlongSpine,
296946
297418
  sweep,