Flush-by-default joins (corner alignment)

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) <noreply@anthropic.com>
This commit is contained in:
rob 2026-05-29 12:21:55 -03:00
parent 7d01144143
commit 9d21816542
4 changed files with 42 additions and 7 deletions

View File

@ -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** ~713s 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.

View File

@ -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 ~713s per utterance (one `claude -p` call).
## License

View File

@ -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,

View File

@ -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()