109 lines
3.3 KiB
Python
109 lines
3.3 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)
|
|
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"
|