All community skills

window-placement

by Cihad Paksoy

architecturefloor-planwindowswallsblocks

Description

Places windows into the walls of a floor plan — breaks the wall to open a gap, closes the jambs, and seats the window block in the opening. Finds the block definition in the drawing, MEASURES its width, and verifies rotation/mirroring on a single instance before batching the rest. Use it for "draw the windows", "put windows on this facade".

Example prompts

  • /window-placement Draw one window in every room in the region I marked.
  • /window-placement Put windows on the south facade of this apartment.
  • /window-placement Add windows to the bedrooms using the block already defined in this drawing.
  • /window-placement Place a balcony door where the living room meets the balcony.

Full content

---
name: window-placement
description: Places windows into the walls of a floor plan — breaks the wall to open a gap, closes the jambs, and seats the window block in the opening. Finds the block definition in the drawing, MEASURES its width, and verifies rotation/mirroring on a single instance before batching the rest. Use it for "draw the windows", "put windows on this facade".
tags: [architecture, floor-plan, windows, walls, blocks]
---

Placing a window is **not just a block insertion**. If the wall is drawn as an unbroken line,
dropping the block on top of it is wrong: in a real drawing the wall is broken, a gap is opened,
the jambs are closed, and the block seats into that gap. This procedure produces that.

This is a **write** operation: the preview → approval → commit chain applies.

## Scope: where is "the region I marked"?

When the user points at an area ("the region I marked", "over there"), do NOT invent the scope —
it comes from one of two places:

1. A region **attached** to the message (the panel's region chip; its label carries the bbox) —
   this wins if present.
2. Otherwise the user may have marked it in AutoCAD with the **CIZGENREGION** command: call
   `get_marked_region` and the returned bbox is the scope.

If neither exists, tell the user to run CIZGENREGION. Never make them re-pick something they
already pointed at.

## The anatomy of a window

In a double-line wall a window is four things:

1. The **outer face line** is broken at both ends of the gap (two pieces remain).
2. The **inner face line** is broken at the SAME two coordinates.
3. A **jamb line** is drawn at each end of the gap, spanning the wall thickness — on the same
   layer as the wall. Without these the wall reads as open-mouthed.
4. The **block** seats into the gap; its projecting parts (sill, balcony slab) face OUTWARD.

**The gap is NOT the block's bbox width.** A window frame usually laps onto the wall: the block's
box comes out a few units wider than the gap on each side. How much varies by office and even
from block to block — for some it is zero. Mistake the bbox for the gap and you cut too much wall
away.

Find the right gap in this order:

1. **Measure it from an existing seated instance** (most reliable): if the drawing has an
   instance of that block already sitting in a wall, measure the OPENING between the broken wall
   lines around it. Work out the difference between the block's box and that opening (the frame
   lap) and apply the same difference. Measure the lap SEPARATELY per block — it can differ
   within one drawing.
2. **The block name** carries the nominal opening in most offices — use it to cross-check step 1.
3. If neither exists, ask the user. Do NOT use the bbox as the gap.

## Procedure

### 1. Find the block — drawing one from scratch is the LAST resort

Look at `get_overview`'s `block_names` list. In order of preference:

1. **A window block already defined in the drawing** — use it (`insert_blocks`). Most project
   files have one; it is the user's office standard, so it is the right answer.
2. Otherwise, if there is sample window geometry in the drawing, clone it **losslessly** with
   `define_block(source_handles)`.
3. If the user has a catalogue, take it from there.
4. If none of those, ASK the user — which window, and how wide. Drawing a window yourself is the
   last option, and declare that you drew it.

If there are several candidate blocks (different widths, balcony door vs window) ask the user
which goes where, or choose by room type and **declare** the choice.

### 1c. Window blocks can be ANONYMOUS — never filter by name

AutoCAD gives copied / exploded-and-reblocked definitions anonymous names like `A$C…`. In a real
project some windows carry a meaningful name (e.g. one encoding the width) while others are
anonymous (`A$C…`) — filter by name and you miss half of them.

Recognise window candidates by **how they are used**, not by their names:

- Is the block instance seated in a wall opening (broken wall lines around it)?
- Blocks sharing the same **depth** are the same family: within a window family the depth follows
  the wall thickness and so stays constant while the width varies. Group the block boxes by
  depth — several widths collecting at a single depth are one family.
- The layer is a hint but not sufficient on its own; windows in one drawing may be spread across
  several layers.

