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:
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.
|
||||
Reference in New Issue
Block a user