"""Tests for the scene model and operations (no heavy 3D deps required).""" import math import pytest from woodshop.lumber import actual_section, normalize_stock from woodshop.scene import Scene, SceneError from woodshop.units import to_inches # ----- lumber ---------------------------------------------------------- def test_nominal_to_actual(): assert actual_section("2x4") == (1.5, 3.5) assert actual_section("4x4") == (3.5, 3.5) @pytest.mark.parametrize("raw,expected", [("2 x 4", "2x4"), ("2X4", "2x4"), ("2by4", "2x4")]) def test_normalize_stock(raw, expected): assert normalize_stock(raw) == expected def test_unknown_stock_lists_options(): with pytest.raises(KeyError, match="Known stock"): actual_section("9x9") # ----- units ----------------------------------------------------------- @pytest.mark.parametrize("value,unit,inches", [ ("6 ft", "inch", 72), ("6 foot", "inch", 72), ("10 inches", "inch", 10), ("3 ft 6 in", "inch", 42), ("2'", "inch", 24), ("72", "inch", 72), ("6", "foot", 72), (6, "foot", 72), ]) def test_to_inches(value, unit, inches): assert to_inches(value, default_unit=unit) == inches def test_to_inches_bad(): with pytest.raises(ValueError): to_inches("a bunch") # ----- operations ------------------------------------------------------ def test_place_sets_section_and_selection(): s = Scene() p = s.place("2x4", 72) assert p.id == "p1" assert p.section_in == (1.5, 3.5) assert s.selection == "p1" def test_the_example_sentence(): """'place a 6 foot 2x4, sand it, attach a 2 foot 2x4 at 90 deg, 10 in from end.'""" s = Scene() s.place("2x4", to_inches("6 ft")) # p1 s.finish("it") # sand the selection s.place("2x4", to_inches("2 ft")) # p2 (now selected) s.join("p1", "p2", angle_deg=90, offset_in=10, anchor="end_b") p1, p2 = s.get_part("p1"), s.get_part("p2") assert "sanded" in p1.finishes # attach point is 10in back from p1's far end (72 - 10 = 62 along +X) assert p2.position_in[0] == pytest.approx(62.0) # p2 rests on p1's top face: z = t_a/2 + t_b/2 = 0.75 + 0.75 assert p2.position_in[2] == pytest.approx(1.5) assert p2.rotation_deg == pytest.approx(90.0) # p2 now runs along +Y ux, uy = p2.axis_unit() assert ux == pytest.approx(0.0, abs=1e-9) assert uy == pytest.approx(1.0) assert len(s.joints) == 1 def test_resolve_it_without_selection_errors(): s = Scene() with pytest.raises(SceneError, match="selected"): s.finish("it") def test_undo_restores_previous_state(): s = Scene() s.place("2x4", 72) s.place("2x4", 24) assert len(s.parts) == 2 s.undo() assert len(s.parts) == 1 assert s.selection == "p1" def test_delete_reassigns_selection_and_drops_joints(): s = Scene() s.place("2x4", 72) s.place("2x4", 24) s.join("p1", "p2") s.delete("p2") assert [p.id for p in s.parts] == ["p1"] assert s.joints == [] assert s.selection == "p1" def test_roundtrip_serialization(tmp_path): s = Scene() s.place("2x4", 72) s.place("2x4", 24) s.join("p1", "p2", angle_deg=90, offset_in=10) path = s.save(tmp_path / "scene.json") loaded = Scene.load(path) assert [p.id for p in loaded.parts] == ["p1", "p2"] assert loaded.parts[0].section_in == (1.5, 3.5) assert loaded.joints[0].angle_deg == 90 assert loaded.selection == "p2"