Fix miter preview/highlight: show the cut-off wedge, not a tenon box

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) <noreply@anthropic.com>
This commit is contained in:
rob 2026-05-31 00:21:47 -03:00
parent b284b58229
commit 12e4bbab88
2 changed files with 65 additions and 5 deletions

View File

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

View File

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