diff --git a/designs/tilting_speaker_wall_bracket/family.json b/designs/tilting_speaker_wall_bracket/family.json new file mode 100644 index 0000000..3494eb2 --- /dev/null +++ b/designs/tilting_speaker_wall_bracket/family.json @@ -0,0 +1,8 @@ +{ + "family": "tilting_speaker_wall_bracket", + "standard": "B-Tech BT77 / BT-series tilt+swivel speaker wall mount", + "base_plane": "YZ", + "description": "Tilt-adjustable loudspeaker wall mount: a wall plate with a bolt pattern, a standoff arm, a tilt-pivot knuckle, and a speaker-side plate held at a set tilt with fixing holes; optional arm gusset. One connected bracket.", + "source": "B-Tech BT77 spec sheet (wall-plate holes, arm reach, tilt); BT1/BT15/BT332 line (reach/tilt ranges); plate/arm proportions", + "contributor": "BenchCAD-org" +} diff --git a/designs/tilting_speaker_wall_bracket/part.py b/designs/tilting_speaker_wall_bracket/part.py new file mode 100644 index 0000000..5102945 --- /dev/null +++ b/designs/tilting_speaker_wall_bracket/part.py @@ -0,0 +1,54 @@ +"""tilting_speaker_wall_bracket — the parametric part. + +A tilt-adjustable loudspeaker wall mount (B-Tech BT-style): a wall plate that +bolts to the wall, an arm that stands the speaker off the wall, and a +speaker-side plate held at a set tilt angle, with fixing holes. An optional +gusset reinforces the arm/wall joint. Modelled as one connected bracket at a +fixed tilt position. The wall is the Y-Z plane at x=0; the arm reaches out +X. + +Interface + examples: docs/DESIGN_SPEC.md +""" + +import cadquery as cq + + +def build(wall_w, wall_h, plate_t, arm_len, arm_w, tilt_angle, spk_w, spk_h, bolt_d, gusset): + # wall plate (against the wall, normal +X), centred at x=0 + result = cq.Workplane("YZ").box(wall_w, wall_h, plate_t) + # wall bolt holes (through the plate along X), a rectangular pattern + bx, bz = wall_w * 0.32, wall_h * 0.34 + holes = cq.Workplane("YZ").pushPoints([(bx, bz), (-bx, bz), (bx, -bz), (-bx, -bz)]).circle(bolt_d / 2.0).extrude(plate_t * 3.0, both=True) + result = result.cut(holes) + + # standoff arm reaching out from the wall plate + x_arm_start = plate_t / 2.0 + result = result.union( + cq.Workplane("XY").box(arm_len, arm_w, arm_w).translate((x_arm_start + arm_len / 2.0, 0.0, 0.0)) + ) + + # optional gusset: a triangular rib under the arm, wall -> arm + if gusset: + g = ( + cq.Workplane("XZ") + .polyline([(x_arm_start, -arm_w / 2.0), (x_arm_start + arm_len * 0.6, -arm_w / 2.0), + (x_arm_start, -arm_w / 2.0 - wall_h * 0.28)]) + .close() + .extrude(arm_w * 0.6, both=True) + ) + result = result.union(g) + + # tilt-pivot knuckle at the arm front (a hub on the horizontal Y axis) — the + # tilted plate always intersects it, so the bracket stays one connected body + x_front = x_arm_start + arm_len + hub = cq.Workplane("XZ").circle(arm_w * 0.55).extrude(arm_w).translate((x_front, arm_w / 2.0, 0.0)) + result = result.union(hub) + + # speaker-side plate, built flat (with its fixing holes) then tilted about the + # horizontal (Y) axis to the set tilt angle and set on the pivot + spk = cq.Workplane("XY").box(plate_t, spk_w, spk_h) + fz = spk_h * 0.3 + spk = spk.faces(">X").workplane().pushPoints([(0.0, fz), (0.0, -fz)]).hole(bolt_d) + spk = spk.rotate((x_front, 0.0, 0.0), (x_front, 1.0, 0.0), tilt_angle).translate((x_front, 0.0, 0.0)) + result = result.union(spk) + + return result diff --git a/designs/tilting_speaker_wall_bracket/preview.png b/designs/tilting_speaker_wall_bracket/preview.png new file mode 100644 index 0000000..cdfd999 Binary files /dev/null and b/designs/tilting_speaker_wall_bracket/preview.png differ diff --git a/designs/tilting_speaker_wall_bracket/preview_extremes.png b/designs/tilting_speaker_wall_bracket/preview_extremes.png new file mode 100644 index 0000000..cd0b74c Binary files /dev/null and b/designs/tilting_speaker_wall_bracket/preview_extremes.png differ diff --git a/designs/tilting_speaker_wall_bracket/preview_hard_zoom.png b/designs/tilting_speaker_wall_bracket/preview_hard_zoom.png new file mode 100644 index 0000000..4441673 Binary files /dev/null and b/designs/tilting_speaker_wall_bracket/preview_hard_zoom.png differ diff --git a/designs/tilting_speaker_wall_bracket/preview_views.png b/designs/tilting_speaker_wall_bracket/preview_views.png new file mode 100644 index 0000000..92b8701 Binary files /dev/null and b/designs/tilting_speaker_wall_bracket/preview_views.png differ diff --git a/designs/tilting_speaker_wall_bracket/spec.py b/designs/tilting_speaker_wall_bracket/spec.py new file mode 100644 index 0000000..0612627 --- /dev/null +++ b/designs/tilting_speaker_wall_bracket/spec.py @@ -0,0 +1,112 @@ +"""tilting_speaker_wall_bracket — the benchmark generator spec. + +A tilt-adjustable loudspeaker wall mount (B-Tech BT-series style). 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: +- B-Tech BT77 (Ultragrip Pro) spec sheet: wall-plate holes Ø6.5/8.5 mm at + 76/127/145 mm vertical spacing; arm reach 135-280 mm; tilt +-10 deg. +- BT1/BT15/BT332 line: reach 72-290 mm; tilt +-10 to +-20 deg; loads 5-25 kg. +- Plate/arm proportions from the BT drawings -> proportion. +""" + + +# ── 1. PARAM_SPEC ──────────────────────────────────────────────────────────── +PARAM_SPEC = { + "wall_w": dict( + desc="wall-plate width", + unit="mm", + range={"easy": (60.0, 75.0), "medium": (50.0, 90.0), "hard": (48.0, 100.0)}, + source="B-Tech BT77 wall plate ~68 mm wide -> proportion", + askable=True, + ), + "wall_h": dict( + desc="wall-plate height", + unit="mm", + range={"easy": (127.0, 150.0), "medium": (100.0, 180.0), "hard": (76.0, 200.0)}, + source="B-Tech BT77 wall-plate hole spacing 76/127/145 mm", + askable=True, + ), + "plate_t": dict( + desc="plate (wall + speaker) thickness", + unit="mm", + range={"easy": (3.0, 5.0), "medium": (3.0, 6.0), "hard": (2.5, 7.0)}, + source="proportion (formed steel bracket plate)", + askable=True, + ), + "arm_len": dict( + desc="arm reach (standoff from the wall)", + unit="mm", + range={"easy": (72.0, 135.0), "medium": (72.0, 200.0), "hard": (72.0, 290.0)}, + source="B-Tech BT-series reach 72-290 mm", + askable=True, + ), + "arm_w": dict( + desc="arm square cross-section size", + unit="mm", + range={"easy": (20.0, 30.0), "medium": (18.0, 40.0), "hard": (15.0, 45.0)}, + source="proportion (standoff arm)", + askable=True, + ), + "tilt_angle": dict( + desc="speaker-plate tilt from vertical", + unit="deg", + range={"easy": (0.0, 10.0), "medium": (0.0, 15.0), "hard": (0.0, 20.0)}, + source="B-Tech tilt +-10 (BT77) to +-20 (BT332)", + askable=True, + ), + "spk_w": dict( + desc="speaker-side plate width", + unit="mm", + range={"easy": (80.0, 110.0), "medium": (60.0, 140.0), "hard": (50.0, 160.0)}, + source="proportion (speaker-side clamp plate)", + askable=True, + ), + "spk_h": dict( + desc="speaker-side plate height", + unit="mm", + range={"easy": (90.0, 130.0), "medium": (70.0, 160.0), "hard": (60.0, 180.0)}, + source="proportion (speaker-side clamp plate)", + askable=True, + ), + "bolt_d": dict( + desc="wall-bolt / speaker-fixing hole diameter", + unit="mm", + range={"easy": (6.0, 7.0), "medium": (6.0, 8.5), "hard": (6.0, 8.5)}, + source="B-Tech BT77 wall holes Ø6.5 / 8.5 mm", + askable=True, + ), + "gusset": dict( + desc="triangular gusset reinforcing the arm/wall joint (1) vs none (0)", + unit="", + range={"easy": (0, 0), "medium": (0, 1), "hard": (0, 1)}, + source="heavier BT models add an arm gusset", + 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.""" + bad = [] + + # the arm must sit on the wall plate, not overhang it + if p["arm_w"] > 0.7 * p["wall_w"]: + bad.append("arm_w > 0.7*wall_w: arm wider than the wall plate can carry") + + # the speaker plate must be larger than the arm/pivot it mounts on + if p["spk_w"] < p["arm_w"] + 10.0 or p["spk_h"] < p["arm_w"] + 10.0: + bad.append("speaker plate smaller than arm+10: plate must cover the pivot knuckle") + + # a slide/plate is a tall standoff, not a stub: reach exceeds the arm section + if p["arm_len"] < 1.5 * p["arm_w"]: + bad.append("arm_len < 1.5*arm_w: the arm is a standoff, not a stub") + + # bolt holes must fit the plates with edge material + if p["bolt_d"] > 0.3 * min(p["wall_w"], p["spk_h"]): + bad.append("bolt_d > 0.3*min(wall_w,spk_h): holes leave too little plate material") + + return bad