diff --git a/designs/duct_takeoff_starting_collar/family.json b/designs/duct_takeoff_starting_collar/family.json new file mode 100644 index 0000000..09f2df1 --- /dev/null +++ b/designs/duct_takeoff_starting_collar/family.json @@ -0,0 +1,8 @@ +{ + "family": "duct_takeoff_starting_collar", + "standard": "GreenSeam / ECCO / M&M round-duct start collar (SMACNA)", + "base_plane": "XY", + "description": "Round-duct takeoff/starting collar: a rolled sheet-metal barrel with a stiffening top bead, a ring of rectangular base tabs (the real trapezoidal tabs simplified) that bend flat onto the duct, and an optional integral butterfly damper on a diametral shaft. One solid.", + "source": "GreenSeam tabbed/spin-in start-collar spec sheets + ECCO / M&M submittals (diameter, body, bead, tabs, damper); gauge per ASTM A653 / SMACNA; tab count/width proportion", + "contributor": "BenchCAD-org" +} diff --git a/designs/duct_takeoff_starting_collar/part.py b/designs/duct_takeoff_starting_collar/part.py new file mode 100644 index 0000000..e114ca2 --- /dev/null +++ b/designs/duct_takeoff_starting_collar/part.py @@ -0,0 +1,60 @@ +"""duct_takeoff_starting_collar — the parametric part. + +A round-duct takeoff / starting collar: a short galvanized sheet-metal round +collar (barrel) that starts a round branch off a plenum or main duct. A rolled +bead stiffens the top (slip-fit) end, the base carries a ring of rectangular tabs +(the real trapezoidal tabs are simplified to rectangles) that bend flat, and an optional butterfly damper sits on a +diametral shaft inside the barrel. The barrel axis is Z; the tabs lie in the +mounting plane at z=0. + +Interface + examples: docs/DESIGN_SPEC.md +""" + +import cadquery as cq + + +def _tab_angles(count): + """Angular position (deg) of each base tab.""" + return [360.0 * i / count for i in range(count)] + + +def build(collar_d, collar_h, wall, bead_proj, tab_h, tab_w, tab_count, damper): + r_out = collar_d / 2.0 + r_in = r_out - wall + + # straight collar barrel (annular tube) along Z, z in [0, collar_h] + result = cq.Workplane("XY").circle(r_out).circle(r_in).extrude(collar_h) + + # rolled bead: an outward stiffening rib around the slip-fit (top) end + bead_h = wall * 4.0 + bead = ( + cq.Workplane("XY") + .circle(r_out + bead_proj) + .circle(r_in) + .extrude(bead_h) + .translate((0.0, 0.0, collar_h - bead_h)) + ) + result = result.union(bead) + + # base tabs: flat rectangular tabs (real ones trapezoidal) bent out into the mounting plane + for ang in _tab_angles(tab_count): + tab = ( + cq.Workplane("XY") + .box(tab_h, tab_w, wall) + .translate((r_out + tab_h / 2.0 - wall, 0.0, wall / 2.0)) + .rotate((0.0, 0.0, 0.0), (0.0, 0.0, 1.0), ang) + ) + result = result.union(tab) + + # optional butterfly damper: a blade disc on a diametral shaft, mid-barrel. + # The blade runs a running clearance inside the bore (not bore-sized) so its + # union is not a fragile coincident-face touch; the shaft ties it to the barrel. + if damper: + z_mid = collar_h / 2.0 + blade_gap = max(1.0, wall) + blade = cq.Workplane("XY").circle(r_in - blade_gap).extrude(wall, both=True).translate((0.0, 0.0, z_mid)) + shaft_l = collar_d + 20.0 + shaft = cq.Workplane("YZ").circle(3.0).extrude(shaft_l).translate((-shaft_l / 2.0, 0.0, z_mid)) + result = result.union(blade).union(shaft) + + return result diff --git a/designs/duct_takeoff_starting_collar/preview.png b/designs/duct_takeoff_starting_collar/preview.png new file mode 100644 index 0000000..c2f9943 Binary files /dev/null and b/designs/duct_takeoff_starting_collar/preview.png differ diff --git a/designs/duct_takeoff_starting_collar/preview_extremes.png b/designs/duct_takeoff_starting_collar/preview_extremes.png new file mode 100644 index 0000000..4e53d03 Binary files /dev/null and b/designs/duct_takeoff_starting_collar/preview_extremes.png differ diff --git a/designs/duct_takeoff_starting_collar/preview_views.png b/designs/duct_takeoff_starting_collar/preview_views.png new file mode 100644 index 0000000..a4f3147 Binary files /dev/null and b/designs/duct_takeoff_starting_collar/preview_views.png differ diff --git a/designs/duct_takeoff_starting_collar/spec.py b/designs/duct_takeoff_starting_collar/spec.py new file mode 100644 index 0000000..1c4a404 --- /dev/null +++ b/designs/duct_takeoff_starting_collar/spec.py @@ -0,0 +1,104 @@ +"""duct_takeoff_starting_collar — the benchmark generator spec. + +A round-duct takeoff / starting collar (tabbed, optional butterfly damper). +PARAM_SPEC declares each build() parameter; check() holds the engineering rules a +reviewer audits. Nothing is coupled, so there is no refine(). +Spec: docs/DESIGN_SPEC.md + +Sources: +- GreenSeam tabbed / spin-in start collar spec sheets + ECCO and M&M submittals: + collar (branch) Ø 4-20 in (100-500 mm), cylindrical body ~6 in, crimp/bead + 1-5/8 in, base tabs ~1 in, ~16 trapezoidal tabs, damper axis 2 in below the + bead. Galvanized steel ASTM A653, 26 ga std / 24 ga option. +- Only diameter varies per catalog row (lengths constant); tab count/width not + dimensioned -> "proportion". +""" + + +# ── 1. PARAM_SPEC ──────────────────────────────────────────────────────────── +PARAM_SPEC = { + "collar_d": dict( + desc="collar (branch-duct) nominal diameter", + unit="mm", + range={"easy": (150.0, 300.0), "medium": (100.0, 400.0), "hard": (100.0, 500.0)}, + source="GreenSeam/ECCO/M&M start-collar sizes 4-20 in (100-500 mm)", + askable=True, + ), + "collar_h": dict( + desc="cylindrical barrel height", + unit="mm", + range={"easy": (80.0, 120.0), "medium": (60.0, 150.0), "hard": (55.0, 160.0)}, + source="GreenSeam ~6 in body -> proportion", + askable=True, + ), + "wall": dict( + desc="sheet-metal wall thickness", + unit="mm", + range={"easy": (0.5, 0.7), "medium": (0.5, 0.85), "hard": (0.4, 0.9)}, + source="26 ga (0.55 mm) / 24 ga (0.7 mm) galvanized, ASTM A653", + askable=True, + ), + "bead_proj": dict( + desc="outward projection of the rolled top bead", + unit="mm", + range={"easy": (2.0, 4.0), "medium": (2.0, 5.0), "hard": (1.5, 6.0)}, + source="proportion (rolled stiffening bead)", + askable=True, + ), + "tab_h": dict( + desc="mounting-tab length (radial reach)", + unit="mm", + range={"easy": (20.0, 28.0), "medium": (15.0, 32.0), "hard": (12.0, 35.0)}, + source="GreenSeam tab ~1 in (25 mm)", + askable=True, + ), + "tab_w": dict( + desc="mounting-tab width", + unit="mm", + range={"easy": (12.0, 18.0), "medium": (10.0, 22.0), "hard": (8.0, 26.0)}, + source="proportion (tab width not dimensioned; ~16 tabs on the photo)", + askable=True, + ), + "tab_count": dict( + desc="number of base mounting tabs", + unit="", + range={"easy": (14, 18), "medium": (12, 20), "hard": (10, 24)}, + source="GreenSeam photo ~16 trapezoidal tabs -> proportion", + integer=True, + askable=True, + ), + "damper": dict( + desc="integral butterfly damper (blade + shaft) fitted (1) vs none (0)", + unit="", + range={"easy": (0, 0), "medium": (0, 1), "hard": (0, 1)}, + source="GreenSeam/ECCO with-damper vs plain start-collar variants", + choices={"easy": [0], "medium": [0, 1], "hard": [0, 1]}, + feature=True, + ), +} + + +# ── 2. check ───────────────────────────────────────────────────────────────── +def check(p: dict) -> list[str]: + """Engineering constraints (empty = valid). Each cites its rule.""" + import math + + bad = [] + + # the tabs must fit around the base circumference with gaps between them + if p["tab_count"] * p["tab_w"] > 0.9 * math.pi * p["collar_d"]: + bad.append("tab_count*tab_w > 0.9*pi*D: tabs overlap around the base") + + # rolled sheet, not thick pipe + if p["wall"] > p["collar_d"] / 60.0: + bad.append("wall > D/60: collar is rolled sheet metal, not thick pipe") + + # the bead must be smaller than the mounting tabs + if p["bead_proj"] > p["tab_h"]: + bad.append("bead_proj > tab_h: bead should be a small rib, not larger than a tab") + + # a barrel, not a flat ring: body height must exceed the wall bead zone + if p["collar_h"] < 6.0 * p["wall"] + 20.0: + bad.append("collar_h too short: barrel must clear the bead zone with a real body") + + return bad