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
234 lines
9.0 KiB
Python
234 lines
9.0 KiB
Python
"""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()}
|