docs: track F (architectural 2D) design spec — the plan model

One plan model — a wall network with hosted openings — and one engine that
resolves L/T/X junctions, cuts openings, hatches cut walls by material and
computes rooms as the faces the walls enclose, so areas are measured, never
typed. A pure planar-face finder reads rooms from foreign plans on both
engines (the live engine has no boundary_trace). Furniture/sanitary catalogue,
grid, symbols, exterior dimension chains, schedules from the model.

12 tools (SECTION 25, pack arch), English by default with lang="tr", poché by
material, three arch_* critique focuses, benchmark arch_roundtrip.

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 14:40:34 +03:00
co-authored by Claude Opus 5.5
parent 2bba18d0f7
commit 83f192ac2f
@@ -0,0 +1,226 @@
# v1.6 Track F — architectural 2D: the plan model
**Date:** 2026-09-23 · **Status:** design approved in conversation ·
**Parent:** `2026-09-15-v1.6-roadmap-design.md` (track F) · **Baseline:** main
after tracks B + G (`2bba18d`, 229 tools, 4005 tests)
## 1. Summary
An architectural plan is not a set of lines: a door **opens the wall it sits
in**, two walls that meet **join into one clean corner**, and a room's area is
**the floor the walls enclose** — a number that must be measured, never typed.
Drawing a plan from primitives gets all three wrong, silently: the door lies on
top of an uncut wall, the corner overlaps, and the area in the label is whatever
the model wrote.
This track applies the lesson of tracks B + G: one **plan model** and one
**engine**, not a zoo of element tools. The model is a network of walls (an
axis polyline, a thickness, a justification and a material) with **hosted
openings** (doors and windows placed by distance along their wall). The engine
turns it into clean wall outlines — L, T and X junctions resolved, openings cut,
cut faces hatched by material — and **rooms are computed** as the faces the wall
outlines enclose. Door, window and room schedules are read back from the model;
a separate reader finds rooms in a **foreign** plan made of plain lines. Both
run on both engines by construction: the planar-face finder is pure geometry
and does not need `boundary_trace`, which the live engine does not have.
Around it: a furniture and sanitary block catalogue (the standard-parts pattern
of track B), the structural grid, the architectural symbols and exterior
dimension chains.
**Tool budget: +12 → 241.** New pack `arch` (SECTION 25). Labels and schedules
speak English by default and Turkish with `lang="tr"` (the user's decision,
2026-09-23), including the decimal comma.
## 2. Scope
**In:** SECTION 25 Architecture (12 tools, §8), the `arch` ISO 13567 layer set,
three `arch_*` critique focuses, benchmark task `arch_roundtrip`, correctness
checks and a live COM smoke.
**Out of 1.6 (recorded):**
- AutoCAD Architecture / Revit objects (AEC walls, door styles), IFC, 3D walls,
generated elevations and sections — the section mark is a symbol; no view is
derived from a plan.
- Curved walls. A wall axis is a polyline of straight segments; an arc segment
is refused by name rather than approximated.
- Annotative objects (recorded in track E): text heights are computed from the
plot scale instead (`scale="1:50"` → 2.5 mm × 50 = 125 drawing units).
- Code compliance (fire escape widths, accessibility, national stair rules):
the stair tool checks the Blondel relation and reports it; it does not claim
a building code.
## 3. Architecture
```
engineering/arch/
__init__.py
layers.py the `arch` ISO 13567 layer set + role → layer map
materials.py wall materials → ISO 128-50 area hatch (brick, aac, concrete, …)
lang.py EN / TR vocabulary: schedule headers, designations, area format
model.py Wall, Opening, Stair, Room — dataclasses, validation, ACADMCP_ARCH payload
walls.py the wall engine: offset outlines, junction clean-up, opening cuts, poché
openings.py door and window geometry (leaf, swing, frame, glazing, sill)
stairs.py straight / L / U stair plan symbols, the Blondel check
faces.py the planar-face finder: segments → closed faces (DCEL), areas
rooms.py room labels (measured area), the foreign-plan reader
schedule.py door / window / room schedules from the model
catalogue.py furniture + sanitary blocks as typed primitive specs (block_define)
symbols.py grid bubbles, north arrow, section mark, level mark
dimension.py exterior dimension chains (openings / walls / overall)
draw.py the async layer — the only module that touches a backend
critique.py the three arch_* critique focuses
```
Everything but `draw.py` is **pure**: numbers in, primitive descriptions out,
using the typed primitives of `engineering/mech/primitives.py` (Line, Arc,
Poly, HatchArea, Text, DimIntent) with the arch role → layer map. No new
contract member: every tool composes the generic ones (`entity_create_*`,
`dimension_*`, `block_define`, `entity_set_xdata`, `entity_list`), so it works
on both engines as written, exactly like the mechanical tools.
The plan is written onto the drawing as `ACADMCP_ARCH` XDATA (the chunked codec
of track B, generalised to take an application id): a wall record on each wall
outline, an opening record on each opening, a room record on each label — so a
schedule is a *read* of the drawing, and a second `arch_opening` call months
later finds the wall it cuts.
## 4. The plan model
```python
class Wall: # frozen dataclass
id: str
axis: tuple[Pt, ...] # 2+ points, straight segments, WCS
thickness: float # > 0
justification: "center" | "left" | "right" # side of the axis, looking along it
material: str = "brick" # → poché hatch
closed: bool = False
class Opening:
id: str
wall: str # the Wall id it is hosted in
kind: "door" | "window"
offset: float # along the wall axis, from its start, to the opening's near jamb
width: float
swing: "in" | "out" = "in" # doors
hand: "left" | "right" = "left"
sill: float | None = None # windows, informational (schedule)
height: float | None = None # schedule
tag: str | None = None # D1 / W1 (EN), K1 / P1 (TR) when omitted
class Stair:
id: str
start: Pt; direction_deg: float; width: float
risers: int; riser_height: float; going: float
kind: "straight" | "l" | "u" = "straight"; turn: "left" | "right" = "left"
class Room:
id: str; name: str; number: str | None; at: Pt # a point inside the face
area: float # measured, written by the tool
```
Validation before any write (the repository rule): finite positive numbers by
path (`walls[2].thickness`), an axis of ≥ 2 distinct points with no zero-length
segment, an opening whose `offset + width` lies inside its wall segment and does
not straddle a vertex, two openings on one wall that overlap refused with both
ids, an unknown material or language refused with the list.
## 5. The wall engine
- **Outlines.** Each segment is offset by the justification into two faces.
- **Junctions.** Where segments meet — within one wall or between walls — the
faces are trimmed or extended to their intersections: an L corner mitres, a T
junction stops the stem's faces on the crossing wall's face, an X junction
cuts all four. A junction the engine cannot resolve (a segment ending inside
another wall at a shallow angle below 5°) is refused by name, not overlapped.
- **Openings.** An opening removes its width from both faces of its wall and
closes the two jambs; a door draws its leaf at 90° and the swing as a quarter
arc on the swing side; a window draws the frame line on each face and two
glazing lines between.
- **Poché.** Each closed cut-face region between openings and junctions is a
`HatchArea` with the material's ISO 128-50 pattern; `poche=False` draws the
outlines only.
- **Honesty.** Anything the engine cannot place is returned in
`omitted: [{element, reason}]` — never silently dropped.
## 6. Rooms
`faces.py` builds a planar graph (a doubly connected edge list) from line and
polyline segments: segments are split at every intersection, near-coincident
endpoints are merged within a tolerance (default 1 mm), and the bounded faces
are walked. A room is the face containing the label point; its area is the
face's polygon area and is written into the label and the `room` record.
`arch_rooms_detect` runs the same finder on a **foreign** plan — any lines and
polylines on the layers it is given (default: every layer whose name contains
`WALL` or `DUVAR`) — and returns faces above a minimum area with their areas,
centroids and a `confidence` (1.0 for a face enclosed only by our walls, 0.6
for plain lines), the way `pid_graph` does. It never modifies the drawing.
## 7. Schedules, symbols, catalogue, dimensions
- `arch_schedule(kind="doors"|"windows"|"rooms", at, lang)` reads the
`ACADMCP_ARCH` records and draws a real TABLE (the parts-list code of track G):
doors and windows by tag with width, height, swing/hand and wall; rooms by
number with name and measured area, plus the total.
- `arch_symbol(kind="north_arrow"|"section_mark"|"level"|"elevation_mark", …)`.
- `arch_grid(x_axes, y_axes, labels)` — axis lines on the grid layer with
bubbles at both ends; numbers one way and letters the other (ISO 4157-style).
- `arch_catalogue_list` / `arch_catalogue_insert` — ~20 authored blocks in two
families: furniture (bed single/double, wardrobe, sofa, armchair, dining table
+ chairs, desk, kitchen counter, fridge) and sanitary (WC, basin, shower tray,
bathtub, kitchen sink). Sizes are **nominal catalogue dimensions stated in the
block's own data**, not standards values, and the tool says so.
- `arch_dimension_chains(sides, offsets)` — the three exterior chains an
architectural plan carries: openings, wall segments / axes, overall.
## 8. Tool surface
| # | Tool | What it does |
|---|---|---|
| 1 | `arch_wall` | add walls to the plan network and redraw the junctions they touch |
| 2 | `arch_opening` | host a door or a window in a wall (`kind=`), cutting it |
| 3 | `arch_stair` | a straight / L / U stair in plan, with the Blondel check |
| 4 | `arch_room` | label a room with its measured area |
| 5 | `arch_rooms_detect` | read rooms and areas from any plan (never modifies) |
| 6 | `arch_schedule` | door / window / room schedule table |
| 7 | `arch_grid` | structural grid with bubbles |
| 8 | `arch_symbol` | north arrow, section mark, level, elevation mark |
| 9 | `arch_catalogue_list` | search the furniture / sanitary catalogue |
| 10 | `arch_catalogue_insert` | insert a catalogue block |
| 11 | `arch_dimension_chains` | the exterior dimension chains |
| 12 | `arch_plan_from_spec` | walls + openings + stairs + rooms + chains + schedules in one transaction |
Pack `arch` holds all twelve. Lean gains `arch_wall`, `arch_opening`,
`arch_room`, `arch_plan_from_spec` (→ 64). `drawing_apply_iso_layers("arch")`
applies the new layer set. Every tool has an alias record with the AutoCAD
commands a drafter would reach for (WALL, DOOR, WINDOW, STAIR, BOUNDARY, AREA)
and shop synonyms in both languages (duvar, kapı, pencere, merdiven, oda, alan,
kat planı, vaziyet).
## 9. Critique focuses
- `arch_room_unlabelled` — a closed face above the minimum area with no room
label inside it.
- `arch_wall_gap` — two wall ends within 50 mm of each other that are not
joined: the room leaks and its area is wrong.
- `arch_opening_clash` — two openings overlapping on one wall, or an opening
that runs past its wall segment.
All three return nothing on a drawing with no `ACADMCP_ARCH` records.
## 10. Evidence
- **Benchmark `arch_roundtrip`** (roadmap evidence for F): a two-room plan with
an entrance door, an interior door, two windows and a stair drawn by
`arch_plan_from_spec`; `arch_rooms_detect` reads it back and each room's area
agrees with the model's within 0.1 %; the door and window schedules list the
drawn tags; `drawing_critique(focus=None)` returns 0 issues and
`drawing_finalize` scores ≥ 90. Matrix v6 (19 tasks).
- **Correctness checks:** `arch_junction_l_t_x` (outline vertices of the three
junction kinds against hand-computed coordinates), `arch_room_area_net`
(a 4 × 5 m room between 200 mm walls measures 20.00 m², not the axis area),
`arch_opening_cuts_wall` (the wall faces are interrupted across the opening),
`arch_rooms_detect_foreign` (a plan of plain lines yields the same areas).
- **Live smoke** `scripts/smoke_arch_com.py` on AutoCAD 2026.