The evidence for track F, in the shape tracks A, E and B + G used. - benchmarks/tasks_v6.py: 19 tasks, DEFAULT_MATRIX = "v6"; v5 to v2 stay addressable. arch_roundtrip draws the example plan with arch_plan_from_spec and reads it back without the spec: rooms_detect must find each labelled room within 0.1 % of its label and of the net floor computed by hand (4825 x 5750 and 3825 x 5750), the schedules must list D1, D2, W1, W2, and the plan is gated on a zero-issue critique and a finalize score of at least 90. A test removes one room label and the gate fails naming arch_room_unlabelled. - Four correctness checks, every number worked out by hand in the docstring: L / T / X junction corners and wall areas, a 4 x 5 m room labelled 20.00 m2 rather than its 21.84 m2 axis area, a door interrupting both faces, and a plan of plain lines read as 20 and 15 m2 at confidence 0.6. The published A/B report is re-run over 43 checks: 26 / 43 -> 43 / 43, zero regressed. - scripts/smoke_arch_com.py runs the whole plan once on a live seat and closes every document it opens; the CHANGELOG records what it returned. - The token suite measures a TOOL_PACKS=core,arch row; check_doc_numbers derives the task count from the v6 matrix; CLAUDE.md, both READMEs and the spec status line say what shipped. - fix(com): the first live smoke exited 1 - the room reader found 0 wall segments. Measured on AutoCAD 2026 (2026-09-23): with a makepy cache, ModelSpace.Item(i) returns the gen_py IAcadEntity wrapper (type(item).__name__ == "IAcadEntity") and item.StartPoint on an AcDbLine raises AttributeError, so every entity_list row lost its type-specific properties (28 wall LINEs, 0 with start/end). _entity_info now re-dispatches exactly that wrapper through _com_unnarrow; after the fix 38 of 53 rows carry start/end and the smoke exits 0 with both rooms at 27 743 750 / 21 993 750 mm2 and 0 critique issues. Re-dispatch measured at ~1.7 ms per entity (dynamic vs gencache alike). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
227 lines
12 KiB
Markdown
227 lines
12 KiB
Markdown
# 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.
|