A drawing somebody else made, read back as one report in which every claim carries its evidence and a confidence; the drawing is never modified. - units.py: $INSUNITS against the unit the geometry implies (text heights, wall thickness, door swings, labelled room areas, overall size). The header is overruled only when the evidence outweighs it, and then with a warning that quotes the numbers. Robust extents grow from the quartile box of entity centres; what never joins is an outlier, named by handle. - clusters.py: separate bodies of model-space content by raster, 8-connectivity and box absorption - two copies of one plan are two clusters. - describe.py: layers with drawn length and the vocabulary's meaning (name keyword, else the texts on the layer), equipment tags (one tag lettered twice beside its symbol merged, repeats flagged ambiguous), room labels in the wall-layer faces of engineering.arch.faces, text languages by character class, blocks, layouts, the title block. - server.py opens SECTION 26 (Understanding & QA) with drawing_understand: core, read-only, tagged analysis/query. Alias record, inventory, README release snapshot 242, frozen group sizes (analysis 13). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
87 lines
3.4 KiB
Python
87 lines
3.4 KiB
Python
"""Bodies of model-space content: two plan copies are two clusters."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from engineering.understand.clusters import find_clusters
|
|
from engineering.understand.snapshot import EntityRecord
|
|
|
|
EPS = 1e-9
|
|
|
|
|
|
def line(handle, a, b, layer="WALL"):
|
|
return EntityRecord(handle=handle, type="LINE", layer=layer, space="Model", points=(a, b))
|
|
|
|
|
|
def text(handle, value, x, y, height=250.0):
|
|
return EntityRecord(
|
|
handle=handle,
|
|
type="TEXT",
|
|
layer="TEXT",
|
|
space="Model",
|
|
points=((x, y),),
|
|
text=value,
|
|
height=height,
|
|
)
|
|
|
|
|
|
def plan(prefix, dx):
|
|
"""A 20 000 x 12 000 outline with a partition, a tag in each half and a
|
|
title under it, shifted ``dx`` along x. The tags float 4 000 from every
|
|
line - far more than one cell - so only the absorb step keeps them in."""
|
|
return [
|
|
line(f"{prefix}1", (dx, 0.0), (dx + 20000.0, 0.0)),
|
|
line(f"{prefix}2", (dx + 20000.0, 0.0), (dx + 20000.0, 12000.0)),
|
|
line(f"{prefix}3", (dx + 20000.0, 12000.0), (dx, 12000.0)),
|
|
line(f"{prefix}4", (dx, 12000.0), (dx, 0.0)),
|
|
line(f"{prefix}5", (dx + 10000.0, 0.0), (dx + 10000.0, 12000.0)),
|
|
text(f"{prefix}T1", "T4100", dx + 4000.0, 6000.0),
|
|
text(f"{prefix}T2", "T4200", dx + 14000.0, 6000.0),
|
|
text(f"{prefix}T3", "PLANT LAYOUT", dx + 2000.0, 12500.0, height=500.0),
|
|
]
|
|
|
|
|
|
def test_two_plan_copies_side_by_side_are_two_clusters():
|
|
# Copy A spans x 0..20 000, copy B 40 000..60 000: a 20 000 gap. The
|
|
# content box is 60 000 x 12 500 (the title's point box ends at its
|
|
# insertion, y 12 500), so a cell is 2 % (CELL_FRACTION) of the
|
|
# hypot(60 000, 12 500) = 61 288 diagonal, about 1 226 units, and the gap
|
|
# is over 16 empty cells.
|
|
clusters, membership = find_clusters([*plan("A", 0.0), *plan("B", 40000.0)])
|
|
assert len(clusters) == 2
|
|
left, right = sorted(clusters, key=lambda c: c["box"][0])
|
|
assert left["entities"] == right["entities"] == 8
|
|
assert left["box"] == pytest.approx([0.0, 0.0, 20000.0, 12500.0], abs=EPS)
|
|
assert right["box"] == pytest.approx([40000.0, 0.0, 60000.0, 12500.0], abs=EPS)
|
|
assert membership["AT1"] == membership["A1"] == left["id"]
|
|
assert membership["BT2"] == membership["B5"] == right["id"]
|
|
# The tallest text leads the labels.
|
|
assert left["labels"][0] == "PLANT LAYOUT"
|
|
assert left["layers"][0] == "WALL"
|
|
|
|
|
|
def test_ids_run_largest_first_then_left_to_right():
|
|
small = [line("S1", (100000.0, 0.0), (100500.0, 0.0))]
|
|
clusters, _ = find_clusters([*small, *plan("A", 40000.0), *plan("B", 0.0)])
|
|
assert [c["id"] for c in clusters] == ["C1", "C2", "C3"]
|
|
assert [c["box"][0] for c in clusters] == pytest.approx([0.0, 40000.0, 100000.0], abs=EPS)
|
|
assert [c["entities"] for c in clusters] == [8, 8, 1]
|
|
|
|
|
|
def test_excluded_handles_take_no_part():
|
|
far = line("FAR", (-5_000_000_000.0, 0.0), (-4_999_999_000.0, 0.0))
|
|
clusters, membership = find_clusters([*plan("A", 0.0), far], exclude={"FAR"})
|
|
assert len(clusters) == 1
|
|
assert "FAR" not in membership
|
|
assert clusters[0]["box"] == pytest.approx([0.0, 0.0, 20000.0, 12500.0], abs=EPS)
|
|
|
|
|
|
def test_nothing_to_cluster_is_an_empty_answer():
|
|
assert find_clusters([]) == ([], {})
|
|
|
|
|
|
def test_a_non_positive_cell_is_refused():
|
|
with pytest.raises(ValueError, match="cell: must be greater than zero"):
|
|
find_clusters(plan("A", 0.0), cell=-1.0)
|