forgecad 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AdminPage-uTtcSXtn.js → AdminPage-DX0mpSZT.js} +1 -1
- package/dist/assets/{BlogPage-DYJMjWx3.js → BlogPage-CI_P0_Pf.js} +1 -1
- package/dist/assets/{DocsPage-C58f0K5v.js → DocsPage-DLhIIZyJ.js} +3 -3
- package/dist/assets/EditorApp-BujZvuwX.js +12874 -0
- package/dist/assets/{EditorApp-DS0AIUrZ.css → EditorApp-DfFT2Dn8.css} +1 -0
- package/dist/assets/{EmbedViewer-CMXWA2LX.js → EmbedViewer-0S0qXKog.js} +2 -2
- package/dist/assets/{LandingPageProofDriven-CAu2OZFn.js → LandingPageProofDriven-O_yMtAri.js} +1 -1
- package/dist/assets/{PricingPage-BIgW7m3X.js → PricingPage-DGkX3Ahr.js} +1 -1
- package/dist/assets/{SettingsPage-N1l1tMXO.js → SettingsPage-DBsqTB_y.js} +82 -22
- package/dist/assets/{app-CFy7g5WP.js → app-BE2nD6Yz.js} +1246 -191
- package/dist/assets/cli/{render-BrVVdj_T.js → render-iP9qh475.js} +841 -586
- package/dist/assets/{evalWorker-c_SB9gg3.js → evalWorker-Ds5U4xtN.js} +2732 -112
- package/dist/assets/inspectWorker-Dll4eVyD.js +12620 -0
- package/dist/assets/{manifold-Dp6pvFr6.js → manifold-Bk26ViCr.js} +1 -1
- package/dist/assets/{manifold-CRoBhJKH.js → manifold-DjYsd7A_.js} +2 -2
- package/dist/assets/{manifold-Cjk7WhRs.js → manifold-sJ-axdXM.js} +1 -1
- package/dist/assets/{renderSceneState-3DfsSASX.js → renderSceneState-Bngp5MrQ.js} +1 -1
- package/dist/assets/{reportWorker-BLkuIoS8.js → reportWorker-CU8RZ4O0.js} +2715 -112
- package/dist/assets/{sectionPlaneMath-CykEnkvQ.js → sectionPlaneMath-BdTjyVfs.js} +3213 -252
- package/dist/cli/render.html +1 -1
- package/dist/docs/index.html +1 -1
- package/dist/docs-raw/AI/usage.md +7 -2
- package/dist/docs-raw/CLI.md +82 -53
- package/dist/docs-raw/beta-operations.md +9 -0
- package/dist/docs-raw/coding.md +1 -1
- package/dist/docs-raw/deployment.md +38 -23
- package/dist/docs-raw/generated/concepts.md +141 -7
- package/dist/docs-raw/generated/core.md +206 -1
- package/dist/docs-raw/generated/curves.md +97 -5
- package/dist/docs-raw/generated/lib.md +17 -1
- package/dist/docs-raw/generated/sketch.md +9 -1
- package/dist/docs-raw/generated/viewport.md +1 -1
- package/dist/docs-raw/guides/inspection-bundles.md +45 -16
- package/dist/docs-raw/platform/auth.md +2 -0
- package/dist/docs-raw/platform/google-oauth-setup.md +4 -0
- package/dist/docs-raw/runbook.md +3 -3
- package/dist/docs-raw/skills/forgecad-make-a-model.md +87 -8
- package/dist/docs-raw/skills/forgecad-prepare-prompt.md +14 -6
- package/dist/docs-raw/skills/forgecad-render-inspect.md +1 -1
- package/dist/docs-raw/skills/index.md +2 -2
- package/dist/index.html +1 -1
- package/dist/sitemap.xml +6 -6
- package/dist-cli/forgecad.js +8725 -4747
- package/dist-cli/forgecad.js.map +1 -1
- package/dist-skill/CONTEXT.md +375 -25
- package/dist-skill/docs/CLI.md +82 -53
- package/dist-skill/docs/generated/core.md +206 -1
- package/dist-skill/docs/generated/curves.md +97 -5
- package/dist-skill/docs/generated/lib.md +17 -1
- package/dist-skill/docs/generated/sketch.md +9 -1
- package/dist-skill/docs/generated/viewport.md +1 -1
- package/dist-skill/docs/guides/inspection-bundles.md +45 -16
- package/dist-skill/docs-dev/CLI.md +82 -53
- package/dist-skill/docs-dev/coding.md +1 -1
- package/dist-skill/docs-dev/generated/core.md +206 -1
- package/dist-skill/docs-dev/generated/curves.md +97 -5
- package/dist-skill/docs-dev/generated/lib.md +17 -1
- package/dist-skill/docs-dev/generated/sketch.md +9 -1
- package/dist-skill/docs-dev/generated/viewport.md +1 -1
- package/dist-skill/docs-dev/guides/inspection-bundles.md +45 -16
- package/dist-skill/library/forgecad-make-a-model/SKILL.md +87 -8
- package/dist-skill/library/forgecad-prepare-prompt/SKILL.md +14 -6
- package/dist-skill/library/forgecad-prepare-prompt/references/default-profiles.md +5 -3
- package/dist-skill/library/forgecad-prepare-prompt/references/master-prompt.md +7 -5
- package/dist-skill/library/forgecad-render-inspect/SKILL.md +1 -1
- package/examples/api/bolted-service-cover.forge.js +17 -0
- package/examples/api/cable-gland-anchor.forge.js +14 -0
- package/examples/api/captured-cartridge-guide.forge.js +14 -0
- package/examples/api/captured-linear-slide.forge.js +13 -0
- package/examples/api/clevis-pin-joint.forge.js +13 -0
- package/examples/api/datum-enclosure.forge.js +16 -0
- package/examples/api/guided-loft-olive-oil-bottle.forge.js +135 -0
- package/examples/api/hose-barb-port.forge.js +14 -0
- package/examples/api/intentional-overlap-overmold.forge.js +16 -0
- package/examples/api/knuckled-hinge-assembly.forge.js +15 -0
- package/examples/api/living-hinge-cover.forge.js +14 -0
- package/examples/api/pcb-terminal-block.forge.js +22 -0
- package/examples/api/pinned-lever-pivot-stack.forge.js +14 -0
- package/examples/api/retained-shaft-knob-stack.forge.js +15 -0
- package/examples/api/routed-tube-clip.forge.js +15 -0
- package/examples/api/seated-bearing-stack.forge.js +30 -0
- package/examples/api/snap-latch-cover.forge.js +14 -0
- package/examples/api/static-assembly-connectors.forge.js +14 -16
- package/examples/api/thumb-screw-clamp.forge.js +15 -0
- package/package.json +20 -2
- package/dist/assets/EditorApp-DNH1TEz1.js +0 -12729
|
@@ -721,7 +721,7 @@ function cloneSdfNode(node) {
|
|
|
721
721
|
}
|
|
722
722
|
}
|
|
723
723
|
const SHEET_METAL_EDGES = ["top", "right", "bottom", "left"];
|
|
724
|
-
const EPS$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1389
|
-
if (mode === "line" && targetRadialLen >= EPS$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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-
|
|
10529
|
+
const Module = (await import("./manifold-sJ-axdXM.js")).default;
|
|
10455
10530
|
performance.mark("manifold:imported");
|
|
10456
10531
|
const wasm = await Module();
|
|
10457
10532
|
wasm.setup();
|
|
@@ -10583,6 +10658,23 @@ let ManifoldShapeBackend = _ManifoldShapeBackend;
|
|
|
10583
10658
|
function wrapManifoldShapeBackend(manifold) {
|
|
10584
10659
|
return new ManifoldShapeBackend(manifold);
|
|
10585
10660
|
}
|
|
10661
|
+
function reconstructBackendFromMesh(mesh) {
|
|
10662
|
+
const wasm = getManifoldWasm();
|
|
10663
|
+
const wasmMesh = new wasm.Mesh({
|
|
10664
|
+
numProp: mesh.numProp,
|
|
10665
|
+
triVerts: mesh.triVerts,
|
|
10666
|
+
vertProperties: mesh.vertProperties,
|
|
10667
|
+
mergeFromVert: mesh.mergeFromVert.length > 0 ? mesh.mergeFromVert : void 0,
|
|
10668
|
+
mergeToVert: mesh.mergeToVert.length > 0 ? mesh.mergeToVert : void 0
|
|
10669
|
+
});
|
|
10670
|
+
let manifold;
|
|
10671
|
+
try {
|
|
10672
|
+
manifold = new wasm.Manifold(wasmMesh);
|
|
10673
|
+
} catch {
|
|
10674
|
+
manifold = wasm.Manifold.cube([0, 0, 0]);
|
|
10675
|
+
}
|
|
10676
|
+
return new ManifoldShapeBackend(manifold);
|
|
10677
|
+
}
|
|
10586
10678
|
function requireManifoldShapeBackend(backend, apiName = "requireManifoldShapeBackend()") {
|
|
10587
10679
|
if (isManifoldCapableBackend(backend)) {
|
|
10588
10680
|
return backend.requireManifold(apiName);
|
|
@@ -47289,10 +47381,8 @@ class PathBuilder {
|
|
|
47289
47381
|
if (radius <= 0) throw new Error("fillet: radius must be positive");
|
|
47290
47382
|
const n = this.segs.length;
|
|
47291
47383
|
if (n < 2) throw new Error("fillet: need at least 2 segments before a fillet");
|
|
47292
|
-
const prev = this.segs[n - 2];
|
|
47293
47384
|
const curr = this.segs[n - 1];
|
|
47294
|
-
|
|
47295
|
-
const { trimA, trimB, arcSeg } = this.computeFilletGeom(radius);
|
|
47385
|
+
const { trimA, arcSeg } = this.computeFilletGeom(radius);
|
|
47296
47386
|
if (!arcSeg) throw new Error("fillet: cannot fillet these segments (parallel or degenerate)");
|
|
47297
47387
|
this.trimLastSegEnd(n - 2, trimA[0], trimA[1]);
|
|
47298
47388
|
const trimmedSeg = { ...curr };
|
|
@@ -47364,7 +47454,6 @@ class PathBuilder {
|
|
|
47364
47454
|
}
|
|
47365
47455
|
getSegDirAt(seg, which) {
|
|
47366
47456
|
if (seg.kind === "line" || seg.kind === "move") {
|
|
47367
|
-
this.segs.length;
|
|
47368
47457
|
const idx = this.segs.indexOf(seg);
|
|
47369
47458
|
if (seg.kind === "line") {
|
|
47370
47459
|
let sx, sy;
|
|
@@ -47606,6 +47695,41 @@ class PathBuilder {
|
|
|
47606
47695
|
}
|
|
47607
47696
|
return pts;
|
|
47608
47697
|
}
|
|
47698
|
+
/**
|
|
47699
|
+
* Return the open path as a sampled 2D polyline.
|
|
47700
|
+
*
|
|
47701
|
+
* This is for construction geometry such as guide rails, measured centerlines,
|
|
47702
|
+
* and curve-driven helpers where the authored path should stay open instead of
|
|
47703
|
+
* becoming a filled sketch or stroked profile.
|
|
47704
|
+
*
|
|
47705
|
+
* **Example**
|
|
47706
|
+
*
|
|
47707
|
+
* ```ts
|
|
47708
|
+
* const rail = path()
|
|
47709
|
+
* .moveTo(24, 0)
|
|
47710
|
+
* .bezierTo(32, 44, 28, 92, 18, 120)
|
|
47711
|
+
* .toPolyline();
|
|
47712
|
+
* ```
|
|
47713
|
+
*
|
|
47714
|
+
* @returns A sampled open polyline.
|
|
47715
|
+
* @category Path Builder
|
|
47716
|
+
*/
|
|
47717
|
+
toPolyline() {
|
|
47718
|
+
const moveCount = this.segs.filter((seg) => seg.kind === "move").length;
|
|
47719
|
+
if (moveCount > 1) {
|
|
47720
|
+
throw new Error("path().toPolyline() supports one continuous open path. Use separate path() builders for separate rails.");
|
|
47721
|
+
}
|
|
47722
|
+
const pts = [];
|
|
47723
|
+
for (const point2 of this.tessellate()) {
|
|
47724
|
+
if (!point2.every(Number.isFinite)) throw new Error("path().toPolyline() produced a non-finite point");
|
|
47725
|
+
const previous = pts[pts.length - 1];
|
|
47726
|
+
if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) {
|
|
47727
|
+
pts.push(point2);
|
|
47728
|
+
}
|
|
47729
|
+
}
|
|
47730
|
+
if (pts.length < 2) throw new Error("path().toPolyline() needs at least 2 points");
|
|
47731
|
+
return pts;
|
|
47732
|
+
}
|
|
47609
47733
|
// ── Output ────────────────────────────────────────────────────────────────
|
|
47610
47734
|
/**
|
|
47611
47735
|
* Close the path and return a filled `Sketch`.
|
|
@@ -49463,7 +49587,7 @@ function spurGear(options) {
|
|
|
49463
49587
|
});
|
|
49464
49588
|
return attachGearMeta(shapeWithConnectors, meta2);
|
|
49465
49589
|
}
|
|
49466
|
-
function requirePositive$
|
|
49590
|
+
function requirePositive$8(scope, name, value) {
|
|
49467
49591
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
49468
49592
|
}
|
|
49469
49593
|
function requireOptionalBore(scope, boreDiameter, maxDiameter) {
|
|
@@ -49485,8 +49609,8 @@ function cutBore$1(shape, boreDiameter) {
|
|
|
49485
49609
|
return shape.subtract(cutter);
|
|
49486
49610
|
}
|
|
49487
49611
|
function gearBodyDisk(options) {
|
|
49488
|
-
requirePositive$
|
|
49489
|
-
requirePositive$
|
|
49612
|
+
requirePositive$8("gearBodyDisk", "outerRadius", options.outerRadius);
|
|
49613
|
+
requirePositive$8("gearBodyDisk", "faceWidth", options.faceWidth);
|
|
49490
49614
|
const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
|
|
49491
49615
|
const segments = resolveSegments(options.segments);
|
|
49492
49616
|
const outer = circle2d(options.outerRadius, segments);
|
|
@@ -49494,14 +49618,14 @@ function gearBodyDisk(options) {
|
|
|
49494
49618
|
return sketchExtrude(profile, options.faceWidth);
|
|
49495
49619
|
}
|
|
49496
49620
|
function gearBodyDiskWithHub(options) {
|
|
49497
|
-
requirePositive$
|
|
49621
|
+
requirePositive$8("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
|
|
49498
49622
|
if (options.hubDiameter >= options.outerRadius * 2) {
|
|
49499
49623
|
throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
|
|
49500
49624
|
}
|
|
49501
49625
|
const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
|
|
49502
49626
|
const base = gearBodyDisk({ ...options, boreDiameter: 0 });
|
|
49503
49627
|
const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
|
|
49504
|
-
requirePositive$
|
|
49628
|
+
requirePositive$8("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
|
|
49505
49629
|
const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
|
|
49506
49630
|
0,
|
|
49507
49631
|
0,
|
|
@@ -49510,11 +49634,11 @@ function gearBodyDiskWithHub(options) {
|
|
|
49510
49634
|
return cutBore$1(base.add(hub), bore);
|
|
49511
49635
|
}
|
|
49512
49636
|
function gearBodySpoked(options) {
|
|
49513
|
-
requirePositive$
|
|
49514
|
-
requirePositive$
|
|
49515
|
-
requirePositive$
|
|
49516
|
-
requirePositive$
|
|
49517
|
-
requirePositive$
|
|
49637
|
+
requirePositive$8("gearBodySpoked", "outerRadius", options.outerRadius);
|
|
49638
|
+
requirePositive$8("gearBodySpoked", "faceWidth", options.faceWidth);
|
|
49639
|
+
requirePositive$8("gearBodySpoked", "rimWidth", options.rimWidth);
|
|
49640
|
+
requirePositive$8("gearBodySpoked", "hubDiameter", options.hubDiameter);
|
|
49641
|
+
requirePositive$8("gearBodySpoked", "spokeWidth", options.spokeWidth);
|
|
49518
49642
|
if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
|
|
49519
49643
|
throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
|
|
49520
49644
|
}
|
|
@@ -49537,12 +49661,12 @@ function gearBodySpoked(options) {
|
|
|
49537
49661
|
}
|
|
49538
49662
|
function gearBodyFromProfile(profile, options) {
|
|
49539
49663
|
if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
|
|
49540
|
-
requirePositive$
|
|
49664
|
+
requirePositive$8("gearBodyFromProfile", "faceWidth", options.faceWidth);
|
|
49541
49665
|
const bore = options.boreDiameter ?? 0;
|
|
49542
49666
|
if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
|
|
49543
49667
|
return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
|
|
49544
49668
|
}
|
|
49545
|
-
function requirePositive$
|
|
49669
|
+
function requirePositive$7(scope, name, value) {
|
|
49546
49670
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
49547
49671
|
}
|
|
49548
49672
|
function requireFiniteAngle(scope, name, value) {
|
|
@@ -49604,7 +49728,7 @@ function buildSpurTeethRegion(options, name, faceWidth) {
|
|
|
49604
49728
|
}
|
|
49605
49729
|
function buildSolidArcRegion(options, name, faceWidth) {
|
|
49606
49730
|
const scope = "driveWheel.addSolidArcBetween";
|
|
49607
|
-
requirePositive$
|
|
49731
|
+
requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
49608
49732
|
const innerRadius = options.innerRadius ?? 0;
|
|
49609
49733
|
if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
49610
49734
|
if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
|
|
@@ -49670,7 +49794,7 @@ class DriveWheelBuilder {
|
|
|
49670
49794
|
__publicField(this, "boreDiameter");
|
|
49671
49795
|
__publicField(this, "regions", []);
|
|
49672
49796
|
if (options.body !== void 0 && !(options.body instanceof Shape)) throw new Error('driveWheel: "body" must be a Shape');
|
|
49673
|
-
if (options.faceWidth !== void 0) requirePositive$
|
|
49797
|
+
if (options.faceWidth !== void 0) requirePositive$7("driveWheel", "faceWidth", options.faceWidth);
|
|
49674
49798
|
const boreDiameter = options.boreDiameter ?? 0;
|
|
49675
49799
|
if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
|
|
49676
49800
|
this.body = options.body;
|
|
@@ -49705,7 +49829,7 @@ class DriveWheelBuilder {
|
|
|
49705
49829
|
if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
|
|
49706
49830
|
throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
49707
49831
|
}
|
|
49708
|
-
if (options.outerRadius !== void 0) requirePositive$
|
|
49832
|
+
if (options.outerRadius !== void 0) requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
49709
49833
|
this.regions.push({
|
|
49710
49834
|
shape: shape.clone(),
|
|
49711
49835
|
meta: {
|
|
@@ -49771,7 +49895,7 @@ class DriveWheelBuilder {
|
|
|
49771
49895
|
resolveFaceWidth(scope, localFaceWidth) {
|
|
49772
49896
|
const faceWidth = localFaceWidth ?? this.faceWidth;
|
|
49773
49897
|
if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
|
|
49774
|
-
requirePositive$
|
|
49898
|
+
requirePositive$7(scope, "faceWidth", faceWidth);
|
|
49775
49899
|
if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
|
|
49776
49900
|
throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
|
|
49777
49901
|
}
|
|
@@ -50924,6 +51048,1867 @@ function washer(size, options) {
|
|
|
50924
51048
|
const bore = cylinder(dims.t + 1, dims.id / 2, void 0, segs);
|
|
50925
51049
|
return outer.subtract(bore);
|
|
50926
51050
|
}
|
|
51051
|
+
function requirePositive$6(value, name) {
|
|
51052
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
|
|
51053
|
+
return value;
|
|
51054
|
+
}
|
|
51055
|
+
function requireNonNegative(value, name) {
|
|
51056
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative finite number`);
|
|
51057
|
+
return value;
|
|
51058
|
+
}
|
|
51059
|
+
function metricWasherSizeForPin(pinDiameter) {
|
|
51060
|
+
if (pinDiameter <= 2) return "M2";
|
|
51061
|
+
if (pinDiameter <= 2.5) return "M2.5";
|
|
51062
|
+
if (pinDiameter <= 3) return "M3";
|
|
51063
|
+
if (pinDiameter <= 4) return "M4";
|
|
51064
|
+
if (pinDiameter <= 5) return "M5";
|
|
51065
|
+
if (pinDiameter <= 6) return "M6";
|
|
51066
|
+
if (pinDiameter <= 8) return "M8";
|
|
51067
|
+
return "M10";
|
|
51068
|
+
}
|
|
51069
|
+
function cylinderAlongX(length4, radius, xCenter, segments) {
|
|
51070
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([1, 0, 0]).translate(xCenter - length4 / 2, 0, 0);
|
|
51071
|
+
}
|
|
51072
|
+
function tubeAlongX(length4, outerRadius, innerRadius, xCenter, segments) {
|
|
51073
|
+
return cylinderAlongX(length4, outerRadius, xCenter, segments).subtract(cylinderAlongX(length4 + 0.4, innerRadius, xCenter, segments));
|
|
51074
|
+
}
|
|
51075
|
+
function cylinderAlongY(length4, radius, yCenter, segments) {
|
|
51076
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([0, 1, 0]).translate(0, yCenter - length4 / 2, 0);
|
|
51077
|
+
}
|
|
51078
|
+
function tubeAlongY(length4, outerRadius, innerRadius, yCenter, segments) {
|
|
51079
|
+
return cylinderAlongY(length4, outerRadius, yCenter, segments).subtract(cylinderAlongY(length4 + 0.4, innerRadius, yCenter, segments));
|
|
51080
|
+
}
|
|
51081
|
+
function tubeAlongZ(height, outerRadius, innerRadius, segments) {
|
|
51082
|
+
return cylinder(height, outerRadius, void 0, segments).subtract(
|
|
51083
|
+
cylinder(height + 0.4, innerRadius, void 0, segments).translate(0, 0, -0.2)
|
|
51084
|
+
);
|
|
51085
|
+
}
|
|
51086
|
+
function washerAlongX(size, xCenter, segments) {
|
|
51087
|
+
const dims = WASHER_TABLE[size];
|
|
51088
|
+
return washer(size, { segments }).pointAlong([1, 0, 0]).translate(xCenter - dims.t / 2, 0, 0);
|
|
51089
|
+
}
|
|
51090
|
+
function resolveBoltInset(raw, fallback) {
|
|
51091
|
+
if (raw === void 0) return [fallback, fallback];
|
|
51092
|
+
if (typeof raw === "number") return [requirePositive$6(raw, "boltInset"), requirePositive$6(raw, "boltInset")];
|
|
51093
|
+
if (raw.length !== 2) throw new Error("boltInset tuple must be [x, y]");
|
|
51094
|
+
return [requirePositive$6(raw[0], "boltInset[0]"), requirePositive$6(raw[1], "boltInset[1]")];
|
|
51095
|
+
}
|
|
51096
|
+
function validateBoltPositionsForServiceCover(args) {
|
|
51097
|
+
args.positions.forEach(([x2, y2], index2) => {
|
|
51098
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
51099
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] must contain finite numbers`);
|
|
51100
|
+
}
|
|
51101
|
+
if (Math.abs(x2) + args.holeRadius >= args.coverWidth / 2 || Math.abs(y2) + args.holeRadius >= args.coverDepth / 2) {
|
|
51102
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] is too close to the cover edge`);
|
|
51103
|
+
}
|
|
51104
|
+
const overlapsOpening = Math.abs(x2) - args.holeRadius <= args.openingWidth / 2 && Math.abs(y2) - args.holeRadius <= args.openingDepth / 2;
|
|
51105
|
+
if (overlapsOpening) {
|
|
51106
|
+
throw new Error(
|
|
51107
|
+
`boltedServiceCover: boltPositions[${index2}] lands over the service opening; decrease boltInset, increase ledgeWidth, or provide a smaller opening`
|
|
51108
|
+
);
|
|
51109
|
+
}
|
|
51110
|
+
});
|
|
51111
|
+
}
|
|
51112
|
+
function placeCutterAtPositions(cutter, positions, z2) {
|
|
51113
|
+
return union(...positions.map(([x2, y2]) => cutter.translate(x2, y2, z2)));
|
|
51114
|
+
}
|
|
51115
|
+
function boltedServiceCover(options) {
|
|
51116
|
+
const width = requirePositive$6(options.width, "width");
|
|
51117
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51118
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 3, "coverThickness");
|
|
51119
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 8, "parentThickness");
|
|
51120
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
51121
|
+
const gasketThickness = Math.max(0, options.gasketThickness ?? 0.8);
|
|
51122
|
+
const gasketInset = Math.max(0, options.gasketInset ?? 2);
|
|
51123
|
+
const screwSize = options.screwSize ?? "M4";
|
|
51124
|
+
const segments = options.segments ?? 36;
|
|
51125
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51126
|
+
if (!sizeData) throw new Error(`boltedServiceCover: unsupported screwSize "${screwSize}"`);
|
|
51127
|
+
const screwLength = requirePositive$6(
|
|
51128
|
+
options.screwLength ?? parentThickness + gasketThickness + coverThickness + 4,
|
|
51129
|
+
"screwLength"
|
|
51130
|
+
);
|
|
51131
|
+
const coverFit = options.coverFit ?? "normal";
|
|
51132
|
+
const counterboreEnabled = options.counterbore ?? true;
|
|
51133
|
+
const [insetX, insetY] = resolveBoltInset(options.boltInset, Math.max(ledgeWidth * 0.65, sizeData.head * 0.75));
|
|
51134
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
51135
|
+
throw new Error("boltedServiceCover: boltInset leaves no room for a four-corner bolt pattern");
|
|
51136
|
+
}
|
|
51137
|
+
const boltPositions = options.boltPositions ?? [
|
|
51138
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
51139
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
51140
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
51141
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
51142
|
+
];
|
|
51143
|
+
if (boltPositions.length === 0) throw new Error("boltedServiceCover: boltPositions must contain at least one point");
|
|
51144
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
51145
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
51146
|
+
const openingWidth = Math.max(1, width - ledgeWidth * 2);
|
|
51147
|
+
const openingDepth = Math.max(1, depth - ledgeWidth * 2);
|
|
51148
|
+
validateBoltPositionsForServiceCover({
|
|
51149
|
+
positions: boltPositions,
|
|
51150
|
+
coverWidth: width,
|
|
51151
|
+
coverDepth: depth,
|
|
51152
|
+
openingWidth,
|
|
51153
|
+
openingDepth,
|
|
51154
|
+
holeRadius: sizeData[coverFit] / 2
|
|
51155
|
+
});
|
|
51156
|
+
const coverHole = fastenerHole({
|
|
51157
|
+
size: screwSize,
|
|
51158
|
+
fit: coverFit,
|
|
51159
|
+
depth: coverThickness + 0.6,
|
|
51160
|
+
center: true,
|
|
51161
|
+
segments,
|
|
51162
|
+
...counterboreEnabled ? { counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.4)) } } : {}
|
|
51163
|
+
});
|
|
51164
|
+
const parentTap = fastenerHole({ size: screwSize, fit: "tap", depth: parentThickness + 0.6, center: true, segments });
|
|
51165
|
+
const parentThreadEnvelope = fastenerHole({
|
|
51166
|
+
size: screwSize,
|
|
51167
|
+
fit: "close",
|
|
51168
|
+
depth: parentThickness + 0.6,
|
|
51169
|
+
center: true,
|
|
51170
|
+
segments
|
|
51171
|
+
});
|
|
51172
|
+
const openingCutter = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
51173
|
+
const parentTappedPattern = placeCutterAtPositions(parentTap, boltPositions, parentThickness / 2);
|
|
51174
|
+
const parentThreadEnvelopePattern = placeCutterAtPositions(parentThreadEnvelope, boltPositions, parentThickness / 2);
|
|
51175
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(openingCutter).subtract(parentThreadEnvelopePattern).color("#4b5563");
|
|
51176
|
+
let coverBlank = box(width, depth, coverThickness);
|
|
51177
|
+
if (options.pullTabs ?? true) {
|
|
51178
|
+
const tabWidth = Math.min(width * 0.18, Math.max(sizeData.head * 1.6, 12));
|
|
51179
|
+
const tabDepth = Math.max(4, coverThickness * 1.4);
|
|
51180
|
+
const tabOverlap = Math.min(0.5, tabDepth * 0.25);
|
|
51181
|
+
const tabY = -depth / 2 - tabDepth / 2 + tabOverlap;
|
|
51182
|
+
const tabX = width * 0.23;
|
|
51183
|
+
coverBlank = union(
|
|
51184
|
+
coverBlank,
|
|
51185
|
+
box(tabWidth, tabDepth, coverThickness).translate(-tabX, tabY, 0),
|
|
51186
|
+
box(tabWidth, tabDepth, coverThickness).translate(tabX, tabY, 0)
|
|
51187
|
+
);
|
|
51188
|
+
}
|
|
51189
|
+
const coverClearancePattern = placeCutterAtPositions(coverHole, boltPositions, coverThickness / 2);
|
|
51190
|
+
const cover = coverBlank.subtract(coverClearancePattern).translate(0, 0, parentThickness + gasketThickness).color("#334155");
|
|
51191
|
+
const gasket = gasketThickness > 0 ? box(Math.max(1, width - gasketInset * 2), Math.max(1, depth - gasketInset * 2), gasketThickness).subtract(placeCutterAtPositions(coverHole, boltPositions, gasketThickness / 2)).translate(0, 0, parentThickness).color("#111827") : null;
|
|
51192
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
51193
|
+
const screwOriginZ = parentThickness + gasketThickness + coverThickness;
|
|
51194
|
+
const screws = boltPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
51195
|
+
const parts = [
|
|
51196
|
+
{ name: "service cover parent ledge with threaded hole envelopes", shape: parent },
|
|
51197
|
+
...gasket ? [{ name: "service cover gasket seated on ledge", shape: gasket }] : [],
|
|
51198
|
+
{ name: "bolted service cover plate with fused pull tabs", shape: cover },
|
|
51199
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} cover screw ${index2 + 1}`, shape }))
|
|
51200
|
+
];
|
|
51201
|
+
return {
|
|
51202
|
+
parts,
|
|
51203
|
+
parent,
|
|
51204
|
+
cover,
|
|
51205
|
+
gasket,
|
|
51206
|
+
screws,
|
|
51207
|
+
boltPositions,
|
|
51208
|
+
cutters: {
|
|
51209
|
+
coverClearance: coverClearancePattern,
|
|
51210
|
+
parentTapped: parentTappedPattern,
|
|
51211
|
+
parentThreadEnvelope: parentThreadEnvelopePattern
|
|
51212
|
+
},
|
|
51213
|
+
dims: {
|
|
51214
|
+
width,
|
|
51215
|
+
depth,
|
|
51216
|
+
coverThickness,
|
|
51217
|
+
parentThickness,
|
|
51218
|
+
ledgeWidth,
|
|
51219
|
+
gasketThickness,
|
|
51220
|
+
screwSize,
|
|
51221
|
+
screwLength,
|
|
51222
|
+
clearanceDia: sizeData[coverFit],
|
|
51223
|
+
tapDia: sizeData.tap,
|
|
51224
|
+
threadEnvelopeDia: sizeData.close
|
|
51225
|
+
}
|
|
51226
|
+
};
|
|
51227
|
+
}
|
|
51228
|
+
function datumEnclosureAssembly(options) {
|
|
51229
|
+
const width = requirePositive$6(options.width, "width");
|
|
51230
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51231
|
+
const height = requirePositive$6(options.height, "height");
|
|
51232
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.4, "wallThickness");
|
|
51233
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? wallThickness, "baseThickness");
|
|
51234
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
51235
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? Math.max(3.6, wallThickness * 1.35), "ledgeWidth");
|
|
51236
|
+
const gasketThickness = requireNonNegative(options.gasketThickness ?? 0.8, "gasketThickness");
|
|
51237
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
51238
|
+
const screwSize = options.screwSize ?? "M3";
|
|
51239
|
+
const coverFit = options.coverFit ?? "normal";
|
|
51240
|
+
const segments = options.segments ?? 32;
|
|
51241
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51242
|
+
if (!sizeData) throw new Error(`datumEnclosureAssembly: unsupported screwSize "${screwSize}"`);
|
|
51243
|
+
const innerWidth = width - wallThickness * 2;
|
|
51244
|
+
const innerDepth = depth - wallThickness * 2;
|
|
51245
|
+
if (innerWidth <= ledgeWidth * 2 + 8 || innerDepth <= ledgeWidth * 2 + 8) {
|
|
51246
|
+
throw new Error("datumEnclosureAssembly: wallThickness and ledgeWidth leave too little internal opening");
|
|
51247
|
+
}
|
|
51248
|
+
if (height <= baseThickness + coverThickness + 4) {
|
|
51249
|
+
throw new Error("datumEnclosureAssembly: height must leave room for internal ribs and standoffs");
|
|
51250
|
+
}
|
|
51251
|
+
const standoffDiameter = requirePositive$6(
|
|
51252
|
+
options.standoffDiameter ?? Math.max(sizeData.head * 1.65, sizeData.close * 2.2),
|
|
51253
|
+
"standoffDiameter"
|
|
51254
|
+
);
|
|
51255
|
+
const minInset = wallThickness + Math.max(ledgeWidth, standoffDiameter / 2 + 1.2);
|
|
51256
|
+
const [insetX, insetY] = resolveBoltInset(options.screwInset, minInset);
|
|
51257
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
51258
|
+
throw new Error("datumEnclosureAssembly: screwInset leaves no room for the standoff datum");
|
|
51259
|
+
}
|
|
51260
|
+
const screwPositions = options.screwPositions ?? [
|
|
51261
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
51262
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
51263
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
51264
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
51265
|
+
];
|
|
51266
|
+
if (screwPositions.length === 0) throw new Error("datumEnclosureAssembly: screwPositions must contain at least one point");
|
|
51267
|
+
for (const [index2, [x2, y2]] of screwPositions.entries()) {
|
|
51268
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
51269
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] must contain finite numbers`);
|
|
51270
|
+
}
|
|
51271
|
+
if (Math.abs(x2) + standoffDiameter / 2 > innerWidth / 2 || Math.abs(y2) + standoffDiameter / 2 > innerDepth / 2) {
|
|
51272
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] does not fit inside the enclosure walls`);
|
|
51273
|
+
}
|
|
51274
|
+
}
|
|
51275
|
+
const ribHeight = requirePositive$6(options.ribHeight ?? Math.min(height * 0.24, Math.max(2.4, baseThickness * 1.4)), "ribHeight");
|
|
51276
|
+
const ribThickness = requirePositive$6(options.ribThickness ?? Math.max(1.2, wallThickness * 0.75), "ribThickness");
|
|
51277
|
+
const portWidth = requirePositive$6(options.portWidth ?? Math.min(innerWidth * 0.28, Math.max(12, width * 0.16)), "portWidth");
|
|
51278
|
+
const portHeight = requirePositive$6(options.portHeight ?? Math.min(height * 0.42, Math.max(5, height * 0.28)), "portHeight");
|
|
51279
|
+
if (portWidth >= innerWidth - ledgeWidth * 2) {
|
|
51280
|
+
throw new Error("datumEnclosureAssembly: portWidth must fit between internal ledges and standoffs");
|
|
51281
|
+
}
|
|
51282
|
+
if (portHeight >= height - baseThickness - 1) {
|
|
51283
|
+
throw new Error("datumEnclosureAssembly: portHeight must leave material above and below the service port");
|
|
51284
|
+
}
|
|
51285
|
+
const screwLength = requirePositive$6(
|
|
51286
|
+
options.screwLength ?? coverThickness + gasketThickness + Math.max(6, height * 0.45),
|
|
51287
|
+
"screwLength"
|
|
51288
|
+
);
|
|
51289
|
+
const coverHole = fastenerHole({
|
|
51290
|
+
size: screwSize,
|
|
51291
|
+
fit: coverFit,
|
|
51292
|
+
depth: coverThickness + 0.6,
|
|
51293
|
+
center: true,
|
|
51294
|
+
segments,
|
|
51295
|
+
counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.35)) }
|
|
51296
|
+
});
|
|
51297
|
+
const standoffTap = fastenerHole({ size: screwSize, fit: "tap", depth: height + 0.8, center: true, segments });
|
|
51298
|
+
const standoffThreadEnvelope = fastenerHole({ size: screwSize, fit: "close", depth: height + 0.8, center: true, segments });
|
|
51299
|
+
const coverClearance = placeCutterAtPositions(coverHole, screwPositions, coverThickness / 2);
|
|
51300
|
+
const standoffTappedPattern = placeCutterAtPositions(standoffTap, screwPositions, height / 2);
|
|
51301
|
+
const standoffThreadEnvelopePattern = placeCutterAtPositions(standoffThreadEnvelope, screwPositions, height / 2);
|
|
51302
|
+
const fuseOverlap = Math.min(0.06, Math.max(0.02, wallThickness * 0.02));
|
|
51303
|
+
const ledgeThickness = Math.min(Math.max(1.1, coverThickness * 0.45), height * 0.2);
|
|
51304
|
+
const sideX = width / 2 - wallThickness / 2;
|
|
51305
|
+
const sideY = depth / 2 - wallThickness / 2;
|
|
51306
|
+
const ledgeZ = height - ledgeThickness;
|
|
51307
|
+
const baseSolids = [
|
|
51308
|
+
box(width, depth, baseThickness),
|
|
51309
|
+
box(wallThickness, depth, height).translate(sideX, 0, 0),
|
|
51310
|
+
box(wallThickness, depth, height).translate(-sideX, 0, 0),
|
|
51311
|
+
box(width, wallThickness, height).translate(0, sideY, 0),
|
|
51312
|
+
box(width, wallThickness, height).translate(0, -sideY, 0),
|
|
51313
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(-width / 2 + wallThickness + ledgeWidth / 2, 0, ledgeZ),
|
|
51314
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(width / 2 - wallThickness - ledgeWidth / 2, 0, ledgeZ),
|
|
51315
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, -depth / 2 + wallThickness + ledgeWidth / 2, ledgeZ),
|
|
51316
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, depth / 2 - wallThickness - ledgeWidth / 2, ledgeZ),
|
|
51317
|
+
box(Math.max(1, innerWidth - standoffDiameter * 1.8), ribThickness, ribHeight + fuseOverlap).translate(
|
|
51318
|
+
0,
|
|
51319
|
+
0,
|
|
51320
|
+
baseThickness - fuseOverlap
|
|
51321
|
+
),
|
|
51322
|
+
box(ribThickness, Math.max(1, innerDepth - standoffDiameter * 1.8), ribHeight + fuseOverlap).translate(
|
|
51323
|
+
0,
|
|
51324
|
+
0,
|
|
51325
|
+
baseThickness - fuseOverlap
|
|
51326
|
+
),
|
|
51327
|
+
...screwPositions.map(
|
|
51328
|
+
([x2, y2]) => cylinder(height - baseThickness + fuseOverlap, standoffDiameter / 2, void 0, segments).translate(
|
|
51329
|
+
x2,
|
|
51330
|
+
y2,
|
|
51331
|
+
baseThickness - fuseOverlap
|
|
51332
|
+
)
|
|
51333
|
+
)
|
|
51334
|
+
];
|
|
51335
|
+
const servicePort = box(portWidth, wallThickness + 1, portHeight).translate(
|
|
51336
|
+
0,
|
|
51337
|
+
-depth / 2 + wallThickness / 2,
|
|
51338
|
+
baseThickness + Math.max(0.8, (height - baseThickness - portHeight) * 0.35)
|
|
51339
|
+
);
|
|
51340
|
+
const base = union(...baseSolids).subtract(standoffThreadEnvelopePattern).subtract(servicePort).color("#475569");
|
|
51341
|
+
const gasketFrameCutter = box(Math.max(1, width - ledgeWidth * 2), Math.max(1, depth - ledgeWidth * 2), gasketThickness + 0.6).translate(
|
|
51342
|
+
0,
|
|
51343
|
+
0,
|
|
51344
|
+
-0.3
|
|
51345
|
+
);
|
|
51346
|
+
const gasket = gasketThickness > 0 ? box(width, depth, gasketThickness).subtract(gasketFrameCutter).subtract(placeCutterAtPositions(coverHole, screwPositions, gasketThickness / 2)).translate(0, 0, height + faceClearance).color("#111827") : null;
|
|
51347
|
+
const coverZ = height + faceClearance + (gasket ? gasketThickness + faceClearance : 0);
|
|
51348
|
+
const cover = box(width, depth, coverThickness).subtract(coverClearance).translate(0, 0, coverZ).color("#334155");
|
|
51349
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
51350
|
+
const screwOriginZ = coverZ + coverThickness;
|
|
51351
|
+
const screws = screwPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
51352
|
+
const parts = [
|
|
51353
|
+
{ name: "datum enclosure base tray with walls ribs standoffs and service port", shape: base },
|
|
51354
|
+
...gasket ? [{ name: "datum enclosure gasket seated on continuous ledge", shape: gasket }] : [],
|
|
51355
|
+
{ name: "datum enclosure cover plate with matched screw pattern", shape: cover },
|
|
51356
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} enclosure screw ${index2 + 1}`, shape }))
|
|
51357
|
+
];
|
|
51358
|
+
return {
|
|
51359
|
+
parts,
|
|
51360
|
+
base,
|
|
51361
|
+
cover,
|
|
51362
|
+
gasket,
|
|
51363
|
+
screws,
|
|
51364
|
+
screwPositions,
|
|
51365
|
+
cutters: {
|
|
51366
|
+
coverClearance,
|
|
51367
|
+
standoffTapped: standoffTappedPattern,
|
|
51368
|
+
standoffThreadEnvelope: standoffThreadEnvelopePattern,
|
|
51369
|
+
servicePort
|
|
51370
|
+
},
|
|
51371
|
+
dims: {
|
|
51372
|
+
width,
|
|
51373
|
+
depth,
|
|
51374
|
+
height,
|
|
51375
|
+
innerWidth,
|
|
51376
|
+
innerDepth,
|
|
51377
|
+
wallThickness,
|
|
51378
|
+
baseThickness,
|
|
51379
|
+
coverThickness,
|
|
51380
|
+
ledgeWidth,
|
|
51381
|
+
gasketThickness,
|
|
51382
|
+
faceClearance,
|
|
51383
|
+
screwSize,
|
|
51384
|
+
screwLength,
|
|
51385
|
+
standoffDiameter,
|
|
51386
|
+
ribHeight,
|
|
51387
|
+
ribThickness,
|
|
51388
|
+
portWidth,
|
|
51389
|
+
portHeight,
|
|
51390
|
+
clearanceDia: sizeData[coverFit],
|
|
51391
|
+
tapDia: sizeData.tap,
|
|
51392
|
+
threadEnvelopeDia: sizeData.close
|
|
51393
|
+
}
|
|
51394
|
+
};
|
|
51395
|
+
}
|
|
51396
|
+
function snapLatchCoverAssembly(options) {
|
|
51397
|
+
const width = requirePositive$6(options.width, "width");
|
|
51398
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51399
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
51400
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 6, "parentThickness");
|
|
51401
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
51402
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.25, "runningClearance");
|
|
51403
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
51404
|
+
const latchWidth = requirePositive$6(options.latchWidth ?? Math.min(width * 0.22, Math.max(12, width * 0.16)), "latchWidth");
|
|
51405
|
+
const latchThickness = requirePositive$6(options.latchThickness ?? 1.6, "latchThickness");
|
|
51406
|
+
const hookThrow = requirePositive$6(options.hookThrow ?? 3.2, "hookThrow");
|
|
51407
|
+
const hookThickness = requirePositive$6(options.hookThickness ?? 1.6, "hookThickness");
|
|
51408
|
+
const openingWidth = width - ledgeWidth * 2;
|
|
51409
|
+
const openingDepth = depth - ledgeWidth * 2;
|
|
51410
|
+
if (openingWidth <= Math.max(8, latchWidth * 0.8) || openingDepth <= 8) {
|
|
51411
|
+
throw new Error("snapLatchCoverAssembly: ledgeWidth leaves too little service opening under the cover");
|
|
51412
|
+
}
|
|
51413
|
+
if (latchWidth >= openingWidth) {
|
|
51414
|
+
throw new Error("snapLatchCoverAssembly: latchWidth must fit along the receiver opening");
|
|
51415
|
+
}
|
|
51416
|
+
if (latchThickness + runningClearance * 2 >= ledgeWidth) {
|
|
51417
|
+
throw new Error("snapLatchCoverAssembly: latchThickness and clearance must fit inside the receiver ledge");
|
|
51418
|
+
}
|
|
51419
|
+
if (hookThrow + latchThickness / 2 + runningClearance >= ledgeWidth * 1.5) {
|
|
51420
|
+
throw new Error("snapLatchCoverAssembly: hookThrow is too large for the available underside catch land");
|
|
51421
|
+
}
|
|
51422
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
51423
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
51424
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
51425
|
+
const hookClearance = Math.min(0.08, runningClearance * 0.32);
|
|
51426
|
+
const coverMinZ = parentThickness + faceClearance;
|
|
51427
|
+
const stemMinZ = -hookClearance - hookThickness;
|
|
51428
|
+
const stemHeight = coverMinZ + fuseOverlap - stemMinZ;
|
|
51429
|
+
const slotY = openingDepth / 2 + ledgeWidth / 2;
|
|
51430
|
+
const latchWindow = (sign2) => box(latchWidth + runningClearance * 2, latchThickness + runningClearance * 2, parentThickness + 0.8).translate(
|
|
51431
|
+
0,
|
|
51432
|
+
sign2 * slotY,
|
|
51433
|
+
-0.4
|
|
51434
|
+
);
|
|
51435
|
+
const latchWindows = union(latchWindow(1), latchWindow(-1));
|
|
51436
|
+
const serviceOpening = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
51437
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(serviceOpening).subtract(latchWindows).color("#475569");
|
|
51438
|
+
const coverPlate = box(width, depth, coverThickness).translate(0, 0, coverMinZ);
|
|
51439
|
+
const snapHook = (sign2) => {
|
|
51440
|
+
const y2 = sign2 * slotY;
|
|
51441
|
+
const stem = box(latchWidth, latchThickness, stemHeight).translate(0, y2, stemMinZ);
|
|
51442
|
+
const barb = box(latchWidth, latchThickness + hookThrow, hookThickness).translate(
|
|
51443
|
+
0,
|
|
51444
|
+
y2 + sign2 * (hookThrow / 2),
|
|
51445
|
+
stemMinZ
|
|
51446
|
+
);
|
|
51447
|
+
const rootRib = box(latchWidth, Math.max(latchThickness, hookThrow * 0.55), coverThickness * 0.65).translate(
|
|
51448
|
+
0,
|
|
51449
|
+
y2 - sign2 * (ledgeWidth * 0.18),
|
|
51450
|
+
coverMinZ
|
|
51451
|
+
);
|
|
51452
|
+
return union(stem, barb, rootRib);
|
|
51453
|
+
};
|
|
51454
|
+
const cover = union(coverPlate, snapHook(1), snapHook(-1)).color("#111827");
|
|
51455
|
+
const parts = [
|
|
51456
|
+
{ name: "snap cover receiver frame with latch windows and catch lands", shape: parent },
|
|
51457
|
+
{ name: "one-piece snap cover with fused hooks and underside barbs", shape: cover }
|
|
51458
|
+
];
|
|
51459
|
+
return {
|
|
51460
|
+
parts,
|
|
51461
|
+
parent,
|
|
51462
|
+
cover,
|
|
51463
|
+
cutters: {
|
|
51464
|
+
serviceOpening,
|
|
51465
|
+
latchWindows
|
|
51466
|
+
},
|
|
51467
|
+
dims: {
|
|
51468
|
+
width,
|
|
51469
|
+
depth,
|
|
51470
|
+
parentWidth,
|
|
51471
|
+
parentDepth,
|
|
51472
|
+
openingWidth,
|
|
51473
|
+
openingDepth,
|
|
51474
|
+
coverThickness,
|
|
51475
|
+
parentThickness,
|
|
51476
|
+
ledgeWidth,
|
|
51477
|
+
latchWidth,
|
|
51478
|
+
latchThickness,
|
|
51479
|
+
hookThrow,
|
|
51480
|
+
hookThickness,
|
|
51481
|
+
runningClearance,
|
|
51482
|
+
faceClearance
|
|
51483
|
+
}
|
|
51484
|
+
};
|
|
51485
|
+
}
|
|
51486
|
+
function pinnedLeverAssembly(options) {
|
|
51487
|
+
const armLength = requirePositive$6(options.armLength, "armLength");
|
|
51488
|
+
const armWidth = requirePositive$6(options.armWidth ?? 10, "armWidth");
|
|
51489
|
+
const leverThickness = requirePositive$6(options.leverThickness ?? 5, "leverThickness");
|
|
51490
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 5, "pinDiameter");
|
|
51491
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
51492
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51493
|
+
const hubRadius = requirePositive$6(options.hubRadius ?? Math.max(armWidth * 0.85, pinDiameter * 1.8), "hubRadius");
|
|
51494
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(6, pinDiameter * 1.4), "supportThickness");
|
|
51495
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? hubRadius * 2 + 18, "supportWidth");
|
|
51496
|
+
const supportDepth = requirePositive$6(options.supportDepth ?? Math.max(armWidth + 18, hubRadius * 2 + 10), "supportDepth");
|
|
51497
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(pinDiameter);
|
|
51498
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
51499
|
+
if (!washerDims) throw new Error(`pinnedLeverAssembly: unsupported washerSize "${washerSize}"`);
|
|
51500
|
+
if (washerDims.id <= pinDiameter) {
|
|
51501
|
+
throw new Error(`pinnedLeverAssembly: ${washerSize} washer inner diameter is too small for a ${pinDiameter} mm pin`);
|
|
51502
|
+
}
|
|
51503
|
+
if (hubRadius <= boreDiameter / 2 + Math.max(1, pinDiameter * 0.25)) {
|
|
51504
|
+
throw new Error("pinnedLeverAssembly: hubRadius leaves too little material around the pivot bore");
|
|
51505
|
+
}
|
|
51506
|
+
if (supportWidth <= boreDiameter + 4 || supportDepth <= boreDiameter + 4) {
|
|
51507
|
+
throw new Error("pinnedLeverAssembly: support dimensions leave too little material around the pivot bore");
|
|
51508
|
+
}
|
|
51509
|
+
const segments = options.segments ?? 40;
|
|
51510
|
+
const gripLength = requirePositive$6(options.gripLength ?? Math.min(armLength * 0.32, Math.max(16, armWidth * 2.4)), "gripLength");
|
|
51511
|
+
const gripWidth = requirePositive$6(options.gripWidth ?? armWidth * 1.55, "gripWidth");
|
|
51512
|
+
if (gripLength >= armLength) throw new Error("pinnedLeverAssembly: gripLength must be shorter than armLength");
|
|
51513
|
+
const armOverlap = Math.min(hubRadius * 0.65, armLength * 0.25);
|
|
51514
|
+
const armStartX = hubRadius - armOverlap;
|
|
51515
|
+
const armCenterX = armStartX + armLength / 2;
|
|
51516
|
+
const gripCenterX = armStartX + armLength - gripLength / 2;
|
|
51517
|
+
const runningClearance = 0.03;
|
|
51518
|
+
const lowerWasherZ = supportThickness + runningClearance;
|
|
51519
|
+
const leverZ = lowerWasherZ + washerDims.t + runningClearance;
|
|
51520
|
+
const upperWasherZ = leverZ + leverThickness + runningClearance;
|
|
51521
|
+
const stackHeight = upperWasherZ + washerDims.t;
|
|
51522
|
+
const pinHeadThickness = Math.max(washerDims.t, pinDiameter * 0.35);
|
|
51523
|
+
const pinHeadRadius = Math.max(washerDims.od * 0.42, pinDiameter * 0.8);
|
|
51524
|
+
const supportBore = cylinder(supportThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51525
|
+
let supportBlank = box(supportWidth, supportDepth, supportThickness);
|
|
51526
|
+
if (options.stopBlock ?? true) {
|
|
51527
|
+
const stopLength = Math.min(armLength * 0.22, Math.max(10, armWidth * 1.4));
|
|
51528
|
+
const stopWidth = Math.max(4, pinDiameter * 0.7);
|
|
51529
|
+
const stopHeight = supportThickness;
|
|
51530
|
+
const stopX = hubRadius + stopLength / 2;
|
|
51531
|
+
const stopY = armWidth / 2 + stopWidth / 2 + runningClearance;
|
|
51532
|
+
supportBlank = union(supportBlank, box(stopLength, stopWidth, stopHeight).translate(stopX, stopY, 0));
|
|
51533
|
+
}
|
|
51534
|
+
const support = supportBlank.subtract(supportBore).color("#475569");
|
|
51535
|
+
const hub = cylinder(leverThickness, hubRadius, void 0, segments);
|
|
51536
|
+
const arm = box(armLength, armWidth, leverThickness).translate(armCenterX, 0, 0);
|
|
51537
|
+
const grip = box(gripLength, gripWidth, leverThickness).translate(gripCenterX, 0, 0);
|
|
51538
|
+
const leverSolids = [hub, arm, grip];
|
|
51539
|
+
if (options.detentBoss ?? true) {
|
|
51540
|
+
const bossRadius = Math.min(armWidth * 0.42, hubRadius * 0.42);
|
|
51541
|
+
const bossX = hubRadius + Math.min(armLength * 0.22, armWidth * 2);
|
|
51542
|
+
const bossY = -armWidth / 2 - bossRadius * 0.45;
|
|
51543
|
+
leverSolids.push(cylinder(leverThickness, bossRadius, void 0, segments).translate(bossX, bossY, 0));
|
|
51544
|
+
}
|
|
51545
|
+
const leverBore = cylinder(leverThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51546
|
+
const lever = union(...leverSolids).subtract(leverBore).translate(0, 0, leverZ).color("#7f1d1d");
|
|
51547
|
+
const lowerWasher = washer(washerSize, { segments }).translate(0, 0, lowerWasherZ).color("#94a3b8");
|
|
51548
|
+
const upperWasher = washer(washerSize, { segments }).translate(0, 0, upperWasherZ).color("#94a3b8");
|
|
51549
|
+
const shaft = cylinder(stackHeight, pinDiameter / 2, void 0, segments);
|
|
51550
|
+
const lowerRetainer = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, -pinHeadThickness - runningClearance);
|
|
51551
|
+
const upperHead = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, stackHeight + runningClearance);
|
|
51552
|
+
const pin = union(shaft, lowerRetainer, upperHead).color("#cbd5e1");
|
|
51553
|
+
const pivotBore = cylinder(stackHeight + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51554
|
+
const parts = [
|
|
51555
|
+
{ name: "pivot support block with bearing bore and low stop land", shape: support },
|
|
51556
|
+
{ name: "lower thrust washer under pinned lever", shape: lowerWasher },
|
|
51557
|
+
{ name: "fused pinned lever with hub arm grip and detent boss", shape: lever },
|
|
51558
|
+
{ name: "upper thrust washer over pinned lever", shape: upperWasher },
|
|
51559
|
+
{ name: "retained pivot pin through lever stack", shape: pin }
|
|
51560
|
+
];
|
|
51561
|
+
return {
|
|
51562
|
+
parts,
|
|
51563
|
+
support,
|
|
51564
|
+
lever,
|
|
51565
|
+
pin,
|
|
51566
|
+
washers: {
|
|
51567
|
+
lower: lowerWasher,
|
|
51568
|
+
upper: upperWasher
|
|
51569
|
+
},
|
|
51570
|
+
cutters: {
|
|
51571
|
+
pivotBore
|
|
51572
|
+
},
|
|
51573
|
+
dims: {
|
|
51574
|
+
armLength,
|
|
51575
|
+
armWidth,
|
|
51576
|
+
leverThickness,
|
|
51577
|
+
hubRadius,
|
|
51578
|
+
pinDiameter,
|
|
51579
|
+
boreDiameter,
|
|
51580
|
+
supportWidth,
|
|
51581
|
+
supportDepth,
|
|
51582
|
+
supportThickness,
|
|
51583
|
+
washerSize,
|
|
51584
|
+
washerThickness: washerDims.t,
|
|
51585
|
+
stackHeight
|
|
51586
|
+
}
|
|
51587
|
+
};
|
|
51588
|
+
}
|
|
51589
|
+
function retainedShaftAssembly(options) {
|
|
51590
|
+
const supportSpacing = requirePositive$6(options.supportSpacing, "supportSpacing");
|
|
51591
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? 8, "shaftDiameter");
|
|
51592
|
+
const boreClearance = requireNonNegative(options.boreClearance ?? 0.35, "boreClearance");
|
|
51593
|
+
const boreDiameter = shaftDiameter + boreClearance;
|
|
51594
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(5, shaftDiameter * 0.75), "supportThickness");
|
|
51595
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(shaftDiameter);
|
|
51596
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
51597
|
+
if (!washerDims) throw new Error(`retainedShaftAssembly: unsupported washerSize "${washerSize}"`);
|
|
51598
|
+
if (washerDims.id <= shaftDiameter) {
|
|
51599
|
+
throw new Error(`retainedShaftAssembly: ${washerSize} washer inner diameter is too small for a ${shaftDiameter} mm shaft`);
|
|
51600
|
+
}
|
|
51601
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? shaftDiameter * 3, "knobDiameter");
|
|
51602
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(8, shaftDiameter), "knobThickness");
|
|
51603
|
+
const retainerThickness = requirePositive$6(
|
|
51604
|
+
options.retainerThickness ?? Math.max(washerDims.t, shaftDiameter * 0.35),
|
|
51605
|
+
"retainerThickness"
|
|
51606
|
+
);
|
|
51607
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
51608
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? Math.max(28, knobDiameter * 1.25), "supportWidth");
|
|
51609
|
+
const supportHeight = requirePositive$6(options.supportHeight ?? Math.max(34, knobDiameter * 1.45), "supportHeight");
|
|
51610
|
+
const segments = options.segments ?? 40;
|
|
51611
|
+
if (supportSpacing <= supportThickness) {
|
|
51612
|
+
throw new Error("retainedShaftAssembly: supportSpacing must leave a gap between support cheeks");
|
|
51613
|
+
}
|
|
51614
|
+
if (supportWidth <= boreDiameter + 4 || supportHeight <= boreDiameter + 4) {
|
|
51615
|
+
throw new Error("retainedShaftAssembly: support dimensions leave too little material around the shaft bore");
|
|
51616
|
+
}
|
|
51617
|
+
const leftSupportX = -supportSpacing / 2;
|
|
51618
|
+
const rightSupportX = supportSpacing / 2;
|
|
51619
|
+
const leftOuterFaceX = leftSupportX - supportThickness / 2;
|
|
51620
|
+
const rightOuterFaceX = rightSupportX + supportThickness / 2;
|
|
51621
|
+
const leftWasherX = leftOuterFaceX - runningClearance - washerDims.t / 2;
|
|
51622
|
+
const rightWasherX = rightOuterFaceX + runningClearance + washerDims.t / 2;
|
|
51623
|
+
const leftKnobX = leftOuterFaceX - runningClearance * 2 - washerDims.t - knobThickness / 2;
|
|
51624
|
+
const rightKnobX = rightOuterFaceX + runningClearance * 2 + washerDims.t + knobThickness / 2;
|
|
51625
|
+
const leftStackOuterX = leftKnobX - knobThickness / 2;
|
|
51626
|
+
const rightStackOuterX = rightKnobX + knobThickness / 2;
|
|
51627
|
+
const minimumShaftLength = rightStackOuterX - leftStackOuterX + retainerThickness * 2 + runningClearance * 2;
|
|
51628
|
+
const shaftLength = requirePositive$6(options.shaftLength ?? minimumShaftLength, "shaftLength");
|
|
51629
|
+
if (shaftLength < minimumShaftLength) {
|
|
51630
|
+
throw new Error("retainedShaftAssembly: shaftLength is too short to retain both supports, washers, and knobs");
|
|
51631
|
+
}
|
|
51632
|
+
const supportBore = cylinderAlongX(supportThickness + 1, boreDiameter / 2, 0, segments);
|
|
51633
|
+
const makeSupport = (x2) => box(supportThickness, supportWidth, supportHeight).translate(x2, 0, -supportHeight / 2).subtract(supportBore.translate(x2, 0, 0)).color("#334155");
|
|
51634
|
+
const knobBore = cylinder(knobThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51635
|
+
const makeKnob = (x2) => cylinder(knobThickness, knobDiameter / 2, void 0, 18).subtract(knobBore).pointAlong([1, 0, 0]).translate(x2 - knobThickness / 2, 0, 0).color("#111827");
|
|
51636
|
+
const retainerRadius = Math.max(shaftDiameter * 0.85, knobDiameter * 0.36);
|
|
51637
|
+
const shaftCore = cylinderAlongX(shaftLength, shaftDiameter / 2, 0, segments);
|
|
51638
|
+
const leftRetainer = cylinderAlongX(retainerThickness, retainerRadius, -shaftLength / 2 + retainerThickness / 2, segments);
|
|
51639
|
+
const rightRetainer = cylinderAlongX(retainerThickness, retainerRadius, shaftLength / 2 - retainerThickness / 2, segments);
|
|
51640
|
+
const shaft = union(shaftCore, leftRetainer, rightRetainer).color("#cbd5e1");
|
|
51641
|
+
const leftSupport = makeSupport(leftSupportX);
|
|
51642
|
+
const rightSupport = makeSupport(rightSupportX);
|
|
51643
|
+
const leftWasher = washerAlongX(washerSize, leftWasherX, segments).color("#94a3b8");
|
|
51644
|
+
const rightWasher = washerAlongX(washerSize, rightWasherX, segments).color("#94a3b8");
|
|
51645
|
+
const leftKnob = makeKnob(leftKnobX);
|
|
51646
|
+
const rightKnob = makeKnob(rightKnobX);
|
|
51647
|
+
const shaftBore = cylinderAlongX(supportThickness + knobThickness + 2, boreDiameter / 2, 0, segments);
|
|
51648
|
+
const parts = [
|
|
51649
|
+
{ name: "left bored support cheek for retained shaft", shape: leftSupport },
|
|
51650
|
+
{ name: "right bored support cheek for retained shaft", shape: rightSupport },
|
|
51651
|
+
{ name: "retained through shaft with end heads", shape: shaft },
|
|
51652
|
+
{ name: `left ${washerSize} thrust washer on shaft`, shape: leftWasher },
|
|
51653
|
+
{ name: `right ${washerSize} thrust washer on shaft`, shape: rightWasher },
|
|
51654
|
+
{ name: "left retained hand knob with shaft bore", shape: leftKnob },
|
|
51655
|
+
{ name: "right retained hand knob with shaft bore", shape: rightKnob }
|
|
51656
|
+
];
|
|
51657
|
+
return {
|
|
51658
|
+
parts,
|
|
51659
|
+
supports: {
|
|
51660
|
+
left: leftSupport,
|
|
51661
|
+
right: rightSupport
|
|
51662
|
+
},
|
|
51663
|
+
shaft,
|
|
51664
|
+
washers: {
|
|
51665
|
+
left: leftWasher,
|
|
51666
|
+
right: rightWasher
|
|
51667
|
+
},
|
|
51668
|
+
knobs: {
|
|
51669
|
+
left: leftKnob,
|
|
51670
|
+
right: rightKnob
|
|
51671
|
+
},
|
|
51672
|
+
cutters: {
|
|
51673
|
+
shaftBore
|
|
51674
|
+
},
|
|
51675
|
+
dims: {
|
|
51676
|
+
supportSpacing,
|
|
51677
|
+
supportThickness,
|
|
51678
|
+
supportWidth,
|
|
51679
|
+
supportHeight,
|
|
51680
|
+
shaftDiameter,
|
|
51681
|
+
shaftLength,
|
|
51682
|
+
boreDiameter,
|
|
51683
|
+
washerSize,
|
|
51684
|
+
washerThickness: washerDims.t,
|
|
51685
|
+
knobDiameter,
|
|
51686
|
+
knobThickness,
|
|
51687
|
+
retainerThickness,
|
|
51688
|
+
runningClearance
|
|
51689
|
+
}
|
|
51690
|
+
};
|
|
51691
|
+
}
|
|
51692
|
+
function capturedLinearSlide(options) {
|
|
51693
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51694
|
+
const railWidth = requirePositive$6(options.railWidth ?? 38, "railWidth");
|
|
51695
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 2.4, "baseThickness");
|
|
51696
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2, "wallThickness");
|
|
51697
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 9, "wallHeight");
|
|
51698
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
51699
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 1.8, "lipThickness");
|
|
51700
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51701
|
+
const endStopLength = requirePositive$6(options.endStopLength ?? 6, "endStopLength");
|
|
51702
|
+
const carriageLength = requirePositive$6(options.carriageLength ?? length4 * 0.32, "carriageLength");
|
|
51703
|
+
const innerWidth = railWidth - wallThickness * 2;
|
|
51704
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
51705
|
+
if (innerWidth <= 0) throw new Error("capturedLinearSlide: wallThickness leaves no inner rail width");
|
|
51706
|
+
if (throatWidth <= 0) throw new Error("capturedLinearSlide: lipWidth closes the rail throat");
|
|
51707
|
+
const carriageWidth = requirePositive$6(options.carriageWidth ?? innerWidth - runningClearance * 2, "carriageWidth");
|
|
51708
|
+
const carriageThickness = requirePositive$6(options.carriageThickness ?? 4, "carriageThickness");
|
|
51709
|
+
if (carriageWidth >= innerWidth - runningClearance) {
|
|
51710
|
+
throw new Error("capturedLinearSlide: carriageWidth leaves too little side clearance inside the rail");
|
|
51711
|
+
}
|
|
51712
|
+
if (carriageWidth <= throatWidth + runningClearance) {
|
|
51713
|
+
throw new Error("capturedLinearSlide: carriageWidth must be wider than the lip throat so the rail actually captures it");
|
|
51714
|
+
}
|
|
51715
|
+
if (carriageThickness + runningClearance * 2 >= wallHeight) {
|
|
51716
|
+
throw new Error("capturedLinearSlide: carriage is too tall to clear the return lips");
|
|
51717
|
+
}
|
|
51718
|
+
const maxTravel = length4 - endStopLength * 2 - carriageLength;
|
|
51719
|
+
if (maxTravel <= 0) {
|
|
51720
|
+
throw new Error("capturedLinearSlide: rail length, end stops, and carriage length leave no travel");
|
|
51721
|
+
}
|
|
51722
|
+
const travel = options.travel ?? maxTravel / 2;
|
|
51723
|
+
if (!Number.isFinite(travel) || travel < 0 || travel > maxTravel) {
|
|
51724
|
+
throw new Error(`capturedLinearSlide: travel must be between 0 and ${maxTravel}`);
|
|
51725
|
+
}
|
|
51726
|
+
const carriageCenterX = -maxTravel / 2 + travel;
|
|
51727
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
51728
|
+
const sideY = railWidth / 2 - wallThickness / 2;
|
|
51729
|
+
const lipY = railWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
51730
|
+
const stopZ = baseThickness - fuseOverlap;
|
|
51731
|
+
const rail2 = union(
|
|
51732
|
+
box(length4, railWidth, baseThickness),
|
|
51733
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
51734
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
51735
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51736
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51737
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(-length4 / 2 + endStopLength / 2, 0, stopZ),
|
|
51738
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(length4 / 2 - endStopLength / 2, 0, stopZ)
|
|
51739
|
+
).color("#475569");
|
|
51740
|
+
const carriage = union(
|
|
51741
|
+
box(carriageLength, carriageWidth, carriageThickness),
|
|
51742
|
+
box(carriageLength * 0.78, throatWidth - runningClearance * 2, Math.max(1, carriageThickness * 0.38)).translate(
|
|
51743
|
+
0,
|
|
51744
|
+
0,
|
|
51745
|
+
carriageThickness
|
|
51746
|
+
)
|
|
51747
|
+
).translate(carriageCenterX, 0, baseThickness + runningClearance).color("#111827");
|
|
51748
|
+
const parts = [
|
|
51749
|
+
{ name: "captured linear rail with return lips and end stops", shape: rail2 },
|
|
51750
|
+
{ name: "sliding carriage captured under rail lips", shape: carriage }
|
|
51751
|
+
];
|
|
51752
|
+
return {
|
|
51753
|
+
parts,
|
|
51754
|
+
rail: rail2,
|
|
51755
|
+
carriage,
|
|
51756
|
+
dims: {
|
|
51757
|
+
length: length4,
|
|
51758
|
+
railWidth,
|
|
51759
|
+
innerWidth,
|
|
51760
|
+
throatWidth,
|
|
51761
|
+
baseThickness,
|
|
51762
|
+
wallThickness,
|
|
51763
|
+
wallHeight,
|
|
51764
|
+
lipWidth,
|
|
51765
|
+
lipThickness,
|
|
51766
|
+
carriageLength,
|
|
51767
|
+
carriageWidth,
|
|
51768
|
+
carriageThickness,
|
|
51769
|
+
endStopLength,
|
|
51770
|
+
runningClearance,
|
|
51771
|
+
maxTravel,
|
|
51772
|
+
travel,
|
|
51773
|
+
carriageCenterX
|
|
51774
|
+
}
|
|
51775
|
+
};
|
|
51776
|
+
}
|
|
51777
|
+
function capturedCartridgeGuideAssembly(options) {
|
|
51778
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51779
|
+
const guideWidth = requirePositive$6(options.guideWidth ?? 42, "guideWidth");
|
|
51780
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 3, "baseThickness");
|
|
51781
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.5, "wallThickness");
|
|
51782
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 12, "wallHeight");
|
|
51783
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
51784
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 2, "lipThickness");
|
|
51785
|
+
const rearStopLength = requirePositive$6(options.rearStopLength ?? 7, "rearStopLength");
|
|
51786
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51787
|
+
const cartridgeLength = requirePositive$6(options.cartridgeLength ?? length4 * 0.58, "cartridgeLength");
|
|
51788
|
+
const cartridgeHeight = requirePositive$6(options.cartridgeHeight ?? 10, "cartridgeHeight");
|
|
51789
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? 3, "flangeThickness");
|
|
51790
|
+
const pullTabLength = requirePositive$6(options.pullTabLength ?? 10, "pullTabLength");
|
|
51791
|
+
const innerWidth = guideWidth - wallThickness * 2;
|
|
51792
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
51793
|
+
if (innerWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: wallThickness leaves no inner guide width");
|
|
51794
|
+
if (throatWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: lipWidth closes the guide throat");
|
|
51795
|
+
if (wallHeight <= lipThickness + flangeThickness + runningClearance * 2) {
|
|
51796
|
+
throw new Error("capturedCartridgeGuideAssembly: wallHeight leaves too little vertical capture clearance");
|
|
51797
|
+
}
|
|
51798
|
+
const cartridgeWidth = requirePositive$6(options.cartridgeWidth ?? innerWidth - runningClearance * 2, "cartridgeWidth");
|
|
51799
|
+
const cartridgeBodyWidth = throatWidth - runningClearance * 2;
|
|
51800
|
+
if (cartridgeBodyWidth <= 0) {
|
|
51801
|
+
throw new Error("capturedCartridgeGuideAssembly: throatWidth and runningClearance leave no cartridge body width");
|
|
51802
|
+
}
|
|
51803
|
+
if (cartridgeWidth >= innerWidth - runningClearance) {
|
|
51804
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridgeWidth leaves too little side clearance inside the guide");
|
|
51805
|
+
}
|
|
51806
|
+
if (cartridgeWidth <= throatWidth + runningClearance) {
|
|
51807
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridge flange must be wider than the guide throat so the cartridge is captured");
|
|
51808
|
+
}
|
|
51809
|
+
const maxInsertion = length4 - rearStopLength - cartridgeLength;
|
|
51810
|
+
if (maxInsertion <= 0) {
|
|
51811
|
+
throw new Error("capturedCartridgeGuideAssembly: length, rearStopLength, and cartridgeLength leave no insertion travel");
|
|
51812
|
+
}
|
|
51813
|
+
const insertion = options.insertion ?? maxInsertion * 0.4;
|
|
51814
|
+
if (!Number.isFinite(insertion) || insertion < 0 || insertion > maxInsertion) {
|
|
51815
|
+
throw new Error(`capturedCartridgeGuideAssembly: insertion must be between 0 and ${maxInsertion}`);
|
|
51816
|
+
}
|
|
51817
|
+
const cartridgeCenterX = -length4 / 2 + cartridgeLength / 2 + insertion;
|
|
51818
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
51819
|
+
const sideY = guideWidth / 2 - wallThickness / 2;
|
|
51820
|
+
const lipY = guideWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
51821
|
+
const guide = union(
|
|
51822
|
+
box(length4, guideWidth, baseThickness),
|
|
51823
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
51824
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
51825
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51826
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51827
|
+
box(rearStopLength, throatWidth, Math.max(flangeThickness + runningClearance, 4)).translate(
|
|
51828
|
+
length4 / 2 - rearStopLength / 2,
|
|
51829
|
+
0,
|
|
51830
|
+
baseThickness - fuseOverlap
|
|
51831
|
+
)
|
|
51832
|
+
).color("#475569");
|
|
51833
|
+
const flangeZ = baseThickness + runningClearance;
|
|
51834
|
+
const bodyHeight = Math.max(1, cartridgeHeight - flangeThickness);
|
|
51835
|
+
const bodyZ = flangeZ + flangeThickness;
|
|
51836
|
+
const tabOverlap = Math.min(0.6, pullTabLength * 0.15);
|
|
51837
|
+
const pullTabX = cartridgeCenterX - cartridgeLength / 2 - pullTabLength / 2 + tabOverlap;
|
|
51838
|
+
const pullTabWidth = Math.max(cartridgeBodyWidth * 0.55, 12);
|
|
51839
|
+
const cartridge = union(
|
|
51840
|
+
box(cartridgeLength, cartridgeWidth, flangeThickness).translate(cartridgeCenterX, 0, flangeZ),
|
|
51841
|
+
box(cartridgeLength * 0.88, cartridgeBodyWidth, bodyHeight).translate(cartridgeCenterX, 0, bodyZ),
|
|
51842
|
+
box(pullTabLength, pullTabWidth, Math.max(flangeThickness, 3)).translate(pullTabX, 0, flangeZ)
|
|
51843
|
+
).color("#111827");
|
|
51844
|
+
const parts = [
|
|
51845
|
+
{ name: "captured cartridge guide with return lips and rear stop", shape: guide },
|
|
51846
|
+
{ name: "removable cartridge with captured flange and pull tab", shape: cartridge }
|
|
51847
|
+
];
|
|
51848
|
+
return {
|
|
51849
|
+
parts,
|
|
51850
|
+
guide,
|
|
51851
|
+
cartridge,
|
|
51852
|
+
dims: {
|
|
51853
|
+
length: length4,
|
|
51854
|
+
guideWidth,
|
|
51855
|
+
innerWidth,
|
|
51856
|
+
throatWidth,
|
|
51857
|
+
baseThickness,
|
|
51858
|
+
wallThickness,
|
|
51859
|
+
wallHeight,
|
|
51860
|
+
lipWidth,
|
|
51861
|
+
lipThickness,
|
|
51862
|
+
rearStopLength,
|
|
51863
|
+
cartridgeLength,
|
|
51864
|
+
cartridgeWidth,
|
|
51865
|
+
cartridgeBodyWidth,
|
|
51866
|
+
cartridgeHeight,
|
|
51867
|
+
flangeThickness,
|
|
51868
|
+
pullTabLength,
|
|
51869
|
+
runningClearance,
|
|
51870
|
+
maxInsertion,
|
|
51871
|
+
insertion,
|
|
51872
|
+
cartridgeCenterX
|
|
51873
|
+
}
|
|
51874
|
+
};
|
|
51875
|
+
}
|
|
51876
|
+
function livingHingeCoverAssembly(options) {
|
|
51877
|
+
const width = requirePositive$6(options.width, "width");
|
|
51878
|
+
const coverDepth = requirePositive$6(options.coverDepth ?? 42, "coverDepth");
|
|
51879
|
+
const fixedLeafDepth = requirePositive$6(options.fixedLeafDepth ?? 18, "fixedLeafDepth");
|
|
51880
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 2, "leafThickness");
|
|
51881
|
+
const hingeWebWidth = requirePositive$6(options.hingeWebWidth ?? 3.2, "hingeWebWidth");
|
|
51882
|
+
const hingeWebThickness = requirePositive$6(options.hingeWebThickness ?? 0.45, "hingeWebThickness");
|
|
51883
|
+
const pullLipDepth = requirePositive$6(options.pullLipDepth ?? 5, "pullLipDepth");
|
|
51884
|
+
const snapBarbWidth = requirePositive$6(options.snapBarbWidth ?? width * 0.35, "snapBarbWidth");
|
|
51885
|
+
const snapBarbDepth = requirePositive$6(options.snapBarbDepth ?? 2.4, "snapBarbDepth");
|
|
51886
|
+
const snapBarbHeight = requirePositive$6(options.snapBarbHeight ?? 1.4, "snapBarbHeight");
|
|
51887
|
+
const catchLandDepth = requirePositive$6(options.catchLandDepth ?? 2.4, "catchLandDepth");
|
|
51888
|
+
if (hingeWebThickness >= leafThickness * 0.55) {
|
|
51889
|
+
throw new Error("livingHingeCoverAssembly: hingeWebThickness must be much thinner than the rigid leaves");
|
|
51890
|
+
}
|
|
51891
|
+
if (hingeWebWidth >= Math.min(coverDepth, fixedLeafDepth) * 0.45) {
|
|
51892
|
+
throw new Error("livingHingeCoverAssembly: hingeWebWidth is too wide for the selected leaves");
|
|
51893
|
+
}
|
|
51894
|
+
if (snapBarbWidth >= width - 2) {
|
|
51895
|
+
throw new Error("livingHingeCoverAssembly: snapBarbWidth must leave side material on the cover leaf");
|
|
51896
|
+
}
|
|
51897
|
+
const fuseOverlap = Math.min(0.04, hingeWebWidth * 0.02);
|
|
51898
|
+
const fixedCenterY = -hingeWebWidth / 2 - fixedLeafDepth / 2 + fuseOverlap / 2;
|
|
51899
|
+
const coverCenterY = hingeWebWidth / 2 + coverDepth / 2 - fuseOverlap / 2;
|
|
51900
|
+
const fixedLeaf = box(width, fixedLeafDepth + fuseOverlap, leafThickness).translate(0, fixedCenterY, 0);
|
|
51901
|
+
const movingLeaf = box(width, coverDepth + fuseOverlap, leafThickness).translate(0, coverCenterY, 0);
|
|
51902
|
+
const hingeWeb = box(width, hingeWebWidth + fuseOverlap * 2, hingeWebThickness).translate(0, 0, 0);
|
|
51903
|
+
const pullLip = box(width * 0.92, pullLipDepth, leafThickness).translate(0, coverCenterY + coverDepth / 2 + pullLipDepth / 2 - fuseOverlap, 0);
|
|
51904
|
+
const snapBarb = box(snapBarbWidth, snapBarbDepth, snapBarbHeight).translate(
|
|
51905
|
+
0,
|
|
51906
|
+
coverCenterY + coverDepth / 2 - snapBarbDepth / 2,
|
|
51907
|
+
leafThickness
|
|
51908
|
+
);
|
|
51909
|
+
const catchLand = box(width * 0.55, catchLandDepth, Math.max(0.8, leafThickness * 0.45)).translate(
|
|
51910
|
+
0,
|
|
51911
|
+
fixedCenterY - fixedLeafDepth / 2 + catchLandDepth / 2,
|
|
51912
|
+
leafThickness
|
|
51913
|
+
);
|
|
51914
|
+
const cover = union(fixedLeaf, movingLeaf, hingeWeb, pullLip, snapBarb, catchLand).color("#0f766e");
|
|
51915
|
+
const overallDepth = fixedLeafDepth + hingeWebWidth + coverDepth + pullLipDepth;
|
|
51916
|
+
const flexRatio = leafThickness / hingeWebThickness;
|
|
51917
|
+
return {
|
|
51918
|
+
parts: [{ name: "one-piece molded living hinge cover with snap barb", shape: cover }],
|
|
51919
|
+
cover,
|
|
51920
|
+
fixedLeaf,
|
|
51921
|
+
movingLeaf,
|
|
51922
|
+
hingeWeb,
|
|
51923
|
+
snapBarb,
|
|
51924
|
+
catchLand,
|
|
51925
|
+
dims: {
|
|
51926
|
+
width,
|
|
51927
|
+
coverDepth,
|
|
51928
|
+
fixedLeafDepth,
|
|
51929
|
+
leafThickness,
|
|
51930
|
+
hingeWebWidth,
|
|
51931
|
+
hingeWebThickness,
|
|
51932
|
+
pullLipDepth,
|
|
51933
|
+
snapBarbWidth,
|
|
51934
|
+
snapBarbDepth,
|
|
51935
|
+
snapBarbHeight,
|
|
51936
|
+
catchLandDepth,
|
|
51937
|
+
flexRatio,
|
|
51938
|
+
overallDepth
|
|
51939
|
+
}
|
|
51940
|
+
};
|
|
51941
|
+
}
|
|
51942
|
+
function knuckledHingeAssembly(options) {
|
|
51943
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51944
|
+
const leafLength = requirePositive$6(options.leafLength ?? 36, "leafLength");
|
|
51945
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 1.6, "leafThickness");
|
|
51946
|
+
const barrelOuterRadius = requirePositive$6(options.barrelOuterRadius ?? 3, "barrelOuterRadius");
|
|
51947
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 2, "pinDiameter");
|
|
51948
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
51949
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51950
|
+
const knuckleGap = requireNonNegative(options.knuckleGap ?? 0.45, "knuckleGap");
|
|
51951
|
+
const openAngleDeg = Number.isFinite(options.openAngleDeg ?? 35) ? options.openAngleDeg ?? 35 : 35;
|
|
51952
|
+
const retainerThickness = requirePositive$6(
|
|
51953
|
+
options.retainerThickness ?? Math.max(leafThickness, pinDiameter * 0.7),
|
|
51954
|
+
"retainerThickness"
|
|
51955
|
+
);
|
|
51956
|
+
const segments = options.segments ?? 36;
|
|
51957
|
+
const knuckleCount = options.knuckleCount ?? 5;
|
|
51958
|
+
if (!Number.isInteger(knuckleCount) || knuckleCount < 3 || knuckleCount % 2 === 0) {
|
|
51959
|
+
throw new Error("knuckledHingeAssembly: knuckleCount must be an odd integer >= 3");
|
|
51960
|
+
}
|
|
51961
|
+
if (barrelOuterRadius <= boreDiameter / 2 + Math.max(0.35, pinDiameter * 0.18)) {
|
|
51962
|
+
throw new Error("knuckledHingeAssembly: barrelOuterRadius leaves too little wall around the pin bore");
|
|
51963
|
+
}
|
|
51964
|
+
const knuckleLength = (length4 - knuckleGap * (knuckleCount - 1)) / knuckleCount;
|
|
51965
|
+
if (knuckleLength <= pinDiameter * 1.4) {
|
|
51966
|
+
throw new Error("knuckledHingeAssembly: length, knuckleCount, and knuckleGap make knuckles too short");
|
|
51967
|
+
}
|
|
51968
|
+
const leafRootClearance = Math.max(0.12, Math.min(knuckleGap * 0.35, 0.35));
|
|
51969
|
+
const barrelLeafOverlap = Math.min(barrelOuterRadius * 0.18, leafThickness * 0.35);
|
|
51970
|
+
const bridgeDepth = leafRootClearance + barrelLeafOverlap + 0.2;
|
|
51971
|
+
const fixedLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51972
|
+
0,
|
|
51973
|
+
barrelOuterRadius + leafRootClearance + leafLength / 2,
|
|
51974
|
+
-leafThickness / 2
|
|
51975
|
+
);
|
|
51976
|
+
const movingLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51977
|
+
0,
|
|
51978
|
+
-barrelOuterRadius - leafRootClearance - leafLength / 2,
|
|
51979
|
+
-leafThickness / 2
|
|
51980
|
+
);
|
|
51981
|
+
const fixedKnuckles = [];
|
|
51982
|
+
const movingKnuckles = [];
|
|
51983
|
+
const fixedBridges = [];
|
|
51984
|
+
const movingBridges = [];
|
|
51985
|
+
for (let index2 = 0; index2 < knuckleCount; index2 += 1) {
|
|
51986
|
+
const xStart = -length4 / 2 + index2 * (knuckleLength + knuckleGap);
|
|
51987
|
+
const xCenter = xStart + knuckleLength / 2;
|
|
51988
|
+
const knuckle = tubeAlongX(knuckleLength, barrelOuterRadius, boreDiameter / 2, xCenter, segments);
|
|
51989
|
+
if (index2 % 2 === 0) {
|
|
51990
|
+
fixedKnuckles.push(knuckle);
|
|
51991
|
+
fixedBridges.push(
|
|
51992
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
51993
|
+
xCenter,
|
|
51994
|
+
barrelOuterRadius - barrelLeafOverlap + bridgeDepth / 2,
|
|
51995
|
+
-leafThickness / 2
|
|
51996
|
+
)
|
|
51997
|
+
);
|
|
51998
|
+
} else {
|
|
51999
|
+
movingKnuckles.push(knuckle);
|
|
52000
|
+
movingBridges.push(
|
|
52001
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
52002
|
+
xCenter,
|
|
52003
|
+
-barrelOuterRadius + barrelLeafOverlap - bridgeDepth / 2,
|
|
52004
|
+
-leafThickness / 2
|
|
52005
|
+
)
|
|
52006
|
+
);
|
|
52007
|
+
}
|
|
52008
|
+
}
|
|
52009
|
+
const fixedLeaf = union(fixedLeafPlate, ...fixedKnuckles, ...fixedBridges).color("#475569");
|
|
52010
|
+
const movingLeaf = union(movingLeafPlate, ...movingKnuckles, ...movingBridges).rotateX(openAngleDeg).color("#111827");
|
|
52011
|
+
const pinCore = cylinderAlongX(length4 + retainerThickness * 2, pinDiameter / 2, 0, segments);
|
|
52012
|
+
const retainerRadius = Math.max(barrelOuterRadius * 0.85, pinDiameter);
|
|
52013
|
+
const leftHead = cylinderAlongX(retainerThickness, retainerRadius, -length4 / 2 - retainerThickness / 2, segments);
|
|
52014
|
+
const rightHead = cylinderAlongX(retainerThickness, retainerRadius, length4 / 2 + retainerThickness / 2, segments);
|
|
52015
|
+
const pin = union(pinCore, leftHead, rightHead).color("#cbd5e1");
|
|
52016
|
+
const pinBore = cylinderAlongX(length4 + retainerThickness * 2, boreDiameter / 2, 0, segments);
|
|
52017
|
+
const parts = [
|
|
52018
|
+
{ name: "fixed hinge leaf with alternating knuckles", shape: fixedLeaf },
|
|
52019
|
+
{ name: "moving hinge leaf with alternating knuckles", shape: movingLeaf },
|
|
52020
|
+
{ name: "retained hinge pin through knuckle stack", shape: pin }
|
|
52021
|
+
];
|
|
52022
|
+
return {
|
|
52023
|
+
parts,
|
|
52024
|
+
fixedLeaf,
|
|
52025
|
+
movingLeaf,
|
|
52026
|
+
pin,
|
|
52027
|
+
cutters: {
|
|
52028
|
+
pinBore
|
|
52029
|
+
},
|
|
52030
|
+
dims: {
|
|
52031
|
+
length: length4,
|
|
52032
|
+
leafLength,
|
|
52033
|
+
leafThickness,
|
|
52034
|
+
barrelOuterRadius,
|
|
52035
|
+
pinDiameter,
|
|
52036
|
+
boreDiameter,
|
|
52037
|
+
knuckleGap,
|
|
52038
|
+
knuckleCount,
|
|
52039
|
+
knuckleLength,
|
|
52040
|
+
openAngleDeg,
|
|
52041
|
+
retainerThickness
|
|
52042
|
+
}
|
|
52043
|
+
};
|
|
52044
|
+
}
|
|
52045
|
+
function clevisPinJointAssembly(options = {}) {
|
|
52046
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 4, "pinDiameter");
|
|
52047
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.3, "pinClearance");
|
|
52048
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
52049
|
+
const linkThickness = requirePositive$6(options.linkThickness ?? Math.max(5, pinDiameter * 1.5), "linkThickness");
|
|
52050
|
+
const earThickness = requirePositive$6(options.earThickness ?? Math.max(3.5, pinDiameter), "earThickness");
|
|
52051
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.25, "runningClearance");
|
|
52052
|
+
const linkArmWidth = requirePositive$6(options.linkArmWidth ?? pinDiameter * 2.4, "linkArmWidth");
|
|
52053
|
+
const eyeOuterRadius = requirePositive$6(
|
|
52054
|
+
options.eyeOuterRadius ?? Math.max(pinDiameter * 1.8, linkArmWidth / 2 + 1.4),
|
|
52055
|
+
"eyeOuterRadius"
|
|
52056
|
+
);
|
|
52057
|
+
const earLength = requirePositive$6(options.earLength ?? Math.max(eyeOuterRadius * 2.55, pinDiameter * 4.2), "earLength");
|
|
52058
|
+
const earHeight = requirePositive$6(options.earHeight ?? Math.max(eyeOuterRadius * 2.25, pinDiameter * 4.4), "earHeight");
|
|
52059
|
+
const linkArmLength = requirePositive$6(options.linkArmLength ?? 34, "linkArmLength");
|
|
52060
|
+
const retainerThickness = requirePositive$6(
|
|
52061
|
+
options.retainerThickness ?? Math.max(1.2, pinDiameter * 0.35),
|
|
52062
|
+
"retainerThickness"
|
|
52063
|
+
);
|
|
52064
|
+
const segments = options.segments ?? 40;
|
|
52065
|
+
if (eyeOuterRadius <= boreDiameter / 2 + Math.max(0.8, pinDiameter * 0.25)) {
|
|
52066
|
+
throw new Error("clevisPinJointAssembly: eyeOuterRadius leaves too little material around the pin bore");
|
|
52067
|
+
}
|
|
52068
|
+
if (earHeight <= boreDiameter + Math.max(3, pinDiameter)) {
|
|
52069
|
+
throw new Error("clevisPinJointAssembly: earHeight leaves too little material around the pin bore");
|
|
52070
|
+
}
|
|
52071
|
+
if (earLength / 2 <= eyeOuterRadius + runningClearance) {
|
|
52072
|
+
throw new Error("clevisPinJointAssembly: earLength must extend behind the link eye for a rear clevis bridge");
|
|
52073
|
+
}
|
|
52074
|
+
const clevisGap = linkThickness + runningClearance * 2;
|
|
52075
|
+
const earCenterY = clevisGap / 2 + earThickness / 2;
|
|
52076
|
+
const totalStackY = clevisGap + earThickness * 2;
|
|
52077
|
+
const pinLength = totalStackY + retainerThickness * 2 + runningClearance * 2;
|
|
52078
|
+
const bridgeClearX = -eyeOuterRadius - runningClearance;
|
|
52079
|
+
const bridgeLength = Math.max(pinDiameter * 2.2, 4);
|
|
52080
|
+
const bridgeHeight = Math.min(earHeight * 0.48, Math.max(pinDiameter * 1.4, eyeOuterRadius * 0.75));
|
|
52081
|
+
const bridgeCenterX = bridgeClearX - bridgeLength / 2;
|
|
52082
|
+
const bridgeCenterZ = -earHeight / 2 + bridgeHeight / 2;
|
|
52083
|
+
const pinBore = cylinderAlongY(totalStackY + 0.8, boreDiameter / 2, 0, segments);
|
|
52084
|
+
const clevisBlank = union(
|
|
52085
|
+
box(earLength, earThickness, earHeight).translate(0, earCenterY, -earHeight / 2),
|
|
52086
|
+
box(earLength, earThickness, earHeight).translate(0, -earCenterY, -earHeight / 2),
|
|
52087
|
+
box(bridgeLength, totalStackY, bridgeHeight).translate(bridgeCenterX, 0, bridgeCenterZ)
|
|
52088
|
+
);
|
|
52089
|
+
const clevis = clevisBlank.subtract(pinBore).color("#475569");
|
|
52090
|
+
const eye = tubeAlongY(linkThickness, eyeOuterRadius, boreDiameter / 2, 0, segments);
|
|
52091
|
+
const armOverlap = Math.min(eyeOuterRadius * 0.65, linkArmLength * 0.25);
|
|
52092
|
+
const armCenterX = eyeOuterRadius - armOverlap + linkArmLength / 2;
|
|
52093
|
+
const linkArm = box(linkArmLength, linkThickness, linkArmWidth).translate(armCenterX, 0, -linkArmWidth / 2);
|
|
52094
|
+
const link = union(eye, linkArm).color("#111827");
|
|
52095
|
+
const pinCore = cylinderAlongY(pinLength, pinDiameter / 2, 0, segments);
|
|
52096
|
+
const headRadius = Math.max(pinDiameter * 0.9, boreDiameter / 2 + 0.8);
|
|
52097
|
+
const headY = totalStackY / 2 + runningClearance + retainerThickness / 2;
|
|
52098
|
+
const headA = cylinderAlongY(retainerThickness, headRadius, headY, segments);
|
|
52099
|
+
const headB = cylinderAlongY(retainerThickness, headRadius, -headY, segments);
|
|
52100
|
+
const pin = union(pinCore, headA, headB).color("#cbd5e1");
|
|
52101
|
+
const cutter = cylinderAlongY(pinLength + 1, boreDiameter / 2, 0, segments);
|
|
52102
|
+
const parts = [
|
|
52103
|
+
{ name: "bored clevis yoke with rear bridge", shape: clevis },
|
|
52104
|
+
{ name: "center link eye captured in clevis", shape: link },
|
|
52105
|
+
{ name: "retained clevis pin through link eye", shape: pin }
|
|
52106
|
+
];
|
|
52107
|
+
return {
|
|
52108
|
+
parts,
|
|
52109
|
+
clevis,
|
|
52110
|
+
link,
|
|
52111
|
+
pin,
|
|
52112
|
+
cutters: {
|
|
52113
|
+
pinBore: cutter
|
|
52114
|
+
},
|
|
52115
|
+
dims: {
|
|
52116
|
+
pinDiameter,
|
|
52117
|
+
boreDiameter,
|
|
52118
|
+
linkThickness,
|
|
52119
|
+
earThickness,
|
|
52120
|
+
runningClearance,
|
|
52121
|
+
earLength,
|
|
52122
|
+
earHeight,
|
|
52123
|
+
linkArmLength,
|
|
52124
|
+
linkArmWidth,
|
|
52125
|
+
eyeOuterRadius,
|
|
52126
|
+
retainerThickness,
|
|
52127
|
+
pinLength,
|
|
52128
|
+
clevisGap
|
|
52129
|
+
}
|
|
52130
|
+
};
|
|
52131
|
+
}
|
|
52132
|
+
function seatedBearingAssembly(options) {
|
|
52133
|
+
const bearingOuterDiameter = requirePositive$6(options.bearingOuterDiameter, "bearingOuterDiameter");
|
|
52134
|
+
const bearingInnerDiameter = requirePositive$6(options.bearingInnerDiameter, "bearingInnerDiameter");
|
|
52135
|
+
const bearingWidth = requirePositive$6(options.bearingWidth, "bearingWidth");
|
|
52136
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? Math.max(1, bearingInnerDiameter - 0.4), "shaftDiameter");
|
|
52137
|
+
const pocketClearance = requireNonNegative(options.pocketClearance ?? 0.2, "pocketClearance");
|
|
52138
|
+
const shaftClearance = requireNonNegative(options.shaftClearance ?? 0.35, "shaftClearance");
|
|
52139
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
52140
|
+
const housingThickness = requirePositive$6(options.housingThickness ?? bearingWidth + 5, "housingThickness");
|
|
52141
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2, bearingWidth * 0.45), "bossHeight");
|
|
52142
|
+
const bossOuterDiameter = requirePositive$6(
|
|
52143
|
+
options.bossOuterDiameter ?? bearingOuterDiameter + Math.max(8, bearingOuterDiameter * 0.36),
|
|
52144
|
+
"bossOuterDiameter"
|
|
52145
|
+
);
|
|
52146
|
+
const housingWidth = requirePositive$6(options.housingWidth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 2.1), "housingWidth");
|
|
52147
|
+
const housingDepth = requirePositive$6(options.housingDepth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 1.8), "housingDepth");
|
|
52148
|
+
const shaftOverhang = requirePositive$6(options.shaftOverhang ?? Math.max(8, bearingOuterDiameter * 0.45), "shaftOverhang");
|
|
52149
|
+
const shoulderDiameter = requirePositive$6(options.shoulderDiameter ?? Math.max(shaftDiameter * 1.65, bearingInnerDiameter + 2), "shoulderDiameter");
|
|
52150
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(1.5, shaftDiameter * 0.32), "shoulderThickness");
|
|
52151
|
+
const segments = options.segments ?? 48;
|
|
52152
|
+
if (bearingOuterDiameter <= bearingInnerDiameter + Math.max(1, bearingOuterDiameter * 0.08)) {
|
|
52153
|
+
throw new Error("seatedBearingAssembly: bearingOuterDiameter leaves too little bearing wall around the bore");
|
|
52154
|
+
}
|
|
52155
|
+
if (shaftDiameter + shaftClearance >= bearingInnerDiameter) {
|
|
52156
|
+
throw new Error("seatedBearingAssembly: shaftDiameter plus shaftClearance must fit inside the bearing bore");
|
|
52157
|
+
}
|
|
52158
|
+
if (shoulderDiameter >= bearingOuterDiameter - runningClearance * 2) {
|
|
52159
|
+
throw new Error("seatedBearingAssembly: shoulderDiameter must stay smaller than the bearing outer race");
|
|
52160
|
+
}
|
|
52161
|
+
const pocketDiameter = bearingOuterDiameter + pocketClearance;
|
|
52162
|
+
const shaftBoreDiameter = shaftDiameter + shaftClearance;
|
|
52163
|
+
const totalHousingHeight = housingThickness + bossHeight;
|
|
52164
|
+
const pocketDepth = bearingWidth + runningClearance * 2;
|
|
52165
|
+
if (pocketDepth >= totalHousingHeight - runningClearance) {
|
|
52166
|
+
throw new Error("seatedBearingAssembly: housingThickness and bossHeight must leave a shoulder below the bearing pocket");
|
|
52167
|
+
}
|
|
52168
|
+
if (bossOuterDiameter <= pocketDiameter + Math.max(2, bearingOuterDiameter * 0.12)) {
|
|
52169
|
+
throw new Error("seatedBearingAssembly: bossOuterDiameter leaves too little wall around the bearing pocket");
|
|
52170
|
+
}
|
|
52171
|
+
if (housingWidth <= pocketDiameter + 6 || housingDepth <= pocketDiameter + 6) {
|
|
52172
|
+
throw new Error("seatedBearingAssembly: housing dimensions leave too little material around the bearing pocket");
|
|
52173
|
+
}
|
|
52174
|
+
if (shoulderThickness * 2 + runningClearance * 2 >= shaftOverhang) {
|
|
52175
|
+
throw new Error("seatedBearingAssembly: shaftOverhang must leave room for retaining collars outside the housing");
|
|
52176
|
+
}
|
|
52177
|
+
const pocketBottomZ = totalHousingHeight - pocketDepth;
|
|
52178
|
+
const bearingZ = pocketBottomZ + runningClearance;
|
|
52179
|
+
const lowerShoulderZ = -runningClearance - shoulderThickness;
|
|
52180
|
+
const upperShoulderZ = totalHousingHeight + runningClearance;
|
|
52181
|
+
const shaftLength = totalHousingHeight + shaftOverhang * 2;
|
|
52182
|
+
const bossFuseOverlap = Math.min(0.08, Math.max(0.02, bossHeight * 0.03));
|
|
52183
|
+
const bearingPocket = cylinder(pocketDepth + 0.4, pocketDiameter / 2, void 0, segments).translate(0, 0, pocketBottomZ - 0.2);
|
|
52184
|
+
const shaftBore = cylinder(totalHousingHeight + 1, shaftBoreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
52185
|
+
const housingBase = box(housingWidth, housingDepth, housingThickness).subtract(bearingPocket).subtract(shaftBore);
|
|
52186
|
+
const housingBoss = cylinder(bossHeight + bossFuseOverlap, bossOuterDiameter / 2, void 0, segments).translate(
|
|
52187
|
+
0,
|
|
52188
|
+
0,
|
|
52189
|
+
housingThickness - bossFuseOverlap
|
|
52190
|
+
).subtract(bearingPocket);
|
|
52191
|
+
const housing = union(housingBase, housingBoss).color("#475569");
|
|
52192
|
+
const bearingRing = tubeAlongZ(bearingWidth, bearingOuterDiameter / 2, bearingInnerDiameter / 2, segments);
|
|
52193
|
+
const shieldInset = Math.min(bearingWidth * 0.18, 0.7);
|
|
52194
|
+
const shieldOuterRadius = bearingOuterDiameter / 2 - Math.max(0.45, (bearingOuterDiameter - bearingInnerDiameter) * 0.08);
|
|
52195
|
+
const shieldInnerRadius = bearingInnerDiameter / 2 + Math.max(0.2, (bearingOuterDiameter - bearingInnerDiameter) * 0.035);
|
|
52196
|
+
const bearingShield = shieldOuterRadius > shieldInnerRadius + 0.2 ? union(
|
|
52197
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(0, 0, shieldInset),
|
|
52198
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(
|
|
52199
|
+
0,
|
|
52200
|
+
0,
|
|
52201
|
+
bearingWidth - shieldInset - Math.min(0.35, bearingWidth * 0.08)
|
|
52202
|
+
)
|
|
52203
|
+
) : null;
|
|
52204
|
+
const bearing = (bearingShield ? union(bearingRing, bearingShield) : bearingRing).translate(0, 0, bearingZ).color("#111827");
|
|
52205
|
+
const shaftCore = cylinder(shaftLength, shaftDiameter / 2, void 0, segments).translate(0, 0, -shaftOverhang);
|
|
52206
|
+
const lowerShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, lowerShoulderZ);
|
|
52207
|
+
const upperShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, upperShoulderZ);
|
|
52208
|
+
const shaft = union(shaftCore, lowerShoulder, upperShoulder).color("#cbd5e1");
|
|
52209
|
+
const parts = [
|
|
52210
|
+
{ name: "bearing housing with counterbore pocket and shoulder", shape: housing },
|
|
52211
|
+
{ name: "purchased radial bearing seated in counterbore", shape: bearing },
|
|
52212
|
+
{ name: "shaft through bearing bore with retaining collars", shape: shaft }
|
|
52213
|
+
];
|
|
52214
|
+
return {
|
|
52215
|
+
parts,
|
|
52216
|
+
housing,
|
|
52217
|
+
bearing,
|
|
52218
|
+
shaft,
|
|
52219
|
+
cutters: {
|
|
52220
|
+
bearingPocket,
|
|
52221
|
+
shaftBore
|
|
52222
|
+
},
|
|
52223
|
+
dims: {
|
|
52224
|
+
bearingOuterDiameter,
|
|
52225
|
+
bearingInnerDiameter,
|
|
52226
|
+
bearingWidth,
|
|
52227
|
+
shaftDiameter,
|
|
52228
|
+
housingWidth,
|
|
52229
|
+
housingDepth,
|
|
52230
|
+
housingThickness,
|
|
52231
|
+
bossOuterDiameter,
|
|
52232
|
+
bossHeight,
|
|
52233
|
+
totalHousingHeight,
|
|
52234
|
+
pocketDiameter,
|
|
52235
|
+
pocketDepth,
|
|
52236
|
+
shaftBoreDiameter,
|
|
52237
|
+
runningClearance,
|
|
52238
|
+
shaftLength,
|
|
52239
|
+
shoulderDiameter,
|
|
52240
|
+
shoulderThickness
|
|
52241
|
+
}
|
|
52242
|
+
};
|
|
52243
|
+
}
|
|
52244
|
+
function cableGlandAnchorAssembly(options) {
|
|
52245
|
+
const cableDiameter = requirePositive$6(options.cableDiameter, "cableDiameter");
|
|
52246
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
52247
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? Math.max(54, cableDiameter * 7), "panelWidth");
|
|
52248
|
+
const panelHeight = requirePositive$6(options.panelHeight ?? Math.max(38, cableDiameter * 5), "panelHeight");
|
|
52249
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52250
|
+
const panelHoleClearance = requirePositive$6(options.panelHoleClearance ?? 0.25, "panelHoleClearance");
|
|
52251
|
+
const cableBoreDiameter = cableDiameter + runningClearance * 2;
|
|
52252
|
+
const glandOuterDiameter = requirePositive$6(options.glandOuterDiameter ?? cableDiameter + Math.max(6, cableDiameter * 0.9), "glandOuterDiameter");
|
|
52253
|
+
const nutOuterDiameter = requirePositive$6(options.nutOuterDiameter ?? glandOuterDiameter + Math.max(6, cableDiameter * 0.8), "nutOuterDiameter");
|
|
52254
|
+
const nutThickness = requirePositive$6(options.nutThickness ?? Math.max(4, cableDiameter * 0.8), "nutThickness");
|
|
52255
|
+
const flangeDiameter = requirePositive$6(options.flangeDiameter ?? glandOuterDiameter + Math.max(5, cableDiameter * 0.7), "flangeDiameter");
|
|
52256
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? Math.max(2, panelThickness * 0.45), "flangeThickness");
|
|
52257
|
+
const minGlandLength = panelThickness + nutThickness + flangeThickness + runningClearance * 4;
|
|
52258
|
+
const glandLength = requirePositive$6(options.glandLength ?? minGlandLength + Math.max(8, cableDiameter), "glandLength");
|
|
52259
|
+
const cableLength = requirePositive$6(options.cableLength ?? glandLength + Math.max(36, cableDiameter * 5), "cableLength");
|
|
52260
|
+
const segments = options.segments ?? 40;
|
|
52261
|
+
if (glandOuterDiameter <= cableBoreDiameter + Math.max(1.2, cableDiameter * 0.18)) {
|
|
52262
|
+
throw new Error("cableGlandAnchorAssembly: glandOuterDiameter leaves too little wall around the cable bore");
|
|
52263
|
+
}
|
|
52264
|
+
if (nutOuterDiameter <= glandOuterDiameter + Math.max(1.5, cableDiameter * 0.2)) {
|
|
52265
|
+
throw new Error("cableGlandAnchorAssembly: nutOuterDiameter must leave material around the gland body");
|
|
52266
|
+
}
|
|
52267
|
+
if (flangeDiameter <= glandOuterDiameter + Math.max(1.2, cableDiameter * 0.16)) {
|
|
52268
|
+
throw new Error("cableGlandAnchorAssembly: flangeDiameter must be larger than the gland body");
|
|
52269
|
+
}
|
|
52270
|
+
if (panelWidth <= flangeDiameter + 8 || panelHeight <= flangeDiameter + 8) {
|
|
52271
|
+
throw new Error("cableGlandAnchorAssembly: panel dimensions leave too little material around the gland hole");
|
|
52272
|
+
}
|
|
52273
|
+
if (glandLength <= minGlandLength) {
|
|
52274
|
+
throw new Error("cableGlandAnchorAssembly: glandLength must span the panel, flange, compression nut, and clearances");
|
|
52275
|
+
}
|
|
52276
|
+
if (cableLength <= glandLength + runningClearance * 2) {
|
|
52277
|
+
throw new Error("cableGlandAnchorAssembly: cableLength must extend beyond the gland body");
|
|
52278
|
+
}
|
|
52279
|
+
const panelHoleDiameter = glandOuterDiameter + panelHoleClearance * 2;
|
|
52280
|
+
const glandOuterRadius = glandOuterDiameter / 2;
|
|
52281
|
+
const cableBoreRadius = cableBoreDiameter / 2;
|
|
52282
|
+
const faceClearance = Math.min(0.05, runningClearance * 0.15);
|
|
52283
|
+
const flangePocketDepth = Math.min(Math.max(0.35, panelThickness * 0.18), panelThickness * 0.4, flangeThickness * 0.55);
|
|
52284
|
+
const panelHole = cylinderAlongX(panelThickness + 0.8, panelHoleDiameter / 2, 0, segments);
|
|
52285
|
+
const flangeSeatPocket = cylinderAlongX(
|
|
52286
|
+
flangePocketDepth + 0.2,
|
|
52287
|
+
flangeDiameter / 2 + panelHoleClearance,
|
|
52288
|
+
panelThickness / 2 - flangePocketDepth / 2,
|
|
52289
|
+
segments
|
|
52290
|
+
);
|
|
52291
|
+
const cableBore = cylinderAlongX(glandLength + 0.8, cableBoreRadius, 0, segments);
|
|
52292
|
+
const panel = box(panelThickness, panelWidth, panelHeight).translate(0, 0, -panelHeight / 2).subtract(panelHole).subtract(flangeSeatPocket).color("#475569");
|
|
52293
|
+
const glandBody = tubeAlongX(glandLength, glandOuterRadius, cableBoreRadius, 0, segments);
|
|
52294
|
+
const flangeCenterX = panelThickness / 2 - flangePocketDepth + faceClearance + flangeThickness / 2;
|
|
52295
|
+
const flange = tubeAlongX(flangeThickness, flangeDiameter / 2, cableBoreRadius, flangeCenterX, segments);
|
|
52296
|
+
const gland = union(glandBody, flange).color("#94a3b8");
|
|
52297
|
+
const nutInnerRadius = glandOuterRadius + Math.min(0.12, runningClearance * 0.4);
|
|
52298
|
+
const nutCenterX = -panelThickness / 2 - faceClearance - nutThickness / 2;
|
|
52299
|
+
const compressionNut = tubeAlongX(nutThickness, nutOuterDiameter / 2, nutInnerRadius, nutCenterX, segments).color("#cbd5e1");
|
|
52300
|
+
const cable = cylinderAlongX(cableLength, cableDiameter / 2, 0, segments).color("#111827");
|
|
52301
|
+
const parts = [
|
|
52302
|
+
{ name: "panel with gland clearance hole", shape: panel },
|
|
52303
|
+
{ name: "hollow cable gland body with panel flange", shape: gland },
|
|
52304
|
+
{ name: "compression nut around gland body", shape: compressionNut },
|
|
52305
|
+
{ name: "routed cable through gland bore", shape: cable }
|
|
52306
|
+
];
|
|
52307
|
+
return {
|
|
52308
|
+
parts,
|
|
52309
|
+
panel,
|
|
52310
|
+
gland,
|
|
52311
|
+
compressionNut,
|
|
52312
|
+
cable,
|
|
52313
|
+
cutters: {
|
|
52314
|
+
panelHole,
|
|
52315
|
+
flangeSeatPocket,
|
|
52316
|
+
cableBore
|
|
52317
|
+
},
|
|
52318
|
+
dims: {
|
|
52319
|
+
cableDiameter,
|
|
52320
|
+
cableBoreDiameter,
|
|
52321
|
+
panelThickness,
|
|
52322
|
+
panelWidth,
|
|
52323
|
+
panelHeight,
|
|
52324
|
+
glandOuterDiameter,
|
|
52325
|
+
glandLength,
|
|
52326
|
+
nutOuterDiameter,
|
|
52327
|
+
nutThickness,
|
|
52328
|
+
flangeDiameter,
|
|
52329
|
+
flangeThickness,
|
|
52330
|
+
runningClearance,
|
|
52331
|
+
faceClearance,
|
|
52332
|
+
flangePocketDepth,
|
|
52333
|
+
panelHoleDiameter,
|
|
52334
|
+
cableLength
|
|
52335
|
+
}
|
|
52336
|
+
};
|
|
52337
|
+
}
|
|
52338
|
+
function hoseBarbPortAssembly(options) {
|
|
52339
|
+
const hoseInnerDiameter = requirePositive$6(options.hoseInnerDiameter, "hoseInnerDiameter");
|
|
52340
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.18, "runningClearance");
|
|
52341
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
52342
|
+
const barbRootDiameter = requirePositive$6(
|
|
52343
|
+
options.barbRootDiameter ?? Math.max(1, hoseInnerDiameter - Math.max(0.25, hoseInnerDiameter * 0.06)),
|
|
52344
|
+
"barbRootDiameter"
|
|
52345
|
+
);
|
|
52346
|
+
const barbPeakDiameter = requirePositive$6(
|
|
52347
|
+
options.barbPeakDiameter ?? hoseInnerDiameter + Math.max(0.65, hoseInnerDiameter * 0.12),
|
|
52348
|
+
"barbPeakDiameter"
|
|
52349
|
+
);
|
|
52350
|
+
const installedHoseBoreDiameter = barbPeakDiameter + runningClearance * 2;
|
|
52351
|
+
const hoseOuterDiameter = requirePositive$6(
|
|
52352
|
+
options.hoseOuterDiameter ?? Math.max(installedHoseBoreDiameter + 2.4, hoseInnerDiameter + Math.max(3, hoseInnerDiameter * 0.55)),
|
|
52353
|
+
"hoseOuterDiameter"
|
|
52354
|
+
);
|
|
52355
|
+
const fluidBoreDiameter = requirePositive$6(options.fluidBoreDiameter ?? hoseInnerDiameter * 0.65, "fluidBoreDiameter");
|
|
52356
|
+
const blockThickness = requirePositive$6(options.blockThickness ?? Math.max(7, hoseInnerDiameter * 1.2), "blockThickness");
|
|
52357
|
+
const barbCount = options.barbCount ?? 3;
|
|
52358
|
+
const barbLength = requirePositive$6(options.barbLength ?? Math.max(2.6, hoseInnerDiameter * 0.55), "barbLength");
|
|
52359
|
+
const barbStackLength = barbCount * barbLength;
|
|
52360
|
+
const shoulderDiameter = requirePositive$6(
|
|
52361
|
+
options.shoulderDiameter ?? barbPeakDiameter + Math.max(4, hoseInnerDiameter * 0.65),
|
|
52362
|
+
"shoulderDiameter"
|
|
52363
|
+
);
|
|
52364
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(2, hoseInnerDiameter * 0.35), "shoulderThickness");
|
|
52365
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? shoulderDiameter + Math.max(4, hoseInnerDiameter * 0.6), "bossDiameter");
|
|
52366
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2.4, hoseInnerDiameter * 0.45), "bossHeight");
|
|
52367
|
+
const blockWidth = requirePositive$6(options.blockWidth ?? bossDiameter + Math.max(14, hoseInnerDiameter * 2.4), "blockWidth");
|
|
52368
|
+
const blockHeight = requirePositive$6(options.blockHeight ?? bossDiameter + Math.max(12, hoseInnerDiameter * 2.1), "blockHeight");
|
|
52369
|
+
const hoseLength = requirePositive$6(options.hoseLength ?? barbStackLength + Math.max(32, hoseInnerDiameter * 5), "hoseLength");
|
|
52370
|
+
const clampWidth = requirePositive$6(options.clampWidth ?? Math.max(4, hoseOuterDiameter * 0.45), "clampWidth");
|
|
52371
|
+
const clampThickness = requirePositive$6(options.clampThickness ?? 0.9, "clampThickness");
|
|
52372
|
+
const segments = options.segments ?? 40;
|
|
52373
|
+
if (!Number.isInteger(barbCount) || barbCount < 1 || barbCount > 8) {
|
|
52374
|
+
throw new Error("hoseBarbPortAssembly: barbCount must be an integer from 1 to 8");
|
|
52375
|
+
}
|
|
52376
|
+
if (barbPeakDiameter <= hoseInnerDiameter) {
|
|
52377
|
+
throw new Error("hoseBarbPortAssembly: barbPeakDiameter must exceed hoseInnerDiameter so the barb retains the hose");
|
|
52378
|
+
}
|
|
52379
|
+
if (barbRootDiameter >= barbPeakDiameter - Math.max(0.25, hoseInnerDiameter * 0.04)) {
|
|
52380
|
+
throw new Error("hoseBarbPortAssembly: barbRootDiameter must leave a visible barb rise");
|
|
52381
|
+
}
|
|
52382
|
+
if (fluidBoreDiameter >= barbRootDiameter - Math.max(0.8, hoseInnerDiameter * 0.12)) {
|
|
52383
|
+
throw new Error("hoseBarbPortAssembly: fluidBoreDiameter leaves too little wall in the barb fitting");
|
|
52384
|
+
}
|
|
52385
|
+
if (hoseOuterDiameter <= installedHoseBoreDiameter + Math.max(1.2, hoseInnerDiameter * 0.16)) {
|
|
52386
|
+
throw new Error("hoseBarbPortAssembly: hoseOuterDiameter leaves too little hose wall around the installed barb envelope");
|
|
52387
|
+
}
|
|
52388
|
+
if (shoulderDiameter <= barbPeakDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
52389
|
+
throw new Error("hoseBarbPortAssembly: shoulderDiameter must be larger than the barb peaks");
|
|
52390
|
+
}
|
|
52391
|
+
if (bossDiameter <= shoulderDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
52392
|
+
throw new Error("hoseBarbPortAssembly: bossDiameter must leave material around the shoulder seat");
|
|
52393
|
+
}
|
|
52394
|
+
if (blockWidth <= bossDiameter + 8 || blockHeight <= bossDiameter + 8) {
|
|
52395
|
+
throw new Error("hoseBarbPortAssembly: receiver block dimensions leave too little material around the port boss");
|
|
52396
|
+
}
|
|
52397
|
+
const portBoreDiameter = barbRootDiameter + runningClearance * 2;
|
|
52398
|
+
const portBore = cylinderAlongX(blockThickness + bossHeight + 0.8, portBoreDiameter / 2, bossHeight / 2, segments);
|
|
52399
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
52400
|
+
const bossCenterX = blockThickness / 2 + bossHeight / 2 - fuseOverlap;
|
|
52401
|
+
const receiver = union(
|
|
52402
|
+
box(blockThickness, blockWidth, blockHeight).translate(0, 0, -blockHeight / 2),
|
|
52403
|
+
cylinderAlongX(bossHeight + fuseOverlap, bossDiameter / 2, bossCenterX, segments)
|
|
52404
|
+
).subtract(portBore).color("#475569");
|
|
52405
|
+
const bossFaceX = blockThickness / 2 + bossHeight;
|
|
52406
|
+
const shoulderCenterX = bossFaceX + faceClearance + shoulderThickness / 2;
|
|
52407
|
+
const barbStartX = shoulderCenterX + shoulderThickness / 2;
|
|
52408
|
+
const fittingStartX = -blockThickness / 2 - runningClearance;
|
|
52409
|
+
const fittingEndX = barbStartX + barbStackLength;
|
|
52410
|
+
const fittingCore = tubeAlongX(fittingEndX - fittingStartX, barbRootDiameter / 2, fluidBoreDiameter / 2, (fittingStartX + fittingEndX) / 2, segments);
|
|
52411
|
+
const shoulder = tubeAlongX(shoulderThickness, shoulderDiameter / 2, fluidBoreDiameter / 2, shoulderCenterX, segments);
|
|
52412
|
+
const barbSolids = [];
|
|
52413
|
+
const ridgeLength = Math.max(0.8, Math.min(barbLength * 0.45, hoseInnerDiameter * 0.28));
|
|
52414
|
+
for (let index2 = 0; index2 < barbCount; index2 += 1) {
|
|
52415
|
+
const startX = barbStartX + index2 * barbLength;
|
|
52416
|
+
const ridgeCenterX = startX + barbLength - ridgeLength / 2;
|
|
52417
|
+
barbSolids.push(tubeAlongX(ridgeLength, barbPeakDiameter / 2, fluidBoreDiameter / 2, ridgeCenterX, segments));
|
|
52418
|
+
}
|
|
52419
|
+
const fitting = union(fittingCore, shoulder, ...barbSolids).color("#94a3b8");
|
|
52420
|
+
const hoseStartX = barbStartX + faceClearance;
|
|
52421
|
+
const hoseCenterX = hoseStartX + hoseLength / 2;
|
|
52422
|
+
const installedHoseBore = cylinderAlongX(hoseLength + 0.8, installedHoseBoreDiameter / 2, hoseCenterX, segments);
|
|
52423
|
+
const hose = tubeAlongX(hoseLength, hoseOuterDiameter / 2, installedHoseBoreDiameter / 2, hoseCenterX, segments).color("#111827");
|
|
52424
|
+
const clampCenterX = barbStartX + Math.min(barbStackLength * 0.55, Math.max(barbLength, clampWidth));
|
|
52425
|
+
const clamp2 = tubeAlongX(
|
|
52426
|
+
clampWidth,
|
|
52427
|
+
hoseOuterDiameter / 2 + clampThickness,
|
|
52428
|
+
hoseOuterDiameter / 2 + Math.min(0.08, runningClearance * 0.45),
|
|
52429
|
+
clampCenterX,
|
|
52430
|
+
segments
|
|
52431
|
+
).color("#cbd5e1");
|
|
52432
|
+
const parts = [
|
|
52433
|
+
{ name: "bored pump or filter body with raised hose-port boss", shape: receiver },
|
|
52434
|
+
{ name: "hollow hose barb fitting with shoulder and retention ridges", shape: fitting },
|
|
52435
|
+
{ name: "installed flexible hose over barb tail", shape: hose },
|
|
52436
|
+
{ name: "clamp band over hose and barb ridges", shape: clamp2 }
|
|
52437
|
+
];
|
|
52438
|
+
return {
|
|
52439
|
+
parts,
|
|
52440
|
+
receiver,
|
|
52441
|
+
fitting,
|
|
52442
|
+
hose,
|
|
52443
|
+
clamp: clamp2,
|
|
52444
|
+
cutters: {
|
|
52445
|
+
portBore,
|
|
52446
|
+
installedHoseBore
|
|
52447
|
+
},
|
|
52448
|
+
dims: {
|
|
52449
|
+
hoseInnerDiameter,
|
|
52450
|
+
hoseOuterDiameter,
|
|
52451
|
+
installedHoseBoreDiameter,
|
|
52452
|
+
blockThickness,
|
|
52453
|
+
blockWidth,
|
|
52454
|
+
blockHeight,
|
|
52455
|
+
bossDiameter,
|
|
52456
|
+
bossHeight,
|
|
52457
|
+
fluidBoreDiameter,
|
|
52458
|
+
barbRootDiameter,
|
|
52459
|
+
barbPeakDiameter,
|
|
52460
|
+
barbCount,
|
|
52461
|
+
barbLength,
|
|
52462
|
+
barbStackLength,
|
|
52463
|
+
shoulderDiameter,
|
|
52464
|
+
shoulderThickness,
|
|
52465
|
+
hoseLength,
|
|
52466
|
+
clampWidth,
|
|
52467
|
+
clampThickness,
|
|
52468
|
+
runningClearance,
|
|
52469
|
+
faceClearance
|
|
52470
|
+
}
|
|
52471
|
+
};
|
|
52472
|
+
}
|
|
52473
|
+
function routedTubeClipAssembly(options) {
|
|
52474
|
+
const tubeDiameter = requirePositive$6(options.tubeDiameter, "tubeDiameter");
|
|
52475
|
+
const tubeLength = requirePositive$6(options.tubeLength ?? 120, "tubeLength");
|
|
52476
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
52477
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52478
|
+
const screwSize = options.screwSize ?? "M3";
|
|
52479
|
+
const segments = options.segments ?? 32;
|
|
52480
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52481
|
+
if (!sizeData) throw new Error(`routedTubeClipAssembly: unsupported screwSize "${screwSize}"`);
|
|
52482
|
+
const clipCount = options.clipCount ?? 3;
|
|
52483
|
+
if (!Number.isInteger(clipCount) || clipCount < 1 || clipCount > 8) {
|
|
52484
|
+
throw new Error("routedTubeClipAssembly: clipCount must be an integer from 1 to 8");
|
|
52485
|
+
}
|
|
52486
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52487
|
+
const screwHeadDiameter = sizeData.head;
|
|
52488
|
+
const tubeBoreDiameter = tubeDiameter + runningClearance * 2;
|
|
52489
|
+
const clipWallThickness = requirePositive$6(
|
|
52490
|
+
options.clipWallThickness ?? Math.max(screwHeadDiameter + 1.2, tubeDiameter * 0.45, 5),
|
|
52491
|
+
"clipWallThickness"
|
|
52492
|
+
);
|
|
52493
|
+
const clipWidth = requirePositive$6(options.clipWidth ?? Math.max(screwHeadDiameter + 3, tubeDiameter * 1.4, 10), "clipWidth");
|
|
52494
|
+
const clipDepth = tubeBoreDiameter + clipWallThickness * 2;
|
|
52495
|
+
const bottomWall = Math.max(1.2, clipWallThickness * 0.35);
|
|
52496
|
+
const topWall = Math.max(2, clipWallThickness * 0.45);
|
|
52497
|
+
const clipHeight = bottomWall + tubeBoreDiameter + topWall;
|
|
52498
|
+
const tubeCenterZ = panelThickness + bottomWall + tubeBoreDiameter / 2;
|
|
52499
|
+
const panelLength = requirePositive$6(options.panelLength ?? tubeLength + 24, "panelLength");
|
|
52500
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? clipDepth + Math.max(14, screwHeadDiameter * 2), "panelWidth");
|
|
52501
|
+
if (tubeLength <= clipWidth + 8) {
|
|
52502
|
+
throw new Error("routedTubeClipAssembly: tubeLength must leave visible tube beyond the clip body");
|
|
52503
|
+
}
|
|
52504
|
+
const defaultSpacing = clipCount === 1 ? 0 : Math.max(clipWidth + 8, (tubeLength - clipWidth * 2) / (clipCount - 1));
|
|
52505
|
+
const clipSpacing = options.clipSpacing === void 0 ? defaultSpacing : requirePositive$6(options.clipSpacing, "clipSpacing");
|
|
52506
|
+
const clipCenters = Array.from({ length: clipCount }, (_2, index2) => (index2 - (clipCount - 1) / 2) * clipSpacing);
|
|
52507
|
+
const maxClipExtent = Math.max(...clipCenters.map((x2) => Math.abs(x2) + clipWidth / 2));
|
|
52508
|
+
if (maxClipExtent > tubeLength / 2 - 2) {
|
|
52509
|
+
throw new Error("routedTubeClipAssembly: clipSpacing places a clip beyond the routed tube length");
|
|
52510
|
+
}
|
|
52511
|
+
if (maxClipExtent > panelLength / 2 - 2) {
|
|
52512
|
+
throw new Error("routedTubeClipAssembly: panelLength is too short for the clip pattern");
|
|
52513
|
+
}
|
|
52514
|
+
const boreRadius = tubeBoreDiameter / 2;
|
|
52515
|
+
const screwY = boreRadius + clipWallThickness / 2;
|
|
52516
|
+
if (screwY + screwHeadDiameter / 2 > clipDepth / 2 - 0.2) {
|
|
52517
|
+
throw new Error("routedTubeClipAssembly: clipWallThickness leaves too little land for screw heads");
|
|
52518
|
+
}
|
|
52519
|
+
if (clipDepth > panelWidth - Math.max(4, screwHeadDiameter * 0.5)) {
|
|
52520
|
+
throw new Error("routedTubeClipAssembly: panelWidth leaves too little material beside the clips");
|
|
52521
|
+
}
|
|
52522
|
+
const screwPositions = clipCenters.flatMap((x2) => [
|
|
52523
|
+
[x2, -screwY],
|
|
52524
|
+
[x2, screwY]
|
|
52525
|
+
]);
|
|
52526
|
+
const screwClearanceDiameter = Math.max(sizeData.loose, screwDiameter + 0.8);
|
|
52527
|
+
const panelThreadEnvelopeDiameter = screwClearanceDiameter;
|
|
52528
|
+
const clipTopZ = panelThickness + clipHeight;
|
|
52529
|
+
const clipTubeBores = union(
|
|
52530
|
+
...clipCenters.map((x2) => cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ))
|
|
52531
|
+
);
|
|
52532
|
+
const clipScrewClearances = union(
|
|
52533
|
+
...screwPositions.map(([x2, y2]) => cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, y2, panelThickness - 0.4))
|
|
52534
|
+
);
|
|
52535
|
+
const panelThreadEnvelopes = union(
|
|
52536
|
+
...screwPositions.map(([x2, y2]) => cylinder(panelThickness + 0.8, panelThreadEnvelopeDiameter / 2, void 0, segments).translate(x2, y2, -0.4))
|
|
52537
|
+
);
|
|
52538
|
+
const panel = box(panelLength, panelWidth, panelThickness).subtract(panelThreadEnvelopes).color("#475569");
|
|
52539
|
+
const tube2 = cylinderAlongX(tubeLength, tubeDiameter / 2, 0, segments).translate(0, 0, tubeCenterZ).color("#0f172a");
|
|
52540
|
+
const clips = clipCenters.map((x2) => {
|
|
52541
|
+
const body = box(clipWidth, clipDepth, clipHeight).translate(x2, 0, panelThickness);
|
|
52542
|
+
const tubeBore = cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ);
|
|
52543
|
+
const screwHoles = union(
|
|
52544
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, -screwY, panelThickness - 0.4),
|
|
52545
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, screwY, panelThickness - 0.4)
|
|
52546
|
+
);
|
|
52547
|
+
return body.subtract(tubeBore).subtract(screwHoles).color("#94a3b8");
|
|
52548
|
+
});
|
|
52549
|
+
const screwLength = clipHeight + panelThickness * 0.65;
|
|
52550
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
52551
|
+
const screwBlank = union(
|
|
52552
|
+
cylinder(screwLength, screwDiameter / 2, void 0, segments).translate(0, 0, clipTopZ - screwLength),
|
|
52553
|
+
cylinder(screwHeadHeight, screwHeadDiameter / 2, void 0, segments).translate(0, 0, clipTopZ)
|
|
52554
|
+
).color("#cbd5e1");
|
|
52555
|
+
const screws = screwPositions.map(([x2, y2]) => screwBlank.translate(x2, y2, 0));
|
|
52556
|
+
const parts = [
|
|
52557
|
+
{ name: "panel with tube-clip screw receiving holes", shape: panel },
|
|
52558
|
+
{ name: "routed flexible tube through retained clip bores", shape: tube2 },
|
|
52559
|
+
...clips.map((shape, index2) => ({ name: `saddle tube clip ${index2 + 1} with through-bore`, shape })),
|
|
52560
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} tube clip screw ${index2 + 1}`, shape }))
|
|
52561
|
+
];
|
|
52562
|
+
return {
|
|
52563
|
+
parts,
|
|
52564
|
+
panel,
|
|
52565
|
+
tube: tube2,
|
|
52566
|
+
clips,
|
|
52567
|
+
screws,
|
|
52568
|
+
clipCenters,
|
|
52569
|
+
screwPositions,
|
|
52570
|
+
cutters: {
|
|
52571
|
+
clipTubeBores,
|
|
52572
|
+
clipScrewClearances,
|
|
52573
|
+
panelThreadEnvelopes
|
|
52574
|
+
},
|
|
52575
|
+
dims: {
|
|
52576
|
+
tubeDiameter,
|
|
52577
|
+
tubeLength,
|
|
52578
|
+
tubeBoreDiameter,
|
|
52579
|
+
panelLength,
|
|
52580
|
+
panelWidth,
|
|
52581
|
+
panelThickness,
|
|
52582
|
+
clipCount,
|
|
52583
|
+
clipWidth,
|
|
52584
|
+
clipDepth,
|
|
52585
|
+
clipHeight,
|
|
52586
|
+
clipWallThickness,
|
|
52587
|
+
tubeCenterZ,
|
|
52588
|
+
screwSize,
|
|
52589
|
+
screwDiameter,
|
|
52590
|
+
screwHeadDiameter,
|
|
52591
|
+
screwLength,
|
|
52592
|
+
screwClearanceDiameter,
|
|
52593
|
+
panelThreadEnvelopeDiameter,
|
|
52594
|
+
runningClearance
|
|
52595
|
+
}
|
|
52596
|
+
};
|
|
52597
|
+
}
|
|
52598
|
+
function pcbTerminalBlockAssembly(options = {}) {
|
|
52599
|
+
const terminalCount = options.terminalCount ?? 4;
|
|
52600
|
+
if (!Number.isInteger(terminalCount) || terminalCount < 1 || terminalCount > 24) {
|
|
52601
|
+
throw new Error("pcbTerminalBlockAssembly: terminalCount must be an integer from 1 to 24");
|
|
52602
|
+
}
|
|
52603
|
+
const terminalPitch = requirePositive$6(options.terminalPitch ?? 5.08, "terminalPitch");
|
|
52604
|
+
const terminalBlockWidth = terminalPitch * terminalCount + 3;
|
|
52605
|
+
const boardWidth = requirePositive$6(options.boardWidth ?? Math.max(50, terminalBlockWidth + 28), "boardWidth");
|
|
52606
|
+
const boardDepth = requirePositive$6(options.boardDepth ?? 38, "boardDepth");
|
|
52607
|
+
const boardThickness = requirePositive$6(options.boardThickness ?? 1.6, "boardThickness");
|
|
52608
|
+
const backplateThickness = requirePositive$6(options.backplateThickness ?? 3, "backplateThickness");
|
|
52609
|
+
const backplateMargin = requirePositive$6(options.backplateMargin ?? 5, "backplateMargin");
|
|
52610
|
+
const standoffHeight = requirePositive$6(options.standoffHeight ?? 6, "standoffHeight");
|
|
52611
|
+
const screwSize = options.screwSize ?? "M3";
|
|
52612
|
+
const segments = options.segments ?? 28;
|
|
52613
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52614
|
+
if (!sizeData) throw new Error(`pcbTerminalBlockAssembly: unsupported screwSize "${screwSize}"`);
|
|
52615
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52616
|
+
const screwHeadDiameter = sizeData.head;
|
|
52617
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
52618
|
+
const standoffDiameter = requirePositive$6(
|
|
52619
|
+
options.standoffDiameter ?? Math.max(screwHeadDiameter * 1.45, sizeData.normal + 3),
|
|
52620
|
+
"standoffDiameter"
|
|
52621
|
+
);
|
|
52622
|
+
const [mountInsetX, mountInsetY] = resolveBoltInset(
|
|
52623
|
+
options.mountingInset,
|
|
52624
|
+
Math.max(standoffDiameter / 2 + 1.2, screwHeadDiameter * 0.75)
|
|
52625
|
+
);
|
|
52626
|
+
if (mountInsetX * 2 >= boardWidth || mountInsetY * 2 >= boardDepth) {
|
|
52627
|
+
throw new Error("pcbTerminalBlockAssembly: mountingInset leaves no room for the PCB mounting pattern");
|
|
52628
|
+
}
|
|
52629
|
+
const terminalBlockDepth = requirePositive$6(options.terminalBlockDepth ?? 10, "terminalBlockDepth");
|
|
52630
|
+
const terminalBlockHeight = requirePositive$6(options.terminalBlockHeight ?? 9, "terminalBlockHeight");
|
|
52631
|
+
const terminalEdgeInset = requirePositive$6(options.terminalEdgeInset ?? 5, "terminalEdgeInset");
|
|
52632
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 0.9, "pinDiameter");
|
|
52633
|
+
const pinClearance = requirePositive$6(options.pinClearance ?? 0.25, "pinClearance");
|
|
52634
|
+
const pinTailLength = requireNonNegative(options.pinTailLength ?? 0, "pinTailLength");
|
|
52635
|
+
const wirePortDiameter = requirePositive$6(options.wirePortDiameter ?? 2.6, "wirePortDiameter");
|
|
52636
|
+
const pinHoleDiameter = pinDiameter + pinClearance;
|
|
52637
|
+
const terminalCenterY = -boardDepth / 2 + terminalEdgeInset + terminalBlockDepth / 2;
|
|
52638
|
+
const pinY = terminalCenterY + terminalBlockDepth * 0.24;
|
|
52639
|
+
const firstPinX = -((terminalCount - 1) * terminalPitch) / 2;
|
|
52640
|
+
const pinPositions = Array.from({ length: terminalCount }, (_2, index2) => [firstPinX + index2 * terminalPitch, pinY]);
|
|
52641
|
+
const mountingPositions = [
|
|
52642
|
+
[-boardWidth / 2 + mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
52643
|
+
[boardWidth / 2 - mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
52644
|
+
[-boardWidth / 2 + mountInsetX, boardDepth / 2 - mountInsetY],
|
|
52645
|
+
[boardWidth / 2 - mountInsetX, boardDepth / 2 - mountInsetY]
|
|
52646
|
+
];
|
|
52647
|
+
if (terminalBlockWidth >= boardWidth - mountInsetX * 2) {
|
|
52648
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block is too wide for the PCB mounting pattern");
|
|
52649
|
+
}
|
|
52650
|
+
if (terminalEdgeInset + terminalBlockDepth >= boardDepth - mountInsetY * 2) {
|
|
52651
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block depth collides with the rear mounting datum");
|
|
52652
|
+
}
|
|
52653
|
+
if (pinHoleDiameter >= terminalPitch * 0.55) {
|
|
52654
|
+
throw new Error("pcbTerminalBlockAssembly: pinDiameter and pinClearance leave too little PCB web between terminal holes");
|
|
52655
|
+
}
|
|
52656
|
+
if (wirePortDiameter >= Math.min(terminalPitch * 0.72, terminalBlockHeight * 0.65)) {
|
|
52657
|
+
throw new Error("pcbTerminalBlockAssembly: wirePortDiameter is too large for the terminal pitch or body height");
|
|
52658
|
+
}
|
|
52659
|
+
for (const [index2, [x2, y2]] of [...mountingPositions, ...pinPositions].entries()) {
|
|
52660
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
52661
|
+
throw new Error(`pcbTerminalBlockAssembly: generated datum position ${index2} is not finite`);
|
|
52662
|
+
}
|
|
52663
|
+
}
|
|
52664
|
+
const backplateWidth = boardWidth + backplateMargin * 2;
|
|
52665
|
+
const backplateDepth = boardDepth + backplateMargin * 2;
|
|
52666
|
+
const boardBottomZ = backplateThickness + standoffHeight;
|
|
52667
|
+
const boardTopZ = boardBottomZ + boardThickness;
|
|
52668
|
+
const standoffOverlap = Math.min(0.08, standoffHeight * 0.03);
|
|
52669
|
+
const standoffThreadEnvelopeDiameter = Math.max(sizeData.loose, screwDiameter + 1);
|
|
52670
|
+
const standoffThreadEnvelope = cylinder(standoffHeight + 0.8, standoffThreadEnvelopeDiameter / 2, void 0, segments).translate(
|
|
52671
|
+
0,
|
|
52672
|
+
0,
|
|
52673
|
+
backplateThickness - 0.4
|
|
52674
|
+
);
|
|
52675
|
+
const standoffThreadEnvelopes = union(...mountingPositions.map(([x2, y2]) => standoffThreadEnvelope.translate(x2, y2, 0)));
|
|
52676
|
+
const standoff = cylinder(standoffHeight + standoffOverlap, standoffDiameter / 2, void 0, segments).translate(0, 0, backplateThickness - standoffOverlap).subtract(standoffThreadEnvelope);
|
|
52677
|
+
const standoffs = union(...mountingPositions.map(([x2, y2]) => standoff.translate(x2, y2, 0)));
|
|
52678
|
+
const backplate = union(box(backplateWidth, backplateDepth, backplateThickness), standoffs).color("#475569");
|
|
52679
|
+
const boardMountingHoleDiameter = sizeData.normal;
|
|
52680
|
+
const boardMountHole = cylinder(boardThickness + 0.8, boardMountingHoleDiameter / 2, void 0, segments).translate(
|
|
52681
|
+
0,
|
|
52682
|
+
0,
|
|
52683
|
+
boardBottomZ - 0.4
|
|
52684
|
+
);
|
|
52685
|
+
const pcbMountingHoles = union(...mountingPositions.map(([x2, y2]) => boardMountHole.translate(x2, y2, 0)));
|
|
52686
|
+
const pinHole = cylinder(boardThickness + 0.8, pinHoleDiameter / 2, void 0, segments).translate(0, 0, boardBottomZ - 0.4);
|
|
52687
|
+
const pcbPinHoles = union(...pinPositions.map(([x2, y2]) => pinHole.translate(x2, y2, 0)));
|
|
52688
|
+
const pcb = box(boardWidth, boardDepth, boardThickness).translate(0, 0, boardBottomZ).subtract(pcbMountingHoles).subtract(pcbPinHoles).color("#166534");
|
|
52689
|
+
const terminalBodyBlank = box(terminalBlockWidth, terminalBlockDepth, terminalBlockHeight).translate(0, terminalCenterY, boardTopZ);
|
|
52690
|
+
const wirePort = cylinderAlongY(terminalBlockDepth + 0.8, wirePortDiameter / 2, terminalCenterY, segments).translate(
|
|
52691
|
+
0,
|
|
52692
|
+
0,
|
|
52693
|
+
boardTopZ + terminalBlockHeight * 0.42
|
|
52694
|
+
);
|
|
52695
|
+
const wirePorts = union(...pinPositions.map(([x2]) => wirePort.translate(x2, 0, 0)));
|
|
52696
|
+
const clampScrewPockets = union(
|
|
52697
|
+
...pinPositions.map(
|
|
52698
|
+
([x2]) => cylinder(Math.max(0.6, terminalBlockHeight * 0.22), Math.min(terminalPitch * 0.22, wirePortDiameter * 0.42), void 0, segments).translate(
|
|
52699
|
+
x2,
|
|
52700
|
+
terminalCenterY + terminalBlockDepth * 0.12,
|
|
52701
|
+
boardTopZ + terminalBlockHeight * 0.76
|
|
52702
|
+
)
|
|
52703
|
+
)
|
|
52704
|
+
);
|
|
52705
|
+
const pinLength = boardThickness + pinTailLength + Math.min(0.6, terminalBlockHeight * 0.08);
|
|
52706
|
+
const pinStartZ = boardBottomZ - pinTailLength;
|
|
52707
|
+
const pins = union(...pinPositions.map(([x2, y2]) => cylinder(pinLength, pinDiameter / 2, void 0, segments).translate(x2, y2, pinStartZ)));
|
|
52708
|
+
const terminalBlock = union(terminalBodyBlank.subtract(wirePorts).subtract(clampScrewPockets), pins).color("#16a34a");
|
|
52709
|
+
const screwShaftLength = boardThickness + standoffHeight * 0.85;
|
|
52710
|
+
const mountingHardware = fastenerSet(screwSize, screwShaftLength, {
|
|
52711
|
+
washerUnderHead: false,
|
|
52712
|
+
washerUnderNut: false,
|
|
52713
|
+
fit: "normal",
|
|
52714
|
+
segments
|
|
52715
|
+
});
|
|
52716
|
+
const screws = mountingPositions.map(([x2, y2]) => mountingHardware.bolt.translate(x2, y2, boardTopZ).color("#cbd5e1"));
|
|
52717
|
+
const parts = [
|
|
52718
|
+
{ name: "electronics backplate with fused PCB standoffs", shape: backplate },
|
|
52719
|
+
{ name: "PCB with mounting holes and terminal pin clearances", shape: pcb },
|
|
52720
|
+
{ name: "seated purchased terminal block with through-board pins", shape: terminalBlock },
|
|
52721
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} PCB mounting screw ${index2 + 1}`, shape }))
|
|
52722
|
+
];
|
|
52723
|
+
return {
|
|
52724
|
+
parts,
|
|
52725
|
+
backplate,
|
|
52726
|
+
pcb,
|
|
52727
|
+
terminalBlock,
|
|
52728
|
+
screws,
|
|
52729
|
+
mountingPositions,
|
|
52730
|
+
pinPositions,
|
|
52731
|
+
cutters: {
|
|
52732
|
+
pcbMountingHoles,
|
|
52733
|
+
pcbPinHoles,
|
|
52734
|
+
standoffThreadEnvelopes
|
|
52735
|
+
},
|
|
52736
|
+
dims: {
|
|
52737
|
+
terminalCount,
|
|
52738
|
+
terminalPitch,
|
|
52739
|
+
boardWidth,
|
|
52740
|
+
boardDepth,
|
|
52741
|
+
boardThickness,
|
|
52742
|
+
backplateWidth,
|
|
52743
|
+
backplateDepth,
|
|
52744
|
+
backplateThickness,
|
|
52745
|
+
standoffHeight,
|
|
52746
|
+
standoffDiameter,
|
|
52747
|
+
screwSize,
|
|
52748
|
+
screwDiameter,
|
|
52749
|
+
screwHeadDiameter,
|
|
52750
|
+
screwHeadHeight,
|
|
52751
|
+
screwShaftLength,
|
|
52752
|
+
boardMountingHoleDiameter,
|
|
52753
|
+
standoffThreadEnvelopeDiameter,
|
|
52754
|
+
terminalBlockWidth,
|
|
52755
|
+
terminalBlockDepth,
|
|
52756
|
+
terminalBlockHeight,
|
|
52757
|
+
terminalEdgeInset,
|
|
52758
|
+
pinDiameter,
|
|
52759
|
+
pinClearance,
|
|
52760
|
+
pinHoleDiameter,
|
|
52761
|
+
pinTailLength,
|
|
52762
|
+
wirePortDiameter
|
|
52763
|
+
}
|
|
52764
|
+
};
|
|
52765
|
+
}
|
|
52766
|
+
function thumbScrewClampAssembly(options = {}) {
|
|
52767
|
+
const screwSize = options.screwSize ?? "M6";
|
|
52768
|
+
const segments = options.segments ?? 36;
|
|
52769
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52770
|
+
if (!sizeData) throw new Error(`thumbScrewClampAssembly: unsupported screwSize "${screwSize}"`);
|
|
52771
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52772
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52773
|
+
const faceClearance = requireNonNegative(options.faceClearance ?? 0, "faceClearance");
|
|
52774
|
+
const threadEnvelopeDiameter = Math.max(sizeData.normal, screwDiameter + runningClearance * 2);
|
|
52775
|
+
const pressurePadDiameter = requirePositive$6(
|
|
52776
|
+
options.pressurePadDiameter ?? Math.max(screwDiameter * 3.2, 18),
|
|
52777
|
+
"pressurePadDiameter"
|
|
52778
|
+
);
|
|
52779
|
+
const pressurePadThickness = requirePositive$6(
|
|
52780
|
+
options.pressurePadThickness ?? Math.max(screwDiameter * 0.72, 4),
|
|
52781
|
+
"pressurePadThickness"
|
|
52782
|
+
);
|
|
52783
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? Math.max(screwDiameter * 4.2, 24), "knobDiameter");
|
|
52784
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(screwDiameter * 0.9, 7), "knobThickness");
|
|
52785
|
+
const workpieceThickness = requirePositive$6(options.workpieceThickness ?? 18, "workpieceThickness");
|
|
52786
|
+
const workpieceDepth = requirePositive$6(options.workpieceDepth ?? Math.max(46, pressurePadDiameter * 1.5), "workpieceDepth");
|
|
52787
|
+
const workpieceHeight = requirePositive$6(options.workpieceHeight ?? Math.max(pressurePadDiameter * 1.35, 24), "workpieceHeight");
|
|
52788
|
+
const frameDepth = requirePositive$6(
|
|
52789
|
+
options.frameDepth ?? Math.max(workpieceDepth + 12, pressurePadDiameter + 16),
|
|
52790
|
+
"frameDepth"
|
|
52791
|
+
);
|
|
52792
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? Math.max(screwDiameter, 6), "baseThickness");
|
|
52793
|
+
const jawThickness = requirePositive$6(options.jawThickness ?? Math.max(screwDiameter * 1.35, 9), "jawThickness");
|
|
52794
|
+
const supportThickness = requirePositive$6(
|
|
52795
|
+
options.supportThickness ?? Math.max(screwDiameter * 1.8, 12),
|
|
52796
|
+
"supportThickness"
|
|
52797
|
+
);
|
|
52798
|
+
const bossLength = requirePositive$6(options.bossLength ?? Math.max(screwDiameter * 1.1, 8), "bossLength");
|
|
52799
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? Math.max(threadEnvelopeDiameter + 5, screwDiameter * 2.5), "bossDiameter");
|
|
52800
|
+
const exposedScrewLength = requirePositive$6(
|
|
52801
|
+
options.exposedScrewLength ?? Math.max(pressurePadDiameter * 0.45, screwDiameter * 2.2),
|
|
52802
|
+
"exposedScrewLength"
|
|
52803
|
+
);
|
|
52804
|
+
const screwCenterZ = baseThickness + Math.max(workpieceHeight * 0.52, pressurePadDiameter * 0.68);
|
|
52805
|
+
const frameHeight = requirePositive$6(
|
|
52806
|
+
options.frameHeight ?? screwCenterZ - baseThickness + pressurePadDiameter / 2 + Math.max(baseThickness, 7),
|
|
52807
|
+
"frameHeight"
|
|
52808
|
+
);
|
|
52809
|
+
if (workpieceDepth > frameDepth - 6) {
|
|
52810
|
+
throw new Error("thumbScrewClampAssembly: frameDepth must leave side material around the clamped workpiece");
|
|
52811
|
+
}
|
|
52812
|
+
if (pressurePadDiameter > frameDepth - 4) {
|
|
52813
|
+
throw new Error("thumbScrewClampAssembly: pressurePadDiameter is too large for the frame depth");
|
|
52814
|
+
}
|
|
52815
|
+
if (bossDiameter > frameDepth - 4) {
|
|
52816
|
+
throw new Error("thumbScrewClampAssembly: bossDiameter is too large for the frame depth");
|
|
52817
|
+
}
|
|
52818
|
+
if (screwCenterZ - pressurePadDiameter / 2 <= baseThickness + 0.5) {
|
|
52819
|
+
throw new Error("thumbScrewClampAssembly: pressure pad collides with the base bridge");
|
|
52820
|
+
}
|
|
52821
|
+
if (baseThickness + frameHeight - screwCenterZ <= pressurePadDiameter / 2 + 2) {
|
|
52822
|
+
throw new Error("thumbScrewClampAssembly: frameHeight leaves too little material above the screw axis");
|
|
52823
|
+
}
|
|
52824
|
+
if (threadEnvelopeDiameter + 4 > Math.min(frameDepth, frameHeight)) {
|
|
52825
|
+
throw new Error("thumbScrewClampAssembly: threaded boss bore leaves too little surrounding frame material");
|
|
52826
|
+
}
|
|
52827
|
+
const workpieceLeftFaceX = -workpieceThickness / 2;
|
|
52828
|
+
const workpieceRightFaceX = workpieceThickness / 2;
|
|
52829
|
+
const anvilOverlap = Math.min(0.35, pressurePadThickness * 0.18);
|
|
52830
|
+
const anvilPadCenterX = workpieceLeftFaceX - faceClearance - pressurePadThickness / 2;
|
|
52831
|
+
const pressurePadCenterX = workpieceRightFaceX + faceClearance + pressurePadThickness / 2;
|
|
52832
|
+
const fixedJawRightFaceX = anvilPadCenterX - pressurePadThickness / 2 + anvilOverlap;
|
|
52833
|
+
const fixedJawCenterX = fixedJawRightFaceX - jawThickness / 2;
|
|
52834
|
+
const pressurePadRightFaceX = pressurePadCenterX + pressurePadThickness / 2;
|
|
52835
|
+
const supportInnerFaceX = pressurePadRightFaceX + exposedScrewLength;
|
|
52836
|
+
const supportCenterX = supportInnerFaceX + supportThickness / 2;
|
|
52837
|
+
const supportOuterFaceX = supportInnerFaceX + supportThickness;
|
|
52838
|
+
const frameLeftFaceX = fixedJawCenterX - jawThickness / 2;
|
|
52839
|
+
const frameRightFaceX = supportOuterFaceX;
|
|
52840
|
+
const baseLength = frameRightFaceX - frameLeftFaceX;
|
|
52841
|
+
if (baseLength <= 0 || !Number.isFinite(baseLength)) {
|
|
52842
|
+
throw new Error("thumbScrewClampAssembly: generated clamp frame length is invalid");
|
|
52843
|
+
}
|
|
52844
|
+
const bossCenterX = supportInnerFaceX + (supportThickness + bossLength) / 2;
|
|
52845
|
+
const threadedBossBore = cylinderAlongX(supportThickness + bossLength + 1, threadEnvelopeDiameter / 2, bossCenterX, segments).translate(
|
|
52846
|
+
0,
|
|
52847
|
+
0,
|
|
52848
|
+
screwCenterZ
|
|
52849
|
+
);
|
|
52850
|
+
const frameOverlap = Math.min(0.12, baseThickness * 0.04);
|
|
52851
|
+
const base = box(baseLength, frameDepth, baseThickness).translate((frameLeftFaceX + frameRightFaceX) / 2, 0, 0);
|
|
52852
|
+
const fixedJaw = box(jawThickness, frameDepth, frameHeight + frameOverlap).translate(fixedJawCenterX, 0, baseThickness - frameOverlap);
|
|
52853
|
+
const support = box(supportThickness, frameDepth, frameHeight + frameOverlap).translate(supportCenterX, 0, baseThickness - frameOverlap);
|
|
52854
|
+
const boss2 = cylinderAlongX(supportThickness + bossLength, bossDiameter / 2, bossCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52855
|
+
const anvilPad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, anvilPadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52856
|
+
const frame = union(base, fixedJaw, support, boss2, anvilPad).subtract(threadedBossBore).color("#475569");
|
|
52857
|
+
const workpieceBottomZ = screwCenterZ - workpieceHeight / 2;
|
|
52858
|
+
const workpiece = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ).color("#a16207");
|
|
52859
|
+
const pressurePad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, pressurePadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52860
|
+
const knobCenterX = supportOuterFaceX + bossLength + runningClearance + knobThickness / 2;
|
|
52861
|
+
const knob = cylinderAlongX(knobThickness, knobDiameter / 2, knobCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52862
|
+
const shaftLeftX = pressurePadRightFaceX - Math.min(pressurePadThickness * 0.45, screwDiameter * 0.45);
|
|
52863
|
+
const shaftRightX = knobCenterX + knobThickness / 2;
|
|
52864
|
+
const shaftLength = shaftRightX - shaftLeftX;
|
|
52865
|
+
if (shaftLength <= supportThickness + bossLength) {
|
|
52866
|
+
throw new Error("thumbScrewClampAssembly: generated screw length is too short for the threaded support");
|
|
52867
|
+
}
|
|
52868
|
+
const shaft = cylinderAlongX(shaftLength, screwDiameter / 2, (shaftLeftX + shaftRightX) / 2, segments).translate(0, 0, screwCenterZ);
|
|
52869
|
+
const clampScrew = union(shaft, pressurePad, knob).color("#cbd5e1");
|
|
52870
|
+
const workpieceEnvelope = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ);
|
|
52871
|
+
return {
|
|
52872
|
+
parts: [
|
|
52873
|
+
{ name: "thumb-screw clamp frame with fixed anvil and threaded boss", shape: frame },
|
|
52874
|
+
{ name: "representative clamped workpiece between pads", shape: workpiece },
|
|
52875
|
+
{ name: "installed thumb screw with captive pressure pad and hand knob", shape: clampScrew }
|
|
52876
|
+
],
|
|
52877
|
+
frame,
|
|
52878
|
+
workpiece,
|
|
52879
|
+
clampScrew,
|
|
52880
|
+
cutters: {
|
|
52881
|
+
threadedBossBore,
|
|
52882
|
+
workpieceEnvelope
|
|
52883
|
+
},
|
|
52884
|
+
dims: {
|
|
52885
|
+
screwSize,
|
|
52886
|
+
screwDiameter,
|
|
52887
|
+
threadEnvelopeDiameter,
|
|
52888
|
+
workpieceThickness,
|
|
52889
|
+
workpieceDepth,
|
|
52890
|
+
workpieceHeight,
|
|
52891
|
+
frameDepth,
|
|
52892
|
+
frameHeight,
|
|
52893
|
+
baseThickness,
|
|
52894
|
+
jawThickness,
|
|
52895
|
+
supportThickness,
|
|
52896
|
+
bossLength,
|
|
52897
|
+
bossDiameter,
|
|
52898
|
+
exposedScrewLength,
|
|
52899
|
+
pressurePadDiameter,
|
|
52900
|
+
pressurePadThickness,
|
|
52901
|
+
knobDiameter,
|
|
52902
|
+
knobThickness,
|
|
52903
|
+
screwCenterZ,
|
|
52904
|
+
fixedAnvilFaceX: workpieceLeftFaceX - faceClearance,
|
|
52905
|
+
pressurePadFaceX: workpieceRightFaceX + faceClearance,
|
|
52906
|
+
supportInnerFaceX,
|
|
52907
|
+
runningClearance,
|
|
52908
|
+
faceClearance
|
|
52909
|
+
}
|
|
52910
|
+
};
|
|
52911
|
+
}
|
|
50927
52912
|
function fastenerSet(size, boltLength, options) {
|
|
50928
52913
|
const sizeData = METRIC_HOLE_TABLE[size];
|
|
50929
52914
|
if (!sizeData) throw new Error(`fastenerSet: unsupported size "${size}"`);
|
|
@@ -50984,6 +52969,22 @@ const partLibrary = {
|
|
|
50984
52969
|
nut,
|
|
50985
52970
|
washer,
|
|
50986
52971
|
fastenerSet,
|
|
52972
|
+
boltedServiceCover,
|
|
52973
|
+
datumEnclosureAssembly,
|
|
52974
|
+
snapLatchCoverAssembly,
|
|
52975
|
+
pinnedLeverAssembly,
|
|
52976
|
+
retainedShaftAssembly,
|
|
52977
|
+
capturedLinearSlide,
|
|
52978
|
+
capturedCartridgeGuideAssembly,
|
|
52979
|
+
livingHingeCoverAssembly,
|
|
52980
|
+
knuckledHingeAssembly,
|
|
52981
|
+
clevisPinJointAssembly,
|
|
52982
|
+
seatedBearingAssembly,
|
|
52983
|
+
cableGlandAnchorAssembly,
|
|
52984
|
+
hoseBarbPortAssembly,
|
|
52985
|
+
routedTubeClipAssembly,
|
|
52986
|
+
pcbTerminalBlockAssembly,
|
|
52987
|
+
thumbScrewClampAssembly,
|
|
50987
52988
|
pipeRoute,
|
|
50988
52989
|
elbow,
|
|
50989
52990
|
beltDrive,
|
|
@@ -52378,7 +54379,7 @@ function requireFinite$7(value, label) {
|
|
|
52378
54379
|
}
|
|
52379
54380
|
return value;
|
|
52380
54381
|
}
|
|
52381
|
-
function requireVec3$
|
|
54382
|
+
function requireVec3$3(value, label) {
|
|
52382
54383
|
if (!Array.isArray(value) || value.length !== 3) {
|
|
52383
54384
|
throw new Error(`${label} must be [x, y, z]`);
|
|
52384
54385
|
}
|
|
@@ -52422,7 +54423,7 @@ function normalizeOptions(options) {
|
|
|
52422
54423
|
out.size = requireFinite$7(options.size, "Viewport.label options.size");
|
|
52423
54424
|
if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
|
|
52424
54425
|
}
|
|
52425
|
-
if (options.offset !== void 0) out.offset = requireVec3$
|
|
54426
|
+
if (options.offset !== void 0) out.offset = requireVec3$3(options.offset, "Viewport.label options.offset");
|
|
52426
54427
|
if (options.anchor !== void 0) {
|
|
52427
54428
|
if (!VALID_ANCHORS.has(options.anchor)) {
|
|
52428
54429
|
throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
|
|
@@ -52439,7 +54440,7 @@ function collectRenderLabel(text, at, options) {
|
|
|
52439
54440
|
if (typeof text !== "string" || text.trim().length === 0) {
|
|
52440
54441
|
throw new Error("Viewport.label text must be a non-empty string");
|
|
52441
54442
|
}
|
|
52442
|
-
const normalizedAt = requireVec3$
|
|
54443
|
+
const normalizedAt = requireVec3$3(at, "Viewport.label at");
|
|
52443
54444
|
const normalizedOptions = normalizeOptions(options);
|
|
52444
54445
|
_collected$4.push({
|
|
52445
54446
|
id: `render-label-${_nextId++}`,
|
|
@@ -52634,7 +54635,7 @@ function requireFinite$6(value, label) {
|
|
|
52634
54635
|
}
|
|
52635
54636
|
return value;
|
|
52636
54637
|
}
|
|
52637
|
-
function requireVec3$
|
|
54638
|
+
function requireVec3$2(value, label) {
|
|
52638
54639
|
if (!Array.isArray(value) || value.length !== 3) {
|
|
52639
54640
|
throw new Error(`${label} must be [x, y, z]`);
|
|
52640
54641
|
}
|
|
@@ -52662,9 +54663,9 @@ const VALID_ENVIRONMENT_PRESETS = /* @__PURE__ */ new Set([
|
|
|
52662
54663
|
]);
|
|
52663
54664
|
function validateCamera(cam, label) {
|
|
52664
54665
|
const out = {};
|
|
52665
|
-
if (cam.position !== void 0) out.position = requireVec3$
|
|
52666
|
-
if (cam.target !== void 0) out.target = requireVec3$
|
|
52667
|
-
if (cam.up !== void 0) out.up = requireVec3$
|
|
54666
|
+
if (cam.position !== void 0) out.position = requireVec3$2(cam.position, `${label}.position`);
|
|
54667
|
+
if (cam.target !== void 0) out.target = requireVec3$2(cam.target, `${label}.target`);
|
|
54668
|
+
if (cam.up !== void 0) out.up = requireVec3$2(cam.up, `${label}.up`);
|
|
52668
54669
|
if (cam.fov !== void 0) {
|
|
52669
54670
|
out.fov = requireFinite$6(cam.fov, `${label}.fov`);
|
|
52670
54671
|
if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
|
|
@@ -52799,8 +54800,8 @@ function validateLight(light, label) {
|
|
|
52799
54800
|
const out = { type: light.type };
|
|
52800
54801
|
if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
|
|
52801
54802
|
if (light.intensity !== void 0) out.intensity = requireFinite$6(light.intensity, `${label}.intensity`);
|
|
52802
|
-
if (light.position !== void 0) out.position = requireVec3$
|
|
52803
|
-
if (light.target !== void 0) out.target = requireVec3$
|
|
54803
|
+
if (light.position !== void 0) out.position = requireVec3$2(light.position, `${label}.position`);
|
|
54804
|
+
if (light.target !== void 0) out.target = requireVec3$2(light.target, `${label}.target`);
|
|
52804
54805
|
if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
|
|
52805
54806
|
if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
|
|
52806
54807
|
if (light.angle !== void 0) out.angle = requireFinite$6(light.angle, `${label}.angle`);
|
|
@@ -54332,7 +56333,7 @@ function scale$1(v, s) {
|
|
|
54332
56333
|
function dot$2(a2, b) {
|
|
54333
56334
|
return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
|
|
54334
56335
|
}
|
|
54335
|
-
function lerp$
|
|
56336
|
+
function lerp$4(a2, b, t) {
|
|
54336
56337
|
return a2 + (b - a2) * t;
|
|
54337
56338
|
}
|
|
54338
56339
|
function frameMatrix$1(x2, y2, z2, p2) {
|
|
@@ -54343,7 +56344,7 @@ function axisVector(axis, sign2 = 1) {
|
|
|
54343
56344
|
if (axis === "Y") return [0, sign2, 0];
|
|
54344
56345
|
return [0, 0, sign2];
|
|
54345
56346
|
}
|
|
54346
|
-
function axisPosition(axis, point2) {
|
|
56347
|
+
function axisPosition$1(axis, point2) {
|
|
54347
56348
|
return point2[AXIS_INDEX[axis]];
|
|
54348
56349
|
}
|
|
54349
56350
|
function crossPointForStation(axis, point2) {
|
|
@@ -54351,7 +56352,7 @@ function crossPointForStation(axis, point2) {
|
|
|
54351
56352
|
if (axis === "Y") return [point2[0], -point2[2]];
|
|
54352
56353
|
return [point2[1], point2[2]];
|
|
54353
56354
|
}
|
|
54354
|
-
function orientLoftToAxis(shape, axis) {
|
|
56355
|
+
function orientLoftToAxis$1(shape, axis) {
|
|
54355
56356
|
if (axis === "Z") return shape;
|
|
54356
56357
|
if (axis === "Y") return shape.rotateX(-90);
|
|
54357
56358
|
return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
|
|
@@ -54408,9 +56409,9 @@ function interpolateQuery(a2, b, t) {
|
|
|
54408
56409
|
}
|
|
54409
56410
|
return {
|
|
54410
56411
|
side: sideA,
|
|
54411
|
-
u: lerp$
|
|
54412
|
-
v: lerp$
|
|
54413
|
-
offset: lerp$
|
|
56412
|
+
u: lerp$4(a2.u ?? 0.5, b.u ?? 0.5, t),
|
|
56413
|
+
v: lerp$4(a2.v ?? 0.5, b.v ?? 0.5, t),
|
|
56414
|
+
offset: lerp$4(a2.offset ?? 0, b.offset ?? 0, t)
|
|
54414
56415
|
};
|
|
54415
56416
|
}
|
|
54416
56417
|
function resolvePathQueries(points) {
|
|
@@ -54477,8 +56478,8 @@ class ProductSkin {
|
|
|
54477
56478
|
this.stations = stations;
|
|
54478
56479
|
this.rails = rails;
|
|
54479
56480
|
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)));
|
|
56481
|
+
this.axisMin = Math.min(...stations.map((station) => axisPosition$1(axis, station.center)));
|
|
56482
|
+
this.axisMax = Math.max(...stations.map((station) => axisPosition$1(axis, station.center)));
|
|
54482
56483
|
this.diagnosticsValue = {
|
|
54483
56484
|
...diagnostics,
|
|
54484
56485
|
stationNames: stations.map((station) => station.name),
|
|
@@ -54535,24 +56536,24 @@ class ProductSkin {
|
|
|
54535
56536
|
}
|
|
54536
56537
|
/** Interpolate center, width, and depth at a normalized v or absolute axis value. */
|
|
54537
56538
|
stationAt(vOrAxis) {
|
|
54538
|
-
const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$
|
|
56539
|
+
const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$4(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
|
|
54539
56540
|
const sorted = this.stations;
|
|
54540
56541
|
for (let index2 = 0; index2 < sorted.length - 1; index2 += 1) {
|
|
54541
56542
|
const a2 = sorted[index2];
|
|
54542
56543
|
const b = sorted[index2 + 1];
|
|
54543
|
-
const aAxis = axisPosition(this.axis, a2.center);
|
|
54544
|
-
const bAxis = axisPosition(this.axis, b.center);
|
|
56544
|
+
const aAxis = axisPosition$1(this.axis, a2.center);
|
|
56545
|
+
const bAxis = axisPosition$1(this.axis, b.center);
|
|
54545
56546
|
if (axisValue < aAxis - EPS$5 || axisValue > bAxis + EPS$5) continue;
|
|
54546
56547
|
const span = Math.max(EPS$5, bAxis - aAxis);
|
|
54547
56548
|
const t = clamp$5((axisValue - aAxis) / span, 0, 1);
|
|
54548
56549
|
return {
|
|
54549
56550
|
axisValue,
|
|
54550
|
-
center: [lerp$
|
|
54551
|
-
width: lerp$
|
|
54552
|
-
depth: lerp$
|
|
56551
|
+
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)],
|
|
56552
|
+
width: lerp$4(a2.profile.width, b.profile.width, t),
|
|
56553
|
+
depth: lerp$4(a2.profile.depth, b.profile.depth, t),
|
|
54553
56554
|
dWidth: (b.profile.width - a2.profile.width) / span,
|
|
54554
56555
|
dDepth: (b.profile.depth - a2.profile.depth) / span,
|
|
54555
|
-
exponent: lerp$
|
|
56556
|
+
exponent: lerp$4(profileExponent(a2), profileExponent(b), t),
|
|
54556
56557
|
kind: a2.profile.kind === b.profile.kind ? a2.profile.kind : "custom"
|
|
54557
56558
|
};
|
|
54558
56559
|
}
|
|
@@ -54674,7 +56675,7 @@ class ProductSkinBuilder {
|
|
|
54674
56675
|
}
|
|
54675
56676
|
/** Set named cross-section stations for the product skin. */
|
|
54676
56677
|
stations(stations) {
|
|
54677
|
-
this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition(this.axisValue, a2.center) - axisPosition(this.axisValue, b.center));
|
|
56678
|
+
this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition$1(this.axisValue, a2.center) - axisPosition$1(this.axisValue, b.center));
|
|
54678
56679
|
return this;
|
|
54679
56680
|
}
|
|
54680
56681
|
/** Attach named guide rails for product-skin construction and downstream surface references. */
|
|
@@ -54724,9 +56725,9 @@ class ProductSkinBuilder {
|
|
|
54724
56725
|
const [x2, y2] = crossPointForStation(this.axisValue, station.center);
|
|
54725
56726
|
return station.profile.sketch.translate(x2, y2);
|
|
54726
56727
|
});
|
|
54727
|
-
const heights = this.stationsValue.map((station) => axisPosition(this.axisValue, station.center));
|
|
56728
|
+
const heights = this.stationsValue.map((station) => axisPosition$1(this.axisValue, station.center));
|
|
54728
56729
|
let shape = loft(localProfiles, heights, { edgeLength: this.edgeLengthValue });
|
|
54729
|
-
shape = orientLoftToAxis(shape, this.axisValue);
|
|
56730
|
+
shape = orientLoftToAxis$1(shape, this.axisValue);
|
|
54730
56731
|
if (this.colorValue) shape = shape.color(this.colorValue);
|
|
54731
56732
|
shape = applyMaterial(shape, this.materialValue).as(this.name);
|
|
54732
56733
|
const warnings = [];
|
|
@@ -55385,7 +57386,7 @@ function requirePositive$3(value, label) {
|
|
|
55385
57386
|
function clamp$4(value, min2, max2) {
|
|
55386
57387
|
return Math.max(min2, Math.min(max2, value));
|
|
55387
57388
|
}
|
|
55388
|
-
function lerp$
|
|
57389
|
+
function lerp$3(a2, b, t) {
|
|
55389
57390
|
return a2 + (b - a2) * t;
|
|
55390
57391
|
}
|
|
55391
57392
|
function add(a2, b) {
|
|
@@ -55435,19 +57436,19 @@ function transformLocal(point2, tangentAcross, normal, tangentAlong, x2, y2, z2
|
|
|
55435
57436
|
function interpolateCylinder(a2, b, t, mode) {
|
|
55436
57437
|
let delta = b.angle - a2.angle;
|
|
55437
57438
|
if (mode === "shortest" && Math.abs(delta) > 180) delta -= Math.sign(delta) * 360;
|
|
55438
|
-
return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$
|
|
57439
|
+
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
57440
|
}
|
|
55440
57441
|
function interpolatePlane(a2, b, t) {
|
|
55441
|
-
return { kind: "plane", x: lerp$
|
|
57442
|
+
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
57443
|
}
|
|
55443
57444
|
function interpolateProductSkin(a2, b, t) {
|
|
55444
57445
|
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
57446
|
return {
|
|
55446
57447
|
kind: "productSkin",
|
|
55447
57448
|
side: a2.side ?? b.side,
|
|
55448
|
-
u: lerp$
|
|
55449
|
-
v: lerp$
|
|
55450
|
-
offset: lerp$
|
|
57449
|
+
u: lerp$3(a2.u ?? 0.5, b.u ?? 0.5, t),
|
|
57450
|
+
v: lerp$3(a2.v ?? 0.5, b.v ?? 0.5, t),
|
|
57451
|
+
offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t)
|
|
55451
57452
|
};
|
|
55452
57453
|
}
|
|
55453
57454
|
class SurfacePath {
|
|
@@ -55770,11 +57771,11 @@ function coordinateOnSide(coordinate, side, label) {
|
|
|
55770
57771
|
return { ...coordinate, kind: "productSkin", side };
|
|
55771
57772
|
}
|
|
55772
57773
|
class ProductSkinCarrier {
|
|
55773
|
-
constructor(skin, name = skin.name,
|
|
57774
|
+
constructor(skin, name = skin.name, sideValue2, offsetValue = 0) {
|
|
55774
57775
|
__publicField(this, "kind", "productSkin");
|
|
55775
57776
|
this.skin = skin;
|
|
55776
57777
|
this.name = name;
|
|
55777
|
-
this.sideValue =
|
|
57778
|
+
this.sideValue = sideValue2;
|
|
55778
57779
|
this.offsetValue = offsetValue;
|
|
55779
57780
|
}
|
|
55780
57781
|
surface(side) {
|
|
@@ -56545,7 +58546,7 @@ function counterboresForPlate(spec2, width, height, thickness, diagnostics) {
|
|
|
56545
58546
|
function minWidthAcrossAlongRange(widthAtT, length4, minAlong, maxAlong) {
|
|
56546
58547
|
let minWidth = Number.POSITIVE_INFINITY;
|
|
56547
58548
|
for (let index2 = 0; index2 <= 8; index2 += 1) {
|
|
56548
|
-
const along = lerp$
|
|
58549
|
+
const along = lerp$3(minAlong, maxAlong, index2 / 8);
|
|
56549
58550
|
const t = Math.max(0, Math.min(1, (along + length4 / 2) / Math.max(length4, 1e-8)));
|
|
56550
58551
|
minWidth = Math.min(minWidth, widthAtT(t));
|
|
56551
58552
|
}
|
|
@@ -56845,7 +58846,7 @@ function pathParameterAtDistance(samples, distance2) {
|
|
|
56845
58846
|
const segmentLength = Math.hypot(b.point[0] - a2.point[0], b.point[1] - a2.point[1], b.point[2] - a2.point[2]);
|
|
56846
58847
|
if (traveled + segmentLength >= distance2) {
|
|
56847
58848
|
const localT = segmentLength <= 1e-8 ? 0 : (distance2 - traveled) / segmentLength;
|
|
56848
|
-
return lerp$
|
|
58849
|
+
return lerp$3(a2.t, b.t, localT);
|
|
56849
58850
|
}
|
|
56850
58851
|
traveled += segmentLength;
|
|
56851
58852
|
}
|
|
@@ -56898,7 +58899,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
56898
58899
|
const width = input.widthAt(t);
|
|
56899
58900
|
const along = distance2 - length4 / 2;
|
|
56900
58901
|
for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
|
|
56901
|
-
const across = lerp$
|
|
58902
|
+
const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
|
|
56902
58903
|
mesh.vertices.push(pointAtProfile([across, along], false));
|
|
56903
58904
|
}
|
|
56904
58905
|
}
|
|
@@ -56908,7 +58909,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
56908
58909
|
const width = input.widthAt(t);
|
|
56909
58910
|
const along = distance2 - length4 / 2;
|
|
56910
58911
|
for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
|
|
56911
|
-
const across = lerp$
|
|
58912
|
+
const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
|
|
56912
58913
|
mesh.vertices.push(pointAtProfile([across, along], true));
|
|
56913
58914
|
}
|
|
56914
58915
|
}
|
|
@@ -56920,7 +58921,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
56920
58921
|
const width = input.widthAt(t);
|
|
56921
58922
|
const along = distance2 - length4 / 2;
|
|
56922
58923
|
for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
|
|
56923
|
-
const across = lerp$
|
|
58924
|
+
const across = lerp$3(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
|
|
56924
58925
|
filled[alongIndex][acrossIndex] = !holes.some((hole2) => pointInProfileLoop([across, along], hole2));
|
|
56925
58926
|
}
|
|
56926
58927
|
}
|
|
@@ -59002,7 +61003,7 @@ const Constraint = {
|
|
|
59002
61003
|
return builder.constrain({ type: "length", line: resolveLineId(builder, line2), value });
|
|
59003
61004
|
}
|
|
59004
61005
|
};
|
|
59005
|
-
function requireVec3(v, label) {
|
|
61006
|
+
function requireVec3$1(v, label) {
|
|
59006
61007
|
if (!Array.isArray(v) || v.length !== 3 || !Number.isFinite(v[0]) || !Number.isFinite(v[1]) || !Number.isFinite(v[2])) {
|
|
59007
61008
|
throw new Error(`${label} must be a [number, number, number] with finite values, got ${JSON.stringify(v)}`);
|
|
59008
61009
|
}
|
|
@@ -59015,24 +61016,24 @@ function requireFiniteNumber(n, label) {
|
|
|
59015
61016
|
return n;
|
|
59016
61017
|
}
|
|
59017
61018
|
function distance$1(a2, b) {
|
|
59018
|
-
requireVec3(a2, "a");
|
|
59019
|
-
requireVec3(b, "b");
|
|
61019
|
+
requireVec3$1(a2, "a");
|
|
61020
|
+
requireVec3$1(b, "b");
|
|
59020
61021
|
return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
|
|
59021
61022
|
}
|
|
59022
61023
|
function midpoint$1(a2, b) {
|
|
59023
|
-
requireVec3(a2, "a");
|
|
59024
|
-
requireVec3(b, "b");
|
|
61024
|
+
requireVec3$1(a2, "a");
|
|
61025
|
+
requireVec3$1(b, "b");
|
|
59025
61026
|
return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
|
|
59026
61027
|
}
|
|
59027
|
-
function lerp(a2, b, t) {
|
|
59028
|
-
requireVec3(a2, "a");
|
|
59029
|
-
requireVec3(b, "b");
|
|
61028
|
+
function lerp$2(a2, b, t) {
|
|
61029
|
+
requireVec3$1(a2, "a");
|
|
61030
|
+
requireVec3$1(b, "b");
|
|
59030
61031
|
requireFiniteNumber(t, "t");
|
|
59031
61032
|
return [a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t];
|
|
59032
61033
|
}
|
|
59033
61034
|
function direction(a2, b) {
|
|
59034
|
-
requireVec3(a2, "a");
|
|
59035
|
-
requireVec3(b, "b");
|
|
61035
|
+
requireVec3$1(a2, "a");
|
|
61036
|
+
requireVec3$1(b, "b");
|
|
59036
61037
|
const dx = b[0] - a2[0];
|
|
59037
61038
|
const dy = b[1] - a2[1];
|
|
59038
61039
|
const dz = b[2] - a2[2];
|
|
@@ -59043,8 +61044,8 @@ function direction(a2, b) {
|
|
|
59043
61044
|
return [dx / len2, dy / len2, dz / len2];
|
|
59044
61045
|
}
|
|
59045
61046
|
function offset(point2, dir, amount) {
|
|
59046
|
-
requireVec3(point2, "point");
|
|
59047
|
-
requireVec3(dir, "dir");
|
|
61047
|
+
requireVec3$1(point2, "point");
|
|
61048
|
+
requireVec3$1(dir, "dir");
|
|
59048
61049
|
requireFiniteNumber(amount, "amount");
|
|
59049
61050
|
return [point2[0] + dir[0] * amount, point2[1] + dir[1] * amount, point2[2] + dir[2] * amount];
|
|
59050
61051
|
}
|
|
@@ -59054,7 +61055,7 @@ const Points = {
|
|
|
59054
61055
|
/** Center point between two 3D points. */
|
|
59055
61056
|
midpoint: midpoint$1,
|
|
59056
61057
|
/** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */
|
|
59057
|
-
lerp,
|
|
61058
|
+
lerp: lerp$2,
|
|
59058
61059
|
/** Unit direction vector from a to b. Throws if a and b are the same point. */
|
|
59059
61060
|
direction,
|
|
59060
61061
|
/** Move a point along a direction vector by a given amount. */
|
|
@@ -64186,9 +66187,84 @@ class ConstraintSketch extends Sketch {
|
|
|
64186
66187
|
* Select the single arrangement region that contains the given seed point.
|
|
64187
66188
|
* Throws if no region contains the seed.
|
|
64188
66189
|
*/
|
|
64189
|
-
detectArrangementRegion(
|
|
66190
|
+
detectArrangementRegion(_seed) {
|
|
64190
66191
|
throw new Error("Not implemented");
|
|
64191
66192
|
}
|
|
66193
|
+
/**
|
|
66194
|
+
* Return the solved constrained path as a sampled 2D polyline.
|
|
66195
|
+
*
|
|
66196
|
+
* Use this when a construction rail was authored with `constrainedSketch()`
|
|
66197
|
+
* and should feed another operation such as `Loft.pathOnXz(...)`.
|
|
66198
|
+
* The sketch must contain exactly one profile path.
|
|
66199
|
+
*
|
|
66200
|
+
* @param samples - Samples per curved segment. Default 32.
|
|
66201
|
+
* @returns The solved path as an open polyline.
|
|
66202
|
+
*/
|
|
66203
|
+
toPolyline(samples = 32) {
|
|
66204
|
+
if (!Number.isFinite(samples) || samples < 2) throw new Error("ConstraintSketch.toPolyline() samples must be at least 2");
|
|
66205
|
+
const profileLoops = this.definition.loops.filter((loop) => loop.type === "profile");
|
|
66206
|
+
if (profileLoops.length !== 1) {
|
|
66207
|
+
throw new Error("ConstraintSketch.toPolyline() requires exactly one profile path");
|
|
66208
|
+
}
|
|
66209
|
+
const sampleCount = Math.max(2, Math.round(samples));
|
|
66210
|
+
const pointMap = new Map(this.definition.points.map((point2) => [point2.id, point2]));
|
|
66211
|
+
const lineMap = new Map(this.definition.lines.map((line2) => [line2.id, line2]));
|
|
66212
|
+
const arcMap = new Map(this.definition.arcs.map((arc) => [arc.id, arc]));
|
|
66213
|
+
const bezierMap = new Map(this.definition.beziers.map((bezier) => [bezier.id, bezier]));
|
|
66214
|
+
const points = [];
|
|
66215
|
+
const appendStart = (point2, label) => {
|
|
66216
|
+
const previous = points[points.length - 1];
|
|
66217
|
+
if (!previous) {
|
|
66218
|
+
points.push(point2);
|
|
66219
|
+
return;
|
|
66220
|
+
}
|
|
66221
|
+
if (Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-6) {
|
|
66222
|
+
throw new Error(`ConstraintSketch.toPolyline() profile path is not continuous at ${label}`);
|
|
66223
|
+
}
|
|
66224
|
+
};
|
|
66225
|
+
const appendPoint = (point2) => {
|
|
66226
|
+
const previous = points[points.length - 1];
|
|
66227
|
+
if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) points.push(point2);
|
|
66228
|
+
};
|
|
66229
|
+
const requirePoint = (id, label) => {
|
|
66230
|
+
const point2 = pointMap.get(id);
|
|
66231
|
+
if (!point2) throw new Error(`ConstraintSketch.toPolyline() missing ${label}`);
|
|
66232
|
+
return [point2.x, point2.y];
|
|
66233
|
+
};
|
|
66234
|
+
for (const segment of profileLoops[0].segments) {
|
|
66235
|
+
if (segment.kind === "line") {
|
|
66236
|
+
const line2 = lineMap.get(segment.line);
|
|
66237
|
+
if (!line2) throw new Error(`ConstraintSketch.toPolyline() missing line "${segment.line}"`);
|
|
66238
|
+
appendStart(requirePoint(line2.a, `line "${segment.line}" start point`), `line "${segment.line}"`);
|
|
66239
|
+
appendPoint(requirePoint(line2.b, `line "${segment.line}" end point`));
|
|
66240
|
+
} else if (segment.kind === "arc") {
|
|
66241
|
+
const arc = arcMap.get(segment.arc);
|
|
66242
|
+
if (!arc) throw new Error(`ConstraintSketch.toPolyline() missing arc "${segment.arc}"`);
|
|
66243
|
+
const center = requirePoint(arc.center, `arc "${segment.arc}" center point`);
|
|
66244
|
+
const start = requirePoint(arc.start, `arc "${segment.arc}" start point`);
|
|
66245
|
+
const end = requirePoint(arc.end, `arc "${segment.arc}" end point`);
|
|
66246
|
+
appendStart(start, `arc "${segment.arc}"`);
|
|
66247
|
+
const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
|
|
66248
|
+
const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
|
|
66249
|
+
for (const point2 of tessellateArc(center[0], center[1], arc.radius, startAngle, endAngle, arc.clockwise, sampleCount)) {
|
|
66250
|
+
appendPoint(point2);
|
|
66251
|
+
}
|
|
66252
|
+
} else {
|
|
66253
|
+
const bezier = bezierMap.get(segment.bezier);
|
|
66254
|
+
if (!bezier) throw new Error(`ConstraintSketch.toPolyline() missing bezier "${segment.bezier}"`);
|
|
66255
|
+
const p0 = requirePoint(bezier.p0, `bezier "${segment.bezier}" start point`);
|
|
66256
|
+
const p1 = requirePoint(bezier.p1, `bezier "${segment.bezier}" first control point`);
|
|
66257
|
+
const p2 = requirePoint(bezier.p2, `bezier "${segment.bezier}" second control point`);
|
|
66258
|
+
const p3 = requirePoint(bezier.p3, `bezier "${segment.bezier}" end point`);
|
|
66259
|
+
appendStart(p0, `bezier "${segment.bezier}"`);
|
|
66260
|
+
for (const point2 of tessellateBezier(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], p3[0], p3[1], sampleCount)) {
|
|
66261
|
+
appendPoint(point2);
|
|
66262
|
+
}
|
|
66263
|
+
}
|
|
66264
|
+
}
|
|
66265
|
+
if (points.length < 2) throw new Error("ConstraintSketch.toPolyline() needs at least 2 points");
|
|
66266
|
+
return points;
|
|
66267
|
+
}
|
|
64192
66268
|
/**
|
|
64193
66269
|
* Re-solve the sketch after changing the value of one existing constraint.
|
|
64194
66270
|
*
|
|
@@ -79473,6 +81549,295 @@ function polygonVertices(sides, radius, options) {
|
|
|
79473
81549
|
centerY: options == null ? void 0 : options.centerY
|
|
79474
81550
|
});
|
|
79475
81551
|
}
|
|
81552
|
+
const LOFT_GUIDE_EPS = 1e-8;
|
|
81553
|
+
function orientLoftToAxis(shape, axis) {
|
|
81554
|
+
if (axis === "Z") return shape;
|
|
81555
|
+
if (axis === "Y") return shape.rotateX(-90);
|
|
81556
|
+
return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
|
|
81557
|
+
}
|
|
81558
|
+
function buildRailEvaluators(rails, axis, start, end, railSamples) {
|
|
81559
|
+
const seen = /* @__PURE__ */ new Set();
|
|
81560
|
+
return rails.map((rail2) => {
|
|
81561
|
+
if (seen.has(rail2.side)) throw new Error(`Loft.withGuideRails() received more than one ${rail2.side} rail`);
|
|
81562
|
+
seen.add(rail2.side);
|
|
81563
|
+
const sampled = sampleRailPath(rail2.path, railSamples);
|
|
81564
|
+
if (sampled.length < 2) throw new Error("Loft guide rails require at least two points");
|
|
81565
|
+
const points = sampled.map((point2) => ({ position: axisPosition(axis, point2), cross: crossPointForAxis(axis, point2) }));
|
|
81566
|
+
const ordered = points[points.length - 1].position >= points[0].position ? points : [...points].reverse();
|
|
81567
|
+
validateRailCoverage(ordered, start, end);
|
|
81568
|
+
return { side: rail2.side, points: ordered };
|
|
81569
|
+
});
|
|
81570
|
+
}
|
|
81571
|
+
function railCrossAt(rail2, position) {
|
|
81572
|
+
const points = rail2.points;
|
|
81573
|
+
if (position <= points[0].position + LOFT_GUIDE_EPS) return points[0].cross;
|
|
81574
|
+
const last = points[points.length - 1];
|
|
81575
|
+
if (position >= last.position - LOFT_GUIDE_EPS) return last.cross;
|
|
81576
|
+
for (let index2 = 0; index2 < points.length - 1; index2 += 1) {
|
|
81577
|
+
const a2 = points[index2];
|
|
81578
|
+
const b = points[index2 + 1];
|
|
81579
|
+
if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
|
|
81580
|
+
const t = (position - a2.position) / (b.position - a2.position);
|
|
81581
|
+
return [lerp$1(a2.cross[0], b.cross[0], t), lerp$1(a2.cross[1], b.cross[1], t)];
|
|
81582
|
+
}
|
|
81583
|
+
}
|
|
81584
|
+
throw new Error("Loft guide rail does not cover requested station position");
|
|
81585
|
+
}
|
|
81586
|
+
function validateRailCoverage(points, start, end) {
|
|
81587
|
+
for (let index2 = 1; index2 < points.length; index2 += 1) {
|
|
81588
|
+
if (points[index2].position - points[index2 - 1].position < LOFT_GUIDE_EPS) {
|
|
81589
|
+
throw new Error("Loft guide rails must be monotone along the loft axis");
|
|
81590
|
+
}
|
|
81591
|
+
}
|
|
81592
|
+
if (points[0].position - start > LOFT_GUIDE_EPS || end - points[points.length - 1].position > LOFT_GUIDE_EPS) {
|
|
81593
|
+
throw new Error("Loft guide rails must cover the full station range");
|
|
81594
|
+
}
|
|
81595
|
+
}
|
|
81596
|
+
function sampleRailPath(path2, samples) {
|
|
81597
|
+
if (Array.isArray(path2)) return path2.map((point2, index2) => requireVec3(point2, `Loft guide rail point ${index2}`));
|
|
81598
|
+
if (path2 instanceof Curve3D || path2 instanceof HermiteCurve3D || path2 instanceof QuinticHermiteCurve3D || path2 instanceof NurbsCurve3D) {
|
|
81599
|
+
return path2.sample(Math.max(2, Math.round(samples))).map((point2, index2) => requireVec3(point2, `Loft guide rail sample ${index2}`));
|
|
81600
|
+
}
|
|
81601
|
+
throw new Error("Loft guide rail path must be a Vec3[] or ForgeCAD 3D curve");
|
|
81602
|
+
}
|
|
81603
|
+
function requireVec3(point2, label) {
|
|
81604
|
+
if (!Array.isArray(point2) || point2.length !== 3 || !point2.every(Number.isFinite)) {
|
|
81605
|
+
throw new Error(`${label} must be a finite [x, y, z] point`);
|
|
81606
|
+
}
|
|
81607
|
+
return [point2[0], point2[1], point2[2]];
|
|
81608
|
+
}
|
|
81609
|
+
function axisPosition(axis, point2) {
|
|
81610
|
+
if (axis === "X") return point2[0];
|
|
81611
|
+
if (axis === "Y") return point2[1];
|
|
81612
|
+
return point2[2];
|
|
81613
|
+
}
|
|
81614
|
+
function crossPointForAxis(axis, point2) {
|
|
81615
|
+
if (axis === "X") return [point2[1], point2[2]];
|
|
81616
|
+
if (axis === "Y") return [point2[0], -point2[2]];
|
|
81617
|
+
return [point2[0], point2[1]];
|
|
81618
|
+
}
|
|
81619
|
+
function lerp$1(a2, b, t) {
|
|
81620
|
+
return a2 + (b - a2) * t;
|
|
81621
|
+
}
|
|
81622
|
+
function loftWithGuideRails(stations, rails, options = {}) {
|
|
81623
|
+
if (stations.length < 2) throw new Error("Loft.withGuideRails() requires at least two stations");
|
|
81624
|
+
if (rails.length === 0) throw new Error("Loft.withGuideRails() requires at least one guide rail");
|
|
81625
|
+
const sortedStations = sortedValidStations(stations);
|
|
81626
|
+
const axis = options.axis ?? "Z";
|
|
81627
|
+
const start = sortedStations[0].position;
|
|
81628
|
+
const end = sortedStations[sortedStations.length - 1].position;
|
|
81629
|
+
const railEvaluators = buildRailEvaluators(rails, axis, start, end, options.railSamples ?? 64);
|
|
81630
|
+
const positions = generatedPositions(sortedStations, options.samples);
|
|
81631
|
+
const profiles2 = positions.map((position) => {
|
|
81632
|
+
const source = profileForPosition(sortedStations, position);
|
|
81633
|
+
const bounds = boundsForPosition(sortedStations, position);
|
|
81634
|
+
return fitProfileToBounds(source, applyRailsToBounds(bounds, railEvaluators, position));
|
|
81635
|
+
});
|
|
81636
|
+
const shape = loft(profiles2, positions, {
|
|
81637
|
+
edgeLength: options.edgeLength,
|
|
81638
|
+
boundsPadding: options.boundsPadding
|
|
81639
|
+
});
|
|
81640
|
+
return orientLoftToAxis(shape, axis);
|
|
81641
|
+
}
|
|
81642
|
+
function sortedValidStations(stations) {
|
|
81643
|
+
const sorted = [...stations].sort((a2, b) => a2.position - b.position);
|
|
81644
|
+
for (let index2 = 0; index2 < sorted.length; index2 += 1) {
|
|
81645
|
+
if (!Number.isFinite(sorted[index2].position)) throw new Error("Loft.withGuideRails station position must be finite");
|
|
81646
|
+
if (!(sorted[index2].profile instanceof Sketch)) throw new Error("Loft.withGuideRails() stations must use Sketch profiles");
|
|
81647
|
+
if (index2 > 0 && sorted[index2].position - sorted[index2 - 1].position < LOFT_GUIDE_EPS) {
|
|
81648
|
+
throw new Error("Loft.withGuideRails() requires unique, strictly increasing station positions");
|
|
81649
|
+
}
|
|
81650
|
+
}
|
|
81651
|
+
return sorted;
|
|
81652
|
+
}
|
|
81653
|
+
function generatedPositions(stations, samples) {
|
|
81654
|
+
const count = Math.max(2, Math.round(samples ?? Math.max(9, (stations.length - 1) * 8 + 1)));
|
|
81655
|
+
const start = stations[0].position;
|
|
81656
|
+
const end = stations[stations.length - 1].position;
|
|
81657
|
+
const values = /* @__PURE__ */ new Set();
|
|
81658
|
+
const positions = [];
|
|
81659
|
+
const addPosition = (position) => {
|
|
81660
|
+
const key = position.toFixed(9);
|
|
81661
|
+
if (!values.has(key)) {
|
|
81662
|
+
values.add(key);
|
|
81663
|
+
positions.push(position);
|
|
81664
|
+
}
|
|
81665
|
+
};
|
|
81666
|
+
for (let index2 = 0; index2 < count; index2 += 1) addPosition(start + (end - start) * index2 / (count - 1));
|
|
81667
|
+
for (const station of stations) addPosition(station.position);
|
|
81668
|
+
return positions.sort((a2, b) => a2 - b);
|
|
81669
|
+
}
|
|
81670
|
+
function profileForPosition(stations, position) {
|
|
81671
|
+
for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
|
|
81672
|
+
if (position <= stations[index2 + 1].position + LOFT_GUIDE_EPS) return stations[index2].profile;
|
|
81673
|
+
}
|
|
81674
|
+
return stations[stations.length - 1].profile;
|
|
81675
|
+
}
|
|
81676
|
+
function boundsForPosition(stations, position) {
|
|
81677
|
+
if (position <= stations[0].position + LOFT_GUIDE_EPS) return sketchBounds(stations[0].profile);
|
|
81678
|
+
const last = stations[stations.length - 1];
|
|
81679
|
+
if (position >= last.position - LOFT_GUIDE_EPS) return sketchBounds(last.profile);
|
|
81680
|
+
for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
|
|
81681
|
+
const a2 = stations[index2];
|
|
81682
|
+
const b = stations[index2 + 1];
|
|
81683
|
+
if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
|
|
81684
|
+
return lerpBounds(sketchBounds(a2.profile), sketchBounds(b.profile), (position - a2.position) / (b.position - a2.position));
|
|
81685
|
+
}
|
|
81686
|
+
}
|
|
81687
|
+
return sketchBounds(last.profile);
|
|
81688
|
+
}
|
|
81689
|
+
function applyRailsToBounds(bounds, rails, position) {
|
|
81690
|
+
const centerRail = rails.find((rail2) => rail2.side === "center");
|
|
81691
|
+
const center = centerRail ? railCrossAt(centerRail, position) : void 0;
|
|
81692
|
+
const next = { ...bounds };
|
|
81693
|
+
applyAxisRail(next, "X", sideValue(rails, "left", position, 0), sideValue(rails, "right", position, 0), center == null ? void 0 : center[0]);
|
|
81694
|
+
applyAxisRail(next, "Y", sideValue(rails, "back", position, 1), sideValue(rails, "front", position, 1), center == null ? void 0 : center[1]);
|
|
81695
|
+
if (next.maxX - next.minX < LOFT_GUIDE_EPS || next.maxY - next.minY < LOFT_GUIDE_EPS) {
|
|
81696
|
+
throw new Error("Loft.withGuideRails() guide rails produced a non-positive section size");
|
|
81697
|
+
}
|
|
81698
|
+
return next;
|
|
81699
|
+
}
|
|
81700
|
+
function sideValue(rails, side, position, crossIndex) {
|
|
81701
|
+
const rail2 = rails.find((entry) => entry.side === side);
|
|
81702
|
+
return rail2 ? railCrossAt(rail2, position)[crossIndex] : void 0;
|
|
81703
|
+
}
|
|
81704
|
+
function applyAxisRail(bounds, axis, minRail, maxRail, center) {
|
|
81705
|
+
const minKey = axis === "X" ? "minX" : "minY";
|
|
81706
|
+
const maxKey = axis === "X" ? "maxX" : "maxY";
|
|
81707
|
+
const width = bounds[maxKey] - bounds[minKey];
|
|
81708
|
+
if (minRail != null && maxRail != null) {
|
|
81709
|
+
if (maxRail - minRail < LOFT_GUIDE_EPS) throw new Error("Loft.withGuideRails() opposite guide rails crossed");
|
|
81710
|
+
if (center != null && Math.abs((minRail + maxRail) / 2 - center) > 1e-5) {
|
|
81711
|
+
throw new Error("Loft.withGuideRails() center rail conflicts with opposite side rails");
|
|
81712
|
+
}
|
|
81713
|
+
bounds[minKey] = minRail;
|
|
81714
|
+
bounds[maxKey] = maxRail;
|
|
81715
|
+
} else if (maxRail != null) {
|
|
81716
|
+
bounds[maxKey] = maxRail;
|
|
81717
|
+
bounds[minKey] = center != null ? 2 * center - maxRail : maxRail - width;
|
|
81718
|
+
} else if (minRail != null) {
|
|
81719
|
+
bounds[minKey] = minRail;
|
|
81720
|
+
bounds[maxKey] = center != null ? 2 * center - minRail : minRail + width;
|
|
81721
|
+
} else if (center != null) {
|
|
81722
|
+
bounds[minKey] = center - width / 2;
|
|
81723
|
+
bounds[maxKey] = center + width / 2;
|
|
81724
|
+
}
|
|
81725
|
+
}
|
|
81726
|
+
function fitProfileToBounds(profile, target) {
|
|
81727
|
+
const source = sketchBounds(profile);
|
|
81728
|
+
const sourceWidth = source.maxX - source.minX;
|
|
81729
|
+
const sourceDepth = source.maxY - source.minY;
|
|
81730
|
+
if (sourceWidth < LOFT_GUIDE_EPS || sourceDepth < LOFT_GUIDE_EPS) {
|
|
81731
|
+
throw new Error("Loft.withGuideRails() station profiles must have positive bounds");
|
|
81732
|
+
}
|
|
81733
|
+
const sourceCenter = [(source.minX + source.maxX) / 2, (source.minY + source.maxY) / 2];
|
|
81734
|
+
const targetCenter = [(target.minX + target.maxX) / 2, (target.minY + target.maxY) / 2];
|
|
81735
|
+
return profile.scaleAround(sourceCenter, [(target.maxX - target.minX) / sourceWidth, (target.maxY - target.minY) / sourceDepth]).translate(targetCenter[0] - sourceCenter[0], targetCenter[1] - sourceCenter[1]);
|
|
81736
|
+
}
|
|
81737
|
+
function sketchBounds(profile) {
|
|
81738
|
+
const bounds = profile.bounds();
|
|
81739
|
+
return { minX: bounds.min[0], maxX: bounds.max[0], minY: bounds.min[1], maxY: bounds.max[1] };
|
|
81740
|
+
}
|
|
81741
|
+
function lerpBounds(a2, b, t) {
|
|
81742
|
+
return {
|
|
81743
|
+
minX: lerp(a2.minX, b.minX, t),
|
|
81744
|
+
maxX: lerp(a2.maxX, b.maxX, t),
|
|
81745
|
+
minY: lerp(a2.minY, b.minY, t),
|
|
81746
|
+
maxY: lerp(a2.maxY, b.maxY, t)
|
|
81747
|
+
};
|
|
81748
|
+
}
|
|
81749
|
+
function lerp(a2, b, t) {
|
|
81750
|
+
return a2 + (b - a2) * t;
|
|
81751
|
+
}
|
|
81752
|
+
function mapLoftPath2D(path2, label, mapper) {
|
|
81753
|
+
const points = sampleLoftPath2D(path2, label);
|
|
81754
|
+
return points.map((point2, index2) => {
|
|
81755
|
+
if (!Array.isArray(point2) || point2.length !== 2 || !point2.every(Number.isFinite)) {
|
|
81756
|
+
throw new Error(`${label} point ${index2} must be a finite [x, y] point`);
|
|
81757
|
+
}
|
|
81758
|
+
return mapper([point2[0], point2[1]]);
|
|
81759
|
+
});
|
|
81760
|
+
}
|
|
81761
|
+
function sampleLoftPath2D(path2, label) {
|
|
81762
|
+
if (Array.isArray(path2)) {
|
|
81763
|
+
if (path2.length < 2) throw new Error(`${label} requires at least two [x, y] points`);
|
|
81764
|
+
return path2;
|
|
81765
|
+
}
|
|
81766
|
+
if (!path2 || typeof path2 !== "object" || typeof path2.toPolyline !== "function") {
|
|
81767
|
+
throw new Error(`${label} requires a 2D path, solved constrained path, or [x, y] point array`);
|
|
81768
|
+
}
|
|
81769
|
+
const points = path2.toPolyline();
|
|
81770
|
+
if (!Array.isArray(points) || points.length < 2) throw new Error(`${label} path must produce at least two [x, y] points`);
|
|
81771
|
+
return points;
|
|
81772
|
+
}
|
|
81773
|
+
const Loft = {
|
|
81774
|
+
/** Create a loft station from a 2D profile and an axis position. */
|
|
81775
|
+
station(profile, position) {
|
|
81776
|
+
if (!Number.isFinite(position)) throw new Error("Loft.station position must be finite");
|
|
81777
|
+
return { profile, position };
|
|
81778
|
+
},
|
|
81779
|
+
/** Create a guide rail that constrains the section-local negative-X side. */
|
|
81780
|
+
leftRail(path2) {
|
|
81781
|
+
return { side: "left", path: path2 };
|
|
81782
|
+
},
|
|
81783
|
+
/** Create a guide rail that constrains the section-local positive-X side. */
|
|
81784
|
+
rightRail(path2) {
|
|
81785
|
+
return { side: "right", path: path2 };
|
|
81786
|
+
},
|
|
81787
|
+
/** Create a guide rail that constrains the section-local positive-Y side. */
|
|
81788
|
+
frontRail(path2) {
|
|
81789
|
+
return { side: "front", path: path2 };
|
|
81790
|
+
},
|
|
81791
|
+
/** Create a guide rail that constrains the section-local negative-Y side. */
|
|
81792
|
+
backRail(path2) {
|
|
81793
|
+
return { side: "back", path: path2 };
|
|
81794
|
+
},
|
|
81795
|
+
/** Create a guide rail that moves section centers along the loft. */
|
|
81796
|
+
centerRail(path2) {
|
|
81797
|
+
return { side: "center", path: path2 };
|
|
81798
|
+
},
|
|
81799
|
+
/**
|
|
81800
|
+
* Place a 2D guide path onto the XZ plane.
|
|
81801
|
+
*
|
|
81802
|
+
* The path's first coordinate becomes X and its second coordinate becomes Z.
|
|
81803
|
+
* Use this for left/right silhouette rails authored with `path()` or `constrainedSketch()`.
|
|
81804
|
+
*/
|
|
81805
|
+
pathOnXz(path2, y2 = 0) {
|
|
81806
|
+
if (!Number.isFinite(y2)) throw new Error("Loft.pathOnXz y must be finite");
|
|
81807
|
+
return mapLoftPath2D(path2, "Loft.pathOnXz", ([x2, z2]) => [x2, y2, z2]);
|
|
81808
|
+
},
|
|
81809
|
+
/**
|
|
81810
|
+
* Place a 2D guide path onto the YZ plane.
|
|
81811
|
+
*
|
|
81812
|
+
* The path's first coordinate becomes Y and its second coordinate becomes Z.
|
|
81813
|
+
* Use this for front/back crown rails authored with `path()` or `constrainedSketch()`.
|
|
81814
|
+
*/
|
|
81815
|
+
pathOnYz(path2, x2 = 0) {
|
|
81816
|
+
if (!Number.isFinite(x2)) throw new Error("Loft.pathOnYz x must be finite");
|
|
81817
|
+
return mapLoftPath2D(path2, "Loft.pathOnYz", ([y2, z2]) => [x2, y2, z2]);
|
|
81818
|
+
},
|
|
81819
|
+
/**
|
|
81820
|
+
* Place a 2D guide path onto the XY plane.
|
|
81821
|
+
*
|
|
81822
|
+
* The path's first coordinate becomes X and its second coordinate becomes Y.
|
|
81823
|
+
* Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
|
|
81824
|
+
*/
|
|
81825
|
+
pathOnXy(path2, z2 = 0) {
|
|
81826
|
+
if (!Number.isFinite(z2)) throw new Error("Loft.pathOnXy z must be finite");
|
|
81827
|
+
return mapLoftPath2D(path2, "Loft.pathOnXy", ([x2, y2]) => [x2, y2, z2]);
|
|
81828
|
+
},
|
|
81829
|
+
/**
|
|
81830
|
+
* Loft through profile stations while forcing generated sections to follow guide rails.
|
|
81831
|
+
*
|
|
81832
|
+
* Stations define the cross-section family. Guide rails define the side or center
|
|
81833
|
+
* paths the loft must pass through. With opposite side rails, the section is scaled
|
|
81834
|
+
* to touch both rails. With one side rail, the section keeps its interpolated size
|
|
81835
|
+
* unless a center rail is also present.
|
|
81836
|
+
*/
|
|
81837
|
+
withGuideRails(stations, rails, options = {}) {
|
|
81838
|
+
return loftWithGuideRails(stations, rails, options);
|
|
81839
|
+
}
|
|
81840
|
+
};
|
|
79476
81841
|
let collectedHighlights = [];
|
|
79477
81842
|
function resetHighlights() {
|
|
79478
81843
|
collectedHighlights = [];
|
|
@@ -84149,10 +86514,14 @@ function spec(name, checkFn) {
|
|
|
84149
86514
|
};
|
|
84150
86515
|
}
|
|
84151
86516
|
let _collected = [];
|
|
86517
|
+
let _collisionAllowances = [];
|
|
86518
|
+
let _physicalComponentExpectations = [];
|
|
84152
86519
|
let _counter = 0;
|
|
84153
86520
|
let _activeGroup = null;
|
|
84154
86521
|
function resetVerifications() {
|
|
84155
86522
|
_collected = [];
|
|
86523
|
+
_collisionAllowances = [];
|
|
86524
|
+
_physicalComponentExpectations = [];
|
|
84156
86525
|
_counter = 0;
|
|
84157
86526
|
}
|
|
84158
86527
|
function getCollectedVerifications() {
|
|
@@ -84186,15 +86555,35 @@ function push(result) {
|
|
|
84186
86555
|
function roundNum(n, digits = 4) {
|
|
84187
86556
|
return Number.isFinite(n) ? n.toFixed(digits).replace(/\.?0+$/, "") : String(n);
|
|
84188
86557
|
}
|
|
86558
|
+
function meshDerivedManifoldBackend(shape) {
|
|
86559
|
+
const mesh = getShapeRuntimeBackend(shape).getMesh();
|
|
86560
|
+
return reconstructBackendFromMesh({
|
|
86561
|
+
numProp: mesh.numProp,
|
|
86562
|
+
triVerts: mesh.triVerts,
|
|
86563
|
+
vertProperties: mesh.vertProperties,
|
|
86564
|
+
mergeFromVert: mesh.mergeFromVert ?? new Uint32Array(),
|
|
86565
|
+
mergeToVert: mesh.mergeToVert ?? new Uint32Array()
|
|
86566
|
+
});
|
|
86567
|
+
}
|
|
86568
|
+
function backendForMinGap(shape) {
|
|
86569
|
+
const backend = getShapeRuntimeBackend(shape);
|
|
86570
|
+
if (isManifoldCapableBackend(backend)) return { backend, method: "exact", dispose: false };
|
|
86571
|
+
return { backend: meshDerivedManifoldBackend(shape), method: "mesh-derived", dispose: true };
|
|
86572
|
+
}
|
|
84189
86573
|
function computeMinGap(a2, b, searchLength) {
|
|
84190
|
-
const backendA =
|
|
84191
|
-
const backendB =
|
|
84192
|
-
|
|
84193
|
-
|
|
86574
|
+
const backendA = backendForMinGap(a2);
|
|
86575
|
+
const backendB = backendForMinGap(b);
|
|
86576
|
+
try {
|
|
86577
|
+
const manifoldA = requireManifoldShapeBackend(backendA.backend, "verification.minGap");
|
|
86578
|
+
const manifoldB = requireManifoldShapeBackend(backendB.backend, "verification.minGap");
|
|
86579
|
+
return {
|
|
86580
|
+
gap: manifoldA.minGap(manifoldB, searchLength),
|
|
86581
|
+
method: backendA.method === "exact" && backendB.method === "exact" ? "exact" : "mesh-derived"
|
|
86582
|
+
};
|
|
86583
|
+
} finally {
|
|
86584
|
+
if (backendA.dispose) disposeShapeBackend(backendA.backend);
|
|
86585
|
+
if (backendB.dispose) disposeShapeBackend(backendB.backend);
|
|
84194
86586
|
}
|
|
84195
|
-
const manifoldA = backendA.requireManifold("verification.minGap");
|
|
84196
|
-
const manifoldB = requireManifoldShapeBackend(backendB, "verification.minGap");
|
|
84197
|
-
return manifoldA.minGap(manifoldB, searchLength);
|
|
84198
86587
|
}
|
|
84199
86588
|
function vec3Dot(a2, b) {
|
|
84200
86589
|
return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
|
|
@@ -84329,8 +86718,143 @@ const verify = {
|
|
|
84329
86718
|
actual: `${roundNum(d2, 3)} mm`
|
|
84330
86719
|
});
|
|
84331
86720
|
} catch (e) {
|
|
84332
|
-
push({
|
|
86721
|
+
push({
|
|
86722
|
+
id: nextId(),
|
|
86723
|
+
label,
|
|
86724
|
+
kind: "interface",
|
|
86725
|
+
status: "fail",
|
|
86726
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
86727
|
+
line: line2
|
|
86728
|
+
});
|
|
86729
|
+
}
|
|
86730
|
+
},
|
|
86731
|
+
/**
|
|
86732
|
+
* Check the distance between two named connectors on a shape or group.
|
|
86733
|
+
*
|
|
86734
|
+
* Use this when connectors + `matchTo()` define a static assembly interface.
|
|
86735
|
+
* It proves the mate at runtime, unlike a plain source-level connector
|
|
86736
|
+
* declaration. The common case is `expected = 0`, meaning the two connector
|
|
86737
|
+
* origins should coincide after placement.
|
|
86738
|
+
*
|
|
86739
|
+
* **Example**
|
|
86740
|
+
*
|
|
86741
|
+
* ```ts
|
|
86742
|
+
* verify.connectorDistance("leg is seated", bench, "Rail.leg_0", "Leg0.head", 0, 0.01);
|
|
86743
|
+
* ```
|
|
86744
|
+
*/
|
|
86745
|
+
connectorDistance(label, target, connectorA, connectorB, expected = 0, tolerance = 0.01) {
|
|
86746
|
+
const line2 = captureSourceLine();
|
|
86747
|
+
try {
|
|
86748
|
+
const actual = target.connectorDistance(connectorA, connectorB);
|
|
86749
|
+
const diff = Math.abs(actual - expected);
|
|
86750
|
+
const passed = diff <= Math.abs(tolerance);
|
|
86751
|
+
push({
|
|
86752
|
+
id: nextId(),
|
|
86753
|
+
label,
|
|
86754
|
+
kind: "interface",
|
|
86755
|
+
status: passed ? "pass" : "fail",
|
|
86756
|
+
message: passed ? `Connector distance ${roundNum(actual, 4)} mm ≈ ${roundNum(expected, 4)} mm` : `Connector distance ${roundNum(actual, 4)} mm is outside ${roundNum(expected, 4)} ± ${roundNum(tolerance, 4)} mm`,
|
|
86757
|
+
line: passed ? void 0 : line2,
|
|
86758
|
+
expected: `${roundNum(expected, 4)} ± ${roundNum(tolerance, 4)} mm`,
|
|
86759
|
+
actual: `${roundNum(actual, 4)} mm`
|
|
86760
|
+
});
|
|
86761
|
+
} catch (e) {
|
|
86762
|
+
push({
|
|
86763
|
+
id: nextId(),
|
|
86764
|
+
label,
|
|
86765
|
+
kind: "interface",
|
|
86766
|
+
status: "fail",
|
|
86767
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
86768
|
+
line: line2
|
|
86769
|
+
});
|
|
86770
|
+
}
|
|
86771
|
+
},
|
|
86772
|
+
/**
|
|
86773
|
+
* Declare the expected physical connectivity component count for the returned visible model.
|
|
86774
|
+
*
|
|
86775
|
+
* **Details**
|
|
86776
|
+
*
|
|
86777
|
+
* Use this for generated mechanical models that should have a clear component graph:
|
|
86778
|
+
* one connected fixture, a purchased part plus a removable cartridge, a root assembly plus
|
|
86779
|
+
* named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned
|
|
86780
|
+
* visible objects with the same physical-connectivity analysis used in the quality gate and
|
|
86781
|
+
* fails if the actual component count differs.
|
|
86782
|
+
*
|
|
86783
|
+
* This catches the common generated-CAD failure where a script returns a visually plausible
|
|
86784
|
+
* artifact but the handle, screw, washer, cover, or terminal block is actually a separate island.
|
|
86785
|
+
*
|
|
86786
|
+
* **Example**
|
|
86787
|
+
*
|
|
86788
|
+
* ```ts
|
|
86789
|
+
* verify.physicalComponentCount("vise is one connected installed assembly", 1);
|
|
86790
|
+
* ```
|
|
86791
|
+
*/
|
|
86792
|
+
physicalComponentCount(label, expected) {
|
|
86793
|
+
const line2 = captureSourceLine();
|
|
86794
|
+
const id = nextId();
|
|
86795
|
+
if (!Number.isInteger(expected) || expected < 0) {
|
|
86796
|
+
push({
|
|
86797
|
+
id,
|
|
86798
|
+
label,
|
|
86799
|
+
kind: "interface",
|
|
86800
|
+
status: "fail",
|
|
86801
|
+
message: "Expected physical component count must be a non-negative integer",
|
|
86802
|
+
line: line2
|
|
86803
|
+
});
|
|
86804
|
+
return;
|
|
84333
86805
|
}
|
|
86806
|
+
_physicalComponentExpectations.push({ id, label, expected, line: line2 });
|
|
86807
|
+
push({
|
|
86808
|
+
id,
|
|
86809
|
+
label,
|
|
86810
|
+
kind: "interface",
|
|
86811
|
+
status: "pass",
|
|
86812
|
+
message: `Expected ${expected} physical component(s); checked by mechanical-integrity connectivity`
|
|
86813
|
+
});
|
|
86814
|
+
},
|
|
86815
|
+
/**
|
|
86816
|
+
* Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent.
|
|
86817
|
+
*
|
|
86818
|
+
* **Details**
|
|
86819
|
+
*
|
|
86820
|
+
* Use this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume:
|
|
86821
|
+
* welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately
|
|
86822
|
+
* bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers
|
|
86823
|
+
* without pockets, or parts placed with collision as a positioning hack.
|
|
86824
|
+
*
|
|
86825
|
+
* `forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are
|
|
86826
|
+
* returned as visible objects and the exact collision report finds that same object pair. Unused or
|
|
86827
|
+
* non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions.
|
|
86828
|
+
*
|
|
86829
|
+
* **Example**
|
|
86830
|
+
*
|
|
86831
|
+
* ```ts
|
|
86832
|
+
* verify.intentionalOverlap("rubber grip is overmolded on handle", rubberGrip, handleCore, "overmolded insert");
|
|
86833
|
+
* ```
|
|
86834
|
+
*/
|
|
86835
|
+
intentionalOverlap(label, a2, b, reason) {
|
|
86836
|
+
const line2 = captureSourceLine();
|
|
86837
|
+
const id = nextId();
|
|
86838
|
+
const trimmedReason = String(reason ?? "").trim();
|
|
86839
|
+
if (trimmedReason.length === 0) {
|
|
86840
|
+
push({
|
|
86841
|
+
id,
|
|
86842
|
+
label,
|
|
86843
|
+
kind: "interface",
|
|
86844
|
+
status: "fail",
|
|
86845
|
+
message: "Intentional overlap requires a manufacturing reason",
|
|
86846
|
+
line: line2
|
|
86847
|
+
});
|
|
86848
|
+
return;
|
|
86849
|
+
}
|
|
86850
|
+
_collisionAllowances.push({ id, label, reason: trimmedReason, a: a2, b, line: line2 });
|
|
86851
|
+
push({
|
|
86852
|
+
id,
|
|
86853
|
+
label,
|
|
86854
|
+
kind: "interface",
|
|
86855
|
+
status: "pass",
|
|
86856
|
+
message: `Intentional overlap declared: ${trimmedReason}`
|
|
86857
|
+
});
|
|
84334
86858
|
},
|
|
84335
86859
|
/**
|
|
84336
86860
|
* Check that two shapes do not collide (minGap > 0).
|
|
@@ -84340,19 +86864,28 @@ const verify = {
|
|
|
84340
86864
|
notColliding(label, a2, b, searchLength = 1) {
|
|
84341
86865
|
const line2 = captureSourceLine();
|
|
84342
86866
|
try {
|
|
84343
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
86867
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
86868
|
+
const methodLabel = method === "exact" ? "exact min gap" : "mesh-derived min gap";
|
|
84344
86869
|
const passed = gap > 0;
|
|
84345
86870
|
push({
|
|
84346
86871
|
id: nextId(),
|
|
84347
86872
|
label,
|
|
86873
|
+
kind: "interface",
|
|
84348
86874
|
status: passed ? "pass" : "fail",
|
|
84349
|
-
message: passed ? `No collision (
|
|
86875
|
+
message: passed ? `No collision (${methodLabel} ${roundNum(gap, 3)} mm)` : `Shapes are colliding (${methodLabel} ${roundNum(gap, 3)} mm ≤ 0)`,
|
|
84350
86876
|
line: passed ? void 0 : line2,
|
|
84351
86877
|
expected: "> 0 mm",
|
|
84352
86878
|
actual: `${roundNum(gap, 3)} mm`
|
|
84353
86879
|
});
|
|
84354
86880
|
} catch (e) {
|
|
84355
|
-
push({
|
|
86881
|
+
push({
|
|
86882
|
+
id: nextId(),
|
|
86883
|
+
label,
|
|
86884
|
+
kind: "interface",
|
|
86885
|
+
status: "fail",
|
|
86886
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
86887
|
+
line: line2
|
|
86888
|
+
});
|
|
84356
86889
|
}
|
|
84357
86890
|
},
|
|
84358
86891
|
/**
|
|
@@ -84361,13 +86894,15 @@ const verify = {
|
|
|
84361
86894
|
minClearance(label, a2, b, minGap, searchLength = 10) {
|
|
84362
86895
|
const line2 = captureSourceLine();
|
|
84363
86896
|
try {
|
|
84364
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
86897
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
86898
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
84365
86899
|
const passed = gap >= minGap;
|
|
84366
86900
|
push({
|
|
84367
86901
|
id: nextId(),
|
|
84368
86902
|
label,
|
|
86903
|
+
kind: "interface",
|
|
84369
86904
|
status: passed ? "pass" : "fail",
|
|
84370
|
-
message: passed ?
|
|
86905
|
+
message: passed ? `${methodLabel} ${roundNum(gap, 3)} mm ≥ ${roundNum(minGap, 3)} mm` : `${methodLabel} ${roundNum(gap, 3)} mm < required ${roundNum(minGap, 3)} mm`,
|
|
84371
86906
|
line: passed ? void 0 : line2,
|
|
84372
86907
|
expected: `≥ ${roundNum(minGap, 3)} mm`,
|
|
84373
86908
|
actual: `${roundNum(gap, 3)} mm`
|
|
@@ -84376,6 +86911,90 @@ const verify = {
|
|
|
84376
86911
|
push({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
|
|
84377
86912
|
}
|
|
84378
86913
|
},
|
|
86914
|
+
/**
|
|
86915
|
+
* Check that the clearance gap between two shapes is inside an allowed range.
|
|
86916
|
+
*
|
|
86917
|
+
* **Details**
|
|
86918
|
+
*
|
|
86919
|
+
* Use this for seated and retained interfaces where a part must be close
|
|
86920
|
+
* enough to be mechanically accountable, but must not collide beyond the
|
|
86921
|
+
* allowed minimum. It catches both failure modes that make generated CAD look
|
|
86922
|
+
* fake: parts floating away from their receiver, and parts intersecting their
|
|
86923
|
+
* receiver because the pocket, bore, or running clearance was not modeled.
|
|
86924
|
+
*
|
|
86925
|
+
* For contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny
|
|
86926
|
+
* numerical noise. For a running fit, use the intended clearance band.
|
|
86927
|
+
*
|
|
86928
|
+
* Manifold-backed shapes use exact min-gap distance. Other backends use a
|
|
86929
|
+
* mesh-derived min-gap check and say so in the verification message; keep
|
|
86930
|
+
* `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for
|
|
86931
|
+
* positive-volume interference.
|
|
86932
|
+
*
|
|
86933
|
+
* **Example**
|
|
86934
|
+
*
|
|
86935
|
+
* ```ts
|
|
86936
|
+
* verify.clearanceBetween("cover is seated on gasket", cover, gasket, -0.01, 0.05);
|
|
86937
|
+
* verify.clearanceBetween("carriage runs inside rail", carriage, rail, 0.2, 0.5);
|
|
86938
|
+
* ```
|
|
86939
|
+
*/
|
|
86940
|
+
clearanceBetween(label, a2, b, minGap, maxGap, searchLength) {
|
|
86941
|
+
const line2 = captureSourceLine();
|
|
86942
|
+
try {
|
|
86943
|
+
if (!Number.isFinite(minGap) || !Number.isFinite(maxGap)) {
|
|
86944
|
+
push({
|
|
86945
|
+
id: nextId(),
|
|
86946
|
+
label,
|
|
86947
|
+
kind: "interface",
|
|
86948
|
+
status: "fail",
|
|
86949
|
+
message: "Clearance range must use finite numbers",
|
|
86950
|
+
line: line2
|
|
86951
|
+
});
|
|
86952
|
+
return;
|
|
86953
|
+
}
|
|
86954
|
+
if (maxGap < minGap) {
|
|
86955
|
+
push({
|
|
86956
|
+
id: nextId(),
|
|
86957
|
+
label,
|
|
86958
|
+
kind: "interface",
|
|
86959
|
+
status: "fail",
|
|
86960
|
+
message: `Clearance max ${roundNum(maxGap, 3)} mm is smaller than min ${roundNum(minGap, 3)} mm`,
|
|
86961
|
+
line: line2
|
|
86962
|
+
});
|
|
86963
|
+
return;
|
|
86964
|
+
}
|
|
86965
|
+
const search = searchLength ?? Math.max(10, Math.abs(maxGap) * 2 + 1);
|
|
86966
|
+
const { gap, method } = computeMinGap(a2, b, search);
|
|
86967
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
86968
|
+
const passed = gap >= minGap && gap <= maxGap;
|
|
86969
|
+
let message;
|
|
86970
|
+
if (passed) {
|
|
86971
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm in [${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`;
|
|
86972
|
+
} else if (gap < minGap) {
|
|
86973
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm < allowed minimum ${roundNum(minGap, 3)} mm`;
|
|
86974
|
+
} else {
|
|
86975
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm > allowed maximum ${roundNum(maxGap, 3)} mm`;
|
|
86976
|
+
}
|
|
86977
|
+
push({
|
|
86978
|
+
id: nextId(),
|
|
86979
|
+
label,
|
|
86980
|
+
kind: "interface",
|
|
86981
|
+
status: passed ? "pass" : "fail",
|
|
86982
|
+
message,
|
|
86983
|
+
line: passed ? void 0 : line2,
|
|
86984
|
+
expected: `[${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`,
|
|
86985
|
+
actual: `${roundNum(gap, 3)} mm`
|
|
86986
|
+
});
|
|
86987
|
+
} catch (e) {
|
|
86988
|
+
push({
|
|
86989
|
+
id: nextId(),
|
|
86990
|
+
label,
|
|
86991
|
+
kind: "interface",
|
|
86992
|
+
status: "fail",
|
|
86993
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
86994
|
+
line: line2
|
|
86995
|
+
});
|
|
86996
|
+
}
|
|
86997
|
+
},
|
|
84379
86998
|
/**
|
|
84380
86999
|
* Check that two face normals are parallel (within toleranceDeg degrees).
|
|
84381
87000
|
*/
|
|
@@ -296941,6 +299560,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
|
|
|
296941
299560
|
nurbsSurface,
|
|
296942
299561
|
spline2d,
|
|
296943
299562
|
spline3d,
|
|
299563
|
+
Loft,
|
|
296944
299564
|
loft,
|
|
296945
299565
|
loftAlongSpine,
|
|
296946
299566
|
sweep,
|