"""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_kerf_gap_required_not_just_overlap(): from woodshop.cutplan import placement_fits s = Scene() s.place("2x4", 30) s.place("2x4", 30) plan = build_cut_plan(s) stick = next(sp for sp in plan.stock_pieces if not sp.is_sheet) p1, p2 = stick.placements k = plan.settings.kerf_in p2.x_in = p1.len_in + 0.01 # closer than a kerf assert not placement_fits(stick, p2, k) p2.x_in = p1.len_in + k # exactly a kerf apart assert placement_fits(stick, p2, k) def test_validate_flags_wrong_stock_and_illegal_rotation(): from woodshop.cutplan import relocate, rotate_placement s = Scene() s.place("2x4", 24) s.place("ply-3/4", 24, width_in=24) plan = build_cut_plan(s) lumber = next(sp for sp in plan.stock_pieces if not sp.is_sheet) sheet = next(sp for sp in plan.stock_pieces if sp.is_sheet) relocate(plan, lumber.placements[0].id, sheet.id, 0.0, 0.0) assert any("stock piece" in p for p in validate_cut_plan(plan)) plan2 = build_cut_plan(s, settings=ShopSettings(allow_plywood_rotation=False)) sh2 = next(sp for sp in plan2.stock_pieces if sp.is_sheet) rotate_placement(plan2, sh2.placements[0].id) assert any("rotation" in p for p in validate_cut_plan(plan2)) def test_recompute_updates_waste_after_move(): from woodshop.cutplan import recompute s = Scene() s.place("2x4", 30) s.place("2x4", 30) plan = build_cut_plan(s) stick = next(sp for sp in plan.stock_pieces if not sp.is_sheet) stick.placements[1].x_in = 60.0 # leave a gap after p1 recompute(plan) assert any(abs(w.x_in - 30) < 1.0 for w in stick.waste) # gap at ~30 now waste def test_stable_hash_is_deterministic(): from woodshop.cutplan import _stable_hash assert _stable_hash("ci1x") == _stable_hash("ci1x") def test_reoptimize_preserves_locked_placement(): from woodshop.cutplan import reoptimize s = Scene() for ln in (40, 40, 40): s.place("2x4", ln) plan = build_cut_plan(s) sticks = [sp for sp in plan.stock_pieces if not sp.is_sheet] locked = sticks[-1].placements[0] locked.locked = True lx, lid = locked.x_in, locked.id re = reoptimize(s, plan, "decreasing") kept = [p for sp in re.stock_pieces for p in sp.placements if p.id == lid] assert kept and kept[0].locked and abs(kept[0].x_in - lx) < 1e-6 placed = {p.item_id for sp in re.stock_pieces for p in sp.placements} assert {it.id for it in re.items} <= placed | set(re.unplaced) # nothing lost assert validate_cut_plan(re) == [] 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