"""homemaker-py-2g7.1: plan->dom composer tests. Synthetic fixtures only -- no real human trace exists yet (see DESIGN.md sec 37.x). These exercise the two acceptance-criteria halves: a slicible partition round-trips to a scoring .dom, and a non-slicible one is reported with the offending region rather than mis-parsed. """ from __future__ import annotations import textwrap import pytest from homemaker_layout import dom, geometry from homemaker_layout.compose import LabelError, NonSlicible, StoreyTrace, compose, parse_svg BOUNDARY_YAML = textwrap.dedent( """\ node: [[0.0, 0.0], [10.0, 0.0], [10.0, 8.0], [0.0, 8.0]] perimeter: {a: null, b: null, c: null, d: null} height: 3.0 elevation: 0.0 wall_inner: 0.08 wall_outer: 0.25 rotation: 0 """ ) # Plot is 10x8. Cut 1 (axis 0, vertical) at x=4 splits into a left column # (x:0-4, full height) and a right column (x:4-10). Cut 2 (axis 1, # horizontal) at y=5 splits the right column into a bottom room (y:0-5) and # a top room (y:5-8) -- exercises both axes and depth-2 recursion. GOOD_SVG = textwrap.dedent( """\ fr1 k1 b1 """ ) # Same partition, endpoints perturbed by < 0.15 (default tol) to exercise # snapping: a hand-drawn line that overlaps/undershoots slightly. SLOPPY_SVG = textwrap.dedent( """\ fr1 k1 b1 """ ) # One dangling interior line that touches neither pair of opposite edges -- # not a guillotine cut of the plot, and there is no other line to try. NON_SLICIBLE_SVG = textwrap.dedent( """\ """ ) def _write(tmp_path, name, content): p = tmp_path / name p.write_text(content) return p def test_composes_synthetic_partition_and_scores(tmp_path): boundary_path = _write(tmp_path, "boundary.dom", BOUNDARY_YAML) svg_path = _write(tmp_path, "plan.svg", GOOD_SVG) boundary = dom.load(str(boundary_path)) storeys = parse_svg(str(svg_path)) root = compose(boundary, storeys) leaves = root.leaves() assert sorted(leaf.type for leaf in leaves) == ["b1", "fr1", "k1"] # round-trips through the .dom text format out_path = tmp_path / "plan.dom" dom.dump(root, str(out_path)) reloaded = dom.load(str(out_path)) reloaded_types = sorted(leaf.type for leaf in reloaded.leaves()) assert reloaded_types == ["b1", "fr1", "k1"] # geometry is sane: leaf areas sum to the (wall-inset) plot area total = sum(geometry.area(leaf) for leaf in reloaded.leaves()) assert total == pytest.approx(geometry.area(reloaded), rel=1e-9) # scores cleanly through the native fitness engine (no config on disk: # unconstrained score, just confirms it runs end-to-end without raising) from homemaker_layout.fitness import Fitness fitness = Fitness({}, {}) score, failures = fitness.score_with_fails(reloaded) assert isinstance(score, float) assert len(failures) > 0 # unconstrained rooms + no programme: expected fails def test_snaps_sloppy_hand_traced_lines(tmp_path): boundary_path = _write(tmp_path, "boundary.dom", BOUNDARY_YAML) svg_path = _write(tmp_path, "plan.svg", SLOPPY_SVG) boundary = dom.load(str(boundary_path)) storeys = parse_svg(str(svg_path)) root = compose(boundary, storeys, tol=0.15) assert sorted(leaf.type for leaf in root.leaves()) == ["b1", "fr1", "k1"] # a tighter tolerance than the sketch's slop should fail to find the cuts boundary2 = dom.load(str(boundary_path)) with pytest.raises((NonSlicible, LabelError)): compose(boundary2, storeys, tol=0.01) def test_non_slicible_region_is_reported(tmp_path): boundary_path = _write(tmp_path, "boundary.dom", BOUNDARY_YAML) svg_path = _write(tmp_path, "plan.svg", NON_SLICIBLE_SVG) boundary = dom.load(str(boundary_path)) storeys = parse_svg(str(svg_path)) with pytest.raises(NonSlicible) as excinfo: compose(boundary, storeys) exc = excinfo.value assert exc.storey == 0 # names the offending region: should be the whole plot (wall-inset # corners), since the dangling line doesn't localise to a sub-quad assert len(exc.corners) == 4 assert "region around" in str(exc) def test_label_count_mismatch_is_reported(tmp_path): svg = textwrap.dedent( """\ """ ) boundary_path = _write(tmp_path, "boundary.dom", BOUNDARY_YAML) svg_path = _write(tmp_path, "plan.svg", svg) boundary = dom.load(str(boundary_path)) storeys = parse_svg(str(svg_path)) with pytest.raises(LabelError) as excinfo: compose(boundary, storeys) assert excinfo.value.storey == 0 assert excinfo.value.labels == [] def test_parse_svg_rejects_missing_storey_layers(tmp_path): svg = textwrap.dedent( """\ """ ) svg_path = _write(tmp_path, "plan.svg", svg) with pytest.raises(ValueError): parse_svg(str(svg_path)) def test_compose_rejects_storey_count_mismatch(tmp_path): boundary_path = _write(tmp_path, "boundary.dom", BOUNDARY_YAML) boundary = dom.load(str(boundary_path)) with pytest.raises(ValueError): compose(boundary, [StoreyTrace(), StoreyTrace()])