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$8(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];
|
|
@@ -5195,13 +5195,13 @@ function parseMeshFile(data, format) {
|
|
|
5195
5195
|
return parse3mf(data);
|
|
5196
5196
|
}
|
|
5197
5197
|
}
|
|
5198
|
-
const EPS$
|
|
5198
|
+
const EPS$a = 1e-8;
|
|
5199
5199
|
function length$3(v) {
|
|
5200
5200
|
return Math.hypot(v[0], v[1], v[2]);
|
|
5201
5201
|
}
|
|
5202
5202
|
function normalize$7(v) {
|
|
5203
5203
|
const len2 = length$3(v);
|
|
5204
|
-
if (len2 < EPS$
|
|
5204
|
+
if (len2 < EPS$a) throw new Error("Plane normal must be non-zero");
|
|
5205
5205
|
return [v[0] / len2, v[1] / len2, v[2] / len2];
|
|
5206
5206
|
}
|
|
5207
5207
|
function resolvePlaneOriginNormal(plane) {
|
|
@@ -5223,12 +5223,12 @@ function resolvePlaneOriginNormal(plane) {
|
|
|
5223
5223
|
function rotationToPlaneSpace(normal) {
|
|
5224
5224
|
const n = normalize$7(normal);
|
|
5225
5225
|
const dot2 = n[2];
|
|
5226
|
-
if (dot2 > 1 - EPS$
|
|
5226
|
+
if (dot2 > 1 - EPS$a) {
|
|
5227
5227
|
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
5228
5228
|
}
|
|
5229
5229
|
let axis;
|
|
5230
5230
|
let angle;
|
|
5231
|
-
if (dot2 < -1 + EPS$
|
|
5231
|
+
if (dot2 < -1 + EPS$a) {
|
|
5232
5232
|
axis = [1, 0, 0];
|
|
5233
5233
|
angle = Math.PI;
|
|
5234
5234
|
} else {
|
|
@@ -7790,7 +7790,7 @@ function scale$6(v, s) {
|
|
|
7790
7790
|
function sub$7(a2, b) {
|
|
7791
7791
|
return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
|
|
7792
7792
|
}
|
|
7793
|
-
function cross$
|
|
7793
|
+
function cross$8(a2, b) {
|
|
7794
7794
|
return [a2[1] * b[2] - a2[2] * b[1], a2[2] * b[0] - a2[0] * b[2], a2[0] * b[1] - a2[1] * b[0]];
|
|
7795
7795
|
}
|
|
7796
7796
|
function makeEdge(name, start, end, faceName, curve) {
|
|
@@ -7826,7 +7826,7 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
|
|
|
7826
7826
|
const center = options.center ?? average$1(corners);
|
|
7827
7827
|
const uAxis = normalizeAxis$1(sub$7(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
|
|
7828
7828
|
const vAxis = normalizeAxis$1(sub$7(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
|
|
7829
|
-
const normal = normalizeAxis$1(options.normal ?? cross$
|
|
7829
|
+
const normal = normalizeAxis$1(options.normal ?? cross$8(uAxis, vAxis));
|
|
7830
7830
|
const faces = /* @__PURE__ */ new Map();
|
|
7831
7831
|
faces.set(faceName, {
|
|
7832
7832
|
name: faceName,
|
|
@@ -9728,6 +9728,7 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
|
|
|
9728
9728
|
edgeLength: options.edgeLength
|
|
9729
9729
|
};
|
|
9730
9730
|
}
|
|
9731
|
+
const EPS$9 = 1e-9;
|
|
9731
9732
|
function resamplePolygon(poly, targetCount) {
|
|
9732
9733
|
if (poly.length < 2) return poly;
|
|
9733
9734
|
if (targetCount <= 0) return [];
|
|
@@ -9765,6 +9766,78 @@ function resamplePolygon(poly, targetCount) {
|
|
|
9765
9766
|
}
|
|
9766
9767
|
return out;
|
|
9767
9768
|
}
|
|
9769
|
+
function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid$2(poly)) {
|
|
9770
|
+
if (poly.length < 3 || targetCount <= 0) return null;
|
|
9771
|
+
if (!isConvexPolygon(poly)) return null;
|
|
9772
|
+
const out = [];
|
|
9773
|
+
for (let index2 = 0; index2 < targetCount; index2 += 1) {
|
|
9774
|
+
const angle = index2 / targetCount * Math.PI * 2;
|
|
9775
|
+
const point2 = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
|
|
9776
|
+
if (!point2) return null;
|
|
9777
|
+
out.push(point2);
|
|
9778
|
+
}
|
|
9779
|
+
return out;
|
|
9780
|
+
}
|
|
9781
|
+
function rayPolygonIntersection(origin, direction2, poly) {
|
|
9782
|
+
let bestT = Infinity;
|
|
9783
|
+
let best = null;
|
|
9784
|
+
for (let index2 = 0; index2 < poly.length; index2 += 1) {
|
|
9785
|
+
const a2 = poly[index2];
|
|
9786
|
+
const b = poly[(index2 + 1) % poly.length];
|
|
9787
|
+
const edge = [b[0] - a2[0], b[1] - a2[1]];
|
|
9788
|
+
const denom = cross$7(direction2, edge);
|
|
9789
|
+
if (Math.abs(denom) < EPS$9) continue;
|
|
9790
|
+
const delta = [a2[0] - origin[0], a2[1] - origin[1]];
|
|
9791
|
+
const rayT = cross$7(delta, edge) / denom;
|
|
9792
|
+
const edgeT = cross$7(delta, direction2) / denom;
|
|
9793
|
+
if (rayT >= -EPS$9 && edgeT >= -EPS$9 && edgeT <= 1 + EPS$9 && rayT < bestT) {
|
|
9794
|
+
bestT = rayT;
|
|
9795
|
+
best = [origin[0] + direction2[0] * rayT, origin[1] + direction2[1] * rayT];
|
|
9796
|
+
}
|
|
9797
|
+
}
|
|
9798
|
+
return best;
|
|
9799
|
+
}
|
|
9800
|
+
function polygonCentroid$2(poly) {
|
|
9801
|
+
let area2 = 0;
|
|
9802
|
+
let cx = 0;
|
|
9803
|
+
let cy = 0;
|
|
9804
|
+
for (let index2 = 0; index2 < poly.length; index2 += 1) {
|
|
9805
|
+
const a2 = poly[index2];
|
|
9806
|
+
const b = poly[(index2 + 1) % poly.length];
|
|
9807
|
+
const crossValue = cross$7(a2, b);
|
|
9808
|
+
area2 += crossValue;
|
|
9809
|
+
cx += (a2[0] + b[0]) * crossValue;
|
|
9810
|
+
cy += (a2[1] + b[1]) * crossValue;
|
|
9811
|
+
}
|
|
9812
|
+
if (Math.abs(area2) < EPS$9) return averagePoint(poly);
|
|
9813
|
+
return [cx / (3 * area2), cy / (3 * area2)];
|
|
9814
|
+
}
|
|
9815
|
+
function averagePoint(poly) {
|
|
9816
|
+
let x2 = 0;
|
|
9817
|
+
let y2 = 0;
|
|
9818
|
+
for (const point2 of poly) {
|
|
9819
|
+
x2 += point2[0];
|
|
9820
|
+
y2 += point2[1];
|
|
9821
|
+
}
|
|
9822
|
+
return [x2 / poly.length, y2 / poly.length];
|
|
9823
|
+
}
|
|
9824
|
+
function isConvexPolygon(poly) {
|
|
9825
|
+
let sign2 = 0;
|
|
9826
|
+
for (let index2 = 0; index2 < poly.length; index2 += 1) {
|
|
9827
|
+
const a2 = poly[index2];
|
|
9828
|
+
const b = poly[(index2 + 1) % poly.length];
|
|
9829
|
+
const c2 = poly[(index2 + 2) % poly.length];
|
|
9830
|
+
const turn = cross$7([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
|
|
9831
|
+
if (Math.abs(turn) < EPS$9) continue;
|
|
9832
|
+
const currentSign = Math.sign(turn);
|
|
9833
|
+
if (sign2 !== 0 && currentSign !== sign2) return false;
|
|
9834
|
+
sign2 = currentSign;
|
|
9835
|
+
}
|
|
9836
|
+
return sign2 !== 0;
|
|
9837
|
+
}
|
|
9838
|
+
function cross$7(a2, b) {
|
|
9839
|
+
return a2[0] * b[1] - a2[1] * b[0];
|
|
9840
|
+
}
|
|
9768
9841
|
function loftStitched(profiles2, heights, wasm) {
|
|
9769
9842
|
if (profiles2.length < 2) return null;
|
|
9770
9843
|
const classified = profiles2.map((loops) => classifyLoops(loops));
|
|
@@ -9893,8 +9966,10 @@ function stitchSingleLoopLoft(loops, heights, wasm) {
|
|
|
9893
9966
|
maxPoints = Math.max(maxPoints, loop.length);
|
|
9894
9967
|
}
|
|
9895
9968
|
const N = Math.max(maxPoints, 24);
|
|
9969
|
+
const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
|
|
9970
|
+
const useAngularSamples = angularSamples.every((samples) => samples != null);
|
|
9896
9971
|
const resampled = normalizedLoops.map((loop, i) => {
|
|
9897
|
-
const pts2d = resamplePolygon(loop, N);
|
|
9972
|
+
const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
|
|
9898
9973
|
const z2 = heights[i];
|
|
9899
9974
|
return pts2d.map(([x2, y2]) => [x2, y2, z2]);
|
|
9900
9975
|
});
|
|
@@ -9955,7 +10030,7 @@ let _wasm$1 = null;
|
|
|
9955
10030
|
async function initManifoldWasm() {
|
|
9956
10031
|
if (_wasm$1) return _wasm$1;
|
|
9957
10032
|
performance.mark("manifold:start");
|
|
9958
|
-
const Module = (await import("./manifold-
|
|
10033
|
+
const Module = (await import("./manifold-Bk26ViCr.js")).default;
|
|
9959
10034
|
performance.mark("manifold:imported");
|
|
9960
10035
|
const wasm = await Module();
|
|
9961
10036
|
wasm.setup();
|
|
@@ -46528,10 +46603,8 @@ class PathBuilder {
|
|
|
46528
46603
|
if (radius <= 0) throw new Error("fillet: radius must be positive");
|
|
46529
46604
|
const n = this.segs.length;
|
|
46530
46605
|
if (n < 2) throw new Error("fillet: need at least 2 segments before a fillet");
|
|
46531
|
-
const prev = this.segs[n - 2];
|
|
46532
46606
|
const curr = this.segs[n - 1];
|
|
46533
|
-
|
|
46534
|
-
const { trimA, trimB, arcSeg } = this.computeFilletGeom(radius);
|
|
46607
|
+
const { trimA, arcSeg } = this.computeFilletGeom(radius);
|
|
46535
46608
|
if (!arcSeg) throw new Error("fillet: cannot fillet these segments (parallel or degenerate)");
|
|
46536
46609
|
this.trimLastSegEnd(n - 2, trimA[0], trimA[1]);
|
|
46537
46610
|
const trimmedSeg = { ...curr };
|
|
@@ -46603,7 +46676,6 @@ class PathBuilder {
|
|
|
46603
46676
|
}
|
|
46604
46677
|
getSegDirAt(seg, which) {
|
|
46605
46678
|
if (seg.kind === "line" || seg.kind === "move") {
|
|
46606
|
-
this.segs.length;
|
|
46607
46679
|
const idx = this.segs.indexOf(seg);
|
|
46608
46680
|
if (seg.kind === "line") {
|
|
46609
46681
|
let sx, sy;
|
|
@@ -46845,6 +46917,41 @@ class PathBuilder {
|
|
|
46845
46917
|
}
|
|
46846
46918
|
return pts;
|
|
46847
46919
|
}
|
|
46920
|
+
/**
|
|
46921
|
+
* Return the open path as a sampled 2D polyline.
|
|
46922
|
+
*
|
|
46923
|
+
* This is for construction geometry such as guide rails, measured centerlines,
|
|
46924
|
+
* and curve-driven helpers where the authored path should stay open instead of
|
|
46925
|
+
* becoming a filled sketch or stroked profile.
|
|
46926
|
+
*
|
|
46927
|
+
* **Example**
|
|
46928
|
+
*
|
|
46929
|
+
* ```ts
|
|
46930
|
+
* const rail = path()
|
|
46931
|
+
* .moveTo(24, 0)
|
|
46932
|
+
* .bezierTo(32, 44, 28, 92, 18, 120)
|
|
46933
|
+
* .toPolyline();
|
|
46934
|
+
* ```
|
|
46935
|
+
*
|
|
46936
|
+
* @returns A sampled open polyline.
|
|
46937
|
+
* @category Path Builder
|
|
46938
|
+
*/
|
|
46939
|
+
toPolyline() {
|
|
46940
|
+
const moveCount = this.segs.filter((seg) => seg.kind === "move").length;
|
|
46941
|
+
if (moveCount > 1) {
|
|
46942
|
+
throw new Error("path().toPolyline() supports one continuous open path. Use separate path() builders for separate rails.");
|
|
46943
|
+
}
|
|
46944
|
+
const pts = [];
|
|
46945
|
+
for (const point2 of this.tessellate()) {
|
|
46946
|
+
if (!point2.every(Number.isFinite)) throw new Error("path().toPolyline() produced a non-finite point");
|
|
46947
|
+
const previous = pts[pts.length - 1];
|
|
46948
|
+
if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) {
|
|
46949
|
+
pts.push(point2);
|
|
46950
|
+
}
|
|
46951
|
+
}
|
|
46952
|
+
if (pts.length < 2) throw new Error("path().toPolyline() needs at least 2 points");
|
|
46953
|
+
return pts;
|
|
46954
|
+
}
|
|
46848
46955
|
// ── Output ────────────────────────────────────────────────────────────────
|
|
46849
46956
|
/**
|
|
46850
46957
|
* Close the path and return a filled `Sketch`.
|
|
@@ -48702,7 +48809,7 @@ function spurGear(options) {
|
|
|
48702
48809
|
});
|
|
48703
48810
|
return attachGearMeta(shapeWithConnectors, meta2);
|
|
48704
48811
|
}
|
|
48705
|
-
function requirePositive$
|
|
48812
|
+
function requirePositive$8(scope, name, value) {
|
|
48706
48813
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
48707
48814
|
}
|
|
48708
48815
|
function requireOptionalBore(scope, boreDiameter, maxDiameter) {
|
|
@@ -48724,8 +48831,8 @@ function cutBore$1(shape, boreDiameter) {
|
|
|
48724
48831
|
return shape.subtract(cutter);
|
|
48725
48832
|
}
|
|
48726
48833
|
function gearBodyDisk(options) {
|
|
48727
|
-
requirePositive$
|
|
48728
|
-
requirePositive$
|
|
48834
|
+
requirePositive$8("gearBodyDisk", "outerRadius", options.outerRadius);
|
|
48835
|
+
requirePositive$8("gearBodyDisk", "faceWidth", options.faceWidth);
|
|
48729
48836
|
const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
|
|
48730
48837
|
const segments = resolveSegments(options.segments);
|
|
48731
48838
|
const outer = circle2d(options.outerRadius, segments);
|
|
@@ -48733,14 +48840,14 @@ function gearBodyDisk(options) {
|
|
|
48733
48840
|
return sketchExtrude(profile, options.faceWidth);
|
|
48734
48841
|
}
|
|
48735
48842
|
function gearBodyDiskWithHub(options) {
|
|
48736
|
-
requirePositive$
|
|
48843
|
+
requirePositive$8("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
|
|
48737
48844
|
if (options.hubDiameter >= options.outerRadius * 2) {
|
|
48738
48845
|
throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
|
|
48739
48846
|
}
|
|
48740
48847
|
const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
|
|
48741
48848
|
const base = gearBodyDisk({ ...options, boreDiameter: 0 });
|
|
48742
48849
|
const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
|
|
48743
|
-
requirePositive$
|
|
48850
|
+
requirePositive$8("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
|
|
48744
48851
|
const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
|
|
48745
48852
|
0,
|
|
48746
48853
|
0,
|
|
@@ -48749,11 +48856,11 @@ function gearBodyDiskWithHub(options) {
|
|
|
48749
48856
|
return cutBore$1(base.add(hub), bore);
|
|
48750
48857
|
}
|
|
48751
48858
|
function gearBodySpoked(options) {
|
|
48752
|
-
requirePositive$
|
|
48753
|
-
requirePositive$
|
|
48754
|
-
requirePositive$
|
|
48755
|
-
requirePositive$
|
|
48756
|
-
requirePositive$
|
|
48859
|
+
requirePositive$8("gearBodySpoked", "outerRadius", options.outerRadius);
|
|
48860
|
+
requirePositive$8("gearBodySpoked", "faceWidth", options.faceWidth);
|
|
48861
|
+
requirePositive$8("gearBodySpoked", "rimWidth", options.rimWidth);
|
|
48862
|
+
requirePositive$8("gearBodySpoked", "hubDiameter", options.hubDiameter);
|
|
48863
|
+
requirePositive$8("gearBodySpoked", "spokeWidth", options.spokeWidth);
|
|
48757
48864
|
if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
|
|
48758
48865
|
throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
|
|
48759
48866
|
}
|
|
@@ -48776,12 +48883,12 @@ function gearBodySpoked(options) {
|
|
|
48776
48883
|
}
|
|
48777
48884
|
function gearBodyFromProfile(profile, options) {
|
|
48778
48885
|
if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
|
|
48779
|
-
requirePositive$
|
|
48886
|
+
requirePositive$8("gearBodyFromProfile", "faceWidth", options.faceWidth);
|
|
48780
48887
|
const bore = options.boreDiameter ?? 0;
|
|
48781
48888
|
if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
|
|
48782
48889
|
return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
|
|
48783
48890
|
}
|
|
48784
|
-
function requirePositive$
|
|
48891
|
+
function requirePositive$7(scope, name, value) {
|
|
48785
48892
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
48786
48893
|
}
|
|
48787
48894
|
function requireFiniteAngle(scope, name, value) {
|
|
@@ -48843,7 +48950,7 @@ function buildSpurTeethRegion(options, name, faceWidth) {
|
|
|
48843
48950
|
}
|
|
48844
48951
|
function buildSolidArcRegion(options, name, faceWidth) {
|
|
48845
48952
|
const scope = "driveWheel.addSolidArcBetween";
|
|
48846
|
-
requirePositive$
|
|
48953
|
+
requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
48847
48954
|
const innerRadius = options.innerRadius ?? 0;
|
|
48848
48955
|
if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
48849
48956
|
if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
|
|
@@ -48909,7 +49016,7 @@ class DriveWheelBuilder {
|
|
|
48909
49016
|
__publicField(this, "boreDiameter");
|
|
48910
49017
|
__publicField(this, "regions", []);
|
|
48911
49018
|
if (options.body !== void 0 && !(options.body instanceof Shape)) throw new Error('driveWheel: "body" must be a Shape');
|
|
48912
|
-
if (options.faceWidth !== void 0) requirePositive$
|
|
49019
|
+
if (options.faceWidth !== void 0) requirePositive$7("driveWheel", "faceWidth", options.faceWidth);
|
|
48913
49020
|
const boreDiameter = options.boreDiameter ?? 0;
|
|
48914
49021
|
if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
|
|
48915
49022
|
this.body = options.body;
|
|
@@ -48944,7 +49051,7 @@ class DriveWheelBuilder {
|
|
|
48944
49051
|
if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
|
|
48945
49052
|
throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
48946
49053
|
}
|
|
48947
|
-
if (options.outerRadius !== void 0) requirePositive$
|
|
49054
|
+
if (options.outerRadius !== void 0) requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
48948
49055
|
this.regions.push({
|
|
48949
49056
|
shape: shape.clone(),
|
|
48950
49057
|
meta: {
|
|
@@ -49010,7 +49117,7 @@ class DriveWheelBuilder {
|
|
|
49010
49117
|
resolveFaceWidth(scope, localFaceWidth) {
|
|
49011
49118
|
const faceWidth = localFaceWidth ?? this.faceWidth;
|
|
49012
49119
|
if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
|
|
49013
|
-
requirePositive$
|
|
49120
|
+
requirePositive$7(scope, "faceWidth", faceWidth);
|
|
49014
49121
|
if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
|
|
49015
49122
|
throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
|
|
49016
49123
|
}
|
|
@@ -50163,6 +50270,1867 @@ function washer(size, options) {
|
|
|
50163
50270
|
const bore = cylinder(dims.t + 1, dims.id / 2, void 0, segs);
|
|
50164
50271
|
return outer.subtract(bore);
|
|
50165
50272
|
}
|
|
50273
|
+
function requirePositive$6(value, name) {
|
|
50274
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
|
|
50275
|
+
return value;
|
|
50276
|
+
}
|
|
50277
|
+
function requireNonNegative(value, name) {
|
|
50278
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative finite number`);
|
|
50279
|
+
return value;
|
|
50280
|
+
}
|
|
50281
|
+
function metricWasherSizeForPin(pinDiameter) {
|
|
50282
|
+
if (pinDiameter <= 2) return "M2";
|
|
50283
|
+
if (pinDiameter <= 2.5) return "M2.5";
|
|
50284
|
+
if (pinDiameter <= 3) return "M3";
|
|
50285
|
+
if (pinDiameter <= 4) return "M4";
|
|
50286
|
+
if (pinDiameter <= 5) return "M5";
|
|
50287
|
+
if (pinDiameter <= 6) return "M6";
|
|
50288
|
+
if (pinDiameter <= 8) return "M8";
|
|
50289
|
+
return "M10";
|
|
50290
|
+
}
|
|
50291
|
+
function cylinderAlongX(length4, radius, xCenter, segments) {
|
|
50292
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([1, 0, 0]).translate(xCenter - length4 / 2, 0, 0);
|
|
50293
|
+
}
|
|
50294
|
+
function tubeAlongX(length4, outerRadius, innerRadius, xCenter, segments) {
|
|
50295
|
+
return cylinderAlongX(length4, outerRadius, xCenter, segments).subtract(cylinderAlongX(length4 + 0.4, innerRadius, xCenter, segments));
|
|
50296
|
+
}
|
|
50297
|
+
function cylinderAlongY(length4, radius, yCenter, segments) {
|
|
50298
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([0, 1, 0]).translate(0, yCenter - length4 / 2, 0);
|
|
50299
|
+
}
|
|
50300
|
+
function tubeAlongY(length4, outerRadius, innerRadius, yCenter, segments) {
|
|
50301
|
+
return cylinderAlongY(length4, outerRadius, yCenter, segments).subtract(cylinderAlongY(length4 + 0.4, innerRadius, yCenter, segments));
|
|
50302
|
+
}
|
|
50303
|
+
function tubeAlongZ(height, outerRadius, innerRadius, segments) {
|
|
50304
|
+
return cylinder(height, outerRadius, void 0, segments).subtract(
|
|
50305
|
+
cylinder(height + 0.4, innerRadius, void 0, segments).translate(0, 0, -0.2)
|
|
50306
|
+
);
|
|
50307
|
+
}
|
|
50308
|
+
function washerAlongX(size, xCenter, segments) {
|
|
50309
|
+
const dims = WASHER_TABLE[size];
|
|
50310
|
+
return washer(size, { segments }).pointAlong([1, 0, 0]).translate(xCenter - dims.t / 2, 0, 0);
|
|
50311
|
+
}
|
|
50312
|
+
function resolveBoltInset(raw, fallback) {
|
|
50313
|
+
if (raw === void 0) return [fallback, fallback];
|
|
50314
|
+
if (typeof raw === "number") return [requirePositive$6(raw, "boltInset"), requirePositive$6(raw, "boltInset")];
|
|
50315
|
+
if (raw.length !== 2) throw new Error("boltInset tuple must be [x, y]");
|
|
50316
|
+
return [requirePositive$6(raw[0], "boltInset[0]"), requirePositive$6(raw[1], "boltInset[1]")];
|
|
50317
|
+
}
|
|
50318
|
+
function validateBoltPositionsForServiceCover(args) {
|
|
50319
|
+
args.positions.forEach(([x2, y2], index2) => {
|
|
50320
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
50321
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] must contain finite numbers`);
|
|
50322
|
+
}
|
|
50323
|
+
if (Math.abs(x2) + args.holeRadius >= args.coverWidth / 2 || Math.abs(y2) + args.holeRadius >= args.coverDepth / 2) {
|
|
50324
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] is too close to the cover edge`);
|
|
50325
|
+
}
|
|
50326
|
+
const overlapsOpening = Math.abs(x2) - args.holeRadius <= args.openingWidth / 2 && Math.abs(y2) - args.holeRadius <= args.openingDepth / 2;
|
|
50327
|
+
if (overlapsOpening) {
|
|
50328
|
+
throw new Error(
|
|
50329
|
+
`boltedServiceCover: boltPositions[${index2}] lands over the service opening; decrease boltInset, increase ledgeWidth, or provide a smaller opening`
|
|
50330
|
+
);
|
|
50331
|
+
}
|
|
50332
|
+
});
|
|
50333
|
+
}
|
|
50334
|
+
function placeCutterAtPositions(cutter, positions, z2) {
|
|
50335
|
+
return union(...positions.map(([x2, y2]) => cutter.translate(x2, y2, z2)));
|
|
50336
|
+
}
|
|
50337
|
+
function boltedServiceCover(options) {
|
|
50338
|
+
const width = requirePositive$6(options.width, "width");
|
|
50339
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
50340
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 3, "coverThickness");
|
|
50341
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 8, "parentThickness");
|
|
50342
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
50343
|
+
const gasketThickness = Math.max(0, options.gasketThickness ?? 0.8);
|
|
50344
|
+
const gasketInset = Math.max(0, options.gasketInset ?? 2);
|
|
50345
|
+
const screwSize = options.screwSize ?? "M4";
|
|
50346
|
+
const segments = options.segments ?? 36;
|
|
50347
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
50348
|
+
if (!sizeData) throw new Error(`boltedServiceCover: unsupported screwSize "${screwSize}"`);
|
|
50349
|
+
const screwLength = requirePositive$6(
|
|
50350
|
+
options.screwLength ?? parentThickness + gasketThickness + coverThickness + 4,
|
|
50351
|
+
"screwLength"
|
|
50352
|
+
);
|
|
50353
|
+
const coverFit = options.coverFit ?? "normal";
|
|
50354
|
+
const counterboreEnabled = options.counterbore ?? true;
|
|
50355
|
+
const [insetX, insetY] = resolveBoltInset(options.boltInset, Math.max(ledgeWidth * 0.65, sizeData.head * 0.75));
|
|
50356
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
50357
|
+
throw new Error("boltedServiceCover: boltInset leaves no room for a four-corner bolt pattern");
|
|
50358
|
+
}
|
|
50359
|
+
const boltPositions = options.boltPositions ?? [
|
|
50360
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
50361
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
50362
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
50363
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
50364
|
+
];
|
|
50365
|
+
if (boltPositions.length === 0) throw new Error("boltedServiceCover: boltPositions must contain at least one point");
|
|
50366
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
50367
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
50368
|
+
const openingWidth = Math.max(1, width - ledgeWidth * 2);
|
|
50369
|
+
const openingDepth = Math.max(1, depth - ledgeWidth * 2);
|
|
50370
|
+
validateBoltPositionsForServiceCover({
|
|
50371
|
+
positions: boltPositions,
|
|
50372
|
+
coverWidth: width,
|
|
50373
|
+
coverDepth: depth,
|
|
50374
|
+
openingWidth,
|
|
50375
|
+
openingDepth,
|
|
50376
|
+
holeRadius: sizeData[coverFit] / 2
|
|
50377
|
+
});
|
|
50378
|
+
const coverHole = fastenerHole({
|
|
50379
|
+
size: screwSize,
|
|
50380
|
+
fit: coverFit,
|
|
50381
|
+
depth: coverThickness + 0.6,
|
|
50382
|
+
center: true,
|
|
50383
|
+
segments,
|
|
50384
|
+
...counterboreEnabled ? { counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.4)) } } : {}
|
|
50385
|
+
});
|
|
50386
|
+
const parentTap = fastenerHole({ size: screwSize, fit: "tap", depth: parentThickness + 0.6, center: true, segments });
|
|
50387
|
+
const parentThreadEnvelope = fastenerHole({
|
|
50388
|
+
size: screwSize,
|
|
50389
|
+
fit: "close",
|
|
50390
|
+
depth: parentThickness + 0.6,
|
|
50391
|
+
center: true,
|
|
50392
|
+
segments
|
|
50393
|
+
});
|
|
50394
|
+
const openingCutter = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
50395
|
+
const parentTappedPattern = placeCutterAtPositions(parentTap, boltPositions, parentThickness / 2);
|
|
50396
|
+
const parentThreadEnvelopePattern = placeCutterAtPositions(parentThreadEnvelope, boltPositions, parentThickness / 2);
|
|
50397
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(openingCutter).subtract(parentThreadEnvelopePattern).color("#4b5563");
|
|
50398
|
+
let coverBlank = box(width, depth, coverThickness);
|
|
50399
|
+
if (options.pullTabs ?? true) {
|
|
50400
|
+
const tabWidth = Math.min(width * 0.18, Math.max(sizeData.head * 1.6, 12));
|
|
50401
|
+
const tabDepth = Math.max(4, coverThickness * 1.4);
|
|
50402
|
+
const tabOverlap = Math.min(0.5, tabDepth * 0.25);
|
|
50403
|
+
const tabY = -depth / 2 - tabDepth / 2 + tabOverlap;
|
|
50404
|
+
const tabX = width * 0.23;
|
|
50405
|
+
coverBlank = union(
|
|
50406
|
+
coverBlank,
|
|
50407
|
+
box(tabWidth, tabDepth, coverThickness).translate(-tabX, tabY, 0),
|
|
50408
|
+
box(tabWidth, tabDepth, coverThickness).translate(tabX, tabY, 0)
|
|
50409
|
+
);
|
|
50410
|
+
}
|
|
50411
|
+
const coverClearancePattern = placeCutterAtPositions(coverHole, boltPositions, coverThickness / 2);
|
|
50412
|
+
const cover = coverBlank.subtract(coverClearancePattern).translate(0, 0, parentThickness + gasketThickness).color("#334155");
|
|
50413
|
+
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;
|
|
50414
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
50415
|
+
const screwOriginZ = parentThickness + gasketThickness + coverThickness;
|
|
50416
|
+
const screws = boltPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
50417
|
+
const parts = [
|
|
50418
|
+
{ name: "service cover parent ledge with threaded hole envelopes", shape: parent },
|
|
50419
|
+
...gasket ? [{ name: "service cover gasket seated on ledge", shape: gasket }] : [],
|
|
50420
|
+
{ name: "bolted service cover plate with fused pull tabs", shape: cover },
|
|
50421
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} cover screw ${index2 + 1}`, shape }))
|
|
50422
|
+
];
|
|
50423
|
+
return {
|
|
50424
|
+
parts,
|
|
50425
|
+
parent,
|
|
50426
|
+
cover,
|
|
50427
|
+
gasket,
|
|
50428
|
+
screws,
|
|
50429
|
+
boltPositions,
|
|
50430
|
+
cutters: {
|
|
50431
|
+
coverClearance: coverClearancePattern,
|
|
50432
|
+
parentTapped: parentTappedPattern,
|
|
50433
|
+
parentThreadEnvelope: parentThreadEnvelopePattern
|
|
50434
|
+
},
|
|
50435
|
+
dims: {
|
|
50436
|
+
width,
|
|
50437
|
+
depth,
|
|
50438
|
+
coverThickness,
|
|
50439
|
+
parentThickness,
|
|
50440
|
+
ledgeWidth,
|
|
50441
|
+
gasketThickness,
|
|
50442
|
+
screwSize,
|
|
50443
|
+
screwLength,
|
|
50444
|
+
clearanceDia: sizeData[coverFit],
|
|
50445
|
+
tapDia: sizeData.tap,
|
|
50446
|
+
threadEnvelopeDia: sizeData.close
|
|
50447
|
+
}
|
|
50448
|
+
};
|
|
50449
|
+
}
|
|
50450
|
+
function datumEnclosureAssembly(options) {
|
|
50451
|
+
const width = requirePositive$6(options.width, "width");
|
|
50452
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
50453
|
+
const height = requirePositive$6(options.height, "height");
|
|
50454
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.4, "wallThickness");
|
|
50455
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? wallThickness, "baseThickness");
|
|
50456
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
50457
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? Math.max(3.6, wallThickness * 1.35), "ledgeWidth");
|
|
50458
|
+
const gasketThickness = requireNonNegative(options.gasketThickness ?? 0.8, "gasketThickness");
|
|
50459
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
50460
|
+
const screwSize = options.screwSize ?? "M3";
|
|
50461
|
+
const coverFit = options.coverFit ?? "normal";
|
|
50462
|
+
const segments = options.segments ?? 32;
|
|
50463
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
50464
|
+
if (!sizeData) throw new Error(`datumEnclosureAssembly: unsupported screwSize "${screwSize}"`);
|
|
50465
|
+
const innerWidth = width - wallThickness * 2;
|
|
50466
|
+
const innerDepth = depth - wallThickness * 2;
|
|
50467
|
+
if (innerWidth <= ledgeWidth * 2 + 8 || innerDepth <= ledgeWidth * 2 + 8) {
|
|
50468
|
+
throw new Error("datumEnclosureAssembly: wallThickness and ledgeWidth leave too little internal opening");
|
|
50469
|
+
}
|
|
50470
|
+
if (height <= baseThickness + coverThickness + 4) {
|
|
50471
|
+
throw new Error("datumEnclosureAssembly: height must leave room for internal ribs and standoffs");
|
|
50472
|
+
}
|
|
50473
|
+
const standoffDiameter = requirePositive$6(
|
|
50474
|
+
options.standoffDiameter ?? Math.max(sizeData.head * 1.65, sizeData.close * 2.2),
|
|
50475
|
+
"standoffDiameter"
|
|
50476
|
+
);
|
|
50477
|
+
const minInset = wallThickness + Math.max(ledgeWidth, standoffDiameter / 2 + 1.2);
|
|
50478
|
+
const [insetX, insetY] = resolveBoltInset(options.screwInset, minInset);
|
|
50479
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
50480
|
+
throw new Error("datumEnclosureAssembly: screwInset leaves no room for the standoff datum");
|
|
50481
|
+
}
|
|
50482
|
+
const screwPositions = options.screwPositions ?? [
|
|
50483
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
50484
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
50485
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
50486
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
50487
|
+
];
|
|
50488
|
+
if (screwPositions.length === 0) throw new Error("datumEnclosureAssembly: screwPositions must contain at least one point");
|
|
50489
|
+
for (const [index2, [x2, y2]] of screwPositions.entries()) {
|
|
50490
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
50491
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] must contain finite numbers`);
|
|
50492
|
+
}
|
|
50493
|
+
if (Math.abs(x2) + standoffDiameter / 2 > innerWidth / 2 || Math.abs(y2) + standoffDiameter / 2 > innerDepth / 2) {
|
|
50494
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] does not fit inside the enclosure walls`);
|
|
50495
|
+
}
|
|
50496
|
+
}
|
|
50497
|
+
const ribHeight = requirePositive$6(options.ribHeight ?? Math.min(height * 0.24, Math.max(2.4, baseThickness * 1.4)), "ribHeight");
|
|
50498
|
+
const ribThickness = requirePositive$6(options.ribThickness ?? Math.max(1.2, wallThickness * 0.75), "ribThickness");
|
|
50499
|
+
const portWidth = requirePositive$6(options.portWidth ?? Math.min(innerWidth * 0.28, Math.max(12, width * 0.16)), "portWidth");
|
|
50500
|
+
const portHeight = requirePositive$6(options.portHeight ?? Math.min(height * 0.42, Math.max(5, height * 0.28)), "portHeight");
|
|
50501
|
+
if (portWidth >= innerWidth - ledgeWidth * 2) {
|
|
50502
|
+
throw new Error("datumEnclosureAssembly: portWidth must fit between internal ledges and standoffs");
|
|
50503
|
+
}
|
|
50504
|
+
if (portHeight >= height - baseThickness - 1) {
|
|
50505
|
+
throw new Error("datumEnclosureAssembly: portHeight must leave material above and below the service port");
|
|
50506
|
+
}
|
|
50507
|
+
const screwLength = requirePositive$6(
|
|
50508
|
+
options.screwLength ?? coverThickness + gasketThickness + Math.max(6, height * 0.45),
|
|
50509
|
+
"screwLength"
|
|
50510
|
+
);
|
|
50511
|
+
const coverHole = fastenerHole({
|
|
50512
|
+
size: screwSize,
|
|
50513
|
+
fit: coverFit,
|
|
50514
|
+
depth: coverThickness + 0.6,
|
|
50515
|
+
center: true,
|
|
50516
|
+
segments,
|
|
50517
|
+
counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.35)) }
|
|
50518
|
+
});
|
|
50519
|
+
const standoffTap = fastenerHole({ size: screwSize, fit: "tap", depth: height + 0.8, center: true, segments });
|
|
50520
|
+
const standoffThreadEnvelope = fastenerHole({ size: screwSize, fit: "close", depth: height + 0.8, center: true, segments });
|
|
50521
|
+
const coverClearance = placeCutterAtPositions(coverHole, screwPositions, coverThickness / 2);
|
|
50522
|
+
const standoffTappedPattern = placeCutterAtPositions(standoffTap, screwPositions, height / 2);
|
|
50523
|
+
const standoffThreadEnvelopePattern = placeCutterAtPositions(standoffThreadEnvelope, screwPositions, height / 2);
|
|
50524
|
+
const fuseOverlap = Math.min(0.06, Math.max(0.02, wallThickness * 0.02));
|
|
50525
|
+
const ledgeThickness = Math.min(Math.max(1.1, coverThickness * 0.45), height * 0.2);
|
|
50526
|
+
const sideX = width / 2 - wallThickness / 2;
|
|
50527
|
+
const sideY = depth / 2 - wallThickness / 2;
|
|
50528
|
+
const ledgeZ = height - ledgeThickness;
|
|
50529
|
+
const baseSolids = [
|
|
50530
|
+
box(width, depth, baseThickness),
|
|
50531
|
+
box(wallThickness, depth, height).translate(sideX, 0, 0),
|
|
50532
|
+
box(wallThickness, depth, height).translate(-sideX, 0, 0),
|
|
50533
|
+
box(width, wallThickness, height).translate(0, sideY, 0),
|
|
50534
|
+
box(width, wallThickness, height).translate(0, -sideY, 0),
|
|
50535
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(-width / 2 + wallThickness + ledgeWidth / 2, 0, ledgeZ),
|
|
50536
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(width / 2 - wallThickness - ledgeWidth / 2, 0, ledgeZ),
|
|
50537
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, -depth / 2 + wallThickness + ledgeWidth / 2, ledgeZ),
|
|
50538
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, depth / 2 - wallThickness - ledgeWidth / 2, ledgeZ),
|
|
50539
|
+
box(Math.max(1, innerWidth - standoffDiameter * 1.8), ribThickness, ribHeight + fuseOverlap).translate(
|
|
50540
|
+
0,
|
|
50541
|
+
0,
|
|
50542
|
+
baseThickness - fuseOverlap
|
|
50543
|
+
),
|
|
50544
|
+
box(ribThickness, Math.max(1, innerDepth - standoffDiameter * 1.8), ribHeight + fuseOverlap).translate(
|
|
50545
|
+
0,
|
|
50546
|
+
0,
|
|
50547
|
+
baseThickness - fuseOverlap
|
|
50548
|
+
),
|
|
50549
|
+
...screwPositions.map(
|
|
50550
|
+
([x2, y2]) => cylinder(height - baseThickness + fuseOverlap, standoffDiameter / 2, void 0, segments).translate(
|
|
50551
|
+
x2,
|
|
50552
|
+
y2,
|
|
50553
|
+
baseThickness - fuseOverlap
|
|
50554
|
+
)
|
|
50555
|
+
)
|
|
50556
|
+
];
|
|
50557
|
+
const servicePort = box(portWidth, wallThickness + 1, portHeight).translate(
|
|
50558
|
+
0,
|
|
50559
|
+
-depth / 2 + wallThickness / 2,
|
|
50560
|
+
baseThickness + Math.max(0.8, (height - baseThickness - portHeight) * 0.35)
|
|
50561
|
+
);
|
|
50562
|
+
const base = union(...baseSolids).subtract(standoffThreadEnvelopePattern).subtract(servicePort).color("#475569");
|
|
50563
|
+
const gasketFrameCutter = box(Math.max(1, width - ledgeWidth * 2), Math.max(1, depth - ledgeWidth * 2), gasketThickness + 0.6).translate(
|
|
50564
|
+
0,
|
|
50565
|
+
0,
|
|
50566
|
+
-0.3
|
|
50567
|
+
);
|
|
50568
|
+
const gasket = gasketThickness > 0 ? box(width, depth, gasketThickness).subtract(gasketFrameCutter).subtract(placeCutterAtPositions(coverHole, screwPositions, gasketThickness / 2)).translate(0, 0, height + faceClearance).color("#111827") : null;
|
|
50569
|
+
const coverZ = height + faceClearance + (gasket ? gasketThickness + faceClearance : 0);
|
|
50570
|
+
const cover = box(width, depth, coverThickness).subtract(coverClearance).translate(0, 0, coverZ).color("#334155");
|
|
50571
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
50572
|
+
const screwOriginZ = coverZ + coverThickness;
|
|
50573
|
+
const screws = screwPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
50574
|
+
const parts = [
|
|
50575
|
+
{ name: "datum enclosure base tray with walls ribs standoffs and service port", shape: base },
|
|
50576
|
+
...gasket ? [{ name: "datum enclosure gasket seated on continuous ledge", shape: gasket }] : [],
|
|
50577
|
+
{ name: "datum enclosure cover plate with matched screw pattern", shape: cover },
|
|
50578
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} enclosure screw ${index2 + 1}`, shape }))
|
|
50579
|
+
];
|
|
50580
|
+
return {
|
|
50581
|
+
parts,
|
|
50582
|
+
base,
|
|
50583
|
+
cover,
|
|
50584
|
+
gasket,
|
|
50585
|
+
screws,
|
|
50586
|
+
screwPositions,
|
|
50587
|
+
cutters: {
|
|
50588
|
+
coverClearance,
|
|
50589
|
+
standoffTapped: standoffTappedPattern,
|
|
50590
|
+
standoffThreadEnvelope: standoffThreadEnvelopePattern,
|
|
50591
|
+
servicePort
|
|
50592
|
+
},
|
|
50593
|
+
dims: {
|
|
50594
|
+
width,
|
|
50595
|
+
depth,
|
|
50596
|
+
height,
|
|
50597
|
+
innerWidth,
|
|
50598
|
+
innerDepth,
|
|
50599
|
+
wallThickness,
|
|
50600
|
+
baseThickness,
|
|
50601
|
+
coverThickness,
|
|
50602
|
+
ledgeWidth,
|
|
50603
|
+
gasketThickness,
|
|
50604
|
+
faceClearance,
|
|
50605
|
+
screwSize,
|
|
50606
|
+
screwLength,
|
|
50607
|
+
standoffDiameter,
|
|
50608
|
+
ribHeight,
|
|
50609
|
+
ribThickness,
|
|
50610
|
+
portWidth,
|
|
50611
|
+
portHeight,
|
|
50612
|
+
clearanceDia: sizeData[coverFit],
|
|
50613
|
+
tapDia: sizeData.tap,
|
|
50614
|
+
threadEnvelopeDia: sizeData.close
|
|
50615
|
+
}
|
|
50616
|
+
};
|
|
50617
|
+
}
|
|
50618
|
+
function snapLatchCoverAssembly(options) {
|
|
50619
|
+
const width = requirePositive$6(options.width, "width");
|
|
50620
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
50621
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
50622
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 6, "parentThickness");
|
|
50623
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
50624
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.25, "runningClearance");
|
|
50625
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
50626
|
+
const latchWidth = requirePositive$6(options.latchWidth ?? Math.min(width * 0.22, Math.max(12, width * 0.16)), "latchWidth");
|
|
50627
|
+
const latchThickness = requirePositive$6(options.latchThickness ?? 1.6, "latchThickness");
|
|
50628
|
+
const hookThrow = requirePositive$6(options.hookThrow ?? 3.2, "hookThrow");
|
|
50629
|
+
const hookThickness = requirePositive$6(options.hookThickness ?? 1.6, "hookThickness");
|
|
50630
|
+
const openingWidth = width - ledgeWidth * 2;
|
|
50631
|
+
const openingDepth = depth - ledgeWidth * 2;
|
|
50632
|
+
if (openingWidth <= Math.max(8, latchWidth * 0.8) || openingDepth <= 8) {
|
|
50633
|
+
throw new Error("snapLatchCoverAssembly: ledgeWidth leaves too little service opening under the cover");
|
|
50634
|
+
}
|
|
50635
|
+
if (latchWidth >= openingWidth) {
|
|
50636
|
+
throw new Error("snapLatchCoverAssembly: latchWidth must fit along the receiver opening");
|
|
50637
|
+
}
|
|
50638
|
+
if (latchThickness + runningClearance * 2 >= ledgeWidth) {
|
|
50639
|
+
throw new Error("snapLatchCoverAssembly: latchThickness and clearance must fit inside the receiver ledge");
|
|
50640
|
+
}
|
|
50641
|
+
if (hookThrow + latchThickness / 2 + runningClearance >= ledgeWidth * 1.5) {
|
|
50642
|
+
throw new Error("snapLatchCoverAssembly: hookThrow is too large for the available underside catch land");
|
|
50643
|
+
}
|
|
50644
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
50645
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
50646
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
50647
|
+
const hookClearance = Math.min(0.08, runningClearance * 0.32);
|
|
50648
|
+
const coverMinZ = parentThickness + faceClearance;
|
|
50649
|
+
const stemMinZ = -hookClearance - hookThickness;
|
|
50650
|
+
const stemHeight = coverMinZ + fuseOverlap - stemMinZ;
|
|
50651
|
+
const slotY = openingDepth / 2 + ledgeWidth / 2;
|
|
50652
|
+
const latchWindow = (sign2) => box(latchWidth + runningClearance * 2, latchThickness + runningClearance * 2, parentThickness + 0.8).translate(
|
|
50653
|
+
0,
|
|
50654
|
+
sign2 * slotY,
|
|
50655
|
+
-0.4
|
|
50656
|
+
);
|
|
50657
|
+
const latchWindows = union(latchWindow(1), latchWindow(-1));
|
|
50658
|
+
const serviceOpening = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
50659
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(serviceOpening).subtract(latchWindows).color("#475569");
|
|
50660
|
+
const coverPlate = box(width, depth, coverThickness).translate(0, 0, coverMinZ);
|
|
50661
|
+
const snapHook = (sign2) => {
|
|
50662
|
+
const y2 = sign2 * slotY;
|
|
50663
|
+
const stem = box(latchWidth, latchThickness, stemHeight).translate(0, y2, stemMinZ);
|
|
50664
|
+
const barb = box(latchWidth, latchThickness + hookThrow, hookThickness).translate(
|
|
50665
|
+
0,
|
|
50666
|
+
y2 + sign2 * (hookThrow / 2),
|
|
50667
|
+
stemMinZ
|
|
50668
|
+
);
|
|
50669
|
+
const rootRib = box(latchWidth, Math.max(latchThickness, hookThrow * 0.55), coverThickness * 0.65).translate(
|
|
50670
|
+
0,
|
|
50671
|
+
y2 - sign2 * (ledgeWidth * 0.18),
|
|
50672
|
+
coverMinZ
|
|
50673
|
+
);
|
|
50674
|
+
return union(stem, barb, rootRib);
|
|
50675
|
+
};
|
|
50676
|
+
const cover = union(coverPlate, snapHook(1), snapHook(-1)).color("#111827");
|
|
50677
|
+
const parts = [
|
|
50678
|
+
{ name: "snap cover receiver frame with latch windows and catch lands", shape: parent },
|
|
50679
|
+
{ name: "one-piece snap cover with fused hooks and underside barbs", shape: cover }
|
|
50680
|
+
];
|
|
50681
|
+
return {
|
|
50682
|
+
parts,
|
|
50683
|
+
parent,
|
|
50684
|
+
cover,
|
|
50685
|
+
cutters: {
|
|
50686
|
+
serviceOpening,
|
|
50687
|
+
latchWindows
|
|
50688
|
+
},
|
|
50689
|
+
dims: {
|
|
50690
|
+
width,
|
|
50691
|
+
depth,
|
|
50692
|
+
parentWidth,
|
|
50693
|
+
parentDepth,
|
|
50694
|
+
openingWidth,
|
|
50695
|
+
openingDepth,
|
|
50696
|
+
coverThickness,
|
|
50697
|
+
parentThickness,
|
|
50698
|
+
ledgeWidth,
|
|
50699
|
+
latchWidth,
|
|
50700
|
+
latchThickness,
|
|
50701
|
+
hookThrow,
|
|
50702
|
+
hookThickness,
|
|
50703
|
+
runningClearance,
|
|
50704
|
+
faceClearance
|
|
50705
|
+
}
|
|
50706
|
+
};
|
|
50707
|
+
}
|
|
50708
|
+
function pinnedLeverAssembly(options) {
|
|
50709
|
+
const armLength = requirePositive$6(options.armLength, "armLength");
|
|
50710
|
+
const armWidth = requirePositive$6(options.armWidth ?? 10, "armWidth");
|
|
50711
|
+
const leverThickness = requirePositive$6(options.leverThickness ?? 5, "leverThickness");
|
|
50712
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 5, "pinDiameter");
|
|
50713
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
50714
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
50715
|
+
const hubRadius = requirePositive$6(options.hubRadius ?? Math.max(armWidth * 0.85, pinDiameter * 1.8), "hubRadius");
|
|
50716
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(6, pinDiameter * 1.4), "supportThickness");
|
|
50717
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? hubRadius * 2 + 18, "supportWidth");
|
|
50718
|
+
const supportDepth = requirePositive$6(options.supportDepth ?? Math.max(armWidth + 18, hubRadius * 2 + 10), "supportDepth");
|
|
50719
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(pinDiameter);
|
|
50720
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
50721
|
+
if (!washerDims) throw new Error(`pinnedLeverAssembly: unsupported washerSize "${washerSize}"`);
|
|
50722
|
+
if (washerDims.id <= pinDiameter) {
|
|
50723
|
+
throw new Error(`pinnedLeverAssembly: ${washerSize} washer inner diameter is too small for a ${pinDiameter} mm pin`);
|
|
50724
|
+
}
|
|
50725
|
+
if (hubRadius <= boreDiameter / 2 + Math.max(1, pinDiameter * 0.25)) {
|
|
50726
|
+
throw new Error("pinnedLeverAssembly: hubRadius leaves too little material around the pivot bore");
|
|
50727
|
+
}
|
|
50728
|
+
if (supportWidth <= boreDiameter + 4 || supportDepth <= boreDiameter + 4) {
|
|
50729
|
+
throw new Error("pinnedLeverAssembly: support dimensions leave too little material around the pivot bore");
|
|
50730
|
+
}
|
|
50731
|
+
const segments = options.segments ?? 40;
|
|
50732
|
+
const gripLength = requirePositive$6(options.gripLength ?? Math.min(armLength * 0.32, Math.max(16, armWidth * 2.4)), "gripLength");
|
|
50733
|
+
const gripWidth = requirePositive$6(options.gripWidth ?? armWidth * 1.55, "gripWidth");
|
|
50734
|
+
if (gripLength >= armLength) throw new Error("pinnedLeverAssembly: gripLength must be shorter than armLength");
|
|
50735
|
+
const armOverlap = Math.min(hubRadius * 0.65, armLength * 0.25);
|
|
50736
|
+
const armStartX = hubRadius - armOverlap;
|
|
50737
|
+
const armCenterX = armStartX + armLength / 2;
|
|
50738
|
+
const gripCenterX = armStartX + armLength - gripLength / 2;
|
|
50739
|
+
const runningClearance = 0.03;
|
|
50740
|
+
const lowerWasherZ = supportThickness + runningClearance;
|
|
50741
|
+
const leverZ = lowerWasherZ + washerDims.t + runningClearance;
|
|
50742
|
+
const upperWasherZ = leverZ + leverThickness + runningClearance;
|
|
50743
|
+
const stackHeight = upperWasherZ + washerDims.t;
|
|
50744
|
+
const pinHeadThickness = Math.max(washerDims.t, pinDiameter * 0.35);
|
|
50745
|
+
const pinHeadRadius = Math.max(washerDims.od * 0.42, pinDiameter * 0.8);
|
|
50746
|
+
const supportBore = cylinder(supportThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
50747
|
+
let supportBlank = box(supportWidth, supportDepth, supportThickness);
|
|
50748
|
+
if (options.stopBlock ?? true) {
|
|
50749
|
+
const stopLength = Math.min(armLength * 0.22, Math.max(10, armWidth * 1.4));
|
|
50750
|
+
const stopWidth = Math.max(4, pinDiameter * 0.7);
|
|
50751
|
+
const stopHeight = supportThickness;
|
|
50752
|
+
const stopX = hubRadius + stopLength / 2;
|
|
50753
|
+
const stopY = armWidth / 2 + stopWidth / 2 + runningClearance;
|
|
50754
|
+
supportBlank = union(supportBlank, box(stopLength, stopWidth, stopHeight).translate(stopX, stopY, 0));
|
|
50755
|
+
}
|
|
50756
|
+
const support = supportBlank.subtract(supportBore).color("#475569");
|
|
50757
|
+
const hub = cylinder(leverThickness, hubRadius, void 0, segments);
|
|
50758
|
+
const arm = box(armLength, armWidth, leverThickness).translate(armCenterX, 0, 0);
|
|
50759
|
+
const grip = box(gripLength, gripWidth, leverThickness).translate(gripCenterX, 0, 0);
|
|
50760
|
+
const leverSolids = [hub, arm, grip];
|
|
50761
|
+
if (options.detentBoss ?? true) {
|
|
50762
|
+
const bossRadius = Math.min(armWidth * 0.42, hubRadius * 0.42);
|
|
50763
|
+
const bossX = hubRadius + Math.min(armLength * 0.22, armWidth * 2);
|
|
50764
|
+
const bossY = -armWidth / 2 - bossRadius * 0.45;
|
|
50765
|
+
leverSolids.push(cylinder(leverThickness, bossRadius, void 0, segments).translate(bossX, bossY, 0));
|
|
50766
|
+
}
|
|
50767
|
+
const leverBore = cylinder(leverThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
50768
|
+
const lever = union(...leverSolids).subtract(leverBore).translate(0, 0, leverZ).color("#7f1d1d");
|
|
50769
|
+
const lowerWasher = washer(washerSize, { segments }).translate(0, 0, lowerWasherZ).color("#94a3b8");
|
|
50770
|
+
const upperWasher = washer(washerSize, { segments }).translate(0, 0, upperWasherZ).color("#94a3b8");
|
|
50771
|
+
const shaft = cylinder(stackHeight, pinDiameter / 2, void 0, segments);
|
|
50772
|
+
const lowerRetainer = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, -pinHeadThickness - runningClearance);
|
|
50773
|
+
const upperHead = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, stackHeight + runningClearance);
|
|
50774
|
+
const pin = union(shaft, lowerRetainer, upperHead).color("#cbd5e1");
|
|
50775
|
+
const pivotBore = cylinder(stackHeight + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
50776
|
+
const parts = [
|
|
50777
|
+
{ name: "pivot support block with bearing bore and low stop land", shape: support },
|
|
50778
|
+
{ name: "lower thrust washer under pinned lever", shape: lowerWasher },
|
|
50779
|
+
{ name: "fused pinned lever with hub arm grip and detent boss", shape: lever },
|
|
50780
|
+
{ name: "upper thrust washer over pinned lever", shape: upperWasher },
|
|
50781
|
+
{ name: "retained pivot pin through lever stack", shape: pin }
|
|
50782
|
+
];
|
|
50783
|
+
return {
|
|
50784
|
+
parts,
|
|
50785
|
+
support,
|
|
50786
|
+
lever,
|
|
50787
|
+
pin,
|
|
50788
|
+
washers: {
|
|
50789
|
+
lower: lowerWasher,
|
|
50790
|
+
upper: upperWasher
|
|
50791
|
+
},
|
|
50792
|
+
cutters: {
|
|
50793
|
+
pivotBore
|
|
50794
|
+
},
|
|
50795
|
+
dims: {
|
|
50796
|
+
armLength,
|
|
50797
|
+
armWidth,
|
|
50798
|
+
leverThickness,
|
|
50799
|
+
hubRadius,
|
|
50800
|
+
pinDiameter,
|
|
50801
|
+
boreDiameter,
|
|
50802
|
+
supportWidth,
|
|
50803
|
+
supportDepth,
|
|
50804
|
+
supportThickness,
|
|
50805
|
+
washerSize,
|
|
50806
|
+
washerThickness: washerDims.t,
|
|
50807
|
+
stackHeight
|
|
50808
|
+
}
|
|
50809
|
+
};
|
|
50810
|
+
}
|
|
50811
|
+
function retainedShaftAssembly(options) {
|
|
50812
|
+
const supportSpacing = requirePositive$6(options.supportSpacing, "supportSpacing");
|
|
50813
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? 8, "shaftDiameter");
|
|
50814
|
+
const boreClearance = requireNonNegative(options.boreClearance ?? 0.35, "boreClearance");
|
|
50815
|
+
const boreDiameter = shaftDiameter + boreClearance;
|
|
50816
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(5, shaftDiameter * 0.75), "supportThickness");
|
|
50817
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(shaftDiameter);
|
|
50818
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
50819
|
+
if (!washerDims) throw new Error(`retainedShaftAssembly: unsupported washerSize "${washerSize}"`);
|
|
50820
|
+
if (washerDims.id <= shaftDiameter) {
|
|
50821
|
+
throw new Error(`retainedShaftAssembly: ${washerSize} washer inner diameter is too small for a ${shaftDiameter} mm shaft`);
|
|
50822
|
+
}
|
|
50823
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? shaftDiameter * 3, "knobDiameter");
|
|
50824
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(8, shaftDiameter), "knobThickness");
|
|
50825
|
+
const retainerThickness = requirePositive$6(
|
|
50826
|
+
options.retainerThickness ?? Math.max(washerDims.t, shaftDiameter * 0.35),
|
|
50827
|
+
"retainerThickness"
|
|
50828
|
+
);
|
|
50829
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
50830
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? Math.max(28, knobDiameter * 1.25), "supportWidth");
|
|
50831
|
+
const supportHeight = requirePositive$6(options.supportHeight ?? Math.max(34, knobDiameter * 1.45), "supportHeight");
|
|
50832
|
+
const segments = options.segments ?? 40;
|
|
50833
|
+
if (supportSpacing <= supportThickness) {
|
|
50834
|
+
throw new Error("retainedShaftAssembly: supportSpacing must leave a gap between support cheeks");
|
|
50835
|
+
}
|
|
50836
|
+
if (supportWidth <= boreDiameter + 4 || supportHeight <= boreDiameter + 4) {
|
|
50837
|
+
throw new Error("retainedShaftAssembly: support dimensions leave too little material around the shaft bore");
|
|
50838
|
+
}
|
|
50839
|
+
const leftSupportX = -supportSpacing / 2;
|
|
50840
|
+
const rightSupportX = supportSpacing / 2;
|
|
50841
|
+
const leftOuterFaceX = leftSupportX - supportThickness / 2;
|
|
50842
|
+
const rightOuterFaceX = rightSupportX + supportThickness / 2;
|
|
50843
|
+
const leftWasherX = leftOuterFaceX - runningClearance - washerDims.t / 2;
|
|
50844
|
+
const rightWasherX = rightOuterFaceX + runningClearance + washerDims.t / 2;
|
|
50845
|
+
const leftKnobX = leftOuterFaceX - runningClearance * 2 - washerDims.t - knobThickness / 2;
|
|
50846
|
+
const rightKnobX = rightOuterFaceX + runningClearance * 2 + washerDims.t + knobThickness / 2;
|
|
50847
|
+
const leftStackOuterX = leftKnobX - knobThickness / 2;
|
|
50848
|
+
const rightStackOuterX = rightKnobX + knobThickness / 2;
|
|
50849
|
+
const minimumShaftLength = rightStackOuterX - leftStackOuterX + retainerThickness * 2 + runningClearance * 2;
|
|
50850
|
+
const shaftLength = requirePositive$6(options.shaftLength ?? minimumShaftLength, "shaftLength");
|
|
50851
|
+
if (shaftLength < minimumShaftLength) {
|
|
50852
|
+
throw new Error("retainedShaftAssembly: shaftLength is too short to retain both supports, washers, and knobs");
|
|
50853
|
+
}
|
|
50854
|
+
const supportBore = cylinderAlongX(supportThickness + 1, boreDiameter / 2, 0, segments);
|
|
50855
|
+
const makeSupport = (x2) => box(supportThickness, supportWidth, supportHeight).translate(x2, 0, -supportHeight / 2).subtract(supportBore.translate(x2, 0, 0)).color("#334155");
|
|
50856
|
+
const knobBore = cylinder(knobThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
50857
|
+
const makeKnob = (x2) => cylinder(knobThickness, knobDiameter / 2, void 0, 18).subtract(knobBore).pointAlong([1, 0, 0]).translate(x2 - knobThickness / 2, 0, 0).color("#111827");
|
|
50858
|
+
const retainerRadius = Math.max(shaftDiameter * 0.85, knobDiameter * 0.36);
|
|
50859
|
+
const shaftCore = cylinderAlongX(shaftLength, shaftDiameter / 2, 0, segments);
|
|
50860
|
+
const leftRetainer = cylinderAlongX(retainerThickness, retainerRadius, -shaftLength / 2 + retainerThickness / 2, segments);
|
|
50861
|
+
const rightRetainer = cylinderAlongX(retainerThickness, retainerRadius, shaftLength / 2 - retainerThickness / 2, segments);
|
|
50862
|
+
const shaft = union(shaftCore, leftRetainer, rightRetainer).color("#cbd5e1");
|
|
50863
|
+
const leftSupport = makeSupport(leftSupportX);
|
|
50864
|
+
const rightSupport = makeSupport(rightSupportX);
|
|
50865
|
+
const leftWasher = washerAlongX(washerSize, leftWasherX, segments).color("#94a3b8");
|
|
50866
|
+
const rightWasher = washerAlongX(washerSize, rightWasherX, segments).color("#94a3b8");
|
|
50867
|
+
const leftKnob = makeKnob(leftKnobX);
|
|
50868
|
+
const rightKnob = makeKnob(rightKnobX);
|
|
50869
|
+
const shaftBore = cylinderAlongX(supportThickness + knobThickness + 2, boreDiameter / 2, 0, segments);
|
|
50870
|
+
const parts = [
|
|
50871
|
+
{ name: "left bored support cheek for retained shaft", shape: leftSupport },
|
|
50872
|
+
{ name: "right bored support cheek for retained shaft", shape: rightSupport },
|
|
50873
|
+
{ name: "retained through shaft with end heads", shape: shaft },
|
|
50874
|
+
{ name: `left ${washerSize} thrust washer on shaft`, shape: leftWasher },
|
|
50875
|
+
{ name: `right ${washerSize} thrust washer on shaft`, shape: rightWasher },
|
|
50876
|
+
{ name: "left retained hand knob with shaft bore", shape: leftKnob },
|
|
50877
|
+
{ name: "right retained hand knob with shaft bore", shape: rightKnob }
|
|
50878
|
+
];
|
|
50879
|
+
return {
|
|
50880
|
+
parts,
|
|
50881
|
+
supports: {
|
|
50882
|
+
left: leftSupport,
|
|
50883
|
+
right: rightSupport
|
|
50884
|
+
},
|
|
50885
|
+
shaft,
|
|
50886
|
+
washers: {
|
|
50887
|
+
left: leftWasher,
|
|
50888
|
+
right: rightWasher
|
|
50889
|
+
},
|
|
50890
|
+
knobs: {
|
|
50891
|
+
left: leftKnob,
|
|
50892
|
+
right: rightKnob
|
|
50893
|
+
},
|
|
50894
|
+
cutters: {
|
|
50895
|
+
shaftBore
|
|
50896
|
+
},
|
|
50897
|
+
dims: {
|
|
50898
|
+
supportSpacing,
|
|
50899
|
+
supportThickness,
|
|
50900
|
+
supportWidth,
|
|
50901
|
+
supportHeight,
|
|
50902
|
+
shaftDiameter,
|
|
50903
|
+
shaftLength,
|
|
50904
|
+
boreDiameter,
|
|
50905
|
+
washerSize,
|
|
50906
|
+
washerThickness: washerDims.t,
|
|
50907
|
+
knobDiameter,
|
|
50908
|
+
knobThickness,
|
|
50909
|
+
retainerThickness,
|
|
50910
|
+
runningClearance
|
|
50911
|
+
}
|
|
50912
|
+
};
|
|
50913
|
+
}
|
|
50914
|
+
function capturedLinearSlide(options) {
|
|
50915
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
50916
|
+
const railWidth = requirePositive$6(options.railWidth ?? 38, "railWidth");
|
|
50917
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 2.4, "baseThickness");
|
|
50918
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2, "wallThickness");
|
|
50919
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 9, "wallHeight");
|
|
50920
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
50921
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 1.8, "lipThickness");
|
|
50922
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
50923
|
+
const endStopLength = requirePositive$6(options.endStopLength ?? 6, "endStopLength");
|
|
50924
|
+
const carriageLength = requirePositive$6(options.carriageLength ?? length4 * 0.32, "carriageLength");
|
|
50925
|
+
const innerWidth = railWidth - wallThickness * 2;
|
|
50926
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
50927
|
+
if (innerWidth <= 0) throw new Error("capturedLinearSlide: wallThickness leaves no inner rail width");
|
|
50928
|
+
if (throatWidth <= 0) throw new Error("capturedLinearSlide: lipWidth closes the rail throat");
|
|
50929
|
+
const carriageWidth = requirePositive$6(options.carriageWidth ?? innerWidth - runningClearance * 2, "carriageWidth");
|
|
50930
|
+
const carriageThickness = requirePositive$6(options.carriageThickness ?? 4, "carriageThickness");
|
|
50931
|
+
if (carriageWidth >= innerWidth - runningClearance) {
|
|
50932
|
+
throw new Error("capturedLinearSlide: carriageWidth leaves too little side clearance inside the rail");
|
|
50933
|
+
}
|
|
50934
|
+
if (carriageWidth <= throatWidth + runningClearance) {
|
|
50935
|
+
throw new Error("capturedLinearSlide: carriageWidth must be wider than the lip throat so the rail actually captures it");
|
|
50936
|
+
}
|
|
50937
|
+
if (carriageThickness + runningClearance * 2 >= wallHeight) {
|
|
50938
|
+
throw new Error("capturedLinearSlide: carriage is too tall to clear the return lips");
|
|
50939
|
+
}
|
|
50940
|
+
const maxTravel = length4 - endStopLength * 2 - carriageLength;
|
|
50941
|
+
if (maxTravel <= 0) {
|
|
50942
|
+
throw new Error("capturedLinearSlide: rail length, end stops, and carriage length leave no travel");
|
|
50943
|
+
}
|
|
50944
|
+
const travel = options.travel ?? maxTravel / 2;
|
|
50945
|
+
if (!Number.isFinite(travel) || travel < 0 || travel > maxTravel) {
|
|
50946
|
+
throw new Error(`capturedLinearSlide: travel must be between 0 and ${maxTravel}`);
|
|
50947
|
+
}
|
|
50948
|
+
const carriageCenterX = -maxTravel / 2 + travel;
|
|
50949
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
50950
|
+
const sideY = railWidth / 2 - wallThickness / 2;
|
|
50951
|
+
const lipY = railWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
50952
|
+
const stopZ = baseThickness - fuseOverlap;
|
|
50953
|
+
const rail2 = union(
|
|
50954
|
+
box(length4, railWidth, baseThickness),
|
|
50955
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
50956
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
50957
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
50958
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
50959
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(-length4 / 2 + endStopLength / 2, 0, stopZ),
|
|
50960
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(length4 / 2 - endStopLength / 2, 0, stopZ)
|
|
50961
|
+
).color("#475569");
|
|
50962
|
+
const carriage = union(
|
|
50963
|
+
box(carriageLength, carriageWidth, carriageThickness),
|
|
50964
|
+
box(carriageLength * 0.78, throatWidth - runningClearance * 2, Math.max(1, carriageThickness * 0.38)).translate(
|
|
50965
|
+
0,
|
|
50966
|
+
0,
|
|
50967
|
+
carriageThickness
|
|
50968
|
+
)
|
|
50969
|
+
).translate(carriageCenterX, 0, baseThickness + runningClearance).color("#111827");
|
|
50970
|
+
const parts = [
|
|
50971
|
+
{ name: "captured linear rail with return lips and end stops", shape: rail2 },
|
|
50972
|
+
{ name: "sliding carriage captured under rail lips", shape: carriage }
|
|
50973
|
+
];
|
|
50974
|
+
return {
|
|
50975
|
+
parts,
|
|
50976
|
+
rail: rail2,
|
|
50977
|
+
carriage,
|
|
50978
|
+
dims: {
|
|
50979
|
+
length: length4,
|
|
50980
|
+
railWidth,
|
|
50981
|
+
innerWidth,
|
|
50982
|
+
throatWidth,
|
|
50983
|
+
baseThickness,
|
|
50984
|
+
wallThickness,
|
|
50985
|
+
wallHeight,
|
|
50986
|
+
lipWidth,
|
|
50987
|
+
lipThickness,
|
|
50988
|
+
carriageLength,
|
|
50989
|
+
carriageWidth,
|
|
50990
|
+
carriageThickness,
|
|
50991
|
+
endStopLength,
|
|
50992
|
+
runningClearance,
|
|
50993
|
+
maxTravel,
|
|
50994
|
+
travel,
|
|
50995
|
+
carriageCenterX
|
|
50996
|
+
}
|
|
50997
|
+
};
|
|
50998
|
+
}
|
|
50999
|
+
function capturedCartridgeGuideAssembly(options) {
|
|
51000
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51001
|
+
const guideWidth = requirePositive$6(options.guideWidth ?? 42, "guideWidth");
|
|
51002
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 3, "baseThickness");
|
|
51003
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.5, "wallThickness");
|
|
51004
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 12, "wallHeight");
|
|
51005
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
51006
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 2, "lipThickness");
|
|
51007
|
+
const rearStopLength = requirePositive$6(options.rearStopLength ?? 7, "rearStopLength");
|
|
51008
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51009
|
+
const cartridgeLength = requirePositive$6(options.cartridgeLength ?? length4 * 0.58, "cartridgeLength");
|
|
51010
|
+
const cartridgeHeight = requirePositive$6(options.cartridgeHeight ?? 10, "cartridgeHeight");
|
|
51011
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? 3, "flangeThickness");
|
|
51012
|
+
const pullTabLength = requirePositive$6(options.pullTabLength ?? 10, "pullTabLength");
|
|
51013
|
+
const innerWidth = guideWidth - wallThickness * 2;
|
|
51014
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
51015
|
+
if (innerWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: wallThickness leaves no inner guide width");
|
|
51016
|
+
if (throatWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: lipWidth closes the guide throat");
|
|
51017
|
+
if (wallHeight <= lipThickness + flangeThickness + runningClearance * 2) {
|
|
51018
|
+
throw new Error("capturedCartridgeGuideAssembly: wallHeight leaves too little vertical capture clearance");
|
|
51019
|
+
}
|
|
51020
|
+
const cartridgeWidth = requirePositive$6(options.cartridgeWidth ?? innerWidth - runningClearance * 2, "cartridgeWidth");
|
|
51021
|
+
const cartridgeBodyWidth = throatWidth - runningClearance * 2;
|
|
51022
|
+
if (cartridgeBodyWidth <= 0) {
|
|
51023
|
+
throw new Error("capturedCartridgeGuideAssembly: throatWidth and runningClearance leave no cartridge body width");
|
|
51024
|
+
}
|
|
51025
|
+
if (cartridgeWidth >= innerWidth - runningClearance) {
|
|
51026
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridgeWidth leaves too little side clearance inside the guide");
|
|
51027
|
+
}
|
|
51028
|
+
if (cartridgeWidth <= throatWidth + runningClearance) {
|
|
51029
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridge flange must be wider than the guide throat so the cartridge is captured");
|
|
51030
|
+
}
|
|
51031
|
+
const maxInsertion = length4 - rearStopLength - cartridgeLength;
|
|
51032
|
+
if (maxInsertion <= 0) {
|
|
51033
|
+
throw new Error("capturedCartridgeGuideAssembly: length, rearStopLength, and cartridgeLength leave no insertion travel");
|
|
51034
|
+
}
|
|
51035
|
+
const insertion = options.insertion ?? maxInsertion * 0.4;
|
|
51036
|
+
if (!Number.isFinite(insertion) || insertion < 0 || insertion > maxInsertion) {
|
|
51037
|
+
throw new Error(`capturedCartridgeGuideAssembly: insertion must be between 0 and ${maxInsertion}`);
|
|
51038
|
+
}
|
|
51039
|
+
const cartridgeCenterX = -length4 / 2 + cartridgeLength / 2 + insertion;
|
|
51040
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
51041
|
+
const sideY = guideWidth / 2 - wallThickness / 2;
|
|
51042
|
+
const lipY = guideWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
51043
|
+
const guide = union(
|
|
51044
|
+
box(length4, guideWidth, baseThickness),
|
|
51045
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
51046
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
51047
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51048
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51049
|
+
box(rearStopLength, throatWidth, Math.max(flangeThickness + runningClearance, 4)).translate(
|
|
51050
|
+
length4 / 2 - rearStopLength / 2,
|
|
51051
|
+
0,
|
|
51052
|
+
baseThickness - fuseOverlap
|
|
51053
|
+
)
|
|
51054
|
+
).color("#475569");
|
|
51055
|
+
const flangeZ = baseThickness + runningClearance;
|
|
51056
|
+
const bodyHeight = Math.max(1, cartridgeHeight - flangeThickness);
|
|
51057
|
+
const bodyZ = flangeZ + flangeThickness;
|
|
51058
|
+
const tabOverlap = Math.min(0.6, pullTabLength * 0.15);
|
|
51059
|
+
const pullTabX = cartridgeCenterX - cartridgeLength / 2 - pullTabLength / 2 + tabOverlap;
|
|
51060
|
+
const pullTabWidth = Math.max(cartridgeBodyWidth * 0.55, 12);
|
|
51061
|
+
const cartridge = union(
|
|
51062
|
+
box(cartridgeLength, cartridgeWidth, flangeThickness).translate(cartridgeCenterX, 0, flangeZ),
|
|
51063
|
+
box(cartridgeLength * 0.88, cartridgeBodyWidth, bodyHeight).translate(cartridgeCenterX, 0, bodyZ),
|
|
51064
|
+
box(pullTabLength, pullTabWidth, Math.max(flangeThickness, 3)).translate(pullTabX, 0, flangeZ)
|
|
51065
|
+
).color("#111827");
|
|
51066
|
+
const parts = [
|
|
51067
|
+
{ name: "captured cartridge guide with return lips and rear stop", shape: guide },
|
|
51068
|
+
{ name: "removable cartridge with captured flange and pull tab", shape: cartridge }
|
|
51069
|
+
];
|
|
51070
|
+
return {
|
|
51071
|
+
parts,
|
|
51072
|
+
guide,
|
|
51073
|
+
cartridge,
|
|
51074
|
+
dims: {
|
|
51075
|
+
length: length4,
|
|
51076
|
+
guideWidth,
|
|
51077
|
+
innerWidth,
|
|
51078
|
+
throatWidth,
|
|
51079
|
+
baseThickness,
|
|
51080
|
+
wallThickness,
|
|
51081
|
+
wallHeight,
|
|
51082
|
+
lipWidth,
|
|
51083
|
+
lipThickness,
|
|
51084
|
+
rearStopLength,
|
|
51085
|
+
cartridgeLength,
|
|
51086
|
+
cartridgeWidth,
|
|
51087
|
+
cartridgeBodyWidth,
|
|
51088
|
+
cartridgeHeight,
|
|
51089
|
+
flangeThickness,
|
|
51090
|
+
pullTabLength,
|
|
51091
|
+
runningClearance,
|
|
51092
|
+
maxInsertion,
|
|
51093
|
+
insertion,
|
|
51094
|
+
cartridgeCenterX
|
|
51095
|
+
}
|
|
51096
|
+
};
|
|
51097
|
+
}
|
|
51098
|
+
function livingHingeCoverAssembly(options) {
|
|
51099
|
+
const width = requirePositive$6(options.width, "width");
|
|
51100
|
+
const coverDepth = requirePositive$6(options.coverDepth ?? 42, "coverDepth");
|
|
51101
|
+
const fixedLeafDepth = requirePositive$6(options.fixedLeafDepth ?? 18, "fixedLeafDepth");
|
|
51102
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 2, "leafThickness");
|
|
51103
|
+
const hingeWebWidth = requirePositive$6(options.hingeWebWidth ?? 3.2, "hingeWebWidth");
|
|
51104
|
+
const hingeWebThickness = requirePositive$6(options.hingeWebThickness ?? 0.45, "hingeWebThickness");
|
|
51105
|
+
const pullLipDepth = requirePositive$6(options.pullLipDepth ?? 5, "pullLipDepth");
|
|
51106
|
+
const snapBarbWidth = requirePositive$6(options.snapBarbWidth ?? width * 0.35, "snapBarbWidth");
|
|
51107
|
+
const snapBarbDepth = requirePositive$6(options.snapBarbDepth ?? 2.4, "snapBarbDepth");
|
|
51108
|
+
const snapBarbHeight = requirePositive$6(options.snapBarbHeight ?? 1.4, "snapBarbHeight");
|
|
51109
|
+
const catchLandDepth = requirePositive$6(options.catchLandDepth ?? 2.4, "catchLandDepth");
|
|
51110
|
+
if (hingeWebThickness >= leafThickness * 0.55) {
|
|
51111
|
+
throw new Error("livingHingeCoverAssembly: hingeWebThickness must be much thinner than the rigid leaves");
|
|
51112
|
+
}
|
|
51113
|
+
if (hingeWebWidth >= Math.min(coverDepth, fixedLeafDepth) * 0.45) {
|
|
51114
|
+
throw new Error("livingHingeCoverAssembly: hingeWebWidth is too wide for the selected leaves");
|
|
51115
|
+
}
|
|
51116
|
+
if (snapBarbWidth >= width - 2) {
|
|
51117
|
+
throw new Error("livingHingeCoverAssembly: snapBarbWidth must leave side material on the cover leaf");
|
|
51118
|
+
}
|
|
51119
|
+
const fuseOverlap = Math.min(0.04, hingeWebWidth * 0.02);
|
|
51120
|
+
const fixedCenterY = -hingeWebWidth / 2 - fixedLeafDepth / 2 + fuseOverlap / 2;
|
|
51121
|
+
const coverCenterY = hingeWebWidth / 2 + coverDepth / 2 - fuseOverlap / 2;
|
|
51122
|
+
const fixedLeaf = box(width, fixedLeafDepth + fuseOverlap, leafThickness).translate(0, fixedCenterY, 0);
|
|
51123
|
+
const movingLeaf = box(width, coverDepth + fuseOverlap, leafThickness).translate(0, coverCenterY, 0);
|
|
51124
|
+
const hingeWeb = box(width, hingeWebWidth + fuseOverlap * 2, hingeWebThickness).translate(0, 0, 0);
|
|
51125
|
+
const pullLip = box(width * 0.92, pullLipDepth, leafThickness).translate(0, coverCenterY + coverDepth / 2 + pullLipDepth / 2 - fuseOverlap, 0);
|
|
51126
|
+
const snapBarb = box(snapBarbWidth, snapBarbDepth, snapBarbHeight).translate(
|
|
51127
|
+
0,
|
|
51128
|
+
coverCenterY + coverDepth / 2 - snapBarbDepth / 2,
|
|
51129
|
+
leafThickness
|
|
51130
|
+
);
|
|
51131
|
+
const catchLand = box(width * 0.55, catchLandDepth, Math.max(0.8, leafThickness * 0.45)).translate(
|
|
51132
|
+
0,
|
|
51133
|
+
fixedCenterY - fixedLeafDepth / 2 + catchLandDepth / 2,
|
|
51134
|
+
leafThickness
|
|
51135
|
+
);
|
|
51136
|
+
const cover = union(fixedLeaf, movingLeaf, hingeWeb, pullLip, snapBarb, catchLand).color("#0f766e");
|
|
51137
|
+
const overallDepth = fixedLeafDepth + hingeWebWidth + coverDepth + pullLipDepth;
|
|
51138
|
+
const flexRatio = leafThickness / hingeWebThickness;
|
|
51139
|
+
return {
|
|
51140
|
+
parts: [{ name: "one-piece molded living hinge cover with snap barb", shape: cover }],
|
|
51141
|
+
cover,
|
|
51142
|
+
fixedLeaf,
|
|
51143
|
+
movingLeaf,
|
|
51144
|
+
hingeWeb,
|
|
51145
|
+
snapBarb,
|
|
51146
|
+
catchLand,
|
|
51147
|
+
dims: {
|
|
51148
|
+
width,
|
|
51149
|
+
coverDepth,
|
|
51150
|
+
fixedLeafDepth,
|
|
51151
|
+
leafThickness,
|
|
51152
|
+
hingeWebWidth,
|
|
51153
|
+
hingeWebThickness,
|
|
51154
|
+
pullLipDepth,
|
|
51155
|
+
snapBarbWidth,
|
|
51156
|
+
snapBarbDepth,
|
|
51157
|
+
snapBarbHeight,
|
|
51158
|
+
catchLandDepth,
|
|
51159
|
+
flexRatio,
|
|
51160
|
+
overallDepth
|
|
51161
|
+
}
|
|
51162
|
+
};
|
|
51163
|
+
}
|
|
51164
|
+
function knuckledHingeAssembly(options) {
|
|
51165
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51166
|
+
const leafLength = requirePositive$6(options.leafLength ?? 36, "leafLength");
|
|
51167
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 1.6, "leafThickness");
|
|
51168
|
+
const barrelOuterRadius = requirePositive$6(options.barrelOuterRadius ?? 3, "barrelOuterRadius");
|
|
51169
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 2, "pinDiameter");
|
|
51170
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
51171
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51172
|
+
const knuckleGap = requireNonNegative(options.knuckleGap ?? 0.45, "knuckleGap");
|
|
51173
|
+
const openAngleDeg = Number.isFinite(options.openAngleDeg ?? 35) ? options.openAngleDeg ?? 35 : 35;
|
|
51174
|
+
const retainerThickness = requirePositive$6(
|
|
51175
|
+
options.retainerThickness ?? Math.max(leafThickness, pinDiameter * 0.7),
|
|
51176
|
+
"retainerThickness"
|
|
51177
|
+
);
|
|
51178
|
+
const segments = options.segments ?? 36;
|
|
51179
|
+
const knuckleCount = options.knuckleCount ?? 5;
|
|
51180
|
+
if (!Number.isInteger(knuckleCount) || knuckleCount < 3 || knuckleCount % 2 === 0) {
|
|
51181
|
+
throw new Error("knuckledHingeAssembly: knuckleCount must be an odd integer >= 3");
|
|
51182
|
+
}
|
|
51183
|
+
if (barrelOuterRadius <= boreDiameter / 2 + Math.max(0.35, pinDiameter * 0.18)) {
|
|
51184
|
+
throw new Error("knuckledHingeAssembly: barrelOuterRadius leaves too little wall around the pin bore");
|
|
51185
|
+
}
|
|
51186
|
+
const knuckleLength = (length4 - knuckleGap * (knuckleCount - 1)) / knuckleCount;
|
|
51187
|
+
if (knuckleLength <= pinDiameter * 1.4) {
|
|
51188
|
+
throw new Error("knuckledHingeAssembly: length, knuckleCount, and knuckleGap make knuckles too short");
|
|
51189
|
+
}
|
|
51190
|
+
const leafRootClearance = Math.max(0.12, Math.min(knuckleGap * 0.35, 0.35));
|
|
51191
|
+
const barrelLeafOverlap = Math.min(barrelOuterRadius * 0.18, leafThickness * 0.35);
|
|
51192
|
+
const bridgeDepth = leafRootClearance + barrelLeafOverlap + 0.2;
|
|
51193
|
+
const fixedLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51194
|
+
0,
|
|
51195
|
+
barrelOuterRadius + leafRootClearance + leafLength / 2,
|
|
51196
|
+
-leafThickness / 2
|
|
51197
|
+
);
|
|
51198
|
+
const movingLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51199
|
+
0,
|
|
51200
|
+
-barrelOuterRadius - leafRootClearance - leafLength / 2,
|
|
51201
|
+
-leafThickness / 2
|
|
51202
|
+
);
|
|
51203
|
+
const fixedKnuckles = [];
|
|
51204
|
+
const movingKnuckles = [];
|
|
51205
|
+
const fixedBridges = [];
|
|
51206
|
+
const movingBridges = [];
|
|
51207
|
+
for (let index2 = 0; index2 < knuckleCount; index2 += 1) {
|
|
51208
|
+
const xStart = -length4 / 2 + index2 * (knuckleLength + knuckleGap);
|
|
51209
|
+
const xCenter = xStart + knuckleLength / 2;
|
|
51210
|
+
const knuckle = tubeAlongX(knuckleLength, barrelOuterRadius, boreDiameter / 2, xCenter, segments);
|
|
51211
|
+
if (index2 % 2 === 0) {
|
|
51212
|
+
fixedKnuckles.push(knuckle);
|
|
51213
|
+
fixedBridges.push(
|
|
51214
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
51215
|
+
xCenter,
|
|
51216
|
+
barrelOuterRadius - barrelLeafOverlap + bridgeDepth / 2,
|
|
51217
|
+
-leafThickness / 2
|
|
51218
|
+
)
|
|
51219
|
+
);
|
|
51220
|
+
} else {
|
|
51221
|
+
movingKnuckles.push(knuckle);
|
|
51222
|
+
movingBridges.push(
|
|
51223
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
51224
|
+
xCenter,
|
|
51225
|
+
-barrelOuterRadius + barrelLeafOverlap - bridgeDepth / 2,
|
|
51226
|
+
-leafThickness / 2
|
|
51227
|
+
)
|
|
51228
|
+
);
|
|
51229
|
+
}
|
|
51230
|
+
}
|
|
51231
|
+
const fixedLeaf = union(fixedLeafPlate, ...fixedKnuckles, ...fixedBridges).color("#475569");
|
|
51232
|
+
const movingLeaf = union(movingLeafPlate, ...movingKnuckles, ...movingBridges).rotateX(openAngleDeg).color("#111827");
|
|
51233
|
+
const pinCore = cylinderAlongX(length4 + retainerThickness * 2, pinDiameter / 2, 0, segments);
|
|
51234
|
+
const retainerRadius = Math.max(barrelOuterRadius * 0.85, pinDiameter);
|
|
51235
|
+
const leftHead = cylinderAlongX(retainerThickness, retainerRadius, -length4 / 2 - retainerThickness / 2, segments);
|
|
51236
|
+
const rightHead = cylinderAlongX(retainerThickness, retainerRadius, length4 / 2 + retainerThickness / 2, segments);
|
|
51237
|
+
const pin = union(pinCore, leftHead, rightHead).color("#cbd5e1");
|
|
51238
|
+
const pinBore = cylinderAlongX(length4 + retainerThickness * 2, boreDiameter / 2, 0, segments);
|
|
51239
|
+
const parts = [
|
|
51240
|
+
{ name: "fixed hinge leaf with alternating knuckles", shape: fixedLeaf },
|
|
51241
|
+
{ name: "moving hinge leaf with alternating knuckles", shape: movingLeaf },
|
|
51242
|
+
{ name: "retained hinge pin through knuckle stack", shape: pin }
|
|
51243
|
+
];
|
|
51244
|
+
return {
|
|
51245
|
+
parts,
|
|
51246
|
+
fixedLeaf,
|
|
51247
|
+
movingLeaf,
|
|
51248
|
+
pin,
|
|
51249
|
+
cutters: {
|
|
51250
|
+
pinBore
|
|
51251
|
+
},
|
|
51252
|
+
dims: {
|
|
51253
|
+
length: length4,
|
|
51254
|
+
leafLength,
|
|
51255
|
+
leafThickness,
|
|
51256
|
+
barrelOuterRadius,
|
|
51257
|
+
pinDiameter,
|
|
51258
|
+
boreDiameter,
|
|
51259
|
+
knuckleGap,
|
|
51260
|
+
knuckleCount,
|
|
51261
|
+
knuckleLength,
|
|
51262
|
+
openAngleDeg,
|
|
51263
|
+
retainerThickness
|
|
51264
|
+
}
|
|
51265
|
+
};
|
|
51266
|
+
}
|
|
51267
|
+
function clevisPinJointAssembly(options = {}) {
|
|
51268
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 4, "pinDiameter");
|
|
51269
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.3, "pinClearance");
|
|
51270
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51271
|
+
const linkThickness = requirePositive$6(options.linkThickness ?? Math.max(5, pinDiameter * 1.5), "linkThickness");
|
|
51272
|
+
const earThickness = requirePositive$6(options.earThickness ?? Math.max(3.5, pinDiameter), "earThickness");
|
|
51273
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.25, "runningClearance");
|
|
51274
|
+
const linkArmWidth = requirePositive$6(options.linkArmWidth ?? pinDiameter * 2.4, "linkArmWidth");
|
|
51275
|
+
const eyeOuterRadius = requirePositive$6(
|
|
51276
|
+
options.eyeOuterRadius ?? Math.max(pinDiameter * 1.8, linkArmWidth / 2 + 1.4),
|
|
51277
|
+
"eyeOuterRadius"
|
|
51278
|
+
);
|
|
51279
|
+
const earLength = requirePositive$6(options.earLength ?? Math.max(eyeOuterRadius * 2.55, pinDiameter * 4.2), "earLength");
|
|
51280
|
+
const earHeight = requirePositive$6(options.earHeight ?? Math.max(eyeOuterRadius * 2.25, pinDiameter * 4.4), "earHeight");
|
|
51281
|
+
const linkArmLength = requirePositive$6(options.linkArmLength ?? 34, "linkArmLength");
|
|
51282
|
+
const retainerThickness = requirePositive$6(
|
|
51283
|
+
options.retainerThickness ?? Math.max(1.2, pinDiameter * 0.35),
|
|
51284
|
+
"retainerThickness"
|
|
51285
|
+
);
|
|
51286
|
+
const segments = options.segments ?? 40;
|
|
51287
|
+
if (eyeOuterRadius <= boreDiameter / 2 + Math.max(0.8, pinDiameter * 0.25)) {
|
|
51288
|
+
throw new Error("clevisPinJointAssembly: eyeOuterRadius leaves too little material around the pin bore");
|
|
51289
|
+
}
|
|
51290
|
+
if (earHeight <= boreDiameter + Math.max(3, pinDiameter)) {
|
|
51291
|
+
throw new Error("clevisPinJointAssembly: earHeight leaves too little material around the pin bore");
|
|
51292
|
+
}
|
|
51293
|
+
if (earLength / 2 <= eyeOuterRadius + runningClearance) {
|
|
51294
|
+
throw new Error("clevisPinJointAssembly: earLength must extend behind the link eye for a rear clevis bridge");
|
|
51295
|
+
}
|
|
51296
|
+
const clevisGap = linkThickness + runningClearance * 2;
|
|
51297
|
+
const earCenterY = clevisGap / 2 + earThickness / 2;
|
|
51298
|
+
const totalStackY = clevisGap + earThickness * 2;
|
|
51299
|
+
const pinLength = totalStackY + retainerThickness * 2 + runningClearance * 2;
|
|
51300
|
+
const bridgeClearX = -eyeOuterRadius - runningClearance;
|
|
51301
|
+
const bridgeLength = Math.max(pinDiameter * 2.2, 4);
|
|
51302
|
+
const bridgeHeight = Math.min(earHeight * 0.48, Math.max(pinDiameter * 1.4, eyeOuterRadius * 0.75));
|
|
51303
|
+
const bridgeCenterX = bridgeClearX - bridgeLength / 2;
|
|
51304
|
+
const bridgeCenterZ = -earHeight / 2 + bridgeHeight / 2;
|
|
51305
|
+
const pinBore = cylinderAlongY(totalStackY + 0.8, boreDiameter / 2, 0, segments);
|
|
51306
|
+
const clevisBlank = union(
|
|
51307
|
+
box(earLength, earThickness, earHeight).translate(0, earCenterY, -earHeight / 2),
|
|
51308
|
+
box(earLength, earThickness, earHeight).translate(0, -earCenterY, -earHeight / 2),
|
|
51309
|
+
box(bridgeLength, totalStackY, bridgeHeight).translate(bridgeCenterX, 0, bridgeCenterZ)
|
|
51310
|
+
);
|
|
51311
|
+
const clevis = clevisBlank.subtract(pinBore).color("#475569");
|
|
51312
|
+
const eye = tubeAlongY(linkThickness, eyeOuterRadius, boreDiameter / 2, 0, segments);
|
|
51313
|
+
const armOverlap = Math.min(eyeOuterRadius * 0.65, linkArmLength * 0.25);
|
|
51314
|
+
const armCenterX = eyeOuterRadius - armOverlap + linkArmLength / 2;
|
|
51315
|
+
const linkArm = box(linkArmLength, linkThickness, linkArmWidth).translate(armCenterX, 0, -linkArmWidth / 2);
|
|
51316
|
+
const link = union(eye, linkArm).color("#111827");
|
|
51317
|
+
const pinCore = cylinderAlongY(pinLength, pinDiameter / 2, 0, segments);
|
|
51318
|
+
const headRadius = Math.max(pinDiameter * 0.9, boreDiameter / 2 + 0.8);
|
|
51319
|
+
const headY = totalStackY / 2 + runningClearance + retainerThickness / 2;
|
|
51320
|
+
const headA = cylinderAlongY(retainerThickness, headRadius, headY, segments);
|
|
51321
|
+
const headB = cylinderAlongY(retainerThickness, headRadius, -headY, segments);
|
|
51322
|
+
const pin = union(pinCore, headA, headB).color("#cbd5e1");
|
|
51323
|
+
const cutter = cylinderAlongY(pinLength + 1, boreDiameter / 2, 0, segments);
|
|
51324
|
+
const parts = [
|
|
51325
|
+
{ name: "bored clevis yoke with rear bridge", shape: clevis },
|
|
51326
|
+
{ name: "center link eye captured in clevis", shape: link },
|
|
51327
|
+
{ name: "retained clevis pin through link eye", shape: pin }
|
|
51328
|
+
];
|
|
51329
|
+
return {
|
|
51330
|
+
parts,
|
|
51331
|
+
clevis,
|
|
51332
|
+
link,
|
|
51333
|
+
pin,
|
|
51334
|
+
cutters: {
|
|
51335
|
+
pinBore: cutter
|
|
51336
|
+
},
|
|
51337
|
+
dims: {
|
|
51338
|
+
pinDiameter,
|
|
51339
|
+
boreDiameter,
|
|
51340
|
+
linkThickness,
|
|
51341
|
+
earThickness,
|
|
51342
|
+
runningClearance,
|
|
51343
|
+
earLength,
|
|
51344
|
+
earHeight,
|
|
51345
|
+
linkArmLength,
|
|
51346
|
+
linkArmWidth,
|
|
51347
|
+
eyeOuterRadius,
|
|
51348
|
+
retainerThickness,
|
|
51349
|
+
pinLength,
|
|
51350
|
+
clevisGap
|
|
51351
|
+
}
|
|
51352
|
+
};
|
|
51353
|
+
}
|
|
51354
|
+
function seatedBearingAssembly(options) {
|
|
51355
|
+
const bearingOuterDiameter = requirePositive$6(options.bearingOuterDiameter, "bearingOuterDiameter");
|
|
51356
|
+
const bearingInnerDiameter = requirePositive$6(options.bearingInnerDiameter, "bearingInnerDiameter");
|
|
51357
|
+
const bearingWidth = requirePositive$6(options.bearingWidth, "bearingWidth");
|
|
51358
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? Math.max(1, bearingInnerDiameter - 0.4), "shaftDiameter");
|
|
51359
|
+
const pocketClearance = requireNonNegative(options.pocketClearance ?? 0.2, "pocketClearance");
|
|
51360
|
+
const shaftClearance = requireNonNegative(options.shaftClearance ?? 0.35, "shaftClearance");
|
|
51361
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
51362
|
+
const housingThickness = requirePositive$6(options.housingThickness ?? bearingWidth + 5, "housingThickness");
|
|
51363
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2, bearingWidth * 0.45), "bossHeight");
|
|
51364
|
+
const bossOuterDiameter = requirePositive$6(
|
|
51365
|
+
options.bossOuterDiameter ?? bearingOuterDiameter + Math.max(8, bearingOuterDiameter * 0.36),
|
|
51366
|
+
"bossOuterDiameter"
|
|
51367
|
+
);
|
|
51368
|
+
const housingWidth = requirePositive$6(options.housingWidth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 2.1), "housingWidth");
|
|
51369
|
+
const housingDepth = requirePositive$6(options.housingDepth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 1.8), "housingDepth");
|
|
51370
|
+
const shaftOverhang = requirePositive$6(options.shaftOverhang ?? Math.max(8, bearingOuterDiameter * 0.45), "shaftOverhang");
|
|
51371
|
+
const shoulderDiameter = requirePositive$6(options.shoulderDiameter ?? Math.max(shaftDiameter * 1.65, bearingInnerDiameter + 2), "shoulderDiameter");
|
|
51372
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(1.5, shaftDiameter * 0.32), "shoulderThickness");
|
|
51373
|
+
const segments = options.segments ?? 48;
|
|
51374
|
+
if (bearingOuterDiameter <= bearingInnerDiameter + Math.max(1, bearingOuterDiameter * 0.08)) {
|
|
51375
|
+
throw new Error("seatedBearingAssembly: bearingOuterDiameter leaves too little bearing wall around the bore");
|
|
51376
|
+
}
|
|
51377
|
+
if (shaftDiameter + shaftClearance >= bearingInnerDiameter) {
|
|
51378
|
+
throw new Error("seatedBearingAssembly: shaftDiameter plus shaftClearance must fit inside the bearing bore");
|
|
51379
|
+
}
|
|
51380
|
+
if (shoulderDiameter >= bearingOuterDiameter - runningClearance * 2) {
|
|
51381
|
+
throw new Error("seatedBearingAssembly: shoulderDiameter must stay smaller than the bearing outer race");
|
|
51382
|
+
}
|
|
51383
|
+
const pocketDiameter = bearingOuterDiameter + pocketClearance;
|
|
51384
|
+
const shaftBoreDiameter = shaftDiameter + shaftClearance;
|
|
51385
|
+
const totalHousingHeight = housingThickness + bossHeight;
|
|
51386
|
+
const pocketDepth = bearingWidth + runningClearance * 2;
|
|
51387
|
+
if (pocketDepth >= totalHousingHeight - runningClearance) {
|
|
51388
|
+
throw new Error("seatedBearingAssembly: housingThickness and bossHeight must leave a shoulder below the bearing pocket");
|
|
51389
|
+
}
|
|
51390
|
+
if (bossOuterDiameter <= pocketDiameter + Math.max(2, bearingOuterDiameter * 0.12)) {
|
|
51391
|
+
throw new Error("seatedBearingAssembly: bossOuterDiameter leaves too little wall around the bearing pocket");
|
|
51392
|
+
}
|
|
51393
|
+
if (housingWidth <= pocketDiameter + 6 || housingDepth <= pocketDiameter + 6) {
|
|
51394
|
+
throw new Error("seatedBearingAssembly: housing dimensions leave too little material around the bearing pocket");
|
|
51395
|
+
}
|
|
51396
|
+
if (shoulderThickness * 2 + runningClearance * 2 >= shaftOverhang) {
|
|
51397
|
+
throw new Error("seatedBearingAssembly: shaftOverhang must leave room for retaining collars outside the housing");
|
|
51398
|
+
}
|
|
51399
|
+
const pocketBottomZ = totalHousingHeight - pocketDepth;
|
|
51400
|
+
const bearingZ = pocketBottomZ + runningClearance;
|
|
51401
|
+
const lowerShoulderZ = -runningClearance - shoulderThickness;
|
|
51402
|
+
const upperShoulderZ = totalHousingHeight + runningClearance;
|
|
51403
|
+
const shaftLength = totalHousingHeight + shaftOverhang * 2;
|
|
51404
|
+
const bossFuseOverlap = Math.min(0.08, Math.max(0.02, bossHeight * 0.03));
|
|
51405
|
+
const bearingPocket = cylinder(pocketDepth + 0.4, pocketDiameter / 2, void 0, segments).translate(0, 0, pocketBottomZ - 0.2);
|
|
51406
|
+
const shaftBore = cylinder(totalHousingHeight + 1, shaftBoreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51407
|
+
const housingBase = box(housingWidth, housingDepth, housingThickness).subtract(bearingPocket).subtract(shaftBore);
|
|
51408
|
+
const housingBoss = cylinder(bossHeight + bossFuseOverlap, bossOuterDiameter / 2, void 0, segments).translate(
|
|
51409
|
+
0,
|
|
51410
|
+
0,
|
|
51411
|
+
housingThickness - bossFuseOverlap
|
|
51412
|
+
).subtract(bearingPocket);
|
|
51413
|
+
const housing = union(housingBase, housingBoss).color("#475569");
|
|
51414
|
+
const bearingRing = tubeAlongZ(bearingWidth, bearingOuterDiameter / 2, bearingInnerDiameter / 2, segments);
|
|
51415
|
+
const shieldInset = Math.min(bearingWidth * 0.18, 0.7);
|
|
51416
|
+
const shieldOuterRadius = bearingOuterDiameter / 2 - Math.max(0.45, (bearingOuterDiameter - bearingInnerDiameter) * 0.08);
|
|
51417
|
+
const shieldInnerRadius = bearingInnerDiameter / 2 + Math.max(0.2, (bearingOuterDiameter - bearingInnerDiameter) * 0.035);
|
|
51418
|
+
const bearingShield = shieldOuterRadius > shieldInnerRadius + 0.2 ? union(
|
|
51419
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(0, 0, shieldInset),
|
|
51420
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(
|
|
51421
|
+
0,
|
|
51422
|
+
0,
|
|
51423
|
+
bearingWidth - shieldInset - Math.min(0.35, bearingWidth * 0.08)
|
|
51424
|
+
)
|
|
51425
|
+
) : null;
|
|
51426
|
+
const bearing = (bearingShield ? union(bearingRing, bearingShield) : bearingRing).translate(0, 0, bearingZ).color("#111827");
|
|
51427
|
+
const shaftCore = cylinder(shaftLength, shaftDiameter / 2, void 0, segments).translate(0, 0, -shaftOverhang);
|
|
51428
|
+
const lowerShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, lowerShoulderZ);
|
|
51429
|
+
const upperShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, upperShoulderZ);
|
|
51430
|
+
const shaft = union(shaftCore, lowerShoulder, upperShoulder).color("#cbd5e1");
|
|
51431
|
+
const parts = [
|
|
51432
|
+
{ name: "bearing housing with counterbore pocket and shoulder", shape: housing },
|
|
51433
|
+
{ name: "purchased radial bearing seated in counterbore", shape: bearing },
|
|
51434
|
+
{ name: "shaft through bearing bore with retaining collars", shape: shaft }
|
|
51435
|
+
];
|
|
51436
|
+
return {
|
|
51437
|
+
parts,
|
|
51438
|
+
housing,
|
|
51439
|
+
bearing,
|
|
51440
|
+
shaft,
|
|
51441
|
+
cutters: {
|
|
51442
|
+
bearingPocket,
|
|
51443
|
+
shaftBore
|
|
51444
|
+
},
|
|
51445
|
+
dims: {
|
|
51446
|
+
bearingOuterDiameter,
|
|
51447
|
+
bearingInnerDiameter,
|
|
51448
|
+
bearingWidth,
|
|
51449
|
+
shaftDiameter,
|
|
51450
|
+
housingWidth,
|
|
51451
|
+
housingDepth,
|
|
51452
|
+
housingThickness,
|
|
51453
|
+
bossOuterDiameter,
|
|
51454
|
+
bossHeight,
|
|
51455
|
+
totalHousingHeight,
|
|
51456
|
+
pocketDiameter,
|
|
51457
|
+
pocketDepth,
|
|
51458
|
+
shaftBoreDiameter,
|
|
51459
|
+
runningClearance,
|
|
51460
|
+
shaftLength,
|
|
51461
|
+
shoulderDiameter,
|
|
51462
|
+
shoulderThickness
|
|
51463
|
+
}
|
|
51464
|
+
};
|
|
51465
|
+
}
|
|
51466
|
+
function cableGlandAnchorAssembly(options) {
|
|
51467
|
+
const cableDiameter = requirePositive$6(options.cableDiameter, "cableDiameter");
|
|
51468
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
51469
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? Math.max(54, cableDiameter * 7), "panelWidth");
|
|
51470
|
+
const panelHeight = requirePositive$6(options.panelHeight ?? Math.max(38, cableDiameter * 5), "panelHeight");
|
|
51471
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51472
|
+
const panelHoleClearance = requirePositive$6(options.panelHoleClearance ?? 0.25, "panelHoleClearance");
|
|
51473
|
+
const cableBoreDiameter = cableDiameter + runningClearance * 2;
|
|
51474
|
+
const glandOuterDiameter = requirePositive$6(options.glandOuterDiameter ?? cableDiameter + Math.max(6, cableDiameter * 0.9), "glandOuterDiameter");
|
|
51475
|
+
const nutOuterDiameter = requirePositive$6(options.nutOuterDiameter ?? glandOuterDiameter + Math.max(6, cableDiameter * 0.8), "nutOuterDiameter");
|
|
51476
|
+
const nutThickness = requirePositive$6(options.nutThickness ?? Math.max(4, cableDiameter * 0.8), "nutThickness");
|
|
51477
|
+
const flangeDiameter = requirePositive$6(options.flangeDiameter ?? glandOuterDiameter + Math.max(5, cableDiameter * 0.7), "flangeDiameter");
|
|
51478
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? Math.max(2, panelThickness * 0.45), "flangeThickness");
|
|
51479
|
+
const minGlandLength = panelThickness + nutThickness + flangeThickness + runningClearance * 4;
|
|
51480
|
+
const glandLength = requirePositive$6(options.glandLength ?? minGlandLength + Math.max(8, cableDiameter), "glandLength");
|
|
51481
|
+
const cableLength = requirePositive$6(options.cableLength ?? glandLength + Math.max(36, cableDiameter * 5), "cableLength");
|
|
51482
|
+
const segments = options.segments ?? 40;
|
|
51483
|
+
if (glandOuterDiameter <= cableBoreDiameter + Math.max(1.2, cableDiameter * 0.18)) {
|
|
51484
|
+
throw new Error("cableGlandAnchorAssembly: glandOuterDiameter leaves too little wall around the cable bore");
|
|
51485
|
+
}
|
|
51486
|
+
if (nutOuterDiameter <= glandOuterDiameter + Math.max(1.5, cableDiameter * 0.2)) {
|
|
51487
|
+
throw new Error("cableGlandAnchorAssembly: nutOuterDiameter must leave material around the gland body");
|
|
51488
|
+
}
|
|
51489
|
+
if (flangeDiameter <= glandOuterDiameter + Math.max(1.2, cableDiameter * 0.16)) {
|
|
51490
|
+
throw new Error("cableGlandAnchorAssembly: flangeDiameter must be larger than the gland body");
|
|
51491
|
+
}
|
|
51492
|
+
if (panelWidth <= flangeDiameter + 8 || panelHeight <= flangeDiameter + 8) {
|
|
51493
|
+
throw new Error("cableGlandAnchorAssembly: panel dimensions leave too little material around the gland hole");
|
|
51494
|
+
}
|
|
51495
|
+
if (glandLength <= minGlandLength) {
|
|
51496
|
+
throw new Error("cableGlandAnchorAssembly: glandLength must span the panel, flange, compression nut, and clearances");
|
|
51497
|
+
}
|
|
51498
|
+
if (cableLength <= glandLength + runningClearance * 2) {
|
|
51499
|
+
throw new Error("cableGlandAnchorAssembly: cableLength must extend beyond the gland body");
|
|
51500
|
+
}
|
|
51501
|
+
const panelHoleDiameter = glandOuterDiameter + panelHoleClearance * 2;
|
|
51502
|
+
const glandOuterRadius = glandOuterDiameter / 2;
|
|
51503
|
+
const cableBoreRadius = cableBoreDiameter / 2;
|
|
51504
|
+
const faceClearance = Math.min(0.05, runningClearance * 0.15);
|
|
51505
|
+
const flangePocketDepth = Math.min(Math.max(0.35, panelThickness * 0.18), panelThickness * 0.4, flangeThickness * 0.55);
|
|
51506
|
+
const panelHole = cylinderAlongX(panelThickness + 0.8, panelHoleDiameter / 2, 0, segments);
|
|
51507
|
+
const flangeSeatPocket = cylinderAlongX(
|
|
51508
|
+
flangePocketDepth + 0.2,
|
|
51509
|
+
flangeDiameter / 2 + panelHoleClearance,
|
|
51510
|
+
panelThickness / 2 - flangePocketDepth / 2,
|
|
51511
|
+
segments
|
|
51512
|
+
);
|
|
51513
|
+
const cableBore = cylinderAlongX(glandLength + 0.8, cableBoreRadius, 0, segments);
|
|
51514
|
+
const panel = box(panelThickness, panelWidth, panelHeight).translate(0, 0, -panelHeight / 2).subtract(panelHole).subtract(flangeSeatPocket).color("#475569");
|
|
51515
|
+
const glandBody = tubeAlongX(glandLength, glandOuterRadius, cableBoreRadius, 0, segments);
|
|
51516
|
+
const flangeCenterX = panelThickness / 2 - flangePocketDepth + faceClearance + flangeThickness / 2;
|
|
51517
|
+
const flange = tubeAlongX(flangeThickness, flangeDiameter / 2, cableBoreRadius, flangeCenterX, segments);
|
|
51518
|
+
const gland = union(glandBody, flange).color("#94a3b8");
|
|
51519
|
+
const nutInnerRadius = glandOuterRadius + Math.min(0.12, runningClearance * 0.4);
|
|
51520
|
+
const nutCenterX = -panelThickness / 2 - faceClearance - nutThickness / 2;
|
|
51521
|
+
const compressionNut = tubeAlongX(nutThickness, nutOuterDiameter / 2, nutInnerRadius, nutCenterX, segments).color("#cbd5e1");
|
|
51522
|
+
const cable = cylinderAlongX(cableLength, cableDiameter / 2, 0, segments).color("#111827");
|
|
51523
|
+
const parts = [
|
|
51524
|
+
{ name: "panel with gland clearance hole", shape: panel },
|
|
51525
|
+
{ name: "hollow cable gland body with panel flange", shape: gland },
|
|
51526
|
+
{ name: "compression nut around gland body", shape: compressionNut },
|
|
51527
|
+
{ name: "routed cable through gland bore", shape: cable }
|
|
51528
|
+
];
|
|
51529
|
+
return {
|
|
51530
|
+
parts,
|
|
51531
|
+
panel,
|
|
51532
|
+
gland,
|
|
51533
|
+
compressionNut,
|
|
51534
|
+
cable,
|
|
51535
|
+
cutters: {
|
|
51536
|
+
panelHole,
|
|
51537
|
+
flangeSeatPocket,
|
|
51538
|
+
cableBore
|
|
51539
|
+
},
|
|
51540
|
+
dims: {
|
|
51541
|
+
cableDiameter,
|
|
51542
|
+
cableBoreDiameter,
|
|
51543
|
+
panelThickness,
|
|
51544
|
+
panelWidth,
|
|
51545
|
+
panelHeight,
|
|
51546
|
+
glandOuterDiameter,
|
|
51547
|
+
glandLength,
|
|
51548
|
+
nutOuterDiameter,
|
|
51549
|
+
nutThickness,
|
|
51550
|
+
flangeDiameter,
|
|
51551
|
+
flangeThickness,
|
|
51552
|
+
runningClearance,
|
|
51553
|
+
faceClearance,
|
|
51554
|
+
flangePocketDepth,
|
|
51555
|
+
panelHoleDiameter,
|
|
51556
|
+
cableLength
|
|
51557
|
+
}
|
|
51558
|
+
};
|
|
51559
|
+
}
|
|
51560
|
+
function hoseBarbPortAssembly(options) {
|
|
51561
|
+
const hoseInnerDiameter = requirePositive$6(options.hoseInnerDiameter, "hoseInnerDiameter");
|
|
51562
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.18, "runningClearance");
|
|
51563
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
51564
|
+
const barbRootDiameter = requirePositive$6(
|
|
51565
|
+
options.barbRootDiameter ?? Math.max(1, hoseInnerDiameter - Math.max(0.25, hoseInnerDiameter * 0.06)),
|
|
51566
|
+
"barbRootDiameter"
|
|
51567
|
+
);
|
|
51568
|
+
const barbPeakDiameter = requirePositive$6(
|
|
51569
|
+
options.barbPeakDiameter ?? hoseInnerDiameter + Math.max(0.65, hoseInnerDiameter * 0.12),
|
|
51570
|
+
"barbPeakDiameter"
|
|
51571
|
+
);
|
|
51572
|
+
const installedHoseBoreDiameter = barbPeakDiameter + runningClearance * 2;
|
|
51573
|
+
const hoseOuterDiameter = requirePositive$6(
|
|
51574
|
+
options.hoseOuterDiameter ?? Math.max(installedHoseBoreDiameter + 2.4, hoseInnerDiameter + Math.max(3, hoseInnerDiameter * 0.55)),
|
|
51575
|
+
"hoseOuterDiameter"
|
|
51576
|
+
);
|
|
51577
|
+
const fluidBoreDiameter = requirePositive$6(options.fluidBoreDiameter ?? hoseInnerDiameter * 0.65, "fluidBoreDiameter");
|
|
51578
|
+
const blockThickness = requirePositive$6(options.blockThickness ?? Math.max(7, hoseInnerDiameter * 1.2), "blockThickness");
|
|
51579
|
+
const barbCount = options.barbCount ?? 3;
|
|
51580
|
+
const barbLength = requirePositive$6(options.barbLength ?? Math.max(2.6, hoseInnerDiameter * 0.55), "barbLength");
|
|
51581
|
+
const barbStackLength = barbCount * barbLength;
|
|
51582
|
+
const shoulderDiameter = requirePositive$6(
|
|
51583
|
+
options.shoulderDiameter ?? barbPeakDiameter + Math.max(4, hoseInnerDiameter * 0.65),
|
|
51584
|
+
"shoulderDiameter"
|
|
51585
|
+
);
|
|
51586
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(2, hoseInnerDiameter * 0.35), "shoulderThickness");
|
|
51587
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? shoulderDiameter + Math.max(4, hoseInnerDiameter * 0.6), "bossDiameter");
|
|
51588
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2.4, hoseInnerDiameter * 0.45), "bossHeight");
|
|
51589
|
+
const blockWidth = requirePositive$6(options.blockWidth ?? bossDiameter + Math.max(14, hoseInnerDiameter * 2.4), "blockWidth");
|
|
51590
|
+
const blockHeight = requirePositive$6(options.blockHeight ?? bossDiameter + Math.max(12, hoseInnerDiameter * 2.1), "blockHeight");
|
|
51591
|
+
const hoseLength = requirePositive$6(options.hoseLength ?? barbStackLength + Math.max(32, hoseInnerDiameter * 5), "hoseLength");
|
|
51592
|
+
const clampWidth = requirePositive$6(options.clampWidth ?? Math.max(4, hoseOuterDiameter * 0.45), "clampWidth");
|
|
51593
|
+
const clampThickness = requirePositive$6(options.clampThickness ?? 0.9, "clampThickness");
|
|
51594
|
+
const segments = options.segments ?? 40;
|
|
51595
|
+
if (!Number.isInteger(barbCount) || barbCount < 1 || barbCount > 8) {
|
|
51596
|
+
throw new Error("hoseBarbPortAssembly: barbCount must be an integer from 1 to 8");
|
|
51597
|
+
}
|
|
51598
|
+
if (barbPeakDiameter <= hoseInnerDiameter) {
|
|
51599
|
+
throw new Error("hoseBarbPortAssembly: barbPeakDiameter must exceed hoseInnerDiameter so the barb retains the hose");
|
|
51600
|
+
}
|
|
51601
|
+
if (barbRootDiameter >= barbPeakDiameter - Math.max(0.25, hoseInnerDiameter * 0.04)) {
|
|
51602
|
+
throw new Error("hoseBarbPortAssembly: barbRootDiameter must leave a visible barb rise");
|
|
51603
|
+
}
|
|
51604
|
+
if (fluidBoreDiameter >= barbRootDiameter - Math.max(0.8, hoseInnerDiameter * 0.12)) {
|
|
51605
|
+
throw new Error("hoseBarbPortAssembly: fluidBoreDiameter leaves too little wall in the barb fitting");
|
|
51606
|
+
}
|
|
51607
|
+
if (hoseOuterDiameter <= installedHoseBoreDiameter + Math.max(1.2, hoseInnerDiameter * 0.16)) {
|
|
51608
|
+
throw new Error("hoseBarbPortAssembly: hoseOuterDiameter leaves too little hose wall around the installed barb envelope");
|
|
51609
|
+
}
|
|
51610
|
+
if (shoulderDiameter <= barbPeakDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
51611
|
+
throw new Error("hoseBarbPortAssembly: shoulderDiameter must be larger than the barb peaks");
|
|
51612
|
+
}
|
|
51613
|
+
if (bossDiameter <= shoulderDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
51614
|
+
throw new Error("hoseBarbPortAssembly: bossDiameter must leave material around the shoulder seat");
|
|
51615
|
+
}
|
|
51616
|
+
if (blockWidth <= bossDiameter + 8 || blockHeight <= bossDiameter + 8) {
|
|
51617
|
+
throw new Error("hoseBarbPortAssembly: receiver block dimensions leave too little material around the port boss");
|
|
51618
|
+
}
|
|
51619
|
+
const portBoreDiameter = barbRootDiameter + runningClearance * 2;
|
|
51620
|
+
const portBore = cylinderAlongX(blockThickness + bossHeight + 0.8, portBoreDiameter / 2, bossHeight / 2, segments);
|
|
51621
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
51622
|
+
const bossCenterX = blockThickness / 2 + bossHeight / 2 - fuseOverlap;
|
|
51623
|
+
const receiver = union(
|
|
51624
|
+
box(blockThickness, blockWidth, blockHeight).translate(0, 0, -blockHeight / 2),
|
|
51625
|
+
cylinderAlongX(bossHeight + fuseOverlap, bossDiameter / 2, bossCenterX, segments)
|
|
51626
|
+
).subtract(portBore).color("#475569");
|
|
51627
|
+
const bossFaceX = blockThickness / 2 + bossHeight;
|
|
51628
|
+
const shoulderCenterX = bossFaceX + faceClearance + shoulderThickness / 2;
|
|
51629
|
+
const barbStartX = shoulderCenterX + shoulderThickness / 2;
|
|
51630
|
+
const fittingStartX = -blockThickness / 2 - runningClearance;
|
|
51631
|
+
const fittingEndX = barbStartX + barbStackLength;
|
|
51632
|
+
const fittingCore = tubeAlongX(fittingEndX - fittingStartX, barbRootDiameter / 2, fluidBoreDiameter / 2, (fittingStartX + fittingEndX) / 2, segments);
|
|
51633
|
+
const shoulder = tubeAlongX(shoulderThickness, shoulderDiameter / 2, fluidBoreDiameter / 2, shoulderCenterX, segments);
|
|
51634
|
+
const barbSolids = [];
|
|
51635
|
+
const ridgeLength = Math.max(0.8, Math.min(barbLength * 0.45, hoseInnerDiameter * 0.28));
|
|
51636
|
+
for (let index2 = 0; index2 < barbCount; index2 += 1) {
|
|
51637
|
+
const startX = barbStartX + index2 * barbLength;
|
|
51638
|
+
const ridgeCenterX = startX + barbLength - ridgeLength / 2;
|
|
51639
|
+
barbSolids.push(tubeAlongX(ridgeLength, barbPeakDiameter / 2, fluidBoreDiameter / 2, ridgeCenterX, segments));
|
|
51640
|
+
}
|
|
51641
|
+
const fitting = union(fittingCore, shoulder, ...barbSolids).color("#94a3b8");
|
|
51642
|
+
const hoseStartX = barbStartX + faceClearance;
|
|
51643
|
+
const hoseCenterX = hoseStartX + hoseLength / 2;
|
|
51644
|
+
const installedHoseBore = cylinderAlongX(hoseLength + 0.8, installedHoseBoreDiameter / 2, hoseCenterX, segments);
|
|
51645
|
+
const hose = tubeAlongX(hoseLength, hoseOuterDiameter / 2, installedHoseBoreDiameter / 2, hoseCenterX, segments).color("#111827");
|
|
51646
|
+
const clampCenterX = barbStartX + Math.min(barbStackLength * 0.55, Math.max(barbLength, clampWidth));
|
|
51647
|
+
const clamp2 = tubeAlongX(
|
|
51648
|
+
clampWidth,
|
|
51649
|
+
hoseOuterDiameter / 2 + clampThickness,
|
|
51650
|
+
hoseOuterDiameter / 2 + Math.min(0.08, runningClearance * 0.45),
|
|
51651
|
+
clampCenterX,
|
|
51652
|
+
segments
|
|
51653
|
+
).color("#cbd5e1");
|
|
51654
|
+
const parts = [
|
|
51655
|
+
{ name: "bored pump or filter body with raised hose-port boss", shape: receiver },
|
|
51656
|
+
{ name: "hollow hose barb fitting with shoulder and retention ridges", shape: fitting },
|
|
51657
|
+
{ name: "installed flexible hose over barb tail", shape: hose },
|
|
51658
|
+
{ name: "clamp band over hose and barb ridges", shape: clamp2 }
|
|
51659
|
+
];
|
|
51660
|
+
return {
|
|
51661
|
+
parts,
|
|
51662
|
+
receiver,
|
|
51663
|
+
fitting,
|
|
51664
|
+
hose,
|
|
51665
|
+
clamp: clamp2,
|
|
51666
|
+
cutters: {
|
|
51667
|
+
portBore,
|
|
51668
|
+
installedHoseBore
|
|
51669
|
+
},
|
|
51670
|
+
dims: {
|
|
51671
|
+
hoseInnerDiameter,
|
|
51672
|
+
hoseOuterDiameter,
|
|
51673
|
+
installedHoseBoreDiameter,
|
|
51674
|
+
blockThickness,
|
|
51675
|
+
blockWidth,
|
|
51676
|
+
blockHeight,
|
|
51677
|
+
bossDiameter,
|
|
51678
|
+
bossHeight,
|
|
51679
|
+
fluidBoreDiameter,
|
|
51680
|
+
barbRootDiameter,
|
|
51681
|
+
barbPeakDiameter,
|
|
51682
|
+
barbCount,
|
|
51683
|
+
barbLength,
|
|
51684
|
+
barbStackLength,
|
|
51685
|
+
shoulderDiameter,
|
|
51686
|
+
shoulderThickness,
|
|
51687
|
+
hoseLength,
|
|
51688
|
+
clampWidth,
|
|
51689
|
+
clampThickness,
|
|
51690
|
+
runningClearance,
|
|
51691
|
+
faceClearance
|
|
51692
|
+
}
|
|
51693
|
+
};
|
|
51694
|
+
}
|
|
51695
|
+
function routedTubeClipAssembly(options) {
|
|
51696
|
+
const tubeDiameter = requirePositive$6(options.tubeDiameter, "tubeDiameter");
|
|
51697
|
+
const tubeLength = requirePositive$6(options.tubeLength ?? 120, "tubeLength");
|
|
51698
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
51699
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51700
|
+
const screwSize = options.screwSize ?? "M3";
|
|
51701
|
+
const segments = options.segments ?? 32;
|
|
51702
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51703
|
+
if (!sizeData) throw new Error(`routedTubeClipAssembly: unsupported screwSize "${screwSize}"`);
|
|
51704
|
+
const clipCount = options.clipCount ?? 3;
|
|
51705
|
+
if (!Number.isInteger(clipCount) || clipCount < 1 || clipCount > 8) {
|
|
51706
|
+
throw new Error("routedTubeClipAssembly: clipCount must be an integer from 1 to 8");
|
|
51707
|
+
}
|
|
51708
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
51709
|
+
const screwHeadDiameter = sizeData.head;
|
|
51710
|
+
const tubeBoreDiameter = tubeDiameter + runningClearance * 2;
|
|
51711
|
+
const clipWallThickness = requirePositive$6(
|
|
51712
|
+
options.clipWallThickness ?? Math.max(screwHeadDiameter + 1.2, tubeDiameter * 0.45, 5),
|
|
51713
|
+
"clipWallThickness"
|
|
51714
|
+
);
|
|
51715
|
+
const clipWidth = requirePositive$6(options.clipWidth ?? Math.max(screwHeadDiameter + 3, tubeDiameter * 1.4, 10), "clipWidth");
|
|
51716
|
+
const clipDepth = tubeBoreDiameter + clipWallThickness * 2;
|
|
51717
|
+
const bottomWall = Math.max(1.2, clipWallThickness * 0.35);
|
|
51718
|
+
const topWall = Math.max(2, clipWallThickness * 0.45);
|
|
51719
|
+
const clipHeight = bottomWall + tubeBoreDiameter + topWall;
|
|
51720
|
+
const tubeCenterZ = panelThickness + bottomWall + tubeBoreDiameter / 2;
|
|
51721
|
+
const panelLength = requirePositive$6(options.panelLength ?? tubeLength + 24, "panelLength");
|
|
51722
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? clipDepth + Math.max(14, screwHeadDiameter * 2), "panelWidth");
|
|
51723
|
+
if (tubeLength <= clipWidth + 8) {
|
|
51724
|
+
throw new Error("routedTubeClipAssembly: tubeLength must leave visible tube beyond the clip body");
|
|
51725
|
+
}
|
|
51726
|
+
const defaultSpacing = clipCount === 1 ? 0 : Math.max(clipWidth + 8, (tubeLength - clipWidth * 2) / (clipCount - 1));
|
|
51727
|
+
const clipSpacing = options.clipSpacing === void 0 ? defaultSpacing : requirePositive$6(options.clipSpacing, "clipSpacing");
|
|
51728
|
+
const clipCenters = Array.from({ length: clipCount }, (_2, index2) => (index2 - (clipCount - 1) / 2) * clipSpacing);
|
|
51729
|
+
const maxClipExtent = Math.max(...clipCenters.map((x2) => Math.abs(x2) + clipWidth / 2));
|
|
51730
|
+
if (maxClipExtent > tubeLength / 2 - 2) {
|
|
51731
|
+
throw new Error("routedTubeClipAssembly: clipSpacing places a clip beyond the routed tube length");
|
|
51732
|
+
}
|
|
51733
|
+
if (maxClipExtent > panelLength / 2 - 2) {
|
|
51734
|
+
throw new Error("routedTubeClipAssembly: panelLength is too short for the clip pattern");
|
|
51735
|
+
}
|
|
51736
|
+
const boreRadius = tubeBoreDiameter / 2;
|
|
51737
|
+
const screwY = boreRadius + clipWallThickness / 2;
|
|
51738
|
+
if (screwY + screwHeadDiameter / 2 > clipDepth / 2 - 0.2) {
|
|
51739
|
+
throw new Error("routedTubeClipAssembly: clipWallThickness leaves too little land for screw heads");
|
|
51740
|
+
}
|
|
51741
|
+
if (clipDepth > panelWidth - Math.max(4, screwHeadDiameter * 0.5)) {
|
|
51742
|
+
throw new Error("routedTubeClipAssembly: panelWidth leaves too little material beside the clips");
|
|
51743
|
+
}
|
|
51744
|
+
const screwPositions = clipCenters.flatMap((x2) => [
|
|
51745
|
+
[x2, -screwY],
|
|
51746
|
+
[x2, screwY]
|
|
51747
|
+
]);
|
|
51748
|
+
const screwClearanceDiameter = Math.max(sizeData.loose, screwDiameter + 0.8);
|
|
51749
|
+
const panelThreadEnvelopeDiameter = screwClearanceDiameter;
|
|
51750
|
+
const clipTopZ = panelThickness + clipHeight;
|
|
51751
|
+
const clipTubeBores = union(
|
|
51752
|
+
...clipCenters.map((x2) => cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ))
|
|
51753
|
+
);
|
|
51754
|
+
const clipScrewClearances = union(
|
|
51755
|
+
...screwPositions.map(([x2, y2]) => cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, y2, panelThickness - 0.4))
|
|
51756
|
+
);
|
|
51757
|
+
const panelThreadEnvelopes = union(
|
|
51758
|
+
...screwPositions.map(([x2, y2]) => cylinder(panelThickness + 0.8, panelThreadEnvelopeDiameter / 2, void 0, segments).translate(x2, y2, -0.4))
|
|
51759
|
+
);
|
|
51760
|
+
const panel = box(panelLength, panelWidth, panelThickness).subtract(panelThreadEnvelopes).color("#475569");
|
|
51761
|
+
const tube2 = cylinderAlongX(tubeLength, tubeDiameter / 2, 0, segments).translate(0, 0, tubeCenterZ).color("#0f172a");
|
|
51762
|
+
const clips = clipCenters.map((x2) => {
|
|
51763
|
+
const body = box(clipWidth, clipDepth, clipHeight).translate(x2, 0, panelThickness);
|
|
51764
|
+
const tubeBore = cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ);
|
|
51765
|
+
const screwHoles = union(
|
|
51766
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, -screwY, panelThickness - 0.4),
|
|
51767
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, screwY, panelThickness - 0.4)
|
|
51768
|
+
);
|
|
51769
|
+
return body.subtract(tubeBore).subtract(screwHoles).color("#94a3b8");
|
|
51770
|
+
});
|
|
51771
|
+
const screwLength = clipHeight + panelThickness * 0.65;
|
|
51772
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
51773
|
+
const screwBlank = union(
|
|
51774
|
+
cylinder(screwLength, screwDiameter / 2, void 0, segments).translate(0, 0, clipTopZ - screwLength),
|
|
51775
|
+
cylinder(screwHeadHeight, screwHeadDiameter / 2, void 0, segments).translate(0, 0, clipTopZ)
|
|
51776
|
+
).color("#cbd5e1");
|
|
51777
|
+
const screws = screwPositions.map(([x2, y2]) => screwBlank.translate(x2, y2, 0));
|
|
51778
|
+
const parts = [
|
|
51779
|
+
{ name: "panel with tube-clip screw receiving holes", shape: panel },
|
|
51780
|
+
{ name: "routed flexible tube through retained clip bores", shape: tube2 },
|
|
51781
|
+
...clips.map((shape, index2) => ({ name: `saddle tube clip ${index2 + 1} with through-bore`, shape })),
|
|
51782
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} tube clip screw ${index2 + 1}`, shape }))
|
|
51783
|
+
];
|
|
51784
|
+
return {
|
|
51785
|
+
parts,
|
|
51786
|
+
panel,
|
|
51787
|
+
tube: tube2,
|
|
51788
|
+
clips,
|
|
51789
|
+
screws,
|
|
51790
|
+
clipCenters,
|
|
51791
|
+
screwPositions,
|
|
51792
|
+
cutters: {
|
|
51793
|
+
clipTubeBores,
|
|
51794
|
+
clipScrewClearances,
|
|
51795
|
+
panelThreadEnvelopes
|
|
51796
|
+
},
|
|
51797
|
+
dims: {
|
|
51798
|
+
tubeDiameter,
|
|
51799
|
+
tubeLength,
|
|
51800
|
+
tubeBoreDiameter,
|
|
51801
|
+
panelLength,
|
|
51802
|
+
panelWidth,
|
|
51803
|
+
panelThickness,
|
|
51804
|
+
clipCount,
|
|
51805
|
+
clipWidth,
|
|
51806
|
+
clipDepth,
|
|
51807
|
+
clipHeight,
|
|
51808
|
+
clipWallThickness,
|
|
51809
|
+
tubeCenterZ,
|
|
51810
|
+
screwSize,
|
|
51811
|
+
screwDiameter,
|
|
51812
|
+
screwHeadDiameter,
|
|
51813
|
+
screwLength,
|
|
51814
|
+
screwClearanceDiameter,
|
|
51815
|
+
panelThreadEnvelopeDiameter,
|
|
51816
|
+
runningClearance
|
|
51817
|
+
}
|
|
51818
|
+
};
|
|
51819
|
+
}
|
|
51820
|
+
function pcbTerminalBlockAssembly(options = {}) {
|
|
51821
|
+
const terminalCount = options.terminalCount ?? 4;
|
|
51822
|
+
if (!Number.isInteger(terminalCount) || terminalCount < 1 || terminalCount > 24) {
|
|
51823
|
+
throw new Error("pcbTerminalBlockAssembly: terminalCount must be an integer from 1 to 24");
|
|
51824
|
+
}
|
|
51825
|
+
const terminalPitch = requirePositive$6(options.terminalPitch ?? 5.08, "terminalPitch");
|
|
51826
|
+
const terminalBlockWidth = terminalPitch * terminalCount + 3;
|
|
51827
|
+
const boardWidth = requirePositive$6(options.boardWidth ?? Math.max(50, terminalBlockWidth + 28), "boardWidth");
|
|
51828
|
+
const boardDepth = requirePositive$6(options.boardDepth ?? 38, "boardDepth");
|
|
51829
|
+
const boardThickness = requirePositive$6(options.boardThickness ?? 1.6, "boardThickness");
|
|
51830
|
+
const backplateThickness = requirePositive$6(options.backplateThickness ?? 3, "backplateThickness");
|
|
51831
|
+
const backplateMargin = requirePositive$6(options.backplateMargin ?? 5, "backplateMargin");
|
|
51832
|
+
const standoffHeight = requirePositive$6(options.standoffHeight ?? 6, "standoffHeight");
|
|
51833
|
+
const screwSize = options.screwSize ?? "M3";
|
|
51834
|
+
const segments = options.segments ?? 28;
|
|
51835
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51836
|
+
if (!sizeData) throw new Error(`pcbTerminalBlockAssembly: unsupported screwSize "${screwSize}"`);
|
|
51837
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
51838
|
+
const screwHeadDiameter = sizeData.head;
|
|
51839
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
51840
|
+
const standoffDiameter = requirePositive$6(
|
|
51841
|
+
options.standoffDiameter ?? Math.max(screwHeadDiameter * 1.45, sizeData.normal + 3),
|
|
51842
|
+
"standoffDiameter"
|
|
51843
|
+
);
|
|
51844
|
+
const [mountInsetX, mountInsetY] = resolveBoltInset(
|
|
51845
|
+
options.mountingInset,
|
|
51846
|
+
Math.max(standoffDiameter / 2 + 1.2, screwHeadDiameter * 0.75)
|
|
51847
|
+
);
|
|
51848
|
+
if (mountInsetX * 2 >= boardWidth || mountInsetY * 2 >= boardDepth) {
|
|
51849
|
+
throw new Error("pcbTerminalBlockAssembly: mountingInset leaves no room for the PCB mounting pattern");
|
|
51850
|
+
}
|
|
51851
|
+
const terminalBlockDepth = requirePositive$6(options.terminalBlockDepth ?? 10, "terminalBlockDepth");
|
|
51852
|
+
const terminalBlockHeight = requirePositive$6(options.terminalBlockHeight ?? 9, "terminalBlockHeight");
|
|
51853
|
+
const terminalEdgeInset = requirePositive$6(options.terminalEdgeInset ?? 5, "terminalEdgeInset");
|
|
51854
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 0.9, "pinDiameter");
|
|
51855
|
+
const pinClearance = requirePositive$6(options.pinClearance ?? 0.25, "pinClearance");
|
|
51856
|
+
const pinTailLength = requireNonNegative(options.pinTailLength ?? 0, "pinTailLength");
|
|
51857
|
+
const wirePortDiameter = requirePositive$6(options.wirePortDiameter ?? 2.6, "wirePortDiameter");
|
|
51858
|
+
const pinHoleDiameter = pinDiameter + pinClearance;
|
|
51859
|
+
const terminalCenterY = -boardDepth / 2 + terminalEdgeInset + terminalBlockDepth / 2;
|
|
51860
|
+
const pinY = terminalCenterY + terminalBlockDepth * 0.24;
|
|
51861
|
+
const firstPinX = -((terminalCount - 1) * terminalPitch) / 2;
|
|
51862
|
+
const pinPositions = Array.from({ length: terminalCount }, (_2, index2) => [firstPinX + index2 * terminalPitch, pinY]);
|
|
51863
|
+
const mountingPositions = [
|
|
51864
|
+
[-boardWidth / 2 + mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
51865
|
+
[boardWidth / 2 - mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
51866
|
+
[-boardWidth / 2 + mountInsetX, boardDepth / 2 - mountInsetY],
|
|
51867
|
+
[boardWidth / 2 - mountInsetX, boardDepth / 2 - mountInsetY]
|
|
51868
|
+
];
|
|
51869
|
+
if (terminalBlockWidth >= boardWidth - mountInsetX * 2) {
|
|
51870
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block is too wide for the PCB mounting pattern");
|
|
51871
|
+
}
|
|
51872
|
+
if (terminalEdgeInset + terminalBlockDepth >= boardDepth - mountInsetY * 2) {
|
|
51873
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block depth collides with the rear mounting datum");
|
|
51874
|
+
}
|
|
51875
|
+
if (pinHoleDiameter >= terminalPitch * 0.55) {
|
|
51876
|
+
throw new Error("pcbTerminalBlockAssembly: pinDiameter and pinClearance leave too little PCB web between terminal holes");
|
|
51877
|
+
}
|
|
51878
|
+
if (wirePortDiameter >= Math.min(terminalPitch * 0.72, terminalBlockHeight * 0.65)) {
|
|
51879
|
+
throw new Error("pcbTerminalBlockAssembly: wirePortDiameter is too large for the terminal pitch or body height");
|
|
51880
|
+
}
|
|
51881
|
+
for (const [index2, [x2, y2]] of [...mountingPositions, ...pinPositions].entries()) {
|
|
51882
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
51883
|
+
throw new Error(`pcbTerminalBlockAssembly: generated datum position ${index2} is not finite`);
|
|
51884
|
+
}
|
|
51885
|
+
}
|
|
51886
|
+
const backplateWidth = boardWidth + backplateMargin * 2;
|
|
51887
|
+
const backplateDepth = boardDepth + backplateMargin * 2;
|
|
51888
|
+
const boardBottomZ = backplateThickness + standoffHeight;
|
|
51889
|
+
const boardTopZ = boardBottomZ + boardThickness;
|
|
51890
|
+
const standoffOverlap = Math.min(0.08, standoffHeight * 0.03);
|
|
51891
|
+
const standoffThreadEnvelopeDiameter = Math.max(sizeData.loose, screwDiameter + 1);
|
|
51892
|
+
const standoffThreadEnvelope = cylinder(standoffHeight + 0.8, standoffThreadEnvelopeDiameter / 2, void 0, segments).translate(
|
|
51893
|
+
0,
|
|
51894
|
+
0,
|
|
51895
|
+
backplateThickness - 0.4
|
|
51896
|
+
);
|
|
51897
|
+
const standoffThreadEnvelopes = union(...mountingPositions.map(([x2, y2]) => standoffThreadEnvelope.translate(x2, y2, 0)));
|
|
51898
|
+
const standoff = cylinder(standoffHeight + standoffOverlap, standoffDiameter / 2, void 0, segments).translate(0, 0, backplateThickness - standoffOverlap).subtract(standoffThreadEnvelope);
|
|
51899
|
+
const standoffs = union(...mountingPositions.map(([x2, y2]) => standoff.translate(x2, y2, 0)));
|
|
51900
|
+
const backplate = union(box(backplateWidth, backplateDepth, backplateThickness), standoffs).color("#475569");
|
|
51901
|
+
const boardMountingHoleDiameter = sizeData.normal;
|
|
51902
|
+
const boardMountHole = cylinder(boardThickness + 0.8, boardMountingHoleDiameter / 2, void 0, segments).translate(
|
|
51903
|
+
0,
|
|
51904
|
+
0,
|
|
51905
|
+
boardBottomZ - 0.4
|
|
51906
|
+
);
|
|
51907
|
+
const pcbMountingHoles = union(...mountingPositions.map(([x2, y2]) => boardMountHole.translate(x2, y2, 0)));
|
|
51908
|
+
const pinHole = cylinder(boardThickness + 0.8, pinHoleDiameter / 2, void 0, segments).translate(0, 0, boardBottomZ - 0.4);
|
|
51909
|
+
const pcbPinHoles = union(...pinPositions.map(([x2, y2]) => pinHole.translate(x2, y2, 0)));
|
|
51910
|
+
const pcb = box(boardWidth, boardDepth, boardThickness).translate(0, 0, boardBottomZ).subtract(pcbMountingHoles).subtract(pcbPinHoles).color("#166534");
|
|
51911
|
+
const terminalBodyBlank = box(terminalBlockWidth, terminalBlockDepth, terminalBlockHeight).translate(0, terminalCenterY, boardTopZ);
|
|
51912
|
+
const wirePort = cylinderAlongY(terminalBlockDepth + 0.8, wirePortDiameter / 2, terminalCenterY, segments).translate(
|
|
51913
|
+
0,
|
|
51914
|
+
0,
|
|
51915
|
+
boardTopZ + terminalBlockHeight * 0.42
|
|
51916
|
+
);
|
|
51917
|
+
const wirePorts = union(...pinPositions.map(([x2]) => wirePort.translate(x2, 0, 0)));
|
|
51918
|
+
const clampScrewPockets = union(
|
|
51919
|
+
...pinPositions.map(
|
|
51920
|
+
([x2]) => cylinder(Math.max(0.6, terminalBlockHeight * 0.22), Math.min(terminalPitch * 0.22, wirePortDiameter * 0.42), void 0, segments).translate(
|
|
51921
|
+
x2,
|
|
51922
|
+
terminalCenterY + terminalBlockDepth * 0.12,
|
|
51923
|
+
boardTopZ + terminalBlockHeight * 0.76
|
|
51924
|
+
)
|
|
51925
|
+
)
|
|
51926
|
+
);
|
|
51927
|
+
const pinLength = boardThickness + pinTailLength + Math.min(0.6, terminalBlockHeight * 0.08);
|
|
51928
|
+
const pinStartZ = boardBottomZ - pinTailLength;
|
|
51929
|
+
const pins = union(...pinPositions.map(([x2, y2]) => cylinder(pinLength, pinDiameter / 2, void 0, segments).translate(x2, y2, pinStartZ)));
|
|
51930
|
+
const terminalBlock = union(terminalBodyBlank.subtract(wirePorts).subtract(clampScrewPockets), pins).color("#16a34a");
|
|
51931
|
+
const screwShaftLength = boardThickness + standoffHeight * 0.85;
|
|
51932
|
+
const mountingHardware = fastenerSet(screwSize, screwShaftLength, {
|
|
51933
|
+
washerUnderHead: false,
|
|
51934
|
+
washerUnderNut: false,
|
|
51935
|
+
fit: "normal",
|
|
51936
|
+
segments
|
|
51937
|
+
});
|
|
51938
|
+
const screws = mountingPositions.map(([x2, y2]) => mountingHardware.bolt.translate(x2, y2, boardTopZ).color("#cbd5e1"));
|
|
51939
|
+
const parts = [
|
|
51940
|
+
{ name: "electronics backplate with fused PCB standoffs", shape: backplate },
|
|
51941
|
+
{ name: "PCB with mounting holes and terminal pin clearances", shape: pcb },
|
|
51942
|
+
{ name: "seated purchased terminal block with through-board pins", shape: terminalBlock },
|
|
51943
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} PCB mounting screw ${index2 + 1}`, shape }))
|
|
51944
|
+
];
|
|
51945
|
+
return {
|
|
51946
|
+
parts,
|
|
51947
|
+
backplate,
|
|
51948
|
+
pcb,
|
|
51949
|
+
terminalBlock,
|
|
51950
|
+
screws,
|
|
51951
|
+
mountingPositions,
|
|
51952
|
+
pinPositions,
|
|
51953
|
+
cutters: {
|
|
51954
|
+
pcbMountingHoles,
|
|
51955
|
+
pcbPinHoles,
|
|
51956
|
+
standoffThreadEnvelopes
|
|
51957
|
+
},
|
|
51958
|
+
dims: {
|
|
51959
|
+
terminalCount,
|
|
51960
|
+
terminalPitch,
|
|
51961
|
+
boardWidth,
|
|
51962
|
+
boardDepth,
|
|
51963
|
+
boardThickness,
|
|
51964
|
+
backplateWidth,
|
|
51965
|
+
backplateDepth,
|
|
51966
|
+
backplateThickness,
|
|
51967
|
+
standoffHeight,
|
|
51968
|
+
standoffDiameter,
|
|
51969
|
+
screwSize,
|
|
51970
|
+
screwDiameter,
|
|
51971
|
+
screwHeadDiameter,
|
|
51972
|
+
screwHeadHeight,
|
|
51973
|
+
screwShaftLength,
|
|
51974
|
+
boardMountingHoleDiameter,
|
|
51975
|
+
standoffThreadEnvelopeDiameter,
|
|
51976
|
+
terminalBlockWidth,
|
|
51977
|
+
terminalBlockDepth,
|
|
51978
|
+
terminalBlockHeight,
|
|
51979
|
+
terminalEdgeInset,
|
|
51980
|
+
pinDiameter,
|
|
51981
|
+
pinClearance,
|
|
51982
|
+
pinHoleDiameter,
|
|
51983
|
+
pinTailLength,
|
|
51984
|
+
wirePortDiameter
|
|
51985
|
+
}
|
|
51986
|
+
};
|
|
51987
|
+
}
|
|
51988
|
+
function thumbScrewClampAssembly(options = {}) {
|
|
51989
|
+
const screwSize = options.screwSize ?? "M6";
|
|
51990
|
+
const segments = options.segments ?? 36;
|
|
51991
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51992
|
+
if (!sizeData) throw new Error(`thumbScrewClampAssembly: unsupported screwSize "${screwSize}"`);
|
|
51993
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
51994
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51995
|
+
const faceClearance = requireNonNegative(options.faceClearance ?? 0, "faceClearance");
|
|
51996
|
+
const threadEnvelopeDiameter = Math.max(sizeData.normal, screwDiameter + runningClearance * 2);
|
|
51997
|
+
const pressurePadDiameter = requirePositive$6(
|
|
51998
|
+
options.pressurePadDiameter ?? Math.max(screwDiameter * 3.2, 18),
|
|
51999
|
+
"pressurePadDiameter"
|
|
52000
|
+
);
|
|
52001
|
+
const pressurePadThickness = requirePositive$6(
|
|
52002
|
+
options.pressurePadThickness ?? Math.max(screwDiameter * 0.72, 4),
|
|
52003
|
+
"pressurePadThickness"
|
|
52004
|
+
);
|
|
52005
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? Math.max(screwDiameter * 4.2, 24), "knobDiameter");
|
|
52006
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(screwDiameter * 0.9, 7), "knobThickness");
|
|
52007
|
+
const workpieceThickness = requirePositive$6(options.workpieceThickness ?? 18, "workpieceThickness");
|
|
52008
|
+
const workpieceDepth = requirePositive$6(options.workpieceDepth ?? Math.max(46, pressurePadDiameter * 1.5), "workpieceDepth");
|
|
52009
|
+
const workpieceHeight = requirePositive$6(options.workpieceHeight ?? Math.max(pressurePadDiameter * 1.35, 24), "workpieceHeight");
|
|
52010
|
+
const frameDepth = requirePositive$6(
|
|
52011
|
+
options.frameDepth ?? Math.max(workpieceDepth + 12, pressurePadDiameter + 16),
|
|
52012
|
+
"frameDepth"
|
|
52013
|
+
);
|
|
52014
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? Math.max(screwDiameter, 6), "baseThickness");
|
|
52015
|
+
const jawThickness = requirePositive$6(options.jawThickness ?? Math.max(screwDiameter * 1.35, 9), "jawThickness");
|
|
52016
|
+
const supportThickness = requirePositive$6(
|
|
52017
|
+
options.supportThickness ?? Math.max(screwDiameter * 1.8, 12),
|
|
52018
|
+
"supportThickness"
|
|
52019
|
+
);
|
|
52020
|
+
const bossLength = requirePositive$6(options.bossLength ?? Math.max(screwDiameter * 1.1, 8), "bossLength");
|
|
52021
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? Math.max(threadEnvelopeDiameter + 5, screwDiameter * 2.5), "bossDiameter");
|
|
52022
|
+
const exposedScrewLength = requirePositive$6(
|
|
52023
|
+
options.exposedScrewLength ?? Math.max(pressurePadDiameter * 0.45, screwDiameter * 2.2),
|
|
52024
|
+
"exposedScrewLength"
|
|
52025
|
+
);
|
|
52026
|
+
const screwCenterZ = baseThickness + Math.max(workpieceHeight * 0.52, pressurePadDiameter * 0.68);
|
|
52027
|
+
const frameHeight = requirePositive$6(
|
|
52028
|
+
options.frameHeight ?? screwCenterZ - baseThickness + pressurePadDiameter / 2 + Math.max(baseThickness, 7),
|
|
52029
|
+
"frameHeight"
|
|
52030
|
+
);
|
|
52031
|
+
if (workpieceDepth > frameDepth - 6) {
|
|
52032
|
+
throw new Error("thumbScrewClampAssembly: frameDepth must leave side material around the clamped workpiece");
|
|
52033
|
+
}
|
|
52034
|
+
if (pressurePadDiameter > frameDepth - 4) {
|
|
52035
|
+
throw new Error("thumbScrewClampAssembly: pressurePadDiameter is too large for the frame depth");
|
|
52036
|
+
}
|
|
52037
|
+
if (bossDiameter > frameDepth - 4) {
|
|
52038
|
+
throw new Error("thumbScrewClampAssembly: bossDiameter is too large for the frame depth");
|
|
52039
|
+
}
|
|
52040
|
+
if (screwCenterZ - pressurePadDiameter / 2 <= baseThickness + 0.5) {
|
|
52041
|
+
throw new Error("thumbScrewClampAssembly: pressure pad collides with the base bridge");
|
|
52042
|
+
}
|
|
52043
|
+
if (baseThickness + frameHeight - screwCenterZ <= pressurePadDiameter / 2 + 2) {
|
|
52044
|
+
throw new Error("thumbScrewClampAssembly: frameHeight leaves too little material above the screw axis");
|
|
52045
|
+
}
|
|
52046
|
+
if (threadEnvelopeDiameter + 4 > Math.min(frameDepth, frameHeight)) {
|
|
52047
|
+
throw new Error("thumbScrewClampAssembly: threaded boss bore leaves too little surrounding frame material");
|
|
52048
|
+
}
|
|
52049
|
+
const workpieceLeftFaceX = -workpieceThickness / 2;
|
|
52050
|
+
const workpieceRightFaceX = workpieceThickness / 2;
|
|
52051
|
+
const anvilOverlap = Math.min(0.35, pressurePadThickness * 0.18);
|
|
52052
|
+
const anvilPadCenterX = workpieceLeftFaceX - faceClearance - pressurePadThickness / 2;
|
|
52053
|
+
const pressurePadCenterX = workpieceRightFaceX + faceClearance + pressurePadThickness / 2;
|
|
52054
|
+
const fixedJawRightFaceX = anvilPadCenterX - pressurePadThickness / 2 + anvilOverlap;
|
|
52055
|
+
const fixedJawCenterX = fixedJawRightFaceX - jawThickness / 2;
|
|
52056
|
+
const pressurePadRightFaceX = pressurePadCenterX + pressurePadThickness / 2;
|
|
52057
|
+
const supportInnerFaceX = pressurePadRightFaceX + exposedScrewLength;
|
|
52058
|
+
const supportCenterX = supportInnerFaceX + supportThickness / 2;
|
|
52059
|
+
const supportOuterFaceX = supportInnerFaceX + supportThickness;
|
|
52060
|
+
const frameLeftFaceX = fixedJawCenterX - jawThickness / 2;
|
|
52061
|
+
const frameRightFaceX = supportOuterFaceX;
|
|
52062
|
+
const baseLength = frameRightFaceX - frameLeftFaceX;
|
|
52063
|
+
if (baseLength <= 0 || !Number.isFinite(baseLength)) {
|
|
52064
|
+
throw new Error("thumbScrewClampAssembly: generated clamp frame length is invalid");
|
|
52065
|
+
}
|
|
52066
|
+
const bossCenterX = supportInnerFaceX + (supportThickness + bossLength) / 2;
|
|
52067
|
+
const threadedBossBore = cylinderAlongX(supportThickness + bossLength + 1, threadEnvelopeDiameter / 2, bossCenterX, segments).translate(
|
|
52068
|
+
0,
|
|
52069
|
+
0,
|
|
52070
|
+
screwCenterZ
|
|
52071
|
+
);
|
|
52072
|
+
const frameOverlap = Math.min(0.12, baseThickness * 0.04);
|
|
52073
|
+
const base = box(baseLength, frameDepth, baseThickness).translate((frameLeftFaceX + frameRightFaceX) / 2, 0, 0);
|
|
52074
|
+
const fixedJaw = box(jawThickness, frameDepth, frameHeight + frameOverlap).translate(fixedJawCenterX, 0, baseThickness - frameOverlap);
|
|
52075
|
+
const support = box(supportThickness, frameDepth, frameHeight + frameOverlap).translate(supportCenterX, 0, baseThickness - frameOverlap);
|
|
52076
|
+
const boss2 = cylinderAlongX(supportThickness + bossLength, bossDiameter / 2, bossCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52077
|
+
const anvilPad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, anvilPadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52078
|
+
const frame = union(base, fixedJaw, support, boss2, anvilPad).subtract(threadedBossBore).color("#475569");
|
|
52079
|
+
const workpieceBottomZ = screwCenterZ - workpieceHeight / 2;
|
|
52080
|
+
const workpiece = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ).color("#a16207");
|
|
52081
|
+
const pressurePad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, pressurePadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52082
|
+
const knobCenterX = supportOuterFaceX + bossLength + runningClearance + knobThickness / 2;
|
|
52083
|
+
const knob = cylinderAlongX(knobThickness, knobDiameter / 2, knobCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52084
|
+
const shaftLeftX = pressurePadRightFaceX - Math.min(pressurePadThickness * 0.45, screwDiameter * 0.45);
|
|
52085
|
+
const shaftRightX = knobCenterX + knobThickness / 2;
|
|
52086
|
+
const shaftLength = shaftRightX - shaftLeftX;
|
|
52087
|
+
if (shaftLength <= supportThickness + bossLength) {
|
|
52088
|
+
throw new Error("thumbScrewClampAssembly: generated screw length is too short for the threaded support");
|
|
52089
|
+
}
|
|
52090
|
+
const shaft = cylinderAlongX(shaftLength, screwDiameter / 2, (shaftLeftX + shaftRightX) / 2, segments).translate(0, 0, screwCenterZ);
|
|
52091
|
+
const clampScrew = union(shaft, pressurePad, knob).color("#cbd5e1");
|
|
52092
|
+
const workpieceEnvelope = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ);
|
|
52093
|
+
return {
|
|
52094
|
+
parts: [
|
|
52095
|
+
{ name: "thumb-screw clamp frame with fixed anvil and threaded boss", shape: frame },
|
|
52096
|
+
{ name: "representative clamped workpiece between pads", shape: workpiece },
|
|
52097
|
+
{ name: "installed thumb screw with captive pressure pad and hand knob", shape: clampScrew }
|
|
52098
|
+
],
|
|
52099
|
+
frame,
|
|
52100
|
+
workpiece,
|
|
52101
|
+
clampScrew,
|
|
52102
|
+
cutters: {
|
|
52103
|
+
threadedBossBore,
|
|
52104
|
+
workpieceEnvelope
|
|
52105
|
+
},
|
|
52106
|
+
dims: {
|
|
52107
|
+
screwSize,
|
|
52108
|
+
screwDiameter,
|
|
52109
|
+
threadEnvelopeDiameter,
|
|
52110
|
+
workpieceThickness,
|
|
52111
|
+
workpieceDepth,
|
|
52112
|
+
workpieceHeight,
|
|
52113
|
+
frameDepth,
|
|
52114
|
+
frameHeight,
|
|
52115
|
+
baseThickness,
|
|
52116
|
+
jawThickness,
|
|
52117
|
+
supportThickness,
|
|
52118
|
+
bossLength,
|
|
52119
|
+
bossDiameter,
|
|
52120
|
+
exposedScrewLength,
|
|
52121
|
+
pressurePadDiameter,
|
|
52122
|
+
pressurePadThickness,
|
|
52123
|
+
knobDiameter,
|
|
52124
|
+
knobThickness,
|
|
52125
|
+
screwCenterZ,
|
|
52126
|
+
fixedAnvilFaceX: workpieceLeftFaceX - faceClearance,
|
|
52127
|
+
pressurePadFaceX: workpieceRightFaceX + faceClearance,
|
|
52128
|
+
supportInnerFaceX,
|
|
52129
|
+
runningClearance,
|
|
52130
|
+
faceClearance
|
|
52131
|
+
}
|
|
52132
|
+
};
|
|
52133
|
+
}
|
|
50166
52134
|
function fastenerSet(size, boltLength, options) {
|
|
50167
52135
|
const sizeData = METRIC_HOLE_TABLE[size];
|
|
50168
52136
|
if (!sizeData) throw new Error(`fastenerSet: unsupported size "${size}"`);
|
|
@@ -50223,6 +52191,22 @@ const partLibrary = {
|
|
|
50223
52191
|
nut,
|
|
50224
52192
|
washer,
|
|
50225
52193
|
fastenerSet,
|
|
52194
|
+
boltedServiceCover,
|
|
52195
|
+
datumEnclosureAssembly,
|
|
52196
|
+
snapLatchCoverAssembly,
|
|
52197
|
+
pinnedLeverAssembly,
|
|
52198
|
+
retainedShaftAssembly,
|
|
52199
|
+
capturedLinearSlide,
|
|
52200
|
+
capturedCartridgeGuideAssembly,
|
|
52201
|
+
livingHingeCoverAssembly,
|
|
52202
|
+
knuckledHingeAssembly,
|
|
52203
|
+
clevisPinJointAssembly,
|
|
52204
|
+
seatedBearingAssembly,
|
|
52205
|
+
cableGlandAnchorAssembly,
|
|
52206
|
+
hoseBarbPortAssembly,
|
|
52207
|
+
routedTubeClipAssembly,
|
|
52208
|
+
pcbTerminalBlockAssembly,
|
|
52209
|
+
thumbScrewClampAssembly,
|
|
50226
52210
|
pipeRoute,
|
|
50227
52211
|
elbow,
|
|
50228
52212
|
beltDrive,
|
|
@@ -58763,7 +60747,7 @@ function requireFinite$7(value, label) {
|
|
|
58763
60747
|
}
|
|
58764
60748
|
return value;
|
|
58765
60749
|
}
|
|
58766
|
-
function requireVec3$
|
|
60750
|
+
function requireVec3$3(value, label) {
|
|
58767
60751
|
if (!Array.isArray(value) || value.length !== 3) {
|
|
58768
60752
|
throw new Error(`${label} must be [x, y, z]`);
|
|
58769
60753
|
}
|
|
@@ -58807,7 +60791,7 @@ function normalizeOptions(options) {
|
|
|
58807
60791
|
out.size = requireFinite$7(options.size, "Viewport.label options.size");
|
|
58808
60792
|
if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
|
|
58809
60793
|
}
|
|
58810
|
-
if (options.offset !== void 0) out.offset = requireVec3$
|
|
60794
|
+
if (options.offset !== void 0) out.offset = requireVec3$3(options.offset, "Viewport.label options.offset");
|
|
58811
60795
|
if (options.anchor !== void 0) {
|
|
58812
60796
|
if (!VALID_ANCHORS.has(options.anchor)) {
|
|
58813
60797
|
throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
|
|
@@ -58824,7 +60808,7 @@ function collectRenderLabel(text, at, options) {
|
|
|
58824
60808
|
if (typeof text !== "string" || text.trim().length === 0) {
|
|
58825
60809
|
throw new Error("Viewport.label text must be a non-empty string");
|
|
58826
60810
|
}
|
|
58827
|
-
const normalizedAt = requireVec3$
|
|
60811
|
+
const normalizedAt = requireVec3$3(at, "Viewport.label at");
|
|
58828
60812
|
const normalizedOptions = normalizeOptions(options);
|
|
58829
60813
|
_collected$4.push({
|
|
58830
60814
|
id: `render-label-${_nextId++}`,
|
|
@@ -59019,7 +61003,7 @@ function requireFinite$6(value, label) {
|
|
|
59019
61003
|
}
|
|
59020
61004
|
return value;
|
|
59021
61005
|
}
|
|
59022
|
-
function requireVec3$
|
|
61006
|
+
function requireVec3$2(value, label) {
|
|
59023
61007
|
if (!Array.isArray(value) || value.length !== 3) {
|
|
59024
61008
|
throw new Error(`${label} must be [x, y, z]`);
|
|
59025
61009
|
}
|
|
@@ -59047,9 +61031,9 @@ const VALID_ENVIRONMENT_PRESETS = /* @__PURE__ */ new Set([
|
|
|
59047
61031
|
]);
|
|
59048
61032
|
function validateCamera(cam, label) {
|
|
59049
61033
|
const out = {};
|
|
59050
|
-
if (cam.position !== void 0) out.position = requireVec3$
|
|
59051
|
-
if (cam.target !== void 0) out.target = requireVec3$
|
|
59052
|
-
if (cam.up !== void 0) out.up = requireVec3$
|
|
61034
|
+
if (cam.position !== void 0) out.position = requireVec3$2(cam.position, `${label}.position`);
|
|
61035
|
+
if (cam.target !== void 0) out.target = requireVec3$2(cam.target, `${label}.target`);
|
|
61036
|
+
if (cam.up !== void 0) out.up = requireVec3$2(cam.up, `${label}.up`);
|
|
59053
61037
|
if (cam.fov !== void 0) {
|
|
59054
61038
|
out.fov = requireFinite$6(cam.fov, `${label}.fov`);
|
|
59055
61039
|
if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
|
|
@@ -59184,8 +61168,8 @@ function validateLight(light, label) {
|
|
|
59184
61168
|
const out = { type: light.type };
|
|
59185
61169
|
if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
|
|
59186
61170
|
if (light.intensity !== void 0) out.intensity = requireFinite$6(light.intensity, `${label}.intensity`);
|
|
59187
|
-
if (light.position !== void 0) out.position = requireVec3$
|
|
59188
|
-
if (light.target !== void 0) out.target = requireVec3$
|
|
61171
|
+
if (light.position !== void 0) out.position = requireVec3$2(light.position, `${label}.position`);
|
|
61172
|
+
if (light.target !== void 0) out.target = requireVec3$2(light.target, `${label}.target`);
|
|
59189
61173
|
if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
|
|
59190
61174
|
if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
|
|
59191
61175
|
if (light.angle !== void 0) out.angle = requireFinite$6(light.angle, `${label}.angle`);
|
|
@@ -60717,7 +62701,7 @@ function scale$1(v, s) {
|
|
|
60717
62701
|
function dot$2(a2, b) {
|
|
60718
62702
|
return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
|
|
60719
62703
|
}
|
|
60720
|
-
function lerp$
|
|
62704
|
+
function lerp$4(a2, b, t) {
|
|
60721
62705
|
return a2 + (b - a2) * t;
|
|
60722
62706
|
}
|
|
60723
62707
|
function frameMatrix$1(x2, y2, z2, p2) {
|
|
@@ -60728,7 +62712,7 @@ function axisVector(axis, sign2 = 1) {
|
|
|
60728
62712
|
if (axis === "Y") return [0, sign2, 0];
|
|
60729
62713
|
return [0, 0, sign2];
|
|
60730
62714
|
}
|
|
60731
|
-
function axisPosition(axis, point2) {
|
|
62715
|
+
function axisPosition$1(axis, point2) {
|
|
60732
62716
|
return point2[AXIS_INDEX[axis]];
|
|
60733
62717
|
}
|
|
60734
62718
|
function crossPointForStation(axis, point2) {
|
|
@@ -60736,7 +62720,7 @@ function crossPointForStation(axis, point2) {
|
|
|
60736
62720
|
if (axis === "Y") return [point2[0], -point2[2]];
|
|
60737
62721
|
return [point2[1], point2[2]];
|
|
60738
62722
|
}
|
|
60739
|
-
function orientLoftToAxis(shape, axis) {
|
|
62723
|
+
function orientLoftToAxis$1(shape, axis) {
|
|
60740
62724
|
if (axis === "Z") return shape;
|
|
60741
62725
|
if (axis === "Y") return shape.rotateX(-90);
|
|
60742
62726
|
return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
|
|
@@ -60793,9 +62777,9 @@ function interpolateQuery(a2, b, t) {
|
|
|
60793
62777
|
}
|
|
60794
62778
|
return {
|
|
60795
62779
|
side: sideA,
|
|
60796
|
-
u: lerp$
|
|
60797
|
-
v: lerp$
|
|
60798
|
-
offset: lerp$
|
|
62780
|
+
u: lerp$4(a2.u ?? 0.5, b.u ?? 0.5, t),
|
|
62781
|
+
v: lerp$4(a2.v ?? 0.5, b.v ?? 0.5, t),
|
|
62782
|
+
offset: lerp$4(a2.offset ?? 0, b.offset ?? 0, t)
|
|
60799
62783
|
};
|
|
60800
62784
|
}
|
|
60801
62785
|
function resolvePathQueries(points) {
|
|
@@ -60862,8 +62846,8 @@ class ProductSkin {
|
|
|
60862
62846
|
this.stations = stations;
|
|
60863
62847
|
this.rails = rails;
|
|
60864
62848
|
for (const [name2, query] of Object.entries(refs)) this.refQueries.set(name2, cloneQuery(query));
|
|
60865
|
-
this.axisMin = Math.min(...stations.map((station) => axisPosition(axis, station.center)));
|
|
60866
|
-
this.axisMax = Math.max(...stations.map((station) => axisPosition(axis, station.center)));
|
|
62849
|
+
this.axisMin = Math.min(...stations.map((station) => axisPosition$1(axis, station.center)));
|
|
62850
|
+
this.axisMax = Math.max(...stations.map((station) => axisPosition$1(axis, station.center)));
|
|
60867
62851
|
this.diagnosticsValue = {
|
|
60868
62852
|
...diagnostics,
|
|
60869
62853
|
stationNames: stations.map((station) => station.name),
|
|
@@ -60920,24 +62904,24 @@ class ProductSkin {
|
|
|
60920
62904
|
}
|
|
60921
62905
|
/** Interpolate center, width, and depth at a normalized v or absolute axis value. */
|
|
60922
62906
|
stationAt(vOrAxis) {
|
|
60923
|
-
const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$
|
|
62907
|
+
const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$4(this.axisMin, this.axisMax, vOrAxis) : clamp$6(vOrAxis, this.axisMin, this.axisMax);
|
|
60924
62908
|
const sorted = this.stations;
|
|
60925
62909
|
for (let index2 = 0; index2 < sorted.length - 1; index2 += 1) {
|
|
60926
62910
|
const a2 = sorted[index2];
|
|
60927
62911
|
const b = sorted[index2 + 1];
|
|
60928
|
-
const aAxis = axisPosition(this.axis, a2.center);
|
|
60929
|
-
const bAxis = axisPosition(this.axis, b.center);
|
|
62912
|
+
const aAxis = axisPosition$1(this.axis, a2.center);
|
|
62913
|
+
const bAxis = axisPosition$1(this.axis, b.center);
|
|
60930
62914
|
if (axisValue < aAxis - EPS$5 || axisValue > bAxis + EPS$5) continue;
|
|
60931
62915
|
const span = Math.max(EPS$5, bAxis - aAxis);
|
|
60932
62916
|
const t = clamp$6((axisValue - aAxis) / span, 0, 1);
|
|
60933
62917
|
return {
|
|
60934
62918
|
axisValue,
|
|
60935
|
-
center: [lerp$
|
|
60936
|
-
width: lerp$
|
|
60937
|
-
depth: lerp$
|
|
62919
|
+
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)],
|
|
62920
|
+
width: lerp$4(a2.profile.width, b.profile.width, t),
|
|
62921
|
+
depth: lerp$4(a2.profile.depth, b.profile.depth, t),
|
|
60938
62922
|
dWidth: (b.profile.width - a2.profile.width) / span,
|
|
60939
62923
|
dDepth: (b.profile.depth - a2.profile.depth) / span,
|
|
60940
|
-
exponent: lerp$
|
|
62924
|
+
exponent: lerp$4(profileExponent(a2), profileExponent(b), t),
|
|
60941
62925
|
kind: a2.profile.kind === b.profile.kind ? a2.profile.kind : "custom"
|
|
60942
62926
|
};
|
|
60943
62927
|
}
|
|
@@ -61059,7 +63043,7 @@ class ProductSkinBuilder {
|
|
|
61059
63043
|
}
|
|
61060
63044
|
/** Set named cross-section stations for the product skin. */
|
|
61061
63045
|
stations(stations) {
|
|
61062
|
-
this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition(this.axisValue, a2.center) - axisPosition(this.axisValue, b.center));
|
|
63046
|
+
this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition$1(this.axisValue, a2.center) - axisPosition$1(this.axisValue, b.center));
|
|
61063
63047
|
return this;
|
|
61064
63048
|
}
|
|
61065
63049
|
/** Attach named guide rails for product-skin construction and downstream surface references. */
|
|
@@ -61109,9 +63093,9 @@ class ProductSkinBuilder {
|
|
|
61109
63093
|
const [x2, y2] = crossPointForStation(this.axisValue, station.center);
|
|
61110
63094
|
return station.profile.sketch.translate(x2, y2);
|
|
61111
63095
|
});
|
|
61112
|
-
const heights = this.stationsValue.map((station) => axisPosition(this.axisValue, station.center));
|
|
63096
|
+
const heights = this.stationsValue.map((station) => axisPosition$1(this.axisValue, station.center));
|
|
61113
63097
|
let shape = loft(localProfiles, heights, { edgeLength: this.edgeLengthValue });
|
|
61114
|
-
shape = orientLoftToAxis(shape, this.axisValue);
|
|
63098
|
+
shape = orientLoftToAxis$1(shape, this.axisValue);
|
|
61115
63099
|
if (this.colorValue) shape = shape.color(this.colorValue);
|
|
61116
63100
|
shape = applyMaterial(shape, this.materialValue).as(this.name);
|
|
61117
63101
|
const warnings = [];
|
|
@@ -61770,7 +63754,7 @@ function requirePositive$3(value, label) {
|
|
|
61770
63754
|
function clamp$5(value, min2, max2) {
|
|
61771
63755
|
return Math.max(min2, Math.min(max2, value));
|
|
61772
63756
|
}
|
|
61773
|
-
function lerp$
|
|
63757
|
+
function lerp$3(a2, b, t) {
|
|
61774
63758
|
return a2 + (b - a2) * t;
|
|
61775
63759
|
}
|
|
61776
63760
|
function add(a2, b) {
|
|
@@ -61820,19 +63804,19 @@ function transformLocal(point2, tangentAcross, normal, tangentAlong, x2, y2, z2
|
|
|
61820
63804
|
function interpolateCylinder(a2, b, t, mode) {
|
|
61821
63805
|
let delta = b.angle - a2.angle;
|
|
61822
63806
|
if (mode === "shortest" && Math.abs(delta) > 180) delta -= Math.sign(delta) * 360;
|
|
61823
|
-
return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$
|
|
63807
|
+
return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$3(a2.z, b.z, t), offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t) };
|
|
61824
63808
|
}
|
|
61825
63809
|
function interpolatePlane(a2, b, t) {
|
|
61826
|
-
return { kind: "plane", x: lerp$
|
|
63810
|
+
return { kind: "plane", x: lerp$3(a2.x, b.x, t), y: lerp$3(a2.y, b.y, t), offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t) };
|
|
61827
63811
|
}
|
|
61828
63812
|
function interpolateProductSkin(a2, b, t) {
|
|
61829
63813
|
if ((a2.side ?? b.side) !== (b.side ?? a2.side)) throw new Error("SurfacePath on ProductSkin currently supports one side per path; split side transitions into separate members.");
|
|
61830
63814
|
return {
|
|
61831
63815
|
kind: "productSkin",
|
|
61832
63816
|
side: a2.side ?? b.side,
|
|
61833
|
-
u: lerp$
|
|
61834
|
-
v: lerp$
|
|
61835
|
-
offset: lerp$
|
|
63817
|
+
u: lerp$3(a2.u ?? 0.5, b.u ?? 0.5, t),
|
|
63818
|
+
v: lerp$3(a2.v ?? 0.5, b.v ?? 0.5, t),
|
|
63819
|
+
offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t)
|
|
61836
63820
|
};
|
|
61837
63821
|
}
|
|
61838
63822
|
class SurfacePath {
|
|
@@ -62155,11 +64139,11 @@ function coordinateOnSide(coordinate, side, label) {
|
|
|
62155
64139
|
return { ...coordinate, kind: "productSkin", side };
|
|
62156
64140
|
}
|
|
62157
64141
|
class ProductSkinCarrier {
|
|
62158
|
-
constructor(skin, name = skin.name,
|
|
64142
|
+
constructor(skin, name = skin.name, sideValue2, offsetValue = 0) {
|
|
62159
64143
|
__publicField(this, "kind", "productSkin");
|
|
62160
64144
|
this.skin = skin;
|
|
62161
64145
|
this.name = name;
|
|
62162
|
-
this.sideValue =
|
|
64146
|
+
this.sideValue = sideValue2;
|
|
62163
64147
|
this.offsetValue = offsetValue;
|
|
62164
64148
|
}
|
|
62165
64149
|
surface(side) {
|
|
@@ -62930,7 +64914,7 @@ function counterboresForPlate(spec2, width, height, thickness, diagnostics) {
|
|
|
62930
64914
|
function minWidthAcrossAlongRange(widthAtT, length4, minAlong, maxAlong) {
|
|
62931
64915
|
let minWidth = Number.POSITIVE_INFINITY;
|
|
62932
64916
|
for (let index2 = 0; index2 <= 8; index2 += 1) {
|
|
62933
|
-
const along = lerp$
|
|
64917
|
+
const along = lerp$3(minAlong, maxAlong, index2 / 8);
|
|
62934
64918
|
const t = Math.max(0, Math.min(1, (along + length4 / 2) / Math.max(length4, 1e-8)));
|
|
62935
64919
|
minWidth = Math.min(minWidth, widthAtT(t));
|
|
62936
64920
|
}
|
|
@@ -63230,7 +65214,7 @@ function pathParameterAtDistance(samples, distance2) {
|
|
|
63230
65214
|
const segmentLength = Math.hypot(b.point[0] - a2.point[0], b.point[1] - a2.point[1], b.point[2] - a2.point[2]);
|
|
63231
65215
|
if (traveled + segmentLength >= distance2) {
|
|
63232
65216
|
const localT = segmentLength <= 1e-8 ? 0 : (distance2 - traveled) / segmentLength;
|
|
63233
|
-
return lerp$
|
|
65217
|
+
return lerp$3(a2.t, b.t, localT);
|
|
63234
65218
|
}
|
|
63235
65219
|
traveled += segmentLength;
|
|
63236
65220
|
}
|
|
@@ -63283,7 +65267,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
63283
65267
|
const width = input.widthAt(t);
|
|
63284
65268
|
const along = distance2 - length4 / 2;
|
|
63285
65269
|
for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
|
|
63286
|
-
const across = lerp$
|
|
65270
|
+
const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
|
|
63287
65271
|
mesh.vertices.push(pointAtProfile([across, along], false));
|
|
63288
65272
|
}
|
|
63289
65273
|
}
|
|
@@ -63293,7 +65277,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
63293
65277
|
const width = input.widthAt(t);
|
|
63294
65278
|
const along = distance2 - length4 / 2;
|
|
63295
65279
|
for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
|
|
63296
|
-
const across = lerp$
|
|
65280
|
+
const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
|
|
63297
65281
|
mesh.vertices.push(pointAtProfile([across, along], true));
|
|
63298
65282
|
}
|
|
63299
65283
|
}
|
|
@@ -63305,7 +65289,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
63305
65289
|
const width = input.widthAt(t);
|
|
63306
65290
|
const along = distance2 - length4 / 2;
|
|
63307
65291
|
for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
|
|
63308
|
-
const across = lerp$
|
|
65292
|
+
const across = lerp$3(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
|
|
63309
65293
|
filled[alongIndex][acrossIndex] = !holes.some((hole2) => pointInProfileLoop([across, along], hole2));
|
|
63310
65294
|
}
|
|
63311
65295
|
}
|
|
@@ -67201,7 +69185,7 @@ const Constraint = {
|
|
|
67201
69185
|
return builder.constrain({ type: "length", line: resolveLineId(builder, line2), value });
|
|
67202
69186
|
}
|
|
67203
69187
|
};
|
|
67204
|
-
function requireVec3(v, label) {
|
|
69188
|
+
function requireVec3$1(v, label) {
|
|
67205
69189
|
if (!Array.isArray(v) || v.length !== 3 || !Number.isFinite(v[0]) || !Number.isFinite(v[1]) || !Number.isFinite(v[2])) {
|
|
67206
69190
|
throw new Error(`${label} must be a [number, number, number] with finite values, got ${JSON.stringify(v)}`);
|
|
67207
69191
|
}
|
|
@@ -67214,24 +69198,24 @@ function requireFiniteNumber(n, label) {
|
|
|
67214
69198
|
return n;
|
|
67215
69199
|
}
|
|
67216
69200
|
function distance$1(a2, b) {
|
|
67217
|
-
requireVec3(a2, "a");
|
|
67218
|
-
requireVec3(b, "b");
|
|
69201
|
+
requireVec3$1(a2, "a");
|
|
69202
|
+
requireVec3$1(b, "b");
|
|
67219
69203
|
return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
|
|
67220
69204
|
}
|
|
67221
69205
|
function midpoint$1(a2, b) {
|
|
67222
|
-
requireVec3(a2, "a");
|
|
67223
|
-
requireVec3(b, "b");
|
|
69206
|
+
requireVec3$1(a2, "a");
|
|
69207
|
+
requireVec3$1(b, "b");
|
|
67224
69208
|
return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
|
|
67225
69209
|
}
|
|
67226
|
-
function lerp(a2, b, t) {
|
|
67227
|
-
requireVec3(a2, "a");
|
|
67228
|
-
requireVec3(b, "b");
|
|
69210
|
+
function lerp$2(a2, b, t) {
|
|
69211
|
+
requireVec3$1(a2, "a");
|
|
69212
|
+
requireVec3$1(b, "b");
|
|
67229
69213
|
requireFiniteNumber(t, "t");
|
|
67230
69214
|
return [a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t];
|
|
67231
69215
|
}
|
|
67232
69216
|
function direction(a2, b) {
|
|
67233
|
-
requireVec3(a2, "a");
|
|
67234
|
-
requireVec3(b, "b");
|
|
69217
|
+
requireVec3$1(a2, "a");
|
|
69218
|
+
requireVec3$1(b, "b");
|
|
67235
69219
|
const dx = b[0] - a2[0];
|
|
67236
69220
|
const dy = b[1] - a2[1];
|
|
67237
69221
|
const dz = b[2] - a2[2];
|
|
@@ -67242,8 +69226,8 @@ function direction(a2, b) {
|
|
|
67242
69226
|
return [dx / len2, dy / len2, dz / len2];
|
|
67243
69227
|
}
|
|
67244
69228
|
function offset(point2, dir, amount) {
|
|
67245
|
-
requireVec3(point2, "point");
|
|
67246
|
-
requireVec3(dir, "dir");
|
|
69229
|
+
requireVec3$1(point2, "point");
|
|
69230
|
+
requireVec3$1(dir, "dir");
|
|
67247
69231
|
requireFiniteNumber(amount, "amount");
|
|
67248
69232
|
return [point2[0] + dir[0] * amount, point2[1] + dir[1] * amount, point2[2] + dir[2] * amount];
|
|
67249
69233
|
}
|
|
@@ -67253,7 +69237,7 @@ const Points = {
|
|
|
67253
69237
|
/** Center point between two 3D points. */
|
|
67254
69238
|
midpoint: midpoint$1,
|
|
67255
69239
|
/** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */
|
|
67256
|
-
lerp,
|
|
69240
|
+
lerp: lerp$2,
|
|
67257
69241
|
/** Unit direction vector from a to b. Throws if a and b are the same point. */
|
|
67258
69242
|
direction,
|
|
67259
69243
|
/** Move a point along a direction vector by a given amount. */
|
|
@@ -72385,9 +74369,84 @@ class ConstraintSketch extends Sketch {
|
|
|
72385
74369
|
* Select the single arrangement region that contains the given seed point.
|
|
72386
74370
|
* Throws if no region contains the seed.
|
|
72387
74371
|
*/
|
|
72388
|
-
detectArrangementRegion(
|
|
74372
|
+
detectArrangementRegion(_seed) {
|
|
72389
74373
|
throw new Error("Not implemented");
|
|
72390
74374
|
}
|
|
74375
|
+
/**
|
|
74376
|
+
* Return the solved constrained path as a sampled 2D polyline.
|
|
74377
|
+
*
|
|
74378
|
+
* Use this when a construction rail was authored with `constrainedSketch()`
|
|
74379
|
+
* and should feed another operation such as `Loft.pathOnXz(...)`.
|
|
74380
|
+
* The sketch must contain exactly one profile path.
|
|
74381
|
+
*
|
|
74382
|
+
* @param samples - Samples per curved segment. Default 32.
|
|
74383
|
+
* @returns The solved path as an open polyline.
|
|
74384
|
+
*/
|
|
74385
|
+
toPolyline(samples = 32) {
|
|
74386
|
+
if (!Number.isFinite(samples) || samples < 2) throw new Error("ConstraintSketch.toPolyline() samples must be at least 2");
|
|
74387
|
+
const profileLoops = this.definition.loops.filter((loop) => loop.type === "profile");
|
|
74388
|
+
if (profileLoops.length !== 1) {
|
|
74389
|
+
throw new Error("ConstraintSketch.toPolyline() requires exactly one profile path");
|
|
74390
|
+
}
|
|
74391
|
+
const sampleCount = Math.max(2, Math.round(samples));
|
|
74392
|
+
const pointMap = new Map(this.definition.points.map((point2) => [point2.id, point2]));
|
|
74393
|
+
const lineMap = new Map(this.definition.lines.map((line2) => [line2.id, line2]));
|
|
74394
|
+
const arcMap = new Map(this.definition.arcs.map((arc) => [arc.id, arc]));
|
|
74395
|
+
const bezierMap = new Map(this.definition.beziers.map((bezier) => [bezier.id, bezier]));
|
|
74396
|
+
const points = [];
|
|
74397
|
+
const appendStart = (point2, label) => {
|
|
74398
|
+
const previous = points[points.length - 1];
|
|
74399
|
+
if (!previous) {
|
|
74400
|
+
points.push(point2);
|
|
74401
|
+
return;
|
|
74402
|
+
}
|
|
74403
|
+
if (Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-6) {
|
|
74404
|
+
throw new Error(`ConstraintSketch.toPolyline() profile path is not continuous at ${label}`);
|
|
74405
|
+
}
|
|
74406
|
+
};
|
|
74407
|
+
const appendPoint = (point2) => {
|
|
74408
|
+
const previous = points[points.length - 1];
|
|
74409
|
+
if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) points.push(point2);
|
|
74410
|
+
};
|
|
74411
|
+
const requirePoint = (id, label) => {
|
|
74412
|
+
const point2 = pointMap.get(id);
|
|
74413
|
+
if (!point2) throw new Error(`ConstraintSketch.toPolyline() missing ${label}`);
|
|
74414
|
+
return [point2.x, point2.y];
|
|
74415
|
+
};
|
|
74416
|
+
for (const segment of profileLoops[0].segments) {
|
|
74417
|
+
if (segment.kind === "line") {
|
|
74418
|
+
const line2 = lineMap.get(segment.line);
|
|
74419
|
+
if (!line2) throw new Error(`ConstraintSketch.toPolyline() missing line "${segment.line}"`);
|
|
74420
|
+
appendStart(requirePoint(line2.a, `line "${segment.line}" start point`), `line "${segment.line}"`);
|
|
74421
|
+
appendPoint(requirePoint(line2.b, `line "${segment.line}" end point`));
|
|
74422
|
+
} else if (segment.kind === "arc") {
|
|
74423
|
+
const arc = arcMap.get(segment.arc);
|
|
74424
|
+
if (!arc) throw new Error(`ConstraintSketch.toPolyline() missing arc "${segment.arc}"`);
|
|
74425
|
+
const center = requirePoint(arc.center, `arc "${segment.arc}" center point`);
|
|
74426
|
+
const start = requirePoint(arc.start, `arc "${segment.arc}" start point`);
|
|
74427
|
+
const end = requirePoint(arc.end, `arc "${segment.arc}" end point`);
|
|
74428
|
+
appendStart(start, `arc "${segment.arc}"`);
|
|
74429
|
+
const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
|
|
74430
|
+
const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
|
|
74431
|
+
for (const point2 of tessellateArc(center[0], center[1], arc.radius, startAngle, endAngle, arc.clockwise, sampleCount)) {
|
|
74432
|
+
appendPoint(point2);
|
|
74433
|
+
}
|
|
74434
|
+
} else {
|
|
74435
|
+
const bezier = bezierMap.get(segment.bezier);
|
|
74436
|
+
if (!bezier) throw new Error(`ConstraintSketch.toPolyline() missing bezier "${segment.bezier}"`);
|
|
74437
|
+
const p0 = requirePoint(bezier.p0, `bezier "${segment.bezier}" start point`);
|
|
74438
|
+
const p1 = requirePoint(bezier.p1, `bezier "${segment.bezier}" first control point`);
|
|
74439
|
+
const p2 = requirePoint(bezier.p2, `bezier "${segment.bezier}" second control point`);
|
|
74440
|
+
const p3 = requirePoint(bezier.p3, `bezier "${segment.bezier}" end point`);
|
|
74441
|
+
appendStart(p0, `bezier "${segment.bezier}"`);
|
|
74442
|
+
for (const point2 of tessellateBezier(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], p3[0], p3[1], sampleCount)) {
|
|
74443
|
+
appendPoint(point2);
|
|
74444
|
+
}
|
|
74445
|
+
}
|
|
74446
|
+
}
|
|
74447
|
+
if (points.length < 2) throw new Error("ConstraintSketch.toPolyline() needs at least 2 points");
|
|
74448
|
+
return points;
|
|
74449
|
+
}
|
|
72391
74450
|
/**
|
|
72392
74451
|
* Re-solve the sketch after changing the value of one existing constraint.
|
|
72393
74452
|
*
|
|
@@ -87672,6 +89731,295 @@ function polygonVertices(sides, radius, options) {
|
|
|
87672
89731
|
centerY: options == null ? void 0 : options.centerY
|
|
87673
89732
|
});
|
|
87674
89733
|
}
|
|
89734
|
+
const LOFT_GUIDE_EPS = 1e-8;
|
|
89735
|
+
function orientLoftToAxis(shape, axis) {
|
|
89736
|
+
if (axis === "Z") return shape;
|
|
89737
|
+
if (axis === "Y") return shape.rotateX(-90);
|
|
89738
|
+
return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
|
|
89739
|
+
}
|
|
89740
|
+
function buildRailEvaluators(rails, axis, start, end, railSamples) {
|
|
89741
|
+
const seen = /* @__PURE__ */ new Set();
|
|
89742
|
+
return rails.map((rail2) => {
|
|
89743
|
+
if (seen.has(rail2.side)) throw new Error(`Loft.withGuideRails() received more than one ${rail2.side} rail`);
|
|
89744
|
+
seen.add(rail2.side);
|
|
89745
|
+
const sampled = sampleRailPath(rail2.path, railSamples);
|
|
89746
|
+
if (sampled.length < 2) throw new Error("Loft guide rails require at least two points");
|
|
89747
|
+
const points = sampled.map((point2) => ({ position: axisPosition(axis, point2), cross: crossPointForAxis(axis, point2) }));
|
|
89748
|
+
const ordered = points[points.length - 1].position >= points[0].position ? points : [...points].reverse();
|
|
89749
|
+
validateRailCoverage(ordered, start, end);
|
|
89750
|
+
return { side: rail2.side, points: ordered };
|
|
89751
|
+
});
|
|
89752
|
+
}
|
|
89753
|
+
function railCrossAt(rail2, position) {
|
|
89754
|
+
const points = rail2.points;
|
|
89755
|
+
if (position <= points[0].position + LOFT_GUIDE_EPS) return points[0].cross;
|
|
89756
|
+
const last = points[points.length - 1];
|
|
89757
|
+
if (position >= last.position - LOFT_GUIDE_EPS) return last.cross;
|
|
89758
|
+
for (let index2 = 0; index2 < points.length - 1; index2 += 1) {
|
|
89759
|
+
const a2 = points[index2];
|
|
89760
|
+
const b = points[index2 + 1];
|
|
89761
|
+
if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
|
|
89762
|
+
const t = (position - a2.position) / (b.position - a2.position);
|
|
89763
|
+
return [lerp$1(a2.cross[0], b.cross[0], t), lerp$1(a2.cross[1], b.cross[1], t)];
|
|
89764
|
+
}
|
|
89765
|
+
}
|
|
89766
|
+
throw new Error("Loft guide rail does not cover requested station position");
|
|
89767
|
+
}
|
|
89768
|
+
function validateRailCoverage(points, start, end) {
|
|
89769
|
+
for (let index2 = 1; index2 < points.length; index2 += 1) {
|
|
89770
|
+
if (points[index2].position - points[index2 - 1].position < LOFT_GUIDE_EPS) {
|
|
89771
|
+
throw new Error("Loft guide rails must be monotone along the loft axis");
|
|
89772
|
+
}
|
|
89773
|
+
}
|
|
89774
|
+
if (points[0].position - start > LOFT_GUIDE_EPS || end - points[points.length - 1].position > LOFT_GUIDE_EPS) {
|
|
89775
|
+
throw new Error("Loft guide rails must cover the full station range");
|
|
89776
|
+
}
|
|
89777
|
+
}
|
|
89778
|
+
function sampleRailPath(path2, samples) {
|
|
89779
|
+
if (Array.isArray(path2)) return path2.map((point2, index2) => requireVec3(point2, `Loft guide rail point ${index2}`));
|
|
89780
|
+
if (path2 instanceof Curve3D || path2 instanceof HermiteCurve3D || path2 instanceof QuinticHermiteCurve3D || path2 instanceof NurbsCurve3D) {
|
|
89781
|
+
return path2.sample(Math.max(2, Math.round(samples))).map((point2, index2) => requireVec3(point2, `Loft guide rail sample ${index2}`));
|
|
89782
|
+
}
|
|
89783
|
+
throw new Error("Loft guide rail path must be a Vec3[] or ForgeCAD 3D curve");
|
|
89784
|
+
}
|
|
89785
|
+
function requireVec3(point2, label) {
|
|
89786
|
+
if (!Array.isArray(point2) || point2.length !== 3 || !point2.every(Number.isFinite)) {
|
|
89787
|
+
throw new Error(`${label} must be a finite [x, y, z] point`);
|
|
89788
|
+
}
|
|
89789
|
+
return [point2[0], point2[1], point2[2]];
|
|
89790
|
+
}
|
|
89791
|
+
function axisPosition(axis, point2) {
|
|
89792
|
+
if (axis === "X") return point2[0];
|
|
89793
|
+
if (axis === "Y") return point2[1];
|
|
89794
|
+
return point2[2];
|
|
89795
|
+
}
|
|
89796
|
+
function crossPointForAxis(axis, point2) {
|
|
89797
|
+
if (axis === "X") return [point2[1], point2[2]];
|
|
89798
|
+
if (axis === "Y") return [point2[0], -point2[2]];
|
|
89799
|
+
return [point2[0], point2[1]];
|
|
89800
|
+
}
|
|
89801
|
+
function lerp$1(a2, b, t) {
|
|
89802
|
+
return a2 + (b - a2) * t;
|
|
89803
|
+
}
|
|
89804
|
+
function loftWithGuideRails(stations, rails, options = {}) {
|
|
89805
|
+
if (stations.length < 2) throw new Error("Loft.withGuideRails() requires at least two stations");
|
|
89806
|
+
if (rails.length === 0) throw new Error("Loft.withGuideRails() requires at least one guide rail");
|
|
89807
|
+
const sortedStations = sortedValidStations(stations);
|
|
89808
|
+
const axis = options.axis ?? "Z";
|
|
89809
|
+
const start = sortedStations[0].position;
|
|
89810
|
+
const end = sortedStations[sortedStations.length - 1].position;
|
|
89811
|
+
const railEvaluators = buildRailEvaluators(rails, axis, start, end, options.railSamples ?? 64);
|
|
89812
|
+
const positions = generatedPositions(sortedStations, options.samples);
|
|
89813
|
+
const profiles2 = positions.map((position) => {
|
|
89814
|
+
const source = profileForPosition(sortedStations, position);
|
|
89815
|
+
const bounds = boundsForPosition(sortedStations, position);
|
|
89816
|
+
return fitProfileToBounds(source, applyRailsToBounds(bounds, railEvaluators, position));
|
|
89817
|
+
});
|
|
89818
|
+
const shape = loft(profiles2, positions, {
|
|
89819
|
+
edgeLength: options.edgeLength,
|
|
89820
|
+
boundsPadding: options.boundsPadding
|
|
89821
|
+
});
|
|
89822
|
+
return orientLoftToAxis(shape, axis);
|
|
89823
|
+
}
|
|
89824
|
+
function sortedValidStations(stations) {
|
|
89825
|
+
const sorted = [...stations].sort((a2, b) => a2.position - b.position);
|
|
89826
|
+
for (let index2 = 0; index2 < sorted.length; index2 += 1) {
|
|
89827
|
+
if (!Number.isFinite(sorted[index2].position)) throw new Error("Loft.withGuideRails station position must be finite");
|
|
89828
|
+
if (!(sorted[index2].profile instanceof Sketch)) throw new Error("Loft.withGuideRails() stations must use Sketch profiles");
|
|
89829
|
+
if (index2 > 0 && sorted[index2].position - sorted[index2 - 1].position < LOFT_GUIDE_EPS) {
|
|
89830
|
+
throw new Error("Loft.withGuideRails() requires unique, strictly increasing station positions");
|
|
89831
|
+
}
|
|
89832
|
+
}
|
|
89833
|
+
return sorted;
|
|
89834
|
+
}
|
|
89835
|
+
function generatedPositions(stations, samples) {
|
|
89836
|
+
const count = Math.max(2, Math.round(samples ?? Math.max(9, (stations.length - 1) * 8 + 1)));
|
|
89837
|
+
const start = stations[0].position;
|
|
89838
|
+
const end = stations[stations.length - 1].position;
|
|
89839
|
+
const values = /* @__PURE__ */ new Set();
|
|
89840
|
+
const positions = [];
|
|
89841
|
+
const addPosition = (position) => {
|
|
89842
|
+
const key = position.toFixed(9);
|
|
89843
|
+
if (!values.has(key)) {
|
|
89844
|
+
values.add(key);
|
|
89845
|
+
positions.push(position);
|
|
89846
|
+
}
|
|
89847
|
+
};
|
|
89848
|
+
for (let index2 = 0; index2 < count; index2 += 1) addPosition(start + (end - start) * index2 / (count - 1));
|
|
89849
|
+
for (const station of stations) addPosition(station.position);
|
|
89850
|
+
return positions.sort((a2, b) => a2 - b);
|
|
89851
|
+
}
|
|
89852
|
+
function profileForPosition(stations, position) {
|
|
89853
|
+
for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
|
|
89854
|
+
if (position <= stations[index2 + 1].position + LOFT_GUIDE_EPS) return stations[index2].profile;
|
|
89855
|
+
}
|
|
89856
|
+
return stations[stations.length - 1].profile;
|
|
89857
|
+
}
|
|
89858
|
+
function boundsForPosition(stations, position) {
|
|
89859
|
+
if (position <= stations[0].position + LOFT_GUIDE_EPS) return sketchBounds(stations[0].profile);
|
|
89860
|
+
const last = stations[stations.length - 1];
|
|
89861
|
+
if (position >= last.position - LOFT_GUIDE_EPS) return sketchBounds(last.profile);
|
|
89862
|
+
for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
|
|
89863
|
+
const a2 = stations[index2];
|
|
89864
|
+
const b = stations[index2 + 1];
|
|
89865
|
+
if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
|
|
89866
|
+
return lerpBounds(sketchBounds(a2.profile), sketchBounds(b.profile), (position - a2.position) / (b.position - a2.position));
|
|
89867
|
+
}
|
|
89868
|
+
}
|
|
89869
|
+
return sketchBounds(last.profile);
|
|
89870
|
+
}
|
|
89871
|
+
function applyRailsToBounds(bounds, rails, position) {
|
|
89872
|
+
const centerRail = rails.find((rail2) => rail2.side === "center");
|
|
89873
|
+
const center = centerRail ? railCrossAt(centerRail, position) : void 0;
|
|
89874
|
+
const next = { ...bounds };
|
|
89875
|
+
applyAxisRail(next, "X", sideValue(rails, "left", position, 0), sideValue(rails, "right", position, 0), center == null ? void 0 : center[0]);
|
|
89876
|
+
applyAxisRail(next, "Y", sideValue(rails, "back", position, 1), sideValue(rails, "front", position, 1), center == null ? void 0 : center[1]);
|
|
89877
|
+
if (next.maxX - next.minX < LOFT_GUIDE_EPS || next.maxY - next.minY < LOFT_GUIDE_EPS) {
|
|
89878
|
+
throw new Error("Loft.withGuideRails() guide rails produced a non-positive section size");
|
|
89879
|
+
}
|
|
89880
|
+
return next;
|
|
89881
|
+
}
|
|
89882
|
+
function sideValue(rails, side, position, crossIndex) {
|
|
89883
|
+
const rail2 = rails.find((entry) => entry.side === side);
|
|
89884
|
+
return rail2 ? railCrossAt(rail2, position)[crossIndex] : void 0;
|
|
89885
|
+
}
|
|
89886
|
+
function applyAxisRail(bounds, axis, minRail, maxRail, center) {
|
|
89887
|
+
const minKey = axis === "X" ? "minX" : "minY";
|
|
89888
|
+
const maxKey = axis === "X" ? "maxX" : "maxY";
|
|
89889
|
+
const width = bounds[maxKey] - bounds[minKey];
|
|
89890
|
+
if (minRail != null && maxRail != null) {
|
|
89891
|
+
if (maxRail - minRail < LOFT_GUIDE_EPS) throw new Error("Loft.withGuideRails() opposite guide rails crossed");
|
|
89892
|
+
if (center != null && Math.abs((minRail + maxRail) / 2 - center) > 1e-5) {
|
|
89893
|
+
throw new Error("Loft.withGuideRails() center rail conflicts with opposite side rails");
|
|
89894
|
+
}
|
|
89895
|
+
bounds[minKey] = minRail;
|
|
89896
|
+
bounds[maxKey] = maxRail;
|
|
89897
|
+
} else if (maxRail != null) {
|
|
89898
|
+
bounds[maxKey] = maxRail;
|
|
89899
|
+
bounds[minKey] = center != null ? 2 * center - maxRail : maxRail - width;
|
|
89900
|
+
} else if (minRail != null) {
|
|
89901
|
+
bounds[minKey] = minRail;
|
|
89902
|
+
bounds[maxKey] = center != null ? 2 * center - minRail : minRail + width;
|
|
89903
|
+
} else if (center != null) {
|
|
89904
|
+
bounds[minKey] = center - width / 2;
|
|
89905
|
+
bounds[maxKey] = center + width / 2;
|
|
89906
|
+
}
|
|
89907
|
+
}
|
|
89908
|
+
function fitProfileToBounds(profile, target) {
|
|
89909
|
+
const source = sketchBounds(profile);
|
|
89910
|
+
const sourceWidth = source.maxX - source.minX;
|
|
89911
|
+
const sourceDepth = source.maxY - source.minY;
|
|
89912
|
+
if (sourceWidth < LOFT_GUIDE_EPS || sourceDepth < LOFT_GUIDE_EPS) {
|
|
89913
|
+
throw new Error("Loft.withGuideRails() station profiles must have positive bounds");
|
|
89914
|
+
}
|
|
89915
|
+
const sourceCenter = [(source.minX + source.maxX) / 2, (source.minY + source.maxY) / 2];
|
|
89916
|
+
const targetCenter = [(target.minX + target.maxX) / 2, (target.minY + target.maxY) / 2];
|
|
89917
|
+
return profile.scaleAround(sourceCenter, [(target.maxX - target.minX) / sourceWidth, (target.maxY - target.minY) / sourceDepth]).translate(targetCenter[0] - sourceCenter[0], targetCenter[1] - sourceCenter[1]);
|
|
89918
|
+
}
|
|
89919
|
+
function sketchBounds(profile) {
|
|
89920
|
+
const bounds = profile.bounds();
|
|
89921
|
+
return { minX: bounds.min[0], maxX: bounds.max[0], minY: bounds.min[1], maxY: bounds.max[1] };
|
|
89922
|
+
}
|
|
89923
|
+
function lerpBounds(a2, b, t) {
|
|
89924
|
+
return {
|
|
89925
|
+
minX: lerp(a2.minX, b.minX, t),
|
|
89926
|
+
maxX: lerp(a2.maxX, b.maxX, t),
|
|
89927
|
+
minY: lerp(a2.minY, b.minY, t),
|
|
89928
|
+
maxY: lerp(a2.maxY, b.maxY, t)
|
|
89929
|
+
};
|
|
89930
|
+
}
|
|
89931
|
+
function lerp(a2, b, t) {
|
|
89932
|
+
return a2 + (b - a2) * t;
|
|
89933
|
+
}
|
|
89934
|
+
function mapLoftPath2D(path2, label, mapper) {
|
|
89935
|
+
const points = sampleLoftPath2D(path2, label);
|
|
89936
|
+
return points.map((point2, index2) => {
|
|
89937
|
+
if (!Array.isArray(point2) || point2.length !== 2 || !point2.every(Number.isFinite)) {
|
|
89938
|
+
throw new Error(`${label} point ${index2} must be a finite [x, y] point`);
|
|
89939
|
+
}
|
|
89940
|
+
return mapper([point2[0], point2[1]]);
|
|
89941
|
+
});
|
|
89942
|
+
}
|
|
89943
|
+
function sampleLoftPath2D(path2, label) {
|
|
89944
|
+
if (Array.isArray(path2)) {
|
|
89945
|
+
if (path2.length < 2) throw new Error(`${label} requires at least two [x, y] points`);
|
|
89946
|
+
return path2;
|
|
89947
|
+
}
|
|
89948
|
+
if (!path2 || typeof path2 !== "object" || typeof path2.toPolyline !== "function") {
|
|
89949
|
+
throw new Error(`${label} requires a 2D path, solved constrained path, or [x, y] point array`);
|
|
89950
|
+
}
|
|
89951
|
+
const points = path2.toPolyline();
|
|
89952
|
+
if (!Array.isArray(points) || points.length < 2) throw new Error(`${label} path must produce at least two [x, y] points`);
|
|
89953
|
+
return points;
|
|
89954
|
+
}
|
|
89955
|
+
const Loft = {
|
|
89956
|
+
/** Create a loft station from a 2D profile and an axis position. */
|
|
89957
|
+
station(profile, position) {
|
|
89958
|
+
if (!Number.isFinite(position)) throw new Error("Loft.station position must be finite");
|
|
89959
|
+
return { profile, position };
|
|
89960
|
+
},
|
|
89961
|
+
/** Create a guide rail that constrains the section-local negative-X side. */
|
|
89962
|
+
leftRail(path2) {
|
|
89963
|
+
return { side: "left", path: path2 };
|
|
89964
|
+
},
|
|
89965
|
+
/** Create a guide rail that constrains the section-local positive-X side. */
|
|
89966
|
+
rightRail(path2) {
|
|
89967
|
+
return { side: "right", path: path2 };
|
|
89968
|
+
},
|
|
89969
|
+
/** Create a guide rail that constrains the section-local positive-Y side. */
|
|
89970
|
+
frontRail(path2) {
|
|
89971
|
+
return { side: "front", path: path2 };
|
|
89972
|
+
},
|
|
89973
|
+
/** Create a guide rail that constrains the section-local negative-Y side. */
|
|
89974
|
+
backRail(path2) {
|
|
89975
|
+
return { side: "back", path: path2 };
|
|
89976
|
+
},
|
|
89977
|
+
/** Create a guide rail that moves section centers along the loft. */
|
|
89978
|
+
centerRail(path2) {
|
|
89979
|
+
return { side: "center", path: path2 };
|
|
89980
|
+
},
|
|
89981
|
+
/**
|
|
89982
|
+
* Place a 2D guide path onto the XZ plane.
|
|
89983
|
+
*
|
|
89984
|
+
* The path's first coordinate becomes X and its second coordinate becomes Z.
|
|
89985
|
+
* Use this for left/right silhouette rails authored with `path()` or `constrainedSketch()`.
|
|
89986
|
+
*/
|
|
89987
|
+
pathOnXz(path2, y2 = 0) {
|
|
89988
|
+
if (!Number.isFinite(y2)) throw new Error("Loft.pathOnXz y must be finite");
|
|
89989
|
+
return mapLoftPath2D(path2, "Loft.pathOnXz", ([x2, z2]) => [x2, y2, z2]);
|
|
89990
|
+
},
|
|
89991
|
+
/**
|
|
89992
|
+
* Place a 2D guide path onto the YZ plane.
|
|
89993
|
+
*
|
|
89994
|
+
* The path's first coordinate becomes Y and its second coordinate becomes Z.
|
|
89995
|
+
* Use this for front/back crown rails authored with `path()` or `constrainedSketch()`.
|
|
89996
|
+
*/
|
|
89997
|
+
pathOnYz(path2, x2 = 0) {
|
|
89998
|
+
if (!Number.isFinite(x2)) throw new Error("Loft.pathOnYz x must be finite");
|
|
89999
|
+
return mapLoftPath2D(path2, "Loft.pathOnYz", ([y2, z2]) => [x2, y2, z2]);
|
|
90000
|
+
},
|
|
90001
|
+
/**
|
|
90002
|
+
* Place a 2D guide path onto the XY plane.
|
|
90003
|
+
*
|
|
90004
|
+
* The path's first coordinate becomes X and its second coordinate becomes Y.
|
|
90005
|
+
* Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
|
|
90006
|
+
*/
|
|
90007
|
+
pathOnXy(path2, z2 = 0) {
|
|
90008
|
+
if (!Number.isFinite(z2)) throw new Error("Loft.pathOnXy z must be finite");
|
|
90009
|
+
return mapLoftPath2D(path2, "Loft.pathOnXy", ([x2, y2]) => [x2, y2, z2]);
|
|
90010
|
+
},
|
|
90011
|
+
/**
|
|
90012
|
+
* Loft through profile stations while forcing generated sections to follow guide rails.
|
|
90013
|
+
*
|
|
90014
|
+
* Stations define the cross-section family. Guide rails define the side or center
|
|
90015
|
+
* paths the loft must pass through. With opposite side rails, the section is scaled
|
|
90016
|
+
* to touch both rails. With one side rail, the section keeps its interpolated size
|
|
90017
|
+
* unless a center rail is also present.
|
|
90018
|
+
*/
|
|
90019
|
+
withGuideRails(stations, rails, options = {}) {
|
|
90020
|
+
return loftWithGuideRails(stations, rails, options);
|
|
90021
|
+
}
|
|
90022
|
+
};
|
|
87675
90023
|
let collectedHighlights = [];
|
|
87676
90024
|
function resetHighlights() {
|
|
87677
90025
|
collectedHighlights = [];
|
|
@@ -92348,10 +94696,14 @@ function spec(name, checkFn) {
|
|
|
92348
94696
|
};
|
|
92349
94697
|
}
|
|
92350
94698
|
let _collected = [];
|
|
94699
|
+
let _collisionAllowances = [];
|
|
94700
|
+
let _physicalComponentExpectations = [];
|
|
92351
94701
|
let _counter = 0;
|
|
92352
94702
|
let _activeGroup = null;
|
|
92353
94703
|
function resetVerifications() {
|
|
92354
94704
|
_collected = [];
|
|
94705
|
+
_collisionAllowances = [];
|
|
94706
|
+
_physicalComponentExpectations = [];
|
|
92355
94707
|
_counter = 0;
|
|
92356
94708
|
}
|
|
92357
94709
|
function getCollectedVerifications() {
|
|
@@ -92385,15 +94737,35 @@ function push(result) {
|
|
|
92385
94737
|
function roundNum(n, digits = 4) {
|
|
92386
94738
|
return Number.isFinite(n) ? n.toFixed(digits).replace(/\.?0+$/, "") : String(n);
|
|
92387
94739
|
}
|
|
94740
|
+
function meshDerivedManifoldBackend(shape) {
|
|
94741
|
+
const mesh = getShapeRuntimeBackend(shape).getMesh();
|
|
94742
|
+
return reconstructBackendFromMesh({
|
|
94743
|
+
numProp: mesh.numProp,
|
|
94744
|
+
triVerts: mesh.triVerts,
|
|
94745
|
+
vertProperties: mesh.vertProperties,
|
|
94746
|
+
mergeFromVert: mesh.mergeFromVert ?? new Uint32Array(),
|
|
94747
|
+
mergeToVert: mesh.mergeToVert ?? new Uint32Array()
|
|
94748
|
+
});
|
|
94749
|
+
}
|
|
94750
|
+
function backendForMinGap(shape) {
|
|
94751
|
+
const backend = getShapeRuntimeBackend(shape);
|
|
94752
|
+
if (isManifoldCapableBackend(backend)) return { backend, method: "exact", dispose: false };
|
|
94753
|
+
return { backend: meshDerivedManifoldBackend(shape), method: "mesh-derived", dispose: true };
|
|
94754
|
+
}
|
|
92388
94755
|
function computeMinGap(a2, b, searchLength) {
|
|
92389
|
-
const backendA =
|
|
92390
|
-
const backendB =
|
|
92391
|
-
|
|
92392
|
-
|
|
94756
|
+
const backendA = backendForMinGap(a2);
|
|
94757
|
+
const backendB = backendForMinGap(b);
|
|
94758
|
+
try {
|
|
94759
|
+
const manifoldA = requireManifoldShapeBackend(backendA.backend, "verification.minGap");
|
|
94760
|
+
const manifoldB = requireManifoldShapeBackend(backendB.backend, "verification.minGap");
|
|
94761
|
+
return {
|
|
94762
|
+
gap: manifoldA.minGap(manifoldB, searchLength),
|
|
94763
|
+
method: backendA.method === "exact" && backendB.method === "exact" ? "exact" : "mesh-derived"
|
|
94764
|
+
};
|
|
94765
|
+
} finally {
|
|
94766
|
+
if (backendA.dispose) disposeShapeBackend(backendA.backend);
|
|
94767
|
+
if (backendB.dispose) disposeShapeBackend(backendB.backend);
|
|
92393
94768
|
}
|
|
92394
|
-
const manifoldA = backendA.requireManifold("verification.minGap");
|
|
92395
|
-
const manifoldB = requireManifoldShapeBackend(backendB, "verification.minGap");
|
|
92396
|
-
return manifoldA.minGap(manifoldB, searchLength);
|
|
92397
94769
|
}
|
|
92398
94770
|
function vec3Dot(a2, b) {
|
|
92399
94771
|
return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
|
|
@@ -92528,9 +94900,144 @@ const verify = {
|
|
|
92528
94900
|
actual: `${roundNum(d2, 3)} mm`
|
|
92529
94901
|
});
|
|
92530
94902
|
} catch (e) {
|
|
92531
|
-
push({
|
|
94903
|
+
push({
|
|
94904
|
+
id: nextId(),
|
|
94905
|
+
label,
|
|
94906
|
+
kind: "interface",
|
|
94907
|
+
status: "fail",
|
|
94908
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
94909
|
+
line: line2
|
|
94910
|
+
});
|
|
92532
94911
|
}
|
|
92533
94912
|
},
|
|
94913
|
+
/**
|
|
94914
|
+
* Check the distance between two named connectors on a shape or group.
|
|
94915
|
+
*
|
|
94916
|
+
* Use this when connectors + `matchTo()` define a static assembly interface.
|
|
94917
|
+
* It proves the mate at runtime, unlike a plain source-level connector
|
|
94918
|
+
* declaration. The common case is `expected = 0`, meaning the two connector
|
|
94919
|
+
* origins should coincide after placement.
|
|
94920
|
+
*
|
|
94921
|
+
* **Example**
|
|
94922
|
+
*
|
|
94923
|
+
* ```ts
|
|
94924
|
+
* verify.connectorDistance("leg is seated", bench, "Rail.leg_0", "Leg0.head", 0, 0.01);
|
|
94925
|
+
* ```
|
|
94926
|
+
*/
|
|
94927
|
+
connectorDistance(label, target, connectorA, connectorB, expected = 0, tolerance = 0.01) {
|
|
94928
|
+
const line2 = captureSourceLine();
|
|
94929
|
+
try {
|
|
94930
|
+
const actual = target.connectorDistance(connectorA, connectorB);
|
|
94931
|
+
const diff = Math.abs(actual - expected);
|
|
94932
|
+
const passed = diff <= Math.abs(tolerance);
|
|
94933
|
+
push({
|
|
94934
|
+
id: nextId(),
|
|
94935
|
+
label,
|
|
94936
|
+
kind: "interface",
|
|
94937
|
+
status: passed ? "pass" : "fail",
|
|
94938
|
+
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`,
|
|
94939
|
+
line: passed ? void 0 : line2,
|
|
94940
|
+
expected: `${roundNum(expected, 4)} ± ${roundNum(tolerance, 4)} mm`,
|
|
94941
|
+
actual: `${roundNum(actual, 4)} mm`
|
|
94942
|
+
});
|
|
94943
|
+
} catch (e) {
|
|
94944
|
+
push({
|
|
94945
|
+
id: nextId(),
|
|
94946
|
+
label,
|
|
94947
|
+
kind: "interface",
|
|
94948
|
+
status: "fail",
|
|
94949
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
94950
|
+
line: line2
|
|
94951
|
+
});
|
|
94952
|
+
}
|
|
94953
|
+
},
|
|
94954
|
+
/**
|
|
94955
|
+
* Declare the expected physical connectivity component count for the returned visible model.
|
|
94956
|
+
*
|
|
94957
|
+
* **Details**
|
|
94958
|
+
*
|
|
94959
|
+
* Use this for generated mechanical models that should have a clear component graph:
|
|
94960
|
+
* one connected fixture, a purchased part plus a removable cartridge, a root assembly plus
|
|
94961
|
+
* named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned
|
|
94962
|
+
* visible objects with the same physical-connectivity analysis used in the quality gate and
|
|
94963
|
+
* fails if the actual component count differs.
|
|
94964
|
+
*
|
|
94965
|
+
* This catches the common generated-CAD failure where a script returns a visually plausible
|
|
94966
|
+
* artifact but the handle, screw, washer, cover, or terminal block is actually a separate island.
|
|
94967
|
+
*
|
|
94968
|
+
* **Example**
|
|
94969
|
+
*
|
|
94970
|
+
* ```ts
|
|
94971
|
+
* verify.physicalComponentCount("vise is one connected installed assembly", 1);
|
|
94972
|
+
* ```
|
|
94973
|
+
*/
|
|
94974
|
+
physicalComponentCount(label, expected) {
|
|
94975
|
+
const line2 = captureSourceLine();
|
|
94976
|
+
const id = nextId();
|
|
94977
|
+
if (!Number.isInteger(expected) || expected < 0) {
|
|
94978
|
+
push({
|
|
94979
|
+
id,
|
|
94980
|
+
label,
|
|
94981
|
+
kind: "interface",
|
|
94982
|
+
status: "fail",
|
|
94983
|
+
message: "Expected physical component count must be a non-negative integer",
|
|
94984
|
+
line: line2
|
|
94985
|
+
});
|
|
94986
|
+
return;
|
|
94987
|
+
}
|
|
94988
|
+
_physicalComponentExpectations.push({ id, label, expected, line: line2 });
|
|
94989
|
+
push({
|
|
94990
|
+
id,
|
|
94991
|
+
label,
|
|
94992
|
+
kind: "interface",
|
|
94993
|
+
status: "pass",
|
|
94994
|
+
message: `Expected ${expected} physical component(s); checked by mechanical-integrity connectivity`
|
|
94995
|
+
});
|
|
94996
|
+
},
|
|
94997
|
+
/**
|
|
94998
|
+
* Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent.
|
|
94999
|
+
*
|
|
95000
|
+
* **Details**
|
|
95001
|
+
*
|
|
95002
|
+
* Use this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume:
|
|
95003
|
+
* welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately
|
|
95004
|
+
* bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers
|
|
95005
|
+
* without pockets, or parts placed with collision as a positioning hack.
|
|
95006
|
+
*
|
|
95007
|
+
* `forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are
|
|
95008
|
+
* returned as visible objects and the exact collision report finds that same object pair. Unused or
|
|
95009
|
+
* non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions.
|
|
95010
|
+
*
|
|
95011
|
+
* **Example**
|
|
95012
|
+
*
|
|
95013
|
+
* ```ts
|
|
95014
|
+
* verify.intentionalOverlap("rubber grip is overmolded on handle", rubberGrip, handleCore, "overmolded insert");
|
|
95015
|
+
* ```
|
|
95016
|
+
*/
|
|
95017
|
+
intentionalOverlap(label, a2, b, reason) {
|
|
95018
|
+
const line2 = captureSourceLine();
|
|
95019
|
+
const id = nextId();
|
|
95020
|
+
const trimmedReason = String(reason ?? "").trim();
|
|
95021
|
+
if (trimmedReason.length === 0) {
|
|
95022
|
+
push({
|
|
95023
|
+
id,
|
|
95024
|
+
label,
|
|
95025
|
+
kind: "interface",
|
|
95026
|
+
status: "fail",
|
|
95027
|
+
message: "Intentional overlap requires a manufacturing reason",
|
|
95028
|
+
line: line2
|
|
95029
|
+
});
|
|
95030
|
+
return;
|
|
95031
|
+
}
|
|
95032
|
+
_collisionAllowances.push({ id, label, reason: trimmedReason, a: a2, b, line: line2 });
|
|
95033
|
+
push({
|
|
95034
|
+
id,
|
|
95035
|
+
label,
|
|
95036
|
+
kind: "interface",
|
|
95037
|
+
status: "pass",
|
|
95038
|
+
message: `Intentional overlap declared: ${trimmedReason}`
|
|
95039
|
+
});
|
|
95040
|
+
},
|
|
92534
95041
|
/**
|
|
92535
95042
|
* Check that two shapes do not collide (minGap > 0).
|
|
92536
95043
|
*
|
|
@@ -92539,19 +95046,28 @@ const verify = {
|
|
|
92539
95046
|
notColliding(label, a2, b, searchLength = 1) {
|
|
92540
95047
|
const line2 = captureSourceLine();
|
|
92541
95048
|
try {
|
|
92542
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
95049
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
95050
|
+
const methodLabel = method === "exact" ? "exact min gap" : "mesh-derived min gap";
|
|
92543
95051
|
const passed = gap > 0;
|
|
92544
95052
|
push({
|
|
92545
95053
|
id: nextId(),
|
|
92546
95054
|
label,
|
|
95055
|
+
kind: "interface",
|
|
92547
95056
|
status: passed ? "pass" : "fail",
|
|
92548
|
-
message: passed ? `No collision (
|
|
95057
|
+
message: passed ? `No collision (${methodLabel} ${roundNum(gap, 3)} mm)` : `Shapes are colliding (${methodLabel} ${roundNum(gap, 3)} mm ≤ 0)`,
|
|
92549
95058
|
line: passed ? void 0 : line2,
|
|
92550
95059
|
expected: "> 0 mm",
|
|
92551
95060
|
actual: `${roundNum(gap, 3)} mm`
|
|
92552
95061
|
});
|
|
92553
95062
|
} catch (e) {
|
|
92554
|
-
push({
|
|
95063
|
+
push({
|
|
95064
|
+
id: nextId(),
|
|
95065
|
+
label,
|
|
95066
|
+
kind: "interface",
|
|
95067
|
+
status: "fail",
|
|
95068
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
95069
|
+
line: line2
|
|
95070
|
+
});
|
|
92555
95071
|
}
|
|
92556
95072
|
},
|
|
92557
95073
|
/**
|
|
@@ -92560,13 +95076,15 @@ const verify = {
|
|
|
92560
95076
|
minClearance(label, a2, b, minGap, searchLength = 10) {
|
|
92561
95077
|
const line2 = captureSourceLine();
|
|
92562
95078
|
try {
|
|
92563
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
95079
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
95080
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
92564
95081
|
const passed = gap >= minGap;
|
|
92565
95082
|
push({
|
|
92566
95083
|
id: nextId(),
|
|
92567
95084
|
label,
|
|
95085
|
+
kind: "interface",
|
|
92568
95086
|
status: passed ? "pass" : "fail",
|
|
92569
|
-
message: passed ?
|
|
95087
|
+
message: passed ? `${methodLabel} ${roundNum(gap, 3)} mm ≥ ${roundNum(minGap, 3)} mm` : `${methodLabel} ${roundNum(gap, 3)} mm < required ${roundNum(minGap, 3)} mm`,
|
|
92570
95088
|
line: passed ? void 0 : line2,
|
|
92571
95089
|
expected: `≥ ${roundNum(minGap, 3)} mm`,
|
|
92572
95090
|
actual: `${roundNum(gap, 3)} mm`
|
|
@@ -92575,6 +95093,90 @@ const verify = {
|
|
|
92575
95093
|
push({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
|
|
92576
95094
|
}
|
|
92577
95095
|
},
|
|
95096
|
+
/**
|
|
95097
|
+
* Check that the clearance gap between two shapes is inside an allowed range.
|
|
95098
|
+
*
|
|
95099
|
+
* **Details**
|
|
95100
|
+
*
|
|
95101
|
+
* Use this for seated and retained interfaces where a part must be close
|
|
95102
|
+
* enough to be mechanically accountable, but must not collide beyond the
|
|
95103
|
+
* allowed minimum. It catches both failure modes that make generated CAD look
|
|
95104
|
+
* fake: parts floating away from their receiver, and parts intersecting their
|
|
95105
|
+
* receiver because the pocket, bore, or running clearance was not modeled.
|
|
95106
|
+
*
|
|
95107
|
+
* For contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny
|
|
95108
|
+
* numerical noise. For a running fit, use the intended clearance band.
|
|
95109
|
+
*
|
|
95110
|
+
* Manifold-backed shapes use exact min-gap distance. Other backends use a
|
|
95111
|
+
* mesh-derived min-gap check and say so in the verification message; keep
|
|
95112
|
+
* `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for
|
|
95113
|
+
* positive-volume interference.
|
|
95114
|
+
*
|
|
95115
|
+
* **Example**
|
|
95116
|
+
*
|
|
95117
|
+
* ```ts
|
|
95118
|
+
* verify.clearanceBetween("cover is seated on gasket", cover, gasket, -0.01, 0.05);
|
|
95119
|
+
* verify.clearanceBetween("carriage runs inside rail", carriage, rail, 0.2, 0.5);
|
|
95120
|
+
* ```
|
|
95121
|
+
*/
|
|
95122
|
+
clearanceBetween(label, a2, b, minGap, maxGap, searchLength) {
|
|
95123
|
+
const line2 = captureSourceLine();
|
|
95124
|
+
try {
|
|
95125
|
+
if (!Number.isFinite(minGap) || !Number.isFinite(maxGap)) {
|
|
95126
|
+
push({
|
|
95127
|
+
id: nextId(),
|
|
95128
|
+
label,
|
|
95129
|
+
kind: "interface",
|
|
95130
|
+
status: "fail",
|
|
95131
|
+
message: "Clearance range must use finite numbers",
|
|
95132
|
+
line: line2
|
|
95133
|
+
});
|
|
95134
|
+
return;
|
|
95135
|
+
}
|
|
95136
|
+
if (maxGap < minGap) {
|
|
95137
|
+
push({
|
|
95138
|
+
id: nextId(),
|
|
95139
|
+
label,
|
|
95140
|
+
kind: "interface",
|
|
95141
|
+
status: "fail",
|
|
95142
|
+
message: `Clearance max ${roundNum(maxGap, 3)} mm is smaller than min ${roundNum(minGap, 3)} mm`,
|
|
95143
|
+
line: line2
|
|
95144
|
+
});
|
|
95145
|
+
return;
|
|
95146
|
+
}
|
|
95147
|
+
const search = searchLength ?? Math.max(10, Math.abs(maxGap) * 2 + 1);
|
|
95148
|
+
const { gap, method } = computeMinGap(a2, b, search);
|
|
95149
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
95150
|
+
const passed = gap >= minGap && gap <= maxGap;
|
|
95151
|
+
let message;
|
|
95152
|
+
if (passed) {
|
|
95153
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm in [${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`;
|
|
95154
|
+
} else if (gap < minGap) {
|
|
95155
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm < allowed minimum ${roundNum(minGap, 3)} mm`;
|
|
95156
|
+
} else {
|
|
95157
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm > allowed maximum ${roundNum(maxGap, 3)} mm`;
|
|
95158
|
+
}
|
|
95159
|
+
push({
|
|
95160
|
+
id: nextId(),
|
|
95161
|
+
label,
|
|
95162
|
+
kind: "interface",
|
|
95163
|
+
status: passed ? "pass" : "fail",
|
|
95164
|
+
message,
|
|
95165
|
+
line: passed ? void 0 : line2,
|
|
95166
|
+
expected: `[${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`,
|
|
95167
|
+
actual: `${roundNum(gap, 3)} mm`
|
|
95168
|
+
});
|
|
95169
|
+
} catch (e) {
|
|
95170
|
+
push({
|
|
95171
|
+
id: nextId(),
|
|
95172
|
+
label,
|
|
95173
|
+
kind: "interface",
|
|
95174
|
+
status: "fail",
|
|
95175
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
95176
|
+
line: line2
|
|
95177
|
+
});
|
|
95178
|
+
}
|
|
95179
|
+
},
|
|
92578
95180
|
/**
|
|
92579
95181
|
* Check that two face normals are parallel (within toleranceDeg degrees).
|
|
92580
95182
|
*/
|
|
@@ -305140,6 +307742,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
|
|
|
305140
307742
|
nurbsSurface,
|
|
305141
307743
|
spline2d,
|
|
305142
307744
|
spline3d,
|
|
307745
|
+
Loft,
|
|
305143
307746
|
loft,
|
|
305144
307747
|
loftAlongSpine,
|
|
305145
307748
|
sweep,
|