From 12e4bbab88fb62c04fcd84cefe9ac930da2975e5 Mon Sep 17 00:00:00 2001 From: rob Date: Sun, 31 May 2026 00:21:47 -0300 Subject: [PATCH] Fix miter preview/highlight: show the cut-off wedge, not a tenon box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cyan (select) / red (edit) overlay for a miter fell through to the tenon/mortise box branch, so it looked like a 1" pocket at the end instead of an angled cut. - viewer._miter_wedge_mesh: build board ∩ cutter (the piece the miter removes), placed in world space, and return that as the preview/highlight mesh; falls back to highlighting the end face when the angle is 0. - factored tessellation into _solid_to_polydata; miter excluded from the normal-axis spin step. - test: the miter preview is a wedge reaching the board end, not a centre box. 238 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/woodshop/viewer.py | 56 ++++++++++++++++++++++++++++++++++++++---- tests/test_miter.py | 14 +++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/woodshop/viewer.py b/src/woodshop/viewer.py index b8d45d0..e392a77 100644 --- a/src/woodshop/viewer.py +++ b/src/woodshop/viewer.py @@ -34,12 +34,11 @@ def _board_color(part: Part, index: int) -> str: return lighten(base, delta) if delta >= 0 else darken(base, -delta) -def _featured_mesh(part: Part): - """Tessellate the true build123d solid (with joinery booleans) for display.""" +def _solid_to_polydata(solid): + """Tessellate a build123d solid into a pyvista PolyData.""" import pyvista as pv - from .geometry import part_solid - verts, tris = part_solid(part).tessellate(0.02) + verts, tris = solid.tessellate(0.02) points = [(v.X, v.Y, v.Z) for v in verts] faces = [] for tri in tris: @@ -47,6 +46,43 @@ def _featured_mesh(part: Part): return pv.PolyData(points, faces) +def _featured_mesh(part: Part): + """Tessellate the true build123d solid (with joinery booleans) for display.""" + from .geometry import part_solid + return _solid_to_polydata(part_solid(part)) + + +def _miter_wedge_mesh(part: Part, feat): + """The piece a miter cuts OFF (board ∩ cutter), placed in world space — so the + preview/highlight shows the angled cut, not a tenon-like box. None if no cut.""" + try: + from build123d import Box, Pos, Rot + L = part.length_in + t, w = part.section_in + board = Pos(L / 2, 0, 0) * Box(L, w, t) + big = 3 * max(L, w, t) + 10 + block = Box(big, big, big) + if feat.face == "end_a": + cutter = Pos(-big / 2, 0, 0) * block + pivot = (0.0, 0.0, 0.0) + else: + cutter = Pos(L + big / 2, 0, 0) * block + pivot = (L, 0.0, 0.0) + cutter = (Pos(*pivot) * Rot(Z=feat.miter_deg, Y=feat.bevel_deg) + * Pos(*[-c for c in pivot]) * cutter) + wedge = board & cutter # the removed piece + mesh = _solid_to_polydata(wedge) + if mesh.n_points == 0: + return None + except Exception: + return None + mesh.rotate_x(part.roll_deg, point=(0, 0, 0), inplace=True) + mesh.rotate_y(-part.tilt_deg, point=(0, 0, 0), inplace=True) + mesh.rotate_z(part.yaw_deg, point=(0, 0, 0), inplace=True) + mesh.translate(part.position_in, inplace=True) + return mesh + + def _part_mesh(part: Part): import pyvista as pv @@ -92,6 +128,16 @@ def feature_preview_mesh(part, feat): h = feat.depth_in if feat.depth_in > 0 else thru c = tuple(fp[i] - n[i] * h / 2 for i in range(3)) mesh = pv.Cylinder(center=c, direction=n, radius=feat.diameter_in / 2, height=h) + elif feat.kind == "miter": # show the wedge it cuts off; fall back to the end face + wedge = _miter_wedge_mesh(part, feat) + if wedge is not None: + return wedge + ue, ve, thin = _axis_extent(u, L, w, t), _axis_extent(v, L, w, t), 0.08 + dims = tuple(ue * abs(u[i]) + ve * abs(v[i]) + thin * abs(n[i]) for i in range(3)) + c = fp + mesh = pv.Box(bounds=(c[0] - dims[0] / 2, c[0] + dims[0] / 2, + c[1] - dims[1] / 2, c[1] + dims[1] / 2, + c[2] - dims[2] / 2, c[2] + dims[2] / 2)) elif feat.kind == "chamfer": # can't cheaply preview the bevel — highlight the face ue, ve, thin = _axis_extent(u, L, w, t), _axis_extent(v, L, w, t), 0.08 dims = tuple(ue * abs(u[i]) + ve * abs(v[i]) + thin * abs(n[i]) for i in range(3)) @@ -109,7 +155,7 @@ def feature_preview_mesh(part, feat): c[1] - dims[1] / 2, c[1] + dims[1] / 2, c[2] - dims[2] / 2, c[2] + dims[2] / 2)) - if feat.rotation_deg and feat.kind not in ("hole", "chamfer"): + if feat.rotation_deg and feat.kind not in ("hole", "chamfer", "miter"): mesh.rotate_vector(n, feat.rotation_deg, point=fp, inplace=True) mesh.rotate_x(part.roll_deg, point=(0, 0, 0), inplace=True) mesh.rotate_y(-part.tilt_deg, point=(0, 0, 0), inplace=True) diff --git a/tests/test_miter.py b/tests/test_miter.py index 53f71a5..2557250 100644 --- a/tests/test_miter.py +++ b/tests/test_miter.py @@ -68,3 +68,17 @@ def test_miter_geometry_removes_material(): mitered_vol = part_solid(s.get_part("p1")).volume assert mitered_vol < square_vol # a wedge was cut off + + +def test_miter_preview_is_a_wedge_not_a_box(): + pytest.importorskip("pyvista") + pytest.importorskip("build123d") + from woodshop.viewer import feature_preview_mesh + s = Scene() + s.place("2x4", 24) + feat = s.add_feature("p1", "miter", miter_deg=45) + mesh = feature_preview_mesh(s.get_part("p1"), feat) + assert mesh is not None and mesh.n_points > 0 + # the wedge sits at the mitered end (x≈24), not a 1" box at the end centre + xmax = mesh.bounds[1] + assert xmax > 20 # spans out to the board end, like the real cut