merge: track F group R

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 23:03:13 +03:00
co-authored by Claude Opus 5.5
16 changed files with 2769 additions and 15 deletions
+156 -1
View File
@@ -9783,7 +9783,7 @@ async def drawing_export_dwg(
# ---------------------------------------------------------------------------
# ── SECTION 25: Architecture (4 tools) ──────────────────────────────────────
# ── SECTION 25: Architecture (7 tools) ──────────────────────────────────────
# ---------------------------------------------------------------------------
@@ -10068,6 +10068,161 @@ async def arch_dimension_chains(
# ── 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 lightweight polyline, both engines; anything else
there is listed in `skipped`), 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`), a point
inside a wall body, and a point in a room that already carries a label (one
face, one room record - a retried call does not add a second area to the
schedule).
"""
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 lightweight 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. Room labels already on the drawing are reported
in `labels` with the face they sit in; a face with more than one is listed
in `label_conflicts`.
Never modifies the drawing. Arcs and bulged polyline edges, old-style
POLYLINEs and block references are listed in `skipped` with their handle
and reason - never flattened into chords, never dropped silently. Read
`skipped` before trusting `count`. 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
@cad_tool(summary="Draw a door, window or room schedule from the plan records.", cost="mutate")
@mcp.tool(
annotations={"title": "Architecture: Schedule", "destructiveHint": False},
tags={"arch", "create"},
)
async def arch_schedule(
kind: Annotated[str, Field(description="doors | windows | rooms")],
x: Annotated[float, Field(description="WCS X of the table's top-left corner.")],
y: Annotated[float, Field(description="WCS Y of the table's top-left corner.")],
lang: Annotated[
str, Field(default="en", description="Headers and numbers: en | tr (decimal comma).")
] = "en",
scale: Annotated[
float,
Field(default=50.0, gt=0, description="Plot-scale denominator: 50 is 1:50."),
] = 50.0,
ctx: Context = None,
) -> dict:
"""Draw a door, window or room schedule as a real TABLE - a read of the drawing.
The rows come from the `ACADMCP_ARCH` records on the drawing (what
`arch_wall` / `arch_opening` / `arch_room` write), never from the caller:
doors by tag with width, height, swing, hand and wall; windows by tag with
width, height, sill and wall; rooms by number with name and the measured
area, plus a total. An opening drawn without a tag gets the next free D1 /
W1 (K1 / P1 in Turkish) in wall order; an explicit tag is kept. A value the
model does not carry is an empty cell, never a default. `representation`
reports what was drawn: `native` (ACAD_TABLE, live) or `composite` (rules
and MTEXT, headless).
Refused before anything is drawn: an unknown `kind` or `lang` (named with
the list), nothing of that kind on the drawing, and more rows than one
TABLE takes (198).
"""
from engineering.arch.schedule import draw_schedule
await ctx.info(f"arch_schedule: {kind} at ({x}, {y})")
try:
return await draw_schedule(_backend(ctx), kind, at=(x, y), lang=lang, scale=scale)
except ValueError as exc:
raise ToolError(str(exc)) from exc
# ── arch: catalogue (group C) ──