160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
"""End miter / bevel angled cuts."""
|
||
import json
|
||
|
||
import pytest
|
||
|
||
from woodshop.scene import Scene
|
||
|
||
|
||
def test_add_miter_defaults_to_45_on_end():
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
f = s.add_feature("p1", "miter") # no angle given
|
||
assert f.kind == "miter" and f.face == "end_b" and f.miter_deg == 45.0
|
||
|
||
|
||
def test_miter_forced_to_end_face():
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
f = s.add_feature("p1", "miter", face="top", miter_deg=30) # top is invalid for miter
|
||
assert f.face == "end_b" and f.miter_deg == 30
|
||
|
||
|
||
def test_miter_roundtrips_through_json():
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
s.add_feature("p1", "miter", miter_deg=45, bevel_deg=15)
|
||
s2 = Scene.from_dict(json.loads(json.dumps(s.to_dict())))
|
||
f = s2.get_part("p1").features[0]
|
||
assert f.kind == "miter" and f.miter_deg == 45 and f.bevel_deg == 15
|
||
|
||
|
||
def test_cutlist_notes_the_miter():
|
||
from woodshop.cutplan import build_cut_plan
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
s.add_feature("p1", "miter", miter_deg=45)
|
||
note = build_cut_plan(s).items[0].note
|
||
assert "miter 45" in note
|
||
|
||
|
||
def test_instructions_describe_miter():
|
||
from woodshop.instructions import build_steps, format_steps
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
s.add_feature("p1", "miter", miter_deg=45, bevel_deg=10)
|
||
text = format_steps(build_steps(s))
|
||
assert "miter 45" in text and "bevel 10" in text
|
||
|
||
|
||
def test_jigs_suggest_miter_sled_for_repeats():
|
||
from woodshop.jigs import suggest_jigs
|
||
s = Scene()
|
||
for _ in range(4):
|
||
s.place("2x4", 24)
|
||
for pid in ("p1", "p2", "p3", "p4"):
|
||
s.add_feature(pid, "miter", miter_deg=45)
|
||
kinds = [j.kind for j in suggest_jigs(s)]
|
||
assert "miter-sled" in kinds
|
||
|
||
|
||
def test_miter_geometry_removes_material():
|
||
pytest.importorskip("build123d")
|
||
from woodshop.geometry import part_solid
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
square_vol = part_solid(s.get_part("p1")).volume
|
||
s.add_feature("p1", "miter", miter_deg=45)
|
||
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
|
||
|
||
|
||
def test_miter_cuts_full_width_not_a_corner():
|
||
"""A 45° miter on a 2x4 should span the FULL width (edge-to-edge), not just
|
||
notch a corner — the removed wedge's width ≈ the board width."""
|
||
pytest.importorskip("build123d")
|
||
from woodshop.geometry import miter_cutter
|
||
from build123d import Box, Pos
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
p = s.get_part("p1")
|
||
t, w = p.section_in
|
||
feat = s.add_feature("p1", "miter", miter_deg=45)
|
||
board = Pos(24 / 2, 0, 0) * Box(24, w, t)
|
||
wedge = board & miter_cutter(feat, 24, w, t)
|
||
b = wedge.bounding_box()
|
||
y_extent = b.max.Y - b.min.Y
|
||
assert y_extent > 0.9 * w # spans (nearly) the full 3.5" width
|
||
|
||
|
||
def test_edit_feature_keeps_miter_on_an_end():
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
f = s.add_feature("p1", "miter", miter_deg=45)
|
||
s.edit_feature(f.id, face="top") # invalid for a miter — should be ignored
|
||
assert f.face in ("end_a", "end_b")
|
||
|
||
|
||
def test_center_miter_notches_a_corner():
|
||
"""A from-center 45° miter removes a corner triangle (~half the width),
|
||
unlike the full-width edge cut."""
|
||
pytest.importorskip("build123d")
|
||
from woodshop.geometry import miter_cutter
|
||
from build123d import Box, Pos
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
t, w = s.get_part("p1").section_in
|
||
f = s.add_feature("p1", "miter", miter_deg=45, from_center=True)
|
||
assert f.from_center is True
|
||
board = Pos(12, 0, 0) * Box(24, w, t)
|
||
wedge = board & miter_cutter(f, 24, w, t)
|
||
b = wedge.bounding_box()
|
||
assert (b.max.Y - b.min.Y) < 0.75 * w # only a corner, not full width
|
||
|
||
|
||
def test_two_center_miters_make_a_point():
|
||
"""+45 and −45 from centre on the same end bring it to a point (picket)."""
|
||
pytest.importorskip("build123d")
|
||
from woodshop.geometry import part_solid
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
full = part_solid(s.get_part("p1")).volume
|
||
s.add_feature("p1", "miter", miter_deg=45, from_center=True)
|
||
s.add_feature("p1", "miter", miter_deg=-45, from_center=True)
|
||
pointed = part_solid(s.get_part("p1")).volume
|
||
assert pointed < full # both corners gone -> a point
|
||
|
||
|
||
def test_from_center_roundtrips():
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
s.add_feature("p1", "miter", miter_deg=45, from_center=True)
|
||
s2 = Scene.from_dict(json.loads(json.dumps(s.to_dict())))
|
||
assert s2.get_part("p1").features[0].from_center is True
|
||
|
||
|
||
def test_chamfer_preview_shows_removed_bevel():
|
||
pytest.importorskip("pyvista")
|
||
pytest.importorskip("build123d")
|
||
from woodshop.viewer import feature_preview_mesh
|
||
s = Scene()
|
||
s.place("2x4", 24)
|
||
f = s.add_feature("p1", "chamfer", face="top", width_in=0.25)
|
||
mesh = feature_preview_mesh(s.get_part("p1"), f)
|
||
assert mesh is not None and mesh.n_points > 0 # the bevel slivers, not a slab
|