room-area-labels
by Cihad Paksoy
architecturefloor-planarearoomsannotation
Description
Writes each room's net internal area as an m² label CENTRED under the room name. Covers unit calibration (cm vs mm — get this wrong and the area is off by 10 000×), room detection, reading the style from an existing label, and the single-batch draw pattern. Use it for "label the room areas", "add m2 under the room names".
Example prompts
- /room-area-labels Write the area of each room under its name.
- /room-area-labels Add m2 labels to the rooms in the region I marked, put them on the AREA layer.
- /room-area-labels Label the room areas — two decimals, centred under the room name.
- /room-area-labels Calculate the net floor area of each room and annotate the plan.
Full content
---
name: room-area-labels
description: Writes each room's net internal area as an m² label CENTRED under the room name. Covers unit calibration (cm vs mm — get this wrong and the area is off by 10 000×), room detection, reading the style from an existing label, and the single-batch draw pattern. Use it for "label the room areas", "add m2 under the room names".
tags: [architecture, floor-plan, area, rooms, annotation]
---
When the user asks to write the room areas, follow this procedure. This is a **write** operation:
the preview → approval → commit chain applies.
The procedure **measures the drawing, it assumes nothing.** Every value below (layer name, text
height, offset, format) is read from that drawing; another office's drawing gives other values.
## 0. Scope: where is "the region I marked"?
When the user points at an area, do NOT invent the scope: a region **attached** to the message
(its label carries the bbox) wins if present; otherwise the user may have marked it in AutoCAD
with **CIZGENREGION** → call `get_marked_region` and use the returned bbox. If neither exists,
tell them to run CIZGENREGION; never make them re-pick something they already pointed at.
## 1. CALIBRATE the unit — skip this and everything is off by 10 000×
If `get_overview` returns `units_assumed: true` the drawing carries **no** unit information;
"mm" is a guess. Before writing any area, derive the unit from the geometry:
- **Wall thicknesses**: the distance between parallel wall faces. In the 8–30 range ⇒ **cm**; in
the 80–300 range ⇒ **mm**.
- **Door openings**: 70–120 ⇒ cm, 700–1200 ⇒ mm.
- A door/window tag already in the drawing ("90/210", "140") is already speaking **cm**.
Area is the square of the drawing unit: cm ⇒ **/10 000**, mm ⇒ **/1 000 000** → m². If the length
error is 2×, the area error is 4×; if the unit error is 10×, the area is off by **100×**. State
your conclusion in your answer ("I took the unit as cm, because the walls came out 10/20/25"). If
you can't be sure, don't write — ask the user.
## 2. Find the rooms and READ the `hint`
Call `find_spaces` with values you measured from the drawing — don't invent round numbers:
- `boundary_layers`: **structural/dividing layers only** — walls, columns, shear walls,
balustrades/parapets (otherwise balconies are never found). Never make a
furniture/equipment/fixture layer a boundary: the equipment outline then polygonizes as a
"room" and you write the equipment's area instead of the room's.
- `max_bridge_gap`: as wide as the widest door/passage opening
- `min_face_width`: a little more than the thickest wall
- `name_layers`: the layer the room names are on
If the returned `hint` is non-empty, **fix and call again** (one or two rounds):
`missing_boundary_layers` names the missing layer → add it if it is structural, do NOT add it if
it is an equipment layer. `merged_labels` tells you which rooms collapsed into one face → if you
can't separate them, don't label those rooms and **declare it**.
## 3. Which faces get an area and which don't
A room to be labelled is a face that **has a name and really is a space**. Do NOT write an area
for:
- **Shaft-type volumes** — lift shafts, light wells, flues, service shafts. These are not rooms;
worse, the equipment inside them (car, duct) can surface as a face, in which case your number is
the equipment's area, not the volume's.
- **Very small niches/recesses** — a wardrobe recess, a cloakroom nook. In practice faces well
under ~1 m² are not labelled; the label doesn't fit and the schedule gains nothing.
- **Texts that are not room names** — sheet titles, door/window tags ("SLIDING DOOR 90/210"),
level notes. These are not room names; if `find_spaces` mistook one for a name, don't label it.
**Declare** every face you skipped and why — if the office convention differs, let the user
correct you.
## 4. Read the style from an EXISTING label
If the drawing already has area labels (another floor, another block, another sheet), find one
with `read_texts` and take the layer, text height, format and offset relative to the room name
**from it** — don't invent a style. If there is none, these defaults are reasonable, but get the
user to **confirm** them:
| What | Default |
|---|---|
| Text height | **2/3** of the room name's height |
| Position | **1.1 × name height** BELOW the room name |
| Alignment | **CENTRED** on the room name (see below) |
| Format | `X.XX m2` (two decimals, space between number and unit) |
| Layer | whatever the user gave (else the layer of the existing labels, else ask) |
### Centring — it has two traps
The label is centred **under** the room name. Aligning it to the name's left corner is not
enough: because the name and the label have different lengths, the "KITCHEN / 16.93 m2" pair
looks skewed to the left.
**Trap 1 — the name's real width is NOT in `cad.texts()`.** That gives only the insertion point
(bottom-left corner). The real glyph box is in `cad.entities([handle])`:
`bbox: {min:[x,y], max:[x,y]}` — a rectangle, not a flat array. The name's centre:
```
cx = (bbox.min[0] + bbox.max[0]) / 2
```
**Trap 2 — `align:'center'` also changes the VERTICAL anchor.** The point you give becomes the
text's middle (centre horizontally, MIDDLE vertically) — whereas with left alignment it is the
baseline. Pass the same `y` and the label rides half a text height too high. The correction:
```
y = name.pos[1] - (1.1 * nameHeight - labelHeight / 2)
```
## 5. Draw in a single batch
A compute-and-draw job with many elements → `run_script`. The pattern (you can verify the key
names with `cad.schema()` / `cad.help('spaces')`):
```javascript
// ---- values you measured from the drawing ----
const DIV = 10000; // cm² → m² (1000000 if mm)
const MIN_M2 = 1.0; // faces under this count as niches, not labelled
const LAYER = 'AREA'; // layer the label goes to
const NAME_LAYER = 'TEXT'; // layer the room names are on
const SKIP = /lift|shaft|flue|light ?well|duct/i; // a volume, not a room
const OPT = { boundary_layers: ['WALL','COLUMN','PARAPET'], max_bridge_gap: 160,
min_face_width: 30, name_layers: [NAME_LAYER] };
const spaces = cad.spaces(OPT).filter((s) => s.name);
const texts = cad.texts({ layer: NAME_LAYER });
// The names' REAL boxes — required for centring. cad.texts gives only the insertion point.
const box = {};
for (const e of cad.entities(texts.map((t) => t.handle))) box[e.handle] = e.bbox;
const labels = [], skipped = [];
for (const s of spaces) {
const m2 = s.area / DIV;
if (m2 < MIN_M2 || SKIP.test(s.name)) { skipped.push([s.name, m2]); continue; }
// The room's NAME: the text inside this face whose content matches the name. Put the label
// UNDER it — not at the centre of the room's bbox; name and label read as a pair.
const name = texts.find((t) => t.content === s.name && s.contained.includes(t.handle));
const b = name && box[name.handle];
if (!name || !b) { skipped.push([s.name, 'name could not be placed']); continue; }
const h = name.height;
const lh = Math.round((h * 2) / 3);
labels.push({
type: 'text', layer: LAYER,
params: {
// x: the CENTRE of the name's box. y: align:'center' moves the anchor to the text's
// middle, so pull back half a text height from the baseline offset.
insertion: [(b.min[0] + b.max[0]) / 2, name.pos[1] - (1.1 * h - lh / 2)],
content: m2.toFixed(2) + ' m2',
height: lh,
align: 'center',
},
});
}
const w = cad.draw(labels); // ONE batch
result = { labeled: labels.length, skipped, op_id: w.op_id };
```
## 6. Check — then preview
Look with `render_and_verify`. In a busy plan the labels are unreadable in one overall image;
render the rooms you are unsure about **one at a time** (the room's bbox + margin). The defects to
look for:
- The label is not **centred** on the name (skewed left/right). The usual cause is using the
name's insertion point instead of its box — make sure you used the `bbox` from `cad.entities`.
- The label is not below the room name but above it / on top of it (you may have skipped the
vertical correction).
- The label spilled outside the room into the neighbour (happens in narrow rooms → reduce the
text height, or skip that room and declare it).
- Two labels in the same room (the same name matched two faces).
- The number doesn't look right: compare against a room's width × length. A 100× deviation is a
unit error — go back to step 1.
Then `create_preview` → STOP and wait for the user's approval → `commit_changes`.
## Declare in your answer
- Your unit assumption and why.
- How many rooms were labelled.
- Every skipped face and why (shaft / too small / name could not be placed / merged).
- Where you got the style from (an existing label, or the defaults).
## Known limitation
The `text` op of `draw` has **no text-style parameter**; the label is born with the drawing's
ACTIVE text style. If the existing area labels use a different style from the names, the font
difference remains. The user can change the style in bulk in AutoCAD after the commit; your job is
to get the height and position right.
## Example prompts
- /room-area-labels Write the area of each room under its name.
- /room-area-labels Add m2 labels to the rooms in the region I marked, put them on the AREA layer.
- /room-area-labels Label the room areas — two decimals, centred under the room name.
- /room-area-labels Calculate the net floor area of each room and annotate the plan.