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
|
@@ -799,18 +799,18 @@ function cloneSdfNode(node) {
|
|
|
799
799
|
}
|
|
800
800
|
}
|
|
801
801
|
const SHEET_METAL_EDGES = ["top", "right", "bottom", "left"];
|
|
802
|
-
const EPS$
|
|
802
|
+
const EPS$d = 1e-9;
|
|
803
803
|
function isFinitePositive$3(value) {
|
|
804
804
|
return Number.isFinite(value) && value > 0;
|
|
805
805
|
}
|
|
806
806
|
function isFiniteNonNegative(value) {
|
|
807
807
|
return Number.isFinite(value) && value >= 0;
|
|
808
808
|
}
|
|
809
|
-
function cloneVec3$
|
|
809
|
+
function cloneVec3$5(vec2) {
|
|
810
810
|
return [vec2[0], vec2[1], vec2[2]];
|
|
811
811
|
}
|
|
812
812
|
function cloneFaceAxis(vec2) {
|
|
813
|
-
return vec2 ? cloneVec3$
|
|
813
|
+
return vec2 ? cloneVec3$5(vec2) : void 0;
|
|
814
814
|
}
|
|
815
815
|
function cloneSheetMetalModel(model) {
|
|
816
816
|
if (!model) return null;
|
|
@@ -840,7 +840,7 @@ function edgeDisplayName(edge) {
|
|
|
840
840
|
return `sheetMetal().flange("${edge}", ...)`;
|
|
841
841
|
}
|
|
842
842
|
function normalizeAngle(angleDeg) {
|
|
843
|
-
return Math.abs(angleDeg) <= EPS$
|
|
843
|
+
return Math.abs(angleDeg) <= EPS$d ? 0 : angleDeg;
|
|
844
844
|
}
|
|
845
845
|
function validateSheetMetalModel(model) {
|
|
846
846
|
if (!isFinitePositive$3(model.panel.width) || !isFinitePositive$3(model.panel.height)) {
|
|
@@ -852,7 +852,7 @@ function validateSheetMetalModel(model) {
|
|
|
852
852
|
if (!isFiniteNonNegative(model.bendRadius)) {
|
|
853
853
|
return "sheetMetal() requires a finite non-negative bendRadius.";
|
|
854
854
|
}
|
|
855
|
-
if (model.bendRadius <= EPS$
|
|
855
|
+
if (model.bendRadius <= EPS$d) {
|
|
856
856
|
return "sheetMetal() v1 requires a positive bendRadius so the bend region stays explicit instead of collapsing into a sharp fold.";
|
|
857
857
|
}
|
|
858
858
|
if (model.bendAllowance.kind !== "k-factor") {
|
|
@@ -914,7 +914,7 @@ function deriveSheetMetalModel(model) {
|
|
|
914
914
|
const trimEnd = flanges.has(adjacent.end) ? model.cornerRelief.size : 0;
|
|
915
915
|
const fullLength = edge === "top" || edge === "bottom" ? model.panel.width : model.panel.height;
|
|
916
916
|
const span = fullLength - trimStart - trimEnd;
|
|
917
|
-
if (!(span > EPS$
|
|
917
|
+
if (!(span > EPS$d)) {
|
|
918
918
|
throw new Error(
|
|
919
919
|
`${edgeDisplayName(edge)} loses all usable span after applying the defended rectangular corner relief size ${model.cornerRelief.size}.`
|
|
920
920
|
);
|
|
@@ -972,7 +972,7 @@ function transformPlacement(origin, u2, v, normal) {
|
|
|
972
972
|
};
|
|
973
973
|
}
|
|
974
974
|
function translatePlan(plan, x2, y2, z2) {
|
|
975
|
-
if (Math.abs(x2) <= EPS$
|
|
975
|
+
if (Math.abs(x2) <= EPS$d && Math.abs(y2) <= EPS$d && Math.abs(z2) <= EPS$d) return cloneShapeCompilePlan(plan);
|
|
976
976
|
return appendShapeCompileTransform(cloneShapeCompilePlan(plan), {
|
|
977
977
|
kind: "translate",
|
|
978
978
|
x: x2,
|
|
@@ -1120,8 +1120,8 @@ function lowerSheetMetalBasePlan(model, output) {
|
|
|
1120
1120
|
function descriptor(name, center, normal, planar, uAxis, vAxis, semantic = "face", memberNames = [name], coplanar = planar) {
|
|
1121
1121
|
return {
|
|
1122
1122
|
name,
|
|
1123
|
-
center: cloneVec3$
|
|
1124
|
-
normal: cloneVec3$
|
|
1123
|
+
center: cloneVec3$5(center),
|
|
1124
|
+
normal: cloneVec3$5(normal),
|
|
1125
1125
|
planar,
|
|
1126
1126
|
uAxis: cloneFaceAxis(uAxis),
|
|
1127
1127
|
vAxis: cloneFaceAxis(vAxis),
|
|
@@ -1374,10 +1374,10 @@ function describeSheetMetalPlanarRegionFrames(model, output) {
|
|
|
1374
1374
|
const size = sheetMetalPlanarRegionSize(derived, face.name);
|
|
1375
1375
|
return {
|
|
1376
1376
|
name: face.name,
|
|
1377
|
-
center: cloneVec3$
|
|
1378
|
-
normal: cloneVec3$
|
|
1379
|
-
uAxis: cloneVec3$
|
|
1380
|
-
vAxis: cloneVec3$
|
|
1377
|
+
center: cloneVec3$5(face.center),
|
|
1378
|
+
normal: cloneVec3$5(face.normal),
|
|
1379
|
+
uAxis: cloneVec3$5(face.uAxis),
|
|
1380
|
+
vAxis: cloneVec3$5(face.vAxis),
|
|
1381
1381
|
width: size.width,
|
|
1382
1382
|
height: size.height,
|
|
1383
1383
|
thickness: derived.thickness
|
|
@@ -1416,7 +1416,7 @@ function cloneShapeWorkplanePlacement(placement) {
|
|
|
1416
1416
|
placement: cloneSketchPlacementModel(placement.placement)
|
|
1417
1417
|
};
|
|
1418
1418
|
}
|
|
1419
|
-
const EPS$
|
|
1419
|
+
const EPS$c = 1e-10;
|
|
1420
1420
|
function subVec3(a2, b) {
|
|
1421
1421
|
return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
|
|
1422
1422
|
}
|
|
@@ -1442,7 +1442,7 @@ function projectRadial(v, axis) {
|
|
|
1442
1442
|
function signedAngleAroundAxis(from, to, axis) {
|
|
1443
1443
|
const fromLen = lengthVec3$1(from);
|
|
1444
1444
|
const toLen = lengthVec3$1(to);
|
|
1445
|
-
if (fromLen < EPS$
|
|
1445
|
+
if (fromLen < EPS$c || toLen < EPS$c) return 0;
|
|
1446
1446
|
const fn = scaleVec3(from, 1 / fromLen);
|
|
1447
1447
|
const tn = scaleVec3(to, 1 / toLen);
|
|
1448
1448
|
const sin2 = dotVec3$4(axis, crossVec3$2(fn, tn));
|
|
@@ -1463,19 +1463,19 @@ function solveRotateAroundAngle(axis, pivot, movingPoint, targetPoint, options =
|
|
|
1463
1463
|
const targetDecomp = projectRadial(target, unitAxis);
|
|
1464
1464
|
const movingRadialLen = lengthVec3$1(movingDecomp.radial);
|
|
1465
1465
|
const targetRadialLen = lengthVec3$1(targetDecomp.radial);
|
|
1466
|
-
if (movingRadialLen < EPS$
|
|
1467
|
-
if (mode === "line" && targetRadialLen >= EPS$
|
|
1466
|
+
if (movingRadialLen < EPS$c) {
|
|
1467
|
+
if (mode === "line" && targetRadialLen >= EPS$c) {
|
|
1468
1468
|
throw new Error("rotateAroundTo(...): moving point lies on the rotation axis, so line alignment is impossible");
|
|
1469
1469
|
}
|
|
1470
1470
|
return 0;
|
|
1471
1471
|
}
|
|
1472
1472
|
if (mode === "plane") {
|
|
1473
|
-
if (targetRadialLen < EPS$
|
|
1473
|
+
if (targetRadialLen < EPS$c) {
|
|
1474
1474
|
throw new Error("rotateAroundTo(...): target point lies on the rotation axis, so the target plane is undefined");
|
|
1475
1475
|
}
|
|
1476
1476
|
return signedAngleAroundAxis(movingDecomp.radial, targetDecomp.radial, unitAxis);
|
|
1477
1477
|
}
|
|
1478
|
-
if (targetRadialLen < EPS$
|
|
1478
|
+
if (targetRadialLen < EPS$c) {
|
|
1479
1479
|
throw new Error("rotateAroundTo(...): target line lies on the rotation axis, but the moving point does not");
|
|
1480
1480
|
}
|
|
1481
1481
|
const axialTol = 1e-8 * Math.max(1, Math.abs(movingDecomp.axial), Math.abs(targetDecomp.axial));
|
|
@@ -1515,10 +1515,10 @@ function multiplyMat4(a2, b) {
|
|
|
1515
1515
|
}
|
|
1516
1516
|
function normalizeVec3$5(v) {
|
|
1517
1517
|
const len2 = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
1518
|
-
if (len2 < EPS$
|
|
1518
|
+
if (len2 < EPS$c) throw new Error("Axis must be non-zero");
|
|
1519
1519
|
return [v[0] / len2, v[1] / len2, v[2] / len2];
|
|
1520
1520
|
}
|
|
1521
|
-
function transformPoint$
|
|
1521
|
+
function transformPoint$2(m2, p2, w2) {
|
|
1522
1522
|
const x2 = p2[0], y2 = p2[1], z2 = p2[2];
|
|
1523
1523
|
return [
|
|
1524
1524
|
m2[0] * x2 + m2[4] * y2 + m2[8] * z2 + m2[12] * w2,
|
|
@@ -1545,7 +1545,7 @@ function invertMat4(m2) {
|
|
|
1545
1545
|
const b10 = a21 * a33 - a23 * a31;
|
|
1546
1546
|
const b11 = a22 * a33 - a23 * a32;
|
|
1547
1547
|
const det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
|
|
1548
|
-
if (Math.abs(det) < EPS$
|
|
1548
|
+
if (Math.abs(det) < EPS$c) throw new Error("Transform matrix is not invertible");
|
|
1549
1549
|
const invDet = 1 / det;
|
|
1550
1550
|
out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * invDet;
|
|
1551
1551
|
out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * invDet;
|
|
@@ -1638,11 +1638,11 @@ class Transform {
|
|
|
1638
1638
|
}
|
|
1639
1639
|
/** Transform a point using homogeneous coordinates. */
|
|
1640
1640
|
point(p2) {
|
|
1641
|
-
return transformPoint$
|
|
1641
|
+
return transformPoint$2(this.m, p2, 1);
|
|
1642
1642
|
}
|
|
1643
1643
|
/** Transform a direction vector without translation. */
|
|
1644
1644
|
vector(v) {
|
|
1645
|
-
return transformPoint$
|
|
1645
|
+
return transformPoint$2(this.m, v, 0);
|
|
1646
1646
|
}
|
|
1647
1647
|
/** Return the transform as a raw 4x4 matrix array. */
|
|
1648
1648
|
toArray() {
|
|
@@ -3302,14 +3302,14 @@ function sweepPathToPolylineAdaptive(path2, baseSamples = 48) {
|
|
|
3302
3302
|
pts.push(evalPathAt(path2, 1));
|
|
3303
3303
|
return pts;
|
|
3304
3304
|
}
|
|
3305
|
-
const EPS$
|
|
3305
|
+
const EPS$b = 1e-8;
|
|
3306
3306
|
const SUPPORTED_VERTICAL_EDGE_NAMES = ["vert-bl", "vert-br", "vert-tr", "vert-tl"];
|
|
3307
3307
|
function midpoint$4(start, end) {
|
|
3308
3308
|
return [(start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5, (start[2] + end[2]) * 0.5];
|
|
3309
3309
|
}
|
|
3310
3310
|
function normalize$8(v) {
|
|
3311
3311
|
const len2 = Math.hypot(v[0], v[1], v[2]);
|
|
3312
|
-
if (len2 <= EPS$
|
|
3312
|
+
if (len2 <= EPS$b) throw new Error("Edge feature selection requires a non-zero direction vector");
|
|
3313
3313
|
return [v[0] / len2, v[1] / len2, v[2] / len2];
|
|
3314
3314
|
}
|
|
3315
3315
|
function subtract(a2, b) {
|
|
@@ -3391,7 +3391,7 @@ function rigidTransformForEdgeStep(step) {
|
|
|
3391
3391
|
case "mirror": {
|
|
3392
3392
|
const [nx0, ny0, nz0] = [step.normalX, step.normalY, step.normalZ];
|
|
3393
3393
|
const len2 = Math.hypot(nx0, ny0, nz0);
|
|
3394
|
-
if (len2 <= EPS$
|
|
3394
|
+
if (len2 <= EPS$b) return Transform.identity();
|
|
3395
3395
|
const nx = nx0 / len2;
|
|
3396
3396
|
const ny = ny0 / len2;
|
|
3397
3397
|
const nz = nz0 / len2;
|
|
@@ -3702,7 +3702,7 @@ function isRectangleProfile(points) {
|
|
|
3702
3702
|
return [next[0] - point2[0], next[1] - point2[1]];
|
|
3703
3703
|
});
|
|
3704
3704
|
const lengths2 = vectors.map(([x2, y2]) => Math.hypot(x2, y2));
|
|
3705
|
-
if (lengths2.some((length4) => length4 <= EPS$
|
|
3705
|
+
if (lengths2.some((length4) => length4 <= EPS$b)) return false;
|
|
3706
3706
|
const dot01 = vectors[0][0] * vectors[1][0] + vectors[0][1] * vectors[1][1];
|
|
3707
3707
|
const dot12 = vectors[1][0] * vectors[2][0] + vectors[1][1] * vectors[2][1];
|
|
3708
3708
|
const dot23 = vectors[2][0] * vectors[3][0] + vectors[2][1] * vectors[3][1];
|
|
@@ -5769,13 +5769,13 @@ function parseMeshFile(data, format) {
|
|
|
5769
5769
|
return parse3mf(data);
|
|
5770
5770
|
}
|
|
5771
5771
|
}
|
|
5772
|
-
const EPS$
|
|
5772
|
+
const EPS$a = 1e-8;
|
|
5773
5773
|
function length$3(v) {
|
|
5774
5774
|
return Math.hypot(v[0], v[1], v[2]);
|
|
5775
5775
|
}
|
|
5776
5776
|
function normalize$7(v) {
|
|
5777
5777
|
const len2 = length$3(v);
|
|
5778
|
-
if (len2 < EPS$
|
|
5778
|
+
if (len2 < EPS$a) throw new Error("Plane normal must be non-zero");
|
|
5779
5779
|
return [v[0] / len2, v[1] / len2, v[2] / len2];
|
|
5780
5780
|
}
|
|
5781
5781
|
function resolvePlaneOriginNormal(plane) {
|
|
@@ -5797,12 +5797,12 @@ function resolvePlaneOriginNormal(plane) {
|
|
|
5797
5797
|
function rotationToPlaneSpace(normal) {
|
|
5798
5798
|
const n = normalize$7(normal);
|
|
5799
5799
|
const dot2 = n[2];
|
|
5800
|
-
if (dot2 > 1 - EPS$
|
|
5800
|
+
if (dot2 > 1 - EPS$a) {
|
|
5801
5801
|
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
5802
5802
|
}
|
|
5803
5803
|
let axis;
|
|
5804
5804
|
let angle;
|
|
5805
|
-
if (dot2 < -1 + EPS$
|
|
5805
|
+
if (dot2 < -1 + EPS$a) {
|
|
5806
5806
|
axis = [1, 0, 0];
|
|
5807
5807
|
angle = Math.PI;
|
|
5808
5808
|
} else {
|
|
@@ -8364,7 +8364,7 @@ function scale$6(v, s) {
|
|
|
8364
8364
|
function sub$7(a2, b) {
|
|
8365
8365
|
return [a2[0] - b[0], a2[1] - b[1], a2[2] - b[2]];
|
|
8366
8366
|
}
|
|
8367
|
-
function cross$
|
|
8367
|
+
function cross$8(a2, b) {
|
|
8368
8368
|
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]];
|
|
8369
8369
|
}
|
|
8370
8370
|
function makeEdge(name, start, end, faceName, curve) {
|
|
@@ -8400,7 +8400,7 @@ function buildSurfaceSheetTopology(boundaries, options = {}) {
|
|
|
8400
8400
|
const center = options.center ?? average$1(corners);
|
|
8401
8401
|
const uAxis = normalizeAxis$1(sub$7(midpoint$3(u1Start, u1End), midpoint$3(u0Start, u0End)));
|
|
8402
8402
|
const vAxis = normalizeAxis$1(sub$7(midpoint$3(v1Start, v1End), midpoint$3(v0Start, v0End)));
|
|
8403
|
-
const normal = normalizeAxis$1(options.normal ?? cross$
|
|
8403
|
+
const normal = normalizeAxis$1(options.normal ?? cross$8(uAxis, vAxis));
|
|
8404
8404
|
const faces = /* @__PURE__ */ new Map();
|
|
8405
8405
|
faces.set(faceName, {
|
|
8406
8406
|
name: faceName,
|
|
@@ -8432,7 +8432,7 @@ function attachSurfaceSheetTopology(shape, boundaries, options = {}) {
|
|
|
8432
8432
|
});
|
|
8433
8433
|
return shape;
|
|
8434
8434
|
}
|
|
8435
|
-
function cloneVec3$
|
|
8435
|
+
function cloneVec3$4(value) {
|
|
8436
8436
|
return [value[0], value[1], value[2]];
|
|
8437
8437
|
}
|
|
8438
8438
|
function cloneNurbsFaceTrimLoop(loop) {
|
|
@@ -8454,38 +8454,38 @@ function cloneFaceSurface(surface) {
|
|
|
8454
8454
|
if (!surface) return void 0;
|
|
8455
8455
|
switch (surface.kind) {
|
|
8456
8456
|
case "plane":
|
|
8457
|
-
return { kind: "plane", normal: cloneVec3$
|
|
8457
|
+
return { kind: "plane", normal: cloneVec3$4(surface.normal) };
|
|
8458
8458
|
case "cylinder":
|
|
8459
8459
|
return {
|
|
8460
8460
|
kind: "cylinder",
|
|
8461
|
-
origin: cloneVec3$
|
|
8462
|
-
axis: cloneVec3$
|
|
8461
|
+
origin: cloneVec3$4(surface.origin),
|
|
8462
|
+
axis: cloneVec3$4(surface.axis),
|
|
8463
8463
|
radius: surface.radius,
|
|
8464
8464
|
height: surface.height
|
|
8465
8465
|
};
|
|
8466
8466
|
case "cone":
|
|
8467
8467
|
return {
|
|
8468
8468
|
kind: "cone",
|
|
8469
|
-
origin: cloneVec3$
|
|
8470
|
-
axis: cloneVec3$
|
|
8469
|
+
origin: cloneVec3$4(surface.origin),
|
|
8470
|
+
axis: cloneVec3$4(surface.axis),
|
|
8471
8471
|
radiusBottom: surface.radiusBottom,
|
|
8472
8472
|
radiusTop: surface.radiusTop,
|
|
8473
8473
|
height: surface.height
|
|
8474
8474
|
};
|
|
8475
8475
|
case "sphere":
|
|
8476
|
-
return { kind: "sphere", center: cloneVec3$
|
|
8476
|
+
return { kind: "sphere", center: cloneVec3$4(surface.center), radius: surface.radius };
|
|
8477
8477
|
case "torus":
|
|
8478
8478
|
return {
|
|
8479
8479
|
kind: "torus",
|
|
8480
|
-
center: cloneVec3$
|
|
8481
|
-
axis: cloneVec3$
|
|
8480
|
+
center: cloneVec3$4(surface.center),
|
|
8481
|
+
axis: cloneVec3$4(surface.axis),
|
|
8482
8482
|
majorRadius: surface.majorRadius,
|
|
8483
8483
|
minorRadius: surface.minorRadius
|
|
8484
8484
|
};
|
|
8485
8485
|
case "ruled":
|
|
8486
8486
|
return {
|
|
8487
8487
|
kind: "ruled",
|
|
8488
|
-
rails: surface.rails.map((rail2) => rail2.map(cloneVec3$
|
|
8488
|
+
rails: surface.rails.map((rail2) => rail2.map(cloneVec3$4))
|
|
8489
8489
|
};
|
|
8490
8490
|
case "nurbs":
|
|
8491
8491
|
return {
|
|
@@ -8498,9 +8498,9 @@ function cloneEdgeCurve(curve) {
|
|
|
8498
8498
|
if (!curve) return void 0;
|
|
8499
8499
|
switch (curve.kind) {
|
|
8500
8500
|
case "line":
|
|
8501
|
-
return { ...curve, start: cloneVec3$
|
|
8501
|
+
return { ...curve, start: cloneVec3$4(curve.start), end: cloneVec3$4(curve.end) };
|
|
8502
8502
|
case "circle":
|
|
8503
|
-
return { ...curve, center: cloneVec3$
|
|
8503
|
+
return { ...curve, center: cloneVec3$4(curve.center), axis: cloneVec3$4(curve.axis) };
|
|
8504
8504
|
case "surfaceIso":
|
|
8505
8505
|
return { ...curve, parameterRange: [curve.parameterRange[0], curve.parameterRange[1]] };
|
|
8506
8506
|
case "nurbsUv":
|
|
@@ -8518,7 +8518,7 @@ function cloneEdgeCurve(curve) {
|
|
|
8518
8518
|
}
|
|
8519
8519
|
}
|
|
8520
8520
|
function makeLineEdgeCurve(start, end, faceName) {
|
|
8521
|
-
return faceName ? { kind: "line", start: cloneVec3$
|
|
8521
|
+
return faceName ? { kind: "line", start: cloneVec3$4(start), end: cloneVec3$4(end), faceName } : { kind: "line", start: cloneVec3$4(start), end: cloneVec3$4(end) };
|
|
8522
8522
|
}
|
|
8523
8523
|
function edgeCurveFaceName(curve) {
|
|
8524
8524
|
switch (curve.kind) {
|
|
@@ -10302,6 +10302,7 @@ function buildSweepLevelSetInput(profilePolygons, pathInput, options) {
|
|
|
10302
10302
|
edgeLength: options.edgeLength
|
|
10303
10303
|
};
|
|
10304
10304
|
}
|
|
10305
|
+
const EPS$9 = 1e-9;
|
|
10305
10306
|
function resamplePolygon(poly, targetCount) {
|
|
10306
10307
|
if (poly.length < 2) return poly;
|
|
10307
10308
|
if (targetCount <= 0) return [];
|
|
@@ -10339,6 +10340,78 @@ function resamplePolygon(poly, targetCount) {
|
|
|
10339
10340
|
}
|
|
10340
10341
|
return out;
|
|
10341
10342
|
}
|
|
10343
|
+
function resamplePolygonByAngle(poly, targetCount, center = polygonCentroid$2(poly)) {
|
|
10344
|
+
if (poly.length < 3 || targetCount <= 0) return null;
|
|
10345
|
+
if (!isConvexPolygon(poly)) return null;
|
|
10346
|
+
const out = [];
|
|
10347
|
+
for (let index2 = 0; index2 < targetCount; index2 += 1) {
|
|
10348
|
+
const angle = index2 / targetCount * Math.PI * 2;
|
|
10349
|
+
const point2 = rayPolygonIntersection(center, [Math.cos(angle), Math.sin(angle)], poly);
|
|
10350
|
+
if (!point2) return null;
|
|
10351
|
+
out.push(point2);
|
|
10352
|
+
}
|
|
10353
|
+
return out;
|
|
10354
|
+
}
|
|
10355
|
+
function rayPolygonIntersection(origin, direction2, poly) {
|
|
10356
|
+
let bestT = Infinity;
|
|
10357
|
+
let best = null;
|
|
10358
|
+
for (let index2 = 0; index2 < poly.length; index2 += 1) {
|
|
10359
|
+
const a2 = poly[index2];
|
|
10360
|
+
const b = poly[(index2 + 1) % poly.length];
|
|
10361
|
+
const edge = [b[0] - a2[0], b[1] - a2[1]];
|
|
10362
|
+
const denom = cross$7(direction2, edge);
|
|
10363
|
+
if (Math.abs(denom) < EPS$9) continue;
|
|
10364
|
+
const delta = [a2[0] - origin[0], a2[1] - origin[1]];
|
|
10365
|
+
const rayT = cross$7(delta, edge) / denom;
|
|
10366
|
+
const edgeT = cross$7(delta, direction2) / denom;
|
|
10367
|
+
if (rayT >= -EPS$9 && edgeT >= -EPS$9 && edgeT <= 1 + EPS$9 && rayT < bestT) {
|
|
10368
|
+
bestT = rayT;
|
|
10369
|
+
best = [origin[0] + direction2[0] * rayT, origin[1] + direction2[1] * rayT];
|
|
10370
|
+
}
|
|
10371
|
+
}
|
|
10372
|
+
return best;
|
|
10373
|
+
}
|
|
10374
|
+
function polygonCentroid$2(poly) {
|
|
10375
|
+
let area2 = 0;
|
|
10376
|
+
let cx = 0;
|
|
10377
|
+
let cy = 0;
|
|
10378
|
+
for (let index2 = 0; index2 < poly.length; index2 += 1) {
|
|
10379
|
+
const a2 = poly[index2];
|
|
10380
|
+
const b = poly[(index2 + 1) % poly.length];
|
|
10381
|
+
const crossValue = cross$7(a2, b);
|
|
10382
|
+
area2 += crossValue;
|
|
10383
|
+
cx += (a2[0] + b[0]) * crossValue;
|
|
10384
|
+
cy += (a2[1] + b[1]) * crossValue;
|
|
10385
|
+
}
|
|
10386
|
+
if (Math.abs(area2) < EPS$9) return averagePoint(poly);
|
|
10387
|
+
return [cx / (3 * area2), cy / (3 * area2)];
|
|
10388
|
+
}
|
|
10389
|
+
function averagePoint(poly) {
|
|
10390
|
+
let x2 = 0;
|
|
10391
|
+
let y2 = 0;
|
|
10392
|
+
for (const point2 of poly) {
|
|
10393
|
+
x2 += point2[0];
|
|
10394
|
+
y2 += point2[1];
|
|
10395
|
+
}
|
|
10396
|
+
return [x2 / poly.length, y2 / poly.length];
|
|
10397
|
+
}
|
|
10398
|
+
function isConvexPolygon(poly) {
|
|
10399
|
+
let sign2 = 0;
|
|
10400
|
+
for (let index2 = 0; index2 < poly.length; index2 += 1) {
|
|
10401
|
+
const a2 = poly[index2];
|
|
10402
|
+
const b = poly[(index2 + 1) % poly.length];
|
|
10403
|
+
const c2 = poly[(index2 + 2) % poly.length];
|
|
10404
|
+
const turn = cross$7([b[0] - a2[0], b[1] - a2[1]], [c2[0] - b[0], c2[1] - b[1]]);
|
|
10405
|
+
if (Math.abs(turn) < EPS$9) continue;
|
|
10406
|
+
const currentSign = Math.sign(turn);
|
|
10407
|
+
if (sign2 !== 0 && currentSign !== sign2) return false;
|
|
10408
|
+
sign2 = currentSign;
|
|
10409
|
+
}
|
|
10410
|
+
return sign2 !== 0;
|
|
10411
|
+
}
|
|
10412
|
+
function cross$7(a2, b) {
|
|
10413
|
+
return a2[0] * b[1] - a2[1] * b[0];
|
|
10414
|
+
}
|
|
10342
10415
|
function loftStitched(profiles2, heights, wasm) {
|
|
10343
10416
|
if (profiles2.length < 2) return null;
|
|
10344
10417
|
const classified = profiles2.map((loops) => classifyLoops(loops));
|
|
@@ -10467,8 +10540,10 @@ function stitchSingleLoopLoft(loops, heights, wasm) {
|
|
|
10467
10540
|
maxPoints = Math.max(maxPoints, loop.length);
|
|
10468
10541
|
}
|
|
10469
10542
|
const N = Math.max(maxPoints, 24);
|
|
10543
|
+
const angularSamples = normalizedLoops.map((loop) => resamplePolygonByAngle(loop, N));
|
|
10544
|
+
const useAngularSamples = angularSamples.every((samples) => samples != null);
|
|
10470
10545
|
const resampled = normalizedLoops.map((loop, i) => {
|
|
10471
|
-
const pts2d = resamplePolygon(loop, N);
|
|
10546
|
+
const pts2d = useAngularSamples ? angularSamples[i] : resamplePolygon(loop, N);
|
|
10472
10547
|
const z2 = heights[i];
|
|
10473
10548
|
return pts2d.map(([x2, y2]) => [x2, y2, z2]);
|
|
10474
10549
|
});
|
|
@@ -10530,7 +10605,7 @@ async function initManifoldWasm() {
|
|
|
10530
10605
|
if (_wasm$1) return _wasm$1;
|
|
10531
10606
|
performance.mark("manifold:start");
|
|
10532
10607
|
const Module = (await __vitePreload(async () => {
|
|
10533
|
-
const { default: __vite_default__ } = await import("./manifold-
|
|
10608
|
+
const { default: __vite_default__ } = await import("./manifold-DjYsd7A_.js");
|
|
10534
10609
|
return { default: __vite_default__ };
|
|
10535
10610
|
}, true ? [] : void 0)).default;
|
|
10536
10611
|
performance.mark("manifold:imported");
|
|
@@ -30608,17 +30683,17 @@ function emptyFaceTable() {
|
|
|
30608
30683
|
blockedQueries: []
|
|
30609
30684
|
};
|
|
30610
30685
|
}
|
|
30611
|
-
function cloneVec3$
|
|
30686
|
+
function cloneVec3$3(vec2) {
|
|
30612
30687
|
return [vec2[0], vec2[1], vec2[2]];
|
|
30613
30688
|
}
|
|
30614
30689
|
function cloneFaceRefValue(face) {
|
|
30615
30690
|
return {
|
|
30616
30691
|
...face,
|
|
30617
|
-
normal: cloneVec3$
|
|
30618
|
-
center: cloneVec3$
|
|
30692
|
+
normal: cloneVec3$3(face.normal),
|
|
30693
|
+
center: cloneVec3$3(face.center),
|
|
30619
30694
|
query: cloneFaceQueryRef(face.query),
|
|
30620
|
-
uAxis: face.uAxis ? cloneVec3$
|
|
30621
|
-
vAxis: face.vAxis ? cloneVec3$
|
|
30695
|
+
uAxis: face.uAxis ? cloneVec3$3(face.uAxis) : void 0,
|
|
30696
|
+
vAxis: face.vAxis ? cloneVec3$3(face.vAxis) : void 0,
|
|
30622
30697
|
surface: cloneFaceSurface(face.surface),
|
|
30623
30698
|
descendant: face.descendant ? cloneFaceDescendantMetadata(face.descendant) : void 0
|
|
30624
30699
|
};
|
|
@@ -31473,11 +31548,11 @@ function resolveShapeFaceTableInternal(plan, owner) {
|
|
|
31473
31548
|
for (const descriptor2 of describeSheetMetalFaces(plan.model, plan.output)) {
|
|
31474
31549
|
registerFace(table2, {
|
|
31475
31550
|
name: descriptor2.name,
|
|
31476
|
-
normal: cloneVec3$
|
|
31477
|
-
center: cloneVec3$
|
|
31551
|
+
normal: cloneVec3$3(descriptor2.normal),
|
|
31552
|
+
center: cloneVec3$3(descriptor2.center),
|
|
31478
31553
|
planar: descriptor2.planar,
|
|
31479
|
-
uAxis: descriptor2.uAxis ? cloneVec3$
|
|
31480
|
-
vAxis: descriptor2.vAxis ? cloneVec3$
|
|
31554
|
+
uAxis: descriptor2.uAxis ? cloneVec3$3(descriptor2.uAxis) : void 0,
|
|
31555
|
+
vAxis: descriptor2.vAxis ? cloneVec3$3(descriptor2.vAxis) : void 0,
|
|
31481
31556
|
query: createTrackedFaceQuery(descriptor2.name, owner),
|
|
31482
31557
|
descendant: createFaceDescendantMetadata(descriptor2.semantic, descriptor2.memberNames, descriptor2.coplanar)
|
|
31483
31558
|
});
|
|
@@ -31504,7 +31579,7 @@ function resolveShapeFaceTableInternal(plan, owner) {
|
|
|
31504
31579
|
baseFace.center[2] - baseFace.normal[2] * plan.thickness
|
|
31505
31580
|
],
|
|
31506
31581
|
normal: [-baseFace.normal[0], -baseFace.normal[1], -baseFace.normal[2]],
|
|
31507
|
-
uAxis: baseFace.uAxis ? cloneVec3$
|
|
31582
|
+
uAxis: baseFace.uAxis ? cloneVec3$3(baseFace.uAxis) : void 0,
|
|
31508
31583
|
vAxis: baseFace.vAxis ? [-baseFace.vAxis[0], -baseFace.vAxis[1], -baseFace.vAxis[2]] : void 0,
|
|
31509
31584
|
query: createdQuery
|
|
31510
31585
|
});
|
|
@@ -31570,15 +31645,15 @@ function resolveShapeFaceTableInternal(plan, owner) {
|
|
|
31570
31645
|
if (counterboreFloorQuery && plan.hole.counterbore) {
|
|
31571
31646
|
registerFace(table2, {
|
|
31572
31647
|
name: "counterbore-floor",
|
|
31573
|
-
normal: cloneVec3$
|
|
31648
|
+
normal: cloneVec3$3(workplane.normal),
|
|
31574
31649
|
center: [
|
|
31575
31650
|
origin[0] + inward[0] * plan.hole.counterbore.depth,
|
|
31576
31651
|
origin[1] + inward[1] * plan.hole.counterbore.depth,
|
|
31577
31652
|
origin[2] + inward[2] * plan.hole.counterbore.depth
|
|
31578
31653
|
],
|
|
31579
31654
|
planar: true,
|
|
31580
|
-
uAxis: cloneVec3$
|
|
31581
|
-
vAxis: cloneVec3$
|
|
31655
|
+
uAxis: cloneVec3$3(workplane.u),
|
|
31656
|
+
vAxis: cloneVec3$3(workplane.v),
|
|
31582
31657
|
query: counterboreFloorQuery
|
|
31583
31658
|
});
|
|
31584
31659
|
}
|
|
@@ -31602,11 +31677,11 @@ function resolveShapeFaceTableInternal(plan, owner) {
|
|
|
31602
31677
|
if (floorQuery) {
|
|
31603
31678
|
registerFace(table2, {
|
|
31604
31679
|
name: "floor",
|
|
31605
|
-
normal: cloneVec3$
|
|
31680
|
+
normal: cloneVec3$3(workplane.normal),
|
|
31606
31681
|
center: [origin[0] + inward[0] * forward.depth, origin[1] + inward[1] * forward.depth, origin[2] + inward[2] * forward.depth],
|
|
31607
31682
|
planar: true,
|
|
31608
|
-
uAxis: cloneVec3$
|
|
31609
|
-
vAxis: cloneVec3$
|
|
31683
|
+
uAxis: cloneVec3$3(workplane.u),
|
|
31684
|
+
vAxis: cloneVec3$3(workplane.v),
|
|
31610
31685
|
query: floorQuery
|
|
31611
31686
|
});
|
|
31612
31687
|
}
|
|
@@ -31617,7 +31692,7 @@ function resolveShapeFaceTableInternal(plan, owner) {
|
|
|
31617
31692
|
normal: [-workplane.normal[0], -workplane.normal[1], -workplane.normal[2]],
|
|
31618
31693
|
center: [origin[0] - inward[0] * reverse.depth, origin[1] - inward[1] * reverse.depth, origin[2] - inward[2] * reverse.depth],
|
|
31619
31694
|
planar: true,
|
|
31620
|
-
uAxis: cloneVec3$
|
|
31695
|
+
uAxis: cloneVec3$3(workplane.u),
|
|
31621
31696
|
vAxis: [-workplane.v[0], -workplane.v[1], -workplane.v[2]],
|
|
31622
31697
|
query: capQuery
|
|
31623
31698
|
});
|
|
@@ -31796,11 +31871,11 @@ function resolveShapeFaceTableInternal(plan, owner) {
|
|
|
31796
31871
|
})();
|
|
31797
31872
|
registerFace(table2, {
|
|
31798
31873
|
name: "floor",
|
|
31799
|
-
normal: cloneVec3$
|
|
31874
|
+
normal: cloneVec3$3(placement.workplane.normal),
|
|
31800
31875
|
center: floorCenter,
|
|
31801
31876
|
planar: true,
|
|
31802
|
-
uAxis: cloneVec3$
|
|
31803
|
-
vAxis: cloneVec3$
|
|
31877
|
+
uAxis: cloneVec3$3(placement.workplane.u),
|
|
31878
|
+
vAxis: cloneVec3$3(placement.workplane.v),
|
|
31804
31879
|
query: floorQuery
|
|
31805
31880
|
});
|
|
31806
31881
|
}
|
|
@@ -31815,7 +31890,7 @@ function resolveShapeFaceTableInternal(plan, owner) {
|
|
|
31815
31890
|
origin[2] - depthDir[2] * reverse.depth
|
|
31816
31891
|
],
|
|
31817
31892
|
planar: true,
|
|
31818
|
-
uAxis: cloneVec3$
|
|
31893
|
+
uAxis: cloneVec3$3(placement.workplane.u),
|
|
31819
31894
|
vAxis: [-placement.workplane.v[0], -placement.workplane.v[1], -placement.workplane.v[2]],
|
|
31820
31895
|
query: capQuery
|
|
31821
31896
|
});
|
|
@@ -32001,7 +32076,7 @@ function preservedShapeFaceQueries(basePlan) {
|
|
|
32001
32076
|
return listShapeFaceQueries(basePlan);
|
|
32002
32077
|
}
|
|
32003
32078
|
const PLACEMENT_REFERENCE_KINDS = ["points", "edges", "surfaces", "objects"];
|
|
32004
|
-
function cloneVec3(value, label) {
|
|
32079
|
+
function cloneVec3$2(value, label) {
|
|
32005
32080
|
if (!Array.isArray(value) || value.length < 3) {
|
|
32006
32081
|
throw new Error(`${label} must be a [x, y, z] tuple`);
|
|
32007
32082
|
}
|
|
@@ -32027,23 +32102,23 @@ function isBoundsObject(value) {
|
|
|
32027
32102
|
function toObjectBounds(value, label) {
|
|
32028
32103
|
if (isBoundsObject(value)) {
|
|
32029
32104
|
return {
|
|
32030
|
-
min: cloneVec3(value.min, `${label}.min`),
|
|
32031
|
-
max: cloneVec3(value.max, `${label}.max`)
|
|
32105
|
+
min: cloneVec3$2(value.min, `${label}.min`),
|
|
32106
|
+
max: cloneVec3$2(value.max, `${label}.max`)
|
|
32032
32107
|
};
|
|
32033
32108
|
}
|
|
32034
32109
|
if (typeof value === "object" && value != null) {
|
|
32035
32110
|
if ("boundingBox" in value && typeof value.boundingBox === "function") {
|
|
32036
32111
|
const bounds = value.boundingBox();
|
|
32037
32112
|
return {
|
|
32038
|
-
min: cloneVec3(bounds.min, `${label}.min`),
|
|
32039
|
-
max: cloneVec3(bounds.max, `${label}.max`)
|
|
32113
|
+
min: cloneVec3$2(bounds.min, `${label}.min`),
|
|
32114
|
+
max: cloneVec3$2(bounds.max, `${label}.max`)
|
|
32040
32115
|
};
|
|
32041
32116
|
}
|
|
32042
32117
|
if ("_bbox" in value && typeof value._bbox === "function") {
|
|
32043
32118
|
const bounds = value._bbox();
|
|
32044
32119
|
return {
|
|
32045
|
-
min: cloneVec3(bounds.min, `${label}.min`),
|
|
32046
|
-
max: cloneVec3(bounds.max, `${label}.max`)
|
|
32120
|
+
min: cloneVec3$2(bounds.min, `${label}.min`),
|
|
32121
|
+
max: cloneVec3$2(bounds.max, `${label}.max`)
|
|
32047
32122
|
};
|
|
32048
32123
|
}
|
|
32049
32124
|
}
|
|
@@ -32087,24 +32162,24 @@ function createPlacementReferences() {
|
|
|
32087
32162
|
function clonePlacementReferences(refs) {
|
|
32088
32163
|
const out = createPlacementReferences();
|
|
32089
32164
|
for (const [name, point2] of Object.entries(refs.points)) {
|
|
32090
|
-
out.points[name] = cloneVec3(point2, `points.${name}`);
|
|
32165
|
+
out.points[name] = cloneVec3$2(point2, `points.${name}`);
|
|
32091
32166
|
}
|
|
32092
32167
|
for (const [name, edge] of Object.entries(refs.edges)) {
|
|
32093
32168
|
out.edges[name] = {
|
|
32094
|
-
start: cloneVec3(edge.start, `edges.${name}.start`),
|
|
32095
|
-
end: cloneVec3(edge.end, `edges.${name}.end`)
|
|
32169
|
+
start: cloneVec3$2(edge.start, `edges.${name}.start`),
|
|
32170
|
+
end: cloneVec3$2(edge.end, `edges.${name}.end`)
|
|
32096
32171
|
};
|
|
32097
32172
|
}
|
|
32098
32173
|
for (const [name, surface] of Object.entries(refs.surfaces)) {
|
|
32099
32174
|
out.surfaces[name] = {
|
|
32100
|
-
center: cloneVec3(surface.center, `surfaces.${name}.center`),
|
|
32101
|
-
normal: cloneVec3(surface.normal, `surfaces.${name}.normal`)
|
|
32175
|
+
center: cloneVec3$2(surface.center, `surfaces.${name}.center`),
|
|
32176
|
+
normal: cloneVec3$2(surface.normal, `surfaces.${name}.normal`)
|
|
32102
32177
|
};
|
|
32103
32178
|
}
|
|
32104
32179
|
for (const [name, objectRef] of Object.entries(refs.objects)) {
|
|
32105
32180
|
out.objects[name] = {
|
|
32106
|
-
min: cloneVec3(objectRef.min, `objects.${name}.min`),
|
|
32107
|
-
max: cloneVec3(objectRef.max, `objects.${name}.max`)
|
|
32181
|
+
min: cloneVec3$2(objectRef.min, `objects.${name}.min`),
|
|
32182
|
+
max: cloneVec3$2(objectRef.max, `objects.${name}.max`)
|
|
32108
32183
|
};
|
|
32109
32184
|
}
|
|
32110
32185
|
return out;
|
|
@@ -32112,18 +32187,18 @@ function clonePlacementReferences(refs) {
|
|
|
32112
32187
|
function normalizePlacementReferenceInput(input = {}) {
|
|
32113
32188
|
const out = createPlacementReferences();
|
|
32114
32189
|
for (const [name, point2] of Object.entries(input.points ?? {})) {
|
|
32115
|
-
out.points[name] = cloneVec3(point2, `points.${name}`);
|
|
32190
|
+
out.points[name] = cloneVec3$2(point2, `points.${name}`);
|
|
32116
32191
|
}
|
|
32117
32192
|
for (const [name, edge] of Object.entries(input.edges ?? {})) {
|
|
32118
32193
|
out.edges[name] = {
|
|
32119
|
-
start: cloneVec3(edge.start, `edges.${name}.start`),
|
|
32120
|
-
end: cloneVec3(edge.end, `edges.${name}.end`)
|
|
32194
|
+
start: cloneVec3$2(edge.start, `edges.${name}.start`),
|
|
32195
|
+
end: cloneVec3$2(edge.end, `edges.${name}.end`)
|
|
32121
32196
|
};
|
|
32122
32197
|
}
|
|
32123
32198
|
for (const [name, surface] of Object.entries(input.surfaces ?? {})) {
|
|
32124
32199
|
out.surfaces[name] = {
|
|
32125
|
-
center: cloneVec3(surface.center, `surfaces.${name}.center`),
|
|
32126
|
-
normal: normalizeVector$1(cloneVec3(surface.normal, `surfaces.${name}.normal`))
|
|
32200
|
+
center: cloneVec3$2(surface.center, `surfaces.${name}.center`),
|
|
32201
|
+
normal: normalizeVector$1(cloneVec3$2(surface.normal, `surfaces.${name}.normal`))
|
|
32127
32202
|
};
|
|
32128
32203
|
}
|
|
32129
32204
|
for (const [name, objectRef] of Object.entries(input.objects ?? {})) {
|
|
@@ -32134,23 +32209,23 @@ function normalizePlacementReferenceInput(input = {}) {
|
|
|
32134
32209
|
function mergePlacementReferences(...refsList) {
|
|
32135
32210
|
const out = createPlacementReferences();
|
|
32136
32211
|
for (const refs of refsList) {
|
|
32137
|
-
for (const [name, point2] of Object.entries(refs.points)) out.points[name] = cloneVec3(point2, `points.${name}`);
|
|
32212
|
+
for (const [name, point2] of Object.entries(refs.points)) out.points[name] = cloneVec3$2(point2, `points.${name}`);
|
|
32138
32213
|
for (const [name, edge] of Object.entries(refs.edges)) {
|
|
32139
32214
|
out.edges[name] = {
|
|
32140
|
-
start: cloneVec3(edge.start, `edges.${name}.start`),
|
|
32141
|
-
end: cloneVec3(edge.end, `edges.${name}.end`)
|
|
32215
|
+
start: cloneVec3$2(edge.start, `edges.${name}.start`),
|
|
32216
|
+
end: cloneVec3$2(edge.end, `edges.${name}.end`)
|
|
32142
32217
|
};
|
|
32143
32218
|
}
|
|
32144
32219
|
for (const [name, surface] of Object.entries(refs.surfaces)) {
|
|
32145
32220
|
out.surfaces[name] = {
|
|
32146
|
-
center: cloneVec3(surface.center, `surfaces.${name}.center`),
|
|
32147
|
-
normal: cloneVec3(surface.normal, `surfaces.${name}.normal`)
|
|
32221
|
+
center: cloneVec3$2(surface.center, `surfaces.${name}.center`),
|
|
32222
|
+
normal: cloneVec3$2(surface.normal, `surfaces.${name}.normal`)
|
|
32148
32223
|
};
|
|
32149
32224
|
}
|
|
32150
32225
|
for (const [name, objectRef] of Object.entries(refs.objects)) {
|
|
32151
32226
|
out.objects[name] = {
|
|
32152
|
-
min: cloneVec3(objectRef.min, `objects.${name}.min`),
|
|
32153
|
-
max: cloneVec3(objectRef.max, `objects.${name}.max`)
|
|
32227
|
+
min: cloneVec3$2(objectRef.min, `objects.${name}.min`),
|
|
32228
|
+
max: cloneVec3$2(objectRef.max, `objects.${name}.max`)
|
|
32154
32229
|
};
|
|
32155
32230
|
}
|
|
32156
32231
|
}
|
|
@@ -32194,7 +32269,7 @@ function resolvePointFromKind(refs, kind, name, selector, originalRef) {
|
|
|
32194
32269
|
const point2 = refs.points[name];
|
|
32195
32270
|
if (!point2) return null;
|
|
32196
32271
|
if (selector != null) placementRefSelectorError(originalRef, "does not support selectors");
|
|
32197
|
-
return cloneVec3(point2, `points.${name}`);
|
|
32272
|
+
return cloneVec3$2(point2, `points.${name}`);
|
|
32198
32273
|
}
|
|
32199
32274
|
case "edges": {
|
|
32200
32275
|
const edge = refs.edges[name];
|
|
@@ -32202,8 +32277,8 @@ function resolvePointFromKind(refs, kind, name, selector, originalRef) {
|
|
|
32202
32277
|
if (selector == null || selector === "midpoint" || selector === "center") {
|
|
32203
32278
|
return midpoint$2(edge.start, edge.end);
|
|
32204
32279
|
}
|
|
32205
|
-
if (selector === "start") return cloneVec3(edge.start, `edges.${name}.start`);
|
|
32206
|
-
if (selector === "end") return cloneVec3(edge.end, `edges.${name}.end`);
|
|
32280
|
+
if (selector === "start") return cloneVec3$2(edge.start, `edges.${name}.start`);
|
|
32281
|
+
if (selector === "end") return cloneVec3$2(edge.end, `edges.${name}.end`);
|
|
32207
32282
|
placementRefSelectorError(originalRef, "supports only .start, .end, or .midpoint");
|
|
32208
32283
|
}
|
|
32209
32284
|
case "surfaces": {
|
|
@@ -32212,7 +32287,7 @@ function resolvePointFromKind(refs, kind, name, selector, originalRef) {
|
|
|
32212
32287
|
if (selector != null && selector !== "center") {
|
|
32213
32288
|
placementRefSelectorError(originalRef, "supports only .center");
|
|
32214
32289
|
}
|
|
32215
|
-
return cloneVec3(surface.center, `surfaces.${name}.center`);
|
|
32290
|
+
return cloneVec3$2(surface.center, `surfaces.${name}.center`);
|
|
32216
32291
|
}
|
|
32217
32292
|
case "objects": {
|
|
32218
32293
|
const objectRef = refs.objects[name];
|
|
@@ -41412,7 +41487,7 @@ function rodrigues(rv) {
|
|
|
41412
41487
|
function rotateVec3(R, v) {
|
|
41413
41488
|
return [R[0] * v[0] + R[1] * v[1] + R[2] * v[2], R[3] * v[0] + R[4] * v[1] + R[5] * v[2], R[6] * v[0] + R[7] * v[1] + R[8] * v[2]];
|
|
41414
41489
|
}
|
|
41415
|
-
function transformPoint(rv, translation, point2) {
|
|
41490
|
+
function transformPoint$1(rv, translation, point2) {
|
|
41416
41491
|
const R = rodrigues(rv);
|
|
41417
41492
|
const rotated = rotateVec3(R, point2);
|
|
41418
41493
|
return [rotated[0] + translation[0], rotated[1] + translation[1], rotated[2] + translation[2]];
|
|
@@ -41642,7 +41717,7 @@ function createContext(bodies) {
|
|
|
41642
41717
|
toWorld(bodyId, point2) {
|
|
41643
41718
|
const body = bodies.get(bodyId);
|
|
41644
41719
|
if (!body) throw new Error(`Unknown body: ${bodyId}`);
|
|
41645
|
-
return transformPoint(body.rotation, body.position, point2);
|
|
41720
|
+
return transformPoint$1(body.rotation, body.position, point2);
|
|
41646
41721
|
},
|
|
41647
41722
|
toWorldDir(bodyId, dir) {
|
|
41648
41723
|
const body = bodies.get(bodyId);
|
|
@@ -41656,7 +41731,7 @@ function createContext(bodies) {
|
|
|
41656
41731
|
if (!face) throw new Error(`Unknown face "${faceName}" on body "${bodyId}"`);
|
|
41657
41732
|
return {
|
|
41658
41733
|
normal: normalize3(transformDir(body.rotation, face.normal)),
|
|
41659
|
-
center: transformPoint(body.rotation, body.position, face.center)
|
|
41734
|
+
center: transformPoint$1(body.rotation, body.position, face.center)
|
|
41660
41735
|
};
|
|
41661
41736
|
},
|
|
41662
41737
|
worldAxis(bodyId, axisName) {
|
|
@@ -41665,7 +41740,7 @@ function createContext(bodies) {
|
|
|
41665
41740
|
const axis = body.axes.get(axisName);
|
|
41666
41741
|
if (!axis) throw new Error(`Unknown axis "${axisName}" on body "${bodyId}"`);
|
|
41667
41742
|
return {
|
|
41668
|
-
origin: transformPoint(body.rotation, body.position, axis.origin),
|
|
41743
|
+
origin: transformPoint$1(body.rotation, body.position, axis.origin),
|
|
41669
41744
|
direction: normalize3(transformDir(body.rotation, axis.direction))
|
|
41670
41745
|
};
|
|
41671
41746
|
},
|
|
@@ -41674,7 +41749,7 @@ function createContext(bodies) {
|
|
|
41674
41749
|
if (!body) throw new Error(`Unknown body: ${bodyId}`);
|
|
41675
41750
|
const pt = body.points.get(pointName);
|
|
41676
41751
|
if (!pt) throw new Error(`Unknown point "${pointName}" on body "${bodyId}"`);
|
|
41677
|
-
return transformPoint(body.rotation, body.position, pt.position);
|
|
41752
|
+
return transformPoint$1(body.rotation, body.position, pt.position);
|
|
41678
41753
|
}
|
|
41679
41754
|
};
|
|
41680
41755
|
}
|
|
@@ -47218,10 +47293,8 @@ class PathBuilder {
|
|
|
47218
47293
|
if (radius <= 0) throw new Error("fillet: radius must be positive");
|
|
47219
47294
|
const n = this.segs.length;
|
|
47220
47295
|
if (n < 2) throw new Error("fillet: need at least 2 segments before a fillet");
|
|
47221
|
-
const prev = this.segs[n - 2];
|
|
47222
47296
|
const curr = this.segs[n - 1];
|
|
47223
|
-
|
|
47224
|
-
const { trimA, trimB, arcSeg } = this.computeFilletGeom(radius);
|
|
47297
|
+
const { trimA, arcSeg } = this.computeFilletGeom(radius);
|
|
47225
47298
|
if (!arcSeg) throw new Error("fillet: cannot fillet these segments (parallel or degenerate)");
|
|
47226
47299
|
this.trimLastSegEnd(n - 2, trimA[0], trimA[1]);
|
|
47227
47300
|
const trimmedSeg = { ...curr };
|
|
@@ -47293,7 +47366,6 @@ class PathBuilder {
|
|
|
47293
47366
|
}
|
|
47294
47367
|
getSegDirAt(seg, which) {
|
|
47295
47368
|
if (seg.kind === "line" || seg.kind === "move") {
|
|
47296
|
-
this.segs.length;
|
|
47297
47369
|
const idx = this.segs.indexOf(seg);
|
|
47298
47370
|
if (seg.kind === "line") {
|
|
47299
47371
|
let sx, sy;
|
|
@@ -47535,6 +47607,41 @@ class PathBuilder {
|
|
|
47535
47607
|
}
|
|
47536
47608
|
return pts;
|
|
47537
47609
|
}
|
|
47610
|
+
/**
|
|
47611
|
+
* Return the open path as a sampled 2D polyline.
|
|
47612
|
+
*
|
|
47613
|
+
* This is for construction geometry such as guide rails, measured centerlines,
|
|
47614
|
+
* and curve-driven helpers where the authored path should stay open instead of
|
|
47615
|
+
* becoming a filled sketch or stroked profile.
|
|
47616
|
+
*
|
|
47617
|
+
* **Example**
|
|
47618
|
+
*
|
|
47619
|
+
* ```ts
|
|
47620
|
+
* const rail = path()
|
|
47621
|
+
* .moveTo(24, 0)
|
|
47622
|
+
* .bezierTo(32, 44, 28, 92, 18, 120)
|
|
47623
|
+
* .toPolyline();
|
|
47624
|
+
* ```
|
|
47625
|
+
*
|
|
47626
|
+
* @returns A sampled open polyline.
|
|
47627
|
+
* @category Path Builder
|
|
47628
|
+
*/
|
|
47629
|
+
toPolyline() {
|
|
47630
|
+
const moveCount = this.segs.filter((seg) => seg.kind === "move").length;
|
|
47631
|
+
if (moveCount > 1) {
|
|
47632
|
+
throw new Error("path().toPolyline() supports one continuous open path. Use separate path() builders for separate rails.");
|
|
47633
|
+
}
|
|
47634
|
+
const pts = [];
|
|
47635
|
+
for (const point2 of this.tessellate()) {
|
|
47636
|
+
if (!point2.every(Number.isFinite)) throw new Error("path().toPolyline() produced a non-finite point");
|
|
47637
|
+
const previous = pts[pts.length - 1];
|
|
47638
|
+
if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) {
|
|
47639
|
+
pts.push(point2);
|
|
47640
|
+
}
|
|
47641
|
+
}
|
|
47642
|
+
if (pts.length < 2) throw new Error("path().toPolyline() needs at least 2 points");
|
|
47643
|
+
return pts;
|
|
47644
|
+
}
|
|
47538
47645
|
// ── Output ────────────────────────────────────────────────────────────────
|
|
47539
47646
|
/**
|
|
47540
47647
|
* Close the path and return a filled `Sketch`.
|
|
@@ -49392,7 +49499,7 @@ function spurGear(options) {
|
|
|
49392
49499
|
});
|
|
49393
49500
|
return attachGearMeta(shapeWithConnectors, meta2);
|
|
49394
49501
|
}
|
|
49395
|
-
function requirePositive$
|
|
49502
|
+
function requirePositive$8(scope, name, value) {
|
|
49396
49503
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
49397
49504
|
}
|
|
49398
49505
|
function requireOptionalBore(scope, boreDiameter, maxDiameter) {
|
|
@@ -49414,8 +49521,8 @@ function cutBore$1(shape, boreDiameter) {
|
|
|
49414
49521
|
return shape.subtract(cutter);
|
|
49415
49522
|
}
|
|
49416
49523
|
function gearBodyDisk(options) {
|
|
49417
|
-
requirePositive$
|
|
49418
|
-
requirePositive$
|
|
49524
|
+
requirePositive$8("gearBodyDisk", "outerRadius", options.outerRadius);
|
|
49525
|
+
requirePositive$8("gearBodyDisk", "faceWidth", options.faceWidth);
|
|
49419
49526
|
const bore = requireOptionalBore("gearBodyDisk", options.boreDiameter, options.outerRadius * 2);
|
|
49420
49527
|
const segments = resolveSegments(options.segments);
|
|
49421
49528
|
const outer = circle2d(options.outerRadius, segments);
|
|
@@ -49423,14 +49530,14 @@ function gearBodyDisk(options) {
|
|
|
49423
49530
|
return sketchExtrude(profile, options.faceWidth);
|
|
49424
49531
|
}
|
|
49425
49532
|
function gearBodyDiskWithHub(options) {
|
|
49426
|
-
requirePositive$
|
|
49533
|
+
requirePositive$8("gearBodyDiskWithHub", "hubDiameter", options.hubDiameter);
|
|
49427
49534
|
if (options.hubDiameter >= options.outerRadius * 2) {
|
|
49428
49535
|
throw new Error('gearBodyDiskWithHub: "hubDiameter" must be smaller than the outer diameter');
|
|
49429
49536
|
}
|
|
49430
49537
|
const bore = requireOptionalBore("gearBodyDiskWithHub", options.boreDiameter, options.hubDiameter);
|
|
49431
49538
|
const base = gearBodyDisk({ ...options, boreDiameter: 0 });
|
|
49432
49539
|
const hubFaceWidth = options.hubFaceWidth ?? options.faceWidth * 1.5;
|
|
49433
|
-
requirePositive$
|
|
49540
|
+
requirePositive$8("gearBodyDiskWithHub", "hubFaceWidth", hubFaceWidth);
|
|
49434
49541
|
const hub = cylinder(hubFaceWidth, options.hubDiameter * 0.5, void 0, options.segments).translate(
|
|
49435
49542
|
0,
|
|
49436
49543
|
0,
|
|
@@ -49439,11 +49546,11 @@ function gearBodyDiskWithHub(options) {
|
|
|
49439
49546
|
return cutBore$1(base.add(hub), bore);
|
|
49440
49547
|
}
|
|
49441
49548
|
function gearBodySpoked(options) {
|
|
49442
|
-
requirePositive$
|
|
49443
|
-
requirePositive$
|
|
49444
|
-
requirePositive$
|
|
49445
|
-
requirePositive$
|
|
49446
|
-
requirePositive$
|
|
49549
|
+
requirePositive$8("gearBodySpoked", "outerRadius", options.outerRadius);
|
|
49550
|
+
requirePositive$8("gearBodySpoked", "faceWidth", options.faceWidth);
|
|
49551
|
+
requirePositive$8("gearBodySpoked", "rimWidth", options.rimWidth);
|
|
49552
|
+
requirePositive$8("gearBodySpoked", "hubDiameter", options.hubDiameter);
|
|
49553
|
+
requirePositive$8("gearBodySpoked", "spokeWidth", options.spokeWidth);
|
|
49447
49554
|
if (!Number.isInteger(options.spokeCount) || options.spokeCount < 2) {
|
|
49448
49555
|
throw new Error('gearBodySpoked: "spokeCount" must be an integer >= 2');
|
|
49449
49556
|
}
|
|
@@ -49466,12 +49573,12 @@ function gearBodySpoked(options) {
|
|
|
49466
49573
|
}
|
|
49467
49574
|
function gearBodyFromProfile(profile, options) {
|
|
49468
49575
|
if (!(profile instanceof Sketch)) throw new Error('gearBodyFromProfile: "profile" must be a Sketch');
|
|
49469
|
-
requirePositive$
|
|
49576
|
+
requirePositive$8("gearBodyFromProfile", "faceWidth", options.faceWidth);
|
|
49470
49577
|
const bore = options.boreDiameter ?? 0;
|
|
49471
49578
|
if (!Number.isFinite(bore) || bore < 0) throw new Error('gearBodyFromProfile: "boreDiameter" must be >= 0');
|
|
49472
49579
|
return cutBore$1(sketchExtrude(profile, options.faceWidth), bore);
|
|
49473
49580
|
}
|
|
49474
|
-
function requirePositive$
|
|
49581
|
+
function requirePositive$7(scope, name, value) {
|
|
49475
49582
|
if (!isFinitePositive(value)) throw new Error(`${scope}: "${name}" must be > 0`);
|
|
49476
49583
|
}
|
|
49477
49584
|
function requireFiniteAngle(scope, name, value) {
|
|
@@ -49533,7 +49640,7 @@ function buildSpurTeethRegion(options, name, faceWidth) {
|
|
|
49533
49640
|
}
|
|
49534
49641
|
function buildSolidArcRegion(options, name, faceWidth) {
|
|
49535
49642
|
const scope = "driveWheel.addSolidArcBetween";
|
|
49536
|
-
requirePositive$
|
|
49643
|
+
requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
49537
49644
|
const innerRadius = options.innerRadius ?? 0;
|
|
49538
49645
|
if (!Number.isFinite(innerRadius) || innerRadius < 0) throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
49539
49646
|
if (innerRadius >= options.outerRadius) throw new Error(`${scope}: "innerRadius" must be smaller than "outerRadius"`);
|
|
@@ -49599,7 +49706,7 @@ class DriveWheelBuilder {
|
|
|
49599
49706
|
__publicField(this, "boreDiameter");
|
|
49600
49707
|
__publicField(this, "regions", []);
|
|
49601
49708
|
if (options.body !== void 0 && !(options.body instanceof Shape$1)) throw new Error('driveWheel: "body" must be a Shape');
|
|
49602
|
-
if (options.faceWidth !== void 0) requirePositive$
|
|
49709
|
+
if (options.faceWidth !== void 0) requirePositive$7("driveWheel", "faceWidth", options.faceWidth);
|
|
49603
49710
|
const boreDiameter = options.boreDiameter ?? 0;
|
|
49604
49711
|
if (!Number.isFinite(boreDiameter) || boreDiameter < 0) throw new Error('driveWheel: "boreDiameter" must be >= 0');
|
|
49605
49712
|
this.body = options.body;
|
|
@@ -49634,7 +49741,7 @@ class DriveWheelBuilder {
|
|
|
49634
49741
|
if (options.innerRadius !== void 0 && (!Number.isFinite(options.innerRadius) || options.innerRadius < 0)) {
|
|
49635
49742
|
throw new Error(`${scope}: "innerRadius" must be >= 0`);
|
|
49636
49743
|
}
|
|
49637
|
-
if (options.outerRadius !== void 0) requirePositive$
|
|
49744
|
+
if (options.outerRadius !== void 0) requirePositive$7(scope, "outerRadius", options.outerRadius);
|
|
49638
49745
|
this.regions.push({
|
|
49639
49746
|
shape: shape.clone(),
|
|
49640
49747
|
meta: {
|
|
@@ -49700,7 +49807,7 @@ class DriveWheelBuilder {
|
|
|
49700
49807
|
resolveFaceWidth(scope, localFaceWidth) {
|
|
49701
49808
|
const faceWidth = localFaceWidth ?? this.faceWidth;
|
|
49702
49809
|
if (faceWidth === void 0) throw new Error(`${scope}: "faceWidth" is required unless driveWheel({ faceWidth }) was set`);
|
|
49703
|
-
requirePositive$
|
|
49810
|
+
requirePositive$7(scope, "faceWidth", faceWidth);
|
|
49704
49811
|
if (this.faceWidth !== void 0 && localFaceWidth !== void 0 && Math.abs(this.faceWidth - localFaceWidth) > EPSILON$1) {
|
|
49705
49812
|
throw new Error(`${scope}: region faceWidth must match driveWheel faceWidth`);
|
|
49706
49813
|
}
|
|
@@ -50853,6 +50960,1867 @@ function washer(size, options) {
|
|
|
50853
50960
|
const bore = cylinder(dims.t + 1, dims.id / 2, void 0, segs);
|
|
50854
50961
|
return outer.subtract(bore);
|
|
50855
50962
|
}
|
|
50963
|
+
function requirePositive$6(value, name) {
|
|
50964
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
|
|
50965
|
+
return value;
|
|
50966
|
+
}
|
|
50967
|
+
function requireNonNegative(value, name) {
|
|
50968
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative finite number`);
|
|
50969
|
+
return value;
|
|
50970
|
+
}
|
|
50971
|
+
function metricWasherSizeForPin(pinDiameter) {
|
|
50972
|
+
if (pinDiameter <= 2) return "M2";
|
|
50973
|
+
if (pinDiameter <= 2.5) return "M2.5";
|
|
50974
|
+
if (pinDiameter <= 3) return "M3";
|
|
50975
|
+
if (pinDiameter <= 4) return "M4";
|
|
50976
|
+
if (pinDiameter <= 5) return "M5";
|
|
50977
|
+
if (pinDiameter <= 6) return "M6";
|
|
50978
|
+
if (pinDiameter <= 8) return "M8";
|
|
50979
|
+
return "M10";
|
|
50980
|
+
}
|
|
50981
|
+
function cylinderAlongX(length4, radius, xCenter, segments) {
|
|
50982
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([1, 0, 0]).translate(xCenter - length4 / 2, 0, 0);
|
|
50983
|
+
}
|
|
50984
|
+
function tubeAlongX(length4, outerRadius, innerRadius, xCenter, segments) {
|
|
50985
|
+
return cylinderAlongX(length4, outerRadius, xCenter, segments).subtract(cylinderAlongX(length4 + 0.4, innerRadius, xCenter, segments));
|
|
50986
|
+
}
|
|
50987
|
+
function cylinderAlongY(length4, radius, yCenter, segments) {
|
|
50988
|
+
return cylinder(length4, radius, void 0, segments).pointAlong([0, 1, 0]).translate(0, yCenter - length4 / 2, 0);
|
|
50989
|
+
}
|
|
50990
|
+
function tubeAlongY(length4, outerRadius, innerRadius, yCenter, segments) {
|
|
50991
|
+
return cylinderAlongY(length4, outerRadius, yCenter, segments).subtract(cylinderAlongY(length4 + 0.4, innerRadius, yCenter, segments));
|
|
50992
|
+
}
|
|
50993
|
+
function tubeAlongZ(height, outerRadius, innerRadius, segments) {
|
|
50994
|
+
return cylinder(height, outerRadius, void 0, segments).subtract(
|
|
50995
|
+
cylinder(height + 0.4, innerRadius, void 0, segments).translate(0, 0, -0.2)
|
|
50996
|
+
);
|
|
50997
|
+
}
|
|
50998
|
+
function washerAlongX(size, xCenter, segments) {
|
|
50999
|
+
const dims = WASHER_TABLE[size];
|
|
51000
|
+
return washer(size, { segments }).pointAlong([1, 0, 0]).translate(xCenter - dims.t / 2, 0, 0);
|
|
51001
|
+
}
|
|
51002
|
+
function resolveBoltInset(raw, fallback) {
|
|
51003
|
+
if (raw === void 0) return [fallback, fallback];
|
|
51004
|
+
if (typeof raw === "number") return [requirePositive$6(raw, "boltInset"), requirePositive$6(raw, "boltInset")];
|
|
51005
|
+
if (raw.length !== 2) throw new Error("boltInset tuple must be [x, y]");
|
|
51006
|
+
return [requirePositive$6(raw[0], "boltInset[0]"), requirePositive$6(raw[1], "boltInset[1]")];
|
|
51007
|
+
}
|
|
51008
|
+
function validateBoltPositionsForServiceCover(args) {
|
|
51009
|
+
args.positions.forEach(([x2, y2], index2) => {
|
|
51010
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
51011
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] must contain finite numbers`);
|
|
51012
|
+
}
|
|
51013
|
+
if (Math.abs(x2) + args.holeRadius >= args.coverWidth / 2 || Math.abs(y2) + args.holeRadius >= args.coverDepth / 2) {
|
|
51014
|
+
throw new Error(`boltedServiceCover: boltPositions[${index2}] is too close to the cover edge`);
|
|
51015
|
+
}
|
|
51016
|
+
const overlapsOpening = Math.abs(x2) - args.holeRadius <= args.openingWidth / 2 && Math.abs(y2) - args.holeRadius <= args.openingDepth / 2;
|
|
51017
|
+
if (overlapsOpening) {
|
|
51018
|
+
throw new Error(
|
|
51019
|
+
`boltedServiceCover: boltPositions[${index2}] lands over the service opening; decrease boltInset, increase ledgeWidth, or provide a smaller opening`
|
|
51020
|
+
);
|
|
51021
|
+
}
|
|
51022
|
+
});
|
|
51023
|
+
}
|
|
51024
|
+
function placeCutterAtPositions(cutter, positions, z2) {
|
|
51025
|
+
return union(...positions.map(([x2, y2]) => cutter.translate(x2, y2, z2)));
|
|
51026
|
+
}
|
|
51027
|
+
function boltedServiceCover(options) {
|
|
51028
|
+
const width = requirePositive$6(options.width, "width");
|
|
51029
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51030
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 3, "coverThickness");
|
|
51031
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 8, "parentThickness");
|
|
51032
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
51033
|
+
const gasketThickness = Math.max(0, options.gasketThickness ?? 0.8);
|
|
51034
|
+
const gasketInset = Math.max(0, options.gasketInset ?? 2);
|
|
51035
|
+
const screwSize = options.screwSize ?? "M4";
|
|
51036
|
+
const segments = options.segments ?? 36;
|
|
51037
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51038
|
+
if (!sizeData) throw new Error(`boltedServiceCover: unsupported screwSize "${screwSize}"`);
|
|
51039
|
+
const screwLength = requirePositive$6(
|
|
51040
|
+
options.screwLength ?? parentThickness + gasketThickness + coverThickness + 4,
|
|
51041
|
+
"screwLength"
|
|
51042
|
+
);
|
|
51043
|
+
const coverFit = options.coverFit ?? "normal";
|
|
51044
|
+
const counterboreEnabled = options.counterbore ?? true;
|
|
51045
|
+
const [insetX, insetY] = resolveBoltInset(options.boltInset, Math.max(ledgeWidth * 0.65, sizeData.head * 0.75));
|
|
51046
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
51047
|
+
throw new Error("boltedServiceCover: boltInset leaves no room for a four-corner bolt pattern");
|
|
51048
|
+
}
|
|
51049
|
+
const boltPositions = options.boltPositions ?? [
|
|
51050
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
51051
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
51052
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
51053
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
51054
|
+
];
|
|
51055
|
+
if (boltPositions.length === 0) throw new Error("boltedServiceCover: boltPositions must contain at least one point");
|
|
51056
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
51057
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
51058
|
+
const openingWidth = Math.max(1, width - ledgeWidth * 2);
|
|
51059
|
+
const openingDepth = Math.max(1, depth - ledgeWidth * 2);
|
|
51060
|
+
validateBoltPositionsForServiceCover({
|
|
51061
|
+
positions: boltPositions,
|
|
51062
|
+
coverWidth: width,
|
|
51063
|
+
coverDepth: depth,
|
|
51064
|
+
openingWidth,
|
|
51065
|
+
openingDepth,
|
|
51066
|
+
holeRadius: sizeData[coverFit] / 2
|
|
51067
|
+
});
|
|
51068
|
+
const coverHole = fastenerHole({
|
|
51069
|
+
size: screwSize,
|
|
51070
|
+
fit: coverFit,
|
|
51071
|
+
depth: coverThickness + 0.6,
|
|
51072
|
+
center: true,
|
|
51073
|
+
segments,
|
|
51074
|
+
...counterboreEnabled ? { counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.4)) } } : {}
|
|
51075
|
+
});
|
|
51076
|
+
const parentTap = fastenerHole({ size: screwSize, fit: "tap", depth: parentThickness + 0.6, center: true, segments });
|
|
51077
|
+
const parentThreadEnvelope = fastenerHole({
|
|
51078
|
+
size: screwSize,
|
|
51079
|
+
fit: "close",
|
|
51080
|
+
depth: parentThickness + 0.6,
|
|
51081
|
+
center: true,
|
|
51082
|
+
segments
|
|
51083
|
+
});
|
|
51084
|
+
const openingCutter = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
51085
|
+
const parentTappedPattern = placeCutterAtPositions(parentTap, boltPositions, parentThickness / 2);
|
|
51086
|
+
const parentThreadEnvelopePattern = placeCutterAtPositions(parentThreadEnvelope, boltPositions, parentThickness / 2);
|
|
51087
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(openingCutter).subtract(parentThreadEnvelopePattern).color("#4b5563");
|
|
51088
|
+
let coverBlank = box(width, depth, coverThickness);
|
|
51089
|
+
if (options.pullTabs ?? true) {
|
|
51090
|
+
const tabWidth = Math.min(width * 0.18, Math.max(sizeData.head * 1.6, 12));
|
|
51091
|
+
const tabDepth = Math.max(4, coverThickness * 1.4);
|
|
51092
|
+
const tabOverlap = Math.min(0.5, tabDepth * 0.25);
|
|
51093
|
+
const tabY = -depth / 2 - tabDepth / 2 + tabOverlap;
|
|
51094
|
+
const tabX = width * 0.23;
|
|
51095
|
+
coverBlank = union(
|
|
51096
|
+
coverBlank,
|
|
51097
|
+
box(tabWidth, tabDepth, coverThickness).translate(-tabX, tabY, 0),
|
|
51098
|
+
box(tabWidth, tabDepth, coverThickness).translate(tabX, tabY, 0)
|
|
51099
|
+
);
|
|
51100
|
+
}
|
|
51101
|
+
const coverClearancePattern = placeCutterAtPositions(coverHole, boltPositions, coverThickness / 2);
|
|
51102
|
+
const cover2 = coverBlank.subtract(coverClearancePattern).translate(0, 0, parentThickness + gasketThickness).color("#334155");
|
|
51103
|
+
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;
|
|
51104
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
51105
|
+
const screwOriginZ = parentThickness + gasketThickness + coverThickness;
|
|
51106
|
+
const screws = boltPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
51107
|
+
const parts = [
|
|
51108
|
+
{ name: "service cover parent ledge with threaded hole envelopes", shape: parent },
|
|
51109
|
+
...gasket ? [{ name: "service cover gasket seated on ledge", shape: gasket }] : [],
|
|
51110
|
+
{ name: "bolted service cover plate with fused pull tabs", shape: cover2 },
|
|
51111
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} cover screw ${index2 + 1}`, shape }))
|
|
51112
|
+
];
|
|
51113
|
+
return {
|
|
51114
|
+
parts,
|
|
51115
|
+
parent,
|
|
51116
|
+
cover: cover2,
|
|
51117
|
+
gasket,
|
|
51118
|
+
screws,
|
|
51119
|
+
boltPositions,
|
|
51120
|
+
cutters: {
|
|
51121
|
+
coverClearance: coverClearancePattern,
|
|
51122
|
+
parentTapped: parentTappedPattern,
|
|
51123
|
+
parentThreadEnvelope: parentThreadEnvelopePattern
|
|
51124
|
+
},
|
|
51125
|
+
dims: {
|
|
51126
|
+
width,
|
|
51127
|
+
depth,
|
|
51128
|
+
coverThickness,
|
|
51129
|
+
parentThickness,
|
|
51130
|
+
ledgeWidth,
|
|
51131
|
+
gasketThickness,
|
|
51132
|
+
screwSize,
|
|
51133
|
+
screwLength,
|
|
51134
|
+
clearanceDia: sizeData[coverFit],
|
|
51135
|
+
tapDia: sizeData.tap,
|
|
51136
|
+
threadEnvelopeDia: sizeData.close
|
|
51137
|
+
}
|
|
51138
|
+
};
|
|
51139
|
+
}
|
|
51140
|
+
function datumEnclosureAssembly(options) {
|
|
51141
|
+
const width = requirePositive$6(options.width, "width");
|
|
51142
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51143
|
+
const height = requirePositive$6(options.height, "height");
|
|
51144
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.4, "wallThickness");
|
|
51145
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? wallThickness, "baseThickness");
|
|
51146
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
51147
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? Math.max(3.6, wallThickness * 1.35), "ledgeWidth");
|
|
51148
|
+
const gasketThickness = requireNonNegative(options.gasketThickness ?? 0.8, "gasketThickness");
|
|
51149
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
51150
|
+
const screwSize = options.screwSize ?? "M3";
|
|
51151
|
+
const coverFit = options.coverFit ?? "normal";
|
|
51152
|
+
const segments = options.segments ?? 32;
|
|
51153
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
51154
|
+
if (!sizeData) throw new Error(`datumEnclosureAssembly: unsupported screwSize "${screwSize}"`);
|
|
51155
|
+
const innerWidth = width - wallThickness * 2;
|
|
51156
|
+
const innerDepth = depth - wallThickness * 2;
|
|
51157
|
+
if (innerWidth <= ledgeWidth * 2 + 8 || innerDepth <= ledgeWidth * 2 + 8) {
|
|
51158
|
+
throw new Error("datumEnclosureAssembly: wallThickness and ledgeWidth leave too little internal opening");
|
|
51159
|
+
}
|
|
51160
|
+
if (height <= baseThickness + coverThickness + 4) {
|
|
51161
|
+
throw new Error("datumEnclosureAssembly: height must leave room for internal ribs and standoffs");
|
|
51162
|
+
}
|
|
51163
|
+
const standoffDiameter = requirePositive$6(
|
|
51164
|
+
options.standoffDiameter ?? Math.max(sizeData.head * 1.65, sizeData.close * 2.2),
|
|
51165
|
+
"standoffDiameter"
|
|
51166
|
+
);
|
|
51167
|
+
const minInset = wallThickness + Math.max(ledgeWidth, standoffDiameter / 2 + 1.2);
|
|
51168
|
+
const [insetX, insetY] = resolveBoltInset(options.screwInset, minInset);
|
|
51169
|
+
if (insetX * 2 >= width || insetY * 2 >= depth) {
|
|
51170
|
+
throw new Error("datumEnclosureAssembly: screwInset leaves no room for the standoff datum");
|
|
51171
|
+
}
|
|
51172
|
+
const screwPositions = options.screwPositions ?? [
|
|
51173
|
+
[-width / 2 + insetX, -depth / 2 + insetY],
|
|
51174
|
+
[width / 2 - insetX, -depth / 2 + insetY],
|
|
51175
|
+
[-width / 2 + insetX, depth / 2 - insetY],
|
|
51176
|
+
[width / 2 - insetX, depth / 2 - insetY]
|
|
51177
|
+
];
|
|
51178
|
+
if (screwPositions.length === 0) throw new Error("datumEnclosureAssembly: screwPositions must contain at least one point");
|
|
51179
|
+
for (const [index2, [x2, y2]] of screwPositions.entries()) {
|
|
51180
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
51181
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] must contain finite numbers`);
|
|
51182
|
+
}
|
|
51183
|
+
if (Math.abs(x2) + standoffDiameter / 2 > innerWidth / 2 || Math.abs(y2) + standoffDiameter / 2 > innerDepth / 2) {
|
|
51184
|
+
throw new Error(`datumEnclosureAssembly: screwPositions[${index2}] does not fit inside the enclosure walls`);
|
|
51185
|
+
}
|
|
51186
|
+
}
|
|
51187
|
+
const ribHeight = requirePositive$6(options.ribHeight ?? Math.min(height * 0.24, Math.max(2.4, baseThickness * 1.4)), "ribHeight");
|
|
51188
|
+
const ribThickness = requirePositive$6(options.ribThickness ?? Math.max(1.2, wallThickness * 0.75), "ribThickness");
|
|
51189
|
+
const portWidth = requirePositive$6(options.portWidth ?? Math.min(innerWidth * 0.28, Math.max(12, width * 0.16)), "portWidth");
|
|
51190
|
+
const portHeight = requirePositive$6(options.portHeight ?? Math.min(height * 0.42, Math.max(5, height * 0.28)), "portHeight");
|
|
51191
|
+
if (portWidth >= innerWidth - ledgeWidth * 2) {
|
|
51192
|
+
throw new Error("datumEnclosureAssembly: portWidth must fit between internal ledges and standoffs");
|
|
51193
|
+
}
|
|
51194
|
+
if (portHeight >= height - baseThickness - 1) {
|
|
51195
|
+
throw new Error("datumEnclosureAssembly: portHeight must leave material above and below the service port");
|
|
51196
|
+
}
|
|
51197
|
+
const screwLength = requirePositive$6(
|
|
51198
|
+
options.screwLength ?? coverThickness + gasketThickness + Math.max(6, height * 0.45),
|
|
51199
|
+
"screwLength"
|
|
51200
|
+
);
|
|
51201
|
+
const coverHole = fastenerHole({
|
|
51202
|
+
size: screwSize,
|
|
51203
|
+
fit: coverFit,
|
|
51204
|
+
depth: coverThickness + 0.6,
|
|
51205
|
+
center: true,
|
|
51206
|
+
segments,
|
|
51207
|
+
counterbore: { depth: Math.min(coverThickness * 0.6, Math.max(0.6, coverThickness - 0.35)) }
|
|
51208
|
+
});
|
|
51209
|
+
const standoffTap = fastenerHole({ size: screwSize, fit: "tap", depth: height + 0.8, center: true, segments });
|
|
51210
|
+
const standoffThreadEnvelope = fastenerHole({ size: screwSize, fit: "close", depth: height + 0.8, center: true, segments });
|
|
51211
|
+
const coverClearance = placeCutterAtPositions(coverHole, screwPositions, coverThickness / 2);
|
|
51212
|
+
const standoffTappedPattern = placeCutterAtPositions(standoffTap, screwPositions, height / 2);
|
|
51213
|
+
const standoffThreadEnvelopePattern = placeCutterAtPositions(standoffThreadEnvelope, screwPositions, height / 2);
|
|
51214
|
+
const fuseOverlap = Math.min(0.06, Math.max(0.02, wallThickness * 0.02));
|
|
51215
|
+
const ledgeThickness = Math.min(Math.max(1.1, coverThickness * 0.45), height * 0.2);
|
|
51216
|
+
const sideX = width / 2 - wallThickness / 2;
|
|
51217
|
+
const sideY = depth / 2 - wallThickness / 2;
|
|
51218
|
+
const ledgeZ = height - ledgeThickness;
|
|
51219
|
+
const baseSolids = [
|
|
51220
|
+
box(width, depth, baseThickness),
|
|
51221
|
+
box(wallThickness, depth, height).translate(sideX, 0, 0),
|
|
51222
|
+
box(wallThickness, depth, height).translate(-sideX, 0, 0),
|
|
51223
|
+
box(width, wallThickness, height).translate(0, sideY, 0),
|
|
51224
|
+
box(width, wallThickness, height).translate(0, -sideY, 0),
|
|
51225
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(-width / 2 + wallThickness + ledgeWidth / 2, 0, ledgeZ),
|
|
51226
|
+
box(ledgeWidth, innerDepth, ledgeThickness).translate(width / 2 - wallThickness - ledgeWidth / 2, 0, ledgeZ),
|
|
51227
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, -depth / 2 + wallThickness + ledgeWidth / 2, ledgeZ),
|
|
51228
|
+
box(innerWidth, ledgeWidth, ledgeThickness).translate(0, depth / 2 - wallThickness - ledgeWidth / 2, ledgeZ),
|
|
51229
|
+
box(Math.max(1, innerWidth - standoffDiameter * 1.8), ribThickness, ribHeight + fuseOverlap).translate(
|
|
51230
|
+
0,
|
|
51231
|
+
0,
|
|
51232
|
+
baseThickness - fuseOverlap
|
|
51233
|
+
),
|
|
51234
|
+
box(ribThickness, Math.max(1, innerDepth - standoffDiameter * 1.8), ribHeight + fuseOverlap).translate(
|
|
51235
|
+
0,
|
|
51236
|
+
0,
|
|
51237
|
+
baseThickness - fuseOverlap
|
|
51238
|
+
),
|
|
51239
|
+
...screwPositions.map(
|
|
51240
|
+
([x2, y2]) => cylinder(height - baseThickness + fuseOverlap, standoffDiameter / 2, void 0, segments).translate(
|
|
51241
|
+
x2,
|
|
51242
|
+
y2,
|
|
51243
|
+
baseThickness - fuseOverlap
|
|
51244
|
+
)
|
|
51245
|
+
)
|
|
51246
|
+
];
|
|
51247
|
+
const servicePort = box(portWidth, wallThickness + 1, portHeight).translate(
|
|
51248
|
+
0,
|
|
51249
|
+
-depth / 2 + wallThickness / 2,
|
|
51250
|
+
baseThickness + Math.max(0.8, (height - baseThickness - portHeight) * 0.35)
|
|
51251
|
+
);
|
|
51252
|
+
const base = union(...baseSolids).subtract(standoffThreadEnvelopePattern).subtract(servicePort).color("#475569");
|
|
51253
|
+
const gasketFrameCutter = box(Math.max(1, width - ledgeWidth * 2), Math.max(1, depth - ledgeWidth * 2), gasketThickness + 0.6).translate(
|
|
51254
|
+
0,
|
|
51255
|
+
0,
|
|
51256
|
+
-0.3
|
|
51257
|
+
);
|
|
51258
|
+
const gasket = gasketThickness > 0 ? box(width, depth, gasketThickness).subtract(gasketFrameCutter).subtract(placeCutterAtPositions(coverHole, screwPositions, gasketThickness / 2)).translate(0, 0, height + faceClearance).color("#111827") : null;
|
|
51259
|
+
const coverZ = height + faceClearance + (gasket ? gasketThickness + faceClearance : 0);
|
|
51260
|
+
const cover2 = box(width, depth, coverThickness).subtract(coverClearance).translate(0, 0, coverZ).color("#334155");
|
|
51261
|
+
const hardware = fastenerSet(screwSize, screwLength, { washerUnderHead: false, washerUnderNut: false, fit: coverFit, segments });
|
|
51262
|
+
const screwOriginZ = coverZ + coverThickness;
|
|
51263
|
+
const screws = screwPositions.map(([x2, y2]) => hardware.bolt.translate(x2, y2, screwOriginZ).color("#94a3b8"));
|
|
51264
|
+
const parts = [
|
|
51265
|
+
{ name: "datum enclosure base tray with walls ribs standoffs and service port", shape: base },
|
|
51266
|
+
...gasket ? [{ name: "datum enclosure gasket seated on continuous ledge", shape: gasket }] : [],
|
|
51267
|
+
{ name: "datum enclosure cover plate with matched screw pattern", shape: cover2 },
|
|
51268
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} enclosure screw ${index2 + 1}`, shape }))
|
|
51269
|
+
];
|
|
51270
|
+
return {
|
|
51271
|
+
parts,
|
|
51272
|
+
base,
|
|
51273
|
+
cover: cover2,
|
|
51274
|
+
gasket,
|
|
51275
|
+
screws,
|
|
51276
|
+
screwPositions,
|
|
51277
|
+
cutters: {
|
|
51278
|
+
coverClearance,
|
|
51279
|
+
standoffTapped: standoffTappedPattern,
|
|
51280
|
+
standoffThreadEnvelope: standoffThreadEnvelopePattern,
|
|
51281
|
+
servicePort
|
|
51282
|
+
},
|
|
51283
|
+
dims: {
|
|
51284
|
+
width,
|
|
51285
|
+
depth,
|
|
51286
|
+
height,
|
|
51287
|
+
innerWidth,
|
|
51288
|
+
innerDepth,
|
|
51289
|
+
wallThickness,
|
|
51290
|
+
baseThickness,
|
|
51291
|
+
coverThickness,
|
|
51292
|
+
ledgeWidth,
|
|
51293
|
+
gasketThickness,
|
|
51294
|
+
faceClearance,
|
|
51295
|
+
screwSize,
|
|
51296
|
+
screwLength,
|
|
51297
|
+
standoffDiameter,
|
|
51298
|
+
ribHeight,
|
|
51299
|
+
ribThickness,
|
|
51300
|
+
portWidth,
|
|
51301
|
+
portHeight,
|
|
51302
|
+
clearanceDia: sizeData[coverFit],
|
|
51303
|
+
tapDia: sizeData.tap,
|
|
51304
|
+
threadEnvelopeDia: sizeData.close
|
|
51305
|
+
}
|
|
51306
|
+
};
|
|
51307
|
+
}
|
|
51308
|
+
function snapLatchCoverAssembly(options) {
|
|
51309
|
+
const width = requirePositive$6(options.width, "width");
|
|
51310
|
+
const depth = requirePositive$6(options.depth, "depth");
|
|
51311
|
+
const coverThickness = requirePositive$6(options.coverThickness ?? 2.4, "coverThickness");
|
|
51312
|
+
const parentThickness = requirePositive$6(options.parentThickness ?? 6, "parentThickness");
|
|
51313
|
+
const ledgeWidth = requirePositive$6(options.ledgeWidth ?? 8, "ledgeWidth");
|
|
51314
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.25, "runningClearance");
|
|
51315
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
51316
|
+
const latchWidth = requirePositive$6(options.latchWidth ?? Math.min(width * 0.22, Math.max(12, width * 0.16)), "latchWidth");
|
|
51317
|
+
const latchThickness = requirePositive$6(options.latchThickness ?? 1.6, "latchThickness");
|
|
51318
|
+
const hookThrow = requirePositive$6(options.hookThrow ?? 3.2, "hookThrow");
|
|
51319
|
+
const hookThickness = requirePositive$6(options.hookThickness ?? 1.6, "hookThickness");
|
|
51320
|
+
const openingWidth = width - ledgeWidth * 2;
|
|
51321
|
+
const openingDepth = depth - ledgeWidth * 2;
|
|
51322
|
+
if (openingWidth <= Math.max(8, latchWidth * 0.8) || openingDepth <= 8) {
|
|
51323
|
+
throw new Error("snapLatchCoverAssembly: ledgeWidth leaves too little service opening under the cover");
|
|
51324
|
+
}
|
|
51325
|
+
if (latchWidth >= openingWidth) {
|
|
51326
|
+
throw new Error("snapLatchCoverAssembly: latchWidth must fit along the receiver opening");
|
|
51327
|
+
}
|
|
51328
|
+
if (latchThickness + runningClearance * 2 >= ledgeWidth) {
|
|
51329
|
+
throw new Error("snapLatchCoverAssembly: latchThickness and clearance must fit inside the receiver ledge");
|
|
51330
|
+
}
|
|
51331
|
+
if (hookThrow + latchThickness / 2 + runningClearance >= ledgeWidth * 1.5) {
|
|
51332
|
+
throw new Error("snapLatchCoverAssembly: hookThrow is too large for the available underside catch land");
|
|
51333
|
+
}
|
|
51334
|
+
const parentWidth = width + ledgeWidth * 2;
|
|
51335
|
+
const parentDepth = depth + ledgeWidth * 2;
|
|
51336
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
51337
|
+
const hookClearance = Math.min(0.08, runningClearance * 0.32);
|
|
51338
|
+
const coverMinZ = parentThickness + faceClearance;
|
|
51339
|
+
const stemMinZ = -hookClearance - hookThickness;
|
|
51340
|
+
const stemHeight = coverMinZ + fuseOverlap - stemMinZ;
|
|
51341
|
+
const slotY = openingDepth / 2 + ledgeWidth / 2;
|
|
51342
|
+
const latchWindow = (sign2) => box(latchWidth + runningClearance * 2, latchThickness + runningClearance * 2, parentThickness + 0.8).translate(
|
|
51343
|
+
0,
|
|
51344
|
+
sign2 * slotY,
|
|
51345
|
+
-0.4
|
|
51346
|
+
);
|
|
51347
|
+
const latchWindows = union(latchWindow(1), latchWindow(-1));
|
|
51348
|
+
const serviceOpening = box(openingWidth, openingDepth, parentThickness + 1).translate(0, 0, -0.5);
|
|
51349
|
+
const parent = box(parentWidth, parentDepth, parentThickness).subtract(serviceOpening).subtract(latchWindows).color("#475569");
|
|
51350
|
+
const coverPlate = box(width, depth, coverThickness).translate(0, 0, coverMinZ);
|
|
51351
|
+
const snapHook = (sign2) => {
|
|
51352
|
+
const y2 = sign2 * slotY;
|
|
51353
|
+
const stem = box(latchWidth, latchThickness, stemHeight).translate(0, y2, stemMinZ);
|
|
51354
|
+
const barb = box(latchWidth, latchThickness + hookThrow, hookThickness).translate(
|
|
51355
|
+
0,
|
|
51356
|
+
y2 + sign2 * (hookThrow / 2),
|
|
51357
|
+
stemMinZ
|
|
51358
|
+
);
|
|
51359
|
+
const rootRib = box(latchWidth, Math.max(latchThickness, hookThrow * 0.55), coverThickness * 0.65).translate(
|
|
51360
|
+
0,
|
|
51361
|
+
y2 - sign2 * (ledgeWidth * 0.18),
|
|
51362
|
+
coverMinZ
|
|
51363
|
+
);
|
|
51364
|
+
return union(stem, barb, rootRib);
|
|
51365
|
+
};
|
|
51366
|
+
const cover2 = union(coverPlate, snapHook(1), snapHook(-1)).color("#111827");
|
|
51367
|
+
const parts = [
|
|
51368
|
+
{ name: "snap cover receiver frame with latch windows and catch lands", shape: parent },
|
|
51369
|
+
{ name: "one-piece snap cover with fused hooks and underside barbs", shape: cover2 }
|
|
51370
|
+
];
|
|
51371
|
+
return {
|
|
51372
|
+
parts,
|
|
51373
|
+
parent,
|
|
51374
|
+
cover: cover2,
|
|
51375
|
+
cutters: {
|
|
51376
|
+
serviceOpening,
|
|
51377
|
+
latchWindows
|
|
51378
|
+
},
|
|
51379
|
+
dims: {
|
|
51380
|
+
width,
|
|
51381
|
+
depth,
|
|
51382
|
+
parentWidth,
|
|
51383
|
+
parentDepth,
|
|
51384
|
+
openingWidth,
|
|
51385
|
+
openingDepth,
|
|
51386
|
+
coverThickness,
|
|
51387
|
+
parentThickness,
|
|
51388
|
+
ledgeWidth,
|
|
51389
|
+
latchWidth,
|
|
51390
|
+
latchThickness,
|
|
51391
|
+
hookThrow,
|
|
51392
|
+
hookThickness,
|
|
51393
|
+
runningClearance,
|
|
51394
|
+
faceClearance
|
|
51395
|
+
}
|
|
51396
|
+
};
|
|
51397
|
+
}
|
|
51398
|
+
function pinnedLeverAssembly(options) {
|
|
51399
|
+
const armLength = requirePositive$6(options.armLength, "armLength");
|
|
51400
|
+
const armWidth = requirePositive$6(options.armWidth ?? 10, "armWidth");
|
|
51401
|
+
const leverThickness = requirePositive$6(options.leverThickness ?? 5, "leverThickness");
|
|
51402
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 5, "pinDiameter");
|
|
51403
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
51404
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51405
|
+
const hubRadius = requirePositive$6(options.hubRadius ?? Math.max(armWidth * 0.85, pinDiameter * 1.8), "hubRadius");
|
|
51406
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(6, pinDiameter * 1.4), "supportThickness");
|
|
51407
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? hubRadius * 2 + 18, "supportWidth");
|
|
51408
|
+
const supportDepth = requirePositive$6(options.supportDepth ?? Math.max(armWidth + 18, hubRadius * 2 + 10), "supportDepth");
|
|
51409
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(pinDiameter);
|
|
51410
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
51411
|
+
if (!washerDims) throw new Error(`pinnedLeverAssembly: unsupported washerSize "${washerSize}"`);
|
|
51412
|
+
if (washerDims.id <= pinDiameter) {
|
|
51413
|
+
throw new Error(`pinnedLeverAssembly: ${washerSize} washer inner diameter is too small for a ${pinDiameter} mm pin`);
|
|
51414
|
+
}
|
|
51415
|
+
if (hubRadius <= boreDiameter / 2 + Math.max(1, pinDiameter * 0.25)) {
|
|
51416
|
+
throw new Error("pinnedLeverAssembly: hubRadius leaves too little material around the pivot bore");
|
|
51417
|
+
}
|
|
51418
|
+
if (supportWidth <= boreDiameter + 4 || supportDepth <= boreDiameter + 4) {
|
|
51419
|
+
throw new Error("pinnedLeverAssembly: support dimensions leave too little material around the pivot bore");
|
|
51420
|
+
}
|
|
51421
|
+
const segments = options.segments ?? 40;
|
|
51422
|
+
const gripLength = requirePositive$6(options.gripLength ?? Math.min(armLength * 0.32, Math.max(16, armWidth * 2.4)), "gripLength");
|
|
51423
|
+
const gripWidth = requirePositive$6(options.gripWidth ?? armWidth * 1.55, "gripWidth");
|
|
51424
|
+
if (gripLength >= armLength) throw new Error("pinnedLeverAssembly: gripLength must be shorter than armLength");
|
|
51425
|
+
const armOverlap = Math.min(hubRadius * 0.65, armLength * 0.25);
|
|
51426
|
+
const armStartX = hubRadius - armOverlap;
|
|
51427
|
+
const armCenterX = armStartX + armLength / 2;
|
|
51428
|
+
const gripCenterX = armStartX + armLength - gripLength / 2;
|
|
51429
|
+
const runningClearance = 0.03;
|
|
51430
|
+
const lowerWasherZ = supportThickness + runningClearance;
|
|
51431
|
+
const leverZ = lowerWasherZ + washerDims.t + runningClearance;
|
|
51432
|
+
const upperWasherZ = leverZ + leverThickness + runningClearance;
|
|
51433
|
+
const stackHeight = upperWasherZ + washerDims.t;
|
|
51434
|
+
const pinHeadThickness = Math.max(washerDims.t, pinDiameter * 0.35);
|
|
51435
|
+
const pinHeadRadius = Math.max(washerDims.od * 0.42, pinDiameter * 0.8);
|
|
51436
|
+
const supportBore = cylinder(supportThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51437
|
+
let supportBlank = box(supportWidth, supportDepth, supportThickness);
|
|
51438
|
+
if (options.stopBlock ?? true) {
|
|
51439
|
+
const stopLength = Math.min(armLength * 0.22, Math.max(10, armWidth * 1.4));
|
|
51440
|
+
const stopWidth = Math.max(4, pinDiameter * 0.7);
|
|
51441
|
+
const stopHeight = supportThickness;
|
|
51442
|
+
const stopX = hubRadius + stopLength / 2;
|
|
51443
|
+
const stopY = armWidth / 2 + stopWidth / 2 + runningClearance;
|
|
51444
|
+
supportBlank = union(supportBlank, box(stopLength, stopWidth, stopHeight).translate(stopX, stopY, 0));
|
|
51445
|
+
}
|
|
51446
|
+
const support = supportBlank.subtract(supportBore).color("#475569");
|
|
51447
|
+
const hub = cylinder(leverThickness, hubRadius, void 0, segments);
|
|
51448
|
+
const arm = box(armLength, armWidth, leverThickness).translate(armCenterX, 0, 0);
|
|
51449
|
+
const grip = box(gripLength, gripWidth, leverThickness).translate(gripCenterX, 0, 0);
|
|
51450
|
+
const leverSolids = [hub, arm, grip];
|
|
51451
|
+
if (options.detentBoss ?? true) {
|
|
51452
|
+
const bossRadius = Math.min(armWidth * 0.42, hubRadius * 0.42);
|
|
51453
|
+
const bossX = hubRadius + Math.min(armLength * 0.22, armWidth * 2);
|
|
51454
|
+
const bossY = -armWidth / 2 - bossRadius * 0.45;
|
|
51455
|
+
leverSolids.push(cylinder(leverThickness, bossRadius, void 0, segments).translate(bossX, bossY, 0));
|
|
51456
|
+
}
|
|
51457
|
+
const leverBore = cylinder(leverThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51458
|
+
const lever = union(...leverSolids).subtract(leverBore).translate(0, 0, leverZ).color("#7f1d1d");
|
|
51459
|
+
const lowerWasher = washer(washerSize, { segments }).translate(0, 0, lowerWasherZ).color("#94a3b8");
|
|
51460
|
+
const upperWasher = washer(washerSize, { segments }).translate(0, 0, upperWasherZ).color("#94a3b8");
|
|
51461
|
+
const shaft = cylinder(stackHeight, pinDiameter / 2, void 0, segments);
|
|
51462
|
+
const lowerRetainer = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, -pinHeadThickness - runningClearance);
|
|
51463
|
+
const upperHead = cylinder(pinHeadThickness, pinHeadRadius, void 0, segments).translate(0, 0, stackHeight + runningClearance);
|
|
51464
|
+
const pin = union(shaft, lowerRetainer, upperHead).color("#cbd5e1");
|
|
51465
|
+
const pivotBore = cylinder(stackHeight + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51466
|
+
const parts = [
|
|
51467
|
+
{ name: "pivot support block with bearing bore and low stop land", shape: support },
|
|
51468
|
+
{ name: "lower thrust washer under pinned lever", shape: lowerWasher },
|
|
51469
|
+
{ name: "fused pinned lever with hub arm grip and detent boss", shape: lever },
|
|
51470
|
+
{ name: "upper thrust washer over pinned lever", shape: upperWasher },
|
|
51471
|
+
{ name: "retained pivot pin through lever stack", shape: pin }
|
|
51472
|
+
];
|
|
51473
|
+
return {
|
|
51474
|
+
parts,
|
|
51475
|
+
support,
|
|
51476
|
+
lever,
|
|
51477
|
+
pin,
|
|
51478
|
+
washers: {
|
|
51479
|
+
lower: lowerWasher,
|
|
51480
|
+
upper: upperWasher
|
|
51481
|
+
},
|
|
51482
|
+
cutters: {
|
|
51483
|
+
pivotBore
|
|
51484
|
+
},
|
|
51485
|
+
dims: {
|
|
51486
|
+
armLength,
|
|
51487
|
+
armWidth,
|
|
51488
|
+
leverThickness,
|
|
51489
|
+
hubRadius,
|
|
51490
|
+
pinDiameter,
|
|
51491
|
+
boreDiameter,
|
|
51492
|
+
supportWidth,
|
|
51493
|
+
supportDepth,
|
|
51494
|
+
supportThickness,
|
|
51495
|
+
washerSize,
|
|
51496
|
+
washerThickness: washerDims.t,
|
|
51497
|
+
stackHeight
|
|
51498
|
+
}
|
|
51499
|
+
};
|
|
51500
|
+
}
|
|
51501
|
+
function retainedShaftAssembly(options) {
|
|
51502
|
+
const supportSpacing = requirePositive$6(options.supportSpacing, "supportSpacing");
|
|
51503
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? 8, "shaftDiameter");
|
|
51504
|
+
const boreClearance = requireNonNegative(options.boreClearance ?? 0.35, "boreClearance");
|
|
51505
|
+
const boreDiameter = shaftDiameter + boreClearance;
|
|
51506
|
+
const supportThickness = requirePositive$6(options.supportThickness ?? Math.max(5, shaftDiameter * 0.75), "supportThickness");
|
|
51507
|
+
const washerSize = options.washerSize ?? metricWasherSizeForPin(shaftDiameter);
|
|
51508
|
+
const washerDims = WASHER_TABLE[washerSize];
|
|
51509
|
+
if (!washerDims) throw new Error(`retainedShaftAssembly: unsupported washerSize "${washerSize}"`);
|
|
51510
|
+
if (washerDims.id <= shaftDiameter) {
|
|
51511
|
+
throw new Error(`retainedShaftAssembly: ${washerSize} washer inner diameter is too small for a ${shaftDiameter} mm shaft`);
|
|
51512
|
+
}
|
|
51513
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? shaftDiameter * 3, "knobDiameter");
|
|
51514
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(8, shaftDiameter), "knobThickness");
|
|
51515
|
+
const retainerThickness = requirePositive$6(
|
|
51516
|
+
options.retainerThickness ?? Math.max(washerDims.t, shaftDiameter * 0.35),
|
|
51517
|
+
"retainerThickness"
|
|
51518
|
+
);
|
|
51519
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
51520
|
+
const supportWidth = requirePositive$6(options.supportWidth ?? Math.max(28, knobDiameter * 1.25), "supportWidth");
|
|
51521
|
+
const supportHeight = requirePositive$6(options.supportHeight ?? Math.max(34, knobDiameter * 1.45), "supportHeight");
|
|
51522
|
+
const segments = options.segments ?? 40;
|
|
51523
|
+
if (supportSpacing <= supportThickness) {
|
|
51524
|
+
throw new Error("retainedShaftAssembly: supportSpacing must leave a gap between support cheeks");
|
|
51525
|
+
}
|
|
51526
|
+
if (supportWidth <= boreDiameter + 4 || supportHeight <= boreDiameter + 4) {
|
|
51527
|
+
throw new Error("retainedShaftAssembly: support dimensions leave too little material around the shaft bore");
|
|
51528
|
+
}
|
|
51529
|
+
const leftSupportX = -supportSpacing / 2;
|
|
51530
|
+
const rightSupportX = supportSpacing / 2;
|
|
51531
|
+
const leftOuterFaceX = leftSupportX - supportThickness / 2;
|
|
51532
|
+
const rightOuterFaceX = rightSupportX + supportThickness / 2;
|
|
51533
|
+
const leftWasherX = leftOuterFaceX - runningClearance - washerDims.t / 2;
|
|
51534
|
+
const rightWasherX = rightOuterFaceX + runningClearance + washerDims.t / 2;
|
|
51535
|
+
const leftKnobX = leftOuterFaceX - runningClearance * 2 - washerDims.t - knobThickness / 2;
|
|
51536
|
+
const rightKnobX = rightOuterFaceX + runningClearance * 2 + washerDims.t + knobThickness / 2;
|
|
51537
|
+
const leftStackOuterX = leftKnobX - knobThickness / 2;
|
|
51538
|
+
const rightStackOuterX = rightKnobX + knobThickness / 2;
|
|
51539
|
+
const minimumShaftLength = rightStackOuterX - leftStackOuterX + retainerThickness * 2 + runningClearance * 2;
|
|
51540
|
+
const shaftLength = requirePositive$6(options.shaftLength ?? minimumShaftLength, "shaftLength");
|
|
51541
|
+
if (shaftLength < minimumShaftLength) {
|
|
51542
|
+
throw new Error("retainedShaftAssembly: shaftLength is too short to retain both supports, washers, and knobs");
|
|
51543
|
+
}
|
|
51544
|
+
const supportBore = cylinderAlongX(supportThickness + 1, boreDiameter / 2, 0, segments);
|
|
51545
|
+
const makeSupport = (x2) => box(supportThickness, supportWidth, supportHeight).translate(x2, 0, -supportHeight / 2).subtract(supportBore.translate(x2, 0, 0)).color("#334155");
|
|
51546
|
+
const knobBore = cylinder(knobThickness + 1, boreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
51547
|
+
const makeKnob = (x2) => cylinder(knobThickness, knobDiameter / 2, void 0, 18).subtract(knobBore).pointAlong([1, 0, 0]).translate(x2 - knobThickness / 2, 0, 0).color("#111827");
|
|
51548
|
+
const retainerRadius = Math.max(shaftDiameter * 0.85, knobDiameter * 0.36);
|
|
51549
|
+
const shaftCore = cylinderAlongX(shaftLength, shaftDiameter / 2, 0, segments);
|
|
51550
|
+
const leftRetainer = cylinderAlongX(retainerThickness, retainerRadius, -shaftLength / 2 + retainerThickness / 2, segments);
|
|
51551
|
+
const rightRetainer = cylinderAlongX(retainerThickness, retainerRadius, shaftLength / 2 - retainerThickness / 2, segments);
|
|
51552
|
+
const shaft = union(shaftCore, leftRetainer, rightRetainer).color("#cbd5e1");
|
|
51553
|
+
const leftSupport = makeSupport(leftSupportX);
|
|
51554
|
+
const rightSupport = makeSupport(rightSupportX);
|
|
51555
|
+
const leftWasher = washerAlongX(washerSize, leftWasherX, segments).color("#94a3b8");
|
|
51556
|
+
const rightWasher = washerAlongX(washerSize, rightWasherX, segments).color("#94a3b8");
|
|
51557
|
+
const leftKnob = makeKnob(leftKnobX);
|
|
51558
|
+
const rightKnob = makeKnob(rightKnobX);
|
|
51559
|
+
const shaftBore = cylinderAlongX(supportThickness + knobThickness + 2, boreDiameter / 2, 0, segments);
|
|
51560
|
+
const parts = [
|
|
51561
|
+
{ name: "left bored support cheek for retained shaft", shape: leftSupport },
|
|
51562
|
+
{ name: "right bored support cheek for retained shaft", shape: rightSupport },
|
|
51563
|
+
{ name: "retained through shaft with end heads", shape: shaft },
|
|
51564
|
+
{ name: `left ${washerSize} thrust washer on shaft`, shape: leftWasher },
|
|
51565
|
+
{ name: `right ${washerSize} thrust washer on shaft`, shape: rightWasher },
|
|
51566
|
+
{ name: "left retained hand knob with shaft bore", shape: leftKnob },
|
|
51567
|
+
{ name: "right retained hand knob with shaft bore", shape: rightKnob }
|
|
51568
|
+
];
|
|
51569
|
+
return {
|
|
51570
|
+
parts,
|
|
51571
|
+
supports: {
|
|
51572
|
+
left: leftSupport,
|
|
51573
|
+
right: rightSupport
|
|
51574
|
+
},
|
|
51575
|
+
shaft,
|
|
51576
|
+
washers: {
|
|
51577
|
+
left: leftWasher,
|
|
51578
|
+
right: rightWasher
|
|
51579
|
+
},
|
|
51580
|
+
knobs: {
|
|
51581
|
+
left: leftKnob,
|
|
51582
|
+
right: rightKnob
|
|
51583
|
+
},
|
|
51584
|
+
cutters: {
|
|
51585
|
+
shaftBore
|
|
51586
|
+
},
|
|
51587
|
+
dims: {
|
|
51588
|
+
supportSpacing,
|
|
51589
|
+
supportThickness,
|
|
51590
|
+
supportWidth,
|
|
51591
|
+
supportHeight,
|
|
51592
|
+
shaftDiameter,
|
|
51593
|
+
shaftLength,
|
|
51594
|
+
boreDiameter,
|
|
51595
|
+
washerSize,
|
|
51596
|
+
washerThickness: washerDims.t,
|
|
51597
|
+
knobDiameter,
|
|
51598
|
+
knobThickness,
|
|
51599
|
+
retainerThickness,
|
|
51600
|
+
runningClearance
|
|
51601
|
+
}
|
|
51602
|
+
};
|
|
51603
|
+
}
|
|
51604
|
+
function capturedLinearSlide(options) {
|
|
51605
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51606
|
+
const railWidth = requirePositive$6(options.railWidth ?? 38, "railWidth");
|
|
51607
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 2.4, "baseThickness");
|
|
51608
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2, "wallThickness");
|
|
51609
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 9, "wallHeight");
|
|
51610
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
51611
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 1.8, "lipThickness");
|
|
51612
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51613
|
+
const endStopLength = requirePositive$6(options.endStopLength ?? 6, "endStopLength");
|
|
51614
|
+
const carriageLength = requirePositive$6(options.carriageLength ?? length4 * 0.32, "carriageLength");
|
|
51615
|
+
const innerWidth = railWidth - wallThickness * 2;
|
|
51616
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
51617
|
+
if (innerWidth <= 0) throw new Error("capturedLinearSlide: wallThickness leaves no inner rail width");
|
|
51618
|
+
if (throatWidth <= 0) throw new Error("capturedLinearSlide: lipWidth closes the rail throat");
|
|
51619
|
+
const carriageWidth = requirePositive$6(options.carriageWidth ?? innerWidth - runningClearance * 2, "carriageWidth");
|
|
51620
|
+
const carriageThickness = requirePositive$6(options.carriageThickness ?? 4, "carriageThickness");
|
|
51621
|
+
if (carriageWidth >= innerWidth - runningClearance) {
|
|
51622
|
+
throw new Error("capturedLinearSlide: carriageWidth leaves too little side clearance inside the rail");
|
|
51623
|
+
}
|
|
51624
|
+
if (carriageWidth <= throatWidth + runningClearance) {
|
|
51625
|
+
throw new Error("capturedLinearSlide: carriageWidth must be wider than the lip throat so the rail actually captures it");
|
|
51626
|
+
}
|
|
51627
|
+
if (carriageThickness + runningClearance * 2 >= wallHeight) {
|
|
51628
|
+
throw new Error("capturedLinearSlide: carriage is too tall to clear the return lips");
|
|
51629
|
+
}
|
|
51630
|
+
const maxTravel = length4 - endStopLength * 2 - carriageLength;
|
|
51631
|
+
if (maxTravel <= 0) {
|
|
51632
|
+
throw new Error("capturedLinearSlide: rail length, end stops, and carriage length leave no travel");
|
|
51633
|
+
}
|
|
51634
|
+
const travel = options.travel ?? maxTravel / 2;
|
|
51635
|
+
if (!Number.isFinite(travel) || travel < 0 || travel > maxTravel) {
|
|
51636
|
+
throw new Error(`capturedLinearSlide: travel must be between 0 and ${maxTravel}`);
|
|
51637
|
+
}
|
|
51638
|
+
const carriageCenterX = -maxTravel / 2 + travel;
|
|
51639
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
51640
|
+
const sideY = railWidth / 2 - wallThickness / 2;
|
|
51641
|
+
const lipY = railWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
51642
|
+
const stopZ = baseThickness - fuseOverlap;
|
|
51643
|
+
const rail2 = union(
|
|
51644
|
+
box(length4, railWidth, baseThickness),
|
|
51645
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
51646
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
51647
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51648
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51649
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(-length4 / 2 + endStopLength / 2, 0, stopZ),
|
|
51650
|
+
box(endStopLength, throatWidth, carriageThickness + fuseOverlap).translate(length4 / 2 - endStopLength / 2, 0, stopZ)
|
|
51651
|
+
).color("#475569");
|
|
51652
|
+
const carriage = union(
|
|
51653
|
+
box(carriageLength, carriageWidth, carriageThickness),
|
|
51654
|
+
box(carriageLength * 0.78, throatWidth - runningClearance * 2, Math.max(1, carriageThickness * 0.38)).translate(
|
|
51655
|
+
0,
|
|
51656
|
+
0,
|
|
51657
|
+
carriageThickness
|
|
51658
|
+
)
|
|
51659
|
+
).translate(carriageCenterX, 0, baseThickness + runningClearance).color("#111827");
|
|
51660
|
+
const parts = [
|
|
51661
|
+
{ name: "captured linear rail with return lips and end stops", shape: rail2 },
|
|
51662
|
+
{ name: "sliding carriage captured under rail lips", shape: carriage }
|
|
51663
|
+
];
|
|
51664
|
+
return {
|
|
51665
|
+
parts,
|
|
51666
|
+
rail: rail2,
|
|
51667
|
+
carriage,
|
|
51668
|
+
dims: {
|
|
51669
|
+
length: length4,
|
|
51670
|
+
railWidth,
|
|
51671
|
+
innerWidth,
|
|
51672
|
+
throatWidth,
|
|
51673
|
+
baseThickness,
|
|
51674
|
+
wallThickness,
|
|
51675
|
+
wallHeight,
|
|
51676
|
+
lipWidth,
|
|
51677
|
+
lipThickness,
|
|
51678
|
+
carriageLength,
|
|
51679
|
+
carriageWidth,
|
|
51680
|
+
carriageThickness,
|
|
51681
|
+
endStopLength,
|
|
51682
|
+
runningClearance,
|
|
51683
|
+
maxTravel,
|
|
51684
|
+
travel,
|
|
51685
|
+
carriageCenterX
|
|
51686
|
+
}
|
|
51687
|
+
};
|
|
51688
|
+
}
|
|
51689
|
+
function capturedCartridgeGuideAssembly(options) {
|
|
51690
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51691
|
+
const guideWidth = requirePositive$6(options.guideWidth ?? 42, "guideWidth");
|
|
51692
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? 3, "baseThickness");
|
|
51693
|
+
const wallThickness = requirePositive$6(options.wallThickness ?? 2.5, "wallThickness");
|
|
51694
|
+
const wallHeight = requirePositive$6(options.wallHeight ?? 12, "wallHeight");
|
|
51695
|
+
const lipWidth = requirePositive$6(options.lipWidth ?? 4, "lipWidth");
|
|
51696
|
+
const lipThickness = requirePositive$6(options.lipThickness ?? 2, "lipThickness");
|
|
51697
|
+
const rearStopLength = requirePositive$6(options.rearStopLength ?? 7, "rearStopLength");
|
|
51698
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
51699
|
+
const cartridgeLength = requirePositive$6(options.cartridgeLength ?? length4 * 0.58, "cartridgeLength");
|
|
51700
|
+
const cartridgeHeight = requirePositive$6(options.cartridgeHeight ?? 10, "cartridgeHeight");
|
|
51701
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? 3, "flangeThickness");
|
|
51702
|
+
const pullTabLength = requirePositive$6(options.pullTabLength ?? 10, "pullTabLength");
|
|
51703
|
+
const innerWidth = guideWidth - wallThickness * 2;
|
|
51704
|
+
const throatWidth = innerWidth - lipWidth * 2;
|
|
51705
|
+
if (innerWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: wallThickness leaves no inner guide width");
|
|
51706
|
+
if (throatWidth <= 0) throw new Error("capturedCartridgeGuideAssembly: lipWidth closes the guide throat");
|
|
51707
|
+
if (wallHeight <= lipThickness + flangeThickness + runningClearance * 2) {
|
|
51708
|
+
throw new Error("capturedCartridgeGuideAssembly: wallHeight leaves too little vertical capture clearance");
|
|
51709
|
+
}
|
|
51710
|
+
const cartridgeWidth = requirePositive$6(options.cartridgeWidth ?? innerWidth - runningClearance * 2, "cartridgeWidth");
|
|
51711
|
+
const cartridgeBodyWidth = throatWidth - runningClearance * 2;
|
|
51712
|
+
if (cartridgeBodyWidth <= 0) {
|
|
51713
|
+
throw new Error("capturedCartridgeGuideAssembly: throatWidth and runningClearance leave no cartridge body width");
|
|
51714
|
+
}
|
|
51715
|
+
if (cartridgeWidth >= innerWidth - runningClearance) {
|
|
51716
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridgeWidth leaves too little side clearance inside the guide");
|
|
51717
|
+
}
|
|
51718
|
+
if (cartridgeWidth <= throatWidth + runningClearance) {
|
|
51719
|
+
throw new Error("capturedCartridgeGuideAssembly: cartridge flange must be wider than the guide throat so the cartridge is captured");
|
|
51720
|
+
}
|
|
51721
|
+
const maxInsertion = length4 - rearStopLength - cartridgeLength;
|
|
51722
|
+
if (maxInsertion <= 0) {
|
|
51723
|
+
throw new Error("capturedCartridgeGuideAssembly: length, rearStopLength, and cartridgeLength leave no insertion travel");
|
|
51724
|
+
}
|
|
51725
|
+
const insertion = options.insertion ?? maxInsertion * 0.4;
|
|
51726
|
+
if (!Number.isFinite(insertion) || insertion < 0 || insertion > maxInsertion) {
|
|
51727
|
+
throw new Error(`capturedCartridgeGuideAssembly: insertion must be between 0 and ${maxInsertion}`);
|
|
51728
|
+
}
|
|
51729
|
+
const cartridgeCenterX = -length4 / 2 + cartridgeLength / 2 + insertion;
|
|
51730
|
+
const fuseOverlap = Math.min(0.04, runningClearance * 0.1);
|
|
51731
|
+
const sideY = guideWidth / 2 - wallThickness / 2;
|
|
51732
|
+
const lipY = guideWidth / 2 - wallThickness - lipWidth / 2 + fuseOverlap / 2;
|
|
51733
|
+
const guide = union(
|
|
51734
|
+
box(length4, guideWidth, baseThickness),
|
|
51735
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, sideY, baseThickness - fuseOverlap),
|
|
51736
|
+
box(length4, wallThickness, wallHeight + fuseOverlap).translate(0, -sideY, baseThickness - fuseOverlap),
|
|
51737
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51738
|
+
box(length4, lipWidth, lipThickness + fuseOverlap).translate(0, -lipY, baseThickness + wallHeight - fuseOverlap),
|
|
51739
|
+
box(rearStopLength, throatWidth, Math.max(flangeThickness + runningClearance, 4)).translate(
|
|
51740
|
+
length4 / 2 - rearStopLength / 2,
|
|
51741
|
+
0,
|
|
51742
|
+
baseThickness - fuseOverlap
|
|
51743
|
+
)
|
|
51744
|
+
).color("#475569");
|
|
51745
|
+
const flangeZ = baseThickness + runningClearance;
|
|
51746
|
+
const bodyHeight = Math.max(1, cartridgeHeight - flangeThickness);
|
|
51747
|
+
const bodyZ = flangeZ + flangeThickness;
|
|
51748
|
+
const tabOverlap = Math.min(0.6, pullTabLength * 0.15);
|
|
51749
|
+
const pullTabX = cartridgeCenterX - cartridgeLength / 2 - pullTabLength / 2 + tabOverlap;
|
|
51750
|
+
const pullTabWidth = Math.max(cartridgeBodyWidth * 0.55, 12);
|
|
51751
|
+
const cartridge = union(
|
|
51752
|
+
box(cartridgeLength, cartridgeWidth, flangeThickness).translate(cartridgeCenterX, 0, flangeZ),
|
|
51753
|
+
box(cartridgeLength * 0.88, cartridgeBodyWidth, bodyHeight).translate(cartridgeCenterX, 0, bodyZ),
|
|
51754
|
+
box(pullTabLength, pullTabWidth, Math.max(flangeThickness, 3)).translate(pullTabX, 0, flangeZ)
|
|
51755
|
+
).color("#111827");
|
|
51756
|
+
const parts = [
|
|
51757
|
+
{ name: "captured cartridge guide with return lips and rear stop", shape: guide },
|
|
51758
|
+
{ name: "removable cartridge with captured flange and pull tab", shape: cartridge }
|
|
51759
|
+
];
|
|
51760
|
+
return {
|
|
51761
|
+
parts,
|
|
51762
|
+
guide,
|
|
51763
|
+
cartridge,
|
|
51764
|
+
dims: {
|
|
51765
|
+
length: length4,
|
|
51766
|
+
guideWidth,
|
|
51767
|
+
innerWidth,
|
|
51768
|
+
throatWidth,
|
|
51769
|
+
baseThickness,
|
|
51770
|
+
wallThickness,
|
|
51771
|
+
wallHeight,
|
|
51772
|
+
lipWidth,
|
|
51773
|
+
lipThickness,
|
|
51774
|
+
rearStopLength,
|
|
51775
|
+
cartridgeLength,
|
|
51776
|
+
cartridgeWidth,
|
|
51777
|
+
cartridgeBodyWidth,
|
|
51778
|
+
cartridgeHeight,
|
|
51779
|
+
flangeThickness,
|
|
51780
|
+
pullTabLength,
|
|
51781
|
+
runningClearance,
|
|
51782
|
+
maxInsertion,
|
|
51783
|
+
insertion,
|
|
51784
|
+
cartridgeCenterX
|
|
51785
|
+
}
|
|
51786
|
+
};
|
|
51787
|
+
}
|
|
51788
|
+
function livingHingeCoverAssembly(options) {
|
|
51789
|
+
const width = requirePositive$6(options.width, "width");
|
|
51790
|
+
const coverDepth = requirePositive$6(options.coverDepth ?? 42, "coverDepth");
|
|
51791
|
+
const fixedLeafDepth = requirePositive$6(options.fixedLeafDepth ?? 18, "fixedLeafDepth");
|
|
51792
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 2, "leafThickness");
|
|
51793
|
+
const hingeWebWidth = requirePositive$6(options.hingeWebWidth ?? 3.2, "hingeWebWidth");
|
|
51794
|
+
const hingeWebThickness = requirePositive$6(options.hingeWebThickness ?? 0.45, "hingeWebThickness");
|
|
51795
|
+
const pullLipDepth = requirePositive$6(options.pullLipDepth ?? 5, "pullLipDepth");
|
|
51796
|
+
const snapBarbWidth = requirePositive$6(options.snapBarbWidth ?? width * 0.35, "snapBarbWidth");
|
|
51797
|
+
const snapBarbDepth = requirePositive$6(options.snapBarbDepth ?? 2.4, "snapBarbDepth");
|
|
51798
|
+
const snapBarbHeight = requirePositive$6(options.snapBarbHeight ?? 1.4, "snapBarbHeight");
|
|
51799
|
+
const catchLandDepth = requirePositive$6(options.catchLandDepth ?? 2.4, "catchLandDepth");
|
|
51800
|
+
if (hingeWebThickness >= leafThickness * 0.55) {
|
|
51801
|
+
throw new Error("livingHingeCoverAssembly: hingeWebThickness must be much thinner than the rigid leaves");
|
|
51802
|
+
}
|
|
51803
|
+
if (hingeWebWidth >= Math.min(coverDepth, fixedLeafDepth) * 0.45) {
|
|
51804
|
+
throw new Error("livingHingeCoverAssembly: hingeWebWidth is too wide for the selected leaves");
|
|
51805
|
+
}
|
|
51806
|
+
if (snapBarbWidth >= width - 2) {
|
|
51807
|
+
throw new Error("livingHingeCoverAssembly: snapBarbWidth must leave side material on the cover leaf");
|
|
51808
|
+
}
|
|
51809
|
+
const fuseOverlap = Math.min(0.04, hingeWebWidth * 0.02);
|
|
51810
|
+
const fixedCenterY = -hingeWebWidth / 2 - fixedLeafDepth / 2 + fuseOverlap / 2;
|
|
51811
|
+
const coverCenterY = hingeWebWidth / 2 + coverDepth / 2 - fuseOverlap / 2;
|
|
51812
|
+
const fixedLeaf = box(width, fixedLeafDepth + fuseOverlap, leafThickness).translate(0, fixedCenterY, 0);
|
|
51813
|
+
const movingLeaf = box(width, coverDepth + fuseOverlap, leafThickness).translate(0, coverCenterY, 0);
|
|
51814
|
+
const hingeWeb = box(width, hingeWebWidth + fuseOverlap * 2, hingeWebThickness).translate(0, 0, 0);
|
|
51815
|
+
const pullLip = box(width * 0.92, pullLipDepth, leafThickness).translate(0, coverCenterY + coverDepth / 2 + pullLipDepth / 2 - fuseOverlap, 0);
|
|
51816
|
+
const snapBarb = box(snapBarbWidth, snapBarbDepth, snapBarbHeight).translate(
|
|
51817
|
+
0,
|
|
51818
|
+
coverCenterY + coverDepth / 2 - snapBarbDepth / 2,
|
|
51819
|
+
leafThickness
|
|
51820
|
+
);
|
|
51821
|
+
const catchLand = box(width * 0.55, catchLandDepth, Math.max(0.8, leafThickness * 0.45)).translate(
|
|
51822
|
+
0,
|
|
51823
|
+
fixedCenterY - fixedLeafDepth / 2 + catchLandDepth / 2,
|
|
51824
|
+
leafThickness
|
|
51825
|
+
);
|
|
51826
|
+
const cover2 = union(fixedLeaf, movingLeaf, hingeWeb, pullLip, snapBarb, catchLand).color("#0f766e");
|
|
51827
|
+
const overallDepth = fixedLeafDepth + hingeWebWidth + coverDepth + pullLipDepth;
|
|
51828
|
+
const flexRatio = leafThickness / hingeWebThickness;
|
|
51829
|
+
return {
|
|
51830
|
+
parts: [{ name: "one-piece molded living hinge cover with snap barb", shape: cover2 }],
|
|
51831
|
+
cover: cover2,
|
|
51832
|
+
fixedLeaf,
|
|
51833
|
+
movingLeaf,
|
|
51834
|
+
hingeWeb,
|
|
51835
|
+
snapBarb,
|
|
51836
|
+
catchLand,
|
|
51837
|
+
dims: {
|
|
51838
|
+
width,
|
|
51839
|
+
coverDepth,
|
|
51840
|
+
fixedLeafDepth,
|
|
51841
|
+
leafThickness,
|
|
51842
|
+
hingeWebWidth,
|
|
51843
|
+
hingeWebThickness,
|
|
51844
|
+
pullLipDepth,
|
|
51845
|
+
snapBarbWidth,
|
|
51846
|
+
snapBarbDepth,
|
|
51847
|
+
snapBarbHeight,
|
|
51848
|
+
catchLandDepth,
|
|
51849
|
+
flexRatio,
|
|
51850
|
+
overallDepth
|
|
51851
|
+
}
|
|
51852
|
+
};
|
|
51853
|
+
}
|
|
51854
|
+
function knuckledHingeAssembly(options) {
|
|
51855
|
+
const length4 = requirePositive$6(options.length, "length");
|
|
51856
|
+
const leafLength = requirePositive$6(options.leafLength ?? 36, "leafLength");
|
|
51857
|
+
const leafThickness = requirePositive$6(options.leafThickness ?? 1.6, "leafThickness");
|
|
51858
|
+
const barrelOuterRadius = requirePositive$6(options.barrelOuterRadius ?? 3, "barrelOuterRadius");
|
|
51859
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 2, "pinDiameter");
|
|
51860
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.25, "pinClearance");
|
|
51861
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51862
|
+
const knuckleGap = requireNonNegative(options.knuckleGap ?? 0.45, "knuckleGap");
|
|
51863
|
+
const openAngleDeg = Number.isFinite(options.openAngleDeg ?? 35) ? options.openAngleDeg ?? 35 : 35;
|
|
51864
|
+
const retainerThickness = requirePositive$6(
|
|
51865
|
+
options.retainerThickness ?? Math.max(leafThickness, pinDiameter * 0.7),
|
|
51866
|
+
"retainerThickness"
|
|
51867
|
+
);
|
|
51868
|
+
const segments = options.segments ?? 36;
|
|
51869
|
+
const knuckleCount = options.knuckleCount ?? 5;
|
|
51870
|
+
if (!Number.isInteger(knuckleCount) || knuckleCount < 3 || knuckleCount % 2 === 0) {
|
|
51871
|
+
throw new Error("knuckledHingeAssembly: knuckleCount must be an odd integer >= 3");
|
|
51872
|
+
}
|
|
51873
|
+
if (barrelOuterRadius <= boreDiameter / 2 + Math.max(0.35, pinDiameter * 0.18)) {
|
|
51874
|
+
throw new Error("knuckledHingeAssembly: barrelOuterRadius leaves too little wall around the pin bore");
|
|
51875
|
+
}
|
|
51876
|
+
const knuckleLength = (length4 - knuckleGap * (knuckleCount - 1)) / knuckleCount;
|
|
51877
|
+
if (knuckleLength <= pinDiameter * 1.4) {
|
|
51878
|
+
throw new Error("knuckledHingeAssembly: length, knuckleCount, and knuckleGap make knuckles too short");
|
|
51879
|
+
}
|
|
51880
|
+
const leafRootClearance = Math.max(0.12, Math.min(knuckleGap * 0.35, 0.35));
|
|
51881
|
+
const barrelLeafOverlap = Math.min(barrelOuterRadius * 0.18, leafThickness * 0.35);
|
|
51882
|
+
const bridgeDepth = leafRootClearance + barrelLeafOverlap + 0.2;
|
|
51883
|
+
const fixedLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51884
|
+
0,
|
|
51885
|
+
barrelOuterRadius + leafRootClearance + leafLength / 2,
|
|
51886
|
+
-leafThickness / 2
|
|
51887
|
+
);
|
|
51888
|
+
const movingLeafPlate = box(length4, leafLength, leafThickness).translate(
|
|
51889
|
+
0,
|
|
51890
|
+
-barrelOuterRadius - leafRootClearance - leafLength / 2,
|
|
51891
|
+
-leafThickness / 2
|
|
51892
|
+
);
|
|
51893
|
+
const fixedKnuckles = [];
|
|
51894
|
+
const movingKnuckles = [];
|
|
51895
|
+
const fixedBridges = [];
|
|
51896
|
+
const movingBridges = [];
|
|
51897
|
+
for (let index2 = 0; index2 < knuckleCount; index2 += 1) {
|
|
51898
|
+
const xStart = -length4 / 2 + index2 * (knuckleLength + knuckleGap);
|
|
51899
|
+
const xCenter = xStart + knuckleLength / 2;
|
|
51900
|
+
const knuckle = tubeAlongX(knuckleLength, barrelOuterRadius, boreDiameter / 2, xCenter, segments);
|
|
51901
|
+
if (index2 % 2 === 0) {
|
|
51902
|
+
fixedKnuckles.push(knuckle);
|
|
51903
|
+
fixedBridges.push(
|
|
51904
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
51905
|
+
xCenter,
|
|
51906
|
+
barrelOuterRadius - barrelLeafOverlap + bridgeDepth / 2,
|
|
51907
|
+
-leafThickness / 2
|
|
51908
|
+
)
|
|
51909
|
+
);
|
|
51910
|
+
} else {
|
|
51911
|
+
movingKnuckles.push(knuckle);
|
|
51912
|
+
movingBridges.push(
|
|
51913
|
+
box(knuckleLength, bridgeDepth, leafThickness).translate(
|
|
51914
|
+
xCenter,
|
|
51915
|
+
-barrelOuterRadius + barrelLeafOverlap - bridgeDepth / 2,
|
|
51916
|
+
-leafThickness / 2
|
|
51917
|
+
)
|
|
51918
|
+
);
|
|
51919
|
+
}
|
|
51920
|
+
}
|
|
51921
|
+
const fixedLeaf = union(fixedLeafPlate, ...fixedKnuckles, ...fixedBridges).color("#475569");
|
|
51922
|
+
const movingLeaf = union(movingLeafPlate, ...movingKnuckles, ...movingBridges).rotateX(openAngleDeg).color("#111827");
|
|
51923
|
+
const pinCore = cylinderAlongX(length4 + retainerThickness * 2, pinDiameter / 2, 0, segments);
|
|
51924
|
+
const retainerRadius = Math.max(barrelOuterRadius * 0.85, pinDiameter);
|
|
51925
|
+
const leftHead = cylinderAlongX(retainerThickness, retainerRadius, -length4 / 2 - retainerThickness / 2, segments);
|
|
51926
|
+
const rightHead = cylinderAlongX(retainerThickness, retainerRadius, length4 / 2 + retainerThickness / 2, segments);
|
|
51927
|
+
const pin = union(pinCore, leftHead, rightHead).color("#cbd5e1");
|
|
51928
|
+
const pinBore = cylinderAlongX(length4 + retainerThickness * 2, boreDiameter / 2, 0, segments);
|
|
51929
|
+
const parts = [
|
|
51930
|
+
{ name: "fixed hinge leaf with alternating knuckles", shape: fixedLeaf },
|
|
51931
|
+
{ name: "moving hinge leaf with alternating knuckles", shape: movingLeaf },
|
|
51932
|
+
{ name: "retained hinge pin through knuckle stack", shape: pin }
|
|
51933
|
+
];
|
|
51934
|
+
return {
|
|
51935
|
+
parts,
|
|
51936
|
+
fixedLeaf,
|
|
51937
|
+
movingLeaf,
|
|
51938
|
+
pin,
|
|
51939
|
+
cutters: {
|
|
51940
|
+
pinBore
|
|
51941
|
+
},
|
|
51942
|
+
dims: {
|
|
51943
|
+
length: length4,
|
|
51944
|
+
leafLength,
|
|
51945
|
+
leafThickness,
|
|
51946
|
+
barrelOuterRadius,
|
|
51947
|
+
pinDiameter,
|
|
51948
|
+
boreDiameter,
|
|
51949
|
+
knuckleGap,
|
|
51950
|
+
knuckleCount,
|
|
51951
|
+
knuckleLength,
|
|
51952
|
+
openAngleDeg,
|
|
51953
|
+
retainerThickness
|
|
51954
|
+
}
|
|
51955
|
+
};
|
|
51956
|
+
}
|
|
51957
|
+
function clevisPinJointAssembly(options = {}) {
|
|
51958
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 4, "pinDiameter");
|
|
51959
|
+
const pinClearance = requireNonNegative(options.pinClearance ?? 0.3, "pinClearance");
|
|
51960
|
+
const boreDiameter = pinDiameter + pinClearance;
|
|
51961
|
+
const linkThickness = requirePositive$6(options.linkThickness ?? Math.max(5, pinDiameter * 1.5), "linkThickness");
|
|
51962
|
+
const earThickness = requirePositive$6(options.earThickness ?? Math.max(3.5, pinDiameter), "earThickness");
|
|
51963
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.25, "runningClearance");
|
|
51964
|
+
const linkArmWidth = requirePositive$6(options.linkArmWidth ?? pinDiameter * 2.4, "linkArmWidth");
|
|
51965
|
+
const eyeOuterRadius = requirePositive$6(
|
|
51966
|
+
options.eyeOuterRadius ?? Math.max(pinDiameter * 1.8, linkArmWidth / 2 + 1.4),
|
|
51967
|
+
"eyeOuterRadius"
|
|
51968
|
+
);
|
|
51969
|
+
const earLength = requirePositive$6(options.earLength ?? Math.max(eyeOuterRadius * 2.55, pinDiameter * 4.2), "earLength");
|
|
51970
|
+
const earHeight = requirePositive$6(options.earHeight ?? Math.max(eyeOuterRadius * 2.25, pinDiameter * 4.4), "earHeight");
|
|
51971
|
+
const linkArmLength = requirePositive$6(options.linkArmLength ?? 34, "linkArmLength");
|
|
51972
|
+
const retainerThickness = requirePositive$6(
|
|
51973
|
+
options.retainerThickness ?? Math.max(1.2, pinDiameter * 0.35),
|
|
51974
|
+
"retainerThickness"
|
|
51975
|
+
);
|
|
51976
|
+
const segments = options.segments ?? 40;
|
|
51977
|
+
if (eyeOuterRadius <= boreDiameter / 2 + Math.max(0.8, pinDiameter * 0.25)) {
|
|
51978
|
+
throw new Error("clevisPinJointAssembly: eyeOuterRadius leaves too little material around the pin bore");
|
|
51979
|
+
}
|
|
51980
|
+
if (earHeight <= boreDiameter + Math.max(3, pinDiameter)) {
|
|
51981
|
+
throw new Error("clevisPinJointAssembly: earHeight leaves too little material around the pin bore");
|
|
51982
|
+
}
|
|
51983
|
+
if (earLength / 2 <= eyeOuterRadius + runningClearance) {
|
|
51984
|
+
throw new Error("clevisPinJointAssembly: earLength must extend behind the link eye for a rear clevis bridge");
|
|
51985
|
+
}
|
|
51986
|
+
const clevisGap = linkThickness + runningClearance * 2;
|
|
51987
|
+
const earCenterY = clevisGap / 2 + earThickness / 2;
|
|
51988
|
+
const totalStackY = clevisGap + earThickness * 2;
|
|
51989
|
+
const pinLength = totalStackY + retainerThickness * 2 + runningClearance * 2;
|
|
51990
|
+
const bridgeClearX = -eyeOuterRadius - runningClearance;
|
|
51991
|
+
const bridgeLength = Math.max(pinDiameter * 2.2, 4);
|
|
51992
|
+
const bridgeHeight = Math.min(earHeight * 0.48, Math.max(pinDiameter * 1.4, eyeOuterRadius * 0.75));
|
|
51993
|
+
const bridgeCenterX = bridgeClearX - bridgeLength / 2;
|
|
51994
|
+
const bridgeCenterZ = -earHeight / 2 + bridgeHeight / 2;
|
|
51995
|
+
const pinBore = cylinderAlongY(totalStackY + 0.8, boreDiameter / 2, 0, segments);
|
|
51996
|
+
const clevisBlank = union(
|
|
51997
|
+
box(earLength, earThickness, earHeight).translate(0, earCenterY, -earHeight / 2),
|
|
51998
|
+
box(earLength, earThickness, earHeight).translate(0, -earCenterY, -earHeight / 2),
|
|
51999
|
+
box(bridgeLength, totalStackY, bridgeHeight).translate(bridgeCenterX, 0, bridgeCenterZ)
|
|
52000
|
+
);
|
|
52001
|
+
const clevis = clevisBlank.subtract(pinBore).color("#475569");
|
|
52002
|
+
const eye = tubeAlongY(linkThickness, eyeOuterRadius, boreDiameter / 2, 0, segments);
|
|
52003
|
+
const armOverlap = Math.min(eyeOuterRadius * 0.65, linkArmLength * 0.25);
|
|
52004
|
+
const armCenterX = eyeOuterRadius - armOverlap + linkArmLength / 2;
|
|
52005
|
+
const linkArm = box(linkArmLength, linkThickness, linkArmWidth).translate(armCenterX, 0, -linkArmWidth / 2);
|
|
52006
|
+
const link = union(eye, linkArm).color("#111827");
|
|
52007
|
+
const pinCore = cylinderAlongY(pinLength, pinDiameter / 2, 0, segments);
|
|
52008
|
+
const headRadius = Math.max(pinDiameter * 0.9, boreDiameter / 2 + 0.8);
|
|
52009
|
+
const headY = totalStackY / 2 + runningClearance + retainerThickness / 2;
|
|
52010
|
+
const headA = cylinderAlongY(retainerThickness, headRadius, headY, segments);
|
|
52011
|
+
const headB = cylinderAlongY(retainerThickness, headRadius, -headY, segments);
|
|
52012
|
+
const pin = union(pinCore, headA, headB).color("#cbd5e1");
|
|
52013
|
+
const cutter = cylinderAlongY(pinLength + 1, boreDiameter / 2, 0, segments);
|
|
52014
|
+
const parts = [
|
|
52015
|
+
{ name: "bored clevis yoke with rear bridge", shape: clevis },
|
|
52016
|
+
{ name: "center link eye captured in clevis", shape: link },
|
|
52017
|
+
{ name: "retained clevis pin through link eye", shape: pin }
|
|
52018
|
+
];
|
|
52019
|
+
return {
|
|
52020
|
+
parts,
|
|
52021
|
+
clevis,
|
|
52022
|
+
link,
|
|
52023
|
+
pin,
|
|
52024
|
+
cutters: {
|
|
52025
|
+
pinBore: cutter
|
|
52026
|
+
},
|
|
52027
|
+
dims: {
|
|
52028
|
+
pinDiameter,
|
|
52029
|
+
boreDiameter,
|
|
52030
|
+
linkThickness,
|
|
52031
|
+
earThickness,
|
|
52032
|
+
runningClearance,
|
|
52033
|
+
earLength,
|
|
52034
|
+
earHeight,
|
|
52035
|
+
linkArmLength,
|
|
52036
|
+
linkArmWidth,
|
|
52037
|
+
eyeOuterRadius,
|
|
52038
|
+
retainerThickness,
|
|
52039
|
+
pinLength,
|
|
52040
|
+
clevisGap
|
|
52041
|
+
}
|
|
52042
|
+
};
|
|
52043
|
+
}
|
|
52044
|
+
function seatedBearingAssembly(options) {
|
|
52045
|
+
const bearingOuterDiameter = requirePositive$6(options.bearingOuterDiameter, "bearingOuterDiameter");
|
|
52046
|
+
const bearingInnerDiameter = requirePositive$6(options.bearingInnerDiameter, "bearingInnerDiameter");
|
|
52047
|
+
const bearingWidth = requirePositive$6(options.bearingWidth, "bearingWidth");
|
|
52048
|
+
const shaftDiameter = requirePositive$6(options.shaftDiameter ?? Math.max(1, bearingInnerDiameter - 0.4), "shaftDiameter");
|
|
52049
|
+
const pocketClearance = requireNonNegative(options.pocketClearance ?? 0.2, "pocketClearance");
|
|
52050
|
+
const shaftClearance = requireNonNegative(options.shaftClearance ?? 0.35, "shaftClearance");
|
|
52051
|
+
const runningClearance = requireNonNegative(options.runningClearance ?? 0.05, "runningClearance");
|
|
52052
|
+
const housingThickness = requirePositive$6(options.housingThickness ?? bearingWidth + 5, "housingThickness");
|
|
52053
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2, bearingWidth * 0.45), "bossHeight");
|
|
52054
|
+
const bossOuterDiameter = requirePositive$6(
|
|
52055
|
+
options.bossOuterDiameter ?? bearingOuterDiameter + Math.max(8, bearingOuterDiameter * 0.36),
|
|
52056
|
+
"bossOuterDiameter"
|
|
52057
|
+
);
|
|
52058
|
+
const housingWidth = requirePositive$6(options.housingWidth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 2.1), "housingWidth");
|
|
52059
|
+
const housingDepth = requirePositive$6(options.housingDepth ?? Math.max(bossOuterDiameter + 12, bearingOuterDiameter * 1.8), "housingDepth");
|
|
52060
|
+
const shaftOverhang = requirePositive$6(options.shaftOverhang ?? Math.max(8, bearingOuterDiameter * 0.45), "shaftOverhang");
|
|
52061
|
+
const shoulderDiameter = requirePositive$6(options.shoulderDiameter ?? Math.max(shaftDiameter * 1.65, bearingInnerDiameter + 2), "shoulderDiameter");
|
|
52062
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(1.5, shaftDiameter * 0.32), "shoulderThickness");
|
|
52063
|
+
const segments = options.segments ?? 48;
|
|
52064
|
+
if (bearingOuterDiameter <= bearingInnerDiameter + Math.max(1, bearingOuterDiameter * 0.08)) {
|
|
52065
|
+
throw new Error("seatedBearingAssembly: bearingOuterDiameter leaves too little bearing wall around the bore");
|
|
52066
|
+
}
|
|
52067
|
+
if (shaftDiameter + shaftClearance >= bearingInnerDiameter) {
|
|
52068
|
+
throw new Error("seatedBearingAssembly: shaftDiameter plus shaftClearance must fit inside the bearing bore");
|
|
52069
|
+
}
|
|
52070
|
+
if (shoulderDiameter >= bearingOuterDiameter - runningClearance * 2) {
|
|
52071
|
+
throw new Error("seatedBearingAssembly: shoulderDiameter must stay smaller than the bearing outer race");
|
|
52072
|
+
}
|
|
52073
|
+
const pocketDiameter = bearingOuterDiameter + pocketClearance;
|
|
52074
|
+
const shaftBoreDiameter = shaftDiameter + shaftClearance;
|
|
52075
|
+
const totalHousingHeight = housingThickness + bossHeight;
|
|
52076
|
+
const pocketDepth = bearingWidth + runningClearance * 2;
|
|
52077
|
+
if (pocketDepth >= totalHousingHeight - runningClearance) {
|
|
52078
|
+
throw new Error("seatedBearingAssembly: housingThickness and bossHeight must leave a shoulder below the bearing pocket");
|
|
52079
|
+
}
|
|
52080
|
+
if (bossOuterDiameter <= pocketDiameter + Math.max(2, bearingOuterDiameter * 0.12)) {
|
|
52081
|
+
throw new Error("seatedBearingAssembly: bossOuterDiameter leaves too little wall around the bearing pocket");
|
|
52082
|
+
}
|
|
52083
|
+
if (housingWidth <= pocketDiameter + 6 || housingDepth <= pocketDiameter + 6) {
|
|
52084
|
+
throw new Error("seatedBearingAssembly: housing dimensions leave too little material around the bearing pocket");
|
|
52085
|
+
}
|
|
52086
|
+
if (shoulderThickness * 2 + runningClearance * 2 >= shaftOverhang) {
|
|
52087
|
+
throw new Error("seatedBearingAssembly: shaftOverhang must leave room for retaining collars outside the housing");
|
|
52088
|
+
}
|
|
52089
|
+
const pocketBottomZ = totalHousingHeight - pocketDepth;
|
|
52090
|
+
const bearingZ = pocketBottomZ + runningClearance;
|
|
52091
|
+
const lowerShoulderZ = -runningClearance - shoulderThickness;
|
|
52092
|
+
const upperShoulderZ = totalHousingHeight + runningClearance;
|
|
52093
|
+
const shaftLength = totalHousingHeight + shaftOverhang * 2;
|
|
52094
|
+
const bossFuseOverlap = Math.min(0.08, Math.max(0.02, bossHeight * 0.03));
|
|
52095
|
+
const bearingPocket = cylinder(pocketDepth + 0.4, pocketDiameter / 2, void 0, segments).translate(0, 0, pocketBottomZ - 0.2);
|
|
52096
|
+
const shaftBore = cylinder(totalHousingHeight + 1, shaftBoreDiameter / 2, void 0, segments).translate(0, 0, -0.5);
|
|
52097
|
+
const housingBase = box(housingWidth, housingDepth, housingThickness).subtract(bearingPocket).subtract(shaftBore);
|
|
52098
|
+
const housingBoss = cylinder(bossHeight + bossFuseOverlap, bossOuterDiameter / 2, void 0, segments).translate(
|
|
52099
|
+
0,
|
|
52100
|
+
0,
|
|
52101
|
+
housingThickness - bossFuseOverlap
|
|
52102
|
+
).subtract(bearingPocket);
|
|
52103
|
+
const housing = union(housingBase, housingBoss).color("#475569");
|
|
52104
|
+
const bearingRing = tubeAlongZ(bearingWidth, bearingOuterDiameter / 2, bearingInnerDiameter / 2, segments);
|
|
52105
|
+
const shieldInset = Math.min(bearingWidth * 0.18, 0.7);
|
|
52106
|
+
const shieldOuterRadius = bearingOuterDiameter / 2 - Math.max(0.45, (bearingOuterDiameter - bearingInnerDiameter) * 0.08);
|
|
52107
|
+
const shieldInnerRadius = bearingInnerDiameter / 2 + Math.max(0.2, (bearingOuterDiameter - bearingInnerDiameter) * 0.035);
|
|
52108
|
+
const bearingShield = shieldOuterRadius > shieldInnerRadius + 0.2 ? union(
|
|
52109
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(0, 0, shieldInset),
|
|
52110
|
+
tubeAlongZ(Math.min(0.35, bearingWidth * 0.08), shieldOuterRadius, shieldInnerRadius, segments).translate(
|
|
52111
|
+
0,
|
|
52112
|
+
0,
|
|
52113
|
+
bearingWidth - shieldInset - Math.min(0.35, bearingWidth * 0.08)
|
|
52114
|
+
)
|
|
52115
|
+
) : null;
|
|
52116
|
+
const bearing = (bearingShield ? union(bearingRing, bearingShield) : bearingRing).translate(0, 0, bearingZ).color("#111827");
|
|
52117
|
+
const shaftCore = cylinder(shaftLength, shaftDiameter / 2, void 0, segments).translate(0, 0, -shaftOverhang);
|
|
52118
|
+
const lowerShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, lowerShoulderZ);
|
|
52119
|
+
const upperShoulder = cylinder(shoulderThickness, shoulderDiameter / 2, void 0, segments).translate(0, 0, upperShoulderZ);
|
|
52120
|
+
const shaft = union(shaftCore, lowerShoulder, upperShoulder).color("#cbd5e1");
|
|
52121
|
+
const parts = [
|
|
52122
|
+
{ name: "bearing housing with counterbore pocket and shoulder", shape: housing },
|
|
52123
|
+
{ name: "purchased radial bearing seated in counterbore", shape: bearing },
|
|
52124
|
+
{ name: "shaft through bearing bore with retaining collars", shape: shaft }
|
|
52125
|
+
];
|
|
52126
|
+
return {
|
|
52127
|
+
parts,
|
|
52128
|
+
housing,
|
|
52129
|
+
bearing,
|
|
52130
|
+
shaft,
|
|
52131
|
+
cutters: {
|
|
52132
|
+
bearingPocket,
|
|
52133
|
+
shaftBore
|
|
52134
|
+
},
|
|
52135
|
+
dims: {
|
|
52136
|
+
bearingOuterDiameter,
|
|
52137
|
+
bearingInnerDiameter,
|
|
52138
|
+
bearingWidth,
|
|
52139
|
+
shaftDiameter,
|
|
52140
|
+
housingWidth,
|
|
52141
|
+
housingDepth,
|
|
52142
|
+
housingThickness,
|
|
52143
|
+
bossOuterDiameter,
|
|
52144
|
+
bossHeight,
|
|
52145
|
+
totalHousingHeight,
|
|
52146
|
+
pocketDiameter,
|
|
52147
|
+
pocketDepth,
|
|
52148
|
+
shaftBoreDiameter,
|
|
52149
|
+
runningClearance,
|
|
52150
|
+
shaftLength,
|
|
52151
|
+
shoulderDiameter,
|
|
52152
|
+
shoulderThickness
|
|
52153
|
+
}
|
|
52154
|
+
};
|
|
52155
|
+
}
|
|
52156
|
+
function cableGlandAnchorAssembly(options) {
|
|
52157
|
+
const cableDiameter = requirePositive$6(options.cableDiameter, "cableDiameter");
|
|
52158
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
52159
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? Math.max(54, cableDiameter * 7), "panelWidth");
|
|
52160
|
+
const panelHeight = requirePositive$6(options.panelHeight ?? Math.max(38, cableDiameter * 5), "panelHeight");
|
|
52161
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52162
|
+
const panelHoleClearance = requirePositive$6(options.panelHoleClearance ?? 0.25, "panelHoleClearance");
|
|
52163
|
+
const cableBoreDiameter = cableDiameter + runningClearance * 2;
|
|
52164
|
+
const glandOuterDiameter = requirePositive$6(options.glandOuterDiameter ?? cableDiameter + Math.max(6, cableDiameter * 0.9), "glandOuterDiameter");
|
|
52165
|
+
const nutOuterDiameter = requirePositive$6(options.nutOuterDiameter ?? glandOuterDiameter + Math.max(6, cableDiameter * 0.8), "nutOuterDiameter");
|
|
52166
|
+
const nutThickness = requirePositive$6(options.nutThickness ?? Math.max(4, cableDiameter * 0.8), "nutThickness");
|
|
52167
|
+
const flangeDiameter = requirePositive$6(options.flangeDiameter ?? glandOuterDiameter + Math.max(5, cableDiameter * 0.7), "flangeDiameter");
|
|
52168
|
+
const flangeThickness = requirePositive$6(options.flangeThickness ?? Math.max(2, panelThickness * 0.45), "flangeThickness");
|
|
52169
|
+
const minGlandLength = panelThickness + nutThickness + flangeThickness + runningClearance * 4;
|
|
52170
|
+
const glandLength = requirePositive$6(options.glandLength ?? minGlandLength + Math.max(8, cableDiameter), "glandLength");
|
|
52171
|
+
const cableLength = requirePositive$6(options.cableLength ?? glandLength + Math.max(36, cableDiameter * 5), "cableLength");
|
|
52172
|
+
const segments = options.segments ?? 40;
|
|
52173
|
+
if (glandOuterDiameter <= cableBoreDiameter + Math.max(1.2, cableDiameter * 0.18)) {
|
|
52174
|
+
throw new Error("cableGlandAnchorAssembly: glandOuterDiameter leaves too little wall around the cable bore");
|
|
52175
|
+
}
|
|
52176
|
+
if (nutOuterDiameter <= glandOuterDiameter + Math.max(1.5, cableDiameter * 0.2)) {
|
|
52177
|
+
throw new Error("cableGlandAnchorAssembly: nutOuterDiameter must leave material around the gland body");
|
|
52178
|
+
}
|
|
52179
|
+
if (flangeDiameter <= glandOuterDiameter + Math.max(1.2, cableDiameter * 0.16)) {
|
|
52180
|
+
throw new Error("cableGlandAnchorAssembly: flangeDiameter must be larger than the gland body");
|
|
52181
|
+
}
|
|
52182
|
+
if (panelWidth <= flangeDiameter + 8 || panelHeight <= flangeDiameter + 8) {
|
|
52183
|
+
throw new Error("cableGlandAnchorAssembly: panel dimensions leave too little material around the gland hole");
|
|
52184
|
+
}
|
|
52185
|
+
if (glandLength <= minGlandLength) {
|
|
52186
|
+
throw new Error("cableGlandAnchorAssembly: glandLength must span the panel, flange, compression nut, and clearances");
|
|
52187
|
+
}
|
|
52188
|
+
if (cableLength <= glandLength + runningClearance * 2) {
|
|
52189
|
+
throw new Error("cableGlandAnchorAssembly: cableLength must extend beyond the gland body");
|
|
52190
|
+
}
|
|
52191
|
+
const panelHoleDiameter = glandOuterDiameter + panelHoleClearance * 2;
|
|
52192
|
+
const glandOuterRadius = glandOuterDiameter / 2;
|
|
52193
|
+
const cableBoreRadius = cableBoreDiameter / 2;
|
|
52194
|
+
const faceClearance = Math.min(0.05, runningClearance * 0.15);
|
|
52195
|
+
const flangePocketDepth = Math.min(Math.max(0.35, panelThickness * 0.18), panelThickness * 0.4, flangeThickness * 0.55);
|
|
52196
|
+
const panelHole = cylinderAlongX(panelThickness + 0.8, panelHoleDiameter / 2, 0, segments);
|
|
52197
|
+
const flangeSeatPocket = cylinderAlongX(
|
|
52198
|
+
flangePocketDepth + 0.2,
|
|
52199
|
+
flangeDiameter / 2 + panelHoleClearance,
|
|
52200
|
+
panelThickness / 2 - flangePocketDepth / 2,
|
|
52201
|
+
segments
|
|
52202
|
+
);
|
|
52203
|
+
const cableBore = cylinderAlongX(glandLength + 0.8, cableBoreRadius, 0, segments);
|
|
52204
|
+
const panel = box(panelThickness, panelWidth, panelHeight).translate(0, 0, -panelHeight / 2).subtract(panelHole).subtract(flangeSeatPocket).color("#475569");
|
|
52205
|
+
const glandBody = tubeAlongX(glandLength, glandOuterRadius, cableBoreRadius, 0, segments);
|
|
52206
|
+
const flangeCenterX = panelThickness / 2 - flangePocketDepth + faceClearance + flangeThickness / 2;
|
|
52207
|
+
const flange = tubeAlongX(flangeThickness, flangeDiameter / 2, cableBoreRadius, flangeCenterX, segments);
|
|
52208
|
+
const gland = union(glandBody, flange).color("#94a3b8");
|
|
52209
|
+
const nutInnerRadius = glandOuterRadius + Math.min(0.12, runningClearance * 0.4);
|
|
52210
|
+
const nutCenterX = -panelThickness / 2 - faceClearance - nutThickness / 2;
|
|
52211
|
+
const compressionNut = tubeAlongX(nutThickness, nutOuterDiameter / 2, nutInnerRadius, nutCenterX, segments).color("#cbd5e1");
|
|
52212
|
+
const cable = cylinderAlongX(cableLength, cableDiameter / 2, 0, segments).color("#111827");
|
|
52213
|
+
const parts = [
|
|
52214
|
+
{ name: "panel with gland clearance hole", shape: panel },
|
|
52215
|
+
{ name: "hollow cable gland body with panel flange", shape: gland },
|
|
52216
|
+
{ name: "compression nut around gland body", shape: compressionNut },
|
|
52217
|
+
{ name: "routed cable through gland bore", shape: cable }
|
|
52218
|
+
];
|
|
52219
|
+
return {
|
|
52220
|
+
parts,
|
|
52221
|
+
panel,
|
|
52222
|
+
gland,
|
|
52223
|
+
compressionNut,
|
|
52224
|
+
cable,
|
|
52225
|
+
cutters: {
|
|
52226
|
+
panelHole,
|
|
52227
|
+
flangeSeatPocket,
|
|
52228
|
+
cableBore
|
|
52229
|
+
},
|
|
52230
|
+
dims: {
|
|
52231
|
+
cableDiameter,
|
|
52232
|
+
cableBoreDiameter,
|
|
52233
|
+
panelThickness,
|
|
52234
|
+
panelWidth,
|
|
52235
|
+
panelHeight,
|
|
52236
|
+
glandOuterDiameter,
|
|
52237
|
+
glandLength,
|
|
52238
|
+
nutOuterDiameter,
|
|
52239
|
+
nutThickness,
|
|
52240
|
+
flangeDiameter,
|
|
52241
|
+
flangeThickness,
|
|
52242
|
+
runningClearance,
|
|
52243
|
+
faceClearance,
|
|
52244
|
+
flangePocketDepth,
|
|
52245
|
+
panelHoleDiameter,
|
|
52246
|
+
cableLength
|
|
52247
|
+
}
|
|
52248
|
+
};
|
|
52249
|
+
}
|
|
52250
|
+
function hoseBarbPortAssembly(options) {
|
|
52251
|
+
const hoseInnerDiameter = requirePositive$6(options.hoseInnerDiameter, "hoseInnerDiameter");
|
|
52252
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.18, "runningClearance");
|
|
52253
|
+
const faceClearance = requirePositive$6(options.faceClearance ?? 0.04, "faceClearance");
|
|
52254
|
+
const barbRootDiameter = requirePositive$6(
|
|
52255
|
+
options.barbRootDiameter ?? Math.max(1, hoseInnerDiameter - Math.max(0.25, hoseInnerDiameter * 0.06)),
|
|
52256
|
+
"barbRootDiameter"
|
|
52257
|
+
);
|
|
52258
|
+
const barbPeakDiameter = requirePositive$6(
|
|
52259
|
+
options.barbPeakDiameter ?? hoseInnerDiameter + Math.max(0.65, hoseInnerDiameter * 0.12),
|
|
52260
|
+
"barbPeakDiameter"
|
|
52261
|
+
);
|
|
52262
|
+
const installedHoseBoreDiameter = barbPeakDiameter + runningClearance * 2;
|
|
52263
|
+
const hoseOuterDiameter = requirePositive$6(
|
|
52264
|
+
options.hoseOuterDiameter ?? Math.max(installedHoseBoreDiameter + 2.4, hoseInnerDiameter + Math.max(3, hoseInnerDiameter * 0.55)),
|
|
52265
|
+
"hoseOuterDiameter"
|
|
52266
|
+
);
|
|
52267
|
+
const fluidBoreDiameter = requirePositive$6(options.fluidBoreDiameter ?? hoseInnerDiameter * 0.65, "fluidBoreDiameter");
|
|
52268
|
+
const blockThickness = requirePositive$6(options.blockThickness ?? Math.max(7, hoseInnerDiameter * 1.2), "blockThickness");
|
|
52269
|
+
const barbCount = options.barbCount ?? 3;
|
|
52270
|
+
const barbLength = requirePositive$6(options.barbLength ?? Math.max(2.6, hoseInnerDiameter * 0.55), "barbLength");
|
|
52271
|
+
const barbStackLength = barbCount * barbLength;
|
|
52272
|
+
const shoulderDiameter = requirePositive$6(
|
|
52273
|
+
options.shoulderDiameter ?? barbPeakDiameter + Math.max(4, hoseInnerDiameter * 0.65),
|
|
52274
|
+
"shoulderDiameter"
|
|
52275
|
+
);
|
|
52276
|
+
const shoulderThickness = requirePositive$6(options.shoulderThickness ?? Math.max(2, hoseInnerDiameter * 0.35), "shoulderThickness");
|
|
52277
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? shoulderDiameter + Math.max(4, hoseInnerDiameter * 0.6), "bossDiameter");
|
|
52278
|
+
const bossHeight = requirePositive$6(options.bossHeight ?? Math.max(2.4, hoseInnerDiameter * 0.45), "bossHeight");
|
|
52279
|
+
const blockWidth = requirePositive$6(options.blockWidth ?? bossDiameter + Math.max(14, hoseInnerDiameter * 2.4), "blockWidth");
|
|
52280
|
+
const blockHeight = requirePositive$6(options.blockHeight ?? bossDiameter + Math.max(12, hoseInnerDiameter * 2.1), "blockHeight");
|
|
52281
|
+
const hoseLength = requirePositive$6(options.hoseLength ?? barbStackLength + Math.max(32, hoseInnerDiameter * 5), "hoseLength");
|
|
52282
|
+
const clampWidth = requirePositive$6(options.clampWidth ?? Math.max(4, hoseOuterDiameter * 0.45), "clampWidth");
|
|
52283
|
+
const clampThickness = requirePositive$6(options.clampThickness ?? 0.9, "clampThickness");
|
|
52284
|
+
const segments = options.segments ?? 40;
|
|
52285
|
+
if (!Number.isInteger(barbCount) || barbCount < 1 || barbCount > 8) {
|
|
52286
|
+
throw new Error("hoseBarbPortAssembly: barbCount must be an integer from 1 to 8");
|
|
52287
|
+
}
|
|
52288
|
+
if (barbPeakDiameter <= hoseInnerDiameter) {
|
|
52289
|
+
throw new Error("hoseBarbPortAssembly: barbPeakDiameter must exceed hoseInnerDiameter so the barb retains the hose");
|
|
52290
|
+
}
|
|
52291
|
+
if (barbRootDiameter >= barbPeakDiameter - Math.max(0.25, hoseInnerDiameter * 0.04)) {
|
|
52292
|
+
throw new Error("hoseBarbPortAssembly: barbRootDiameter must leave a visible barb rise");
|
|
52293
|
+
}
|
|
52294
|
+
if (fluidBoreDiameter >= barbRootDiameter - Math.max(0.8, hoseInnerDiameter * 0.12)) {
|
|
52295
|
+
throw new Error("hoseBarbPortAssembly: fluidBoreDiameter leaves too little wall in the barb fitting");
|
|
52296
|
+
}
|
|
52297
|
+
if (hoseOuterDiameter <= installedHoseBoreDiameter + Math.max(1.2, hoseInnerDiameter * 0.16)) {
|
|
52298
|
+
throw new Error("hoseBarbPortAssembly: hoseOuterDiameter leaves too little hose wall around the installed barb envelope");
|
|
52299
|
+
}
|
|
52300
|
+
if (shoulderDiameter <= barbPeakDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
52301
|
+
throw new Error("hoseBarbPortAssembly: shoulderDiameter must be larger than the barb peaks");
|
|
52302
|
+
}
|
|
52303
|
+
if (bossDiameter <= shoulderDiameter + Math.max(1.5, hoseInnerDiameter * 0.2)) {
|
|
52304
|
+
throw new Error("hoseBarbPortAssembly: bossDiameter must leave material around the shoulder seat");
|
|
52305
|
+
}
|
|
52306
|
+
if (blockWidth <= bossDiameter + 8 || blockHeight <= bossDiameter + 8) {
|
|
52307
|
+
throw new Error("hoseBarbPortAssembly: receiver block dimensions leave too little material around the port boss");
|
|
52308
|
+
}
|
|
52309
|
+
const portBoreDiameter = barbRootDiameter + runningClearance * 2;
|
|
52310
|
+
const portBore = cylinderAlongX(blockThickness + bossHeight + 0.8, portBoreDiameter / 2, bossHeight / 2, segments);
|
|
52311
|
+
const fuseOverlap = Math.min(0.04, faceClearance * 0.7);
|
|
52312
|
+
const bossCenterX = blockThickness / 2 + bossHeight / 2 - fuseOverlap;
|
|
52313
|
+
const receiver = union(
|
|
52314
|
+
box(blockThickness, blockWidth, blockHeight).translate(0, 0, -blockHeight / 2),
|
|
52315
|
+
cylinderAlongX(bossHeight + fuseOverlap, bossDiameter / 2, bossCenterX, segments)
|
|
52316
|
+
).subtract(portBore).color("#475569");
|
|
52317
|
+
const bossFaceX = blockThickness / 2 + bossHeight;
|
|
52318
|
+
const shoulderCenterX = bossFaceX + faceClearance + shoulderThickness / 2;
|
|
52319
|
+
const barbStartX = shoulderCenterX + shoulderThickness / 2;
|
|
52320
|
+
const fittingStartX = -blockThickness / 2 - runningClearance;
|
|
52321
|
+
const fittingEndX = barbStartX + barbStackLength;
|
|
52322
|
+
const fittingCore = tubeAlongX(fittingEndX - fittingStartX, barbRootDiameter / 2, fluidBoreDiameter / 2, (fittingStartX + fittingEndX) / 2, segments);
|
|
52323
|
+
const shoulder = tubeAlongX(shoulderThickness, shoulderDiameter / 2, fluidBoreDiameter / 2, shoulderCenterX, segments);
|
|
52324
|
+
const barbSolids = [];
|
|
52325
|
+
const ridgeLength = Math.max(0.8, Math.min(barbLength * 0.45, hoseInnerDiameter * 0.28));
|
|
52326
|
+
for (let index2 = 0; index2 < barbCount; index2 += 1) {
|
|
52327
|
+
const startX = barbStartX + index2 * barbLength;
|
|
52328
|
+
const ridgeCenterX = startX + barbLength - ridgeLength / 2;
|
|
52329
|
+
barbSolids.push(tubeAlongX(ridgeLength, barbPeakDiameter / 2, fluidBoreDiameter / 2, ridgeCenterX, segments));
|
|
52330
|
+
}
|
|
52331
|
+
const fitting = union(fittingCore, shoulder, ...barbSolids).color("#94a3b8");
|
|
52332
|
+
const hoseStartX = barbStartX + faceClearance;
|
|
52333
|
+
const hoseCenterX = hoseStartX + hoseLength / 2;
|
|
52334
|
+
const installedHoseBore = cylinderAlongX(hoseLength + 0.8, installedHoseBoreDiameter / 2, hoseCenterX, segments);
|
|
52335
|
+
const hose = tubeAlongX(hoseLength, hoseOuterDiameter / 2, installedHoseBoreDiameter / 2, hoseCenterX, segments).color("#111827");
|
|
52336
|
+
const clampCenterX = barbStartX + Math.min(barbStackLength * 0.55, Math.max(barbLength, clampWidth));
|
|
52337
|
+
const clamp2 = tubeAlongX(
|
|
52338
|
+
clampWidth,
|
|
52339
|
+
hoseOuterDiameter / 2 + clampThickness,
|
|
52340
|
+
hoseOuterDiameter / 2 + Math.min(0.08, runningClearance * 0.45),
|
|
52341
|
+
clampCenterX,
|
|
52342
|
+
segments
|
|
52343
|
+
).color("#cbd5e1");
|
|
52344
|
+
const parts = [
|
|
52345
|
+
{ name: "bored pump or filter body with raised hose-port boss", shape: receiver },
|
|
52346
|
+
{ name: "hollow hose barb fitting with shoulder and retention ridges", shape: fitting },
|
|
52347
|
+
{ name: "installed flexible hose over barb tail", shape: hose },
|
|
52348
|
+
{ name: "clamp band over hose and barb ridges", shape: clamp2 }
|
|
52349
|
+
];
|
|
52350
|
+
return {
|
|
52351
|
+
parts,
|
|
52352
|
+
receiver,
|
|
52353
|
+
fitting,
|
|
52354
|
+
hose,
|
|
52355
|
+
clamp: clamp2,
|
|
52356
|
+
cutters: {
|
|
52357
|
+
portBore,
|
|
52358
|
+
installedHoseBore
|
|
52359
|
+
},
|
|
52360
|
+
dims: {
|
|
52361
|
+
hoseInnerDiameter,
|
|
52362
|
+
hoseOuterDiameter,
|
|
52363
|
+
installedHoseBoreDiameter,
|
|
52364
|
+
blockThickness,
|
|
52365
|
+
blockWidth,
|
|
52366
|
+
blockHeight,
|
|
52367
|
+
bossDiameter,
|
|
52368
|
+
bossHeight,
|
|
52369
|
+
fluidBoreDiameter,
|
|
52370
|
+
barbRootDiameter,
|
|
52371
|
+
barbPeakDiameter,
|
|
52372
|
+
barbCount,
|
|
52373
|
+
barbLength,
|
|
52374
|
+
barbStackLength,
|
|
52375
|
+
shoulderDiameter,
|
|
52376
|
+
shoulderThickness,
|
|
52377
|
+
hoseLength,
|
|
52378
|
+
clampWidth,
|
|
52379
|
+
clampThickness,
|
|
52380
|
+
runningClearance,
|
|
52381
|
+
faceClearance
|
|
52382
|
+
}
|
|
52383
|
+
};
|
|
52384
|
+
}
|
|
52385
|
+
function routedTubeClipAssembly(options) {
|
|
52386
|
+
const tubeDiameter = requirePositive$6(options.tubeDiameter, "tubeDiameter");
|
|
52387
|
+
const tubeLength = requirePositive$6(options.tubeLength ?? 120, "tubeLength");
|
|
52388
|
+
const panelThickness = requirePositive$6(options.panelThickness ?? 3, "panelThickness");
|
|
52389
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52390
|
+
const screwSize = options.screwSize ?? "M3";
|
|
52391
|
+
const segments = options.segments ?? 32;
|
|
52392
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52393
|
+
if (!sizeData) throw new Error(`routedTubeClipAssembly: unsupported screwSize "${screwSize}"`);
|
|
52394
|
+
const clipCount = options.clipCount ?? 3;
|
|
52395
|
+
if (!Number.isInteger(clipCount) || clipCount < 1 || clipCount > 8) {
|
|
52396
|
+
throw new Error("routedTubeClipAssembly: clipCount must be an integer from 1 to 8");
|
|
52397
|
+
}
|
|
52398
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52399
|
+
const screwHeadDiameter = sizeData.head;
|
|
52400
|
+
const tubeBoreDiameter = tubeDiameter + runningClearance * 2;
|
|
52401
|
+
const clipWallThickness = requirePositive$6(
|
|
52402
|
+
options.clipWallThickness ?? Math.max(screwHeadDiameter + 1.2, tubeDiameter * 0.45, 5),
|
|
52403
|
+
"clipWallThickness"
|
|
52404
|
+
);
|
|
52405
|
+
const clipWidth = requirePositive$6(options.clipWidth ?? Math.max(screwHeadDiameter + 3, tubeDiameter * 1.4, 10), "clipWidth");
|
|
52406
|
+
const clipDepth = tubeBoreDiameter + clipWallThickness * 2;
|
|
52407
|
+
const bottomWall = Math.max(1.2, clipWallThickness * 0.35);
|
|
52408
|
+
const topWall = Math.max(2, clipWallThickness * 0.45);
|
|
52409
|
+
const clipHeight = bottomWall + tubeBoreDiameter + topWall;
|
|
52410
|
+
const tubeCenterZ = panelThickness + bottomWall + tubeBoreDiameter / 2;
|
|
52411
|
+
const panelLength = requirePositive$6(options.panelLength ?? tubeLength + 24, "panelLength");
|
|
52412
|
+
const panelWidth = requirePositive$6(options.panelWidth ?? clipDepth + Math.max(14, screwHeadDiameter * 2), "panelWidth");
|
|
52413
|
+
if (tubeLength <= clipWidth + 8) {
|
|
52414
|
+
throw new Error("routedTubeClipAssembly: tubeLength must leave visible tube beyond the clip body");
|
|
52415
|
+
}
|
|
52416
|
+
const defaultSpacing = clipCount === 1 ? 0 : Math.max(clipWidth + 8, (tubeLength - clipWidth * 2) / (clipCount - 1));
|
|
52417
|
+
const clipSpacing = options.clipSpacing === void 0 ? defaultSpacing : requirePositive$6(options.clipSpacing, "clipSpacing");
|
|
52418
|
+
const clipCenters = Array.from({ length: clipCount }, (_2, index2) => (index2 - (clipCount - 1) / 2) * clipSpacing);
|
|
52419
|
+
const maxClipExtent = Math.max(...clipCenters.map((x2) => Math.abs(x2) + clipWidth / 2));
|
|
52420
|
+
if (maxClipExtent > tubeLength / 2 - 2) {
|
|
52421
|
+
throw new Error("routedTubeClipAssembly: clipSpacing places a clip beyond the routed tube length");
|
|
52422
|
+
}
|
|
52423
|
+
if (maxClipExtent > panelLength / 2 - 2) {
|
|
52424
|
+
throw new Error("routedTubeClipAssembly: panelLength is too short for the clip pattern");
|
|
52425
|
+
}
|
|
52426
|
+
const boreRadius = tubeBoreDiameter / 2;
|
|
52427
|
+
const screwY = boreRadius + clipWallThickness / 2;
|
|
52428
|
+
if (screwY + screwHeadDiameter / 2 > clipDepth / 2 - 0.2) {
|
|
52429
|
+
throw new Error("routedTubeClipAssembly: clipWallThickness leaves too little land for screw heads");
|
|
52430
|
+
}
|
|
52431
|
+
if (clipDepth > panelWidth - Math.max(4, screwHeadDiameter * 0.5)) {
|
|
52432
|
+
throw new Error("routedTubeClipAssembly: panelWidth leaves too little material beside the clips");
|
|
52433
|
+
}
|
|
52434
|
+
const screwPositions = clipCenters.flatMap((x2) => [
|
|
52435
|
+
[x2, -screwY],
|
|
52436
|
+
[x2, screwY]
|
|
52437
|
+
]);
|
|
52438
|
+
const screwClearanceDiameter = Math.max(sizeData.loose, screwDiameter + 0.8);
|
|
52439
|
+
const panelThreadEnvelopeDiameter = screwClearanceDiameter;
|
|
52440
|
+
const clipTopZ = panelThickness + clipHeight;
|
|
52441
|
+
const clipTubeBores = union(
|
|
52442
|
+
...clipCenters.map((x2) => cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ))
|
|
52443
|
+
);
|
|
52444
|
+
const clipScrewClearances = union(
|
|
52445
|
+
...screwPositions.map(([x2, y2]) => cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, y2, panelThickness - 0.4))
|
|
52446
|
+
);
|
|
52447
|
+
const panelThreadEnvelopes = union(
|
|
52448
|
+
...screwPositions.map(([x2, y2]) => cylinder(panelThickness + 0.8, panelThreadEnvelopeDiameter / 2, void 0, segments).translate(x2, y2, -0.4))
|
|
52449
|
+
);
|
|
52450
|
+
const panel = box(panelLength, panelWidth, panelThickness).subtract(panelThreadEnvelopes).color("#475569");
|
|
52451
|
+
const tube2 = cylinderAlongX(tubeLength, tubeDiameter / 2, 0, segments).translate(0, 0, tubeCenterZ).color("#0f172a");
|
|
52452
|
+
const clips = clipCenters.map((x2) => {
|
|
52453
|
+
const body = box(clipWidth, clipDepth, clipHeight).translate(x2, 0, panelThickness);
|
|
52454
|
+
const tubeBore = cylinderAlongX(clipWidth + 0.8, boreRadius, x2, segments).translate(0, 0, tubeCenterZ);
|
|
52455
|
+
const screwHoles = union(
|
|
52456
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, -screwY, panelThickness - 0.4),
|
|
52457
|
+
cylinder(clipHeight + 0.8, screwClearanceDiameter / 2, void 0, segments).translate(x2, screwY, panelThickness - 0.4)
|
|
52458
|
+
);
|
|
52459
|
+
return body.subtract(tubeBore).subtract(screwHoles).color("#94a3b8");
|
|
52460
|
+
});
|
|
52461
|
+
const screwLength = clipHeight + panelThickness * 0.65;
|
|
52462
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
52463
|
+
const screwBlank = union(
|
|
52464
|
+
cylinder(screwLength, screwDiameter / 2, void 0, segments).translate(0, 0, clipTopZ - screwLength),
|
|
52465
|
+
cylinder(screwHeadHeight, screwHeadDiameter / 2, void 0, segments).translate(0, 0, clipTopZ)
|
|
52466
|
+
).color("#cbd5e1");
|
|
52467
|
+
const screws = screwPositions.map(([x2, y2]) => screwBlank.translate(x2, y2, 0));
|
|
52468
|
+
const parts = [
|
|
52469
|
+
{ name: "panel with tube-clip screw receiving holes", shape: panel },
|
|
52470
|
+
{ name: "routed flexible tube through retained clip bores", shape: tube2 },
|
|
52471
|
+
...clips.map((shape, index2) => ({ name: `saddle tube clip ${index2 + 1} with through-bore`, shape })),
|
|
52472
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} tube clip screw ${index2 + 1}`, shape }))
|
|
52473
|
+
];
|
|
52474
|
+
return {
|
|
52475
|
+
parts,
|
|
52476
|
+
panel,
|
|
52477
|
+
tube: tube2,
|
|
52478
|
+
clips,
|
|
52479
|
+
screws,
|
|
52480
|
+
clipCenters,
|
|
52481
|
+
screwPositions,
|
|
52482
|
+
cutters: {
|
|
52483
|
+
clipTubeBores,
|
|
52484
|
+
clipScrewClearances,
|
|
52485
|
+
panelThreadEnvelopes
|
|
52486
|
+
},
|
|
52487
|
+
dims: {
|
|
52488
|
+
tubeDiameter,
|
|
52489
|
+
tubeLength,
|
|
52490
|
+
tubeBoreDiameter,
|
|
52491
|
+
panelLength,
|
|
52492
|
+
panelWidth,
|
|
52493
|
+
panelThickness,
|
|
52494
|
+
clipCount,
|
|
52495
|
+
clipWidth,
|
|
52496
|
+
clipDepth,
|
|
52497
|
+
clipHeight,
|
|
52498
|
+
clipWallThickness,
|
|
52499
|
+
tubeCenterZ,
|
|
52500
|
+
screwSize,
|
|
52501
|
+
screwDiameter,
|
|
52502
|
+
screwHeadDiameter,
|
|
52503
|
+
screwLength,
|
|
52504
|
+
screwClearanceDiameter,
|
|
52505
|
+
panelThreadEnvelopeDiameter,
|
|
52506
|
+
runningClearance
|
|
52507
|
+
}
|
|
52508
|
+
};
|
|
52509
|
+
}
|
|
52510
|
+
function pcbTerminalBlockAssembly(options = {}) {
|
|
52511
|
+
const terminalCount = options.terminalCount ?? 4;
|
|
52512
|
+
if (!Number.isInteger(terminalCount) || terminalCount < 1 || terminalCount > 24) {
|
|
52513
|
+
throw new Error("pcbTerminalBlockAssembly: terminalCount must be an integer from 1 to 24");
|
|
52514
|
+
}
|
|
52515
|
+
const terminalPitch = requirePositive$6(options.terminalPitch ?? 5.08, "terminalPitch");
|
|
52516
|
+
const terminalBlockWidth = terminalPitch * terminalCount + 3;
|
|
52517
|
+
const boardWidth = requirePositive$6(options.boardWidth ?? Math.max(50, terminalBlockWidth + 28), "boardWidth");
|
|
52518
|
+
const boardDepth = requirePositive$6(options.boardDepth ?? 38, "boardDepth");
|
|
52519
|
+
const boardThickness = requirePositive$6(options.boardThickness ?? 1.6, "boardThickness");
|
|
52520
|
+
const backplateThickness = requirePositive$6(options.backplateThickness ?? 3, "backplateThickness");
|
|
52521
|
+
const backplateMargin = requirePositive$6(options.backplateMargin ?? 5, "backplateMargin");
|
|
52522
|
+
const standoffHeight = requirePositive$6(options.standoffHeight ?? 6, "standoffHeight");
|
|
52523
|
+
const screwSize = options.screwSize ?? "M3";
|
|
52524
|
+
const segments = options.segments ?? 28;
|
|
52525
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52526
|
+
if (!sizeData) throw new Error(`pcbTerminalBlockAssembly: unsupported screwSize "${screwSize}"`);
|
|
52527
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52528
|
+
const screwHeadDiameter = sizeData.head;
|
|
52529
|
+
const screwHeadHeight = Math.max(1.2, screwDiameter * 0.55);
|
|
52530
|
+
const standoffDiameter = requirePositive$6(
|
|
52531
|
+
options.standoffDiameter ?? Math.max(screwHeadDiameter * 1.45, sizeData.normal + 3),
|
|
52532
|
+
"standoffDiameter"
|
|
52533
|
+
);
|
|
52534
|
+
const [mountInsetX, mountInsetY] = resolveBoltInset(
|
|
52535
|
+
options.mountingInset,
|
|
52536
|
+
Math.max(standoffDiameter / 2 + 1.2, screwHeadDiameter * 0.75)
|
|
52537
|
+
);
|
|
52538
|
+
if (mountInsetX * 2 >= boardWidth || mountInsetY * 2 >= boardDepth) {
|
|
52539
|
+
throw new Error("pcbTerminalBlockAssembly: mountingInset leaves no room for the PCB mounting pattern");
|
|
52540
|
+
}
|
|
52541
|
+
const terminalBlockDepth = requirePositive$6(options.terminalBlockDepth ?? 10, "terminalBlockDepth");
|
|
52542
|
+
const terminalBlockHeight = requirePositive$6(options.terminalBlockHeight ?? 9, "terminalBlockHeight");
|
|
52543
|
+
const terminalEdgeInset = requirePositive$6(options.terminalEdgeInset ?? 5, "terminalEdgeInset");
|
|
52544
|
+
const pinDiameter = requirePositive$6(options.pinDiameter ?? 0.9, "pinDiameter");
|
|
52545
|
+
const pinClearance = requirePositive$6(options.pinClearance ?? 0.25, "pinClearance");
|
|
52546
|
+
const pinTailLength = requireNonNegative(options.pinTailLength ?? 0, "pinTailLength");
|
|
52547
|
+
const wirePortDiameter = requirePositive$6(options.wirePortDiameter ?? 2.6, "wirePortDiameter");
|
|
52548
|
+
const pinHoleDiameter = pinDiameter + pinClearance;
|
|
52549
|
+
const terminalCenterY = -boardDepth / 2 + terminalEdgeInset + terminalBlockDepth / 2;
|
|
52550
|
+
const pinY = terminalCenterY + terminalBlockDepth * 0.24;
|
|
52551
|
+
const firstPinX = -((terminalCount - 1) * terminalPitch) / 2;
|
|
52552
|
+
const pinPositions = Array.from({ length: terminalCount }, (_2, index2) => [firstPinX + index2 * terminalPitch, pinY]);
|
|
52553
|
+
const mountingPositions = [
|
|
52554
|
+
[-boardWidth / 2 + mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
52555
|
+
[boardWidth / 2 - mountInsetX, -boardDepth / 2 + mountInsetY],
|
|
52556
|
+
[-boardWidth / 2 + mountInsetX, boardDepth / 2 - mountInsetY],
|
|
52557
|
+
[boardWidth / 2 - mountInsetX, boardDepth / 2 - mountInsetY]
|
|
52558
|
+
];
|
|
52559
|
+
if (terminalBlockWidth >= boardWidth - mountInsetX * 2) {
|
|
52560
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block is too wide for the PCB mounting pattern");
|
|
52561
|
+
}
|
|
52562
|
+
if (terminalEdgeInset + terminalBlockDepth >= boardDepth - mountInsetY * 2) {
|
|
52563
|
+
throw new Error("pcbTerminalBlockAssembly: terminal block depth collides with the rear mounting datum");
|
|
52564
|
+
}
|
|
52565
|
+
if (pinHoleDiameter >= terminalPitch * 0.55) {
|
|
52566
|
+
throw new Error("pcbTerminalBlockAssembly: pinDiameter and pinClearance leave too little PCB web between terminal holes");
|
|
52567
|
+
}
|
|
52568
|
+
if (wirePortDiameter >= Math.min(terminalPitch * 0.72, terminalBlockHeight * 0.65)) {
|
|
52569
|
+
throw new Error("pcbTerminalBlockAssembly: wirePortDiameter is too large for the terminal pitch or body height");
|
|
52570
|
+
}
|
|
52571
|
+
for (const [index2, [x2, y2]] of [...mountingPositions, ...pinPositions].entries()) {
|
|
52572
|
+
if (!Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
52573
|
+
throw new Error(`pcbTerminalBlockAssembly: generated datum position ${index2} is not finite`);
|
|
52574
|
+
}
|
|
52575
|
+
}
|
|
52576
|
+
const backplateWidth = boardWidth + backplateMargin * 2;
|
|
52577
|
+
const backplateDepth = boardDepth + backplateMargin * 2;
|
|
52578
|
+
const boardBottomZ = backplateThickness + standoffHeight;
|
|
52579
|
+
const boardTopZ = boardBottomZ + boardThickness;
|
|
52580
|
+
const standoffOverlap = Math.min(0.08, standoffHeight * 0.03);
|
|
52581
|
+
const standoffThreadEnvelopeDiameter = Math.max(sizeData.loose, screwDiameter + 1);
|
|
52582
|
+
const standoffThreadEnvelope = cylinder(standoffHeight + 0.8, standoffThreadEnvelopeDiameter / 2, void 0, segments).translate(
|
|
52583
|
+
0,
|
|
52584
|
+
0,
|
|
52585
|
+
backplateThickness - 0.4
|
|
52586
|
+
);
|
|
52587
|
+
const standoffThreadEnvelopes = union(...mountingPositions.map(([x2, y2]) => standoffThreadEnvelope.translate(x2, y2, 0)));
|
|
52588
|
+
const standoff = cylinder(standoffHeight + standoffOverlap, standoffDiameter / 2, void 0, segments).translate(0, 0, backplateThickness - standoffOverlap).subtract(standoffThreadEnvelope);
|
|
52589
|
+
const standoffs = union(...mountingPositions.map(([x2, y2]) => standoff.translate(x2, y2, 0)));
|
|
52590
|
+
const backplate = union(box(backplateWidth, backplateDepth, backplateThickness), standoffs).color("#475569");
|
|
52591
|
+
const boardMountingHoleDiameter = sizeData.normal;
|
|
52592
|
+
const boardMountHole = cylinder(boardThickness + 0.8, boardMountingHoleDiameter / 2, void 0, segments).translate(
|
|
52593
|
+
0,
|
|
52594
|
+
0,
|
|
52595
|
+
boardBottomZ - 0.4
|
|
52596
|
+
);
|
|
52597
|
+
const pcbMountingHoles = union(...mountingPositions.map(([x2, y2]) => boardMountHole.translate(x2, y2, 0)));
|
|
52598
|
+
const pinHole = cylinder(boardThickness + 0.8, pinHoleDiameter / 2, void 0, segments).translate(0, 0, boardBottomZ - 0.4);
|
|
52599
|
+
const pcbPinHoles = union(...pinPositions.map(([x2, y2]) => pinHole.translate(x2, y2, 0)));
|
|
52600
|
+
const pcb = box(boardWidth, boardDepth, boardThickness).translate(0, 0, boardBottomZ).subtract(pcbMountingHoles).subtract(pcbPinHoles).color("#166534");
|
|
52601
|
+
const terminalBodyBlank = box(terminalBlockWidth, terminalBlockDepth, terminalBlockHeight).translate(0, terminalCenterY, boardTopZ);
|
|
52602
|
+
const wirePort = cylinderAlongY(terminalBlockDepth + 0.8, wirePortDiameter / 2, terminalCenterY, segments).translate(
|
|
52603
|
+
0,
|
|
52604
|
+
0,
|
|
52605
|
+
boardTopZ + terminalBlockHeight * 0.42
|
|
52606
|
+
);
|
|
52607
|
+
const wirePorts = union(...pinPositions.map(([x2]) => wirePort.translate(x2, 0, 0)));
|
|
52608
|
+
const clampScrewPockets = union(
|
|
52609
|
+
...pinPositions.map(
|
|
52610
|
+
([x2]) => cylinder(Math.max(0.6, terminalBlockHeight * 0.22), Math.min(terminalPitch * 0.22, wirePortDiameter * 0.42), void 0, segments).translate(
|
|
52611
|
+
x2,
|
|
52612
|
+
terminalCenterY + terminalBlockDepth * 0.12,
|
|
52613
|
+
boardTopZ + terminalBlockHeight * 0.76
|
|
52614
|
+
)
|
|
52615
|
+
)
|
|
52616
|
+
);
|
|
52617
|
+
const pinLength = boardThickness + pinTailLength + Math.min(0.6, terminalBlockHeight * 0.08);
|
|
52618
|
+
const pinStartZ = boardBottomZ - pinTailLength;
|
|
52619
|
+
const pins = union(...pinPositions.map(([x2, y2]) => cylinder(pinLength, pinDiameter / 2, void 0, segments).translate(x2, y2, pinStartZ)));
|
|
52620
|
+
const terminalBlock = union(terminalBodyBlank.subtract(wirePorts).subtract(clampScrewPockets), pins).color("#16a34a");
|
|
52621
|
+
const screwShaftLength = boardThickness + standoffHeight * 0.85;
|
|
52622
|
+
const mountingHardware = fastenerSet(screwSize, screwShaftLength, {
|
|
52623
|
+
washerUnderHead: false,
|
|
52624
|
+
washerUnderNut: false,
|
|
52625
|
+
fit: "normal",
|
|
52626
|
+
segments
|
|
52627
|
+
});
|
|
52628
|
+
const screws = mountingPositions.map(([x2, y2]) => mountingHardware.bolt.translate(x2, y2, boardTopZ).color("#cbd5e1"));
|
|
52629
|
+
const parts = [
|
|
52630
|
+
{ name: "electronics backplate with fused PCB standoffs", shape: backplate },
|
|
52631
|
+
{ name: "PCB with mounting holes and terminal pin clearances", shape: pcb },
|
|
52632
|
+
{ name: "seated purchased terminal block with through-board pins", shape: terminalBlock },
|
|
52633
|
+
...screws.map((shape, index2) => ({ name: `installed ${screwSize} PCB mounting screw ${index2 + 1}`, shape }))
|
|
52634
|
+
];
|
|
52635
|
+
return {
|
|
52636
|
+
parts,
|
|
52637
|
+
backplate,
|
|
52638
|
+
pcb,
|
|
52639
|
+
terminalBlock,
|
|
52640
|
+
screws,
|
|
52641
|
+
mountingPositions,
|
|
52642
|
+
pinPositions,
|
|
52643
|
+
cutters: {
|
|
52644
|
+
pcbMountingHoles,
|
|
52645
|
+
pcbPinHoles,
|
|
52646
|
+
standoffThreadEnvelopes
|
|
52647
|
+
},
|
|
52648
|
+
dims: {
|
|
52649
|
+
terminalCount,
|
|
52650
|
+
terminalPitch,
|
|
52651
|
+
boardWidth,
|
|
52652
|
+
boardDepth,
|
|
52653
|
+
boardThickness,
|
|
52654
|
+
backplateWidth,
|
|
52655
|
+
backplateDepth,
|
|
52656
|
+
backplateThickness,
|
|
52657
|
+
standoffHeight,
|
|
52658
|
+
standoffDiameter,
|
|
52659
|
+
screwSize,
|
|
52660
|
+
screwDiameter,
|
|
52661
|
+
screwHeadDiameter,
|
|
52662
|
+
screwHeadHeight,
|
|
52663
|
+
screwShaftLength,
|
|
52664
|
+
boardMountingHoleDiameter,
|
|
52665
|
+
standoffThreadEnvelopeDiameter,
|
|
52666
|
+
terminalBlockWidth,
|
|
52667
|
+
terminalBlockDepth,
|
|
52668
|
+
terminalBlockHeight,
|
|
52669
|
+
terminalEdgeInset,
|
|
52670
|
+
pinDiameter,
|
|
52671
|
+
pinClearance,
|
|
52672
|
+
pinHoleDiameter,
|
|
52673
|
+
pinTailLength,
|
|
52674
|
+
wirePortDiameter
|
|
52675
|
+
}
|
|
52676
|
+
};
|
|
52677
|
+
}
|
|
52678
|
+
function thumbScrewClampAssembly(options = {}) {
|
|
52679
|
+
const screwSize = options.screwSize ?? "M6";
|
|
52680
|
+
const segments = options.segments ?? 36;
|
|
52681
|
+
const sizeData = METRIC_HOLE_TABLE[screwSize];
|
|
52682
|
+
if (!sizeData) throw new Error(`thumbScrewClampAssembly: unsupported screwSize "${screwSize}"`);
|
|
52683
|
+
const screwDiameter = parseFloat(screwSize.replace("M", ""));
|
|
52684
|
+
const runningClearance = requirePositive$6(options.runningClearance ?? 0.35, "runningClearance");
|
|
52685
|
+
const faceClearance = requireNonNegative(options.faceClearance ?? 0, "faceClearance");
|
|
52686
|
+
const threadEnvelopeDiameter = Math.max(sizeData.normal, screwDiameter + runningClearance * 2);
|
|
52687
|
+
const pressurePadDiameter = requirePositive$6(
|
|
52688
|
+
options.pressurePadDiameter ?? Math.max(screwDiameter * 3.2, 18),
|
|
52689
|
+
"pressurePadDiameter"
|
|
52690
|
+
);
|
|
52691
|
+
const pressurePadThickness = requirePositive$6(
|
|
52692
|
+
options.pressurePadThickness ?? Math.max(screwDiameter * 0.72, 4),
|
|
52693
|
+
"pressurePadThickness"
|
|
52694
|
+
);
|
|
52695
|
+
const knobDiameter = requirePositive$6(options.knobDiameter ?? Math.max(screwDiameter * 4.2, 24), "knobDiameter");
|
|
52696
|
+
const knobThickness = requirePositive$6(options.knobThickness ?? Math.max(screwDiameter * 0.9, 7), "knobThickness");
|
|
52697
|
+
const workpieceThickness = requirePositive$6(options.workpieceThickness ?? 18, "workpieceThickness");
|
|
52698
|
+
const workpieceDepth = requirePositive$6(options.workpieceDepth ?? Math.max(46, pressurePadDiameter * 1.5), "workpieceDepth");
|
|
52699
|
+
const workpieceHeight = requirePositive$6(options.workpieceHeight ?? Math.max(pressurePadDiameter * 1.35, 24), "workpieceHeight");
|
|
52700
|
+
const frameDepth = requirePositive$6(
|
|
52701
|
+
options.frameDepth ?? Math.max(workpieceDepth + 12, pressurePadDiameter + 16),
|
|
52702
|
+
"frameDepth"
|
|
52703
|
+
);
|
|
52704
|
+
const baseThickness = requirePositive$6(options.baseThickness ?? Math.max(screwDiameter, 6), "baseThickness");
|
|
52705
|
+
const jawThickness = requirePositive$6(options.jawThickness ?? Math.max(screwDiameter * 1.35, 9), "jawThickness");
|
|
52706
|
+
const supportThickness = requirePositive$6(
|
|
52707
|
+
options.supportThickness ?? Math.max(screwDiameter * 1.8, 12),
|
|
52708
|
+
"supportThickness"
|
|
52709
|
+
);
|
|
52710
|
+
const bossLength = requirePositive$6(options.bossLength ?? Math.max(screwDiameter * 1.1, 8), "bossLength");
|
|
52711
|
+
const bossDiameter = requirePositive$6(options.bossDiameter ?? Math.max(threadEnvelopeDiameter + 5, screwDiameter * 2.5), "bossDiameter");
|
|
52712
|
+
const exposedScrewLength = requirePositive$6(
|
|
52713
|
+
options.exposedScrewLength ?? Math.max(pressurePadDiameter * 0.45, screwDiameter * 2.2),
|
|
52714
|
+
"exposedScrewLength"
|
|
52715
|
+
);
|
|
52716
|
+
const screwCenterZ = baseThickness + Math.max(workpieceHeight * 0.52, pressurePadDiameter * 0.68);
|
|
52717
|
+
const frameHeight = requirePositive$6(
|
|
52718
|
+
options.frameHeight ?? screwCenterZ - baseThickness + pressurePadDiameter / 2 + Math.max(baseThickness, 7),
|
|
52719
|
+
"frameHeight"
|
|
52720
|
+
);
|
|
52721
|
+
if (workpieceDepth > frameDepth - 6) {
|
|
52722
|
+
throw new Error("thumbScrewClampAssembly: frameDepth must leave side material around the clamped workpiece");
|
|
52723
|
+
}
|
|
52724
|
+
if (pressurePadDiameter > frameDepth - 4) {
|
|
52725
|
+
throw new Error("thumbScrewClampAssembly: pressurePadDiameter is too large for the frame depth");
|
|
52726
|
+
}
|
|
52727
|
+
if (bossDiameter > frameDepth - 4) {
|
|
52728
|
+
throw new Error("thumbScrewClampAssembly: bossDiameter is too large for the frame depth");
|
|
52729
|
+
}
|
|
52730
|
+
if (screwCenterZ - pressurePadDiameter / 2 <= baseThickness + 0.5) {
|
|
52731
|
+
throw new Error("thumbScrewClampAssembly: pressure pad collides with the base bridge");
|
|
52732
|
+
}
|
|
52733
|
+
if (baseThickness + frameHeight - screwCenterZ <= pressurePadDiameter / 2 + 2) {
|
|
52734
|
+
throw new Error("thumbScrewClampAssembly: frameHeight leaves too little material above the screw axis");
|
|
52735
|
+
}
|
|
52736
|
+
if (threadEnvelopeDiameter + 4 > Math.min(frameDepth, frameHeight)) {
|
|
52737
|
+
throw new Error("thumbScrewClampAssembly: threaded boss bore leaves too little surrounding frame material");
|
|
52738
|
+
}
|
|
52739
|
+
const workpieceLeftFaceX = -workpieceThickness / 2;
|
|
52740
|
+
const workpieceRightFaceX = workpieceThickness / 2;
|
|
52741
|
+
const anvilOverlap = Math.min(0.35, pressurePadThickness * 0.18);
|
|
52742
|
+
const anvilPadCenterX = workpieceLeftFaceX - faceClearance - pressurePadThickness / 2;
|
|
52743
|
+
const pressurePadCenterX = workpieceRightFaceX + faceClearance + pressurePadThickness / 2;
|
|
52744
|
+
const fixedJawRightFaceX = anvilPadCenterX - pressurePadThickness / 2 + anvilOverlap;
|
|
52745
|
+
const fixedJawCenterX = fixedJawRightFaceX - jawThickness / 2;
|
|
52746
|
+
const pressurePadRightFaceX = pressurePadCenterX + pressurePadThickness / 2;
|
|
52747
|
+
const supportInnerFaceX = pressurePadRightFaceX + exposedScrewLength;
|
|
52748
|
+
const supportCenterX = supportInnerFaceX + supportThickness / 2;
|
|
52749
|
+
const supportOuterFaceX = supportInnerFaceX + supportThickness;
|
|
52750
|
+
const frameLeftFaceX = fixedJawCenterX - jawThickness / 2;
|
|
52751
|
+
const frameRightFaceX = supportOuterFaceX;
|
|
52752
|
+
const baseLength = frameRightFaceX - frameLeftFaceX;
|
|
52753
|
+
if (baseLength <= 0 || !Number.isFinite(baseLength)) {
|
|
52754
|
+
throw new Error("thumbScrewClampAssembly: generated clamp frame length is invalid");
|
|
52755
|
+
}
|
|
52756
|
+
const bossCenterX = supportInnerFaceX + (supportThickness + bossLength) / 2;
|
|
52757
|
+
const threadedBossBore = cylinderAlongX(supportThickness + bossLength + 1, threadEnvelopeDiameter / 2, bossCenterX, segments).translate(
|
|
52758
|
+
0,
|
|
52759
|
+
0,
|
|
52760
|
+
screwCenterZ
|
|
52761
|
+
);
|
|
52762
|
+
const frameOverlap = Math.min(0.12, baseThickness * 0.04);
|
|
52763
|
+
const base = box(baseLength, frameDepth, baseThickness).translate((frameLeftFaceX + frameRightFaceX) / 2, 0, 0);
|
|
52764
|
+
const fixedJaw = box(jawThickness, frameDepth, frameHeight + frameOverlap).translate(fixedJawCenterX, 0, baseThickness - frameOverlap);
|
|
52765
|
+
const support = box(supportThickness, frameDepth, frameHeight + frameOverlap).translate(supportCenterX, 0, baseThickness - frameOverlap);
|
|
52766
|
+
const boss2 = cylinderAlongX(supportThickness + bossLength, bossDiameter / 2, bossCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52767
|
+
const anvilPad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, anvilPadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52768
|
+
const frame = union(base, fixedJaw, support, boss2, anvilPad).subtract(threadedBossBore).color("#475569");
|
|
52769
|
+
const workpieceBottomZ = screwCenterZ - workpieceHeight / 2;
|
|
52770
|
+
const workpiece = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ).color("#a16207");
|
|
52771
|
+
const pressurePad = cylinderAlongX(pressurePadThickness, pressurePadDiameter / 2, pressurePadCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52772
|
+
const knobCenterX = supportOuterFaceX + bossLength + runningClearance + knobThickness / 2;
|
|
52773
|
+
const knob = cylinderAlongX(knobThickness, knobDiameter / 2, knobCenterX, segments).translate(0, 0, screwCenterZ);
|
|
52774
|
+
const shaftLeftX = pressurePadRightFaceX - Math.min(pressurePadThickness * 0.45, screwDiameter * 0.45);
|
|
52775
|
+
const shaftRightX = knobCenterX + knobThickness / 2;
|
|
52776
|
+
const shaftLength = shaftRightX - shaftLeftX;
|
|
52777
|
+
if (shaftLength <= supportThickness + bossLength) {
|
|
52778
|
+
throw new Error("thumbScrewClampAssembly: generated screw length is too short for the threaded support");
|
|
52779
|
+
}
|
|
52780
|
+
const shaft = cylinderAlongX(shaftLength, screwDiameter / 2, (shaftLeftX + shaftRightX) / 2, segments).translate(0, 0, screwCenterZ);
|
|
52781
|
+
const clampScrew = union(shaft, pressurePad, knob).color("#cbd5e1");
|
|
52782
|
+
const workpieceEnvelope = box(workpieceThickness, workpieceDepth, workpieceHeight).translate(0, 0, workpieceBottomZ);
|
|
52783
|
+
return {
|
|
52784
|
+
parts: [
|
|
52785
|
+
{ name: "thumb-screw clamp frame with fixed anvil and threaded boss", shape: frame },
|
|
52786
|
+
{ name: "representative clamped workpiece between pads", shape: workpiece },
|
|
52787
|
+
{ name: "installed thumb screw with captive pressure pad and hand knob", shape: clampScrew }
|
|
52788
|
+
],
|
|
52789
|
+
frame,
|
|
52790
|
+
workpiece,
|
|
52791
|
+
clampScrew,
|
|
52792
|
+
cutters: {
|
|
52793
|
+
threadedBossBore,
|
|
52794
|
+
workpieceEnvelope
|
|
52795
|
+
},
|
|
52796
|
+
dims: {
|
|
52797
|
+
screwSize,
|
|
52798
|
+
screwDiameter,
|
|
52799
|
+
threadEnvelopeDiameter,
|
|
52800
|
+
workpieceThickness,
|
|
52801
|
+
workpieceDepth,
|
|
52802
|
+
workpieceHeight,
|
|
52803
|
+
frameDepth,
|
|
52804
|
+
frameHeight,
|
|
52805
|
+
baseThickness,
|
|
52806
|
+
jawThickness,
|
|
52807
|
+
supportThickness,
|
|
52808
|
+
bossLength,
|
|
52809
|
+
bossDiameter,
|
|
52810
|
+
exposedScrewLength,
|
|
52811
|
+
pressurePadDiameter,
|
|
52812
|
+
pressurePadThickness,
|
|
52813
|
+
knobDiameter,
|
|
52814
|
+
knobThickness,
|
|
52815
|
+
screwCenterZ,
|
|
52816
|
+
fixedAnvilFaceX: workpieceLeftFaceX - faceClearance,
|
|
52817
|
+
pressurePadFaceX: workpieceRightFaceX + faceClearance,
|
|
52818
|
+
supportInnerFaceX,
|
|
52819
|
+
runningClearance,
|
|
52820
|
+
faceClearance
|
|
52821
|
+
}
|
|
52822
|
+
};
|
|
52823
|
+
}
|
|
50856
52824
|
function fastenerSet(size, boltLength, options) {
|
|
50857
52825
|
const sizeData = METRIC_HOLE_TABLE[size];
|
|
50858
52826
|
if (!sizeData) throw new Error(`fastenerSet: unsupported size "${size}"`);
|
|
@@ -50913,6 +52881,22 @@ const partLibrary = {
|
|
|
50913
52881
|
nut,
|
|
50914
52882
|
washer,
|
|
50915
52883
|
fastenerSet,
|
|
52884
|
+
boltedServiceCover,
|
|
52885
|
+
datumEnclosureAssembly,
|
|
52886
|
+
snapLatchCoverAssembly,
|
|
52887
|
+
pinnedLeverAssembly,
|
|
52888
|
+
retainedShaftAssembly,
|
|
52889
|
+
capturedLinearSlide,
|
|
52890
|
+
capturedCartridgeGuideAssembly,
|
|
52891
|
+
livingHingeCoverAssembly,
|
|
52892
|
+
knuckledHingeAssembly,
|
|
52893
|
+
clevisPinJointAssembly,
|
|
52894
|
+
seatedBearingAssembly,
|
|
52895
|
+
cableGlandAnchorAssembly,
|
|
52896
|
+
hoseBarbPortAssembly,
|
|
52897
|
+
routedTubeClipAssembly,
|
|
52898
|
+
pcbTerminalBlockAssembly,
|
|
52899
|
+
thumbScrewClampAssembly,
|
|
50916
52900
|
pipeRoute,
|
|
50917
52901
|
elbow,
|
|
50918
52902
|
beltDrive,
|
|
@@ -51253,11 +53237,11 @@ function inverseLerp(x2, y2, value) {
|
|
|
51253
53237
|
return 0;
|
|
51254
53238
|
}
|
|
51255
53239
|
}
|
|
51256
|
-
function lerp$
|
|
53240
|
+
function lerp$5(x2, y2, t) {
|
|
51257
53241
|
return (1 - t) * x2 + t * y2;
|
|
51258
53242
|
}
|
|
51259
53243
|
function damp(x2, y2, lambda, dt) {
|
|
51260
|
-
return lerp$
|
|
53244
|
+
return lerp$5(x2, y2, 1 - Math.exp(-lambda * dt));
|
|
51261
53245
|
}
|
|
51262
53246
|
function pingpong(x2, length4 = 1) {
|
|
51263
53247
|
return length4 - Math.abs(euclideanModulo(x2, length4 * 2) - length4);
|
|
@@ -51450,7 +53434,7 @@ const MathUtils = {
|
|
|
51450
53434
|
* @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
|
|
51451
53435
|
* @return {number} The interpolated value.
|
|
51452
53436
|
*/
|
|
51453
|
-
lerp: lerp$
|
|
53437
|
+
lerp: lerp$5,
|
|
51454
53438
|
/**
|
|
51455
53439
|
* Smoothly interpolate a number from `x` to `y` in a spring-like manner using a delta
|
|
51456
53440
|
* time to maintain frame rate independent movement. For details, see
|
|
@@ -60404,9 +62388,9 @@ class Color {
|
|
|
60404
62388
|
lerpHSL(color, alpha) {
|
|
60405
62389
|
this.getHSL(_hslA);
|
|
60406
62390
|
color.getHSL(_hslB);
|
|
60407
|
-
const h = lerp$
|
|
60408
|
-
const s = lerp$
|
|
60409
|
-
const l = lerp$
|
|
62391
|
+
const h = lerp$5(_hslA.h, _hslB.h, alpha);
|
|
62392
|
+
const s = lerp$5(_hslA.s, _hslB.s, alpha);
|
|
62393
|
+
const l = lerp$5(_hslA.l, _hslB.l, alpha);
|
|
60410
62394
|
this.setHSL(h, s, l);
|
|
60411
62395
|
return this;
|
|
60412
62396
|
}
|
|
@@ -92404,7 +94388,7 @@ function requireFinite$7(value, label) {
|
|
|
92404
94388
|
}
|
|
92405
94389
|
return value;
|
|
92406
94390
|
}
|
|
92407
|
-
function requireVec3$
|
|
94391
|
+
function requireVec3$3(value, label) {
|
|
92408
94392
|
if (!Array.isArray(value) || value.length !== 3) {
|
|
92409
94393
|
throw new Error(`${label} must be [x, y, z]`);
|
|
92410
94394
|
}
|
|
@@ -92448,7 +94432,7 @@ function normalizeOptions(options) {
|
|
|
92448
94432
|
out.size = requireFinite$7(options.size, "Viewport.label options.size");
|
|
92449
94433
|
if (out.size <= 0) throw new Error("Viewport.label options.size must be positive");
|
|
92450
94434
|
}
|
|
92451
|
-
if (options.offset !== void 0) out.offset = requireVec3$
|
|
94435
|
+
if (options.offset !== void 0) out.offset = requireVec3$3(options.offset, "Viewport.label options.offset");
|
|
92452
94436
|
if (options.anchor !== void 0) {
|
|
92453
94437
|
if (!VALID_ANCHORS.has(options.anchor)) {
|
|
92454
94438
|
throw new Error(`Viewport.label options.anchor must be one of: ${Array.from(VALID_ANCHORS).join(", ")}`);
|
|
@@ -92465,7 +94449,7 @@ function collectRenderLabel(text, at, options) {
|
|
|
92465
94449
|
if (typeof text !== "string" || text.trim().length === 0) {
|
|
92466
94450
|
throw new Error("Viewport.label text must be a non-empty string");
|
|
92467
94451
|
}
|
|
92468
|
-
const normalizedAt = requireVec3$
|
|
94452
|
+
const normalizedAt = requireVec3$3(at, "Viewport.label at");
|
|
92469
94453
|
const normalizedOptions = normalizeOptions(options);
|
|
92470
94454
|
_collected$4.push({
|
|
92471
94455
|
id: `render-label-${_nextId++}`,
|
|
@@ -92884,7 +94868,7 @@ function requireFinite$6(value, label) {
|
|
|
92884
94868
|
}
|
|
92885
94869
|
return value;
|
|
92886
94870
|
}
|
|
92887
|
-
function requireVec3$
|
|
94871
|
+
function requireVec3$2(value, label) {
|
|
92888
94872
|
if (!Array.isArray(value) || value.length !== 3) {
|
|
92889
94873
|
throw new Error(`${label} must be [x, y, z]`);
|
|
92890
94874
|
}
|
|
@@ -92912,9 +94896,9 @@ const VALID_ENVIRONMENT_PRESETS = /* @__PURE__ */ new Set([
|
|
|
92912
94896
|
]);
|
|
92913
94897
|
function validateCamera(cam, label) {
|
|
92914
94898
|
const out = {};
|
|
92915
|
-
if (cam.position !== void 0) out.position = requireVec3$
|
|
92916
|
-
if (cam.target !== void 0) out.target = requireVec3$
|
|
92917
|
-
if (cam.up !== void 0) out.up = requireVec3$
|
|
94899
|
+
if (cam.position !== void 0) out.position = requireVec3$2(cam.position, `${label}.position`);
|
|
94900
|
+
if (cam.target !== void 0) out.target = requireVec3$2(cam.target, `${label}.target`);
|
|
94901
|
+
if (cam.up !== void 0) out.up = requireVec3$2(cam.up, `${label}.up`);
|
|
92918
94902
|
if (cam.fov !== void 0) {
|
|
92919
94903
|
out.fov = requireFinite$6(cam.fov, `${label}.fov`);
|
|
92920
94904
|
if (out.fov <= 0 || out.fov >= 180) throw new Error(`${label}.fov must be between 0 and 180`);
|
|
@@ -93049,8 +95033,8 @@ function validateLight(light, label) {
|
|
|
93049
95033
|
const out = { type: light.type };
|
|
93050
95034
|
if (light.color !== void 0) out.color = requireColor(light.color, `${label}.color`);
|
|
93051
95035
|
if (light.intensity !== void 0) out.intensity = requireFinite$6(light.intensity, `${label}.intensity`);
|
|
93052
|
-
if (light.position !== void 0) out.position = requireVec3$
|
|
93053
|
-
if (light.target !== void 0) out.target = requireVec3$
|
|
95036
|
+
if (light.position !== void 0) out.position = requireVec3$2(light.position, `${label}.position`);
|
|
95037
|
+
if (light.target !== void 0) out.target = requireVec3$2(light.target, `${label}.target`);
|
|
93054
95038
|
if (light.groundColor !== void 0) out.groundColor = requireColor(light.groundColor, `${label}.groundColor`);
|
|
93055
95039
|
if (light.skyColor !== void 0) out.skyColor = requireColor(light.skyColor, `${label}.skyColor`);
|
|
93056
95040
|
if (light.angle !== void 0) out.angle = requireFinite$6(light.angle, `${label}.angle`);
|
|
@@ -94582,7 +96566,7 @@ function scale$1(v, s) {
|
|
|
94582
96566
|
function dot$2(a2, b) {
|
|
94583
96567
|
return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
|
|
94584
96568
|
}
|
|
94585
|
-
function lerp$
|
|
96569
|
+
function lerp$4(a2, b, t) {
|
|
94586
96570
|
return a2 + (b - a2) * t;
|
|
94587
96571
|
}
|
|
94588
96572
|
function frameMatrix$1(x2, y2, z2, p2) {
|
|
@@ -94593,7 +96577,7 @@ function axisVector(axis, sign2 = 1) {
|
|
|
94593
96577
|
if (axis === "Y") return [0, sign2, 0];
|
|
94594
96578
|
return [0, 0, sign2];
|
|
94595
96579
|
}
|
|
94596
|
-
function axisPosition(axis, point2) {
|
|
96580
|
+
function axisPosition$1(axis, point2) {
|
|
94597
96581
|
return point2[AXIS_INDEX[axis]];
|
|
94598
96582
|
}
|
|
94599
96583
|
function crossPointForStation(axis, point2) {
|
|
@@ -94601,7 +96585,7 @@ function crossPointForStation(axis, point2) {
|
|
|
94601
96585
|
if (axis === "Y") return [point2[0], -point2[2]];
|
|
94602
96586
|
return [point2[1], point2[2]];
|
|
94603
96587
|
}
|
|
94604
|
-
function orientLoftToAxis(shape, axis) {
|
|
96588
|
+
function orientLoftToAxis$1(shape, axis) {
|
|
94605
96589
|
if (axis === "Z") return shape;
|
|
94606
96590
|
if (axis === "Y") return shape.rotateX(-90);
|
|
94607
96591
|
return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
|
|
@@ -94658,9 +96642,9 @@ function interpolateQuery(a2, b, t) {
|
|
|
94658
96642
|
}
|
|
94659
96643
|
return {
|
|
94660
96644
|
side: sideA,
|
|
94661
|
-
u: lerp$
|
|
94662
|
-
v: lerp$
|
|
94663
|
-
offset: lerp$
|
|
96645
|
+
u: lerp$4(a2.u ?? 0.5, b.u ?? 0.5, t),
|
|
96646
|
+
v: lerp$4(a2.v ?? 0.5, b.v ?? 0.5, t),
|
|
96647
|
+
offset: lerp$4(a2.offset ?? 0, b.offset ?? 0, t)
|
|
94664
96648
|
};
|
|
94665
96649
|
}
|
|
94666
96650
|
function resolvePathQueries(points) {
|
|
@@ -94727,8 +96711,8 @@ class ProductSkin {
|
|
|
94727
96711
|
this.stations = stations;
|
|
94728
96712
|
this.rails = rails;
|
|
94729
96713
|
for (const [name2, query] of Object.entries(refs)) this.refQueries.set(name2, cloneQuery(query));
|
|
94730
|
-
this.axisMin = Math.min(...stations.map((station) => axisPosition(axis, station.center)));
|
|
94731
|
-
this.axisMax = Math.max(...stations.map((station) => axisPosition(axis, station.center)));
|
|
96714
|
+
this.axisMin = Math.min(...stations.map((station) => axisPosition$1(axis, station.center)));
|
|
96715
|
+
this.axisMax = Math.max(...stations.map((station) => axisPosition$1(axis, station.center)));
|
|
94732
96716
|
this.diagnosticsValue = {
|
|
94733
96717
|
...diagnostics,
|
|
94734
96718
|
stationNames: stations.map((station) => station.name),
|
|
@@ -94785,24 +96769,24 @@ class ProductSkin {
|
|
|
94785
96769
|
}
|
|
94786
96770
|
/** Interpolate center, width, and depth at a normalized v or absolute axis value. */
|
|
94787
96771
|
stationAt(vOrAxis) {
|
|
94788
|
-
const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$
|
|
96772
|
+
const axisValue = vOrAxis >= 0 && vOrAxis <= 1 ? lerp$4(this.axisMin, this.axisMax, vOrAxis) : clamp$5(vOrAxis, this.axisMin, this.axisMax);
|
|
94789
96773
|
const sorted = this.stations;
|
|
94790
96774
|
for (let index2 = 0; index2 < sorted.length - 1; index2 += 1) {
|
|
94791
96775
|
const a2 = sorted[index2];
|
|
94792
96776
|
const b = sorted[index2 + 1];
|
|
94793
|
-
const aAxis = axisPosition(this.axis, a2.center);
|
|
94794
|
-
const bAxis = axisPosition(this.axis, b.center);
|
|
96777
|
+
const aAxis = axisPosition$1(this.axis, a2.center);
|
|
96778
|
+
const bAxis = axisPosition$1(this.axis, b.center);
|
|
94795
96779
|
if (axisValue < aAxis - EPS$5 || axisValue > bAxis + EPS$5) continue;
|
|
94796
96780
|
const span = Math.max(EPS$5, bAxis - aAxis);
|
|
94797
96781
|
const t = clamp$5((axisValue - aAxis) / span, 0, 1);
|
|
94798
96782
|
return {
|
|
94799
96783
|
axisValue,
|
|
94800
|
-
center: [lerp$
|
|
94801
|
-
width: lerp$
|
|
94802
|
-
depth: lerp$
|
|
96784
|
+
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)],
|
|
96785
|
+
width: lerp$4(a2.profile.width, b.profile.width, t),
|
|
96786
|
+
depth: lerp$4(a2.profile.depth, b.profile.depth, t),
|
|
94803
96787
|
dWidth: (b.profile.width - a2.profile.width) / span,
|
|
94804
96788
|
dDepth: (b.profile.depth - a2.profile.depth) / span,
|
|
94805
|
-
exponent: lerp$
|
|
96789
|
+
exponent: lerp$4(profileExponent(a2), profileExponent(b), t),
|
|
94806
96790
|
kind: a2.profile.kind === b.profile.kind ? a2.profile.kind : "custom"
|
|
94807
96791
|
};
|
|
94808
96792
|
}
|
|
@@ -94924,7 +96908,7 @@ class ProductSkinBuilder {
|
|
|
94924
96908
|
}
|
|
94925
96909
|
/** Set named cross-section stations for the product skin. */
|
|
94926
96910
|
stations(stations) {
|
|
94927
|
-
this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition(this.axisValue, a2.center) - axisPosition(this.axisValue, b.center));
|
|
96911
|
+
this.stationsValue = stations.map(toStationSpec).sort((a2, b) => axisPosition$1(this.axisValue, a2.center) - axisPosition$1(this.axisValue, b.center));
|
|
94928
96912
|
return this;
|
|
94929
96913
|
}
|
|
94930
96914
|
/** Attach named guide rails for product-skin construction and downstream surface references. */
|
|
@@ -94974,9 +96958,9 @@ class ProductSkinBuilder {
|
|
|
94974
96958
|
const [x2, y2] = crossPointForStation(this.axisValue, station.center);
|
|
94975
96959
|
return station.profile.sketch.translate(x2, y2);
|
|
94976
96960
|
});
|
|
94977
|
-
const heights = this.stationsValue.map((station) => axisPosition(this.axisValue, station.center));
|
|
96961
|
+
const heights = this.stationsValue.map((station) => axisPosition$1(this.axisValue, station.center));
|
|
94978
96962
|
let shape = loft(localProfiles, heights, { edgeLength: this.edgeLengthValue });
|
|
94979
|
-
shape = orientLoftToAxis(shape, this.axisValue);
|
|
96963
|
+
shape = orientLoftToAxis$1(shape, this.axisValue);
|
|
94980
96964
|
if (this.colorValue) shape = shape.color(this.colorValue);
|
|
94981
96965
|
shape = applyMaterial(shape, this.materialValue).as(this.name);
|
|
94982
96966
|
const warnings = [];
|
|
@@ -95635,7 +97619,7 @@ function requirePositive$3(value, label) {
|
|
|
95635
97619
|
function clamp$4(value, min2, max2) {
|
|
95636
97620
|
return Math.max(min2, Math.min(max2, value));
|
|
95637
97621
|
}
|
|
95638
|
-
function lerp$
|
|
97622
|
+
function lerp$3(a2, b, t) {
|
|
95639
97623
|
return a2 + (b - a2) * t;
|
|
95640
97624
|
}
|
|
95641
97625
|
function add(a2, b) {
|
|
@@ -95685,19 +97669,19 @@ function transformLocal(point2, tangentAcross, normal, tangentAlong, x2, y2, z2
|
|
|
95685
97669
|
function interpolateCylinder(a2, b, t, mode) {
|
|
95686
97670
|
let delta = b.angle - a2.angle;
|
|
95687
97671
|
if (mode === "shortest" && Math.abs(delta) > 180) delta -= Math.sign(delta) * 360;
|
|
95688
|
-
return { kind: "cylinder", angle: a2.angle + delta * t, z: lerp$
|
|
97672
|
+
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) };
|
|
95689
97673
|
}
|
|
95690
97674
|
function interpolatePlane(a2, b, t) {
|
|
95691
|
-
return { kind: "plane", x: lerp$
|
|
97675
|
+
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) };
|
|
95692
97676
|
}
|
|
95693
97677
|
function interpolateProductSkin(a2, b, t) {
|
|
95694
97678
|
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.");
|
|
95695
97679
|
return {
|
|
95696
97680
|
kind: "productSkin",
|
|
95697
97681
|
side: a2.side ?? b.side,
|
|
95698
|
-
u: lerp$
|
|
95699
|
-
v: lerp$
|
|
95700
|
-
offset: lerp$
|
|
97682
|
+
u: lerp$3(a2.u ?? 0.5, b.u ?? 0.5, t),
|
|
97683
|
+
v: lerp$3(a2.v ?? 0.5, b.v ?? 0.5, t),
|
|
97684
|
+
offset: lerp$3(a2.offset ?? 0, b.offset ?? 0, t)
|
|
95701
97685
|
};
|
|
95702
97686
|
}
|
|
95703
97687
|
class SurfacePath {
|
|
@@ -96020,11 +98004,11 @@ function coordinateOnSide(coordinate, side, label) {
|
|
|
96020
98004
|
return { ...coordinate, kind: "productSkin", side };
|
|
96021
98005
|
}
|
|
96022
98006
|
class ProductSkinCarrier {
|
|
96023
|
-
constructor(skin, name = skin.name,
|
|
98007
|
+
constructor(skin, name = skin.name, sideValue2, offsetValue = 0) {
|
|
96024
98008
|
__publicField(this, "kind", "productSkin");
|
|
96025
98009
|
this.skin = skin;
|
|
96026
98010
|
this.name = name;
|
|
96027
|
-
this.sideValue =
|
|
98011
|
+
this.sideValue = sideValue2;
|
|
96028
98012
|
this.offsetValue = offsetValue;
|
|
96029
98013
|
}
|
|
96030
98014
|
surface(side) {
|
|
@@ -96795,7 +98779,7 @@ function counterboresForPlate(spec2, width, height, thickness, diagnostics) {
|
|
|
96795
98779
|
function minWidthAcrossAlongRange(widthAtT, length4, minAlong, maxAlong) {
|
|
96796
98780
|
let minWidth = Number.POSITIVE_INFINITY;
|
|
96797
98781
|
for (let index2 = 0; index2 <= 8; index2 += 1) {
|
|
96798
|
-
const along = lerp$
|
|
98782
|
+
const along = lerp$3(minAlong, maxAlong, index2 / 8);
|
|
96799
98783
|
const t = Math.max(0, Math.min(1, (along + length4 / 2) / Math.max(length4, 1e-8)));
|
|
96800
98784
|
minWidth = Math.min(minWidth, widthAtT(t));
|
|
96801
98785
|
}
|
|
@@ -97095,7 +99079,7 @@ function pathParameterAtDistance(samples, distance2) {
|
|
|
97095
99079
|
const segmentLength = Math.hypot(b.point[0] - a2.point[0], b.point[1] - a2.point[1], b.point[2] - a2.point[2]);
|
|
97096
99080
|
if (traveled + segmentLength >= distance2) {
|
|
97097
99081
|
const localT = segmentLength <= 1e-8 ? 0 : (distance2 - traveled) / segmentLength;
|
|
97098
|
-
return lerp$
|
|
99082
|
+
return lerp$3(a2.t, b.t, localT);
|
|
97099
99083
|
}
|
|
97100
99084
|
traveled += segmentLength;
|
|
97101
99085
|
}
|
|
@@ -97148,7 +99132,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
97148
99132
|
const width = input.widthAt(t);
|
|
97149
99133
|
const along = distance2 - length4 / 2;
|
|
97150
99134
|
for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
|
|
97151
|
-
const across = lerp$
|
|
99135
|
+
const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
|
|
97152
99136
|
mesh.vertices.push(pointAtProfile([across, along], false));
|
|
97153
99137
|
}
|
|
97154
99138
|
}
|
|
@@ -97158,7 +99142,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
97158
99142
|
const width = input.widthAt(t);
|
|
97159
99143
|
const along = distance2 - length4 / 2;
|
|
97160
99144
|
for (let acrossIndex = 0; acrossIndex <= acrossSegments; acrossIndex += 1) {
|
|
97161
|
-
const across = lerp$
|
|
99145
|
+
const across = lerp$3(-width / 2, width / 2, acrossIndex / acrossSegments);
|
|
97162
99146
|
mesh.vertices.push(pointAtProfile([across, along], true));
|
|
97163
99147
|
}
|
|
97164
99148
|
}
|
|
@@ -97170,7 +99154,7 @@ function compileBandFootprintMesh(path2, input) {
|
|
|
97170
99154
|
const width = input.widthAt(t);
|
|
97171
99155
|
const along = distance2 - length4 / 2;
|
|
97172
99156
|
for (let acrossIndex = 0; acrossIndex < acrossSegments; acrossIndex += 1) {
|
|
97173
|
-
const across = lerp$
|
|
99157
|
+
const across = lerp$3(-width / 2, width / 2, (acrossIndex + 0.5) / acrossSegments);
|
|
97174
99158
|
filled[alongIndex][acrossIndex] = !holes.some((hole2) => pointInProfileLoop([across, along], hole2));
|
|
97175
99159
|
}
|
|
97176
99160
|
}
|
|
@@ -99348,7 +101332,7 @@ const Constraint = {
|
|
|
99348
101332
|
return builder.constrain({ type: "length", line: resolveLineId(builder, line2), value });
|
|
99349
101333
|
}
|
|
99350
101334
|
};
|
|
99351
|
-
function requireVec3(v, label) {
|
|
101335
|
+
function requireVec3$1(v, label) {
|
|
99352
101336
|
if (!Array.isArray(v) || v.length !== 3 || !Number.isFinite(v[0]) || !Number.isFinite(v[1]) || !Number.isFinite(v[2])) {
|
|
99353
101337
|
throw new Error(`${label} must be a [number, number, number] with finite values, got ${JSON.stringify(v)}`);
|
|
99354
101338
|
}
|
|
@@ -99361,24 +101345,24 @@ function requireFiniteNumber(n, label) {
|
|
|
99361
101345
|
return n;
|
|
99362
101346
|
}
|
|
99363
101347
|
function distance$1(a2, b) {
|
|
99364
|
-
requireVec3(a2, "a");
|
|
99365
|
-
requireVec3(b, "b");
|
|
101348
|
+
requireVec3$1(a2, "a");
|
|
101349
|
+
requireVec3$1(b, "b");
|
|
99366
101350
|
return Math.hypot(b[0] - a2[0], b[1] - a2[1], b[2] - a2[2]);
|
|
99367
101351
|
}
|
|
99368
101352
|
function midpoint$1(a2, b) {
|
|
99369
|
-
requireVec3(a2, "a");
|
|
99370
|
-
requireVec3(b, "b");
|
|
101353
|
+
requireVec3$1(a2, "a");
|
|
101354
|
+
requireVec3$1(b, "b");
|
|
99371
101355
|
return [(a2[0] + b[0]) / 2, (a2[1] + b[1]) / 2, (a2[2] + b[2]) / 2];
|
|
99372
101356
|
}
|
|
99373
|
-
function lerp(a2, b, t) {
|
|
99374
|
-
requireVec3(a2, "a");
|
|
99375
|
-
requireVec3(b, "b");
|
|
101357
|
+
function lerp$2(a2, b, t) {
|
|
101358
|
+
requireVec3$1(a2, "a");
|
|
101359
|
+
requireVec3$1(b, "b");
|
|
99376
101360
|
requireFiniteNumber(t, "t");
|
|
99377
101361
|
return [a2[0] + (b[0] - a2[0]) * t, a2[1] + (b[1] - a2[1]) * t, a2[2] + (b[2] - a2[2]) * t];
|
|
99378
101362
|
}
|
|
99379
101363
|
function direction(a2, b) {
|
|
99380
|
-
requireVec3(a2, "a");
|
|
99381
|
-
requireVec3(b, "b");
|
|
101364
|
+
requireVec3$1(a2, "a");
|
|
101365
|
+
requireVec3$1(b, "b");
|
|
99382
101366
|
const dx = b[0] - a2[0];
|
|
99383
101367
|
const dy = b[1] - a2[1];
|
|
99384
101368
|
const dz = b[2] - a2[2];
|
|
@@ -99389,8 +101373,8 @@ function direction(a2, b) {
|
|
|
99389
101373
|
return [dx / len2, dy / len2, dz / len2];
|
|
99390
101374
|
}
|
|
99391
101375
|
function offset(point2, dir, amount) {
|
|
99392
|
-
requireVec3(point2, "point");
|
|
99393
|
-
requireVec3(dir, "dir");
|
|
101376
|
+
requireVec3$1(point2, "point");
|
|
101377
|
+
requireVec3$1(dir, "dir");
|
|
99394
101378
|
requireFiniteNumber(amount, "amount");
|
|
99395
101379
|
return [point2[0] + dir[0] * amount, point2[1] + dir[1] * amount, point2[2] + dir[2] * amount];
|
|
99396
101380
|
}
|
|
@@ -99400,7 +101384,7 @@ const Points2 = {
|
|
|
99400
101384
|
/** Center point between two 3D points. */
|
|
99401
101385
|
midpoint: midpoint$1,
|
|
99402
101386
|
/** Linearly interpolate between two 3D points. t=0 returns a, t=1 returns b. */
|
|
99403
|
-
lerp,
|
|
101387
|
+
lerp: lerp$2,
|
|
99404
101388
|
/** Unit direction vector from a to b. Throws if a and b are the same point. */
|
|
99405
101389
|
direction,
|
|
99406
101390
|
/** Move a point along a direction vector by a given amount. */
|
|
@@ -104532,9 +106516,84 @@ class ConstraintSketch extends Sketch {
|
|
|
104532
106516
|
* Select the single arrangement region that contains the given seed point.
|
|
104533
106517
|
* Throws if no region contains the seed.
|
|
104534
106518
|
*/
|
|
104535
|
-
detectArrangementRegion(
|
|
106519
|
+
detectArrangementRegion(_seed2) {
|
|
104536
106520
|
throw new Error("Not implemented");
|
|
104537
106521
|
}
|
|
106522
|
+
/**
|
|
106523
|
+
* Return the solved constrained path as a sampled 2D polyline.
|
|
106524
|
+
*
|
|
106525
|
+
* Use this when a construction rail was authored with `constrainedSketch()`
|
|
106526
|
+
* and should feed another operation such as `Loft.pathOnXz(...)`.
|
|
106527
|
+
* The sketch must contain exactly one profile path.
|
|
106528
|
+
*
|
|
106529
|
+
* @param samples - Samples per curved segment. Default 32.
|
|
106530
|
+
* @returns The solved path as an open polyline.
|
|
106531
|
+
*/
|
|
106532
|
+
toPolyline(samples = 32) {
|
|
106533
|
+
if (!Number.isFinite(samples) || samples < 2) throw new Error("ConstraintSketch.toPolyline() samples must be at least 2");
|
|
106534
|
+
const profileLoops = this.definition.loops.filter((loop) => loop.type === "profile");
|
|
106535
|
+
if (profileLoops.length !== 1) {
|
|
106536
|
+
throw new Error("ConstraintSketch.toPolyline() requires exactly one profile path");
|
|
106537
|
+
}
|
|
106538
|
+
const sampleCount = Math.max(2, Math.round(samples));
|
|
106539
|
+
const pointMap = new Map(this.definition.points.map((point2) => [point2.id, point2]));
|
|
106540
|
+
const lineMap = new Map(this.definition.lines.map((line2) => [line2.id, line2]));
|
|
106541
|
+
const arcMap = new Map(this.definition.arcs.map((arc) => [arc.id, arc]));
|
|
106542
|
+
const bezierMap = new Map(this.definition.beziers.map((bezier) => [bezier.id, bezier]));
|
|
106543
|
+
const points = [];
|
|
106544
|
+
const appendStart = (point2, label) => {
|
|
106545
|
+
const previous = points[points.length - 1];
|
|
106546
|
+
if (!previous) {
|
|
106547
|
+
points.push(point2);
|
|
106548
|
+
return;
|
|
106549
|
+
}
|
|
106550
|
+
if (Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-6) {
|
|
106551
|
+
throw new Error(`ConstraintSketch.toPolyline() profile path is not continuous at ${label}`);
|
|
106552
|
+
}
|
|
106553
|
+
};
|
|
106554
|
+
const appendPoint = (point2) => {
|
|
106555
|
+
const previous = points[points.length - 1];
|
|
106556
|
+
if (!previous || Math.hypot(point2[0] - previous[0], point2[1] - previous[1]) > 1e-9) points.push(point2);
|
|
106557
|
+
};
|
|
106558
|
+
const requirePoint = (id, label) => {
|
|
106559
|
+
const point2 = pointMap.get(id);
|
|
106560
|
+
if (!point2) throw new Error(`ConstraintSketch.toPolyline() missing ${label}`);
|
|
106561
|
+
return [point2.x, point2.y];
|
|
106562
|
+
};
|
|
106563
|
+
for (const segment of profileLoops[0].segments) {
|
|
106564
|
+
if (segment.kind === "line") {
|
|
106565
|
+
const line2 = lineMap.get(segment.line);
|
|
106566
|
+
if (!line2) throw new Error(`ConstraintSketch.toPolyline() missing line "${segment.line}"`);
|
|
106567
|
+
appendStart(requirePoint(line2.a, `line "${segment.line}" start point`), `line "${segment.line}"`);
|
|
106568
|
+
appendPoint(requirePoint(line2.b, `line "${segment.line}" end point`));
|
|
106569
|
+
} else if (segment.kind === "arc") {
|
|
106570
|
+
const arc = arcMap.get(segment.arc);
|
|
106571
|
+
if (!arc) throw new Error(`ConstraintSketch.toPolyline() missing arc "${segment.arc}"`);
|
|
106572
|
+
const center = requirePoint(arc.center, `arc "${segment.arc}" center point`);
|
|
106573
|
+
const start = requirePoint(arc.start, `arc "${segment.arc}" start point`);
|
|
106574
|
+
const end = requirePoint(arc.end, `arc "${segment.arc}" end point`);
|
|
106575
|
+
appendStart(start, `arc "${segment.arc}"`);
|
|
106576
|
+
const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
|
|
106577
|
+
const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
|
|
106578
|
+
for (const point2 of tessellateArc(center[0], center[1], arc.radius, startAngle, endAngle, arc.clockwise, sampleCount)) {
|
|
106579
|
+
appendPoint(point2);
|
|
106580
|
+
}
|
|
106581
|
+
} else {
|
|
106582
|
+
const bezier = bezierMap.get(segment.bezier);
|
|
106583
|
+
if (!bezier) throw new Error(`ConstraintSketch.toPolyline() missing bezier "${segment.bezier}"`);
|
|
106584
|
+
const p0 = requirePoint(bezier.p0, `bezier "${segment.bezier}" start point`);
|
|
106585
|
+
const p1 = requirePoint(bezier.p1, `bezier "${segment.bezier}" first control point`);
|
|
106586
|
+
const p2 = requirePoint(bezier.p2, `bezier "${segment.bezier}" second control point`);
|
|
106587
|
+
const p3 = requirePoint(bezier.p3, `bezier "${segment.bezier}" end point`);
|
|
106588
|
+
appendStart(p0, `bezier "${segment.bezier}"`);
|
|
106589
|
+
for (const point2 of tessellateBezier(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1], p3[0], p3[1], sampleCount)) {
|
|
106590
|
+
appendPoint(point2);
|
|
106591
|
+
}
|
|
106592
|
+
}
|
|
106593
|
+
}
|
|
106594
|
+
if (points.length < 2) throw new Error("ConstraintSketch.toPolyline() needs at least 2 points");
|
|
106595
|
+
return points;
|
|
106596
|
+
}
|
|
104538
106597
|
/**
|
|
104539
106598
|
* Re-solve the sketch after changing the value of one existing constraint.
|
|
104540
106599
|
*
|
|
@@ -119832,6 +121891,295 @@ function polygonVertices(sides, radius, options) {
|
|
|
119832
121891
|
centerY: options == null ? void 0 : options.centerY
|
|
119833
121892
|
});
|
|
119834
121893
|
}
|
|
121894
|
+
const LOFT_GUIDE_EPS = 1e-8;
|
|
121895
|
+
function orientLoftToAxis(shape, axis) {
|
|
121896
|
+
if (axis === "Z") return shape;
|
|
121897
|
+
if (axis === "Y") return shape.rotateX(-90);
|
|
121898
|
+
return shape.transform([0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1]);
|
|
121899
|
+
}
|
|
121900
|
+
function buildRailEvaluators(rails, axis, start, end, railSamples) {
|
|
121901
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
121902
|
+
return rails.map((rail2) => {
|
|
121903
|
+
if (seen2.has(rail2.side)) throw new Error(`Loft.withGuideRails() received more than one ${rail2.side} rail`);
|
|
121904
|
+
seen2.add(rail2.side);
|
|
121905
|
+
const sampled = sampleRailPath(rail2.path, railSamples);
|
|
121906
|
+
if (sampled.length < 2) throw new Error("Loft guide rails require at least two points");
|
|
121907
|
+
const points = sampled.map((point2) => ({ position: axisPosition(axis, point2), cross: crossPointForAxis(axis, point2) }));
|
|
121908
|
+
const ordered = points[points.length - 1].position >= points[0].position ? points : [...points].reverse();
|
|
121909
|
+
validateRailCoverage(ordered, start, end);
|
|
121910
|
+
return { side: rail2.side, points: ordered };
|
|
121911
|
+
});
|
|
121912
|
+
}
|
|
121913
|
+
function railCrossAt(rail2, position) {
|
|
121914
|
+
const points = rail2.points;
|
|
121915
|
+
if (position <= points[0].position + LOFT_GUIDE_EPS) return points[0].cross;
|
|
121916
|
+
const last = points[points.length - 1];
|
|
121917
|
+
if (position >= last.position - LOFT_GUIDE_EPS) return last.cross;
|
|
121918
|
+
for (let index2 = 0; index2 < points.length - 1; index2 += 1) {
|
|
121919
|
+
const a2 = points[index2];
|
|
121920
|
+
const b = points[index2 + 1];
|
|
121921
|
+
if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
|
|
121922
|
+
const t = (position - a2.position) / (b.position - a2.position);
|
|
121923
|
+
return [lerp$1(a2.cross[0], b.cross[0], t), lerp$1(a2.cross[1], b.cross[1], t)];
|
|
121924
|
+
}
|
|
121925
|
+
}
|
|
121926
|
+
throw new Error("Loft guide rail does not cover requested station position");
|
|
121927
|
+
}
|
|
121928
|
+
function validateRailCoverage(points, start, end) {
|
|
121929
|
+
for (let index2 = 1; index2 < points.length; index2 += 1) {
|
|
121930
|
+
if (points[index2].position - points[index2 - 1].position < LOFT_GUIDE_EPS) {
|
|
121931
|
+
throw new Error("Loft guide rails must be monotone along the loft axis");
|
|
121932
|
+
}
|
|
121933
|
+
}
|
|
121934
|
+
if (points[0].position - start > LOFT_GUIDE_EPS || end - points[points.length - 1].position > LOFT_GUIDE_EPS) {
|
|
121935
|
+
throw new Error("Loft guide rails must cover the full station range");
|
|
121936
|
+
}
|
|
121937
|
+
}
|
|
121938
|
+
function sampleRailPath(path2, samples) {
|
|
121939
|
+
if (Array.isArray(path2)) return path2.map((point2, index2) => requireVec3(point2, `Loft guide rail point ${index2}`));
|
|
121940
|
+
if (path2 instanceof Curve3D || path2 instanceof HermiteCurve3D || path2 instanceof QuinticHermiteCurve3D || path2 instanceof NurbsCurve3D) {
|
|
121941
|
+
return path2.sample(Math.max(2, Math.round(samples))).map((point2, index2) => requireVec3(point2, `Loft guide rail sample ${index2}`));
|
|
121942
|
+
}
|
|
121943
|
+
throw new Error("Loft guide rail path must be a Vec3[] or ForgeCAD 3D curve");
|
|
121944
|
+
}
|
|
121945
|
+
function requireVec3(point2, label) {
|
|
121946
|
+
if (!Array.isArray(point2) || point2.length !== 3 || !point2.every(Number.isFinite)) {
|
|
121947
|
+
throw new Error(`${label} must be a finite [x, y, z] point`);
|
|
121948
|
+
}
|
|
121949
|
+
return [point2[0], point2[1], point2[2]];
|
|
121950
|
+
}
|
|
121951
|
+
function axisPosition(axis, point2) {
|
|
121952
|
+
if (axis === "X") return point2[0];
|
|
121953
|
+
if (axis === "Y") return point2[1];
|
|
121954
|
+
return point2[2];
|
|
121955
|
+
}
|
|
121956
|
+
function crossPointForAxis(axis, point2) {
|
|
121957
|
+
if (axis === "X") return [point2[1], point2[2]];
|
|
121958
|
+
if (axis === "Y") return [point2[0], -point2[2]];
|
|
121959
|
+
return [point2[0], point2[1]];
|
|
121960
|
+
}
|
|
121961
|
+
function lerp$1(a2, b, t) {
|
|
121962
|
+
return a2 + (b - a2) * t;
|
|
121963
|
+
}
|
|
121964
|
+
function loftWithGuideRails(stations, rails, options = {}) {
|
|
121965
|
+
if (stations.length < 2) throw new Error("Loft.withGuideRails() requires at least two stations");
|
|
121966
|
+
if (rails.length === 0) throw new Error("Loft.withGuideRails() requires at least one guide rail");
|
|
121967
|
+
const sortedStations = sortedValidStations(stations);
|
|
121968
|
+
const axis = options.axis ?? "Z";
|
|
121969
|
+
const start = sortedStations[0].position;
|
|
121970
|
+
const end = sortedStations[sortedStations.length - 1].position;
|
|
121971
|
+
const railEvaluators = buildRailEvaluators(rails, axis, start, end, options.railSamples ?? 64);
|
|
121972
|
+
const positions = generatedPositions(sortedStations, options.samples);
|
|
121973
|
+
const profiles2 = positions.map((position) => {
|
|
121974
|
+
const source = profileForPosition(sortedStations, position);
|
|
121975
|
+
const bounds = boundsForPosition(sortedStations, position);
|
|
121976
|
+
return fitProfileToBounds(source, applyRailsToBounds(bounds, railEvaluators, position));
|
|
121977
|
+
});
|
|
121978
|
+
const shape = loft(profiles2, positions, {
|
|
121979
|
+
edgeLength: options.edgeLength,
|
|
121980
|
+
boundsPadding: options.boundsPadding
|
|
121981
|
+
});
|
|
121982
|
+
return orientLoftToAxis(shape, axis);
|
|
121983
|
+
}
|
|
121984
|
+
function sortedValidStations(stations) {
|
|
121985
|
+
const sorted = [...stations].sort((a2, b) => a2.position - b.position);
|
|
121986
|
+
for (let index2 = 0; index2 < sorted.length; index2 += 1) {
|
|
121987
|
+
if (!Number.isFinite(sorted[index2].position)) throw new Error("Loft.withGuideRails station position must be finite");
|
|
121988
|
+
if (!(sorted[index2].profile instanceof Sketch)) throw new Error("Loft.withGuideRails() stations must use Sketch profiles");
|
|
121989
|
+
if (index2 > 0 && sorted[index2].position - sorted[index2 - 1].position < LOFT_GUIDE_EPS) {
|
|
121990
|
+
throw new Error("Loft.withGuideRails() requires unique, strictly increasing station positions");
|
|
121991
|
+
}
|
|
121992
|
+
}
|
|
121993
|
+
return sorted;
|
|
121994
|
+
}
|
|
121995
|
+
function generatedPositions(stations, samples) {
|
|
121996
|
+
const count = Math.max(2, Math.round(samples ?? Math.max(9, (stations.length - 1) * 8 + 1)));
|
|
121997
|
+
const start = stations[0].position;
|
|
121998
|
+
const end = stations[stations.length - 1].position;
|
|
121999
|
+
const values = /* @__PURE__ */ new Set();
|
|
122000
|
+
const positions = [];
|
|
122001
|
+
const addPosition = (position) => {
|
|
122002
|
+
const key = position.toFixed(9);
|
|
122003
|
+
if (!values.has(key)) {
|
|
122004
|
+
values.add(key);
|
|
122005
|
+
positions.push(position);
|
|
122006
|
+
}
|
|
122007
|
+
};
|
|
122008
|
+
for (let index2 = 0; index2 < count; index2 += 1) addPosition(start + (end - start) * index2 / (count - 1));
|
|
122009
|
+
for (const station of stations) addPosition(station.position);
|
|
122010
|
+
return positions.sort((a2, b) => a2 - b);
|
|
122011
|
+
}
|
|
122012
|
+
function profileForPosition(stations, position) {
|
|
122013
|
+
for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
|
|
122014
|
+
if (position <= stations[index2 + 1].position + LOFT_GUIDE_EPS) return stations[index2].profile;
|
|
122015
|
+
}
|
|
122016
|
+
return stations[stations.length - 1].profile;
|
|
122017
|
+
}
|
|
122018
|
+
function boundsForPosition(stations, position) {
|
|
122019
|
+
if (position <= stations[0].position + LOFT_GUIDE_EPS) return sketchBounds(stations[0].profile);
|
|
122020
|
+
const last = stations[stations.length - 1];
|
|
122021
|
+
if (position >= last.position - LOFT_GUIDE_EPS) return sketchBounds(last.profile);
|
|
122022
|
+
for (let index2 = 0; index2 < stations.length - 1; index2 += 1) {
|
|
122023
|
+
const a2 = stations[index2];
|
|
122024
|
+
const b = stations[index2 + 1];
|
|
122025
|
+
if (position >= a2.position - LOFT_GUIDE_EPS && position <= b.position + LOFT_GUIDE_EPS) {
|
|
122026
|
+
return lerpBounds(sketchBounds(a2.profile), sketchBounds(b.profile), (position - a2.position) / (b.position - a2.position));
|
|
122027
|
+
}
|
|
122028
|
+
}
|
|
122029
|
+
return sketchBounds(last.profile);
|
|
122030
|
+
}
|
|
122031
|
+
function applyRailsToBounds(bounds, rails, position) {
|
|
122032
|
+
const centerRail = rails.find((rail2) => rail2.side === "center");
|
|
122033
|
+
const center = centerRail ? railCrossAt(centerRail, position) : void 0;
|
|
122034
|
+
const next = { ...bounds };
|
|
122035
|
+
applyAxisRail(next, "X", sideValue(rails, "left", position, 0), sideValue(rails, "right", position, 0), center == null ? void 0 : center[0]);
|
|
122036
|
+
applyAxisRail(next, "Y", sideValue(rails, "back", position, 1), sideValue(rails, "front", position, 1), center == null ? void 0 : center[1]);
|
|
122037
|
+
if (next.maxX - next.minX < LOFT_GUIDE_EPS || next.maxY - next.minY < LOFT_GUIDE_EPS) {
|
|
122038
|
+
throw new Error("Loft.withGuideRails() guide rails produced a non-positive section size");
|
|
122039
|
+
}
|
|
122040
|
+
return next;
|
|
122041
|
+
}
|
|
122042
|
+
function sideValue(rails, side, position, crossIndex) {
|
|
122043
|
+
const rail2 = rails.find((entry) => entry.side === side);
|
|
122044
|
+
return rail2 ? railCrossAt(rail2, position)[crossIndex] : void 0;
|
|
122045
|
+
}
|
|
122046
|
+
function applyAxisRail(bounds, axis, minRail, maxRail, center) {
|
|
122047
|
+
const minKey = axis === "X" ? "minX" : "minY";
|
|
122048
|
+
const maxKey = axis === "X" ? "maxX" : "maxY";
|
|
122049
|
+
const width = bounds[maxKey] - bounds[minKey];
|
|
122050
|
+
if (minRail != null && maxRail != null) {
|
|
122051
|
+
if (maxRail - minRail < LOFT_GUIDE_EPS) throw new Error("Loft.withGuideRails() opposite guide rails crossed");
|
|
122052
|
+
if (center != null && Math.abs((minRail + maxRail) / 2 - center) > 1e-5) {
|
|
122053
|
+
throw new Error("Loft.withGuideRails() center rail conflicts with opposite side rails");
|
|
122054
|
+
}
|
|
122055
|
+
bounds[minKey] = minRail;
|
|
122056
|
+
bounds[maxKey] = maxRail;
|
|
122057
|
+
} else if (maxRail != null) {
|
|
122058
|
+
bounds[maxKey] = maxRail;
|
|
122059
|
+
bounds[minKey] = center != null ? 2 * center - maxRail : maxRail - width;
|
|
122060
|
+
} else if (minRail != null) {
|
|
122061
|
+
bounds[minKey] = minRail;
|
|
122062
|
+
bounds[maxKey] = center != null ? 2 * center - minRail : minRail + width;
|
|
122063
|
+
} else if (center != null) {
|
|
122064
|
+
bounds[minKey] = center - width / 2;
|
|
122065
|
+
bounds[maxKey] = center + width / 2;
|
|
122066
|
+
}
|
|
122067
|
+
}
|
|
122068
|
+
function fitProfileToBounds(profile, target) {
|
|
122069
|
+
const source = sketchBounds(profile);
|
|
122070
|
+
const sourceWidth = source.maxX - source.minX;
|
|
122071
|
+
const sourceDepth = source.maxY - source.minY;
|
|
122072
|
+
if (sourceWidth < LOFT_GUIDE_EPS || sourceDepth < LOFT_GUIDE_EPS) {
|
|
122073
|
+
throw new Error("Loft.withGuideRails() station profiles must have positive bounds");
|
|
122074
|
+
}
|
|
122075
|
+
const sourceCenter = [(source.minX + source.maxX) / 2, (source.minY + source.maxY) / 2];
|
|
122076
|
+
const targetCenter = [(target.minX + target.maxX) / 2, (target.minY + target.maxY) / 2];
|
|
122077
|
+
return profile.scaleAround(sourceCenter, [(target.maxX - target.minX) / sourceWidth, (target.maxY - target.minY) / sourceDepth]).translate(targetCenter[0] - sourceCenter[0], targetCenter[1] - sourceCenter[1]);
|
|
122078
|
+
}
|
|
122079
|
+
function sketchBounds(profile) {
|
|
122080
|
+
const bounds = profile.bounds();
|
|
122081
|
+
return { minX: bounds.min[0], maxX: bounds.max[0], minY: bounds.min[1], maxY: bounds.max[1] };
|
|
122082
|
+
}
|
|
122083
|
+
function lerpBounds(a2, b, t) {
|
|
122084
|
+
return {
|
|
122085
|
+
minX: lerp(a2.minX, b.minX, t),
|
|
122086
|
+
maxX: lerp(a2.maxX, b.maxX, t),
|
|
122087
|
+
minY: lerp(a2.minY, b.minY, t),
|
|
122088
|
+
maxY: lerp(a2.maxY, b.maxY, t)
|
|
122089
|
+
};
|
|
122090
|
+
}
|
|
122091
|
+
function lerp(a2, b, t) {
|
|
122092
|
+
return a2 + (b - a2) * t;
|
|
122093
|
+
}
|
|
122094
|
+
function mapLoftPath2D(path2, label, mapper) {
|
|
122095
|
+
const points = sampleLoftPath2D(path2, label);
|
|
122096
|
+
return points.map((point2, index2) => {
|
|
122097
|
+
if (!Array.isArray(point2) || point2.length !== 2 || !point2.every(Number.isFinite)) {
|
|
122098
|
+
throw new Error(`${label} point ${index2} must be a finite [x, y] point`);
|
|
122099
|
+
}
|
|
122100
|
+
return mapper([point2[0], point2[1]]);
|
|
122101
|
+
});
|
|
122102
|
+
}
|
|
122103
|
+
function sampleLoftPath2D(path2, label) {
|
|
122104
|
+
if (Array.isArray(path2)) {
|
|
122105
|
+
if (path2.length < 2) throw new Error(`${label} requires at least two [x, y] points`);
|
|
122106
|
+
return path2;
|
|
122107
|
+
}
|
|
122108
|
+
if (!path2 || typeof path2 !== "object" || typeof path2.toPolyline !== "function") {
|
|
122109
|
+
throw new Error(`${label} requires a 2D path, solved constrained path, or [x, y] point array`);
|
|
122110
|
+
}
|
|
122111
|
+
const points = path2.toPolyline();
|
|
122112
|
+
if (!Array.isArray(points) || points.length < 2) throw new Error(`${label} path must produce at least two [x, y] points`);
|
|
122113
|
+
return points;
|
|
122114
|
+
}
|
|
122115
|
+
const Loft = {
|
|
122116
|
+
/** Create a loft station from a 2D profile and an axis position. */
|
|
122117
|
+
station(profile, position) {
|
|
122118
|
+
if (!Number.isFinite(position)) throw new Error("Loft.station position must be finite");
|
|
122119
|
+
return { profile, position };
|
|
122120
|
+
},
|
|
122121
|
+
/** Create a guide rail that constrains the section-local negative-X side. */
|
|
122122
|
+
leftRail(path2) {
|
|
122123
|
+
return { side: "left", path: path2 };
|
|
122124
|
+
},
|
|
122125
|
+
/** Create a guide rail that constrains the section-local positive-X side. */
|
|
122126
|
+
rightRail(path2) {
|
|
122127
|
+
return { side: "right", path: path2 };
|
|
122128
|
+
},
|
|
122129
|
+
/** Create a guide rail that constrains the section-local positive-Y side. */
|
|
122130
|
+
frontRail(path2) {
|
|
122131
|
+
return { side: "front", path: path2 };
|
|
122132
|
+
},
|
|
122133
|
+
/** Create a guide rail that constrains the section-local negative-Y side. */
|
|
122134
|
+
backRail(path2) {
|
|
122135
|
+
return { side: "back", path: path2 };
|
|
122136
|
+
},
|
|
122137
|
+
/** Create a guide rail that moves section centers along the loft. */
|
|
122138
|
+
centerRail(path2) {
|
|
122139
|
+
return { side: "center", path: path2 };
|
|
122140
|
+
},
|
|
122141
|
+
/**
|
|
122142
|
+
* Place a 2D guide path onto the XZ plane.
|
|
122143
|
+
*
|
|
122144
|
+
* The path's first coordinate becomes X and its second coordinate becomes Z.
|
|
122145
|
+
* Use this for left/right silhouette rails authored with `path()` or `constrainedSketch()`.
|
|
122146
|
+
*/
|
|
122147
|
+
pathOnXz(path2, y2 = 0) {
|
|
122148
|
+
if (!Number.isFinite(y2)) throw new Error("Loft.pathOnXz y must be finite");
|
|
122149
|
+
return mapLoftPath2D(path2, "Loft.pathOnXz", ([x2, z2]) => [x2, y2, z2]);
|
|
122150
|
+
},
|
|
122151
|
+
/**
|
|
122152
|
+
* Place a 2D guide path onto the YZ plane.
|
|
122153
|
+
*
|
|
122154
|
+
* The path's first coordinate becomes Y and its second coordinate becomes Z.
|
|
122155
|
+
* Use this for front/back crown rails authored with `path()` or `constrainedSketch()`.
|
|
122156
|
+
*/
|
|
122157
|
+
pathOnYz(path2, x2 = 0) {
|
|
122158
|
+
if (!Number.isFinite(x2)) throw new Error("Loft.pathOnYz x must be finite");
|
|
122159
|
+
return mapLoftPath2D(path2, "Loft.pathOnYz", ([y2, z2]) => [x2, y2, z2]);
|
|
122160
|
+
},
|
|
122161
|
+
/**
|
|
122162
|
+
* Place a 2D guide path onto the XY plane.
|
|
122163
|
+
*
|
|
122164
|
+
* The path's first coordinate becomes X and its second coordinate becomes Y.
|
|
122165
|
+
* Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
|
|
122166
|
+
*/
|
|
122167
|
+
pathOnXy(path2, z2 = 0) {
|
|
122168
|
+
if (!Number.isFinite(z2)) throw new Error("Loft.pathOnXy z must be finite");
|
|
122169
|
+
return mapLoftPath2D(path2, "Loft.pathOnXy", ([x2, y2]) => [x2, y2, z2]);
|
|
122170
|
+
},
|
|
122171
|
+
/**
|
|
122172
|
+
* Loft through profile stations while forcing generated sections to follow guide rails.
|
|
122173
|
+
*
|
|
122174
|
+
* Stations define the cross-section family. Guide rails define the side or center
|
|
122175
|
+
* paths the loft must pass through. With opposite side rails, the section is scaled
|
|
122176
|
+
* to touch both rails. With one side rail, the section keeps its interpolated size
|
|
122177
|
+
* unless a center rail is also present.
|
|
122178
|
+
*/
|
|
122179
|
+
withGuideRails(stations, rails, options = {}) {
|
|
122180
|
+
return loftWithGuideRails(stations, rails, options);
|
|
122181
|
+
}
|
|
122182
|
+
};
|
|
119835
122183
|
let collectedHighlights = [];
|
|
119836
122184
|
function resetHighlights() {
|
|
119837
122185
|
collectedHighlights = [];
|
|
@@ -124953,10 +127301,14 @@ function spec(name, checkFn) {
|
|
|
124953
127301
|
};
|
|
124954
127302
|
}
|
|
124955
127303
|
let _collected = [];
|
|
127304
|
+
let _collisionAllowances = [];
|
|
127305
|
+
let _physicalComponentExpectations = [];
|
|
124956
127306
|
let _counter = 0;
|
|
124957
127307
|
let _activeGroup = null;
|
|
124958
127308
|
function resetVerifications() {
|
|
124959
127309
|
_collected = [];
|
|
127310
|
+
_collisionAllowances = [];
|
|
127311
|
+
_physicalComponentExpectations = [];
|
|
124960
127312
|
_counter = 0;
|
|
124961
127313
|
}
|
|
124962
127314
|
function getCollectedVerifications() {
|
|
@@ -124990,15 +127342,35 @@ function push(result) {
|
|
|
124990
127342
|
function roundNum(n, digits = 4) {
|
|
124991
127343
|
return Number.isFinite(n) ? n.toFixed(digits).replace(/\.?0+$/, "") : String(n);
|
|
124992
127344
|
}
|
|
127345
|
+
function meshDerivedManifoldBackend(shape) {
|
|
127346
|
+
const mesh = getShapeRuntimeBackend(shape).getMesh();
|
|
127347
|
+
return reconstructBackendFromMesh({
|
|
127348
|
+
numProp: mesh.numProp,
|
|
127349
|
+
triVerts: mesh.triVerts,
|
|
127350
|
+
vertProperties: mesh.vertProperties,
|
|
127351
|
+
mergeFromVert: mesh.mergeFromVert ?? new Uint32Array(),
|
|
127352
|
+
mergeToVert: mesh.mergeToVert ?? new Uint32Array()
|
|
127353
|
+
});
|
|
127354
|
+
}
|
|
127355
|
+
function backendForMinGap(shape) {
|
|
127356
|
+
const backend = getShapeRuntimeBackend(shape);
|
|
127357
|
+
if (isManifoldCapableBackend(backend)) return { backend, method: "exact", dispose: false };
|
|
127358
|
+
return { backend: meshDerivedManifoldBackend(shape), method: "mesh-derived", dispose: true };
|
|
127359
|
+
}
|
|
124993
127360
|
function computeMinGap(a2, b, searchLength) {
|
|
124994
|
-
const backendA =
|
|
124995
|
-
const backendB =
|
|
124996
|
-
|
|
124997
|
-
|
|
127361
|
+
const backendA = backendForMinGap(a2);
|
|
127362
|
+
const backendB = backendForMinGap(b);
|
|
127363
|
+
try {
|
|
127364
|
+
const manifoldA = requireManifoldShapeBackend(backendA.backend, "verification.minGap");
|
|
127365
|
+
const manifoldB = requireManifoldShapeBackend(backendB.backend, "verification.minGap");
|
|
127366
|
+
return {
|
|
127367
|
+
gap: manifoldA.minGap(manifoldB, searchLength),
|
|
127368
|
+
method: backendA.method === "exact" && backendB.method === "exact" ? "exact" : "mesh-derived"
|
|
127369
|
+
};
|
|
127370
|
+
} finally {
|
|
127371
|
+
if (backendA.dispose) disposeShapeBackend(backendA.backend);
|
|
127372
|
+
if (backendB.dispose) disposeShapeBackend(backendB.backend);
|
|
124998
127373
|
}
|
|
124999
|
-
const manifoldA = backendA.requireManifold("verification.minGap");
|
|
125000
|
-
const manifoldB = requireManifoldShapeBackend(backendB, "verification.minGap");
|
|
125001
|
-
return manifoldA.minGap(manifoldB, searchLength);
|
|
125002
127374
|
}
|
|
125003
127375
|
function vec3Dot(a2, b) {
|
|
125004
127376
|
return a2[0] * b[0] + a2[1] * b[1] + a2[2] * b[2];
|
|
@@ -125133,8 +127505,143 @@ const verify = {
|
|
|
125133
127505
|
actual: `${roundNum(d2, 3)} mm`
|
|
125134
127506
|
});
|
|
125135
127507
|
} catch (e) {
|
|
125136
|
-
push({
|
|
127508
|
+
push({
|
|
127509
|
+
id: nextId(),
|
|
127510
|
+
label,
|
|
127511
|
+
kind: "interface",
|
|
127512
|
+
status: "fail",
|
|
127513
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
127514
|
+
line: line2
|
|
127515
|
+
});
|
|
127516
|
+
}
|
|
127517
|
+
},
|
|
127518
|
+
/**
|
|
127519
|
+
* Check the distance between two named connectors on a shape or group.
|
|
127520
|
+
*
|
|
127521
|
+
* Use this when connectors + `matchTo()` define a static assembly interface.
|
|
127522
|
+
* It proves the mate at runtime, unlike a plain source-level connector
|
|
127523
|
+
* declaration. The common case is `expected = 0`, meaning the two connector
|
|
127524
|
+
* origins should coincide after placement.
|
|
127525
|
+
*
|
|
127526
|
+
* **Example**
|
|
127527
|
+
*
|
|
127528
|
+
* ```ts
|
|
127529
|
+
* verify.connectorDistance("leg is seated", bench, "Rail.leg_0", "Leg0.head", 0, 0.01);
|
|
127530
|
+
* ```
|
|
127531
|
+
*/
|
|
127532
|
+
connectorDistance(label, target, connectorA, connectorB, expected = 0, tolerance = 0.01) {
|
|
127533
|
+
const line2 = captureSourceLine();
|
|
127534
|
+
try {
|
|
127535
|
+
const actual = target.connectorDistance(connectorA, connectorB);
|
|
127536
|
+
const diff = Math.abs(actual - expected);
|
|
127537
|
+
const passed = diff <= Math.abs(tolerance);
|
|
127538
|
+
push({
|
|
127539
|
+
id: nextId(),
|
|
127540
|
+
label,
|
|
127541
|
+
kind: "interface",
|
|
127542
|
+
status: passed ? "pass" : "fail",
|
|
127543
|
+
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`,
|
|
127544
|
+
line: passed ? void 0 : line2,
|
|
127545
|
+
expected: `${roundNum(expected, 4)} ± ${roundNum(tolerance, 4)} mm`,
|
|
127546
|
+
actual: `${roundNum(actual, 4)} mm`
|
|
127547
|
+
});
|
|
127548
|
+
} catch (e) {
|
|
127549
|
+
push({
|
|
127550
|
+
id: nextId(),
|
|
127551
|
+
label,
|
|
127552
|
+
kind: "interface",
|
|
127553
|
+
status: "fail",
|
|
127554
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
127555
|
+
line: line2
|
|
127556
|
+
});
|
|
127557
|
+
}
|
|
127558
|
+
},
|
|
127559
|
+
/**
|
|
127560
|
+
* Declare the expected physical connectivity component count for the returned visible model.
|
|
127561
|
+
*
|
|
127562
|
+
* **Details**
|
|
127563
|
+
*
|
|
127564
|
+
* Use this for generated mechanical models that should have a clear component graph:
|
|
127565
|
+
* one connected fixture, a purchased part plus a removable cartridge, a root assembly plus
|
|
127566
|
+
* named intentional ghosts, and so on. `forgecad inspect mechanical-integrity` resolves the returned
|
|
127567
|
+
* visible objects with the same physical-connectivity analysis used in the quality gate and
|
|
127568
|
+
* fails if the actual component count differs.
|
|
127569
|
+
*
|
|
127570
|
+
* This catches the common generated-CAD failure where a script returns a visually plausible
|
|
127571
|
+
* artifact but the handle, screw, washer, cover, or terminal block is actually a separate island.
|
|
127572
|
+
*
|
|
127573
|
+
* **Example**
|
|
127574
|
+
*
|
|
127575
|
+
* ```ts
|
|
127576
|
+
* verify.physicalComponentCount("vise is one connected installed assembly", 1);
|
|
127577
|
+
* ```
|
|
127578
|
+
*/
|
|
127579
|
+
physicalComponentCount(label, expected) {
|
|
127580
|
+
const line2 = captureSourceLine();
|
|
127581
|
+
const id = nextId();
|
|
127582
|
+
if (!Number.isInteger(expected) || expected < 0) {
|
|
127583
|
+
push({
|
|
127584
|
+
id,
|
|
127585
|
+
label,
|
|
127586
|
+
kind: "interface",
|
|
127587
|
+
status: "fail",
|
|
127588
|
+
message: "Expected physical component count must be a non-negative integer",
|
|
127589
|
+
line: line2
|
|
127590
|
+
});
|
|
127591
|
+
return;
|
|
127592
|
+
}
|
|
127593
|
+
_physicalComponentExpectations.push({ id, label, expected, line: line2 });
|
|
127594
|
+
push({
|
|
127595
|
+
id,
|
|
127596
|
+
label,
|
|
127597
|
+
kind: "interface",
|
|
127598
|
+
status: "pass",
|
|
127599
|
+
message: `Expected ${expected} physical component(s); checked by mechanical-integrity connectivity`
|
|
127600
|
+
});
|
|
127601
|
+
},
|
|
127602
|
+
/**
|
|
127603
|
+
* Declare that two visible objects intentionally overlap because the overlap is real manufacturing intent.
|
|
127604
|
+
*
|
|
127605
|
+
* **Details**
|
|
127606
|
+
*
|
|
127607
|
+
* Use this only for overlaps that a mechanical reviewer would accept as actual matter sharing volume:
|
|
127608
|
+
* welded/fused regions, overmolded inserts, potted electronics, cast-in hardware, or deliberately
|
|
127609
|
+
* bonded laminations. This is not a shortcut for screws without holes, shafts without bores, covers
|
|
127610
|
+
* without pockets, or parts placed with collision as a positioning hack.
|
|
127611
|
+
*
|
|
127612
|
+
* `forgecad inspect mechanical-integrity --collisions` only honors this declaration when both shapes are
|
|
127613
|
+
* returned as visible objects and the exact collision report finds that same object pair. Unused or
|
|
127614
|
+
* non-visible declarations fail the quality gate so annotations cannot hide unrelated collisions.
|
|
127615
|
+
*
|
|
127616
|
+
* **Example**
|
|
127617
|
+
*
|
|
127618
|
+
* ```ts
|
|
127619
|
+
* verify.intentionalOverlap("rubber grip is overmolded on handle", rubberGrip, handleCore, "overmolded insert");
|
|
127620
|
+
* ```
|
|
127621
|
+
*/
|
|
127622
|
+
intentionalOverlap(label, a2, b, reason) {
|
|
127623
|
+
const line2 = captureSourceLine();
|
|
127624
|
+
const id = nextId();
|
|
127625
|
+
const trimmedReason = String(reason ?? "").trim();
|
|
127626
|
+
if (trimmedReason.length === 0) {
|
|
127627
|
+
push({
|
|
127628
|
+
id,
|
|
127629
|
+
label,
|
|
127630
|
+
kind: "interface",
|
|
127631
|
+
status: "fail",
|
|
127632
|
+
message: "Intentional overlap requires a manufacturing reason",
|
|
127633
|
+
line: line2
|
|
127634
|
+
});
|
|
127635
|
+
return;
|
|
125137
127636
|
}
|
|
127637
|
+
_collisionAllowances.push({ id, label, reason: trimmedReason, a: a2, b, line: line2 });
|
|
127638
|
+
push({
|
|
127639
|
+
id,
|
|
127640
|
+
label,
|
|
127641
|
+
kind: "interface",
|
|
127642
|
+
status: "pass",
|
|
127643
|
+
message: `Intentional overlap declared: ${trimmedReason}`
|
|
127644
|
+
});
|
|
125138
127645
|
},
|
|
125139
127646
|
/**
|
|
125140
127647
|
* Check that two shapes do not collide (minGap > 0).
|
|
@@ -125144,19 +127651,28 @@ const verify = {
|
|
|
125144
127651
|
notColliding(label, a2, b, searchLength = 1) {
|
|
125145
127652
|
const line2 = captureSourceLine();
|
|
125146
127653
|
try {
|
|
125147
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
127654
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
127655
|
+
const methodLabel = method === "exact" ? "exact min gap" : "mesh-derived min gap";
|
|
125148
127656
|
const passed = gap > 0;
|
|
125149
127657
|
push({
|
|
125150
127658
|
id: nextId(),
|
|
125151
127659
|
label,
|
|
127660
|
+
kind: "interface",
|
|
125152
127661
|
status: passed ? "pass" : "fail",
|
|
125153
|
-
message: passed ? `No collision (
|
|
127662
|
+
message: passed ? `No collision (${methodLabel} ${roundNum(gap, 3)} mm)` : `Shapes are colliding (${methodLabel} ${roundNum(gap, 3)} mm ≤ 0)`,
|
|
125154
127663
|
line: passed ? void 0 : line2,
|
|
125155
127664
|
expected: "> 0 mm",
|
|
125156
127665
|
actual: `${roundNum(gap, 3)} mm`
|
|
125157
127666
|
});
|
|
125158
127667
|
} catch (e) {
|
|
125159
|
-
push({
|
|
127668
|
+
push({
|
|
127669
|
+
id: nextId(),
|
|
127670
|
+
label,
|
|
127671
|
+
kind: "interface",
|
|
127672
|
+
status: "fail",
|
|
127673
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
127674
|
+
line: line2
|
|
127675
|
+
});
|
|
125160
127676
|
}
|
|
125161
127677
|
},
|
|
125162
127678
|
/**
|
|
@@ -125165,13 +127681,15 @@ const verify = {
|
|
|
125165
127681
|
minClearance(label, a2, b, minGap, searchLength = 10) {
|
|
125166
127682
|
const line2 = captureSourceLine();
|
|
125167
127683
|
try {
|
|
125168
|
-
const gap = computeMinGap(a2, b, searchLength);
|
|
127684
|
+
const { gap, method } = computeMinGap(a2, b, searchLength);
|
|
127685
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
125169
127686
|
const passed = gap >= minGap;
|
|
125170
127687
|
push({
|
|
125171
127688
|
id: nextId(),
|
|
125172
127689
|
label,
|
|
127690
|
+
kind: "interface",
|
|
125173
127691
|
status: passed ? "pass" : "fail",
|
|
125174
|
-
message: passed ?
|
|
127692
|
+
message: passed ? `${methodLabel} ${roundNum(gap, 3)} mm ≥ ${roundNum(minGap, 3)} mm` : `${methodLabel} ${roundNum(gap, 3)} mm < required ${roundNum(minGap, 3)} mm`,
|
|
125175
127693
|
line: passed ? void 0 : line2,
|
|
125176
127694
|
expected: `≥ ${roundNum(minGap, 3)} mm`,
|
|
125177
127695
|
actual: `${roundNum(gap, 3)} mm`
|
|
@@ -125180,6 +127698,90 @@ const verify = {
|
|
|
125180
127698
|
push({ id: nextId(), label, status: "fail", message: `Error: ${e instanceof Error ? e.message : String(e)}`, line: line2 });
|
|
125181
127699
|
}
|
|
125182
127700
|
},
|
|
127701
|
+
/**
|
|
127702
|
+
* Check that the clearance gap between two shapes is inside an allowed range.
|
|
127703
|
+
*
|
|
127704
|
+
* **Details**
|
|
127705
|
+
*
|
|
127706
|
+
* Use this for seated and retained interfaces where a part must be close
|
|
127707
|
+
* enough to be mechanically accountable, but must not collide beyond the
|
|
127708
|
+
* allowed minimum. It catches both failure modes that make generated CAD look
|
|
127709
|
+
* fake: parts floating away from their receiver, and parts intersecting their
|
|
127710
|
+
* receiver because the pocket, bore, or running clearance was not modeled.
|
|
127711
|
+
*
|
|
127712
|
+
* For contact, use a narrow range such as `[-0.01, 0.05]` to tolerate tiny
|
|
127713
|
+
* numerical noise. For a running fit, use the intended clearance band.
|
|
127714
|
+
*
|
|
127715
|
+
* Manifold-backed shapes use exact min-gap distance. Other backends use a
|
|
127716
|
+
* mesh-derived min-gap check and say so in the verification message; keep
|
|
127717
|
+
* `forgecad inspect mechanical-integrity --collisions` in the acceptance gate for
|
|
127718
|
+
* positive-volume interference.
|
|
127719
|
+
*
|
|
127720
|
+
* **Example**
|
|
127721
|
+
*
|
|
127722
|
+
* ```ts
|
|
127723
|
+
* verify.clearanceBetween("cover is seated on gasket", cover, gasket, -0.01, 0.05);
|
|
127724
|
+
* verify.clearanceBetween("carriage runs inside rail", carriage, rail, 0.2, 0.5);
|
|
127725
|
+
* ```
|
|
127726
|
+
*/
|
|
127727
|
+
clearanceBetween(label, a2, b, minGap, maxGap, searchLength) {
|
|
127728
|
+
const line2 = captureSourceLine();
|
|
127729
|
+
try {
|
|
127730
|
+
if (!Number.isFinite(minGap) || !Number.isFinite(maxGap)) {
|
|
127731
|
+
push({
|
|
127732
|
+
id: nextId(),
|
|
127733
|
+
label,
|
|
127734
|
+
kind: "interface",
|
|
127735
|
+
status: "fail",
|
|
127736
|
+
message: "Clearance range must use finite numbers",
|
|
127737
|
+
line: line2
|
|
127738
|
+
});
|
|
127739
|
+
return;
|
|
127740
|
+
}
|
|
127741
|
+
if (maxGap < minGap) {
|
|
127742
|
+
push({
|
|
127743
|
+
id: nextId(),
|
|
127744
|
+
label,
|
|
127745
|
+
kind: "interface",
|
|
127746
|
+
status: "fail",
|
|
127747
|
+
message: `Clearance max ${roundNum(maxGap, 3)} mm is smaller than min ${roundNum(minGap, 3)} mm`,
|
|
127748
|
+
line: line2
|
|
127749
|
+
});
|
|
127750
|
+
return;
|
|
127751
|
+
}
|
|
127752
|
+
const search = searchLength ?? Math.max(10, Math.abs(maxGap) * 2 + 1);
|
|
127753
|
+
const { gap, method } = computeMinGap(a2, b, search);
|
|
127754
|
+
const methodLabel = method === "exact" ? "exact gap" : "mesh-derived gap";
|
|
127755
|
+
const passed = gap >= minGap && gap <= maxGap;
|
|
127756
|
+
let message;
|
|
127757
|
+
if (passed) {
|
|
127758
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm in [${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`;
|
|
127759
|
+
} else if (gap < minGap) {
|
|
127760
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm < allowed minimum ${roundNum(minGap, 3)} mm`;
|
|
127761
|
+
} else {
|
|
127762
|
+
message = `${methodLabel} ${roundNum(gap, 3)} mm > allowed maximum ${roundNum(maxGap, 3)} mm`;
|
|
127763
|
+
}
|
|
127764
|
+
push({
|
|
127765
|
+
id: nextId(),
|
|
127766
|
+
label,
|
|
127767
|
+
kind: "interface",
|
|
127768
|
+
status: passed ? "pass" : "fail",
|
|
127769
|
+
message,
|
|
127770
|
+
line: passed ? void 0 : line2,
|
|
127771
|
+
expected: `[${roundNum(minGap, 3)}, ${roundNum(maxGap, 3)}] mm`,
|
|
127772
|
+
actual: `${roundNum(gap, 3)} mm`
|
|
127773
|
+
});
|
|
127774
|
+
} catch (e) {
|
|
127775
|
+
push({
|
|
127776
|
+
id: nextId(),
|
|
127777
|
+
label,
|
|
127778
|
+
kind: "interface",
|
|
127779
|
+
status: "fail",
|
|
127780
|
+
message: `Error: ${e instanceof Error ? e.message : String(e)}`,
|
|
127781
|
+
line: line2
|
|
127782
|
+
});
|
|
127783
|
+
}
|
|
127784
|
+
},
|
|
125183
127785
|
/**
|
|
125184
127786
|
* Check that two face normals are parallel (within toleranceDeg degrees).
|
|
125185
127787
|
*/
|
|
@@ -338491,6 +341093,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
|
|
|
338491
341093
|
nurbsSurface,
|
|
338492
341094
|
spline2d,
|
|
338493
341095
|
spline3d,
|
|
341096
|
+
Loft,
|
|
338494
341097
|
loft,
|
|
338495
341098
|
loftAlongSpine,
|
|
338496
341099
|
sweep,
|
|
@@ -338946,6 +341549,356 @@ function runScript(code, fileName = "main.forge.js", allFiles = {}, options = {}
|
|
|
338946
341549
|
}
|
|
338947
341550
|
}
|
|
338948
341551
|
}
|
|
341552
|
+
const DEFAULT_LEAF_SIZE = 8;
|
|
341553
|
+
function cloneVec3$1(value) {
|
|
341554
|
+
return [value[0], value[1], value[2]];
|
|
341555
|
+
}
|
|
341556
|
+
function emptyBounds() {
|
|
341557
|
+
return {
|
|
341558
|
+
min: [Infinity, Infinity, Infinity],
|
|
341559
|
+
max: [-Infinity, -Infinity, -Infinity]
|
|
341560
|
+
};
|
|
341561
|
+
}
|
|
341562
|
+
function expandBounds(bounds, entry) {
|
|
341563
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
341564
|
+
bounds.min[axis] = Math.min(bounds.min[axis], entry.min[axis]);
|
|
341565
|
+
bounds.max[axis] = Math.max(bounds.max[axis], entry.max[axis]);
|
|
341566
|
+
}
|
|
341567
|
+
}
|
|
341568
|
+
function boundsFor(entries, indexes) {
|
|
341569
|
+
const bounds = emptyBounds();
|
|
341570
|
+
for (const index2 of indexes) expandBounds(bounds, entries[index2]);
|
|
341571
|
+
return bounds;
|
|
341572
|
+
}
|
|
341573
|
+
function longestAxis(bounds) {
|
|
341574
|
+
const sizes = [bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]];
|
|
341575
|
+
if (sizes[1] > sizes[0] && sizes[1] >= sizes[2]) return 1;
|
|
341576
|
+
if (sizes[2] > sizes[0] && sizes[2] > sizes[1]) return 2;
|
|
341577
|
+
return 0;
|
|
341578
|
+
}
|
|
341579
|
+
function centerOnAxis(entry, axis) {
|
|
341580
|
+
return (entry.min[axis] + entry.max[axis]) * 0.5;
|
|
341581
|
+
}
|
|
341582
|
+
function nodeVolume(node) {
|
|
341583
|
+
return Math.max(0, node.max[0] - node.min[0]) * Math.max(0, node.max[1] - node.min[1]) * Math.max(0, node.max[2] - node.min[2]);
|
|
341584
|
+
}
|
|
341585
|
+
function aabbOverlaps(a2, b, padding = 0) {
|
|
341586
|
+
if (padding <= 0) {
|
|
341587
|
+
return [0, 1, 2].every((axis) => a2.min[axis] < b.max[axis] && a2.max[axis] > b.min[axis]);
|
|
341588
|
+
}
|
|
341589
|
+
return [0, 1, 2].every((axis) => a2.min[axis] <= b.max[axis] + padding && a2.max[axis] + padding >= b.min[axis]);
|
|
341590
|
+
}
|
|
341591
|
+
function aabbGaps(a2, b) {
|
|
341592
|
+
return [
|
|
341593
|
+
Math.max(0, b.min[0] - a2.max[0], a2.min[0] - b.max[0]),
|
|
341594
|
+
Math.max(0, b.min[1] - a2.max[1], a2.min[1] - b.max[1]),
|
|
341595
|
+
Math.max(0, b.min[2] - a2.max[2], a2.min[2] - b.max[2])
|
|
341596
|
+
];
|
|
341597
|
+
}
|
|
341598
|
+
function aabbInteriorOverlaps(a2, b) {
|
|
341599
|
+
return [0, 1, 2].every((axis) => Math.min(a2.max[axis], b.max[axis]) - Math.max(a2.min[axis], b.min[axis]) > 0);
|
|
341600
|
+
}
|
|
341601
|
+
function aabbOverlapVolume(a2, b) {
|
|
341602
|
+
let volume = 1;
|
|
341603
|
+
for (const axis of [0, 1, 2]) {
|
|
341604
|
+
const overlap = Math.min(a2.max[axis], b.max[axis]) - Math.max(a2.min[axis], b.min[axis]);
|
|
341605
|
+
if (overlap <= 0) return 0;
|
|
341606
|
+
volume *= overlap;
|
|
341607
|
+
}
|
|
341608
|
+
return volume;
|
|
341609
|
+
}
|
|
341610
|
+
class AabbSpatialIndex {
|
|
341611
|
+
constructor(entries, leafSize = DEFAULT_LEAF_SIZE) {
|
|
341612
|
+
__publicField(this, "root");
|
|
341613
|
+
__publicField(this, "nodeCount");
|
|
341614
|
+
this.entries = entries;
|
|
341615
|
+
this.leafSize = leafSize;
|
|
341616
|
+
let nodeCount = 0;
|
|
341617
|
+
const build = (indexes) => {
|
|
341618
|
+
nodeCount += 1;
|
|
341619
|
+
const bounds = boundsFor(entries, indexes);
|
|
341620
|
+
if (indexes.length <= this.leafSize) {
|
|
341621
|
+
return { min: cloneVec3$1(bounds.min), max: cloneVec3$1(bounds.max), left: null, right: null, indexes };
|
|
341622
|
+
}
|
|
341623
|
+
const axis = longestAxis(bounds);
|
|
341624
|
+
indexes.sort((a2, b) => centerOnAxis(entries[a2], axis) - centerOnAxis(entries[b], axis) || a2 - b);
|
|
341625
|
+
const mid = Math.max(1, Math.floor(indexes.length / 2));
|
|
341626
|
+
return {
|
|
341627
|
+
min: cloneVec3$1(bounds.min),
|
|
341628
|
+
max: cloneVec3$1(bounds.max),
|
|
341629
|
+
left: build(indexes.slice(0, mid)),
|
|
341630
|
+
right: build(indexes.slice(mid)),
|
|
341631
|
+
indexes: null
|
|
341632
|
+
};
|
|
341633
|
+
};
|
|
341634
|
+
this.root = entries.length > 0 ? build(entries.map((_2, index2) => index2)) : null;
|
|
341635
|
+
this.nodeCount = nodeCount;
|
|
341636
|
+
}
|
|
341637
|
+
overlapPairs(options = {}) {
|
|
341638
|
+
const padding = Math.max(0, options.padding ?? 0);
|
|
341639
|
+
const pairs = [];
|
|
341640
|
+
let nodePairChecks = 0;
|
|
341641
|
+
let leafPairChecks = 0;
|
|
341642
|
+
const addLeafPair = (sourceIndex, targetIndex) => {
|
|
341643
|
+
var _a3;
|
|
341644
|
+
const source = this.entries[sourceIndex];
|
|
341645
|
+
const target = this.entries[targetIndex];
|
|
341646
|
+
leafPairChecks += 1;
|
|
341647
|
+
if ((_a3 = options.skipPair) == null ? void 0 : _a3.call(options, source, target, sourceIndex, targetIndex)) return;
|
|
341648
|
+
if (!aabbOverlaps(source, target, padding)) return;
|
|
341649
|
+
pairs.push({ sourceIndex, targetIndex });
|
|
341650
|
+
};
|
|
341651
|
+
const between = (a2, b) => {
|
|
341652
|
+
nodePairChecks += 1;
|
|
341653
|
+
if (!aabbOverlaps(a2, b, padding)) return;
|
|
341654
|
+
if (a2.indexes && b.indexes) {
|
|
341655
|
+
for (const sourceIndex of a2.indexes) {
|
|
341656
|
+
for (const targetIndex of b.indexes) addLeafPair(Math.min(sourceIndex, targetIndex), Math.max(sourceIndex, targetIndex));
|
|
341657
|
+
}
|
|
341658
|
+
return;
|
|
341659
|
+
}
|
|
341660
|
+
if (!b.indexes && (a2.indexes || nodeVolume(a2) <= nodeVolume(b))) {
|
|
341661
|
+
between(a2, b.left);
|
|
341662
|
+
between(a2, b.right);
|
|
341663
|
+
return;
|
|
341664
|
+
}
|
|
341665
|
+
between(a2.left, b);
|
|
341666
|
+
between(a2.right, b);
|
|
341667
|
+
};
|
|
341668
|
+
const within = (node) => {
|
|
341669
|
+
if (node.indexes) {
|
|
341670
|
+
for (let i = 0; i < node.indexes.length; i += 1) {
|
|
341671
|
+
for (let j = i + 1; j < node.indexes.length; j += 1) addLeafPair(node.indexes[i], node.indexes[j]);
|
|
341672
|
+
}
|
|
341673
|
+
return;
|
|
341674
|
+
}
|
|
341675
|
+
within(node.left);
|
|
341676
|
+
within(node.right);
|
|
341677
|
+
between(node.left, node.right);
|
|
341678
|
+
};
|
|
341679
|
+
if (this.root) within(this.root);
|
|
341680
|
+
return {
|
|
341681
|
+
pairs,
|
|
341682
|
+
stats: {
|
|
341683
|
+
objectCount: this.entries.length,
|
|
341684
|
+
nodeCount: this.nodeCount,
|
|
341685
|
+
nodePairChecks,
|
|
341686
|
+
leafPairChecks
|
|
341687
|
+
}
|
|
341688
|
+
};
|
|
341689
|
+
}
|
|
341690
|
+
}
|
|
341691
|
+
const DEFAULT_COLLISION_INSPECTION_OPTIONS = {
|
|
341692
|
+
minOverlapVolume: 0.1
|
|
341693
|
+
};
|
|
341694
|
+
function cloneVec3(value) {
|
|
341695
|
+
return [value[0], value[1], value[2]];
|
|
341696
|
+
}
|
|
341697
|
+
function isIdentityTransform(matrix) {
|
|
341698
|
+
if (!matrix) return true;
|
|
341699
|
+
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
341700
|
+
return identity.every((value, index2) => Math.abs(matrix[index2] - value) <= 1e-12);
|
|
341701
|
+
}
|
|
341702
|
+
function transformPoint(matrix, point2) {
|
|
341703
|
+
const [x2, y2, z2] = point2;
|
|
341704
|
+
return [
|
|
341705
|
+
matrix[0] * x2 + matrix[4] * y2 + matrix[8] * z2 + matrix[12],
|
|
341706
|
+
matrix[1] * x2 + matrix[5] * y2 + matrix[9] * z2 + matrix[13],
|
|
341707
|
+
matrix[2] * x2 + matrix[6] * y2 + matrix[10] * z2 + matrix[14]
|
|
341708
|
+
];
|
|
341709
|
+
}
|
|
341710
|
+
function transformBBox(min2, max2, matrix) {
|
|
341711
|
+
const corners = [
|
|
341712
|
+
[min2[0], min2[1], min2[2]],
|
|
341713
|
+
[min2[0], min2[1], max2[2]],
|
|
341714
|
+
[min2[0], max2[1], min2[2]],
|
|
341715
|
+
[min2[0], max2[1], max2[2]],
|
|
341716
|
+
[max2[0], min2[1], min2[2]],
|
|
341717
|
+
[max2[0], min2[1], max2[2]],
|
|
341718
|
+
[max2[0], max2[1], min2[2]],
|
|
341719
|
+
[max2[0], max2[1], max2[2]]
|
|
341720
|
+
];
|
|
341721
|
+
const outMin = [Infinity, Infinity, Infinity];
|
|
341722
|
+
const outMax = [-Infinity, -Infinity, -Infinity];
|
|
341723
|
+
for (const corner of corners) {
|
|
341724
|
+
const transformed = transformPoint(matrix, corner);
|
|
341725
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
341726
|
+
outMin[axis] = Math.min(outMin[axis], transformed[axis]);
|
|
341727
|
+
outMax[axis] = Math.max(outMax[axis], transformed[axis]);
|
|
341728
|
+
}
|
|
341729
|
+
}
|
|
341730
|
+
return { min: outMin, max: outMax };
|
|
341731
|
+
}
|
|
341732
|
+
function prepareEntry(entry) {
|
|
341733
|
+
if (isIdentityTransform(entry.transform)) {
|
|
341734
|
+
return {
|
|
341735
|
+
...entry,
|
|
341736
|
+
min: cloneVec3(entry.min),
|
|
341737
|
+
max: cloneVec3(entry.max)
|
|
341738
|
+
};
|
|
341739
|
+
}
|
|
341740
|
+
const bbox = transformBBox(entry.min, entry.max, entry.transform);
|
|
341741
|
+
return {
|
|
341742
|
+
...entry,
|
|
341743
|
+
shape: entry.shape.transform(entry.transform),
|
|
341744
|
+
min: bbox.min,
|
|
341745
|
+
max: bbox.max
|
|
341746
|
+
};
|
|
341747
|
+
}
|
|
341748
|
+
function collisionId(a2, b) {
|
|
341749
|
+
return `${a2.id}__${b.id}`;
|
|
341750
|
+
}
|
|
341751
|
+
function shouldSkipPair(a2, b, options) {
|
|
341752
|
+
if (options.skipIntraGroup && a2.groupName && a2.groupName === b.groupName) return true;
|
|
341753
|
+
if (options.skipMockPairs && a2.mock && b.mock) return true;
|
|
341754
|
+
return false;
|
|
341755
|
+
}
|
|
341756
|
+
function collectCandidatePairs(entries, options) {
|
|
341757
|
+
const index2 = new AabbSpatialIndex(entries, 1);
|
|
341758
|
+
const result = index2.overlapPairs({
|
|
341759
|
+
padding: options.bboxPadding,
|
|
341760
|
+
skipPair: (a2, b) => shouldSkipPair(a2, b, options)
|
|
341761
|
+
});
|
|
341762
|
+
const pairs = result.pairs.map((pair) => ({
|
|
341763
|
+
...pair,
|
|
341764
|
+
bboxOverlapVolume: aabbOverlapVolume(entries[pair.sourceIndex], entries[pair.targetIndex])
|
|
341765
|
+
}));
|
|
341766
|
+
pairs.sort((a2, b) => b.bboxOverlapVolume - a2.bboxOverlapVolume || a2.sourceIndex - b.sourceIndex || a2.targetIndex - b.targetIndex);
|
|
341767
|
+
return { pairs, bboxPairChecks: result.stats.leafPairChecks };
|
|
341768
|
+
}
|
|
341769
|
+
function serializeCollisionFinding(finding) {
|
|
341770
|
+
return {
|
|
341771
|
+
index: finding.index,
|
|
341772
|
+
id: finding.id,
|
|
341773
|
+
sourceIndex: finding.sourceIndex,
|
|
341774
|
+
targetIndex: finding.targetIndex,
|
|
341775
|
+
sourceId: finding.sourceId,
|
|
341776
|
+
targetId: finding.targetId,
|
|
341777
|
+
sourceName: finding.sourceName,
|
|
341778
|
+
targetName: finding.targetName,
|
|
341779
|
+
overlapVolume: finding.overlapVolume
|
|
341780
|
+
};
|
|
341781
|
+
}
|
|
341782
|
+
function analyzeCollisionIntersections(entries, rawOptions = {}) {
|
|
341783
|
+
const maxCandidatePairs = rawOptions.maxCandidatePairs === void 0 ? null : Math.max(0, Math.trunc(rawOptions.maxCandidatePairs));
|
|
341784
|
+
const maxElapsedMs = rawOptions.maxElapsedMs === void 0 ? null : Math.max(0, rawOptions.maxElapsedMs);
|
|
341785
|
+
const options = {
|
|
341786
|
+
minOverlapVolume: rawOptions.minOverlapVolume ?? DEFAULT_COLLISION_INSPECTION_OPTIONS.minOverlapVolume,
|
|
341787
|
+
maxCandidatePairs,
|
|
341788
|
+
maxElapsedMs,
|
|
341789
|
+
bboxPadding: Math.max(0, rawOptions.bboxPadding ?? 0),
|
|
341790
|
+
skipIntraGroup: rawOptions.skipIntraGroup === true,
|
|
341791
|
+
skipMockPairs: rawOptions.skipMockPairs === true,
|
|
341792
|
+
includeBBoxCandidates: rawOptions.includeBBoxCandidates !== false
|
|
341793
|
+
};
|
|
341794
|
+
const warnings = [];
|
|
341795
|
+
const collisions = [];
|
|
341796
|
+
const preparedEntries = entries.map((entry) => prepareEntry(entry));
|
|
341797
|
+
const { pairs: candidatePairs, bboxPairChecks } = collectCandidatePairs(preparedEntries, options);
|
|
341798
|
+
const limitedCandidatePairs = options.maxCandidatePairs === null ? candidatePairs : candidatePairs.slice(0, options.maxCandidatePairs);
|
|
341799
|
+
const pairLimitSkippedPairCount = candidatePairs.length - limitedCandidatePairs.length;
|
|
341800
|
+
let testedPairCount = 0;
|
|
341801
|
+
let timeBudgetStopped = false;
|
|
341802
|
+
const exactStarted = performance.now();
|
|
341803
|
+
for (const pair of limitedCandidatePairs) {
|
|
341804
|
+
if (options.maxElapsedMs !== null && (options.maxElapsedMs <= 0 || performance.now() - exactStarted >= options.maxElapsedMs)) {
|
|
341805
|
+
timeBudgetStopped = true;
|
|
341806
|
+
break;
|
|
341807
|
+
}
|
|
341808
|
+
testedPairCount += 1;
|
|
341809
|
+
const a2 = preparedEntries[pair.sourceIndex];
|
|
341810
|
+
const b = preparedEntries[pair.targetIndex];
|
|
341811
|
+
try {
|
|
341812
|
+
const hit = a2.shape.intersect(b.shape);
|
|
341813
|
+
if (hit.isEmpty()) continue;
|
|
341814
|
+
const overlapVolume = hit.volume();
|
|
341815
|
+
if (!Number.isFinite(overlapVolume) || overlapVolume <= options.minOverlapVolume) continue;
|
|
341816
|
+
collisions.push({
|
|
341817
|
+
index: collisions.length + 1,
|
|
341818
|
+
id: collisionId(a2, b),
|
|
341819
|
+
sourceIndex: pair.sourceIndex,
|
|
341820
|
+
targetIndex: pair.targetIndex,
|
|
341821
|
+
sourceId: a2.id,
|
|
341822
|
+
targetId: b.id,
|
|
341823
|
+
sourceName: a2.name,
|
|
341824
|
+
targetName: b.name,
|
|
341825
|
+
overlapVolume,
|
|
341826
|
+
shape: hit
|
|
341827
|
+
});
|
|
341828
|
+
} catch (err2) {
|
|
341829
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
341830
|
+
warnings.push(`Could not boolean-test ${a2.name} against ${b.name}: ${message}`);
|
|
341831
|
+
}
|
|
341832
|
+
}
|
|
341833
|
+
const exactCheckMs = performance.now() - exactStarted;
|
|
341834
|
+
const timeBudgetSkippedPairCount = timeBudgetStopped ? limitedCandidatePairs.length - testedPairCount : 0;
|
|
341835
|
+
const skippedPairCount = pairLimitSkippedPairCount + timeBudgetSkippedPairCount;
|
|
341836
|
+
const bboxCandidates = options.includeBBoxCandidates ? candidatePairs.map((pair, index2) => {
|
|
341837
|
+
const source = preparedEntries[pair.sourceIndex];
|
|
341838
|
+
const target = preparedEntries[pair.targetIndex];
|
|
341839
|
+
const exactChecked = index2 < testedPairCount;
|
|
341840
|
+
let skippedBy;
|
|
341841
|
+
if (!exactChecked) {
|
|
341842
|
+
skippedBy = index2 >= limitedCandidatePairs.length ? "pair-limit" : "time-budget";
|
|
341843
|
+
}
|
|
341844
|
+
return {
|
|
341845
|
+
index: index2 + 1,
|
|
341846
|
+
sourceIndex: pair.sourceIndex,
|
|
341847
|
+
targetIndex: pair.targetIndex,
|
|
341848
|
+
sourceId: source.id,
|
|
341849
|
+
targetId: target.id,
|
|
341850
|
+
sourceName: source.name,
|
|
341851
|
+
targetName: target.name,
|
|
341852
|
+
bboxOverlapVolume: pair.bboxOverlapVolume,
|
|
341853
|
+
exactChecked,
|
|
341854
|
+
...skippedBy ? { skippedBy } : {}
|
|
341855
|
+
};
|
|
341856
|
+
}) : [];
|
|
341857
|
+
if (pairLimitSkippedPairCount > 0) {
|
|
341858
|
+
warnings.push(
|
|
341859
|
+
`Collision check candidate set was limited to ${limitedCandidatePairs.length}/${candidatePairs.length} bbox-overlapping object pair(s); ${pairLimitSkippedPairCount} lower-priority pair(s) were skipped by maxCandidatePairs=${options.maxCandidatePairs}. Increase the collision pair limit for exhaustive checking.`
|
|
341860
|
+
);
|
|
341861
|
+
}
|
|
341862
|
+
if (timeBudgetSkippedPairCount > 0) {
|
|
341863
|
+
warnings.push(
|
|
341864
|
+
`Collision check stopped after ${exactCheckMs.toFixed(0)} ms; tested ${testedPairCount}/${limitedCandidatePairs.length} selected bbox-overlapping object pair(s), skipping ${timeBudgetSkippedPairCount} by maxElapsedMs=${options.maxElapsedMs}. Increase the collision time budget for a deeper check.`
|
|
341865
|
+
);
|
|
341866
|
+
}
|
|
341867
|
+
const objects = preparedEntries.map((entry, index2) => ({
|
|
341868
|
+
index: index2,
|
|
341869
|
+
id: entry.id,
|
|
341870
|
+
name: entry.name,
|
|
341871
|
+
groupName: entry.groupName,
|
|
341872
|
+
treePath: entry.treePath,
|
|
341873
|
+
mock: entry.mock === true,
|
|
341874
|
+
bbox: {
|
|
341875
|
+
min: cloneVec3(entry.min),
|
|
341876
|
+
max: cloneVec3(entry.max)
|
|
341877
|
+
}
|
|
341878
|
+
}));
|
|
341879
|
+
return {
|
|
341880
|
+
method: "boolean-intersection",
|
|
341881
|
+
options,
|
|
341882
|
+
broadphase: {
|
|
341883
|
+
method: "aabb-bvh",
|
|
341884
|
+
allPairCount: preparedEntries.length * (preparedEntries.length - 1) / 2,
|
|
341885
|
+
bboxPairChecks,
|
|
341886
|
+
booleanPairChecks: testedPairCount
|
|
341887
|
+
},
|
|
341888
|
+
objectCount: objects.length,
|
|
341889
|
+
candidatePairCount: candidatePairs.length,
|
|
341890
|
+
testedPairCount,
|
|
341891
|
+
skippedPairCount,
|
|
341892
|
+
pairLimitSkippedPairCount,
|
|
341893
|
+
timeBudgetSkippedPairCount,
|
|
341894
|
+
exactCheckMs,
|
|
341895
|
+
collisionCount: collisions.length,
|
|
341896
|
+
bboxCandidates,
|
|
341897
|
+
objects,
|
|
341898
|
+
collisions,
|
|
341899
|
+
warnings
|
|
341900
|
+
};
|
|
341901
|
+
}
|
|
338949
341902
|
const VECTOR_KEYS = /* @__PURE__ */ new Set(["pos", "position", "target", "lookat", "aim", "up"]);
|
|
338950
341903
|
const roundNumber = (value, digits) => {
|
|
338951
341904
|
const scale2 = 10 ** digits;
|
|
@@ -339212,64 +342165,72 @@ export {
|
|
|
339212
342165
|
DEFAULT_ACTIVE_BACKEND as ay,
|
|
339213
342166
|
isConstraintSketch as az,
|
|
339214
342167
|
PCFShadowMap as b,
|
|
342168
|
+
initKernelManifoldOnly as b$,
|
|
339215
342169
|
PointLight as b0,
|
|
339216
342170
|
DirectionalLight as b1,
|
|
339217
|
-
|
|
342171
|
+
analyzeCollisionIntersections as b2,
|
|
339218
342172
|
shapeToGeometry as b3,
|
|
339219
|
-
|
|
339220
|
-
|
|
339221
|
-
|
|
339222
|
-
|
|
339223
|
-
|
|
339224
|
-
|
|
339225
|
-
|
|
339226
|
-
|
|
339227
|
-
|
|
339228
|
-
|
|
339229
|
-
|
|
339230
|
-
|
|
339231
|
-
|
|
339232
|
-
|
|
339233
|
-
|
|
339234
|
-
|
|
339235
|
-
|
|
339236
|
-
|
|
339237
|
-
|
|
339238
|
-
|
|
339239
|
-
|
|
339240
|
-
|
|
339241
|
-
|
|
339242
|
-
|
|
339243
|
-
|
|
339244
|
-
|
|
339245
|
-
|
|
339246
|
-
|
|
339247
|
-
|
|
339248
|
-
|
|
339249
|
-
|
|
339250
|
-
|
|
339251
|
-
|
|
339252
|
-
|
|
339253
|
-
|
|
339254
|
-
|
|
339255
|
-
|
|
339256
|
-
|
|
339257
|
-
|
|
339258
|
-
|
|
339259
|
-
|
|
339260
|
-
|
|
339261
|
-
|
|
339262
|
-
|
|
339263
|
-
|
|
339264
|
-
|
|
339265
|
-
|
|
339266
|
-
|
|
339267
|
-
|
|
339268
|
-
|
|
339269
|
-
|
|
339270
|
-
|
|
339271
|
-
|
|
342173
|
+
buildShapeFromCompilePlan as b4,
|
|
342174
|
+
sketchToSvg as b5,
|
|
342175
|
+
sketchToDxf as b6,
|
|
342176
|
+
runScript as b7,
|
|
342177
|
+
MeshPhysicalMaterial as b8,
|
|
342178
|
+
LineSegments as b9,
|
|
342179
|
+
findJointAnimationClip as bA,
|
|
342180
|
+
resolveJointAnimation as bB,
|
|
342181
|
+
resolveJointViewValues as bC,
|
|
342182
|
+
getShapePorts as bD,
|
|
342183
|
+
getShapeUsedPorts as bE,
|
|
342184
|
+
DEFAULT_VIEW_CONFIG as bF,
|
|
342185
|
+
getKernelFaceNameForTriangle as bG,
|
|
342186
|
+
initKernel as bH,
|
|
342187
|
+
initSolverWasm as bI,
|
|
342188
|
+
BoxGeometry as bJ,
|
|
342189
|
+
localAabbPlaneRelation as bK,
|
|
342190
|
+
ShapeUtils as bL,
|
|
342191
|
+
aabbInteriorOverlaps as bM,
|
|
342192
|
+
aabbOverlapVolume as bN,
|
|
342193
|
+
AabbSpatialIndex as bO,
|
|
342194
|
+
aabbGaps as bP,
|
|
342195
|
+
Group as bQ,
|
|
342196
|
+
intersectWithPlane as bR,
|
|
342197
|
+
parseCameraCliSpec as bS,
|
|
342198
|
+
PMREMGenerator as bT,
|
|
342199
|
+
PointsMaterial as bU,
|
|
342200
|
+
Points$1 as bV,
|
|
342201
|
+
serializeCollisionFinding as bW,
|
|
342202
|
+
worldAuthorPlaneToLocal as bX,
|
|
342203
|
+
generateCuttingLayoutPdf as bY,
|
|
342204
|
+
getCameraForwardVector as bZ,
|
|
342205
|
+
RENDER_STYLE_OPTIONS as b_,
|
|
342206
|
+
getRenderStylePreset as ba,
|
|
342207
|
+
AdditiveBlending as bb,
|
|
342208
|
+
CatmullRomCurve3 as bc,
|
|
342209
|
+
TubeGeometry as bd,
|
|
342210
|
+
MeshStandardMaterial as be,
|
|
342211
|
+
compileSdfNode3 as bf,
|
|
342212
|
+
buildSdfRaymarchFragmentShader as bg,
|
|
342213
|
+
SDF_RAYMARCH_PROXY_VERTEX_SHADER as bh,
|
|
342214
|
+
Shape2 as bi,
|
|
342215
|
+
ShapeGeometry as bj,
|
|
342216
|
+
ShaderLib as bk,
|
|
342217
|
+
CylinderGeometry as bl,
|
|
342218
|
+
parseViewportCameraState as bm,
|
|
342219
|
+
createResolvedExplodeConfig as bn,
|
|
342220
|
+
explodeBoundsCenter as bo,
|
|
342221
|
+
explodeMergeBounds as bp,
|
|
342222
|
+
resolveExplodeDirective as bq,
|
|
342223
|
+
computeExplodeMotion as br,
|
|
342224
|
+
getSketchWorldMatrix as bs,
|
|
342225
|
+
explodeAdd as bt,
|
|
342226
|
+
hasExplodeOverride as bu,
|
|
342227
|
+
resolveExplodeLocalFanDirection as bv,
|
|
342228
|
+
explodeMul as bw,
|
|
342229
|
+
explodeLeafFanStage as bx,
|
|
342230
|
+
normalizeCutPlane as by,
|
|
342231
|
+
toClippingPlane as bz,
|
|
339272
342232
|
SRGBColorSpace as c,
|
|
342233
|
+
__viteBrowserExternal$1 as c0,
|
|
339273
342234
|
Layers as d,
|
|
339274
342235
|
Color as e,
|
|
339275
342236
|
RGBAFormat as f,
|