From 9d21816542405dbaeb6258a570247fd31c3c6866 Mon Sep 17 00:00:00 2001 From: rob Date: Fri, 29 May 2026 12:21:55 -0300 Subject: [PATCH] Flush-by-default joins (corner alignment) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boards now align to A's reference corner when they butt — top faces level and one side flush — instead of B floating centered on A. The flush step snaps B's +faces onto A's +faces along A's cross-section axes, skipping the axis B extends along so the butt contact is preserved. Equal-size flat joints are unchanged; mixed sizes (e.g. a 1x8 shelf on a 2x4) now line up cleanly (tops level). Test: a 1x8 joined to a 2x4 sits tops-flush (center z=0.375), not centered. 53 tests passing; verified with a render. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 12 +++++++----- README.md | 5 +++-- src/woodshop/scene.py | 19 +++++++++++++++++++ tests/test_scene.py | 13 +++++++++++++ 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 712ac97..49bc4f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,11 +84,13 @@ pytest # 25 tests ### Known limitations / next steps -1. **Joins are butt joints**: B's end sits flush against A's surface (offset by - A's cross-section half-extent in B's approach direction), so boards meet at - faces rather than centerlines and don't interpenetrate. Not yet modeled: - joinery *cuts* (mortise/tenon, lap, pocket holes) and flush-outer-face vs - centered alignment options (currently B is centered on A at the attach point). +1. **Joins are flush butt joints**: B's end sits flush against A's surface, and + B is aligned to A's reference corner (top faces level + one side flush) rather + than centered — so mixed-size boards line up cleanly. The flush step snaps + B's +faces to A's +faces on A's cross-section axes, skipping the axis B + extends along (so the butt contact is preserved). Not yet modeled: joinery + *cuts* (mortise/tenon, lap, pocket holes), and the flush corner is fixed + (A's +width/+thick side; no per-join choice of which corner / centered). 2. **Latency** ~7–13s per utterance (one `claude -p` call). 3. Voice path (`--voice`) reuses `dictate`; the driver loop is hardened against failures but the mic path isn't exercised in the unit tests. diff --git a/README.md b/README.md index 40dd7f7..9b15cde 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,9 @@ Key modules: ### Known limitations -- Joins are butt joints (B's end sits flush against A's face, not its - centerline); joinery *cuts* (mortise/tenon, lap, pocket holes) aren't modeled yet. +- Joins are flush butt joints: B's end sits against A's face and B aligns to + A's reference corner (tops level + one side flush), so mixed-size boards line + up. Joinery *cuts* (mortise/tenon, lap, pocket holes) aren't modeled yet. - Command interpretation latency is ~7–13s per utterance (one `claude -p` call). ## License diff --git a/src/woodshop/scene.py b/src/woodshop/scene.py index bbfb194..67d99ba 100644 --- a/src/woodshop/scene.py +++ b/src/woodshop/scene.py @@ -253,6 +253,25 @@ class Scene: + a_half_t * abs(_dot(b_len, a_thick))) b.position_in = [anchor_pt[i] + b_len[i] * surface for i in range(3)] + # Flush-by-default: align B to A's reference corner — its faces line up + # with A's top and one side, rather than B floating centered on A. For + # each of A's cross-section axes (skipping the one B extends along, so + # the butt contact is preserved) snap B's +face onto A's +face. + b_l, b_w, b_t = b.local_frame() + + def b_half_extent(axis): + return (b.length_in / 2 * abs(_dot(b_l, axis)) + + b.section_in[1] / 2 * abs(_dot(b_w, axis)) + + b.section_in[0] / 2 * abs(_dot(b_t, axis))) + + for axis, a_half in ((a_thick, a_half_t), (a_width, a_half_w)): + if abs(_dot(b_len, axis)) > 0.9: # B runs along this axis — leave it + continue + a_face = _dot(a.position_in, axis) + a_half # A's far face + b_face = _dot(b.position_in, axis) + b_half_extent(axis) + delta = a_face - b_face + b.position_in = [b.position_in[i] + delta * axis[i] for i in range(3)] + jid = f"j{self._next_joint}" self._next_joint += 1 joint = Joint(id=jid, part_a=a.id, part_b=b.id, diff --git a/tests/test_scene.py b/tests/test_scene.py index 111e831..e4ef22a 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -99,6 +99,19 @@ def test_butt_joint_meets_surface_not_centerline(): assert p2.end_point()[1] == pytest.approx(13.75) +def test_flush_aligns_tops_for_different_thicknesses(): + """A thinner board joined to a thicker one should sit with TOPS level, not + centers level (flush-by-default).""" + s = Scene() + s.place("2x4", 48) # p1: thickness 1.5 -> top face at z=0.75 + s.place("1x8", 12) # p2: thickness 0.75 + s.join("p1", "p2", angle_deg=90, offset_in=24, anchor="end_a") + p2 = s.get_part("p2") + # p2's top (z + 0.375) is flush with p1's top (0.75) -> p2 center at 0.375, + # NOT centered on p1 (which would leave p2 at z=0). + assert p2.position_in[2] == pytest.approx(0.375) + + def test_join_preserves_vertical_tilt(): """A stood-up leg stays vertical when attached to a horizontal apron.""" s = Scene()