Files
autocad-mcp-pro/tests/test_arch_catalogue.py
Umut CANandClaude Opus 5.5 afd42356b5 feat(task-7): SECTION 25 catalogue - arch_catalogue_list, arch_catalogue_insert
engineering/arch/catalogue.py is the standard-parts pattern of track B for an
architectural plan: twenty authored plan symbols in two families - thirteen
furniture items (single and double bed, wardrobe, three-seat sofa, armchair,
chair, dining table with four chairs, desk, kitchen counter run, fridge,
bookshelf, nightstand, coffee table) and seven sanitary items (WC, bidet, wall
basin, shower tray, bathtub, kitchen sink, washing machine). Every size is a
nominal catalogue dimension and says so in SIZE_BASIS, in each row and in both
tool docstrings; no standard is cited because none fixes these sizes.

Each item is typed block_define primitives on layer 0 inside [0, w] x [0, d],
origin at the back-left corner with the wall side along +X - a test holds all
twenty to their footprint. catalogue() searches name, family and the English
and Turkish labels, ignoring case and Turkish diacritics (buzdolabi finds
buzdolabı). insert_item() defines ARCH_<NAME> once, creates the furniture or
sanitary layer from its ARCH_LAYERS row when missing (live ActiveX refuses a
missing layer), inserts with an invisible ITEM attribute, and refuses an
unknown name with the nearest catalogue names before any write.

SECTION 25 opens on this branch with the two tools, alias records (INSERT,
ADCENTER, TOOLPALETTES and English/Turkish synonyms), a regenerated inventory,
the README snapshot at 231 tools, and the branch-local group snapshot
(entity_query 10, entity_creation 19) that the track F merge recomputes.

No new ActiveX call: insert_item composes the existing layer_list,
layer_create, block_list, block_define and block_insert members. Run once live
through ComBackend on AutoCAD 2026 (scratch document, closed after): all
twenty items defined and inserted, a second double_bed insert reported
defined=false, ITEM read back as double_bed, 20 ARCH_* blocks, 21 INSERTs on
A-FURN-E-N / A-SANR-E-N, both layers created at ACI 5 / 0.18 mm.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-23 22:35:44 +03:00

