feat(task-5): planar faces, measured room labels and the foreign-plan reader

engineering/arch/faces.py finds the closed faces a set of straight segments
bounds: endpoints within tol merge, every segment splits where another crosses
or touches it (the T-junction a partition makes), dangling edges are pruned,
and the half-edge walk turns to the next edge clockwise so every bounded face
comes out counter-clockwise. A piece nested inside a face of another piece is
a hole in it, so a ring of walls measures its body and a room with a column
measures its floor. Areas come from engineering.measure.polygon_area_perimeter,
the arithmetic analysis_measure_entity reports.

engineering/arch/rooms.py labels a room with the area of the face its point
lies in - a 4000 x 5000 room between 200 mm walls reads 20.00 m2, never the
axis area - and writes the room record onto the name text. Opening records
close door and window gaps along the wall faces, so two rooms either side of a
door stay two rooms. rooms_detect reads any plan's lines, drops faces under
min_area and thin wall bodies (mean width under 600 mm, a declared heuristic),
reports confidence 1.0 for faces enclosed only by our wall layer and 0.6
otherwise, lists arcs as skipped rather than flattening them, and never
modifies the drawing.

Two hardenings beyond the plan text, each with a test: read_records pages the
current space once and filters to the arch layers locally (the plan paged it
once per arch layer - twelve full scans, each a cross-process call per entity
on a live seat), keeping the same layer-then-drawing order; and an opening
record whose offset falls outside its wall (a stale record) is listed in
omitted instead of raising out of the read-only detector.

No new ActiveX member is called: both tools compose entity_list,
entity_get/set_xdata, layer_list and draw_prims, whose COM paths are already
live-verified.

SECTION 25 opens on this branch with arch_room and arch_rooms_detect under
the rooms marker; until Task 9 gives arch its group they file under
entity_creation and entity_query. Alias records, a regenerated inventory, the
README snapshot at 231 tools in 25 groups and the frozen group-size snapshot
follow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-23 22:33:37 +03:00
co-authored by Claude Opus 5.5
parent d5c5e90406
commit 67d578e09a
11 changed files with 1779 additions and 10 deletions
+112
View File
@@ -9780,6 +9780,118 @@ async def drawing_export_dwg(
return await _backend(ctx).drawing_export_dwg(str(validated), version)
# ---------------------------------------------------------------------------
# ── SECTION 25: Architecture (2 tools) ──────────────────────────────────────
# ---------------------------------------------------------------------------
# ── arch: rooms (group R) ──
@cad_tool(summary="Label a room with its name, number and measured area.", cost="mutate")
@mcp.tool(
annotations={"title": "Architecture: Label Room", "destructiveHint": False},
tags={"arch", "create"},
)
async def arch_room(
name: Annotated[str, Field(description="Room name, e.g. LIVING / SALON.")],
x: Annotated[
float, Field(description="WCS X of a point inside the room; the label starts here.")
],
y: Annotated[float, Field(description="WCS Y of a point inside the room.")],
number: Annotated[
str | None, Field(default=None, description="Room number, e.g. 01; omit for none.")
] = None,
room_id: Annotated[
str | None,
Field(default=None, description="Record id; default is the first free R<n>."),
] = None,
lang: Annotated[
str, Field(default="en", description="Label language: en | tr (tr writes 24,50 m²).")
] = "en",
scale: Annotated[
float,
Field(default=50.0, gt=0, description="Plot-scale denominator: 50 is 1:50 (text x 50)."),
] = 50.0,
ctx: Context = None,
) -> dict:
"""Label the room around (x, y) with its name, number and MEASURED area.
The area is never typed: the tool reads the wall faces on the `wall` layer
of the arch set (LINE and polyline, both engines), finds the closed face
the point lies in and writes that face's area - net of the wall thickness
and of any column inside it - into the label and into an `ACADMCP_ARCH`
room record on the name text, which `arch_schedule` reads. A door or window
recorded on the drawing closes its gap along the wall faces, so two rooms
either side of a door stay two rooms.
Refused before anything is drawn: an unknown `lang` (named with en, tr), a
`number`/`name` the room model refuses, a `room_id` already labelled on the
drawing, a point in no closed face of the walls (draw or close them first,
or read a plan drawn by other means with `arch_rooms_detect`), and a point
inside a wall body.
"""
from engineering.arch.rooms import label_room
room = {"name": name, "number": number, "at": [x, y]}
if room_id:
room["id"] = room_id
await ctx.info(f"arch_room: {name!r} at ({x}, {y})")
try:
return await label_room(_backend(ctx), room, lang=lang, scale=scale)
except ValueError as exc:
raise ToolError(str(exc)) from exc
@cad_tool(summary="Find the rooms of any plan and measure them; never modifies it.", cost="read")
@mcp.tool(
annotations={"title": "Architecture: Detect Rooms", "readOnlyHint": True},
tags={"arch", "query"},
)
async def arch_rooms_detect(
layers: Annotated[
list[str] | None,
Field(
default=None,
description="Layers holding the walls; default: every layer whose name contains "
"WALL or DUVAR.",
),
] = None,
min_area: Annotated[
float,
Field(default=1.0e6, gt=0, description="Smallest room reported, mm² (1e6 = 1 m²)."),
] = 1.0e6,
tol: Annotated[
float,
Field(default=1.0, gt=0, description="Endpoints closer than this (mm) are one corner."),
] = 1.0,
ctx: Context = None,
) -> dict:
"""Read the rooms of a plan - ours or a foreign one made of plain lines.
Every LINE and polyline on the layers is split where lines cross or touch,
and the closed faces they bound are measured (mm², net of nested pieces).
Faces under `min_area`, and thin faces (mean width under 600 mm - wall
bodies and reveals, this reader's heuristic) are left out. Each room
reports `confidence`: 1.0 when every edge lies on the arch wall layer, 0.6
when any edge is a plain line from elsewhere - read `confidence_min` before
trusting a foreign plan. A room label already on the drawing is reported
with the face it sits in.
Never modifies the drawing. Arcs and bulged polyline edges are listed in
`skipped`, never flattened into chords. A foreign plan's open doorways are
not closed - two rooms joined by one read as one face. Refused: a named
layer the drawing does not have (with the list of the ones it has), and no
layer matching WALL or DUVAR when `layers` is omitted.
"""
from engineering.arch.rooms import rooms_detect
try:
return await rooms_detect(_backend(ctx), layers=layers, min_area=min_area, tol=tol)
except ValueError as exc:
raise ToolError(str(exc)) from exc
# ---------------------------------------------------------------------------
# ── RESOURCES ───────────────────────────────────────────────────────────────
# ---------------------------------------------------------------------------