diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af80e7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Virtual environment — a legitimate artifact inside install_dir, never tracked. +venv/ +.venv/ + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +.coverage +htmlcov/ + +# Instance configuration — never committed. See docs/ENVIRONMENT.md section 13. +deploy/site.env + +# Generated output +/out/ +*.stl +*.step + +# Shell history from interactive sessions +.lesshst + +# Service user's HOME is install_dir, so caches land here +.cache/ +.local/ + +# Service user HOME is install_dir, so SSH keys land here +.ssh/ +.gitconfig diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9ce3e2f --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +# Entry points. Every target is safe to run more than once. + +PY ?= venv/bin/python +PIP ?= venv/bin/pip + +.PHONY: help venv deps deps-nocad toolchain verify-oracle regenerate-oracle test test-nocad clean + +help: + @echo "venv create the virtualenv (Python 3.11)" + @echo "deps install base + CAD requirements" + @echo "deps-nocad install base requirements only" + @echo "toolchain build the pinned reference toolchain image" + @echo "verify-oracle check the committed oracle is intact" + @echo "regenerate-oracle rebuild the oracle inside the pinned image" + @echo "test pytest -n auto" + @echo "test-nocad the separability check: suite must pass without CAD" + +venv: + test -d venv || python3 -m venv venv + $(PIP) install --upgrade pip + +deps: venv + $(PIP) install -r requirements-base.txt -r requirements-cad.txt + $(PIP) install -e . + +deps-nocad: venv + $(PIP) install -r requirements-base.txt + $(PIP) install -e . + +toolchain: + docker build -t mechcomp/reference-toolchain:8.0.0 tools/reference-toolchain + +verify-oracle: + python3 fixtures/strap-beam-8.0.0/make_fixtures.py --verify + +regenerate-oracle: toolchain + ./tools/reference-toolchain/verify.sh --full + +test: + $(PY) -m pytest -n auto + +test-nocad: + $(PY) -m pytest -n auto -k "not cad" + +clean: + rm -rf .pytest_cache src/*.egg-info + find . -name __pycache__ -type d -prune -exec rm -rf {} + diff --git a/README.md b/README.md index ca23b0b..f837ff7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,85 @@ -# mechanical-compiler +# Mechanical Compiler +Build the capacity to construct real structures from reclaimed and commodity +materials, using whatever fabrication is actually to hand — 3D printing, +tabletop CNC, welding, cement casting, COTS stock. + +The reference case is a faceted timber shell: planar panels meeting along +straight fold lines, converging on nodes, sitting on a platform. Buildable +without a factory, provided someone has worked out what the pieces are and how +they meet. **That last clause is the project.** The Mechanical Compiler exists +to make the pieces computable, qualifiable, and reproducible by someone who was +not present when they were designed. + +## Scope + +**In scope:** geometry, qualification, and reproducibility of structural +members and their interfaces. + +**Not in scope:** building codes, permitting, jurisdictional approval. +Qualification and compliance are different things. A qualification says *this +member is what it claims to be, made this way, from this stock*. Compliance is +a jurisdiction-specific argument someone else may build on top. + +The project records **physical claims, never verdicts** — section modulus, +moment of inertia, material provenance, process parameters. Those are what any +future argument would need. A pass/fail verdict would bake in a jurisdiction we +do not want to be bound to. Measure and attest; never adjudicate. + +## Repository layout + +``` +docs/ specification, state, failure log, roadmap +legacy/openscad/ rev 8.0.0 generators — reference, not a live target +fixtures/ frozen acceptance oracles +tools/reference-toolchain/ pinned OpenSCAD + BOSL2, build-time only +src/mechcomp/ the application +tests/ acceptance against the oracle +``` + +Read `docs/ROADMAP.md` first for what this is, then `docs/ENVIRONMENT.md` for +how an instance is built. If you are writing provisioning automation, read +`docs/FAILURES.md` **before** the specification — every entry is something a +script written from the specification alone would have got wrong. + +## Getting started + +```bash +make deps # virtualenv, base + CAD requirements +make verify-oracle # confirm the frozen oracle is intact +make test # pytest -n auto +``` + +`make verify-oracle` needs nothing but Python. It should pass on any machine at +any time; if it does not, stop. + +## The oracle + +`fixtures/strap-beam-8.0.0/` holds 123 frozen cases — 113 accepted, 10 +rejected — produced by OpenSCAD 2021.01 with BOSL2 at `92d697c2`. It is the +acceptance criterion for any reimplementation of the generators. + +**The ten rejected cases are part of the contract.** A port that accepts them is +wrong, however good its numbers look elsewhere. It is easy to reproduce the +geometry and quietly lose the constraint that made it trustworthy. + +The generators under `legacy/openscad/` are the reference implementation, kept +so the oracle can be regenerated. They are not a live target and the running +application has no OpenSCAD dependency. + +## Provenance + +**This project's documents, code and roadmap are LLM-generated under human +direction.** That is stated plainly here and in any downstream submission. We +do not obscure it. + +The maintainer reviews and owns every line. Anything that could not be defended +in a review thread does not ship. + +## Licence + +AGPL-3.0-or-later. See `LICENSE`. + +Section 13 obliges us to offer source to users interacting over a network, so +any deployed web tier carries a visible link back to this repository. That is a +licence obligation, not a courtesy. diff --git a/fixtures/strap-beam-8.0.0/README.md b/fixtures/strap-beam-8.0.0/README.md new file mode 100644 index 0000000..a94bdd0 --- /dev/null +++ b/fixtures/strap-beam-8.0.0/README.md @@ -0,0 +1,68 @@ +# Acceptance oracle — strap-beam revision 8.0.0 + +123 cases. 113 accepted, 10 rejected. `sha256 ddd0f154…` + +## What it is for + +This is the acceptance criterion for any reimplementation of the generators. A +port is correct when it reproduces these values — not when it looks right, and +not when it renders something plausible. + +## The rejected cases are part of the contract + +Ten cases were rejected by the reference, each with a message naming the +parameter and the limit. **A port that accepts them is wrong**, however good its +numbers are elsewhere. + +This is the part a naive reimplementation loses. Reproducing geometry is +straightforward; keeping the constraint that made the geometry trustworthy is +the actual work. `tests/test_oracle.py` asserts both directions. + +## What the fields mean + +| Key | Role | +|---|---| +| `MIN_WALL_ACTUAL_MM` | thinnest surviving material anywhere in the section | +| `SECTION_PARTS` | connected solids; must be 1 | +| `STRAP_CHANNELS` | separate strap slots; must equal the member count | +| `RING_*`, `FIN_*`, `AF_*`, `SPOKE_*` | solved placement values — these pin the solvers | + +The first three are invariants. The solved values are what catch a port that +reaches the right shape by a different route, which will diverge later. + +`tolerance` in the document governs comparison: lengths to 1e-4 mm, areas to +1e-3 mm², counts exact. + +## The hash + +`fixtures_sha256` covers the document **without that field**, serialised with +`indent=2` and `sort_keys=True`. Hashing the file will not reproduce it. + +```bash +python3 make_fixtures.py --verify +``` + +That needs nothing but Python and should pass anywhere, any time. + +## Regenerating + +Only inside the pinned toolchain, never on a host whose OpenSCAD merely looks +similar: + +```bash +./tools/reference-toolchain/verify.sh --full +``` + +The `frozen` date changes on every regeneration, so a one-line diff there is +expected. **Any other difference means the toolchain has drifted** — stop and +investigate. A drifting oracle is worse than no oracle. + +## Coverage + +Beyond the eleven default profiles: bundle counts 2 and 3; conditioned +intermediate stock at 13.4 mm and steel strap at 0.79 mm on every profile; +asymmetric walls as a chirality regression; parameter sweeps across corner +radius, corner web, leg angle, fin projection, fillet radius, bore size, +rotation, junction webs, triangle shape and rectangle aspect; and four +cross-profile isolation cases proving one profile's parameters cannot affect +another's. diff --git a/fixtures/strap-beam-8.0.0/make_fixtures.py b/fixtures/strap-beam-8.0.0/make_fixtures.py new file mode 100644 index 0000000..7a64c82 --- /dev/null +++ b/fixtures/strap-beam-8.0.0/make_fixtures.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +""" +Regenerate the frozen rev-8.0.0 acceptance oracle. + +WHAT THIS IS +------------ +The oracle is the acceptance criterion for any reimplementation of the rev-8 +generators. A port is correct when it reproduces these values -- not when it +looks right. The ten rejected cases are part of the contract: a port that +accepts them is wrong. + +WHY IT IS PINNED +---------------- +Every case here was produced by OpenSCAD 2021.01 with BOSL2 at commit +92d697c2856de2fed93a33e858068589cefc2898. Those versions live in a container +image (tools/reference-toolchain) and nowhere else. The running application has +no OpenSCAD dependency at all, so the host's OpenSCAD version cannot drift into +this file. + +HOW THE HASH WORKS +------------------ +`fixtures_sha256` covers the document WITHOUT that field, serialised with +indent=2 and sort_keys=True. Hashing the file itself will not reproduce it. +`--verify` performs the correct comparison. + +USAGE +----- + python3 make_fixtures.py --verify # check the committed oracle + python3 make_fixtures.py --regenerate # rebuild it (needs OpenSCAD + BOSL2) + +Run --regenerate inside the reference toolchain image, never on a host whose +OpenSCAD version is merely similar. +""" + +from __future__ import annotations + +import argparse +import datetime +import hashlib +import json +import subprocess +import sys +from pathlib import Path + +REVISION = "8.0.0" +OPENSCAD_VERSION = "2021.01" +BOSL2_COMMIT = "92d697c2856de2fed93a33e858068589cefc2898" + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[1] +SCAD_DIR = REPO / "legacy" / "openscad" +OUT = HERE / f"strap-beam-fixtures-{REVISION}.json" + +GEN_3X = "strap-beam-3x.scad" +GEN_4X = "strap-beam-4x.scad" + +P3 = ["Equilateral Triangle", "General Triangle", "A Frame", "Y", "T", "Three-Fin"] +P4 = ["Square", "Rectangle", "Diamond", "Cross", "Four-Fin"] + +# Report keys carrying numeric values. Everything else stays a string. +NUM = set( + """STRAP_WIDTH_MM STRAP_THICK_MM BUNDLE_COUNT BUNDLE_THICK_MM CLEARANCE_MM + WALL_INSIDE_MM WALL_OUTSIDE_MM WALL_EDGE_MM MIN_WALL_SPEC_MM MIN_WALL_ACTUAL_MM + SECTION_AREA_MM2 SECTION_PARTS STRAP_CHANNELS ENVELOPE_X_MM ENVELOPE_Y_MM + LENGTH_MM VOLUME_MM3 MASS_G LENGTH_FT RING_SCALE RING_CORNER_WEB_MM + RING_CORNER_R_MAX_MM SPOKE_RADIUS_MM SPOKE_WEB_MM FIN_CORE_SIDE_MM + FIN_PROJECTION_MM FIN_SETBACK_MM FIN_BORE_SIDE_MM FIN_JUNCTION_WEB_MM + AF_APEX_SETBACK_MM AF_CROSSBAR_DEPTH_MM AF_APEX_WEB_MM AF_CROSSBAR_WEB_MM + T_FLANGE_SPAN_MM""".split() +) + + +# --------------------------------------------------------------------------- +# Case matrix +# --------------------------------------------------------------------------- + +def build_cases() -> list[dict]: + cases: list[dict] = [] + + def case(gen: str, profile: str, label: str, defs: list[str]) -> None: + cases.append({"generator": gen, "profile": profile, "label": label, "defs": defs}) + + for p in P3: + case(GEN_3X, p, "default", []) + for p in P4: + case(GEN_4X, p, "default", []) + + # A bundle is a stack of laminae, not a thicker strap. + for p in P3: + for b in (2, 3): + case(GEN_3X, p, f"bundle{b}", [f"-Dbundle_count={b}"]) + for p in P4: + for b in (2, 3): + case(GEN_4X, p, f"bundle{b}", [f"-Dbundle_count={b}"]) + + # Conditioned intermediate stock (Utility Two) is a first-class input, not + # an edge case. Steel strap thickness likewise. + for p in P3 + P4: + g = GEN_3X if p in P3 else GEN_4X + case(g, p, "width13.4", ["-Dstrap_width_mm=13.4"]) + case(g, p, "steel0.79", ["-Dstrap_thickness_mm=0.79"]) + + # The chirality regression. + for p in P3 + P4: + g = GEN_3X if p in P3 else GEN_4X + case(g, p, "wall_asym", [ + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8", + ]) + + # Profile ranges, including boundaries that MUST be rejected. + for a in (20, 30, 45, 53, 55, 70): + case(GEN_3X, "A Frame", f"leg{a}", [f"-Da_frame_leg_angle_deg={a}"]) + for r in (0, 1, 2, 2.8, 2.9, 5): + case(GEN_3X, "Equilateral Triangle", f"cornerR{r}", [f"-Dring_corner_radius_mm={r}"]) + for w in (0.4, 1.2, 2.0, 4.0): + case(GEN_3X, "Equilateral Triangle", f"web{w}", + [f"-Dring_corner_web_mm={w}", f"-Dmin_wall_mm={min(w, 1.2)}"]) + for f in (0, 2, 6.25, 12, 25): + case(GEN_3X, "Three-Fin", f"fin{f}", [f"-Dthree_fin_fin_mm={f}"]) + for r in (0, 1, 2, 8): + case(GEN_3X, "Three-Fin", f"fillet{r}", [f"-Dthree_fin_junction_round_mm={r}"]) + for b in (1, 7.5, 15, 30): + case(GEN_3X, "Three-Fin", f"bore{b}", [f"-Dthree_fin_bore_side_mm={b}"]) + for a in (0, 17, 33, 90): + case(GEN_3X, "Three-Fin", f"rot{a}", [f"-Dthree_fin_rotation_deg={a}"]) + for w in (0, 0.5, 1.2, 4.0): + case(GEN_3X, "Y", f"ywebb{w}", + [f"-Dy_junction_web_mm={w}", f"-Dmin_wall_mm={max(0.01, min(w, 1.2))}"]) + for w in (0, 0.25, 1.2, 3.0): + case(GEN_3X, "T", f"tweb{w}", + [f"-Dt_stem_web_mm={w}", f"-Dmin_wall_mm={max(0.01, min(w, 1.2))}"]) + for h in (4, 8, 15.5, 30): + case(GEN_3X, "General Triangle", f"h{h}", [f"-Dgeneral_triangle_height={h}"]) + for a in (1.0, 1.6, 2.5, 4.0): + case(GEN_4X, "Rectangle", f"aspect{a}", [f"-Drectangle_aspect={a}"]) + for f in (0, 2, 6.25, 15): + case(GEN_4X, "Four-Fin", f"fin{f}", [f"-Dfour_fin_fin_mm={f}"]) + + # Isolation: a foreign parameter must never affect this profile. + case(GEN_3X, "Y", "isolate_aframe", ["-Da_frame_leg_angle_deg=89"]) + case(GEN_3X, "Y", "isolate_threefin", ["-Dthree_fin_fin_mm=0"]) + case(GEN_3X, "T", "isolate_gentri", ["-Dgeneral_triangle_height=0.001"]) + case(GEN_3X, "Equilateral Triangle", "isolate_t", ["-Dt_stem_web_mm=0"]) + + return cases + + +# --------------------------------------------------------------------------- +# Execution +# --------------------------------------------------------------------------- + +def run_case(generator: str, defs: list[str]) -> tuple[dict, str | None]: + """Run one generator invocation; return (report, rejection_message).""" + cmd = ["openscad", "-o", "/dev/null", "--export-format=asciistl", *defs, generator] + proc = subprocess.run(cmd, cwd=SCAD_DIR, capture_output=True, text=True, timeout=900) + text = proc.stdout + proc.stderr + + report: dict = {} + rejection: str | None = None + for line in text.splitlines(): + if line.startswith('ECHO: "SB_') and "=" in line: + key, _, value = line[len('ECHO: "SB_'):].rstrip('"').partition("=") + if key == "END": + continue + if key in NUM: + try: + value = float(value) + except ValueError: + pass + report[key] = value + elif "[strap-beam]" in line: + rejection = line.split("] ", 1)[-1].strip() + return report, rejection + + +def regenerate() -> int: + cases = build_cases() + results = [] + for i, c in enumerate(cases, 1): + defs = [f'-Dprofile_type="{c["profile"]}"'] + c["defs"] + report, rejection = run_case(c["generator"], defs) + results.append({ + **c, + "defs": defs, + "outcome": "ok" if report else "rejected", + "report": report or None, + "rejection": rejection, + }) + print(f" [{i:3d}/{len(cases)}] {c['profile']:22s} {c['label']:16s} " + f"{'ok' if report else 'REJECTED'}", file=sys.stderr) + + accepted = sum(1 for r in results if r["outcome"] == "ok") + doc = { + "schema": "strap-beam.fixtures/1", + "generator_revision": REVISION, + "frozen": datetime.date.today().isoformat(), + "toolchain": {"openscad": OPENSCAD_VERSION, "bosl2_commit": BOSL2_COMMIT}, + "note": ( + "Acceptance oracle for any reimplementation of the rev-8 generators. " + "MIN_WALL_ACTUAL_MM, SECTION_PARTS and STRAP_CHANNELS are the invariants; " + "the solved values (RING_*, FIN_*, AF_*, SPOKE_*) pin the placement solvers. " + "Rejected cases are part of the contract: a port that accepts them is wrong." + ), + "tolerance": {"lengths_mm": 1e-4, "areas_mm2": 1e-3, "note": "counts are exact"}, + "summary": {"cases": len(results), "accepted": accepted, + "rejected": len(results) - accepted}, + "cases": results, + } + doc["fixtures_sha256"] = hashlib.sha256( + json.dumps(doc, indent=2, sort_keys=True).encode() + ).hexdigest() + OUT.write_text(json.dumps(doc, indent=2, sort_keys=True)) + + print(f"\n{len(results)} cases: {accepted} accepted, {len(results) - accepted} rejected") + print("sha256:", doc["fixtures_sha256"]) + return 0 + + +def verify() -> int: + if not OUT.exists(): + print(f"MISSING: {OUT}", file=sys.stderr) + return 2 + doc = json.loads(OUT.read_text()) + recorded = doc.pop("fixtures_sha256", None) + actual = hashlib.sha256(json.dumps(doc, indent=2, sort_keys=True).encode()).hexdigest() + + print(f"file: {OUT.name}") + print(f"revision: {doc.get('generator_revision')}") + print(f"toolchain: openscad {doc['toolchain']['openscad']}, " + f"bosl2 {doc['toolchain']['bosl2_commit'][:10]}") + print(f"summary: {doc['summary']}") + print(f"recorded: {recorded}") + print(f"computed: {actual}") + + if recorded == actual: + print("\nOK - oracle is intact.") + return 0 + print("\nFAIL - the oracle has been modified. A drifting oracle is worse " + "than no oracle. Investigate before proceeding.", file=sys.stderr) + return 1 + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + g = ap.add_mutually_exclusive_group(required=True) + g.add_argument("--verify", action="store_true", help="check the committed oracle") + g.add_argument("--regenerate", action="store_true", + help="rebuild it; requires OpenSCAD and BOSL2") + args = ap.parse_args() + return regenerate() if args.regenerate else verify() + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/fixtures/strap-beam-8.0.0/strap-beam-fixtures-8.0.0.json b/fixtures/strap-beam-8.0.0/strap-beam-fixtures-8.0.0.json new file mode 100644 index 0000000..9c47dc3 --- /dev/null +++ b/fixtures/strap-beam-8.0.0/strap-beam-fixtures-8.0.0.json @@ -0,0 +1,4794 @@ +{ + "cases": [ + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"" + ], + "generator": "strap-beam-3x.scad", + "label": "default", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 23.4931, + "ENVELOPE_Y_MM": 20.8788, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.8516, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.87663, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.5209, 20.5209, 20.5209]", + "RING_SCALE": 1.29266, + "SECTION_AREA_MM2": 152.029, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15202.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"General Triangle\"" + ], + "generator": "strap-beam-3x.scad", + "label": "default", + "outcome": "ok", + "profile": "General Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 23.3733, + "ENVELOPE_Y_MM": 21.1587, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 19.0213, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "General Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.68089, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.3925, 19.9401, 21.654]", + "RING_SCALE": 1.16529, + "SECTION_AREA_MM2": 153.398, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15339.8, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"" + ], + "generator": "strap-beam-3x.scad", + "label": "default", + "outcome": "ok", + "profile": "A Frame", + "rejection": null, + "report": { + "AF_APEX_SETBACK_MM": 1.35253, + "AF_APEX_WEB_MM": 1.2, + "AF_CROSSBAR_DEPTH_MM": 11.1013, + "AF_CROSSBAR_WEB_MM": 1.2, + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 30.4303, + "ENVELOPE_Y_MM": 15.7936, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.1457, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "A Frame", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 138.272, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13827.2, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"" + ], + "generator": "strap-beam-3x.scad", + "label": "default", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 33.8488, + "ENVELOPE_Y_MM": 29.3139, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.8112, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 135.574, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.1713, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13557.4, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"T\"" + ], + "generator": "strap-beam-3x.scad", + "label": "default", + "outcome": "ok", + "profile": "T", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.35, + "ENVELOPE_Y_MM": 20.983, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.7693, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "T", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 135.236, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "T_FLANGE_SPAN_MM": 36.35, + "T_NOTE": "inside_wall unused: no enclosed bore", + "VOLUME_MM3": 13523.6, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"" + ], + "generator": "strap-beam-3x.scad", + "label": "default", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.4285, + "ENVELOPE_Y_MM": 26.648, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 2.29919, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.77783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.2016, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 146.787, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14678.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Square\"" + ], + "generator": "strap-beam-4x.scad", + "label": "default", + "outcome": "ok", + "profile": "Square", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 22.4881, + "ENVELOPE_Y_MM": 22.4881, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 23.897, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Square", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.01368, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[19.0801, 19.0801, 19.0801, 19.0801]", + "RING_SCALE": 19.0801, + "SECTION_AREA_MM2": 192.717, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 19271.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Rectangle\"" + ], + "generator": "strap-beam-4x.scad", + "label": "default", + "outcome": "ok", + "profile": "Rectangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 28.808, + "ENVELOPE_Y_MM": 19.283, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 26.5298, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Rectangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 1.41116, + "RING_CORNER_WEB_MM": 4.0085, + "RING_EDGES_MM": "[25.4, 15.875, 25.4, 15.875]", + "RING_SCALE": 15.875, + "SECTION_AREA_MM2": 213.95, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 21395.0, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Diamond\"" + ], + "generator": "strap-beam-4x.scad", + "label": "default", + "outcome": "ok", + "profile": "Diamond", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 30.761, + "ENVELOPE_Y_MM": 30.761, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 23.897, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Diamond", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.01368, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[19.0801, 19.0801, 19.0801, 19.0801]", + "RING_SCALE": 19.0801, + "SECTION_AREA_MM2": 192.717, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 19271.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Cross\"" + ], + "generator": "strap-beam-4x.scad", + "label": "default", + "outcome": "ok", + "profile": "Cross", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 37.8551, + "ENVELOPE_Y_MM": 37.8551, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 22.6098, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Cross", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 182.337, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.54003, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 18233.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Four-Fin\"" + ], + "generator": "strap-beam-4x.scad", + "label": "default", + "outcome": "ok", + "profile": "Four-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.979, + "ENVELOPE_Y_MM": 26.979, + "FAMILY": "4x", + "FIN_BORE_SIDE_MM": 8.171, + "FIN_CORE_SIDE_MM": 11.579, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 1.954, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 21.9523, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Four-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 177.035, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 17703.5, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 25.2529, + "ENVELOPE_Y_MM": 22.4028, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 20.9168, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.38463, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[21.4008, 21.4008, 21.4008]", + "RING_SCALE": 1.34808, + "SECTION_AREA_MM2": 168.684, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 16868.4, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 27.0126, + "ENVELOPE_Y_MM": 23.9268, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 23.3146, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.89263, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[22.2807, 22.2807, 22.2807]", + "RING_SCALE": 1.40351, + "SECTION_AREA_MM2": 188.021, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 18802.1, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"General Triangle\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "General Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 25.0501, + "ENVELOPE_Y_MM": 22.6438, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 21.0198, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "General Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.15552, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[21.1976, 20.7273, 22.5089]", + "RING_SCALE": 1.21129, + "SECTION_AREA_MM2": 169.514, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 16951.4, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"General Triangle\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "General Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.7267, + "ENVELOPE_Y_MM": 24.1289, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 23.3264, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "General Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.63011, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[22.0025, 21.5144, 23.3637]", + "RING_SCALE": 1.25729, + "SECTION_AREA_MM2": 188.116, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 18811.6, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "A Frame", + "rejection": null, + "report": { + "AF_APEX_SETBACK_MM": 1.60653, + "AF_APEX_WEB_MM": 1.2, + "AF_CROSSBAR_DEPTH_MM": 11.7145, + "AF_CROSSBAR_WEB_MM": 1.2, + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 32.8831, + "ENVELOPE_Y_MM": 16.3324, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.4225, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "A Frame", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 148.568, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14856.8, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "A Frame", + "rejection": null, + "report": { + "AF_APEX_SETBACK_MM": 1.86053, + "AF_APEX_WEB_MM": 1.2, + "AF_CROSSBAR_DEPTH_MM": 12.3277, + "AF_CROSSBAR_WEB_MM": 1.2, + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 35.336, + "ENVELOPE_Y_MM": 16.8712, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 20.1855, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "A Frame", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 162.786, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 16278.6, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 34.3568, + "ENVELOPE_Y_MM": 29.7539, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.2376, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 139.013, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.31795, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13901.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 34.8648, + "ENVELOPE_Y_MM": 30.1938, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.692, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 142.677, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.4646, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14267.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"T\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "T", + "rejection": null, + "report": { + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.35, + "ENVELOPE_Y_MM": 21.491, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.0716, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "T", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 137.675, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "T_FLANGE_SPAN_MM": 36.35, + "T_NOTE": "inside_wall unused: no enclosed bore", + "VOLUME_MM3": 13767.5, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"T\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "T", + "rejection": null, + "report": { + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.35, + "ENVELOPE_Y_MM": 21.999, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.374, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "T", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 140.113, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "T_FLANGE_SPAN_MM": 36.35, + "T_NOTE": "inside_wall unused: no enclosed bore", + "VOLUME_MM3": 14011.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 27.5284, + "ENVELOPE_Y_MM": 27.664, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 14.2827, + "FIN_JUNCTION_WEB_MM": 2.68019, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 4.65771, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 19.5777, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 157.885, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15788.5, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-3x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 28.6282, + "ENVELOPE_Y_MM": 28.68, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 15.1626, + "FIN_JUNCTION_WEB_MM": 3.06119, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 5.53759, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 21.1756, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 170.771, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 17077.1, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Square\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-4x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "Square", + "rejection": null, + "report": { + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 23.5041, + "ENVELOPE_Y_MM": 23.5041, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 25.5653, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Square", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.52168, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[19.5881, 19.5881, 19.5881, 19.5881]", + "RING_SCALE": 19.5881, + "SECTION_AREA_MM2": 206.171, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 20617.1, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Square\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-4x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "Square", + "rejection": null, + "report": { + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 24.5201, + "ENVELOPE_Y_MM": 24.5201, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 27.4895, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Square", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 4.02968, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.0961, 20.0961, 20.0961, 20.0961]", + "RING_SCALE": 20.0961, + "SECTION_AREA_MM2": 221.69, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 22169.0, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Rectangle\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-4x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "Rectangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 29.316, + "ENVELOPE_Y_MM": 19.791, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 27.6036, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Rectangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 1.66516, + "RING_CORNER_WEB_MM": 3.7545, + "RING_EDGES_MM": "[25.4, 15.875, 25.4, 15.875]", + "RING_SCALE": 15.875, + "SECTION_AREA_MM2": 222.61, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 22261.0, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Rectangle\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-4x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "Rectangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 29.824, + "ENVELOPE_Y_MM": 20.299, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 28.6778, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Rectangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 1.91916, + "RING_CORNER_WEB_MM": 3.5005, + "RING_EDGES_MM": "[25.4, 15.875, 25.4, 15.875]", + "RING_SCALE": 15.875, + "SECTION_AREA_MM2": 231.273, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 23127.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Diamond\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-4x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "Diamond", + "rejection": null, + "report": { + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 32.1979, + "ENVELOPE_Y_MM": 32.1979, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 25.5653, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Diamond", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.52168, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[19.5881, 19.5881, 19.5881, 19.5881]", + "RING_SCALE": 19.5881, + "SECTION_AREA_MM2": 206.171, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 20617.1, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Diamond\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-4x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "Diamond", + "rejection": null, + "report": { + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 33.6347, + "ENVELOPE_Y_MM": 33.6347, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 27.4895, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Diamond", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 4.02968, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.0961, 20.0961, 20.0961, 20.0961]", + "RING_SCALE": 20.0961, + "SECTION_AREA_MM2": 221.69, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 22169.0, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Cross\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-4x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "Cross", + "rejection": null, + "report": { + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 38.3631, + "ENVELOPE_Y_MM": 38.3631, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 23.2849, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Cross", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 187.782, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.79403, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 18778.2, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Cross\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-4x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "Cross", + "rejection": null, + "report": { + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 38.8711, + "ENVELOPE_Y_MM": 38.8711, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 24.0241, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Cross", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 193.743, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 10.048, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 19374.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Four-Fin\"", + "-Dbundle_count=2" + ], + "generator": "strap-beam-4x.scad", + "label": "bundle2", + "outcome": "ok", + "profile": "Four-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 2.0, + "BUNDLE_THICK_MM": 1.016, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 27.233, + "ENVELOPE_Y_MM": 27.233, + "FAMILY": "4x", + "FIN_BORE_SIDE_MM": 7.917, + "FIN_CORE_SIDE_MM": 11.833, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 2.208, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 22.2547, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Four-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 179.473, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 17947.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Four-Fin\"", + "-Dbundle_count=3" + ], + "generator": "strap-beam-4x.scad", + "label": "bundle3", + "outcome": "ok", + "profile": "Four-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 3.0, + "BUNDLE_THICK_MM": 1.524, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 27.487, + "ENVELOPE_Y_MM": 27.487, + "FAMILY": "4x", + "FIN_BORE_SIDE_MM": 7.663, + "FIN_CORE_SIDE_MM": 12.087, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 2.462, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 22.557, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Four-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 181.911, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 18191.1, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-3x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 21.0181, + "ENVELOPE_Y_MM": 18.7353, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.6419, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.87663, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[18.0459, 18.0459, 18.0459]", + "RING_SCALE": 1.34671, + "SECTION_AREA_MM2": 134.209, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "VOLUME_MM3": 13420.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-3x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 24.47, + "ENVELOPE_Y_MM": 21.7248, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 19.957, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.15863, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[21.0093, 21.0093, 21.0093]", + "RING_SCALE": 1.32342, + "SECTION_AREA_MM2": 160.943, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 16094.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"General Triangle\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-3x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "General Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 20.8772, + "ENVELOPE_Y_MM": 18.9479, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.743, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "General Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.69049, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[17.8964, 17.4994, 19.0035]", + "RING_SCALE": 1.02265, + "SECTION_AREA_MM2": 135.024, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "VOLUME_MM3": 13502.4, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"General Triangle\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-3x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "General Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 24.3041, + "ENVELOPE_Y_MM": 21.9831, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 20.0927, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "General Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.94437, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.8394, 20.3771, 22.1286]", + "RING_SCALE": 1.19082, + "SECTION_AREA_MM2": 162.038, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 16203.8, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-3x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "A Frame", + "rejection": null, + "report": { + "AF_APEX_SETBACK_MM": 1.35253, + "AF_APEX_WEB_MM": 1.2, + "AF_CROSSBAR_DEPTH_MM": 9.86382, + "AF_CROSSBAR_WEB_MM": 1.2, + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 27.9553, + "ENVELOPE_Y_MM": 14.0435, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 15.1605, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "A Frame", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 122.262, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "VOLUME_MM3": 12226.2, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-3x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "A Frame", + "rejection": null, + "report": { + "AF_APEX_SETBACK_MM": 1.49353, + "AF_APEX_WEB_MM": 1.2, + "AF_CROSSBAR_DEPTH_MM": 11.4417, + "AF_CROSSBAR_WEB_MM": 1.2, + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 31.7919, + "ENVELOPE_Y_MM": 16.0927, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.7936, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "A Frame", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 143.497, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14349.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-3x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 29.562, + "ENVELOPE_Y_MM": 25.6014, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 14.6014, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 117.753, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 7.9338, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "VOLUME_MM3": 11775.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-3x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 34.1308, + "ENVELOPE_Y_MM": 29.5581, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.0445, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 137.456, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.25271, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13745.6, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"T\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-3x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "T", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 31.4, + "ENVELOPE_Y_MM": 18.508, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 14.5596, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "T", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 117.416, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "T_FLANGE_SPAN_MM": 31.4, + "T_NOTE": "inside_wall unused: no enclosed bore", + "VOLUME_MM3": 11741.6, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"T\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-3x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "T", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.35, + "ENVELOPE_Y_MM": 21.265, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.937, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "T", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 136.589, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "T_FLANGE_SPAN_MM": 36.35, + "T_NOTE": "inside_wall unused: no enclosed bore", + "VOLUME_MM3": 13658.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-3x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.4285, + "ENVELOPE_Y_MM": 26.648, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 4.4426, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 6.25283, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 19.1299, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 154.273, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "VOLUME_MM3": 15427.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-3x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 27.0391, + "ENVELOPE_Y_MM": 27.212, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.8913, + "FIN_JUNCTION_WEB_MM": 2.51069, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 4.26627, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.9379, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 152.725, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15272.5, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Square\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-4x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "Square", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 20.0131, + "ENVELOPE_Y_MM": 20.0131, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 20.9507, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Square", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.01368, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[16.6051, 16.6051, 16.6051, 16.6051]", + "RING_SCALE": 16.6051, + "SECTION_AREA_MM2": 168.957, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "VOLUME_MM3": 16895.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Square\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-4x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "Square", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 23.0521, + "ENVELOPE_Y_MM": 23.0521, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 24.7914, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Square", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.29568, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[19.3621, 19.3621, 19.3621, 19.3621]", + "RING_SCALE": 19.3621, + "SECTION_AREA_MM2": 199.931, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 19993.1, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Rectangle\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-4x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "Rectangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 24.848, + "ENVELOPE_Y_MM": 16.808, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 22.3284, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Rectangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 1.41116, + "RING_CORNER_WEB_MM": 3.266, + "RING_EDGES_MM": "[21.44, 13.4, 21.44, 13.4]", + "RING_SCALE": 13.4, + "SECTION_AREA_MM2": 180.068, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "VOLUME_MM3": 18006.8, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Rectangle\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-4x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "Rectangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 29.09, + "ENVELOPE_Y_MM": 19.565, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 27.1258, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Rectangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 1.55216, + "RING_CORNER_WEB_MM": 3.8675, + "RING_EDGES_MM": "[25.4, 15.875, 25.4, 15.875]", + "RING_SCALE": 15.875, + "SECTION_AREA_MM2": 218.757, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 21875.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Diamond\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-4x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "Diamond", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 27.2608, + "ENVELOPE_Y_MM": 27.2608, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 20.9507, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Diamond", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.01368, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[16.6051, 16.6051, 16.6051, 16.6051]", + "RING_SCALE": 16.6051, + "SECTION_AREA_MM2": 168.957, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "VOLUME_MM3": 16895.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Diamond\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-4x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "Diamond", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 31.5586, + "ENVELOPE_Y_MM": 31.5586, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 24.7914, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Diamond", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.29568, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[19.3621, 19.3621, 19.3621, 19.3621]", + "RING_SCALE": 19.3621, + "SECTION_AREA_MM2": 199.931, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 19993.1, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Cross\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-4x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "Cross", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 32.9051, + "ENVELOPE_Y_MM": 32.9051, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 19.6635, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Cross", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 158.577, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 8.30253, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "VOLUME_MM3": 15857.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Cross\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-4x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "Cross", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 38.1371, + "ENVELOPE_Y_MM": 38.1371, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 22.9767, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Cross", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 185.296, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.68103, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 18529.6, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Four-Fin\"", + "-Dstrap_width_mm=13.4" + ], + "generator": "strap-beam-4x.scad", + "label": "width13.4", + "outcome": "ok", + "profile": "Four-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.308, + "ENVELOPE_Y_MM": 26.308, + "FAMILY": "4x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 10.908, + "FIN_JUNCTION_WEB_MM": 3.004, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.758, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 22.0555, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Four-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 177.867, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 13.4, + "VOLUME_MM3": 17786.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Four-Fin\"", + "-Dstrap_thickness_mm=0.79" + ], + "generator": "strap-beam-4x.scad", + "label": "steel0.79", + "outcome": "ok", + "profile": "Four-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.79, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 27.12, + "ENVELOPE_Y_MM": 27.12, + "FAMILY": "4x", + "FIN_BORE_SIDE_MM": 8.03, + "FIN_CORE_SIDE_MM": 11.72, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 2.095, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 22.1201, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Four-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 178.388, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.79, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 17838.8, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-3x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 27.65, + "ENVELOPE_Y_MM": 24.4788, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 29.2967, + "MIN_WALL_ACTUAL_MM": 0.8, + "MIN_WALL_SPEC_MM": 0.8, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 6.33591, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.5209, 20.5209, 20.5209]", + "RING_SCALE": 1.29266, + "SECTION_AREA_MM2": 236.264, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 23626.4, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"General Triangle\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-3x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "General Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 27.4916, + "ENVELOPE_Y_MM": 24.8063, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 29.525, + "MIN_WALL_ACTUAL_MM": 0.8, + "MIN_WALL_SPEC_MM": 0.8, + "PROFILE": "General Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 5.99884, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.3925, 19.9401, 21.654]", + "RING_SCALE": 1.16529, + "SECTION_AREA_MM2": 238.105, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 23810.5, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-3x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "A Frame", + "rejection": null, + "report": { + "AF_APEX_SETBACK_MM": 1.35253, + "AF_APEX_WEB_MM": 1.2, + "AF_CROSSBAR_DEPTH_MM": 11.1013, + "AF_CROSSBAR_WEB_MM": 1.2, + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.2244, + "ENVELOPE_Y_MM": 16.3593, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 26.1972, + "MIN_WALL_ACTUAL_MM": 0.8, + "MIN_WALL_SPEC_MM": 0.8, + "PROFILE": "A Frame", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 211.268, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 21126.8, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-3x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 35.0488, + "ENVELOPE_Y_MM": 30.3531, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 32.1927, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 0.8, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 259.619, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.1713, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 25961.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"T\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-3x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "T", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.35, + "ENVELOPE_Y_MM": 22.183, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 31.9531, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 0.8, + "PROFILE": "T", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 257.687, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "T_FLANGE_SPAN_MM": 36.35, + "T_NOTE": "inside_wall unused: no enclosed bore", + "VOLUME_MM3": 25768.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-3x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.1985, + "ENVELOPE_Y_MM": 25.9488, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.61641, + "FIN_CORE_SIDE_MM": 12.1336, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 2.50859, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 23.3552, + "MIN_WALL_ACTUAL_MM": 0.8, + "MIN_WALL_SPEC_MM": 0.8, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 188.349, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 18834.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"Square\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-4x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "Square", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 24.8881, + "ENVELOPE_Y_MM": 24.8881, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 34.8074, + "MIN_WALL_ACTUAL_MM": 0.8, + "MIN_WALL_SPEC_MM": 0.8, + "PROFILE": "Square", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 9.12636, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[19.0801, 19.0801, 19.0801, 19.0801]", + "RING_SCALE": 19.0801, + "SECTION_AREA_MM2": 280.705, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 28070.5, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"Rectangle\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-4x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "Rectangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 31.208, + "ENVELOPE_Y_MM": 21.683, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 38.058, + "MIN_WALL_ACTUAL_MM": 0.8, + "MIN_WALL_SPEC_MM": 0.8, + "PROFILE": "Rectangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 6.65541, + "RING_CORNER_WEB_MM": 4.0085, + "RING_EDGES_MM": "[25.4, 15.875, 25.4, 15.875]", + "RING_SCALE": 15.875, + "SECTION_AREA_MM2": 306.92, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 30692.0, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"Diamond\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-4x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "Diamond", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 34.1551, + "ENVELOPE_Y_MM": 34.1551, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 34.8074, + "MIN_WALL_ACTUAL_MM": 0.8, + "MIN_WALL_SPEC_MM": 0.8, + "PROFILE": "Diamond", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 9.12636, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[19.0801, 19.0801, 19.0801, 19.0801]", + "RING_SCALE": 19.0801, + "SECTION_AREA_MM2": 280.705, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 28070.5, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"Cross\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-4x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "Cross", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 37.8551, + "ENVELOPE_Y_MM": 37.8551, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 42.3984, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 0.8, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Cross", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 341.923, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.54003, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 34192.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"Four-Fin\"", + "-Dinside_wall_thickness_mm=0.8", + "-Doutside_wall_thickness_mm=2.4", + "-Dmin_wall_mm=0.8" + ], + "generator": "strap-beam-4x.scad", + "label": "wall_asym", + "outcome": "ok", + "profile": "Four-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.979, + "ENVELOPE_Y_MM": 26.979, + "FAMILY": "4x", + "FIN_BORE_SIDE_MM": 8.971, + "FIN_CORE_SIDE_MM": 11.579, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 1.954, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 29.7609, + "MIN_WALL_ACTUAL_MM": 0.8, + "MIN_WALL_SPEC_MM": 0.8, + "PROFILE": "Four-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 240.007, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 24000.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 0.8, + "WALL_OUTSIDE_MM": 2.4 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Da_frame_leg_angle_deg=20" + ], + "generator": "strap-beam-3x.scad", + "label": "leg20", + "outcome": "ok", + "profile": "A Frame", + "rejection": null, + "report": { + "AF_APEX_SETBACK_MM": 0.821948, + "AF_APEX_WEB_MM": 1.2, + "AF_CROSSBAR_DEPTH_MM": 5.29737, + "AF_CROSSBAR_WEB_MM": 1.2, + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 48.4365, + "ENVELOPE_Y_MM": 9.6239, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 19.5676, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "A Frame", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 157.803, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15780.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Da_frame_leg_angle_deg=30" + ], + "generator": "strap-beam-3x.scad", + "label": "leg30", + "outcome": "ok", + "profile": "A Frame", + "rejection": null, + "report": { + "AF_APEX_SETBACK_MM": 0.983805, + "AF_APEX_WEB_MM": 1.2, + "AF_CROSSBAR_DEPTH_MM": 7.19867, + "AF_CROSSBAR_WEB_MM": 1.2, + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 37.6557, + "ENVELOPE_Y_MM": 12.3389, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.5526, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "A Frame", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 141.553, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14155.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Da_frame_leg_angle_deg=45" + ], + "generator": "strap-beam-3x.scad", + "label": "leg45", + "outcome": "ok", + "profile": "A Frame", + "rejection": null, + "report": { + "AF_APEX_SETBACK_MM": 1.35253, + "AF_APEX_WEB_MM": 1.2, + "AF_CROSSBAR_DEPTH_MM": 11.1013, + "AF_CROSSBAR_WEB_MM": 1.2, + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 30.4303, + "ENVELOPE_Y_MM": 15.7936, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.1457, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "A Frame", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 138.272, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13827.2, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Da_frame_leg_angle_deg=53" + ], + "generator": "strap-beam-3x.scad", + "label": "leg53", + "outcome": "ok", + "profile": "A Frame", + "rejection": null, + "report": { + "AF_APEX_SETBACK_MM": 1.66581, + "AF_APEX_WEB_MM": 1.2, + "AF_CROSSBAR_DEPTH_MM": 14.2006, + "AF_CROSSBAR_WEB_MM": 1.2, + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 28.2373, + "ENVELOPE_Y_MM": 17.4174, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.6967, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "A Frame", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 142.716, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14271.6, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Da_frame_leg_angle_deg=55" + ], + "generator": "strap-beam-3x.scad", + "label": "leg55", + "outcome": "rejected", + "profile": "A Frame", + "rejection": "A Frame: the crossbar sits at 15.1678 mm below the apex but the legs only reach 14.8601 mm. Reduce a_frame_crossbar_web_mm or the leg angle.", + "report": null + }, + { + "defs": [ + "-Dprofile_type=\"A Frame\"", + "-Da_frame_leg_angle_deg=70" + ], + "generator": "strap-beam-3x.scad", + "label": "leg70", + "outcome": "rejected", + "profile": "A Frame", + "rejection": "A Frame: the crossbar sits at 20.2136 mm below the apex but the legs only reach 18.3372 mm. Reduce a_frame_crossbar_web_mm or the leg angle.", + "report": null + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dring_corner_radius_mm=0" + ], + "generator": "strap-beam-3x.scad", + "label": "cornerR0", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.4237, + "ENVELOPE_Y_MM": 22.8836, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 19.8757, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.87663, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.5209, 20.5209, 20.5209]", + "RING_SCALE": 1.29266, + "SECTION_AREA_MM2": 160.288, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 16028.8, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dring_corner_radius_mm=1" + ], + "generator": "strap-beam-3x.scad", + "label": "cornerR1", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 24.9584, + "ENVELOPE_Y_MM": 21.8812, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 19.6197, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.87663, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.5209, 20.5209, 20.5209]", + "RING_SCALE": 1.29266, + "SECTION_AREA_MM2": 158.223, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15822.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dring_corner_radius_mm=2" + ], + "generator": "strap-beam-3x.scad", + "label": "cornerR2", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 23.4931, + "ENVELOPE_Y_MM": 20.8788, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.8516, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.87663, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.5209, 20.5209, 20.5209]", + "RING_SCALE": 1.29266, + "SECTION_AREA_MM2": 152.029, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15202.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dring_corner_radius_mm=2.8" + ], + "generator": "strap-beam-3x.scad", + "label": "cornerR2.8", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 22.3208, + "ENVELOPE_Y_MM": 20.0768, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.8684, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.87663, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.5209, 20.5209, 20.5209]", + "RING_SCALE": 1.29266, + "SECTION_AREA_MM2": 144.1, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14410.0, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dring_corner_radius_mm=2.9" + ], + "generator": "strap-beam-3x.scad", + "label": "cornerR2.9", + "outcome": "rejected", + "profile": "Equilateral Triangle", + "rejection": "Equilateral Triangle: corner radius of 2.9 mm is not usable here - it would cut the outer wall below 1.2 mm at the corners, or exceed what the envelope can accept. Maximum is 2.87663 mm.", + "report": null + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dring_corner_radius_mm=5" + ], + "generator": "strap-beam-3x.scad", + "label": "cornerR5", + "outcome": "rejected", + "profile": "Equilateral Triangle", + "rejection": "Equilateral Triangle: corner radius of 5 mm is not usable here - it would cut the outer wall below 1.2 mm at the corners, or exceed what the envelope can accept. Maximum is 2.87663 mm.", + "report": null + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dring_corner_web_mm=0.4", + "-Dmin_wall_mm=0.4" + ], + "generator": "strap-beam-3x.scad", + "label": "web0.4", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 21.8931, + "ENVELOPE_Y_MM": 19.4931, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.8231, + "MIN_WALL_ACTUAL_MM": 0.4, + "MIN_WALL_SPEC_MM": 0.4, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.67335, + "RING_CORNER_WEB_MM": 0.4, + "RING_EDGES_MM": "[18.9209, 18.9209, 18.9209]", + "RING_SCALE": 1.19187, + "SECTION_AREA_MM2": 135.67, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13567.0, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dring_corner_web_mm=1.2", + "-Dmin_wall_mm=1.2" + ], + "generator": "strap-beam-3x.scad", + "label": "web1.2", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 23.4931, + "ENVELOPE_Y_MM": 20.8788, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.8516, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.87663, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.5209, 20.5209, 20.5209]", + "RING_SCALE": 1.29266, + "SECTION_AREA_MM2": 152.029, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15202.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dring_corner_web_mm=2.0", + "-Dmin_wall_mm=1.2" + ], + "generator": "strap-beam-3x.scad", + "label": "web2.0", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 25.0931, + "ENVELOPE_Y_MM": 22.2644, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 20.88, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.33851, + "RING_CORNER_WEB_MM": 2.0, + "RING_EDGES_MM": "[22.1209, 22.1209, 22.1209]", + "RING_SCALE": 1.39344, + "SECTION_AREA_MM2": 168.387, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 16838.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dring_corner_web_mm=4.0", + "-Dmin_wall_mm=1.2" + ], + "generator": "strap-beam-3x.scad", + "label": "web4.0", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 29.0931, + "ENVELOPE_Y_MM": 25.7285, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 25.9511, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 4.49322, + "RING_CORNER_WEB_MM": 4.0, + "RING_EDGES_MM": "[26.1209, 26.1209, 26.1209]", + "RING_SCALE": 1.64541, + "SECTION_AREA_MM2": 209.283, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 20928.3, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_fin_mm=0" + ], + "generator": "strap-beam-3x.scad", + "label": "fin0", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 24.2864, + "ENVELOPE_Y_MM": 21.4364, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 12.4808, + "FIN_CORE_SIDE_MM": 18.3836, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 0.0, + "FIN_SETBACK_MM": 2.50859, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.2452, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 139.074, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13907.4, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_fin_mm=2" + ], + "generator": "strap-beam-3x.scad", + "label": "fin2", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 23.0343, + "ENVELOPE_Y_MM": 21.8682, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 10.4808, + "FIN_CORE_SIDE_MM": 16.3836, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 2.0, + "FIN_SETBACK_MM": 2.50859, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.5925, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 133.81, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13381.0, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_fin_mm=6.25" + ], + "generator": "strap-beam-3x.scad", + "label": "fin6.25", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.4285, + "ENVELOPE_Y_MM": 26.648, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 2.29919, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.77783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.2016, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 146.787, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14678.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_fin_mm=12" + ], + "generator": "strap-beam-3x.scad", + "label": "fin12", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 35.0535, + "ENVELOPE_Y_MM": 36.6073, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 7.34062, + "FIN_PROJECTION_MM": 12.0, + "FIN_SETBACK_MM": 9.52783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 25.4915, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 205.577, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 20557.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_fin_mm=25" + ], + "generator": "strap-beam-3x.scad", + "label": "fin25", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 54.5535, + "ENVELOPE_Y_MM": 59.1239, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 27.3101, + "FIN_PROJECTION_MM": 25.0, + "FIN_SETBACK_MM": 22.5278, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 41.9722, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 338.485, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 33848.5, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_junction_round_mm=0" + ], + "generator": "strap-beam-3x.scad", + "label": "fillet0", + "outcome": "rejected", + "profile": "Three-Fin", + "rejection": "Three-Fin: the junction fillet radius must be greater than zero. A butt joint with no fillet meets its neighbour along an exactly tangent boundary, which is a valid outline but cannot be tessellated.", + "report": null + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_junction_round_mm=1" + ], + "generator": "strap-beam-3x.scad", + "label": "fillet1", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.4285, + "ENVELOPE_Y_MM": 26.648, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 2.29919, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.77783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.1396, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 146.287, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14628.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_junction_round_mm=2" + ], + "generator": "strap-beam-3x.scad", + "label": "fillet2", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.4285, + "ENVELOPE_Y_MM": 26.648, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 2.29919, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.77783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.2016, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 146.787, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14678.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_junction_round_mm=8" + ], + "generator": "strap-beam-3x.scad", + "label": "fillet8", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.4285, + "ENVELOPE_Y_MM": 26.648, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 2.29919, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.77783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.7903, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 151.535, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15153.5, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_bore_side_mm=1" + ], + "generator": "strap-beam-3x.scad", + "label": "bore1", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 25.1593, + "ENVELOPE_Y_MM": 25.5488, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 6.23077, + "FIN_CORE_SIDE_MM": 12.1336, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 2.50859, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.5927, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 133.812, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13381.2, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_bore_side_mm=7.5" + ], + "generator": "strap-beam-3x.scad", + "label": "bore7.5", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.4285, + "ENVELOPE_Y_MM": 26.648, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 2.29919, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.77783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.2016, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 146.787, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14678.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_bore_side_mm=15" + ], + "generator": "strap-beam-3x.scad", + "label": "bore15", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 33.9285, + "ENVELOPE_Y_MM": 33.1432, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 15.0, + "FIN_CORE_SIDE_MM": 20.9028, + "FIN_JUNCTION_WEB_MM": 8.79438, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 11.2778, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 27.7099, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 223.467, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 22346.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_bore_side_mm=30" + ], + "generator": "strap-beam-3x.scad", + "label": "bore30", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 48.9285, + "ENVELOPE_Y_MM": 46.1336, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 30.0, + "FIN_CORE_SIDE_MM": 35.9028, + "FIN_JUNCTION_WEB_MM": 22.0762, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 26.2778, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 46.7268, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 376.829, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 37682.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_rotation_deg=0" + ], + "generator": "strap-beam-3x.scad", + "label": "rot0", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.4285, + "ENVELOPE_Y_MM": 26.648, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 2.29919, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.77783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.2016, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 146.787, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14678.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_rotation_deg=17" + ], + "generator": "strap-beam-3x.scad", + "label": "rot17", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 24.4575, + "ENVELOPE_Y_MM": 27.4429, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 2.29919, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.77783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.2018, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 146.789, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14678.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_rotation_deg=33" + ], + "generator": "strap-beam-3x.scad", + "label": "rot33", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.9622, + "ENVELOPE_Y_MM": 26.1771, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 2.29919, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.77783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.2018, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 146.789, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14678.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Three-Fin\"", + "-Dthree_fin_rotation_deg=90" + ], + "generator": "strap-beam-3x.scad", + "label": "rot90", + "outcome": "ok", + "profile": "Three-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.648, + "ENVELOPE_Y_MM": 26.4285, + "FAMILY": "3x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 13.4028, + "FIN_JUNCTION_WEB_MM": 2.29919, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 3.77783, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.2016, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Three-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 146.787, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 14678.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Dy_junction_web_mm=0", + "-Dmin_wall_mm=0.01" + ], + "generator": "strap-beam-3x.scad", + "label": "ywebb0", + "outcome": "rejected", + "profile": "Y", + "rejection": "Expected 3 separate strap channels but found 1. Neighbouring channels have merged, so those straps share one slot and are not retained. Increase the relevant web.", + "report": null + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Dy_junction_web_mm=0.5", + "-Dmin_wall_mm=0.5" + ], + "generator": "strap-beam-3x.scad", + "label": "ywebb0.5", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 33.1488, + "ENVELOPE_Y_MM": 28.7077, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.2988, + "MIN_WALL_ACTUAL_MM": 0.5, + "MIN_WALL_SPEC_MM": 0.5, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 131.442, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 8.76716, + "SPOKE_WEB_MM": 0.5, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13144.2, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Dy_junction_web_mm=1.2", + "-Dmin_wall_mm=1.2" + ], + "generator": "strap-beam-3x.scad", + "label": "ywebb1.2", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 33.8488, + "ENVELOPE_Y_MM": 29.3139, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.8112, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 135.574, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.1713, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13557.4, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Dy_junction_web_mm=4.0", + "-Dmin_wall_mm=1.2" + ], + "generator": "strap-beam-3x.scad", + "label": "ywebb4.0", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.6488, + "ENVELOPE_Y_MM": 31.7388, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.842, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 151.951, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 10.7879, + "SPOKE_WEB_MM": 4.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15195.1, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"T\"", + "-Dt_stem_web_mm=0", + "-Dmin_wall_mm=0.01" + ], + "generator": "strap-beam-3x.scad", + "label": "tweb0", + "outcome": "rejected", + "profile": "T", + "rejection": "T: t_stem_web_mm of 0 mm is below min_wall_mm.", + "report": null + }, + { + "defs": [ + "-Dprofile_type=\"T\"", + "-Dt_stem_web_mm=0.25", + "-Dmin_wall_mm=0.25" + ], + "generator": "strap-beam-3x.scad", + "label": "tweb0.25", + "outcome": "ok", + "profile": "T", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.35, + "ENVELOPE_Y_MM": 20.033, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.3678, + "MIN_WALL_ACTUAL_MM": 0.267798, + "MIN_WALL_SPEC_MM": 0.25, + "PROFILE": "T", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 131.999, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "T_FLANGE_SPAN_MM": 36.35, + "T_NOTE": "inside_wall unused: no enclosed bore", + "VOLUME_MM3": 13199.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"T\"", + "-Dt_stem_web_mm=1.2", + "-Dmin_wall_mm=1.2" + ], + "generator": "strap-beam-3x.scad", + "label": "tweb1.2", + "outcome": "ok", + "profile": "T", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.35, + "ENVELOPE_Y_MM": 20.983, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.7693, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "T", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 135.236, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "T_FLANGE_SPAN_MM": 36.35, + "T_NOTE": "inside_wall unused: no enclosed bore", + "VOLUME_MM3": 13523.6, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"T\"", + "-Dt_stem_web_mm=3.0", + "-Dmin_wall_mm=1.2" + ], + "generator": "strap-beam-3x.scad", + "label": "tweb3.0", + "outcome": "ok", + "profile": "T", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.35, + "ENVELOPE_Y_MM": 22.783, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 17.5299, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "T", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 141.371, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "T_FLANGE_SPAN_MM": 36.35, + "T_NOTE": "inside_wall unused: no enclosed bore", + "VOLUME_MM3": 14137.1, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"General Triangle\"", + "-Dgeneral_triangle_height=4" + ], + "generator": "strap-beam-3x.scad", + "label": "h4", + "outcome": "rejected", + "profile": "General Triangle", + "rejection": "General Triangle: corner radius of 2 mm is not usable here - it would cut the outer wall below 1.2 mm at the corners, or exceed what the envelope can accept. Maximum is 8.26006e-14 mm.", + "report": null + }, + { + "defs": [ + "-Dprofile_type=\"General Triangle\"", + "-Dgeneral_triangle_height=8" + ], + "generator": "strap-beam-3x.scad", + "label": "h8", + "outcome": "rejected", + "profile": "General Triangle", + "rejection": "General Triangle: corner radius of 2 mm is not usable here - it would cut the outer wall below 1.2 mm at the corners, or exceed what the envelope can accept. Maximum is 1.68508 mm.", + "report": null + }, + { + "defs": [ + "-Dprofile_type=\"General Triangle\"", + "-Dgeneral_triangle_height=15.5" + ], + "generator": "strap-beam-3x.scad", + "label": "h15.5", + "outcome": "ok", + "profile": "General Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 23.3733, + "ENVELOPE_Y_MM": 21.1587, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 19.0213, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "General Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.68089, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.3925, 19.9401, 21.654]", + "RING_SCALE": 1.16529, + "SECTION_AREA_MM2": 153.398, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15339.8, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"General Triangle\"", + "-Dgeneral_triangle_height=30" + ], + "generator": "strap-beam-3x.scad", + "label": "h30", + "outcome": "rejected", + "profile": "General Triangle", + "rejection": "General Triangle: corner radius of 2 mm is not usable here - it would cut the outer wall below 1.2 mm at the corners, or exceed what the envelope can accept. Maximum is 1.47438 mm.", + "report": null + }, + { + "defs": [ + "-Dprofile_type=\"Rectangle\"", + "-Drectangle_aspect=1.0" + ], + "generator": "strap-beam-4x.scad", + "label": "aspect1.0", + "outcome": "ok", + "profile": "Rectangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 22.4881, + "ENVELOPE_Y_MM": 22.4881, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 23.897, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Rectangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 3.01368, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[19.0801, 19.0801, 19.0801, 19.0801]", + "RING_SCALE": 19.0801, + "SECTION_AREA_MM2": 192.717, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 19271.7, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Rectangle\"", + "-Drectangle_aspect=1.6" + ], + "generator": "strap-beam-4x.scad", + "label": "aspect1.6", + "outcome": "ok", + "profile": "Rectangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 28.808, + "ENVELOPE_Y_MM": 19.283, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 26.5298, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Rectangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 1.41116, + "RING_CORNER_WEB_MM": 4.0085, + "RING_EDGES_MM": "[25.4, 15.875, 25.4, 15.875]", + "RING_SCALE": 15.875, + "SECTION_AREA_MM2": 213.95, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 21395.0, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Rectangle\"", + "-Drectangle_aspect=2.5" + ], + "generator": "strap-beam-4x.scad", + "label": "aspect2.5", + "outcome": "ok", + "profile": "Rectangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 43.0955, + "ENVELOPE_Y_MM": 19.283, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 38.6052, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Rectangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 1.41116, + "RING_CORNER_WEB_MM": 11.1523, + "RING_EDGES_MM": "[39.6875, 15.875, 39.6875, 15.875]", + "RING_SCALE": 15.875, + "SECTION_AREA_MM2": 311.332, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 31133.2, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Rectangle\"", + "-Drectangle_aspect=4.0" + ], + "generator": "strap-beam-4x.scad", + "label": "aspect4.0", + "outcome": "ok", + "profile": "Rectangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 66.908, + "ENVELOPE_Y_MM": 19.283, + "FAMILY": "4x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 58.7313, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Rectangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 1.41116, + "RING_CORNER_WEB_MM": 23.0585, + "RING_EDGES_MM": "[63.5, 15.875, 63.5, 15.875]", + "RING_SCALE": 15.875, + "SECTION_AREA_MM2": 473.639, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 47363.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Four-Fin\"", + "-Dfour_fin_fin_mm=0" + ], + "generator": "strap-beam-4x.scad", + "label": "fin0", + "outcome": "ok", + "profile": "Four-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 21.237, + "ENVELOPE_Y_MM": 21.237, + "FAMILY": "4x", + "FIN_BORE_SIDE_MM": 14.421, + "FIN_CORE_SIDE_MM": 17.829, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 0.0, + "FIN_SETBACK_MM": 1.954, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 21.9506, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Four-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 177.021, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 17702.1, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Four-Fin\"", + "-Dfour_fin_fin_mm=2" + ], + "generator": "strap-beam-4x.scad", + "label": "fin2", + "outcome": "ok", + "profile": "Four-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 22.729, + "ENVELOPE_Y_MM": 22.729, + "FAMILY": "4x", + "FIN_BORE_SIDE_MM": 12.421, + "FIN_CORE_SIDE_MM": 15.829, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 2.0, + "FIN_SETBACK_MM": 1.954, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 21.6001, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Four-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 174.194, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 17419.4, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Four-Fin\"", + "-Dfour_fin_fin_mm=6.25" + ], + "generator": "strap-beam-4x.scad", + "label": "fin6.25", + "outcome": "ok", + "profile": "Four-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 26.979, + "ENVELOPE_Y_MM": 26.979, + "FAMILY": "4x", + "FIN_BORE_SIDE_MM": 8.171, + "FIN_CORE_SIDE_MM": 11.579, + "FIN_JUNCTION_WEB_MM": 1.2, + "FIN_PROJECTION_MM": 6.25, + "FIN_SETBACK_MM": 1.954, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 21.9523, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Four-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 177.035, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 17703.5, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Four-Fin\"", + "-Dfour_fin_fin_mm=15" + ], + "generator": "strap-beam-4x.scad", + "label": "fin15", + "outcome": "ok", + "profile": "Four-Fin", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 43.808, + "ENVELOPE_Y_MM": 43.808, + "FAMILY": "4x", + "FIN_BORE_SIDE_MM": 7.5, + "FIN_CORE_SIDE_MM": 10.908, + "FIN_JUNCTION_WEB_MM": 9.279, + "FIN_PROJECTION_MM": 15.0, + "FIN_SETBACK_MM": 10.033, + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 35.6088, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Four-Fin", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 287.168, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 4.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 28716.8, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Da_frame_leg_angle_deg=89" + ], + "generator": "strap-beam-3x.scad", + "label": "isolate_aframe", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 33.8488, + "ENVELOPE_Y_MM": 29.3139, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.8112, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 135.574, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.1713, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13557.4, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Y\"", + "-Dthree_fin_fin_mm=0" + ], + "generator": "strap-beam-3x.scad", + "label": "isolate_threefin", + "outcome": "ok", + "profile": "Y", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 33.8488, + "ENVELOPE_Y_MM": 29.3139, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.8112, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "NOTE": "inside wall unused: no enclosed bore", + "PROFILE": "Y", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 135.574, + "SECTION_PARTS": 1.0, + "SPOKE_RADIUS_MM": 9.1713, + "SPOKE_WEB_MM": 1.2, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 13557.4, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"T\"", + "-Dgeneral_triangle_height=0.001" + ], + "generator": "strap-beam-3x.scad", + "label": "isolate_gentri", + "outcome": "ok", + "profile": "T", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 36.35, + "ENVELOPE_Y_MM": 20.983, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 16.7693, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "T", + "REVISION": "8.0.0", + "SECTION_AREA_MM2": 135.236, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "T_FLANGE_SPAN_MM": 36.35, + "T_NOTE": "inside_wall unused: no enclosed bore", + "VOLUME_MM3": 13523.6, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + }, + { + "defs": [ + "-Dprofile_type=\"Equilateral Triangle\"", + "-Dt_stem_web_mm=0" + ], + "generator": "strap-beam-3x.scad", + "label": "isolate_t", + "outcome": "ok", + "profile": "Equilateral Triangle", + "rejection": null, + "report": { + "BUNDLE_COUNT": 1.0, + "BUNDLE_THICK_MM": 0.508, + "CLEARANCE_MM": 0.25, + "ENVELOPE_X_MM": 23.4931, + "ENVELOPE_Y_MM": 20.8788, + "FAMILY": "3x", + "LENGTH_FT": 10.0, + "LENGTH_MM": 100.0, + "LENGTH_VIEW": "Preview", + "MASS_G": 18.8516, + "MIN_WALL_ACTUAL_MM": 1.2, + "MIN_WALL_SPEC_MM": 1.2, + "PROFILE": "Equilateral Triangle", + "REVISION": "8.0.0", + "RING_CORNER_R_MAX_MM": 2.87663, + "RING_CORNER_WEB_MM": 1.2, + "RING_EDGES_MM": "[20.5209, 20.5209, 20.5209]", + "RING_SCALE": 1.29266, + "SECTION_AREA_MM2": 152.029, + "SECTION_PARTS": 1.0, + "STATUS": "ok", + "STRAP_CHANNELS": 3.0, + "STRAP_THICK_MM": 0.508, + "STRAP_WIDTH_MM": 15.875, + "VOLUME_MM3": 15202.9, + "WALL_EDGE_MM": 1.2, + "WALL_INSIDE_MM": 1.2, + "WALL_OUTSIDE_MM": 1.2 + } + } + ], + "fixtures_sha256": "ddd0f1548379205dd0c652ec07285b0dae331e52ff0a0437005dfc6cddcc2cb2", + "frozen": "2026-08-14", + "generator_revision": "8.0.0", + "note": "Acceptance oracle for any reimplementation of the rev-8 generators. MIN_WALL_ACTUAL_MM, SECTION_PARTS and STRAP_CHANNELS are the invariants; the solved values (RING_*, FIN_*, AF_*, SPOKE_*) pin the placement solvers. Rejected cases are part of the contract: a port that accepts them is wrong.", + "schema": "strap-beam.fixtures/1", + "summary": { + "accepted": 113, + "cases": 123, + "rejected": 10 + }, + "tolerance": { + "areas_mm2": 0.001, + "lengths_mm": 0.0001, + "note": "counts are exact" + }, + "toolchain": { + "bosl2_commit": "92d697c2856de2fed93a33e858068589cefc2898", + "openscad": "2021.01" + } +} \ No newline at end of file diff --git a/legacy/openscad/README.md b/legacy/openscad/README.md new file mode 100644 index 0000000..2c687e1 --- /dev/null +++ b/legacy/openscad/README.md @@ -0,0 +1,74 @@ +# Reference implementation — OpenSCAD, revision 8.0.0 + +**This is not a live target.** It is kept so the acceptance oracle in +`fixtures/strap-beam-8.0.0/` can be regenerated, and so the port has something +to be checked against. The running application has no OpenSCAD dependency, and +none of this is installed on an instance host. + +## What it produces + +Eleven profiles across two generators. A profile is a cross-section swept along +a longitudinal axis; a "member" is the end view of one strap bundle, so its +length in the cross-section is the strap's **width**, never the beam's length. + +| Generator | Profiles | +|---|---| +| `strap-beam-3x.scad` | Equilateral Triangle, General Triangle, A Frame, Y, T, Three-Fin | +| `strap-beam-4x.scad` | Square, Rectangle, Diamond, Cross, Four-Fin | + +4x is the end of the N-strap family. Higher polygon counts compose from +triangles and squares rather than getting their own generators. + +## The shared library + +Everything reusable is in `lib/`, written for N members and used unchanged by +both generators: + +| File | Contents | +|---|---| +| `sb-geom.scad` | GEO and MEMBER records, path construction, exact polyline distance, the monotone solver | +| `sb-join.scad` | butt joints, hull caps, concave-corner fillets, ring envelopes and bores, polygon fitting | +| `sb-report.scad` | value-based checks, section metrics, the `SB_KEY=value` report | +| `sb-profiles.scad` | three complete N-generic arrangements: ring, spokes, fins | +| `sb-core.scad` | umbrella include and the PROFILE record | + +`strap-beam-4x.scad` is 210 lines and contains **no geometry** — five one-line +calls into the shared library. The same three calls with N=3 produce the +triangles, the Y and the Three-Fin. That is the evidence the split is real. + +## Design rules the library enforces + +1. Declared webs are cavity-to-cavity. A stated 1.2 mm web is 1.2 mm of plastic. +2. Placement that cannot be derived exactly is solved numerically against the + measured quantity, not approximated with a closed form. +3. Junctions are structural before they are decorative: members butt through + their neighbours, and fillets are applied on top of that overlap. +4. A member with no enclosed side takes the outside wall on both faces, so + asymmetric wall settings never make a symmetric profile chiral. +5. Validation measures the finished section. Connectivity is necessary and + never sufficient — revision 7 passed cross-sections joined by 0.13 mm. + +## Running it + +Requires OpenSCAD 2021.01 and BOSL2 at `92d697c2`. Use the pinned image: + +```bash +docker run --rm -v "$PWD:/repo" mechcomp/reference-toolchain:8.0.0 \ + openscad -o /dev/null --export-format=asciistl \ + -Dprofile_type='"Three-Fin"' /repo/legacy/openscad/strap-beam-3x.scad +``` + +Every render emits a flat `SB_KEY=value` block on stderr, terminated by +`SB_END=1`. `render_mode="Section"` emits the 2D region instead of the swept +solid, which is what `--export-format=svg` consumes. + +## Three BOSL2 behaviours worth knowing + +Found the hard way; the library works around all three: + +- Boolean functions return a bare `[]` rather than an empty region. +- `offset()` on a single-part region returns a bare path rather than a region. +- `round_corners()` raises rather than reporting when a radius will not fit. + +`sb_as_region()`, `sb_area()`/`sb_nparts()` and `sb_path_max_round()` exist for +exactly these. diff --git a/legacy/openscad/lib/sb-core.scad b/legacy/openscad/lib/sb-core.scad new file mode 100644 index 0000000..4259b96 --- /dev/null +++ b/legacy/openscad/lib/sb-core.scad @@ -0,0 +1,133 @@ +/* + sb-core.scad — Strap-Beam library, umbrella include + =================================================== + + A "strap beam" is N pallet-strap bundles running parallel to a common + longitudinal axis (Z), held in a printed PLA+ enclosure. A profile only + decides how the N cross-sections are arranged in XY; that arrangement is + then swept along Z. Member length in the cross-section is therefore the + strap's WIDTH, never the beam's length. + + include + + brings in BOSL2 and the three library files. Generators for a particular + member count (strap-beam-3x.scad, strap-beam-4x.scad, ...) include this + and supply only their own profile catalogue. + + --------------------------------------------------------------------- + What lives where + --------------------------------------------------------------------- + sb-geom.scad GEO and MEMBER records; strap, cavity and sleeve paths; + exact polyline distance; the monotone solver. + sb-join.scad Butt joints, hull caps, fillets; ring envelopes and + bores; polygon fitting. All written for N members. + sb-report.scad Value-based checks, section metrics, the SB_KEY=value + report, and the 2D/3D output modules. + sb-profiles.scad Three complete N-generic arrangements - ring, spokes, + fins - each returning a finished PROFILE record. + + --------------------------------------------------------------------- + Contract for a profile builder + --------------------------------------------------------------------- + A generator supplies one function per profile that takes a GEO record and + returns a PROFILE record built with sb_profile(): + + members list of MEMBER records, one per strap bundle + shell region: all PLA+ before any void is removed + bore path: the enclosed central void, or [] if there is none + checks list from sb_check(), covering only this profile's own + parameters — never another profile's + info list of [key, value] pairs to add to the report + + The core removes the bore and all cavities from the shell in one step, so + one member's plastic can never fill another member's channel. Everything + a builder needs to construct `shell` is in sb-join.scad; a builder should + not be doing its own boolean algebra. + + --------------------------------------------------------------------- + Design rules the library enforces + --------------------------------------------------------------------- + 1. Declared webs are cavity-to-cavity. A stated 1.2 mm web is 1.2 mm of + plastic; the library adds the fit clearance internally. + 2. Placement that cannot be derived exactly is solved numerically against + the measured web, not approximated with a closed form. + 3. Junctions are structural before they are pretty: members butt through + their neighbours, and fillets are applied on top of that overlap. + 4. A member with no enclosed side gets the outside wall on both faces, so + asymmetric wall settings never make a symmetric profile chiral. + 5. Validation measures the finished section. Connectivity is necessary + but never sufficient; the minimum wall is what is actually checked. +*/ + +include + +include +include +include + + +// --------------------------------------------------------------------------- +// PROFILE record +// --------------------------------------------------------------------------- + +SB_P_MEMBERS = 0; +SB_P_SHELL = 1; +SB_P_BORE = 2; +SB_P_CHECKS = 3; +SB_P_INFO = 4; + +function sb_profile(members, shell, bore = [], checks = [], info = []) = + [members, shell, bore, checks, info]; + +function sb_p_members(p) = p[SB_P_MEMBERS]; +function sb_p_shell(p) = p[SB_P_SHELL]; +function sb_p_bore(p) = p[SB_P_BORE]; +function sb_p_checks(p) = p[SB_P_CHECKS]; +function sb_p_info(p) = p[SB_P_INFO]; + +// A builder that could not produce a usable arrangement returns this instead +// of guessing. The message reaches the user through the normal check list. +function sb_profile_failed(message) = + sb_profile([], [], [], [sb_check(false, message)], []); + +function sb_profile_ok(p) = len(sb_p_members(p)) > 0; + + +// Shared arrangements. Included last because they build PROFILE records. +include + + +// --------------------------------------------------------------------------- +// Centred results +// --------------------------------------------------------------------------- +/* + Everything is generated about whatever origin the profile found natural, + then shifted once so the finished envelope's bounding box is centred. The + shift is applied to the section, the straps and the cavities together, so + they stay registered with each other. +*/ + +function sb_centred(p, g) = + let( + shell = sb_p_shell(p), + shift = sb_centering_shift(shell) + ) + [ + move(shift, p = sb_clean_region(sb_section(shell, sb_p_members(p), g, sb_p_bore(p)))), + move(shift, p = sb_strap_region(sb_p_members(p), g)), + move(shift, p = sb_cavity_region(sb_p_members(p), g)), + move(shift, p = shell), + shift + ]; + +SB_C_SECTION = 0; +SB_C_STRAPS = 1; +SB_C_CAVITY = 2; +SB_C_SHELL = 3; +SB_C_SHIFT = 4; + +// Members translated by the same shift, for drawing individual laminae. +function sb_centred_members(p, shift) = + [for (m = sb_p_members(p)) + sb_member(m[SB_M_CX] + shift.x, m[SB_M_CY] + shift.y, + sb_mang(m), sb_mface(m))]; diff --git a/legacy/openscad/lib/sb-geom.scad b/legacy/openscad/lib/sb-geom.scad new file mode 100644 index 0000000..bc5b5c7 --- /dev/null +++ b/legacy/openscad/lib/sb-geom.scad @@ -0,0 +1,488 @@ +/* + sb-geom.scad — Strap-Beam shared geometry primitives + ==================================================== + Part of the Strap-Beam library. Nothing in this file knows how many + straps a profile has, so it is reused unchanged by the 3x, 4x and any + later N-strap generator. + + Two record types are defined here. + + GEO record + Everything about a single strap bundle and the PLA+ that wraps it. + Built once per render by sb_geo() and threaded through every call. + + MEMBER record + One strap bundle's cross-section placement: centre, angle, and which + broad face (if either) looks into an enclosed interior. + + Coordinate convention for a member + local +X = along the strap's WIDTH (the 15.875 mm direction) + local +Y = along the strap's THICKNESS (the 0.508 mm direction) + The member's angle rotates local +X onto the global direction given. + "normal+" is local +Y expressed globally. + + Requires BOSL2 (std.scad) to be included by the caller. +*/ + + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +SB_SQRT3 = sqrt(3); +SB_EPS = 1e-7; + +// Face modes — how a member's two broad faces are walled. +SB_FACE_PLUS_IN = 1; // local +Y faces an enclosed interior -> inside wall +SB_FACE_MINUS_IN = -1; // local -Y faces an enclosed interior -> inside wall +SB_FACE_BOTH_OUT = 0; // neither face encloses anything -> outside wall both + +// GEO field indices. +SB_G_WIDTH = 0; // nominal strap width +SB_G_STRAP_T = 1; // one strap's thickness +SB_G_COUNT = 2; // straps per bundle +SB_G_CLEAR = 3; // fit clearance, applied to every cavity face +SB_G_WIN = 4; // inside wall +SB_G_WOUT = 5; // outside wall +SB_G_WEDGE = 6; // edge wall (caps the strap's narrow edges) +SB_G_MINWALL = 7; // minimum acceptable PLA thickness anywhere + +// MEMBER field indices. +SB_M_CX = 0; +SB_M_CY = 1; +SB_M_ANG = 2; +SB_M_FACE = 3; + + +// --------------------------------------------------------------------------- +// GEO record +// --------------------------------------------------------------------------- + +function sb_geo( + strap_width, + strap_thickness, + bundle_count, + clearance, + wall_inside, + wall_outside, + wall_edge, + min_wall +) = [ + strap_width, strap_thickness, bundle_count, clearance, + wall_inside, wall_outside, wall_edge, min_wall +]; + +function sb_width(g) = g[SB_G_WIDTH]; +function sb_strap_t(g) = g[SB_G_STRAP_T]; +function sb_count(g) = g[SB_G_COUNT]; +function sb_clear(g) = g[SB_G_CLEAR]; +function sb_wall_in(g) = g[SB_G_WIN]; +function sb_wall_out(g) = g[SB_G_WOUT]; +function sb_wall_edge(g) = g[SB_G_WEDGE]; +function sb_min_wall(g) = g[SB_G_MINWALL]; + +function sb_bundle_t(g) = sb_count(g) * sb_strap_t(g); + +// Cavity = strap bundle grown by the fit clearance on all four faces. +function sb_cavity_w(g) = sb_width(g) + 2 * sb_clear(g); +function sb_cavity_t(g) = sb_bundle_t(g) + 2 * sb_clear(g); + +/* + A declared "web" is the PLA+ that must survive between two neighbouring + strap CAVITIES. Because every cavity is inflated by the clearance, the + corresponding gap between the physical STRAPS is larger. Callers state + the web they want; this converts to the strap-to-strap spacing that + produces it, so a declared 1.2 mm web really is 1.2 mm of plastic. +*/ +function sb_web_to_strap_gap(g, web) = web + 2 * sb_clear(g); + +// Distance from a member centreline out to each of its four sleeve faces. +function sb_reach_plus(g, face) = + sb_cavity_t(g) / 2 + (face > 0 ? sb_wall_in(g) : sb_wall_out(g)); + +function sb_reach_minus(g, face) = + sb_cavity_t(g) / 2 + (face < 0 ? sb_wall_in(g) : sb_wall_out(g)); + +// Distance from centreline to the enclosed-interior side of the sleeve. +// Only meaningful when the member actually has an interior face. +function sb_reach_inside(g) = sb_cavity_t(g) / 2 + sb_wall_in(g); + + +// --------------------------------------------------------------------------- +// Small vector helpers +// --------------------------------------------------------------------------- + +function sb_rot2(p, a) = [ + p.x * cos(a) - p.y * sin(a), + p.x * sin(a) + p.y * cos(a) +]; + +function sb_mid(a, b) = [(a.x + b.x) / 2, (a.y + b.y) / 2]; +function sb_dist(a, b) = norm([b.x - a.x, b.y - a.y]); +function sb_cross2(a, b) = a.x * b.y - a.y * b.x; + +function sb_centroid(pts) = [ + sum([for (p = pts) p.x]) / len(pts), + sum([for (p = pts) p.y]) / len(pts) +]; + +// Signed area; positive means counter-clockwise. +function sb_signed_area(path) = + sum([for (i = [0 : len(path) - 1]) + let(a = path[i], b = path[(i + 1) % len(path)]) + (a.x * b.y - b.x * a.y)]) / 2; + +function sb_ccw(path) = sb_signed_area(path) >= 0 ? path : reverse(path); + +// Intersection of line (p1,d1) with line (p2,d2). Returns undef if parallel. +function sb_line_isect(p1, d1, p2, d2) = + let(denom = sb_cross2(d1, d2)) + abs(denom) < SB_EPS + ? undef + : let( + delta = [p2.x - p1.x, p2.y - p1.y], + t = sb_cross2(delta, d2) / denom + ) + [p1.x + t * d1.x, p1.y + t * d1.y]; + + +// --------------------------------------------------------------------------- +// MEMBER record +// --------------------------------------------------------------------------- + +function sb_member(cx, cy, angle, face = SB_FACE_BOTH_OUT) = [cx, cy, angle, face]; + +function sb_mc(m) = [m[SB_M_CX], m[SB_M_CY]]; +function sb_mang(m) = m[SB_M_ANG]; +function sb_mface(m) = m[SB_M_FACE]; + +function sb_maxis(m) = [cos(sb_mang(m)), sin(sb_mang(m))]; // along width +function sb_mnormal(m) = [-sin(sb_mang(m)), cos(sb_mang(m))]; // local +Y + +// Unit vector pointing from the member towards the profile interior. +// Returns undef for SB_FACE_BOTH_OUT, which has no interior. +function sb_minside_dir(m) = + let(n = sb_mnormal(m), f = sb_mface(m)) + f == 0 ? undef : [f * n.x, f * n.y]; + +// A point on the interior-facing surface of the member's sleeve. +function sb_minside_wall_pt(m, g) = + let(d = sb_minside_dir(m), c = sb_mc(m)) + is_undef(d) ? undef + : [c.x + d.x * sb_reach_inside(g), + c.y + d.y * sb_reach_inside(g)]; + +/* + Decide the face mode from a target point that lies inside the profile. + Pass interior_target = undef for members with no enclosed side, which + keeps the member symmetric and stops the walls from becoming chiral. +*/ +function sb_face_toward(centre, angle, interior_target) = + is_undef(interior_target) ? SB_FACE_BOTH_OUT + : let( + n = [-sin(angle), cos(angle)], + v = [interior_target.x - centre.x, interior_target.y - centre.y] + ) + (n.x * v.x + n.y * v.y) >= 0 ? SB_FACE_PLUS_IN : SB_FACE_MINUS_IN; + +// Member lying on the segment a->b, optionally slid along its own axis. +function sb_member_on_edge(a, b, interior_target, shift = 0) = + let( + mid = sb_mid(a, b), + angle = atan2(b.y - a.y, b.x - a.x), + c = [mid.x + shift * cos(angle), mid.y + shift * sin(angle)], + face = sb_face_toward(c, angle, interior_target) + ) + sb_member(c.x, c.y, angle, face); + +// Member placed radially: centre sits at distance r from origin along angle a, +// with its width axis pointing outward. Used by spoke profiles. +function sb_member_radial(r, angle, face = SB_FACE_BOTH_OUT) = + sb_member(r * cos(angle), r * sin(angle), angle, face); + +// Translate a member along its outward normal (away from the interior). +function sb_member_offset_out(m, d) = + let( + n = sb_mnormal(m), + s = sb_mface(m) == 0 ? 1 : -sb_mface(m) + ) + sb_member(m[SB_M_CX] + s * d * n.x, + m[SB_M_CY] + s * d * n.y, + sb_mang(m), sb_mface(m)); + + +// --------------------------------------------------------------------------- +// Cross-section paths for one member +// --------------------------------------------------------------------------- + +// Place a locally-defined path into the member's frame. +function sb_place(m, path) = + move(sb_mc(m), p = zrot(sb_mang(m), p = path)); + +/* + Rectangle in member-local coordinates. + half_w_lead : extent along +X (towards the member's leading end) + half_w_trail : extent along -X + up / down : extents along +Y / -Y +*/ +function sb_local_rect(half_w_lead, half_w_trail, up, down) = [ + [ half_w_lead, -down], + [ half_w_lead, up ], + [-half_w_trail, up ], + [-half_w_trail, -down] +]; + +// The physical strap bundle, as one rectangle. +function sb_strap_path(m, g) = + sb_place(m, sb_local_rect(sb_width(g) / 2, sb_width(g) / 2, + sb_bundle_t(g) / 2, sb_bundle_t(g) / 2)); + +// Individual strap laminae, for display when bundle_count > 1. +function sb_strap_layer_paths(m, g) = [ + for (i = [0 : sb_count(g) - 1]) + let( + y = (i - (sb_count(g) - 1) / 2) * sb_strap_t(g), + t = sb_strap_t(g) / 2 + ) + sb_place(m, move([0, y], p = sb_local_rect(sb_width(g) / 2, + sb_width(g) / 2, t, t))) +]; + +// The void the strap slides through. +function sb_cavity_path(m, g) = + sb_place(m, sb_local_rect(sb_cavity_w(g) / 2, sb_cavity_w(g) / 2, + sb_cavity_t(g) / 2, sb_cavity_t(g) / 2)); + +/* + The PLA+ sleeve around one member. + + ext_lead / ext_trail extend the sleeve along its own axis beyond the + default edge wall. Junction construction uses this to make neighbouring + sleeves genuinely overlap instead of merely touching at a corner. +*/ +function sb_sleeve_path(m, g, ext_lead = 0, ext_trail = 0) = + let( + half = sb_cavity_w(g) / 2 + sb_wall_edge(g), + f = sb_mface(m) + ) + sb_place(m, sb_local_rect(half + ext_lead, half + ext_trail, + sb_reach_plus(g, f), sb_reach_minus(g, f))); + +/* + Sleeve whose trailing end is cut by an arbitrary line rather than by a + face perpendicular to the axis. This produces a butt joint flush against + a neighbouring member's outer face, which is how junctions are made + structural rather than decorative. + + line_pt / line_dir describe the cutting line. The trailing end face is + placed on that line; the leading end stays perpendicular as usual. + Falls back to a plain sleeve if the line is parallel to the axis. +*/ +function sb_sleeve_to_line(m, g, line_pt, line_dir, ext_lead = 0) = + let( + c = sb_mc(m), + u = sb_maxis(m), + n = sb_mnormal(m), + f = sb_mface(m), + up = sb_reach_plus(g, f), + dn = sb_reach_minus(g, f), + half = sb_cavity_w(g) / 2 + sb_wall_edge(g), + // The two long edges of the sleeve, as lines. + p_up = [c.x + n.x * up, c.y + n.y * up], + p_dn = [c.x - n.x * dn, c.y - n.y * dn], + t_up = sb_line_isect(p_up, u, line_pt, line_dir), + t_dn = sb_line_isect(p_dn, u, line_pt, line_dir), + lead_up = [p_up.x + u.x * (half + ext_lead), + p_up.y + u.y * (half + ext_lead)], + lead_dn = [p_dn.x + u.x * (half + ext_lead), + p_dn.y + u.y * (half + ext_lead)] + ) + (is_undef(t_up) || is_undef(t_dn)) + ? sb_sleeve_path(m, g, ext_lead, 0) + : sb_ccw([lead_dn, lead_up, t_up, t_dn]); + + +/* + Sleeve cut by a line at BOTH ends. A member that spans between two + neighbours - a gable crossbar, a chord across a polygon - butts flush + against each of them instead of stopping short or poking through. +*/ +function sb_sleeve_span(m, g, pt_a, dir_a, pt_b, dir_b) = + let( + c = sb_mc(m), + u = sb_maxis(m), + n = sb_mnormal(m), + f = sb_mface(m), + p_up = [c.x + n.x * sb_reach_plus(g, f), c.y + n.y * sb_reach_plus(g, f)], + p_dn = [c.x - n.x * sb_reach_minus(g, f), c.y - n.y * sb_reach_minus(g, f)], + a_up = sb_line_isect(p_up, u, pt_a, dir_a), + a_dn = sb_line_isect(p_dn, u, pt_a, dir_a), + b_up = sb_line_isect(p_up, u, pt_b, dir_b), + b_dn = sb_line_isect(p_dn, u, pt_b, dir_b) + ) + (is_undef(a_up) || is_undef(a_dn) || is_undef(b_up) || is_undef(b_dn)) + ? sb_sleeve_path(m, g) + : sb_ccw([a_dn, a_up, b_up, b_dn]); + + +// --------------------------------------------------------------------------- +// Measurement — exact distance between two closed polylines +// --------------------------------------------------------------------------- +/* + The minimum distance between two disjoint polygons is always attained at + a vertex of one of them, so sampling every vertex against every segment of + the other (both ways round) is exact, not an approximation. +*/ + +function sb_pt_seg_dist(p, a, b) = + let( + ab = [b.x - a.x, b.y - a.y], + L2 = ab.x * ab.x + ab.y * ab.y + ) + L2 < SB_EPS + ? sb_dist(p, a) + : let( + t = max(0, min(1, ((p.x - a.x) * ab.x + (p.y - a.y) * ab.y) / L2)) + ) + sb_dist(p, [a.x + t * ab.x, a.y + t * ab.y]); + +function sb_pt_path_dist(p, path) = + min([for (i = [0 : len(path) - 1]) + sb_pt_seg_dist(p, path[i], path[(i + 1) % len(path)])]); + +// Do two segments properly cross or touch? +function sb_segs_cross(a1, a2, b1, b2) = + let( + d1 = [a2.x - a1.x, a2.y - a1.y], + d2 = [b2.x - b1.x, b2.y - b1.y], + den = sb_cross2(d1, d2), + w = [b1.x - a1.x, b1.y - a1.y] + ) + abs(den) < SB_EPS + ? false + : let(t = sb_cross2(w, d2) / den, u = sb_cross2(w, d1) / den) + t >= 0 && t <= 1 && u >= 0 && u <= 1; + +function sb_paths_cross(p, q) = + len([for (i = [0 : len(p) - 1], j = [0 : len(q) - 1]) + if (sb_segs_cross(p[i], p[(i + 1) % len(p)], + q[j], q[(j + 1) % len(q)])) 1]) > 0; + +/* + Minimum distance between two closed paths. + + Two disjoint polygons always attain their minimum at a vertex of one of + them, so vertex-against-segment both ways round is exact. Two polygons + that CROSS may have no vertex near the other's boundary at all, and the + naive vertex test then reports a comfortable clearance across an outright + overlap - which is exactly the kind of false pass that lets a solver + settle on a degenerate arrangement. Crossing is therefore tested first + and reported as zero. + + Nesting is deliberately not treated as overlap: a hole inside an outer + boundary is the normal case, and the distance between them is the wall + thickness that this whole library exists to measure. +*/ +function sb_path_gap(p, q) = + sb_paths_cross(p, q) + ? 0 + : min(min([for (v = p) sb_pt_path_dist(v, q)]), + min([for (v = q) sb_pt_path_dist(v, p)])); + +// Minimum distance between any two paths in a region. For a finished +// cross-section this is the thinnest surviving piece of PLA+. +function sb_region_min_gap(rgn) = + len(rgn) < 2 + ? 1e9 + : min([for (i = [0 : len(rgn) - 2], j = [i + 1 : len(rgn) - 1]) + sb_path_gap(rgn[i], rgn[j])]); + + +/* + Largest corner radius a path can physically accept: at every vertex the + roundover's tangent points must stay on their own edges. Probing this by + trial is not an option because the rounding routine raises a library-level + error rather than returning a flag, so it is derived up front. +*/ +function sb_corner_radii(path, r) = + let(n = len(path), cw = sb_signed_area(path) < 0) + [for (i = [0 : n - 1]) + let( + prev = path[(i + n - 1) % n], + here = path[i], + next = path[(i + 1) % n], + turn = sb_cross2([here.x - prev.x, here.y - prev.y], + [next.x - here.x, next.y - here.y]), + reflex = cw ? (turn > SB_EPS) : (turn < -SB_EPS), + ang = vector_angle(prev, here, next), + fits = (ang <= 0.05 || ang >= 179.95) + ? 0 + : 0.98 * min(sb_dist(prev, here), sb_dist(here, next)) + / 2 * tan(ang / 2) + ) + reflex ? min(r, fits) : 0]; + + +function sb_path_max_round(path) = + let(n = len(path)) + n < 3 ? 0 : + 0.999 * min([for (i = [0 : n - 1]) + let( + prev = path[(i + n - 1) % n], + here = path[i], + next = path[(i + 1) % n], + l1 = sb_dist(prev, here), + l2 = sb_dist(here, next), + ang = vector_angle(prev, here, next) + ) + (ang <= 0.05 || ang >= 179.95) ? 1e9 : min(l1, l2) / 2 * tan(ang / 2) + ]); + + +// --------------------------------------------------------------------------- +// Empty-safe wrappers +// --------------------------------------------------------------------------- +// BOSL2's boolean functions return a bare [] when a result is empty, which is +// not a valid region. Every measurement goes through these so a legitimately +// empty result reads as zero instead of raising a library error. + +/* + Remove duplicate and collinear vertices from every path in a region. + + Exact butt joints and zero-radius fillets produce coincident or perfectly + collinear vertices. They are harmless in 2D but leave zero-area triangles + that the tessellator cannot resolve, so a section that measures perfectly + can still fail to extrude. Cleaning once, at the end, removes that entire + class of failure. +*/ +function sb_clean_region(rgn) = [ + for (path = rgn) + let(d = deduplicate(path, closed = true)) + if (len(d) >= 3) + let(m = path_merge_collinear(d, closed = true)) + if (len(m) >= 3) m +]; + +function sb_area(rgn) = len(rgn) == 0 ? 0 : region_area(rgn); +function sb_nparts(rgn) = len(rgn) == 0 ? 0 : len(region_parts(rgn)); +function sb_as_region(x) = is_path(x) ? [x] : x; + + +// --------------------------------------------------------------------------- +// Monotone solver +// --------------------------------------------------------------------------- +/* + Several profiles need "place this member so that the resulting web is + exactly W". Rather than deriving a closed form per profile — the source + of most of the wrong-by-a-cosine errors in earlier revisions — solve the + real measured quantity numerically. f must be non-decreasing on [lo,hi]. +*/ +function sb_solve(f, lo, hi, target, iters = 44) = + iters <= 0 + ? (lo + hi) / 2 + : let(mid = (lo + hi) / 2) + f(mid) < target ? sb_solve(f, mid, hi, target, iters - 1) + : sb_solve(f, lo, mid, target, iters - 1); + +// True when f(hi) actually reaches the target, i.e. the solve is feasible. +function sb_solvable(f, hi, target) = f(hi) >= target; diff --git a/legacy/openscad/lib/sb-join.scad b/legacy/openscad/lib/sb-join.scad new file mode 100644 index 0000000..e77051a --- /dev/null +++ b/legacy/openscad/lib/sb-join.scad @@ -0,0 +1,311 @@ +/* + sb-join.scad — Strap-Beam shared junction and envelope strategies + ================================================================= + Part of the Strap-Beam library. Everything here is written for N members + and is shared verbatim by the 3x, 4x and later generators. + + Three families of strategy live here. + + ENVELOPE how the outer PLA+ solid is generated: + * sleeve style — union of per-member sleeves (open profiles) + * ring style — one closed envelope offset from a centreline + polygon, with solid rounded corners + BORE the enclosed central void, derived from the members' actual + inside-wall lines rather than from a separately scaled shape + FIT placement solved against a measured web, so a declared wall + thickness is the wall thickness you get + + Requires sb-geom.scad and BOSL2. +*/ + + +// --------------------------------------------------------------------------- +// Face lines +// --------------------------------------------------------------------------- +/* + A member's sleeve has two long surfaces. Junction construction needs to + talk about them as infinite lines. side = +1 selects the local +Y + surface, side = -1 the local -Y surface. Returns [point, direction]. +*/ +function sb_face_line(m, g, side) = + let( + c = sb_mc(m), + n = sb_mnormal(m), + d = side > 0 ? sb_reach_plus(g, sb_mface(m)) + : sb_reach_minus(g, sb_mface(m)) + ) + [[c.x + side * d * n.x, c.y + side * d * n.y], sb_maxis(m)]; + +// The surface of m that lies farther from the given point. Used to butt one +// member flush against the far side of another without needing to know which +// way either of them is facing. +function sb_far_face_line(m, g, from_pt) = + let( + a = sb_face_line(m, g, 1), + b = sb_face_line(m, g, -1) + ) + sb_dist(a[0], from_pt) >= sb_dist(b[0], from_pt) ? a : b; + + +// --------------------------------------------------------------------------- +// Structural junctions +// --------------------------------------------------------------------------- +/* + sb_sleeve_butt + + The core junction primitive. Rather than letting two sleeves clip each + other at a corner and relying on a cosmetic fillet to hold the result + together, the trailing end of `m` is run all the way through `into` and + cut off flush with that member's far surface. The two sleeves then share + a full-width overlap, so the joint carries load whether or not a fillet is + applied afterwards. + + ext_lead extends the opposite (free) end, which is left untouched. +*/ +function sb_sleeve_butt(m, g, into, ext_lead = 0) = + let( + line = sb_far_face_line(into, g, sb_mc(m)) + ) + sb_sleeve_to_line(m, g, line[0], line[1], ext_lead); + +/* + sb_hull_cap + + Plugs the space enclosed by a set of member end faces with their convex + hull. Deterministic, cheap, and free of the spikes and V-notches that a + bare union of crossing rectangles leaves behind. Used for spoke-style + centres and for gable apexes. + + Pass the end-face segments (two points each); the hull of all of them is + the plug. +*/ +function sb_hull_cap(segments) = + let(pts = [for (s = segments) each s]) + len(pts) < 3 ? [] : hull_region([pts]); + +// The end-face segment of a member, at its leading (+1) or trailing (-1) end, +// taken at the sleeve surface. extra pushes the face further along the axis. +function sb_end_face(m, g, end = 1, extra = 0) = + let( + c = sb_mc(m), + u = sb_maxis(m), + n = sb_mnormal(m), + f = sb_mface(m), + half = sb_cavity_w(g) / 2 + sb_wall_edge(g) + extra, + p = [c.x + end * half * u.x, c.y + end * half * u.y], + up = sb_reach_plus(g, f), + dn = sb_reach_minus(g, f) + ) + [[p.x + n.x * up, p.y + n.y * up], + [p.x - n.x * dn, p.y - n.y * dn]]; + +/* + sb_fillet_concave + + Rounds only the reflex corners of a path, leaving every convex corner + bit-exact. Each radius is clamped to what its own corner can accept, so + the operation cannot fail on a tight junction. + + This replaces the morphological closing (grow by r, shrink by r) used in + earlier revisions. Closing had three problems: an inward offset on a + many-vertex path is the least reliable operation in the pipeline and + raises a library-level error rather than reporting one; the arc + discretisation it introduces is not mirror-symmetric, so it quietly made + symmetric profiles chiral; and it filled every concavity within reach + rather than the junction actually being treated. Rounding named corners + has none of those failure modes and is considerably faster. +*/ +function sb_fillet_concave(path, r) = + len(path) < 3 ? path + : let(radii = sb_corner_radii(path, r)) + max(radii) <= 1e-6 ? path + : round_corners(path, radius = radii, closed = true); + +// Fillet the junction between two sleeves. Cosmetic only: it is applied on +// top of a structural butt joint, never in place of one. If the two solids +// do not merge into a single simple outline there is nothing sane to round, +// so the pair is returned untouched rather than guessed at. +function sb_fillet_pair(path_a, path_b, r) = + r <= 0 ? [path_a, path_b] + : let(u = sb_as_region(union([[path_a], [path_b]]))) + len(u) == 1 ? [sb_fillet_concave(u[0], r)] : u; + + +// --------------------------------------------------------------------------- +// Bore — the enclosed central void +// --------------------------------------------------------------------------- +/* + sb_bore_from_members + + Members must be supplied in cyclic order around the interior, each with a + real interior face. The bore is the polygon bounded by their actual + inside-wall surfaces, so the declared inside wall is exactly what remains + between each cavity and the void. Nothing here is specific to three + members; a four-sided profile produces a quadrilateral bore from the same + call. + + Returns [] when any pair of consecutive inside lines is parallel, which + means the profile has no closed interior. +*/ +function sb_bore_from_members(ms, g) = + len([for (m = ms) if (sb_mface(m) == 0) 1]) > 0 ? [] + : let( + n = len(ms), + pts = [for (m = ms) sb_minside_wall_pt(m, g)], + dirs = [for (m = ms) sb_maxis(m)], + verts = [for (i = [0 : n - 1]) + sb_line_isect(pts[(i + n - 1) % n], dirs[(i + n - 1) % n], + pts[i], dirs[i])] + ) + (len([for (v = verts) if (is_undef(v)) 1]) > 0) ? [] : verts; + +/* + Is the derived bore real? A collapsed interior does not vanish, it turns + itself inside out, so area alone proves nothing. The test that matters is + that the bore's own centre still lies on the interior side of every + member's inside wall. +*/ +function sb_bore_valid(bore, ms, g) = + len(bore) < 3 ? false + : abs(sb_signed_area(bore)) <= 0.01 ? false + : let(c = sb_centroid(bore)) + len([for (m = ms) + let(d = sb_minside_dir(m), p = sb_minside_wall_pt(m, g)) + if (is_undef(d) || + (d.x * (c.x - p.x) + d.y * (c.y - p.y)) <= 0.01) 1]) == 0; + + +// --------------------------------------------------------------------------- +// Ring profiles — members laid along the edges of a closed polygon +// --------------------------------------------------------------------------- + +// One member per edge, each centred on its edge, interior face towards the +// polygon centroid. Centring keeps the profile mirror-symmetric; the corner +// webs are then set by the polygon's size, solved for below. +function sb_ring_members(path, g) = + let(c = sb_centroid(path), n = len(path)) + [for (i = [0 : n - 1]) sb_member_on_edge(path[i], path[(i + 1) % n], c)]; + +// Smallest PLA+ web between any two neighbouring strap cavities on the ring. +function sb_ring_web(path, g) = + let( + ms = sb_ring_members(path, g), + n = len(ms), + cv = [for (m = ms) sb_cavity_path(m, g)] + ) + min([for (i = [0 : n - 1]) sb_path_gap(cv[i], cv[(i + 1) % n])]); + +function sb_scale_about_centroid(path, k) = + let(c = sb_centroid(path)) + [for (p = path) [c.x + k * (p.x - c.x), c.y + k * (p.y - c.y)]]; + +/* + sb_fit_ring + + Straps have a fixed width, so on a polygon of a given size the corner webs + are whatever they are — they cannot be dialled in by sliding members along + their edges, because every edge shares its budget with two corners. The + only free variable that raises all N webs at once is the polygon's size. + + This grows the caller's polygon about its centroid, preserving its shape + and proportions exactly, until the tightest corner web reaches `web`. + The same call fits a triangle, a quadrilateral, or any N-gon. +*/ +function sb_ring_fit_scale(path, g, web, max_scale = 12) = + let( + n = len(path), + edges = [for (i = [0 : n - 1]) sb_dist(path[i], path[(i + 1) % n])], + // Normalise first, so the caller's outline really is shape-only: a + // unit square and a 200 mm square must fit to the same result. At + // relative scale 1 the shortest edge is exactly one strap wide. + k0 = sb_width(g) / min(edges), + f = function(k) sb_ring_web(sb_scale_about_centroid(path, k0 * k), g) + ) + !sb_solvable(f, max_scale, web) + ? undef + // The search starts at 1, never below. Once an edge is shorter than + // a strap, that member overhangs both of its own corners and the + // corner-setback model no longer describes the geometry - yet the + // measured web can come back positive there, which is exactly the + // kind of spurious lower branch a bisection will happily settle on. + : k0 * sb_solve(f, 1, max_scale, web); + +function sb_fit_ring(path, g, web, max_scale = 12) = + let(k = sb_ring_fit_scale(path, g, web, max_scale)) + is_undef(k) ? undef : sb_scale_about_centroid(path, k); + +/* + Outer envelope of a ring profile: the centreline polygon pushed out to the + outside-wall surface, with its corners rounded. Corner rounding removes + material from precisely the region where a strap cavity approaches the + corner, so the caller must check the result against the minimum wall + rather than assume a radius is safe. +*/ +function sb_ring_shell(path, g, corner_r = 0) = + let( + sharp = offset(sb_ccw(path), + delta = sb_cavity_t(g) / 2 + sb_wall_out(g), + closed = true) + ) + corner_r > 0 ? round_corners(sharp, radius = corner_r, closed = true) + : sharp; + +// Largest corner radius that still leaves min_wall between the envelope and +// every strap cavity, and that the envelope can geometrically accept. +// Reported so a catalogue entry can be tuned once and then trusted. +function sb_ring_max_corner_r(path, g) = + let( + sharp = sb_ring_shell(path, g, 0), + hi = sb_path_max_round(sharp), + ms = sb_ring_members(path, g), + cav = [for (m = ms) sb_cavity_path(m, g)], + f = function(r) + let(sh = sb_ring_shell(path, g, max(0, hi - r))) + len(sh) < 3 ? 0 : min([for (c = cav) sb_path_gap(sh, c)]) + ) + hi <= 0 ? 0 : max(0, hi - sb_solve(f, 0, hi, sb_min_wall(g) - 1e-6)); + + +// --------------------------------------------------------------------------- +// Sleeve profiles — union of per-member sleeves +// --------------------------------------------------------------------------- + +function sb_sleeve_shell(paths) = sb_as_region(union([for (p = paths) [p]])); + +// Apply one cosmetic fillet per declared junction, each computed from only +// the two members involved, then merge with the untouched shell. Keeping the +// closings pairwise stops distant parts of the profile from bridging to each +// other through the middle of the section. +function sb_fillet_junctions(shell, pairs, paths, r) = + r <= 0 ? shell + : sb_as_region(union(concat([shell], + [for (p = pairs) sb_fillet_pair(paths[p[0]], paths[p[1]], r)]))); + + +// --------------------------------------------------------------------------- +// Assembly +// --------------------------------------------------------------------------- +/* + sb_section + + The one place where solid and void meet. All sleeve solids are unioned + first and every cavity is removed afterwards, so no member's PLA+ can ever + intrude into another member's strap channel. +*/ +function sb_section(shell, members, g, bore = []) = + let( + cavities = sb_as_region(union([for (m = members) [sb_cavity_path(m, g)]])), + cut = sb_as_region(len(bore) >= 3 ? union([[bore], cavities]) : cavities) + ) + sb_as_region(difference(shell, cut)); + +function sb_strap_region(members, g) = + sb_as_region(union([for (m = members) [sb_strap_path(m, g)]])); + +function sb_cavity_region(members, g) = + sb_as_region(union([for (m = members) [sb_cavity_path(m, g)]])); + +// Translation that puts the finished envelope's bounding box on the origin. +function sb_centering_shift(shell) = + let(b = pointlist_bounds(hull_region(shell))) + [-(b[0].x + b[1].x) / 2, -(b[0].y + b[1].y) / 2]; diff --git a/legacy/openscad/lib/sb-profiles.scad b/legacy/openscad/lib/sb-profiles.scad new file mode 100644 index 0000000..f24949e --- /dev/null +++ b/legacy/openscad/lib/sb-profiles.scad @@ -0,0 +1,221 @@ +/* + sb-profiles.scad — Strap-Beam shared profile constructions + ========================================================== + Part of the Strap-Beam library. Three complete arrangements, each written + for N members and each returning a finished PROFILE record. + + sb_ring_polygon_profile members on the edges of a closed polygon, + wrapped in one envelope with solid rounded + corners and an enclosed bore + sb_spoke_profile N members radiating from a plugged centre + sb_fin_profile N members lying tangentially on the sides of + a regular core polygon, slid cyclically so + each overhangs one corner + + A family generator supplies N and the parameters; nothing below changes + between the 3x and 4x files. strap-beam-3x.scad calls all three + (Triangles, Y, Three-Fin) and strap-beam-4x.scad calls the same three + (Quadrilaterals, Cross, Four-Fin) with N = 4. + + Requires sb-geom.scad, sb-join.scad and sb-report.scad. +*/ + + +// --------------------------------------------------------------------------- +// Ring: members on the edges of a closed polygon +// --------------------------------------------------------------------------- +/* + Straps have a fixed width, so on a polygon of a given size the corner webs + are whatever they are - sliding members along their edges cannot raise all + N at once, because every edge shares its budget with two corners. The one + free variable that lifts them together is the polygon's size, so the + caller's outline is treated as a SHAPE and grown about its centroid until + the tightest corner reaches `web`. + + Members are centred on their edges, which keeps the result mirror- + symmetric whenever the outline is. +*/ +function sb_ring_polygon_profile(seed_path, g, web, corner_r, label) = + abs(sb_signed_area(seed_path)) < 1e-6 + ? sb_profile_failed(str(label, ": the supplied outline is degenerate.")) + : let( + k = sb_ring_fit_scale(seed_path, g, web), + path = is_undef(k) ? undef : sb_scale_about_centroid(seed_path, k) + ) + is_undef(path) + ? sb_profile_failed(str(label, ": no polygon size gives a ", web, + " mm corner web. Reduce the web, the wall thicknesses, or the strap width.")) + : let( + ms = sb_ring_members(path, g), + max_r = sb_ring_max_corner_r(path, g), + // Build with a radius the envelope can actually accept; if the + // caller asked for more, the check below reports it rather than + // letting the rounding routine fail with a library error. + shell = [sb_ring_shell(path, g, min(corner_r, max_r))], + bore = sb_bore_from_members(ms, g), + edges = [for (i = [0 : len(path) - 1]) + sb_dist(path[i], path[(i + 1) % len(path)])] + ) + sb_profile(ms, shell, bore, + [ + sb_check(len(shell[0]) >= 3, + str(label, ": the outer envelope collapsed.")), + sb_check(sb_bore_valid(bore, ms, g), + str(label, ": the central bore has collapsed. Reduce inside_wall_thickness_mm or the bundle thickness.")), + sb_check(corner_r <= max_r + 1e-6, + str(label, ": corner radius of ", corner_r, + " mm is not usable here - it would cut the outer wall below ", + sb_min_wall(g), " mm at the corners, or exceed what the envelope can accept. Maximum is ", max_r, " mm.")) + ], + [ + sb_kv("RING_SCALE", k), + sb_kv("RING_EDGES_MM", edges), + sb_kv("RING_CORNER_WEB_MM", sb_ring_web(path, g)), + sb_kv("RING_CORNER_R_MAX_MM", max_r) + ]); + + +// --------------------------------------------------------------------------- +// Spokes: N members radiating from a common centre +// --------------------------------------------------------------------------- +/* + A spoke's two broad faces both look at open air, so both take the outside + wall. There is no interior to face and therefore no bore; forcing an + interior direction on these members is what made earlier revisions chiral + under asymmetric wall settings. + + The centre is plugged with the convex hull of the N inner end faces rather + than left as whatever the crossing rectangles happened to produce. The + spoke radius is solved against the measured web between neighbours. +*/ +function sb_spoke_members(n, radius, rotation) = [ + for (i = [0 : n - 1]) + sb_member_radial(radius, rotation + 360 * i / n, SB_FACE_BOTH_OUT) +]; + +function sb_spoke_web(n, radius, rotation, g) = + let(cv = [for (m = sb_spoke_members(n, radius, rotation)) + sb_cavity_path(m, g)]) + min([for (i = [0 : n - 1]) sb_path_gap(cv[i], cv[(i + 1) % n])]); + +function sb_spoke_profile(n, rotation, web, fillet_r, g, label) = + let( + W = sb_width(g), + f = function(r) sb_spoke_web(n, r, rotation, g), + hi = 6 * W + ) + !sb_solvable(f, hi, web) + ? sb_profile_failed(str(label, ": cannot open a ", web, + " mm web between neighbouring spokes. Reduce the web or the wall thicknesses.")) + : let( + radius = sb_solve(f, W / 2, hi, web), + ms = sb_spoke_members(n, radius, rotation), + paths = [for (m = ms) sb_sleeve_path(m, g)], + cap = sb_hull_cap([for (m = ms) sb_end_face(m, g, -1)]), + raw = union(concat([for (p = paths) [p]], + len(cap) > 0 ? [cap] : [])), + pairs = [for (i = [0 : n - 1]) [i, (i + 1) % n]], + shell = sb_fillet_junctions(raw, pairs, paths, fillet_r) + ) + sb_profile(ms, shell, [], + [ + sb_check(fillet_r > 0, + str(label, ": the junction fillet radius must be greater than zero. A hull-plugged centre with no fillet meets the spokes along an exactly tangent boundary, which is a valid outline but cannot be tessellated.")) + ], + [ + sb_kv("SPOKE_RADIUS_MM", radius), + sb_kv("SPOKE_WEB_MM", sb_spoke_web(n, radius, rotation, g)), + sb_kv("NOTE", "inside wall unused: no enclosed bore") + ]); + + +// --------------------------------------------------------------------------- +// Fins: N members lying tangentially on a regular core polygon +// --------------------------------------------------------------------------- +/* + Tangential, not radial. Each member lies along one side of a regular core + polygon and is slid cyclically along that side, so it stops short of the + corner behind it and overhangs the corner ahead of it. Those N overhangs + are the fins. + + spokes N members leaving a common centre, ends pointing outward + fins N members wrapping a core, each with one cyclic overhang + + The core size is not a free parameter. It is solved so the cyclic + junctions carry the declared web AND the bore reaches its declared + minimum, whichever demands more; the fin projection is then exact because + it is measured against that same solved polygon. + + Each member's trailing end is run through the member behind it and cut off + flush on its far face, so every junction has a full-width overlap and the + fillet that follows is cosmetic. Junctions that merely touch at a corner + and rely on a fillet to bridge them are not load paths. +*/ +function sb_regular_polygon(n, side, rotation = 0) = + let(R = side / (2 * sin(180 / n))) + [for (k = [0 : n - 1]) + let(a = -90 + 180 / n + 360 * k / n + rotation) + [R * cos(a), R * sin(a)]]; + +function sb_fin_members(n, side, fin, rotation, g) = + let( + path = sb_regular_polygon(n, side, rotation), + c = sb_centroid(path), + shift = fin + (side - sb_width(g)) / 2 + ) + [for (i = [0 : n - 1]) + sb_member_on_edge(path[i], path[(i + 1) % n], c, shift)]; + +function sb_fin_web(n, side, fin, rotation, g) = + let(cv = [for (m = sb_fin_members(n, side, fin, rotation, g)) + sb_cavity_path(m, g)]) + min([for (i = [0 : n - 1]) sb_path_gap(cv[i], cv[(i + n - 1) % n])]); + +// Side length of the bore left by N inside walls around a regular core. +function sb_fin_bore_side(n, side, g) = + let(t = tan(180 / n)) + 2 * t * (side / (2 * t) - sb_reach_inside(g)); + +function sb_fin_profile(n, fin, web, bore_side, rotation, fillet_r, g, label) = + let( + W = sb_width(g), + fweb = function(s) sb_fin_web(n, s, fin, rotation, g), + fbor = function(s) sb_fin_bore_side(n, s, g), + hi = 10 * W + ) + !sb_solvable(fweb, hi, web) + ? sb_profile_failed(str(label, ": cannot open a ", web, + " mm junction web. Reduce the web or the wall thicknesses.")) + : !sb_solvable(fbor, hi, bore_side) + ? sb_profile_failed(str(label, ": cannot reach a ", bore_side, + " mm bore. Reduce the requested bore.")) + : let( + side = max(sb_solve(fweb, 0.1, hi, web), + sb_solve(fbor, 0.1, hi, bore_side)), + ms = sb_fin_members(n, side, fin, rotation, g), + // Member i butts through member i-1, the one whose fin crosses the + // corner that member i stops short of. + paths = [for (i = [0 : n - 1]) + sb_sleeve_butt(ms[i], g, ms[(i + n - 1) % n])], + pairs = [for (i = [0 : n - 1]) [(i + n - 1) % n, i]], + shell = sb_fillet_junctions(sb_sleeve_shell(paths), pairs, paths, fillet_r), + bore = sb_bore_from_members(ms, g), + setback = fin + side - W + ) + sb_profile(ms, shell, bore, + [ + sb_check(sb_bore_valid(bore, ms, g), + str(label, ": the central bore has collapsed. Raise the requested bore or reduce inside_wall_thickness_mm.")), + sb_check(setback > 0, + str(label, ": the solved trailing setback is ", setback, + " mm, so the members overlap instead of stepping cyclically. Increase the fin projection.")), + sb_check(fillet_r > 0, + str(label, ": the junction fillet radius must be greater than zero. A butt joint with no fillet meets its neighbour along an exactly tangent boundary, which is a valid outline but cannot be tessellated.")) + ], + [ + sb_kv("FIN_CORE_SIDE_MM", side), + sb_kv("FIN_PROJECTION_MM", fin), + sb_kv("FIN_SETBACK_MM", setback), + sb_kv("FIN_BORE_SIDE_MM", sb_fin_bore_side(n, side, g)), + sb_kv("FIN_JUNCTION_WEB_MM", sb_fin_web(n, side, fin, rotation, g)) + ]); diff --git a/legacy/openscad/lib/sb-report.scad b/legacy/openscad/lib/sb-report.scad new file mode 100644 index 0000000..a0c6cbd --- /dev/null +++ b/legacy/openscad/lib/sb-report.scad @@ -0,0 +1,175 @@ +/* + sb-report.scad — Strap-Beam shared validation and reporting + =========================================================== + Part of the Strap-Beam library. + + VALIDATION + Checks are values, not statements. A profile builder returns a list + of [condition, message] pairs and the core asserts over that list. + Because the list is built inside the selected profile's own function, + no other profile's parameters are ever touched — a slider that belongs + to one catalogue entry cannot break a different one. + + REPORTING + Every render emits a block of SB_KEY=value lines on stderr. These are + stable, flat, and trivially scraped by the catalogue front end, which + needs dimensions and a pass/fail without parsing geometry. + + Requires sb-geom.scad, sb-join.scad and BOSL2. +*/ + + +// --------------------------------------------------------------------------- +// Checks +// --------------------------------------------------------------------------- + +function sb_check(condition, message) = [condition ? true : false, message]; + +function sb_first_failure(checks, i = 0) = + i >= len(checks) ? undef + : checks[i][0] ? sb_first_failure(checks, i + 1) + : checks[i][1]; + +/* + Assert the whole list and return a status string. Assign the result to a + variable at file scope so the assertion runs before any geometry does. +*/ +function sb_require(checks) = + let(fail = sb_first_failure(checks)) + assert(is_undef(fail), str("\n[strap-beam] ", fail, "\n")) + "ok"; + + +// --------------------------------------------------------------------------- +// Metrics +// --------------------------------------------------------------------------- + +SB_R_AREA = 0; +SB_R_PARTS = 1; +SB_R_SLOTS = 2; +SB_R_MINW = 3; +SB_R_SIZE = 4; +SB_R_LEAK = 5; + +function sb_metrics(section, shell, members, g) = + let( + cav = sb_cavity_region(members, g), + hull = hull_region(shell), + b = pointlist_bounds(hull) + ) + [ + sb_area(section), // PLA+ per unit length + sb_nparts(section), // connected solids + sb_nparts(cav), // separate strap channels + sb_region_min_gap(section), // thinnest surviving wall + [b[1].x - b[0].x, b[1].y - b[0].y], // envelope size + sb_area(difference(cav, shell)) // cavity outside envelope + ]; + +/* + Checks every profile must pass, whatever its shape or member count. + + The connectivity test alone is not enough: a cross-section joined by a + 0.14 mm knife edge is topologically connected and physically useless. The + minimum-wall test is what actually catches over-large corner radii, + swallowed junction gaps and fillets that have stopped bridging. +*/ +function sb_universal_checks(section, metrics, expected_members, g) = [ + sb_check(metrics[SB_R_PARTS] == 1, + str("Cross-section is not one connected solid (", metrics[SB_R_PARTS], + " separate pieces). Widen the junctions or thicken the walls.")), + + sb_check(metrics[SB_R_SLOTS] == expected_members, + str("Expected ", expected_members, " separate strap channels but found ", + metrics[SB_R_SLOTS], + ". Neighbouring channels have merged, so those straps share one slot and are not retained. Increase the relevant web.")), + + sb_check(metrics[SB_R_MINW] >= sb_min_wall(g) - 1e-4, + str("Thinnest PLA+ wall is ", metrics[SB_R_MINW], + " mm, below the required minimum of ", sb_min_wall(g), + " mm. Reduce the corner radius, increase the web, or lower min_wall_mm if this really is acceptable.")), + + sb_check(is_region_simple(section), + str("The cross-section touches itself at a point rather than ", + "crossing cleanly. Such an outline is valid but cannot be ", + "tessellated, so it would fail on extrusion. Nudge the junction ", + "fillet radius away from zero, or change the web slightly.")), + + sb_check(metrics[SB_R_LEAK] < 1e-4, + str("A strap cavity breaks out of the outer envelope (", + metrics[SB_R_LEAK], " mm^2 outside). The straps would not be enclosed.")) +]; + + +// --------------------------------------------------------------------------- +// Report +// --------------------------------------------------------------------------- + +function sb_kv(key, value) = [key, value]; + +module sb_emit(key, value) { echo(str("SB_", key, "=", value)); } + +module sb_emit_all(pairs) { for (p = pairs) sb_emit(p[0], p[1]); } + +/* + Standard report. `extra` carries whatever the individual profile wants to + publish — solved sizes, effective projections, headroom on a radius — as + a list of [key, value] pairs. +*/ +module sb_report( + family, profile, status, g, metrics, length_mm, density_g_cm3, extra = [] +) { + section_area = metrics[SB_R_AREA]; + volume_mm3 = section_area * length_mm; + + sb_emit("STATUS", status); + sb_emit("FAMILY", family); + sb_emit("PROFILE", profile); + + sb_emit("STRAP_WIDTH_MM", sb_width(g)); + sb_emit("STRAP_THICK_MM", sb_strap_t(g)); + sb_emit("BUNDLE_COUNT", sb_count(g)); + sb_emit("BUNDLE_THICK_MM", sb_bundle_t(g)); + sb_emit("CLEARANCE_MM", sb_clear(g)); + sb_emit("WALL_INSIDE_MM", sb_wall_in(g)); + sb_emit("WALL_OUTSIDE_MM", sb_wall_out(g)); + sb_emit("WALL_EDGE_MM", sb_wall_edge(g)); + sb_emit("MIN_WALL_SPEC_MM", sb_min_wall(g)); + + sb_emit("MIN_WALL_ACTUAL_MM", metrics[SB_R_MINW]); + sb_emit("SECTION_AREA_MM2", section_area); + sb_emit("SECTION_PARTS", metrics[SB_R_PARTS]); + sb_emit("STRAP_CHANNELS", metrics[SB_R_SLOTS]); + sb_emit("ENVELOPE_X_MM", metrics[SB_R_SIZE].x); + sb_emit("ENVELOPE_Y_MM", metrics[SB_R_SIZE].y); + + sb_emit("LENGTH_MM", length_mm); + sb_emit("VOLUME_MM3", volume_mm3); + sb_emit("MASS_G", volume_mm3 * density_g_cm3 / 1000); + + sb_emit_all(extra); + sb_emit("END", 1); +} + + +// --------------------------------------------------------------------------- +// 3D output +// --------------------------------------------------------------------------- +/* + A profile is a cross-section swept along Z. The section is what every + consumer actually cares about — the catalogue renderer projects it, the + slicer extrudes it — so it is generated once and reused for both. +*/ + +module sb_extrude_section(rgn, length) { + linear_sweep(rgn, height = length, center = true); +} + +module sb_extrude_straps(members, g, length) { + for (m = members) + for (p = sb_strap_layer_paths(m, g)) + linear_sweep([p], height = length, center = true); +} + +// Flat 2D output, for the catalogue's SVG pipeline. +module sb_draw_section(rgn) { region(rgn); } diff --git a/legacy/openscad/strap-beam-3x.scad b/legacy/openscad/strap-beam-3x.scad new file mode 100644 index 0000000..85bab96 --- /dev/null +++ b/legacy/openscad/strap-beam-3x.scad @@ -0,0 +1,471 @@ +/* + strap-beam-3x.scad — Three-strap longitudinal beam enclosures + ============================================================= + Revision 8. Replaces triangle-strap-enclosure_v7.scad. + + Three pallet-strap bundles run parallel to one longitudinal axis (Z). + A profile decides only how the three cross-sections are arranged in XY; + that arrangement is swept along Z. A "member" here is the end view of + one 10-100 ft strap bundle, so its length in the cross-section is the + strap's WIDTH. + + Profiles + 1. Equilateral Triangle ring, three equal sides + 2. General Triangle ring, caller-shaped triangle + 3. A Frame two legs meeting at a gable apex, plus a + crossbar butted between them + 4. Y three radial spokes about a plugged centre + 5. T two-strap flange with a perpendicular stem + 6. Three-Fin tangential triangle whose members are slid + cyclically so each projects one fin + + Everything reusable lives in lib/sb-core.scad and is shared unchanged + with strap-beam-4x.scad. This file contains only the six arrangements + and their parameters. + + --------------------------------------------------------------------- + Changes from v7 that alter dimensions + --------------------------------------------------------------------- + * Every gap parameter is now a WEB: the PLA+ that survives between two + strap cavities. v7 stated gaps strap-to-strap and then inflated the + cavities by the fit clearance afterwards, which quietly removed + 2 x clearance from every junction. A declared 1.2 mm web is now + 1.2 mm of plastic. + * Ring profiles size themselves. Straps have a fixed width, so on a + polygon of a given size the corner webs are whatever they are; sliding + members along their edges cannot raise all three at once because each + edge shares its budget with two corners. The polygon is therefore + grown about its centroid until the tightest corner reaches the + declared web. Caller-supplied triangle dimensions set the SHAPE; the + size follows from the strap width. + * Ring members are centred on their edges rather than shifted + cyclically, so the triangle profiles are now mirror-symmetric. + * Junctions are structural. Members butt through their neighbours and + are cut flush on the far face; fillets are applied on top of that + overlap rather than in place of it. In v7 the Three-Fin was held + together by a 0.4 mm corner touch that the fillet happened to bridge. + * Members with no enclosed side (Y spokes, T stem and flange) take the + outside wall on both faces. v7 hard-coded an interior direction for + these, which made the Y a pinwheel whenever the two wall thicknesses + differed. + * Validation measures the finished section. Connectivity is necessary + but not sufficient - v7 passed cross-sections joined by 0.13 mm. + * Checks are scoped to the selected profile. In v7 every assert was + unconditional, so an A Frame slider could kill the Y profile. +*/ + +include + + +// --------------------------------------------------------------------------- +// Parameters +// --------------------------------------------------------------------------- + +/* [Profile] */ +profile_type = "Equilateral Triangle"; // ["Equilateral Triangle","General Triangle","A Frame","Y","T","Three-Fin"] + +/* [Pallet strap] */ +strap_width_mm = 15.875; // ULINE 5/8 in nominal width +strap_thickness_mm = 0.508; // ULINE .020 in nominal thickness +bundle_count = 1; // [1:1:3] + +/* [Longitudinal axis] */ +member_length_ft = 10; // [10:1:100] +length_view = "Preview"; // ["Preview","Full Length"] +preview_length_mm = 100; + +/* [PLA+ enclosure] */ +// Clearance is added to every cavity face. Declared webs are unaffected by +// it; the library compensates internally. +fit_clearance_mm = 0.25; +inside_wall_thickness_mm = 1.20; // faces that look into an enclosed bore +outside_wall_thickness_mm = 1.20; // faces exposed to the outside world +edge_wall_thickness_mm = 1.20; // caps over a strap's narrow edges +// Nothing anywhere in the section is allowed to be thinner than this. +min_wall_mm = 1.20; + +/* [Ring profiles] */ +// Applies to both triangle profiles. The polygon grows until its tightest +// corner reaches this web. +ring_corner_web_mm = 1.20; +// Rounding of the complete outer envelope, never of individual sleeves. +ring_corner_radius_mm = 2.00; + +/* [General Triangle shape] */ +// These set proportions only; absolute size is solved from the strap width. +general_triangle_base = 17.50; +general_triangle_height = 15.50; +general_triangle_apex_offset = 1.50; + +/* [A Frame] */ +a_frame_leg_angle_deg = 45; // leg angle measured from horizontal +a_frame_apex_web_mm = 1.20; // PLA+ between the two leg cavities +a_frame_crossbar_web_mm = 1.20; // PLA+ between crossbar and leg cavities +a_frame_crossbar_offset_mm = 0; +a_frame_junction_round_mm = 1.50; + +/* [Y Profile] */ +y_rotation_deg = 90; +y_junction_web_mm = 1.20; // PLA+ between neighbouring spoke cavities +y_junction_round_mm = 1.50; + +/* [T Profile] */ +t_stem_web_mm = 1.20; // PLA+ between stem and flange cavities +t_flange_web_mm = 1.20; // PLA+ between the two flange cavities +t_junction_round_mm = 1.50; + +/* [Three-Fin Profile] */ +// Fin projection past the corner it overhangs. The core triangle is solved +// from this plus the web and bore minimums below. +three_fin_fin_mm = 6.25; +three_fin_web_mm = 1.20; // PLA+ at each cyclic junction +three_fin_bore_side_mm = 7.50; // minimum central opening +three_fin_rotation_deg = 0; +three_fin_junction_round_mm = 2.00; + +/* [Display] */ +render_mode = "3D"; // ["3D","Section"] +show_straps = true; +show_enclosure = true; + +/* [Quality] */ +facets = 48; // [12:4:128] + +/* [Reporting] */ +material_density_g_cm3 = 1.24; // PLA+ nominal + +$fn = facets; + + +// --------------------------------------------------------------------------- +// Derived +// --------------------------------------------------------------------------- + +SB_FAMILY = "3x"; +// Generator revision. Qualification attestations bind to this, so any change +// that alters emitted geometry MUST bump it. Cosmetic or comment-only edits +// must not. See sb-report.scad for what is published. +SB_REVISION = "8.0.0"; +SB_MEMBER_COUNT = 3; + +geo = sb_geo( + strap_width_mm, strap_thickness_mm, bundle_count, fit_clearance_mm, + inside_wall_thickness_mm, outside_wall_thickness_mm, + edge_wall_thickness_mm, min_wall_mm +); + +model_length_mm = length_view == "Full Length" + ? member_length_ft * 304.8 + : preview_length_mm; + + +// --------------------------------------------------------------------------- +// 1-2. Ring profiles +// --------------------------------------------------------------------------- +/* + Both triangle profiles are the same construction: a centreline polygon, + grown until its corners are legal, wrapped in one envelope with solid + rounded corners, with the bore taken from the members' real inside walls. + + The 4x generator gets these for free by handing sb_ring_profile() a + quadrilateral instead. +*/ + +function sb3_equilateral(g) = + let(s = sb_width(g), a = s / (2 * SB_SQRT3)) + sb_ring_polygon_profile([[-s / 2, -a], [s / 2, -a], [0, 2 * a]], + g, ring_corner_web_mm, ring_corner_radius_mm, + "Equilateral Triangle"); + +function sb3_general_triangle(g) = + sb_ring_polygon_profile( + [[-general_triangle_base / 2, -general_triangle_height / 2], + [ general_triangle_base / 2, -general_triangle_height / 2], + [ general_triangle_apex_offset, general_triangle_height / 2]], + g, ring_corner_web_mm, ring_corner_radius_mm, "General Triangle"); + +// --------------------------------------------------------------------------- +// 3. A Frame +// --------------------------------------------------------------------------- +/* + The apex is a real meeting point: both leg centrelines pass through it and + each leg is set back along its own axis until the two cavities are exactly + a_frame_apex_web_mm apart. v7 instead offset both legs from the origin by + a fixed radius, which delivered apex_gap x cos(leg_angle) and collapsed to + nothing as the legs approached vertical. + + The crossbar is likewise solved by depth against the measured web and then + butted flush between the two legs' outer faces. v7 measured to the leg + CENTRELINE, so its declared 1.40 mm arrived as 0.16 mm. +*/ + +function sb3_a_frame_legs(g, setback) = + let( + a = a_frame_leg_angle_deg, + target = [0, -sb_width(g)], // deep inside the counter + d = sb_cavity_w(g) / 2 + setback, + angL = 180 + a, + angR = -a, + cL = [d * cos(angL), d * sin(angL)], + cR = [d * cos(angR), d * sin(angR)] + ) + [ + sb_member(cL.x, cL.y, angL, sb_face_toward(cL, angL, target)), + sb_member(cR.x, cR.y, angR, sb_face_toward(cR, angR, target)) + ]; + +function sb3_a_frame_apex_web(g, setback) = + let(l = sb3_a_frame_legs(g, setback)) + sb_path_gap(sb_cavity_path(l[0], g), sb_cavity_path(l[1], g)); + +function sb3_a_frame_crossbar(g, depth) = + let(c = [a_frame_crossbar_offset_mm, -depth]) + sb_member(c.x, c.y, 0, sb_face_toward(c, 0, [0, 0])); + +function sb3_a_frame_bar_web(g, legs, depth) = + let(cb = sb_cavity_path(sb3_a_frame_crossbar(g, depth), g)) + min(sb_path_gap(cb, sb_cavity_path(legs[0], g)), + sb_path_gap(cb, sb_cavity_path(legs[1], g))); + +function sb3_a_frame(g) = + let( + W = sb_width(g), + f_apex = function(s) sb3_a_frame_apex_web(g, s), + feasible = sb_solvable(f_apex, 6 * W, a_frame_apex_web_mm) + ) + !feasible + ? sb_profile_failed(str( + "A Frame: at ", a_frame_leg_angle_deg, + " degrees the legs are too close to parallel to open a ", + a_frame_apex_web_mm, + " mm apex web. Reduce a_frame_leg_angle_deg.")) + : let( + setback = sb_solve(f_apex, 0, 6 * W, a_frame_apex_web_mm), + legs = sb3_a_frame_legs(g, setback), + f_bar = function(d) sb3_a_frame_bar_web(g, legs, d), + bar_ok = sb_solvable(f_bar, 8 * W, a_frame_crossbar_web_mm) + ) + !bar_ok + ? sb_profile_failed( + "A Frame: the crossbar cannot reach a legal web against the legs. Reduce a_frame_crossbar_web_mm or the crossbar offset.") + : let( + depth = sb_solve(f_bar, 0, 8 * W, a_frame_crossbar_web_mm), + bar = sb3_a_frame_crossbar(g, depth), + ms = [legs[0], bar, legs[1]], + + // Legs run up to the apex; the wedge between their end faces + // is plugged with their convex hull, which leaves no spike + // and no V-notch. + ext = max(0, setback - sb_wall_edge(g)), + legL = sb_sleeve_path(legs[0], g, 0, ext), + legR = sb_sleeve_path(legs[1], g, 0, ext), + cap = sb_hull_cap([sb_end_face(legs[0], g, -1, ext), + sb_end_face(legs[1], g, -1, ext)]), + + // Crossbar spans flush between the legs' outer faces. + lineL = sb_far_face_line(legs[0], g, sb_mc(bar)), + lineR = sb_far_face_line(legs[1], g, sb_mc(bar)), + barP = sb_sleeve_span(bar, g, lineL[0], lineL[1], + lineR[0], lineR[1]), + + paths = [legL, barP, legR], + raw = union(concat([[legL], [legR], [barP]], + len(cap) > 0 ? [cap] : [])), + shell = sb_fillet_junctions(raw, [[0, 1], [1, 2], [0, 2]], + paths, a_frame_junction_round_mm), + bore = sb_bore_from_members(ms, g), + leg_end_depth = (setback + sb_cavity_w(g)) * sin(a_frame_leg_angle_deg) + ) + sb_profile( + ms, shell, bore, + [ + sb_check(depth < leg_end_depth, + str("A Frame: the crossbar sits at ", depth, + " mm below the apex but the legs only reach ", + leg_end_depth, + " mm. Reduce a_frame_crossbar_web_mm or the leg angle.")), + sb_check(sb_bore_valid(bore, ms, g), + "A Frame: the enclosed counter has collapsed. Reduce inside_wall_thickness_mm or open the frame out."), + sb_check(a_frame_junction_round_mm > 0, + "A Frame: a_frame_junction_round_mm must be greater than zero. A butt joint with no fillet meets its neighbour along an exactly tangent boundary, which is a valid outline but cannot be tessellated."), + sb_check(abs(a_frame_crossbar_offset_mm) < sb_width(g) / 2, + "A Frame: a_frame_crossbar_offset_mm has pushed the crossbar off the frame.") + ], + [ + sb_kv("AF_APEX_SETBACK_MM", setback), + sb_kv("AF_CROSSBAR_DEPTH_MM", depth), + sb_kv("AF_APEX_WEB_MM", sb3_a_frame_apex_web(g, setback)), + sb_kv("AF_CROSSBAR_WEB_MM", sb3_a_frame_bar_web(g, legs, depth)) + ] + ); + + +// --------------------------------------------------------------------------- +// 4. Y +// --------------------------------------------------------------------------- +/* + Three spokes pointing away from a common centre. A spoke's broad faces + both look at open air, so both get the outside wall - the profile stays + three-fold symmetric and non-chiral under any wall settings. The centre + is plugged with the convex hull of the three inner end faces rather than + left to whatever the crossing rectangles happened to produce. +*/ + +function sb3_y(g) = + sb_spoke_profile(3, y_rotation_deg, y_junction_web_mm, + y_junction_round_mm, g, "Y"); + + +// --------------------------------------------------------------------------- +// 5. T +// --------------------------------------------------------------------------- +/* + Two straps form the flange, separated by their own web so each keeps a + private channel - in v7 they touched edge to edge and shared one slot. + The stem is placed so its cavity clears the flange cavities by exactly + t_stem_web_mm and is then butted flush into the flange's far face. +*/ + +function sb3_t_members(g) = + let( + half = t_flange_web_mm / 2 + sb_cavity_w(g) / 2, + top = -sb_cavity_t(g) / 2 - t_stem_web_mm, + stemy = top - sb_cavity_w(g) / 2 + ) + [ + sb_member(-half, 0, 0, SB_FACE_BOTH_OUT), + sb_member( half, 0, 0, SB_FACE_BOTH_OUT), + sb_member(0, stemy, -90, SB_FACE_BOTH_OUT) // leading end points down + ]; + +function sb3_t(g) = + let( + ms = sb3_t_members(g), + flL = sb_sleeve_path(ms[0], g), + flR = sb_sleeve_path(ms[1], g), + stem = sb_sleeve_butt(ms[2], g, ms[0]), + paths = [flL, flR, stem], + raw = union([[flL], [flR], [stem]]), + shell = sb_fillet_junctions(raw, [[0, 2], [1, 2]], paths, + t_junction_round_mm), + flange_overlap = 2 * sb_wall_edge(g) - t_flange_web_mm + ) + sb_profile(ms, shell, [], [ + sb_check(flange_overlap > 0, + str("T: t_flange_web_mm of ", t_flange_web_mm, + " mm exceeds twice the edge wall, so the two flange sleeves no longer meet. Reduce it or raise edge_wall_thickness_mm.")), + sb_check(t_stem_web_mm >= sb_min_wall(g) - 1e-9, + str("T: t_stem_web_mm of ", t_stem_web_mm, " mm is below min_wall_mm.")), + sb_check(t_flange_web_mm >= sb_min_wall(g) - 1e-9, + str("T: t_flange_web_mm of ", t_flange_web_mm, " mm is below min_wall_mm.")), + sb_check(t_junction_round_mm > 0, + "T: t_junction_round_mm must be greater than zero. A butt joint with no fillet meets the flange along an exactly tangent boundary, which is a valid outline but cannot be tessellated.") + ], [ + sb_kv("T_FLANGE_SPAN_MM", 2 * sb_cavity_w(g) + t_flange_web_mm + + 2 * sb_wall_edge(g)), + sb_kv("T_NOTE", "inside_wall unused: no enclosed bore") + ]); + + +// --------------------------------------------------------------------------- +// 6. Three-Fin +// --------------------------------------------------------------------------- +/* + Tangential, not radial. Three straight members lie on the sides of a core + triangle and are slid cyclically along those sides, so each stops short of + the corner behind it and overhangs the corner ahead of it. The three + overhangs are the fins. + + Y three radial arms leaving a common centre + Three-Fin three tangential sides around a core, one fin each + + The core triangle is not a free parameter. It is solved so that the + cyclic junctions carry the declared web AND the bore reaches its declared + minimum, whichever demands more. The fin projection stays exactly as + asked, because it is measured against the same solved triangle. + + Each member's trailing end is run through the member behind it and cut off + flush on its far face. That gives every junction a full-width overlap; + the fillet that follows is cosmetic. In v7 the junctions were a 0.4 mm + corner touch and the fillet was the only thing holding the part together. +*/ + +function sb3_three_fin(g) = + sb_fin_profile(3, three_fin_fin_mm, three_fin_web_mm, + three_fin_bore_side_mm, three_fin_rotation_deg, + three_fin_junction_round_mm, g, "Three-Fin"); + + +// --------------------------------------------------------------------------- +// Catalogue +// --------------------------------------------------------------------------- +/* + Only the selected builder runs, so a parameter belonging to one entry can + never invalidate another. A 4x generator supplies its own table here and + nothing else changes. +*/ + +function sb3_build(name, g) = + name == "Equilateral Triangle" ? sb3_equilateral(g) + : name == "General Triangle" ? sb3_general_triangle(g) + : name == "A Frame" ? sb3_a_frame(g) + : name == "Y" ? sb3_y(g) + : name == "T" ? sb3_t(g) + : name == "Three-Fin" ? sb3_three_fin(g) + : sb_profile_failed(str("Unknown profile_type: ", name)); + +profile = sb3_build(profile_type, geo); + +// Parameters that apply to every profile, so they are checked once here. +base_checks = [ + sb_check(strap_width_mm > 0, "strap_width_mm must be greater than zero."), + sb_check(strap_thickness_mm > 0, "strap_thickness_mm must be greater than zero."), + sb_check(bundle_count >= 1 && bundle_count == floor(bundle_count), + "bundle_count must be a whole number of at least 1."), + sb_check(fit_clearance_mm >= 0, "fit_clearance_mm cannot be negative."), + sb_check(inside_wall_thickness_mm > 0, "inside_wall_thickness_mm must be positive."), + sb_check(outside_wall_thickness_mm > 0, "outside_wall_thickness_mm must be positive."), + sb_check(edge_wall_thickness_mm > 0, "edge_wall_thickness_mm must be positive."), + sb_check(min_wall_mm > 0, "min_wall_mm must be positive."), + sb_check(preview_length_mm > 0, "preview_length_mm must be greater than zero.") +]; + +// Fail on the profile's own checks before touching geometry, so an +// unbuildable arrangement reports its cause instead of a library error. +build_status = sb_require(concat(base_checks, sb_p_checks(profile))); + +centred = sb_centred(profile, geo); +section = centred[SB_C_SECTION]; +members = sb_centred_members(profile, centred[SB_C_SHIFT]); +metrics = sb_metrics(section, centred[SB_C_SHELL], members, geo); + +status = sb_require(sb_universal_checks(section, metrics, SB_MEMBER_COUNT, geo)); + + +// --------------------------------------------------------------------------- +// Report +// --------------------------------------------------------------------------- + +sb_report(SB_FAMILY, profile_type, status, geo, metrics, + model_length_mm, material_density_g_cm3, + concat([sb_kv("REVISION", SB_REVISION), + sb_kv("LENGTH_FT", member_length_ft), + sb_kv("LENGTH_VIEW", length_view)], + sb_p_info(profile))); + + +// --------------------------------------------------------------------------- +// Output +// --------------------------------------------------------------------------- + +if (render_mode == "Section") { + if (show_enclosure) + color([0.20, 0.55, 0.95]) sb_draw_section(section); + if (show_straps) + color([0.96, 0.72, 0.05]) sb_draw_section(centred[SB_C_STRAPS]); +} else { + if (show_enclosure) + color([0.20, 0.55, 0.95, 0.55]) sb_extrude_section(section, model_length_mm); + if (show_straps) + color([0.96, 0.72, 0.05, 0.82]) sb_extrude_straps(members, geo, model_length_mm); +} diff --git a/legacy/openscad/strap-beam-4x.scad b/legacy/openscad/strap-beam-4x.scad new file mode 100644 index 0000000..7e04876 --- /dev/null +++ b/legacy/openscad/strap-beam-4x.scad @@ -0,0 +1,215 @@ +/* + strap-beam-4x.scad — Four-strap longitudinal beam enclosures + ============================================================ + Companion to strap-beam-3x.scad. Four pallet-strap bundles run parallel + to one longitudinal axis (Z); a profile decides only how the four + cross-sections are arranged in XY. + + Profiles + 1. Square ring, four equal sides + 2. Rectangle ring, caller-shaped by aspect ratio + 3. Diamond ring, square rotated 45 degrees + 4. Cross four spokes about a plugged centre + 5. Four-Fin tangential square, one cyclic fin per side + + --------------------------------------------------------------------- + How much of this file is new + --------------------------------------------------------------------- + None of the geometry. All five profiles are one call each into + lib/sb-profiles.scad with N = 4: + + Square / Rectangle / Diamond -> sb_ring_polygon_profile() + Cross -> sb_spoke_profile(4, ...) + Four-Fin -> sb_fin_profile(4, ...) + + The same three calls with N = 3 produce the Triangles, the Y and the + Three-Fin in strap-beam-3x.scad. The polygon fitting, the bore + derivation, the butt joints, the fillets, the validation and the report + are all member-count agnostic, so this file contains only parameters, a + catalogue table and the outline shapes. + + A profile that genuinely does not generalise - the 3x A Frame and T, which + are specific arrangements rather than instances of a family - stays in its + own generator. That is the intended split: shapes that are an N-instance + live in the library, one-off arrangements live with their family. +*/ + +include + + +// --------------------------------------------------------------------------- +// Parameters +// --------------------------------------------------------------------------- + +/* [Profile] */ +profile_type = "Square"; // ["Square","Rectangle","Diamond","Cross","Four-Fin"] + +/* [Pallet strap] */ +strap_width_mm = 15.875; +strap_thickness_mm = 0.508; +bundle_count = 1; // [1:1:3] + +/* [Longitudinal axis] */ +member_length_ft = 10; // [10:1:100] +length_view = "Preview"; // ["Preview","Full Length"] +preview_length_mm = 100; + +/* [PLA+ enclosure] */ +fit_clearance_mm = 0.25; +inside_wall_thickness_mm = 1.20; +outside_wall_thickness_mm = 1.20; +edge_wall_thickness_mm = 1.20; +min_wall_mm = 1.20; + +/* [Ring profiles] */ +ring_corner_web_mm = 1.20; +// 1.25 mm is usable by all three ring outlines. The report publishes +// RING_CORNER_R_MAX_MM per profile if you want to push a specific entry. +ring_corner_radius_mm = 1.25; +// Proportions only; absolute size is solved from the strap width. +rectangle_aspect = 1.60; + +/* [Cross Profile] */ +cross_rotation_deg = 0; +cross_junction_web_mm = 1.20; +cross_junction_round_mm = 1.50; + +/* [Four-Fin Profile] */ +four_fin_fin_mm = 6.25; +four_fin_web_mm = 1.20; +four_fin_bore_side_mm = 7.50; +four_fin_rotation_deg = 0; +four_fin_junction_round_mm = 2.00; + +/* [Display] */ +render_mode = "3D"; // ["3D","Section"] +show_straps = true; +show_enclosure = true; + +/* [Quality] */ +facets = 48; // [12:4:128] + +/* [Reporting] */ +material_density_g_cm3 = 1.24; + +$fn = facets; + + +// --------------------------------------------------------------------------- +// Derived +// --------------------------------------------------------------------------- + +SB_FAMILY = "4x"; +// Generator revision. Qualification attestations bind to this, so any change +// that alters emitted geometry MUST bump it. Cosmetic or comment-only edits +// must not. See sb-report.scad for what is published. +SB_REVISION = "8.0.0"; +SB_MEMBER_COUNT = 4; + +geo = sb_geo( + strap_width_mm, strap_thickness_mm, bundle_count, fit_clearance_mm, + inside_wall_thickness_mm, outside_wall_thickness_mm, + edge_wall_thickness_mm, min_wall_mm +); + +model_length_mm = length_view == "Full Length" + ? member_length_ft * 304.8 + : preview_length_mm; + + +// --------------------------------------------------------------------------- +// Profiles +// --------------------------------------------------------------------------- +// Each is an outline plus a library call. The outlines set shape only; the +// ring fitter grows them until every corner carries ring_corner_web_mm. + +function sb4_rect_outline(w, h) = + [[-w / 2, -h / 2], [w / 2, -h / 2], [w / 2, h / 2], [-w / 2, h / 2]]; + +function sb4_square(g) = + sb_ring_polygon_profile(sb4_rect_outline(1, 1), g, + ring_corner_web_mm, ring_corner_radius_mm, "Square"); + +function sb4_rectangle(g) = + sb_ring_polygon_profile(sb4_rect_outline(rectangle_aspect, 1), g, + ring_corner_web_mm, ring_corner_radius_mm, "Rectangle"); + +function sb4_diamond(g) = + sb_ring_polygon_profile([for (p = sb4_rect_outline(1, 1)) sb_rot2(p, 45)], g, + ring_corner_web_mm, ring_corner_radius_mm, "Diamond"); + +function sb4_cross(g) = + sb_spoke_profile(4, cross_rotation_deg, cross_junction_web_mm, + cross_junction_round_mm, g, "Cross"); + +function sb4_four_fin(g) = + sb_fin_profile(4, four_fin_fin_mm, four_fin_web_mm, four_fin_bore_side_mm, + four_fin_rotation_deg, four_fin_junction_round_mm, g, + "Four-Fin"); + + +// --------------------------------------------------------------------------- +// Catalogue +// --------------------------------------------------------------------------- + +function sb4_build(name, g) = + name == "Square" ? sb4_square(g) + : name == "Rectangle" ? sb4_rectangle(g) + : name == "Diamond" ? sb4_diamond(g) + : name == "Cross" ? sb4_cross(g) + : name == "Four-Fin" ? sb4_four_fin(g) + : sb_profile_failed(str("Unknown profile_type: ", name)); + +profile = sb4_build(profile_type, geo); + +base_checks = [ + sb_check(strap_width_mm > 0, "strap_width_mm must be greater than zero."), + sb_check(strap_thickness_mm > 0, "strap_thickness_mm must be greater than zero."), + sb_check(bundle_count >= 1 && bundle_count == floor(bundle_count), + "bundle_count must be a whole number of at least 1."), + sb_check(fit_clearance_mm >= 0, "fit_clearance_mm cannot be negative."), + sb_check(inside_wall_thickness_mm > 0, "inside_wall_thickness_mm must be positive."), + sb_check(outside_wall_thickness_mm > 0, "outside_wall_thickness_mm must be positive."), + sb_check(edge_wall_thickness_mm > 0, "edge_wall_thickness_mm must be positive."), + sb_check(min_wall_mm > 0, "min_wall_mm must be positive."), + sb_check(rectangle_aspect > 0, "rectangle_aspect must be greater than zero."), + sb_check(preview_length_mm > 0, "preview_length_mm must be greater than zero.") +]; + +build_status = sb_require(concat(base_checks, sb_p_checks(profile))); + +centred = sb_centred(profile, geo); +section = centred[SB_C_SECTION]; +members = sb_centred_members(profile, centred[SB_C_SHIFT]); +metrics = sb_metrics(section, centred[SB_C_SHELL], members, geo); + +status = sb_require(sb_universal_checks(section, metrics, SB_MEMBER_COUNT, geo)); + + +// --------------------------------------------------------------------------- +// Report +// --------------------------------------------------------------------------- + +sb_report(SB_FAMILY, profile_type, status, geo, metrics, + model_length_mm, material_density_g_cm3, + concat([sb_kv("REVISION", SB_REVISION), + sb_kv("LENGTH_FT", member_length_ft), + sb_kv("LENGTH_VIEW", length_view)], + sb_p_info(profile))); + + +// --------------------------------------------------------------------------- +// Output +// --------------------------------------------------------------------------- + +if (render_mode == "Section") { + if (show_enclosure) + color([0.20, 0.55, 0.95]) sb_draw_section(section); + if (show_straps) + color([0.96, 0.72, 0.05]) sb_draw_section(centred[SB_C_STRAPS]); +} else { + if (show_enclosure) + color([0.20, 0.55, 0.95, 0.55]) sb_extrude_section(section, model_length_mm); + if (show_straps) + color([0.96, 0.72, 0.05, 0.82]) sb_extrude_straps(members, geo, model_length_mm); +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..becc894 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "mechcomp" +version = "0.1.0.dev0" +description = "Mechanical Compiler — structural member generation from reclaimed and commodity stock" +requires-python = ">=3.11" +license = { text = "AGPL-3.0-or-later" } + +[project.urls] +Source = "https://gitea.barternetwork.us/TheRON/mechanical-compiler" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +markers = [ + "oracle: acceptance against the frozen rev-8 fixture set", + "cad: requires requirements-cad.txt", +] diff --git a/requirements-base.txt b/requirements-base.txt new file mode 100644 index 0000000..bcc36a6 --- /dev/null +++ b/requirements-base.txt @@ -0,0 +1,15 @@ +# Base dependencies. The catalogue's 2D path runs on these alone. +# +# Constraints, not pins. The lock file is generated ON THE TARGET HOST against +# its own Python 3.11, because resolving here would pin wheels for a different +# interpreter. See docs/ENVIRONMENT.md section 8.3. + +shapely>=2.0,<3 +fastapi>=0.110 +uvicorn[standard]>=0.29 +pydantic>=2.6 +sqlalchemy>=2.0 +jinja2>=3.1 + +pytest>=8.0 +pytest-xdist>=3.5 diff --git a/requirements-cad.txt b/requirements-cad.txt new file mode 100644 index 0000000..87b0f87 --- /dev/null +++ b/requirements-cad.txt @@ -0,0 +1,8 @@ +# 3D export only: STEP and mesh. Installed by default, but the test suite must +# pass with this file absent — that is the mechanism keeping the 2D and 3D paths +# separable. See docs/ENVIRONMENT.md section 1.1. +# +# build123d is a viable alternative on the same OCCT kernel. If it is chosen, +# MECHCOMP_CAD_BACKEND changes with it. + +cadquery>=2.4 diff --git a/src/mechcomp/__init__.py b/src/mechcomp/__init__.py new file mode 100644 index 0000000..3efd9b9 --- /dev/null +++ b/src/mechcomp/__init__.py @@ -0,0 +1 @@ +"""Mechanical Compiler.""" diff --git a/src/mechcomp/cad/__init__.py b/src/mechcomp/cad/__init__.py new file mode 100644 index 0000000..004fa90 --- /dev/null +++ b/src/mechcomp/cad/__init__.py @@ -0,0 +1 @@ +"""Mechanical Compiler - cad.""" diff --git a/src/mechcomp/geom/__init__.py b/src/mechcomp/geom/__init__.py new file mode 100644 index 0000000..37a3e0a --- /dev/null +++ b/src/mechcomp/geom/__init__.py @@ -0,0 +1 @@ +"""Mechanical Compiler - geom.""" diff --git a/src/mechcomp/profiles/__init__.py b/src/mechcomp/profiles/__init__.py new file mode 100644 index 0000000..c07c8b9 --- /dev/null +++ b/src/mechcomp/profiles/__init__.py @@ -0,0 +1 @@ +"""Mechanical Compiler - profiles.""" diff --git a/src/mechcomp/web/__init__.py b/src/mechcomp/web/__init__.py new file mode 100644 index 0000000..65cb58a --- /dev/null +++ b/src/mechcomp/web/__init__.py @@ -0,0 +1 @@ +"""Mechanical Compiler - web.""" diff --git a/src/mechcomp/worker/__init__.py b/src/mechcomp/worker/__init__.py new file mode 100644 index 0000000..045a2dd --- /dev/null +++ b/src/mechcomp/worker/__init__.py @@ -0,0 +1 @@ +"""Mechanical Compiler - worker.""" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a3b25f3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,153 @@ +""" +Shared fixtures. + +The main job here is translating an oracle case into a call the port can make. +The oracle records OpenSCAD command-line overrides, because that is what +produced it; the port takes a plain dictionary. That translation lives in one +place so the acceptance tests stay readable and the mapping is auditable. + +The parameter names are deliberately unchanged. `bundle_count` in the OpenSCAD +generator is `bundle_count` in the port. A renaming layer would be one more +thing to get wrong for no benefit. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +ORACLE = ( + Path(__file__).resolve().parents[1] + / "fixtures" / "strap-beam-8.0.0" / "strap-beam-fixtures-8.0.0.json" +) + +PORT_MISSING = ( + "the Shapely port does not exist yet: mechcomp.profiles.build is not " + "importable. This is expected until the first work item in ROADMAP.md is " + "done. The integrity tests still run." +) + + +# --------------------------------------------------------------------------- +# Oracle loading +# --------------------------------------------------------------------------- + +def _parse_define(token: str) -> tuple[str, object]: + """ + Turn one OpenSCAD -D override into a (name, value) pair. + + -Dprofile_type="Y" -> ("profile_type", "Y") + -Dbundle_count=2 -> ("bundle_count", 2) + -Dstrap_width_mm=13.4-> ("strap_width_mm", 13.4) + """ + assert token.startswith("-D"), f"not an override: {token!r}" + name, _, raw = token[2:].partition("=") + raw = raw.strip() + if raw.startswith('"') and raw.endswith('"'): + return name, raw[1:-1] + try: + value = float(raw) + except ValueError: + return name, raw + return name, int(value) if value.is_integer() and "." not in raw else value + + +def _case_params(case: dict) -> dict: + """Every override except profile_type, which is passed separately.""" + params = dict(_parse_define(t) for t in case["defs"]) + params.pop("profile_type", None) + return params + + +def _enrich(case: dict) -> dict: + return { + **case, + "family": "3x" if "3x" in case["generator"] else "4x", + "params": _case_params(case), + } + + +@pytest.fixture(scope="session") +def oracle_raw() -> dict: + if not ORACLE.exists(): + pytest.fail(f"oracle missing: {ORACLE}") + return json.loads(ORACLE.read_text()) + + +@pytest.fixture(scope="session") +def tolerance(oracle_raw) -> dict: + return oracle_raw["tolerance"] + + +@pytest.fixture(scope="session") +def all_cases(oracle_raw) -> list[dict]: + return [_enrich(c) for c in oracle_raw["cases"]] + + +@pytest.fixture(scope="session") +def accepted_cases(all_cases) -> list[dict]: + return [c for c in all_cases if c["outcome"] == "ok"] + + +@pytest.fixture(scope="session") +def rejected_cases(all_cases) -> list[dict]: + return [c for c in all_cases if c["outcome"] == "rejected"] + + +# --------------------------------------------------------------------------- +# The port under test +# --------------------------------------------------------------------------- + +@pytest.fixture(scope="session") +def port(): + """ + The module implementing the generators. + + Required surface: + + build(family: str, profile: str, params: dict) -> Result + Result.report -> dict, the SB_* keys the oracle records + raises ProfileRejected for an unbuildable arrangement + + ProfileRejected(Exception) + str() must name the parameter and the limit, as the reference does. + """ + profiles = pytest.importorskip("mechcomp.profiles", reason=PORT_MISSING) + if not hasattr(profiles, "build"): + pytest.skip(PORT_MISSING) + return profiles + + +# --------------------------------------------------------------------------- +# Per-case parametrisation +# --------------------------------------------------------------------------- + +def _load_cases_eagerly() -> list[dict]: + """ + Read the oracle at collection time. + + Parametrisation happens before fixtures resolve, so this cannot use them. + A missing oracle yields an empty list, and the integrity tests report the + real problem rather than every acceptance test failing obscurely. + """ + if not ORACLE.exists(): + return [] + return [_enrich(c) for c in json.loads(ORACLE.read_text())["cases"]] + + +_CASES = _load_cases_eagerly() + + +def _ident(case: dict) -> str: + return f"{case['family']}-{case['profile'].replace(' ', '_')}-{case['label']}" + + +def pytest_generate_tests(metafunc): + if "accepted_case" in metafunc.fixturenames: + cases = [c for c in _CASES if c["outcome"] == "ok"] + metafunc.parametrize("accepted_case", cases, ids=[_ident(c) for c in cases]) + if "rejected_case" in metafunc.fixturenames: + cases = [c for c in _CASES if c["outcome"] == "rejected"] + metafunc.parametrize("rejected_case", cases, ids=[_ident(c) for c in cases]) diff --git a/tests/test_oracle.py b/tests/test_oracle.py new file mode 100644 index 0000000..220a0ae --- /dev/null +++ b/tests/test_oracle.py @@ -0,0 +1,157 @@ +""" +Acceptance of any reimplementation against the frozen rev-8.0.0 oracle. + +This file is the specification of the port's public API. It was written before +the port existed, deliberately: the shape of the interface should be decided by +what has to be verified, not by what happens to be convenient to implement. + +Until `mechcomp.profiles.build` exists, everything except the integrity checks +skips with a clear reason. Those integrity checks always run -- an oracle that +has been edited is worse than no oracle, and that should fail loudly on any +machine, at any time, with no dependencies. + + pytest -n auto # all of it + pytest -m oracle # acceptance only + pytest -k integrity # oracle checks alone, always runnable +""" + +from __future__ import annotations + +import pytest + +pytestmark = pytest.mark.oracle + + +# --------------------------------------------------------------------------- +# Integrity - no dependency on the port +# --------------------------------------------------------------------------- + +def test_integrity_hash(oracle_raw): + """The committed oracle has not been modified.""" + import hashlib + import json + + doc = dict(oracle_raw) + recorded = doc.pop("fixtures_sha256") + actual = hashlib.sha256(json.dumps(doc, indent=2, sort_keys=True).encode()).hexdigest() + assert actual == recorded, ( + "The oracle has been edited. Its hash covers the document without the " + "hash field, serialised with indent=2 and sort_keys=True. If this was " + "intentional, regenerate it inside the reference toolchain image and " + "record why in FAILURES.md." + ) + + +def test_integrity_shape(oracle_raw): + """The case matrix is the one the documents describe.""" + assert oracle_raw["generator_revision"] == "8.0.0" + assert oracle_raw["toolchain"]["openscad"] == "2021.01" + assert oracle_raw["toolchain"]["bosl2_commit"].startswith("92d697c2") + + summary = oracle_raw["summary"] + assert summary["cases"] == 123 + assert summary["accepted"] == 113 + assert summary["rejected"] == 10 + assert len(oracle_raw["cases"]) == summary["cases"] + + +def test_integrity_invariants_hold_in_the_oracle(accepted_cases): + """ + Every accepted case in the oracle satisfies the invariants. + + This validates the oracle itself rather than the port. If it ever fails, + the fixture set is describing geometry that should never have been + accepted, and no port should be measured against it. + """ + for case in accepted_cases: + r = case["report"] + where = f"{case['profile']}/{case['label']}" + expected_members = 3 if "3x" in case["generator"] else 4 + + assert r["SECTION_PARTS"] == 1, f"{where}: not one connected solid" + assert r["STRAP_CHANNELS"] == expected_members, f"{where}: channels merged" + assert r["MIN_WALL_ACTUAL_MM"] >= r["MIN_WALL_SPEC_MM"] - 1e-4, \ + f"{where}: wall below its declared minimum" + + +# --------------------------------------------------------------------------- +# Acceptance - requires the port +# --------------------------------------------------------------------------- + +def test_accepted_case_matches_oracle(port, accepted_case, tolerance): + """ + A case the reference accepted must be accepted, with matching geometry. + + Both halves matter. Reproducing the measured values while accepting a case + the reference rejected is not a passing port. + """ + expected = accepted_case["report"] + where = f"{accepted_case['profile']}/{accepted_case['label']}" + + result = port.build( + family=accepted_case["family"], + profile=accepted_case["profile"], + params=accepted_case["params"], + ) + got = result.report + + # Counts are exact. + for key in ("SECTION_PARTS", "STRAP_CHANNELS", "BUNDLE_COUNT"): + assert got[key] == expected[key], f"{where}: {key}" + + # Lengths and areas carry the tolerance the oracle declares. + for key, want in expected.items(): + if not isinstance(want, float) or key in ("SECTION_PARTS", "STRAP_CHANNELS"): + continue + tol = tolerance["areas_mm2"] if key.endswith("_MM2") else tolerance["lengths_mm"] + assert key in got, f"{where}: port did not report {key}" + assert abs(got[key] - want) <= tol, ( + f"{where}: {key} is {got[key]}, oracle says {want}" + ) + + +def test_rejected_case_is_rejected(port, rejected_case): + """ + A case the reference rejected must be rejected. + + These ten are the part of the contract a naive reimplementation loses: it + is easy to reproduce the geometry and quietly drop the constraint that made + it trustworthy. Accepting any of them is a failure, however good the + numbers look elsewhere. + """ + where = f"{rejected_case['profile']}/{rejected_case['label']}" + with pytest.raises(port.ProfileRejected) as excinfo: + port.build( + family=rejected_case["family"], + profile=rejected_case["profile"], + params=rejected_case["params"], + ) + assert str(excinfo.value).strip(), ( + f"{where}: rejected without a message. A rejection must name the " + f"parameter and the limit, as the reference does:\n" + f" {rejected_case['rejection']}" + ) + + +def test_no_cad_dependency_on_the_2d_path(port, accepted_case): + """ + Building a cross-section must not import the 3D kernel. + + ENVIRONMENT.md section 1.1: a slow OCCT import must never land in the + request path for a page that only draws a cross-section. This test also + runs in the CI job where requirements-cad.txt is absent, where an accidental + import fails outright rather than merely being slow. + """ + import sys + + for module in ("cadquery", "OCP", "build123d"): + sys.modules.pop(module, None) + + port.build( + family=accepted_case["family"], + profile=accepted_case["profile"], + params=accepted_case["params"], + ) + + leaked = [m for m in ("cadquery", "OCP", "build123d") if m in sys.modules] + assert not leaked, f"the 2D path imported {leaked}" diff --git a/tools/reference-toolchain/Dockerfile b/tools/reference-toolchain/Dockerfile new file mode 100644 index 0000000..fb21dec --- /dev/null +++ b/tools/reference-toolchain/Dockerfile @@ -0,0 +1,31 @@ +# Pinned reference toolchain for the rev-8.0.0 acceptance oracle. +# +# This image exists for one purpose: regenerating and verifying +# fixtures/strap-beam-8.0.0/. The running application has no OpenSCAD +# dependency at all -- see docs/ENVIRONMENT.md section 8.2. Confining OpenSCAD +# here is why the host's package version cannot drift into the oracle. +# +# Do not add anything to this image. Its value is that it contains nothing else. + +FROM debian:12-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + openscad \ + python3 \ + git \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# BOSL2 at the exact commit the oracle was frozen against. +ARG BOSL2_COMMIT=92d697c2856de2fed93a33e858068589cefc2898 +RUN git clone https://github.com/BelfrySCAD/BOSL2.git /BOSL2 \ + && git -C /BOSL2 checkout "${BOSL2_COMMIT}" \ + && rm -rf /BOSL2/.git + +# The generators use `include `; OpenSCAD resolves that against +# OPENSCADPATH after trying the including file's own directory. +ENV OPENSCADPATH=/ + +WORKDIR /repo +CMD ["openscad", "--version"] diff --git a/tools/reference-toolchain/verify.sh b/tools/reference-toolchain/verify.sh new file mode 100644 index 0000000..2e6fdb9 --- /dev/null +++ b/tools/reference-toolchain/verify.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# Build the pinned toolchain and prove the committed oracle still reproduces. +# +# ./tools/reference-toolchain/verify.sh # verify the hash only +# ./tools/reference-toolchain/verify.sh --full # regenerate and compare +# +# --full runs all 123 cases through OpenSCAD. On the staging host that takes a +# while: the solvers are single-threaded and the CPU is old. That is expected. + +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +IMAGE="mechcomp/reference-toolchain:8.0.0" +EXPECTED="ddd0f1548379205dd0c652ec07285b0dae331e52ff0a0437005dfc6cddcc2cb2" + +echo "==> building ${IMAGE}" +docker build -t "${IMAGE}" "${REPO}/tools/reference-toolchain" + +echo +echo "==> toolchain versions" +docker run --rm "${IMAGE}" openscad --version +docker run --rm "${IMAGE}" git -C /BOSL2 rev-parse HEAD 2>/dev/null \ + || echo "(BOSL2 .git removed at build time; commit is pinned by ARG)" + +echo +echo "==> verifying the committed oracle" +python3 "${REPO}/fixtures/strap-beam-8.0.0/make_fixtures.py" --verify + +if [[ "${1:-}" == "--full" ]]; then + echo + echo "==> regenerating all 123 cases inside the pinned toolchain" + tmp="$(mktemp -d)" + trap 'rm -rf "${tmp}"' EXIT + cp "${REPO}/fixtures/strap-beam-8.0.0/strap-beam-fixtures-8.0.0.json" "${tmp}/before.json" + + docker run --rm -v "${REPO}:/repo" "${IMAGE}" \ + python3 /repo/fixtures/strap-beam-8.0.0/make_fixtures.py --regenerate + + echo + echo "==> comparing against the committed oracle" + if diff -q "${tmp}/before.json" \ + "${REPO}/fixtures/strap-beam-8.0.0/strap-beam-fixtures-8.0.0.json" >/dev/null; then + echo "OK - byte-identical." + else + echo "DIFFERS from the committed oracle." >&2 + echo "The 'frozen' date changes on every regeneration, so a one-line" >&2 + echo "diff there is expected. Any other difference means the toolchain" >&2 + echo "has drifted; investigate before proceeding." >&2 + diff "${tmp}/before.json" \ + "${REPO}/fixtures/strap-beam-8.0.0/strap-beam-fixtures-8.0.0.json" | head -40 >&2 + cp "${tmp}/before.json" \ + "${REPO}/fixtures/strap-beam-8.0.0/strap-beam-fixtures-8.0.0.json" + echo >&2 + echo "The committed oracle has been restored. Nothing was overwritten." >&2 + exit 1 + fi +fi + +echo +echo "expected sha256: ${EXPECTED}"