woodshop/tests/test_cutplan.py

151 lines
5.0 KiB
Python

"""Phase 0 tests for the CutPlan model."""
import json
from woodshop.cutplan import CutPlan, ShopSettings, build_cut_plan, validate_cut_plan
from woodshop.scene import Scene
def test_lumber_plan_packs_and_validates():
s = Scene()
for _ in range(3):
s.place("2x4", 40)
plan = build_cut_plan(s)
sticks = [sp for sp in plan.stock_pieces if not sp.is_sheet]
assert len(sticks) == 2
assert sum(len(sp.placements) for sp in sticks) == 3
assert plan.score["stock_count"] == 2
assert validate_cut_plan(plan) == []
def test_kerf_prevents_two_48_in_one_stick():
s = Scene()
s.place("2x4", 48)
s.place("2x4", 48)
assert build_cut_plan(s).score["stock_count"] == 2
def test_tenon_extends_cut_item_length():
s = Scene()
s.place("2x4", 24)
s.add_feature("p1", "tenon", face="end_b", depth_in=2)
item = build_cut_plan(s).items[0]
assert item.length_in == 26 and "tenon" in item.note
def test_plywood_plan_and_validate():
s = Scene()
s.place("ply-3/4", 40, width_in=20)
s.place("ply-3/4", 40, width_in=20)
plan = build_cut_plan(s)
sheets = [sp for sp in plan.stock_pieces if sp.is_sheet]
assert len(sheets) == 1 and len(sheets[0].placements) == 2
assert validate_cut_plan(plan) == []
def test_oversize_lumber_warns_and_is_unplaced():
s = Scene()
s.place("2x4", 120) # longer than a 96" stick
plan = build_cut_plan(s)
assert plan.unplaced and plan.warnings
assert validate_cut_plan(plan) == [] # flagged, so still valid
def test_stable_ids_present():
s = Scene()
s.place("2x4", 40)
plan = build_cut_plan(s)
assert all(it.id for it in plan.items)
assert all(sp.id for sp in plan.stock_pieces)
assert all(p.id for sp in plan.stock_pieces for p in sp.placements)
def test_json_roundtrip():
s = Scene()
s.place("2x4", 40)
s.place("ply-3/4", 40, width_in=20)
plan = build_cut_plan(s)
plan2 = CutPlan.from_dict(json.loads(json.dumps(plan.to_dict())))
assert plan2.settings.kerf_in == plan.settings.kerf_in
assert [sp.id for sp in plan2.stock_pieces] == [sp.id for sp in plan.stock_pieces]
assert plan2.score["stock_count"] == plan.score["stock_count"]
assert validate_cut_plan(plan2) == []
def test_plywood_rotation_fits_panel():
s = Scene()
s.place("ply-3/4", 30, width_in=60) # 60" wide > 48" sheet — needs rotating
plan = build_cut_plan(s) # rotation allowed by default
sheets = [sp for sp in plan.stock_pieces if sp.is_sheet]
assert len(sheets) == 1
p = sheets[0].placements[0]
assert p.rotated and p.len_in == 60 and p.wid_in == 30
assert validate_cut_plan(plan) == []
def test_rotation_disabled_flags_unfit():
s = Scene()
s.place("ply-3/4", 30, width_in=60)
plan = build_cut_plan(s, settings=ShopSettings(allow_plywood_rotation=False))
assert plan.unplaced and plan.warnings
def test_best_cut_plan_is_no_worse():
from woodshop.cutplan import _plan_key, best_cut_plan
s = Scene()
for ln in (50, 46, 30, 30, 20):
s.place("2x4", ln)
best = best_cut_plan(s)
base = build_cut_plan(s, strategy="decreasing")
assert _plan_key(best) <= _plan_key(base)
assert best.strategy == "optimized"
assert validate_cut_plan(best) == []
def test_snap_and_fits():
from woodshop.cutplan import placement_fits, snap_x
s = Scene()
s.place("2x4", 30)
s.place("2x4", 30) # both fit one stick
plan = build_cut_plan(s)
stick = next(sp for sp in plan.stock_pieces if not sp.is_sheet)
p1, p2 = stick.placements[0], stick.placements[1]
k = plan.settings.kerf_in
assert abs(snap_x(stick, p2, 31.0, k) - (p1.x_in + p1.len_in + k)) < 1e-6
p2.x_in = 0.0
assert not placement_fits(stick, p2, k) # now overlaps p1
p2.x_in = p1.len_in + k
assert placement_fits(stick, p2, k) # butted clear
def test_relocate_between_sticks():
from woodshop.cutplan import relocate
s = Scene()
for _ in range(3):
s.place("2x4", 60) # each needs its own stick
plan = build_cut_plan(s)
sticks = [sp for sp in plan.stock_pieces if not sp.is_sheet]
assert len(sticks) == 3
pid = sticks[2].placements[0].id
relocate(plan, pid, sticks[0].id, 0.0)
assert any(p.id == pid for p in sticks[0].placements)
assert all(p.id != pid for p in sticks[2].placements)
def test_rotate_placement_swaps_footprint():
from woodshop.cutplan import rotate_placement
s = Scene()
s.place("ply-3/4", 40, width_in=20)
plan = build_cut_plan(s)
p = next(sp for sp in plan.stock_pieces if sp.is_sheet).placements[0]
L, W, rot = p.len_in, p.wid_in, p.rotated
rotate_placement(plan, p.id)
assert p.len_in == W and p.wid_in == L and p.rotated != rot
def test_custom_settings_kerf():
s = Scene()
s.place("2x4", 48)
s.place("2x4", 48)
# zero kerf -> both 48" fit in one 96" stick
assert build_cut_plan(s, settings=ShopSettings(kerf_in=0.0)).score["stock_count"] == 1