List the family you found by width; read which width is used in which room type from an existing
plan (e.g. wide in the living room, narrow in a wet room) and **declare** it.

**The family may also be ONE DYNAMIC block.** Then the different widths are not separate block
names but parameter values of the same block. On an instance, `get_entities` returns
`dynamic_properties` (parameter → current value), `dynamic_property_choices` (the values each one
accepts) and `dynamic_readonly`; a static block carries none of these fields. Never conclude
"there is only one width" from the block name — read an instance's dynamic parameters first. List
the variants from `dynamic_property_choices` and declare which one you used where.

### 1d. Is there a SOLVED copy in the drawing?

Project files often hold several copies of the same plan: one raw, one with the windows already
placed (another floor, an earlier revision, a dimensioned sheet). If one exists it is the most
valuable source there is — the window positions, block choices and rotations are already that
office's decisions. Don't rediscover them, **copy them**.

How to find it: read the room names, spot the same sequence of names repeating in a second
cluster, and compute the constant offset (Δx, Δy) between the two clusters. If the wall geometry
matches apart from that offset, it is the same plan.

How to use it: shift the solved copy's wall lines by the offset and take the **difference**
against the raw plan's. That difference IS the window work: the unbroken lines to delete plus the
pieces and jambs to draw. Apply the same offset to the blocks.

**If you used this, DECLARE it** — the result is a copy, not a derivation. Also check the closure
numerically: `raw line count − deleted + added = the solved copy's line count`. If it doesn't
balance, the difference was extracted incompletely.

If there is NO solved copy the steps below already give the full route; this shortcut is a
convenience, not a prerequisite.

### 2. MEASURE the block — do NOT trust its name

The real size:

- If an instance of the block exists in the drawing: get its handle with `cad.query` →
  `cad.entities([handle])` → `bbox: {min:[x,y], max:[x,y]}` → width = `max[0] - min[0]` (look at
  the height instead if the block is rotated). **That is the FRAME size, not the gap** — for the
  gap go back to the anatomy section above.
- **On a dynamic block you measured only THAT instance's variant.** The same block yields a
  different width at another parameter value; measure every variant you intend to use (place one,
  read its bbox, delete it).
- If there is no instance: place one in an empty area, read its bbox, delete it when done.

**Declare the frame width and the derived gap separately** — "the block box is ⟨F⟩, the wall
opening is ⟨G⟩, the frame laps ⟨(F−G)/2⟩ per side".

### 3. Decide the wall and the position of the gap

**First work out which rooms even have a facade.** "A window in every room" cannot be taken
literally: in a floor plan half the rooms are internal (hall, corridor, cloakroom, bathroom,
shower, lift shaft, light well, service shaft). A window only goes in a room that has a wall
**facing outside**.

For each room, look at what lies beyond each of its four sides (a neighbouring room, or the
outside). Sort the result into three groups and **declare** it:

- **has a facade** → gets a window,
- **opens onto a balcony/light well** → not a window but a balcony-door type block,
- **internal room** → gets NO window.

Once you have that split, **carry on WITHOUT asking**: do the ones that are possible and list the
ones that aren't, with the reason. Even when the user said "every room", doing what is possible
and reporting the difference beats asking about the impossible part.

A window goes in an **external wall** (the face of the room that looks outside). For each room:

- Find the outward-facing edge from the room's `find_spaces` bbox (if there is no other room
  beyond that edge, it is an external wall).
- Read that edge's **two wall face lines** and the thickness (`cad.query` + `cad.entities`).
- Position of the gap: if the user said it, do that ("one per room", "two in the living room").
  If they didn't, put it in the **middle of the wall** and declare it — when the user gives no
  rule such as "one window per room", make your own call and state it.
- Keep the gap at least one wall thickness away from the end of the wall; a window jammed into a
  corner is wrong.

### 4. BREAK the wall — with `edit_geometry`, not by deleting

Opening a gap is a "break" and has no primitive of its own, but `edit_geometry` now exists and
its **`trim` PRESERVES the handle, the xdata and the layer.** Delete-and-redraw is the last
resort; a deleted line loses its identity.

The order matters — the jamb lines become the trim boundary:

