Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions designs/cabinet_bar_pull_handle/family.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"family": "cabinet_bar_pull_handle",
"standard": "Berenson SS bar-pull series (7068-7072) / Richelieu BP305",
"base_plane": "XY",
"description": "Cabinet/drawer bar pull as a simple assembly: a straight round bar and two standoff posts as three separate solids, each post coped to seat against the bar and tapped from the base for the mounting screw; bar ends optionally chamfered.",
"source": "Berenson 7068-7072 + Richelieu BP305 dimensioned drawings (C-C, overall length, bar/post diameter, projection, tap); bar/post/projection cross-checked constant across Amerock + Hickory",
"contributor": "BenchCAD-org",
"solids": 3
}
52 changes: 52 additions & 0 deletions designs/cabinet_bar_pull_handle/part.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""cabinet_bar_pull_handle — the parametric part (simple assembly, solids=3).

A cabinet/drawer bar pull as the three parts it is assembled from: a straight
round bar, and two cylindrical standoff posts that carry it off the cabinet
face, each post tapped from the base for the mounting machine screw. The post
tops are coped (saddle-cut by the bar cylinder) so each post mates flush against
the bar's underside with zero interpenetration — the members stay three separate
solids, as assembled. The bar overhangs each post; its ends are optionally
chamfered. The cabinet face is z=0; posts stand up +Z, the bar runs along X.

Interface + examples: docs/DESIGN_SPEC.md
"""

import cadquery as cq


def build(center_to_center, overhang, projection, bar_d, post_d, tap_d, tap_depth, chamfer_ends):
length = center_to_center + 2.0 * overhang # overall bar length

# projection is the standoff to the bar's OUTER face, so the bar axis sits
# half a bar-diameter below it
z_bar = projection - bar_d / 2.0

# round bar across the posts (along X), its axis at z_bar — one solid
bar = cq.Workplane("YZ").circle(bar_d / 2.0).extrude(length).translate((-length / 2.0, 0.0, z_bar))
if chamfer_ends:
bar = bar.faces(">X").chamfer(bar_d * 0.1)
bar = bar.faces("<X").chamfer(bar_d * 0.1)

# an uncut, unchamfered bar cylinder as the coping tool, so the post saddle
# follows the bar surface even at a chamfered end
bar_tool = (
cq.Workplane("YZ").circle(bar_d / 2.0).extrude(length).translate((-length / 2.0, 0.0, z_bar))
)

# two posts standing off the cabinet face, each tapped from the base; each
# rises to the bar axis and is coped by the bar so it seats on the underside
# without intersecting it — separate solids, in contact only
posts = []
for sx in (-1.0, 1.0):
post = (
cq.Workplane("XY")
.circle(post_d / 2.0)
.extrude(z_bar)
.translate((sx * center_to_center / 2.0, 0.0, 0.0))
)
post = post.faces("<Z").workplane(centerOption="CenterOfBoundBox").hole(tap_d, depth=tap_depth)
post = post.cut(bar_tool)
posts.append(post)

result = cq.Compound.makeCompound([bar.val(), posts[0].val(), posts[1].val()])
return result
Binary file added designs/cabinet_bar_pull_handle/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
105 changes: 105 additions & 0 deletions designs/cabinet_bar_pull_handle/spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""cabinet_bar_pull_handle — the benchmark generator spec.

A cabinet/drawer bar pull (round bar on two standoff posts). 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:
- Berenson SS hollow bar-pull series (7068-7072) + Richelieu BP305 line: bar
Ø12 mm, post Ø10 mm, projection 32.5-35 mm, center-to-center 76-638 mm, mount
tapped 8-32 / M4. Overall length = C-C + ~2*overhang.
- Bar Ø / post Ø / projection cross-checked constant across Berenson, Amerock,
Hickory; C-C is the catalog ladder (96/128/160/192/224/320 mm). Within one
line only C-C + length vary, so bar Ø / post Ø / projection are "proportion".
"""


# ── 1. PARAM_SPEC ────────────────────────────────────────────────────────────
PARAM_SPEC = {
"center_to_center": dict(
desc="center-to-center spacing of the two mounting posts",
unit="mm",
range={"easy": (96.0, 192.0), "medium": (76.0, 320.0), "hard": (76.0, 638.0)},
source="Berenson 7068-7072 / Richelieu BP305 C-C ladder (76-638 mm)",
askable=True,
),
"overhang": dict(
desc="bar overhang past each post (overall length = C-C + 2*overhang)",
unit="mm",
range={"easy": (24.0, 40.0), "medium": (20.0, 50.0), "hard": (18.0, 60.0)},
source="Richelieu overhang ~40 mm; Berenson ~24 mm -> proportion",
askable=True,
),
"projection": dict(
desc="standoff height (how far the bar stands off the cabinet face)",
unit="mm",
range={"easy": (32.0, 35.0), "medium": (28.0, 38.0), "hard": (25.0, 42.0)},
source="Berenson 32.5 mm / Amerock 35 mm / Hickory 32 mm",
askable=True,
),
"bar_d": dict(
desc="handle bar diameter",
unit="mm",
range={"easy": (11.0, 13.0), "medium": (10.0, 16.0), "hard": (9.0, 16.0)},
source="Berenson/Amerock/Hickory Ø12 mm (1/2 in) -> proportion across lines",
askable=True,
),
"post_d": dict(
desc="standoff-post diameter",
unit="mm",
range={"easy": (9.0, 11.0), "medium": (8.0, 12.0), "hard": (7.0, 13.0)},
source="Berenson Ø10 mm / Amerock 3/8 in (9.5 mm) -> proportion",
askable=True,
),
"tap_d": dict(
desc="mounting-screw tap hole diameter (in the post base)",
unit="mm",
range={"easy": (3.2, 3.6), "medium": (3.0, 4.5), "hard": (2.5, 5.0)},
source="8-32 / M4 tap (Berenson M4; Richelieu 8-32)",
askable=True,
),
"tap_depth": dict(
desc="tap-hole depth into the post",
unit="mm",
range={"easy": (5.0, 7.0), "medium": (4.0, 10.0), "hard": (4.0, 12.0)},
source="Richelieu 8-32 x 6 mm deep -> proportion",
askable=True,
),
"chamfer_ends": dict(
desc="bar ends chamfered (1) vs square (0)",
unit="",
range={"easy": (0, 0), "medium": (0, 1), "hard": (0, 1)},
source="Richelieu bar ends C1.2x45 chamfer",
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 = []

# finger clearance under the bar (projection is to the bar's outer face, so
# the clear gap below the bar is projection - bar_d)
if p["projection"] - p["bar_d"] < 8.0:
bad.append("projection - bar_d < 8: too little finger clearance under the bar")

# the mounting tap must fit within the post (post rises to z = projection - bar_d/2)
if p["tap_depth"] > p["projection"] - p["bar_d"] / 2.0 - 2.0:
bad.append("tap_depth exceeds the post height (projection - bar_d/2 - 2): tap would break through")

# the tap hole must leave a post wall
if p["tap_d"] > p["post_d"] - 2.5:
bad.append("tap_d > post_d-2.5: tap leaves <1.25 mm post wall")

# posts must be clearly separated
if p["center_to_center"] < p["post_d"] + 8.0:
bad.append("center_to_center < post_d+8: posts too close / overlapping")

# the bar must actually overhang the posts
if p["overhang"] < p["bar_d"]:
bad.append("overhang < bar_d: bar ends barely clear the posts")

return bad
Loading