woodshop/tests/test_scene.py

111 lines
3.4 KiB
Python

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