234 lines
9.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The furniture and sanitary catalogue: authored outlines, search, blocks.
Pure half first (every item's primitives against its declared nominal size,
the search in both languages, the refusal), then the insert through the
headless backend - the `backend` fixture in tests/conftest.py.
"""
from __future__ import annotations
import pytest
from backends.block_specs import validate_attdef_specs, validate_entity_specs
from engineering.arch.catalogue import (
CATALOGUE_NAMES,
FAMILIES,
ITEMS,
SIZE_BASIS,
block_spec,
catalogue,
insert_item,
)
from engineering.arch.layers import ARCH_LAYERS, ARCH_ROLE_LAYER
from engineering.mech.primitives import Arc, points_of
def _spec_points(spec: dict) -> list[tuple[float, float]]:
"""Every point that bounds one block_define spec - an arc by its real sweep."""
kind = spec["type"]
if kind == "line":
return [(spec["x1"], spec["y1"]), (spec["x2"], spec["y2"])]
if kind == "circle":
cx, cy, r = spec["cx"], spec["cy"], spec["r"]
return [(cx - r, cy), (cx + r, cy), (cx, cy - r), (cx, cy + r)]
if kind == "arc":
arc = Arc((spec["cx"], spec["cy"]), spec["r"], spec["start_deg"], spec["end_deg"])
return list(points_of(arc))
if kind == "polyline":
return [tuple(p) for p in spec["points"]]
raise AssertionError(f"unexpected spec type {kind!r}")
def test_the_catalogue_holds_twenty_items_in_two_families():
assert len(ITEMS) == 20
assert len(set(CATALOGUE_NAMES)) == 20
assert FAMILIES == ("furniture", "sanitary")
assert sum(item.family == "furniture" for item in ITEMS) == 13
assert sum(item.family == "sanitary" for item in ITEMS) == 7
assert {
"single_bed",
"double_bed",
"wardrobe",
"sofa_3_seat",
"armchair",
"dining_table_4",
"desk",
"kitchen_counter",
"fridge",
"bookshelf",
"wc",
"wall_basin",
"shower_tray",
"bathtub",
"kitchen_sink",
"washing_machine",
} <= set(CATALOGUE_NAMES)
for name in CATALOGUE_NAMES:
assert name == name.lower() and " " not in name
@pytest.mark.parametrize("name", CATALOGUE_NAMES)
def test_every_item_stays_inside_its_declared_nominal_size(name):
spec = block_spec(name)
w, d = spec["size"]
assert w > 0 and d > 0
for entity in spec["entities"]:
for x, y in _spec_points(entity):
assert -1e-9 <= x <= w + 1e-9, (name, entity)
assert -1e-9 <= y <= d + 1e-9, (name, entity)
for att in spec["attdefs"]:
assert 0.0 <= att["x"] <= w and 0.0 <= att["y"] <= d
@pytest.mark.parametrize("name", CATALOGUE_NAMES)
def test_every_block_spec_validates_against_block_define(name):
spec = block_spec(name)
normalised = validate_entity_specs(spec["entities"])
assert {entity["layer"] for entity in normalised} == {"0"}
(att,) = validate_attdef_specs(spec["attdefs"])
assert att["tag"] == "ITEM" and att["default"] == name and att["invisible"] is True
assert spec["name"] == "ARCH_" + name.upper()
assert spec["size_basis"] == SIZE_BASIS
def test_every_outline_reaches_its_declared_size():
"""The declared size is the item's footprint, not a loose bound."""
for name in CATALOGUE_NAMES:
spec = block_spec(name)
points = [p for entity in spec["entities"] for p in _spec_points(entity)]
w, d = spec["size"]
assert min(x for x, _ in points) == pytest.approx(0.0, abs=1e-9), name
assert max(x for x, _ in points) == pytest.approx(w, abs=1e-9), name
assert min(y for _, y in points) == pytest.approx(0.0, abs=1e-9), name
# the wc's bowl arc stops at y = 680 inside its 700 mm nominal depth
assert max(y for _, y in points) <= d + 1e-9, name
def test_the_wc_bowl_is_a_semicircle_joining_its_two_sides():
spec = block_spec("wc")
arc = next(e for e in spec["entities"] if e["type"] == "arc")
assert (arc["cx"], arc["cy"], arc["r"], arc["start_deg"], arc["end_deg"]) == (
190.0,
530.0,
150.0,
0.0,
180.0,
)
sides = [e for e in spec["entities"] if e["type"] == "line"]
assert [(s["x1"], s["y2"]) for s in sides] == [(40.0, 530.0), (340.0, 530.0)]
def test_the_sofa_cushions_split_the_seat_into_three_equal_widths():
spec = block_spec("sofa_3_seat")
verticals = sorted(
e["x1"] for e in spec["entities"] if e["type"] == "line" and e["x1"] == e["x2"]
)
assert verticals == [200.0, 800.0, 1400.0, 2000.0]
def test_search_by_english_word():
names = [row["name"] for row in catalogue("bed")]
assert names == ["single_bed", "double_bed"]
assert [row["name"] for row in catalogue("washing machine")] == ["washing_machine"]
def test_search_by_turkish_word_with_and_without_diacritics():
assert [row["name"] for row in catalogue("yatak")] == ["single_bed", "double_bed"]
assert [row["name"] for row in catalogue("buzdolabı")] == ["fridge"]
assert [row["name"] for row in catalogue("buzdolabi")] == ["fridge"]
assert [row["name"] for row in catalogue("KLOZET")] == ["wc"]
assert [row["name"] for row in catalogue("çamaşır")] == ["washing_machine"]
assert [row["name"] for row in catalogue("camasir")] == ["washing_machine"]
def test_search_every_word_must_match():
assert [row["name"] for row in catalogue("masa")] == ["dining_table_4", "desk"]
assert [row["name"] for row in catalogue("yemek masa")] == ["dining_table_4"]
assert catalogue("bed sink") == ()
def test_family_filter_and_the_label_language():
sanitary = catalogue(family="sanitary")
assert [row["name"] for row in sanitary] == [
"wc",
"bidet",
"wall_basin",
"shower_tray",
"bathtub",
"kitchen_sink",
"washing_machine",
]
assert all(row["layer"] == ARCH_ROLE_LAYER["sanitary"] for row in sanitary)
(row,) = catalogue("fridge", lang="tr")
assert row["label"] == "buzdolabı" and row["label_en"] == "fridge"
assert row["size"] == [600.0, 650.0]
assert row["size_basis"] == SIZE_BASIS
assert row["block"] == "ARCH_FRIDGE"
assert len(catalogue()) == 20
def test_unknown_family_and_language_are_refused_with_the_list():
with pytest.raises(ValueError, match="furniture, sanitary"):
catalogue(family="lighting")
with pytest.raises(ValueError, match="en, tr"):
catalogue(lang="de")
def test_an_unknown_name_is_refused_with_the_nearest_names():
with pytest.raises(ValueError, match=r"no item 'singlebed'; nearest: single_bed"):
block_spec("singlebed")
with pytest.raises(ValueError, match="the catalogue holds single_bed"):
block_spec("zzz")
def test_a_label_spelling_resolves_to_the_item():
assert block_spec("Single Bed")["item"] == "single_bed"
assert block_spec("washing-machine")["item"] == "washing_machine"
@pytest.mark.asyncio
async def test_insert_defines_the_block_once_across_two_inserts(backend):
first = await insert_item(backend, "double_bed", at=(1000.0, 2000.0))
assert first["ok"] is True
assert first["block_name"] == "ARCH_DOUBLE_BED"
assert first["defined"] is True
assert first["layer"] == ARCH_ROLE_LAYER["furniture"]
assert first["layer_created"] is True
assert first["size"] == [1600.0, 2000.0]
assert first["size_basis"] == SIZE_BASIS
second = await insert_item(backend, "double_bed", at=(4000.0, 2000.0), rotation=90.0)
assert second["defined"] is False
assert second["layer_created"] is False
assert second["rotation"] == 90.0
blocks = [b.name for b in await backend.block_list() if b.name == "ARCH_DOUBLE_BED"]
assert blocks == ["ARCH_DOUBLE_BED"]
inserts = await backend.entity_list(type_filter="INSERT", limit=100)
assert sorted(e.handle for e in inserts) == sorted([first["handle"], second["handle"]])
assert {e.layer for e in inserts} == {ARCH_ROLE_LAYER["furniture"]}
assert await backend.block_get_attributes(first["handle"]) == {"ITEM": "double_bed"}
@pytest.mark.asyncio
async def test_insert_creates_the_layer_from_its_arch_layers_row(backend):
result = await insert_item(backend, "wc", at=(0.0, 0.0))
layer = ARCH_ROLE_LAYER["sanitary"]
assert result["layer"] == layer
row = next(row for row in ARCH_LAYERS if row[0] == layer)
info = next(info for info in await backend.layer_list() if info.name == layer)
assert info.color == row[1]
@pytest.mark.asyncio
async def test_insert_refuses_before_writing_anything(backend):
before = len(await backend.entity_list())
with pytest.raises(ValueError, match="nearest: bathtub"):
await insert_item(backend, "bathtup", at=(0.0, 0.0))
with pytest.raises(ValueError, match="finite"):
await insert_item(backend, "bathtub", at=(float("nan"), 0.0))
with pytest.raises(ValueError, match="rotation must be finite"):
await insert_item(backend, "bathtub", at=(0.0, 0.0), rotation=float("inf"))
assert len(await backend.entity_list()) == before
assert "ARCH_BATHTUB" not in {b.name for b in await backend.block_list()}