# v1.6 Track F — architectural 2D: the plan model **Date:** 2026-09-23 · **Status:** shipped (v1.6.0-dev, track F) · **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.