1. Use `get_objects_in_region` to get the **handles of the two face lines** where the gap will
   fall (give a narrow region; don't sweep up other lines). Read their endpoint coordinates too;
   you build the pieces from those.
2. `draw` the **two jamb lines**: at the ends of the gap, from the outer face to the inner face,
   on the same layer as the wall.
3. `edit_geometry op:"trim"` — trim each face line passing those jamb lines as
   `boundary_handles`. `keep_point` marks the side to KEEP (not the side to discard): to keep
   the piece left of the gap, give a point on the left.
4. Trim leaves ONE piece — draw the piece on the far side of the gap with `draw`.

So the original line lives on as one piece and only the remaining piece is newly born.
`delete_entities` + two new lines produces the same picture but loses two handles at once; don't
do it unless you have to.

If one face line carries several windows: trim it once (the first piece) and draw the remaining
N pieces in a single `run_script` via `cad.draw` (N windows → N+1 pieces).

If something needs moving or rotating, `modify_entities`' `transform` field does that with the
handle preserved too (`move`/`rotate`/`scale`/`mirror`, angles in RADIANS) — don't
delete-and-redraw for that either.

### 5. Insert the block — ONE FIRST

**`rotation` is in RADIANS, not degrees.** 90° = `Math.PI/2`, 180° = `Math.PI`. Passing degrees
raises NO error — the block just seats at the wrong angle. A negative `scale` MIRRORS the block,
which on an asymmetric window (sill, leaf side) is what decides which face points outward.

**On a dynamic block, choose the variant AT INSERT TIME.** Put the parameter values in
`insert_blocks`' `dynamic_properties`; leave it out and the block is born in its **DEFAULT**
variant, leaving the user to fix 20 windows by hand. Take the names and values from
`dynamic_property_choices` — don't invent them; an unknown name or a value outside the list
rejects that placement with an error. To change the variant of blocks already placed, use
`set_block_properties`.

**Unlabelled 0/1 parameters (Flip, Hinge, Swing) are mirroring parameters** and which number
means which direction VARIES BY BLOCK — only 0 and 1 reach the API. Don't guess; verify it in the
single-window trial below.

The combination of rotation and mirroring cannot be worked out reliably in your head: the block
ends up extending the wrong way, or its projection faces inward. So:

1. Insert **one** (`insert_blocks`).
2. Render that window's bbox (with a little margin) with `render_and_verify` and LOOK:
   - Does the block fill the gap EXACTLY? (overhanging or falling short means the wrong end or
     scale)
   - Does the projecting part (sill/balcony) face OUTWARD?
   - Are the jamb lines visible?
3. If it's wrong, fix the rotation/scale and look again. Once you have the right combination,
   **batch the rest** — the rotation differs per facade, and is the same within a facade.

### 6. Check and submit for approval

Render each window's area with `render_and_verify` (an error at this scale is invisible in an
overall image). The defects to look for:

- No gap was opened; the block sits on top of an unbroken wall.
- A jamb line is missing → the wall is open-mouthed.
- The gap differs from the block width → the block overhangs or a slot is left.
- The block is reversed/mirrored the wrong way.
- Too much or too little wall was deleted (the neighbouring wall went too).

Then `create_preview` → user approval → `commit_changes`.

## Declare in your answer

| window | room | block | frame | gap | wall | rotation |
|---|---|---|---|---|---|---|
| 1 | ⟨room name⟩ | ⟨block name⟩ | ⟨bbox⟩ | ⟨opening⟩ | ⟨facade⟩ | ⟨angle⟩ |

Also:

- Where the block came from (already defined in the drawing / cloned / supplied by the user /
  anonymously named and recognised by usage).
- Per block, the frame width you measured and the gap you derived (two separate numbers).
- **If you copied from a solved copy, say so plainly** and give the closure figure
  (`raw − deleted + added = target`).
- How you decided the number of windows (did the user say, or did you choose).
- The rooms that got NO window and why (internal / no facade / too narrow).

## Known limitation

There is no single "break" that splits a line in two: `edit_geometry op:"trim"` keeps only the
piece `keep_point` indicates, and the other piece is newly drawn. So one side of the gap keeps
the original handle and the other gets a new one — if another step refers to that line by handle,
finish the breaking first.

`edit_geometry` v1 edits only Line and open, bulge-free Polyline; arcs, circles, splines and
closed/bulged polylines are DECLARED in `skipped` with the reason. On a plan whose walls are
drawn with one of those, breaking falls back to delete + draw.

## Example prompts

- /window-placement Draw one window in every room in the region I marked.
- /window-placement Put windows on the south facade of this apartment.
- /window-placement Add windows to the bedrooms using the block already defined in this drawing.
- /window-placement Place a balcony door where the living room meets the balcony.