- src/homemaker/ → src/homemaker_layout/; all imports updated - pyproject.toml: name = homemaker-layout, entry point updated - .beads/config.yaml: dolt sync.remote updated to homemaker-layout.git - Delete temporary debug/perl scripts from project root - README.md, DESIGN.md: package path references updated - GitHub repo renamed; git remote updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
186 lines
5.5 KiB
Python
186 lines
5.5 KiB
Python
"""Unit tests for geometry.py with known analytic values (oracle-free)."""
|
|
|
|
import math
|
|
|
|
import pytest
|
|
|
|
from homemaker_layout import geometry
|
|
from homemaker_layout.dom import Node
|
|
|
|
|
|
def _square(size: float = 10.0) -> Node:
|
|
geometry.clear_cache()
|
|
return Node(node=[[0.0, 0.0], [size, 0.0], [size, size], [0.0, size]])
|
|
|
|
|
|
def _rect(w: float, h: float) -> Node:
|
|
geometry.clear_cache()
|
|
return Node(node=[[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]])
|
|
|
|
|
|
def _divided(size: float = 10.0, split: float = 0.5):
|
|
"""Return (root, left, right) for a square split at `split`."""
|
|
geometry.clear_cache()
|
|
root = Node(
|
|
node=[[0.0, 0.0], [size, 0.0], [size, size], [0.0, size]],
|
|
division=[split, split],
|
|
)
|
|
left = Node(position="l")
|
|
right = Node(position="r")
|
|
root.left, root.right = left, right
|
|
left.parent = right.parent = root
|
|
return root, left, right
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def test_area_square():
|
|
assert geometry.area(_square(10.0)) == pytest.approx(100.0)
|
|
|
|
|
|
def test_area_rectangle():
|
|
assert geometry.area(_rect(6.0, 4.0)) == pytest.approx(24.0)
|
|
|
|
|
|
def test_area_divided_halves():
|
|
_, left, right = _divided(10.0, 0.5)
|
|
assert geometry.area(left) == pytest.approx(50.0)
|
|
assert geometry.area(right) == pytest.approx(50.0)
|
|
|
|
|
|
def test_area_divided_unequal():
|
|
_, left, right = _divided(10.0, 0.3)
|
|
assert geometry.area(left) == pytest.approx(30.0, rel=1e-9)
|
|
assert geometry.area(right) == pytest.approx(70.0, rel=1e-9)
|
|
|
|
|
|
def test_edge_length_square_all_equal():
|
|
r = _square(8.0)
|
|
for i in range(4):
|
|
assert geometry.edge_length(r, i) == pytest.approx(8.0)
|
|
|
|
|
|
def test_edge_length_rectangle():
|
|
r = _rect(6.0, 4.0)
|
|
assert geometry.edge_length(r, 0) == pytest.approx(6.0)
|
|
assert geometry.edge_length(r, 1) == pytest.approx(4.0)
|
|
assert geometry.edge_length(r, 2) == pytest.approx(6.0)
|
|
assert geometry.edge_length(r, 3) == pytest.approx(4.0)
|
|
|
|
|
|
def test_angle_square_corners_are_right_angles():
|
|
r = _square(10.0)
|
|
for i in range(4):
|
|
assert geometry.angle(r, i) == pytest.approx(math.pi / 2, abs=1e-9)
|
|
|
|
|
|
def test_angle_rectangle_corners_are_right_angles():
|
|
r = _rect(6.0, 4.0)
|
|
for i in range(4):
|
|
assert geometry.angle(r, i) == pytest.approx(math.pi / 2, abs=1e-9)
|
|
|
|
|
|
def test_aspect_square_is_one():
|
|
assert geometry.aspect(_square(10.0)) == pytest.approx(1.0)
|
|
|
|
|
|
def test_aspect_wide_rectangle():
|
|
assert geometry.aspect(_rect(2.0, 1.0)) == pytest.approx(2.0)
|
|
|
|
|
|
def test_aspect_tall_rectangle_is_same_as_wide():
|
|
assert geometry.aspect(_rect(1.0, 2.0)) == pytest.approx(2.0)
|
|
|
|
|
|
def test_length_narrowest_square():
|
|
assert geometry.length_narrowest(_square(5.0)) == pytest.approx(5.0)
|
|
|
|
|
|
def test_length_narrowest_rectangle():
|
|
assert geometry.length_narrowest(_rect(6.0, 3.0)) == pytest.approx(3.0)
|
|
|
|
|
|
def test_centroid_square():
|
|
cx, cy = geometry.centroid(_square(10.0))
|
|
assert cx == pytest.approx(5.0)
|
|
assert cy == pytest.approx(5.0)
|
|
|
|
|
|
def test_centroid_rectangle():
|
|
cx, cy = geometry.centroid(_rect(6.0, 4.0))
|
|
assert cx == pytest.approx(3.0)
|
|
assert cy == pytest.approx(2.0)
|
|
|
|
|
|
def test_boundary_id_root_returns_external_letters():
|
|
r = _square()
|
|
assert geometry.boundary_id(r, 0) == "a"
|
|
assert geometry.boundary_id(r, 1) == "b"
|
|
assert geometry.boundary_id(r, 2) == "c"
|
|
assert geometry.boundary_id(r, 3) == "d"
|
|
|
|
|
|
def test_boundary_id_left_child_internal_edge():
|
|
root, left, right = _divided()
|
|
# left child edge 1 (rid=1, position='l') → the division line → parent.id
|
|
assert geometry.boundary_id(left, 1) == root.id
|
|
assert geometry.boundary_id(left, 1) == "" # root id is '' (empty path)
|
|
|
|
|
|
def test_boundary_id_right_child_internal_edge():
|
|
root, left, right = _divided()
|
|
# right child edge 3 → division line → parent.id
|
|
assert geometry.boundary_id(right, 3) == root.id
|
|
|
|
|
|
def test_boundary_id_children_inherit_external_edges():
|
|
_, left, right = _divided()
|
|
assert geometry.boundary_id(left, 0) == "a"
|
|
assert geometry.boundary_id(left, 3) == "d"
|
|
assert geometry.boundary_id(right, 0) == "a"
|
|
assert geometry.boundary_id(right, 1) == "b"
|
|
|
|
|
|
def test_is_between_2d_midpoint():
|
|
assert geometry.is_between_2d([5.0, 0.0], [0.0, 0.0], [10.0, 0.0])
|
|
|
|
|
|
def test_is_between_2d_endpoint():
|
|
assert geometry.is_between_2d([0.0, 0.0], [0.0, 0.0], [10.0, 0.0])
|
|
|
|
|
|
def test_is_between_2d_outside():
|
|
assert not geometry.is_between_2d([11.0, 0.0], [0.0, 0.0], [10.0, 0.0])
|
|
|
|
|
|
def test_is_between_2d_none():
|
|
assert not geometry.is_between_2d(None, [0.0, 0.0], [10.0, 0.0])
|
|
|
|
|
|
def test_offset_quad_inward_shrinks_area():
|
|
corners = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]
|
|
inset = geometry.offset_quad(corners, -1.0)
|
|
# inward offset of a square → smaller square
|
|
assert inset[0][0] > corners[0][0] # x moves right
|
|
assert inset[0][1] > corners[0][1] # y moves up
|
|
assert inset[2][0] < corners[2][0] # top-right x moves left
|
|
assert inset[2][1] < corners[2][1] # top-right y moves down
|
|
|
|
|
|
def test_boundary_groups_single_leaf():
|
|
r = _square()
|
|
groups = geometry.boundary_groups(r)
|
|
# undivided root has no internal boundaries
|
|
assert len(groups) == 0
|
|
|
|
|
|
def test_boundary_groups_divided_root():
|
|
root, left, right = _divided()
|
|
groups = geometry.boundary_groups(root)
|
|
# one internal boundary between left and right
|
|
assert len(groups) == 1
|
|
contributors = list(groups.values())[0]
|
|
nodes = {leaf for leaf, _ in contributors}
|
|
assert left in nodes
|
|
assert right in nodes
|