skilldrop-cli 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/catalogue.json +8 -1
- package/model-routing.json +4 -0
- package/package.json +1 -1
- package/packs/civil-architect/pack.json +60 -0
- package/packs/civil-architect/skills/floor-plan-layout/SKILL.md +103 -0
- package/packs/civil-architect/skills/floor-plan-layout/evals/eval_queries.json +30 -0
- package/packs/civil-architect/skills/floor-plan-layout/evals/evals.json +57 -0
- package/packs/civil-architect/skills/floor-plan-layout/evals/files/4/adu-programme.md +12 -0
- package/packs/civil-architect/skills/floor-plan-layout/examples/ground-floor.md +65 -0
- package/packs/civil-architect/skills/floor-plan-layout/examples/ground-floor.svg +135 -0
- package/packs/civil-architect/skills/floor-plan-layout/manifest.json +34 -0
- package/packs/civil-architect/skills/floor-plan-layout/scripts/plan_check.py +449 -0
- package/packs/civil-architect/skills/floor-plan-layout/templates/plan.json +38 -0
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ A good result runs your lint, typecheck and tests through a gate script, then a
|
|
|
51
51
|
In Claude Code you can use the plugin marketplace instead: run `/plugin marketplace add sananthanarayan/skilldrop`, then `/plugin install dev-team@skilldrop`. [Every install route](guides/how-to/install.md) covers single skills, other IDEs, loops, reviewer subagents, profiles and updates.
|
|
52
52
|
|
|
53
53
|
<details>
|
|
54
|
-
<summary><b>Choose your role:</b>
|
|
54
|
+
<summary><b>Choose your role:</b> 20 packs, from product management and architecture to SRE, GRC, job applications and building design</summary>
|
|
55
55
|
|
|
56
56
|
Install one pack. Every role pack also brings `core` (intake, critique, review council, output hygiene, and the decisions behind a codebase). Each pack page has a starter prompt to paste and says what a good result looks like.
|
|
57
57
|
|
|
@@ -73,6 +73,7 @@ Install one pack. Every role pack also brings `core` (intake, critique, review c
|
|
|
73
73
|
- **Frontend:** `frontend` — what makes a page slow for real users, what makes a bundle heavy, and how each route should be rendered. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/frontend/)
|
|
74
74
|
- **Writing agent skills:** `skill-engineering` — author a portable skill with evals, and review one before it ships. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/skill-engineering/)
|
|
75
75
|
- **Applying for a job:** `career` — an honest fit check against the posting, a tailored resume and cover letter that claim nothing you didn't, and the application form's answers. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/career/)
|
|
76
|
+
- **Designing buildings:** `civil-architect` — a dimensioned concept floor plan from a room list and a footprint, checked for overlapping rooms, doors on no wall and rooms no one can reach. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/civil-architect/)
|
|
76
77
|
- **Everyone:** `core` on its own. [Open pack](https://sananthanarayan.github.io/skilldrop/packs/core/)
|
|
77
78
|
|
|
78
79
|
</details>
|
package/catalogue.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "RFC-0034. The catalogue index: pack display order and the outcome browse axis (RFC-0026). Pack metadata lives in packs/<pack>/pack.json; membership is the folders — a skill is in a pack because it sits at packs/<pack>/skills/<name>/.",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"packs": ["core", "solution-architect", "product-manager", "dev-team", "sre-oncall", "stakeholder-comms", "design", "ai-engineering", "api-builder", "converters", "experience-design", "research", "trackers", "data-analytics", "grc", "infra-as-code", "frontend", "skill-engineering", "career"],
|
|
4
|
+
"packs": ["core", "solution-architect", "product-manager", "dev-team", "sre-oncall", "stakeholder-comms", "design", "ai-engineering", "api-builder", "converters", "experience-design", "research", "trackers", "data-analytics", "grc", "infra-as-code", "frontend", "skill-engineering", "career", "civil-architect"],
|
|
5
5
|
"outcomes": {
|
|
6
6
|
"decide-what-to-build": {
|
|
7
7
|
"for": "Product managers · founders · strategists",
|
|
@@ -205,6 +205,13 @@
|
|
|
205
205
|
"cover-letter",
|
|
206
206
|
"application-form-answers"
|
|
207
207
|
]
|
|
208
|
+
},
|
|
209
|
+
"design-a-building": {
|
|
210
|
+
"for": "Architects · building designers · owners planning a build",
|
|
211
|
+
"description": "Go from a room list and a footprint to a first floor plan: every room at its size, doors on real walls, every room reachable, drawn to scale and checked before anyone sees it.",
|
|
212
|
+
"skills": [
|
|
213
|
+
"floor-plan-layout"
|
|
214
|
+
]
|
|
208
215
|
}
|
|
209
216
|
},
|
|
210
217
|
"retired": {
|
package/model-routing.json
CHANGED
|
@@ -287,6 +287,10 @@
|
|
|
287
287
|
"tier": "light",
|
|
288
288
|
"rationale": "A deterministic stdlib script does the extraction; the model runs it, relays the dropped-content report and hands off. No generation beyond a short summary of losses."
|
|
289
289
|
},
|
|
290
|
+
"floor-plan-layout": {
|
|
291
|
+
"tier": "heavy",
|
|
292
|
+
"rationale": "Spatial layout is design judgment: zoning, adjacencies, daylight and circulation traded against areas, with a checker to iterate against."
|
|
293
|
+
},
|
|
290
294
|
"guide-builder": {
|
|
291
295
|
"tier": "standard",
|
|
292
296
|
"rationale": "Shapes notes into a guide without adding to them. Generation, not adversarial review."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilldrop-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Portable AI-agent skills, measured against the agent without them. ADRs, PRDs, runbooks, decks and reviews, installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sanjay Ananthanarayan",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Building design from a client's room list and a footprint: a schematic floor plan placed at real sizes and drawn as a dimensioned SVG, shown only once a script has checked it for overlapping rooms, rooms outside the footprint, doors on no shared wall, rooms no one can reach and habitable rooms with no window.",
|
|
3
|
+
"display_name": "Civil architect",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"architecture",
|
|
6
|
+
"architect",
|
|
7
|
+
"floor plan",
|
|
8
|
+
"test fit",
|
|
9
|
+
"concept plan",
|
|
10
|
+
"zoning",
|
|
11
|
+
"layout",
|
|
12
|
+
"dimensions",
|
|
13
|
+
"svg"
|
|
14
|
+
],
|
|
15
|
+
"links": {
|
|
16
|
+
"documentation": "https://sananthanarayan.github.io/skilldrop/packs/civil-architect/"
|
|
17
|
+
},
|
|
18
|
+
"maintainers": [
|
|
19
|
+
{
|
|
20
|
+
"name": "sananthanarayan"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"first-value": {
|
|
24
|
+
"audience-posture": "non-technical",
|
|
25
|
+
"prerequisites": [
|
|
26
|
+
"The rooms you need with their sizes, and the footprint's width and depth",
|
|
27
|
+
"Python 3 on the machine, which the checker and drawing script run on"
|
|
28
|
+
],
|
|
29
|
+
"starter-task": "Get a checked, dimensioned concept plan from a room list and a footprint",
|
|
30
|
+
"starter-prompt": "Draw a ground floor plan for a 10 m wide × 9 m deep footprint, road on the south, front door from the road. Rooms: foyer 6 m², living 26 m², dining 14 m², kitchen 12.6 m², utility 4 m² next to the kitchen, prayer room 4.5 m² not next to the kitchen or the toilet, guest toilet 3.2 m², and a stair. Living, dining, kitchen and prayer room need windows.",
|
|
31
|
+
"verification": "floor-plan-layout states its zoning in a few lines, writes an SVG plan with overall dimensions, room sizes and areas, door swings, windows, a scale bar and a north arrow, and shows a room table from a checker that reported no overlaps, every door on a shared wall and every room reachable.",
|
|
32
|
+
"recovery": "If floor-plan-layout does not activate, ask for it by name (\"use floor-plan-layout\"). If the plan is too tight, ask for a second option with the rooms it may shrink named."
|
|
33
|
+
},
|
|
34
|
+
"requires": [
|
|
35
|
+
"core"
|
|
36
|
+
],
|
|
37
|
+
"journey": {
|
|
38
|
+
"when": "You have a client's room list and a footprint, and want a first plan to discuss.",
|
|
39
|
+
"you_bring": [
|
|
40
|
+
"the rooms, with their sizes or areas",
|
|
41
|
+
"the footprint's width and depth, or its corner coordinates",
|
|
42
|
+
"the road or entrance side, and north",
|
|
43
|
+
"what must sit next to what, and what must be kept apart"
|
|
44
|
+
],
|
|
45
|
+
"steps": [
|
|
46
|
+
{
|
|
47
|
+
"skill": "floor-plan-layout",
|
|
48
|
+
"you_do": "Give it the rooms, the footprint and the entrance side.",
|
|
49
|
+
"you_get": "Zoning in a few lines, then a dimensioned SVG plan per floor that has passed the checker, with every room's size and area.",
|
|
50
|
+
"decide": "Take the plan to the client, or ask for another option."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"skill": "floor-plan-layout",
|
|
54
|
+
"you_do": "Ask for a second option, naming what may change (a smaller hall, the stair moved).",
|
|
55
|
+
"you_get": "Another checked plan, with each room that changed against its target."
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"you_end_with": "A checked concept plan, ready to develop in your CAD or BIM tool."
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: floor-plan-layout
|
|
3
|
+
description: Lay out a schematic floor plan from a space programme and a footprint, and draw it as a dimensioned SVG — rooms placed as rectangles at real sizes, doors and windows on real walls, overall dimensions, a scale bar and north arrow — with a script that fails the plan when rooms overlap, a room spills outside the footprint, a door sits on no shared wall or a room can't be reached from the entrance. Use when an architect asks to "draw a floor plan", "lay out these rooms", "make a plan for a 10 × 9 m footprint", for a concept plan, a test fit, a zoning diagram or a dimensioned layout from room sizes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# floor-plan-layout
|
|
7
|
+
|
|
8
|
+
A concept floor plan: every room from the programme placed at its real size inside the
|
|
9
|
+
footprint, connected by doors, lit by windows on outside walls, and drawn to scale. The
|
|
10
|
+
agent does the design reasoning (what goes where and why); a script checks the geometry and
|
|
11
|
+
draws it, so a plan that looks right but has a room overlapping its neighbour or a bedroom
|
|
12
|
+
reachable only through a bathroom never reaches the client. Its inputs are the
|
|
13
|
+
client's room list and the footprint the site allows.
|
|
14
|
+
|
|
15
|
+
## How to respond
|
|
16
|
+
|
|
17
|
+
1. **Gather the three inputs.** Take them from the conversation or the files the user names:
|
|
18
|
+
- **Rooms and target areas,** with adjacencies: an area schedule or the user's list
|
|
19
|
+
- **The outline** of the floor: footprint width × depth, or corner coordinates, inside the site's envelope
|
|
20
|
+
- **Fixed points:** the road side and entrance, north, a stair or core position, an existing wall to keep
|
|
21
|
+
|
|
22
|
+
Ask at most two questions, only for a missing outline or entrance side. A plan with no
|
|
23
|
+
footprint and no plot to derive one from is `BLOCKED`.
|
|
24
|
+
|
|
25
|
+
2. **Decide the zoning before any coordinates.** Write three to six lines:
|
|
26
|
+
- public rooms (living, reception) on the entrance side; private rooms (bedrooms) away from it
|
|
27
|
+
- wet rooms (kitchen, toilets, bathrooms) grouped so plumbing stacks, and stacked over each other across floors
|
|
28
|
+
- every `near` from the programme met by a shared wall, every `apart` by at least one room between
|
|
29
|
+
- daylight: every habitable room on an outside wall; north or south as the climate wants, stated
|
|
30
|
+
- the stair in the same place on every floor
|
|
31
|
+
|
|
32
|
+
Then place rooms as rectangles on a grid of 0.3 m (1 ft) or its multiples, starting from a
|
|
33
|
+
corner, so shared walls line up exactly. An L-shaped room is two or three rectangles under
|
|
34
|
+
one name.
|
|
35
|
+
|
|
36
|
+
3. **Write `plan.json`** in the shape of [`templates/plan.json`](templates/plan.json): `outline`,
|
|
37
|
+
each room's `rect` `[x, y, width, depth]` (or `rects`) with `target_area`, `min_width` and
|
|
38
|
+
`habitable`, every door as `between` two rooms (or `"outside"`) `at` a point on the shared
|
|
39
|
+
wall, and windows `at` a point on an outside wall. x runs left to right, y from the front
|
|
40
|
+
(road side) to the back. One file per floor.
|
|
41
|
+
|
|
42
|
+
4. **Run the checker and fix every FAIL before showing anything.**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Claude Code
|
|
46
|
+
python3 "${CLAUDE_SKILL_DIR}/scripts/plan_check.py" plan.json --svg ground-floor.svg
|
|
47
|
+
# Other IDEs (from the skill folder)
|
|
48
|
+
python3 scripts/plan_check.py plan.json --svg ground-floor.svg
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Exit 0 means no failures; 1 means a failure (overlap, outside the outline, door on no
|
|
52
|
+
shared wall, a room unreachable, no entrance); 2 means the input is malformed. Move rooms
|
|
53
|
+
and rerun until it exits 0. A WARN can stand only with a sentence saying why.
|
|
54
|
+
|
|
55
|
+
5. **Report in this order:**
|
|
56
|
+
1. **The plan's path** and one line on its organising idea. ✅ *"Public rooms along the road, wet rooms stacked on the east wall, stair by the entrance so the first floor is reached without crossing the living room."*
|
|
57
|
+
2. **The zoning lines** from step 2.
|
|
58
|
+
3. **The checker's room table and result,** unchanged.
|
|
59
|
+
4. **Every remaining WARN with its reason,** and every area off target with what it traded for.
|
|
60
|
+
5. **What this plan is not:** schematic, wall thicknesses nominal, no structure, services or code check. The architect checks it against the local rules before it goes further.
|
|
61
|
+
|
|
62
|
+
**Non-interactive runs** (subagent, CI, headless): take the entrance on the first outline edge
|
|
63
|
+
and north as up, tagged `[assumption]`, and size unsized rooms from the programme. With no
|
|
64
|
+
outline and no plot dimensions, emit `BLOCKED: need the footprint's dimensions or the plot and
|
|
65
|
+
setbacks to derive it`.
|
|
66
|
+
|
|
67
|
+
## Useful references in this skill
|
|
68
|
+
|
|
69
|
+
- [`templates/plan.json`](templates/plan.json) — the input shape: a 10 × 9 m ground floor with an L-shaped hall
|
|
70
|
+
- [`scripts/plan_check.py`](scripts/plan_check.py) — overlap, containment, doors on shared walls, reachability, areas, windows; draws the SVG
|
|
71
|
+
- [`examples/ground-floor.md`](examples/ground-floor.md) — worked example: a programme and footprint become a checked, drawn plan, including the FAIL the first draft hit
|
|
72
|
+
|
|
73
|
+
## Quality bar
|
|
74
|
+
|
|
75
|
+
- **The checker exits 0.** No plan with an overlap, a room outside the outline, a door on no wall or an unreachable room is shown to the user.
|
|
76
|
+
- **Every room from the programme is on the plan,** at its target area within 10 %, or the difference is explained.
|
|
77
|
+
- **Every stated adjacency is a shared wall,** and every `apart` has a room between.
|
|
78
|
+
- **Every habitable room has a window on an outside wall.**
|
|
79
|
+
- **No bedroom or living space is reached only through a bathroom, toilet or another bedroom.**
|
|
80
|
+
- **The drawing has overall dimensions, room sizes, a scale bar and a north arrow,** and says it is schematic.
|
|
81
|
+
|
|
82
|
+
## When to use this skill
|
|
83
|
+
|
|
84
|
+
- ✅ A concept plan or test fit from a programme and footprint
|
|
85
|
+
- ✅ Two or three layout options for the same brief, each checked
|
|
86
|
+
- ✅ Re-planning after the client changes a room
|
|
87
|
+
- ✅ A zoning diagram before detailed design
|
|
88
|
+
|
|
89
|
+
## When NOT to use this skill
|
|
90
|
+
|
|
91
|
+
- ❌ Working out the room list and areas from a brief, before any layout: total them first, then plan
|
|
92
|
+
- ❌ How big the footprint may be under setbacks, coverage and FAR: that is a site feasibility check, done before the plan
|
|
93
|
+
- ❌ Construction, working or submission drawings in CAD/BIM: this is a schematic for discussion
|
|
94
|
+
- ❌ Structural grids, MEP routing or a code compliance check
|
|
95
|
+
|
|
96
|
+
## Anti-patterns to avoid
|
|
97
|
+
|
|
98
|
+
- ❌ **Drawing the plan in ASCII or Mermaid.** Neither holds a dimension; the SVG does.
|
|
99
|
+
- ❌ **Showing a plan before the checker passes.** The overlap you can't see in a list of coordinates is the one the client finds.
|
|
100
|
+
- ❌ **Rooms that touch "nearly":** a 3.95 and a 4.0 leave a 50 mm sliver no one can build. Use the grid.
|
|
101
|
+
- ❌ **A corridor that serves nothing.** Circulation that reaches no door is area the client pays for.
|
|
102
|
+
- ❌ **Toilet doors opening onto the dining or kitchen,** and a prayer or puja room sharing a wall with a toilet when the brief keeps them apart.
|
|
103
|
+
- ❌ **Calling the plan code-compliant.** The checker tests geometry, not the building code.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "Draw a floor plan for these rooms in a 12 × 15 m footprint",
|
|
4
|
+
"should_trigger": true
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"query": "Lay out a 3BHK in this footprint and give me a dimensioned plan",
|
|
8
|
+
"should_trigger": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "Can you do a quick test fit of the clinic programme on the ground floor?",
|
|
12
|
+
"should_trigger": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"query": "Make a zoning diagram for the house with the bedrooms away from the road",
|
|
16
|
+
"should_trigger": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "Make a Figma diagram of our microservices",
|
|
20
|
+
"should_trigger": false
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"query": "Draw a sequence diagram of the checkout flow",
|
|
24
|
+
"should_trigger": false
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "Build an interactive diagram of our data pipeline I can click through",
|
|
28
|
+
"should_trigger": false
|
|
29
|
+
}
|
|
30
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "floor-plan-layout",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "Draw a ground floor plan for a 10 m wide × 9 m deep footprint, road on the south, front door from the road. Rooms: foyer 6 m², living 26 m², dining 14 m², kitchen 12.6 m², utility 4 m² next to the kitchen, prayer room 4.5 m² not next to the kitchen or the toilet, guest toilet 3.2 m², and a stair to the first floor. Living, dining, kitchen and prayer room need windows.",
|
|
7
|
+
"assertions": [
|
|
8
|
+
"Writes a plan.json and runs plan_check.py, and the final run exits 0 (no FAIL lines)",
|
|
9
|
+
"Writes an SVG with overall dimensions, room sizes and areas, a scale bar and a north arrow",
|
|
10
|
+
"Every room listed is on the plan, each within 10 % of its area or the difference is explained",
|
|
11
|
+
"The prayer room shares no wall with the kitchen or the toilet",
|
|
12
|
+
"Living, dining, kitchen and prayer room each have a window on an outside wall",
|
|
13
|
+
"States the zoning decisions (public at the front, wet rooms grouped, stair placement) before or with the plan",
|
|
14
|
+
"Says the plan is schematic and not checked against the building code"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": 2,
|
|
19
|
+
"prompt": "Quick test fit: 2-bed apartment, footprint is 7.2 x 10.8 m, entrance in the middle of the long east side. Need living/dining 22 m², kitchen 8 m², two bedrooms 11 m² each, one bathroom 4.5 m², and a hall. Bedrooms must not open off the living room. Just give me the plan.",
|
|
20
|
+
"assertions": [
|
|
21
|
+
"Runs plan_check.py and shows a plan only after it exits 0",
|
|
22
|
+
"Neither bedroom's only door opens onto the living/dining room; both are reached from the hall",
|
|
23
|
+
"No bedroom or living space is reached only through the bathroom",
|
|
24
|
+
"Every room has its size and area on the drawing, and the totals fit inside 7.2 × 10.8 m",
|
|
25
|
+
"Delivers the plan without asking questions first, tagging anything it had to assume",
|
|
26
|
+
"Does not draw the plan in ASCII or Mermaid"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": 3,
|
|
31
|
+
"held_out": "2026-11-05",
|
|
32
|
+
"prompt": "make a plan for a 10 x 9 m footprint. 10m runs E-W, entrance on the south side. rooms: living 5x4.5, kitchen 3x3, bed 1 3.5x3.5, bed 2 3x3, bath 2.4x2. bath door must NOT open off the kitchen. single storey holiday cottage at Lake Ternholm, concept only",
|
|
33
|
+
"assertions": [
|
|
34
|
+
"It produces an SVG plan with overall dimensions of 10.0 m (E-W) × 9.0 m (N-S), a scale bar, and a north arrow that agrees with the entrance being on the south wall",
|
|
35
|
+
"All five rooms are drawn at their stated sizes (to within about 0.1 m), all fully inside the footprint, with no two rooms overlapping",
|
|
36
|
+
"The entrance door is on the south external wall",
|
|
37
|
+
"Every room can be reached from the entrance through doors, via the living room or a hall/corridor",
|
|
38
|
+
"The bathroom door does not open into the kitchen, and there is no door between the bathroom and the kitchen",
|
|
39
|
+
"Windows are only on external walls, never on internal partitions",
|
|
40
|
+
"It does not invent rooms beyond the five requested plus circulation (no second bathroom, store, garage or porch presented as part of the brief)"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": 4,
|
|
45
|
+
"held_out": "2026-11-05",
|
|
46
|
+
"prompt": "Hi, attached is the space programme from our meeting with the Okafor-Lindqvist family for their backyard ADU in Brambleton (adu-programme.md). Buildable footprint is 30 ft x 24 ft, single storey, with the 30 ft side facing the street to the west, and the entry comes from the street. Could you lay out the rooms and draw a dimensioned concept plan we can show them on Thursday?",
|
|
47
|
+
"assertions": [
|
|
48
|
+
"It reports that the programme's rooms add up to 869 sq ft (320 + 144 + 182 + 121 + 54 + 48) against a 720 sq ft footprint (30 × 24), 149 sq ft over before any circulation, so it cannot fit as given",
|
|
49
|
+
"It does not hand over a plan where rooms overlap, rooms sit outside the 30 × 24 ft footprint, or rooms have been shrunk without saying which ones and by how much",
|
|
50
|
+
"It offers concrete ways to fix it (named rooms cut or resized with new sizes, a larger footprint, or a second storey as an option for the user to decide) or asks the user to choose; if it draws a test fit, it lists every changed room size next to the programme size",
|
|
51
|
+
"Any plan it draws is an SVG dimensioned in feet and inches, 30'-0\" × 24'-0\" overall, with a scale bar and a north arrow that puts the street on the west",
|
|
52
|
+
"Any plan it draws has the entry door on the west (street) wall, and every room can be reached from it",
|
|
53
|
+
"It does not invent rooms, a second storey stated as fact, or zoning rules that are not in the prompt or the programme"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Okafor-Lindqvist ADU - space programme (draft 2)
|
|
2
|
+
|
|
3
|
+
| Room | Size |
|
|
4
|
+
|---|---|
|
|
5
|
+
| Great room (living + dining) | 20 x 16 ft |
|
|
6
|
+
| Kitchen | 12 x 12 ft |
|
|
7
|
+
| Primary bedroom | 14 x 13 ft |
|
|
8
|
+
| Bedroom 2 | 11 x 11 ft |
|
|
9
|
+
| Bathroom | 9 x 6 ft |
|
|
10
|
+
| Laundry | 8 x 6 ft |
|
|
11
|
+
|
|
12
|
+
Single storey. Entry from the street side.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Worked example: ground floor on Plot 14
|
|
2
|
+
|
|
3
|
+
## Input
|
|
4
|
+
|
|
5
|
+
> *"Lay out the ground floor for the family house. Programme is in program.json (foyer,
|
|
6
|
+
> living, dining, kitchen, utility, prayer room, guest toilet), footprint 10 m wide × 9 m
|
|
7
|
+
> deep, road on the south side, front door from the road. Keep the prayer room away from the
|
|
8
|
+
> kitchen and the toilet. Stair to the first floor."*
|
|
9
|
+
|
|
10
|
+
## Zoning, decided before coordinates
|
|
11
|
+
|
|
12
|
+
- Public front: foyer, then living along the road, so a guest never passes the kitchen.
|
|
13
|
+
- Stair beside the foyer, so the first floor is reached without crossing the living room.
|
|
14
|
+
- Service back: dining, kitchen, utility and toilet across the rear, with a back door from the hall for deliveries.
|
|
15
|
+
- Wet rooms (kitchen, utility, toilet) side by side at the back, so drains run to one side of the plot and stack under the first-floor bathrooms.
|
|
16
|
+
- Prayer room in the south-east corner, off the living room: an outside wall for a window, and the hall, not the kitchen or toilet, on its other side.
|
|
17
|
+
|
|
18
|
+
## First draft: the checker caught it
|
|
19
|
+
|
|
20
|
+
The first draft widened the kitchen to 3.2 m. The checker exited 1:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
FAIL Kitchen and Utility overlap by 0.46
|
|
24
|
+
FAIL Kitchen and Toilet overlap by 0.38
|
|
25
|
+
FAIL door 8 (Kitchen – Utility) at (6.6, 6) is not on a wall both sides share
|
|
26
|
+
FAIL Utility cannot be reached from an entrance
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The extra 0.2 m pushed the kitchen into the utility and the toilet. The utility door, moved
|
|
30
|
+
to the kitchen's new wall, then sat inside the utility rather than on a wall the two share,
|
|
31
|
+
so the utility could not be reached. Kitchen back to 3.0 m (12.6 m², its target).
|
|
32
|
+
|
|
33
|
+
## `plan.json`
|
|
34
|
+
|
|
35
|
+
See [`../templates/plan.json`](../templates/plan.json), unchanged.
|
|
36
|
+
|
|
37
|
+
## Response
|
|
38
|
+
|
|
39
|
+
**Plan:** [`ground-floor.svg`](ground-floor.svg). Public rooms along the road, service
|
|
40
|
+
rooms across the back, the stair beside the foyer.
|
|
41
|
+
|
|
42
|
+
| Room | Size (m) | Area (m²) | Target (m²) |
|
|
43
|
+
|---|---|---:|---:|
|
|
44
|
+
| Foyer | 2.5 × 2.4 | 6.0 | 6.0 |
|
|
45
|
+
| Stair | 2.5 × 2.4 | 6.0 | |
|
|
46
|
+
| Living | 5.4 × 4.8 | 25.9 | 25.9 |
|
|
47
|
+
| Prayer room | 2.1 × 2.2 | 4.6 | 4.5 |
|
|
48
|
+
| Dining | 3.4 × 4.2 | 14.3 | 14.0 |
|
|
49
|
+
| Kitchen | 3 × 4.2 | 12.6 | 12.6 |
|
|
50
|
+
| Utility | 1.6 × 2.3 | 3.7 | 4.0 |
|
|
51
|
+
| Toilet | 1.6 × 1.9 | 3.0 | 3.2 |
|
|
52
|
+
| Hall | 2.1 × 2.6 + 2 × 4.2 | 13.9 | |
|
|
53
|
+
| **Rooms** | | **90.0** | |
|
|
54
|
+
| **Outline** | | **90.0** | |
|
|
55
|
+
|
|
56
|
+
PASS no overlaps, every door on a shared wall, every room reachable.
|
|
57
|
+
|
|
58
|
+
**Off target:** utility 3.7 m² against 4 (−8 %) and toilet 3.0 against 3.2 (−6 %), both inside
|
|
59
|
+
the 10 % tolerance, traded for keeping the toilet off the prayer room. The hall is 13.9 m²
|
|
60
|
+
because it doubles as the back entrance; a tighter option moves the back door to the utility
|
|
61
|
+
and gives the spare 4 m² to the kitchen.
|
|
62
|
+
|
|
63
|
+
**What this plan is not:** schematic; walls drawn at a nominal 230 mm; no structure, services
|
|
64
|
+
or bye-law check. The architect checks the toilet's ventilation and the stair's width against
|
|
65
|
+
the local rules.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="914" viewBox="0 0 960 914" font-family="Helvetica, Arial, sans-serif" font-size="11">
|
|
2
|
+
<rect width="960" height="914" fill="#fff"/>
|
|
3
|
+
<text x="20" y="28" font-size="16" font-weight="bold">Ground floor, house on Plot 14</text>
|
|
4
|
+
<text x="20" y="46" fill="#444">Rooms 90.0 m² · outline 90.0 m² · schematic, not for construction</text>
|
|
5
|
+
<rect x="80.0" y="621.6" width="190.0" height="182.4" fill="#eef4f1"/>
|
|
6
|
+
<rect x="80.0" y="439.2" width="190.0" height="182.4" fill="#f5f0e6"/>
|
|
7
|
+
<rect x="270.0" y="439.2" width="410.4" height="364.8" fill="#eef0f6"/>
|
|
8
|
+
<rect x="680.4" y="636.8" width="159.6" height="167.2" fill="#f6eef0"/>
|
|
9
|
+
<rect x="80.0" y="120.0" width="258.4" height="319.2" fill="#f0f6ee"/>
|
|
10
|
+
<rect x="338.4" y="120.0" width="228.0" height="319.2" fill="#f3f3f3"/>
|
|
11
|
+
<rect x="566.4" y="264.4" width="121.6" height="174.8" fill="#eef4f1"/>
|
|
12
|
+
<rect x="566.4" y="120.0" width="121.6" height="144.4" fill="#f5f0e6"/>
|
|
13
|
+
<rect x="680.4" y="439.2" width="159.6" height="197.6" fill="#eef0f6"/>
|
|
14
|
+
<rect x="688.0" y="120.0" width="152.0" height="319.2" fill="#eef0f6"/>
|
|
15
|
+
<line x1="80.0" y1="804.0" x2="270.0" y2="804.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
16
|
+
<line x1="80.0" y1="621.6" x2="270.0" y2="621.6" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
17
|
+
<line x1="80.0" y1="804.0" x2="80.0" y2="621.6" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
18
|
+
<line x1="270.0" y1="804.0" x2="270.0" y2="621.6" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
19
|
+
<line x1="80.0" y1="621.6" x2="270.0" y2="621.6" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
20
|
+
<line x1="80.0" y1="439.2" x2="270.0" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
21
|
+
<line x1="80.0" y1="621.6" x2="80.0" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
22
|
+
<line x1="270.0" y1="621.6" x2="270.0" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
23
|
+
<line x1="270.0" y1="804.0" x2="680.4" y2="804.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
24
|
+
<line x1="270.0" y1="439.2" x2="680.4" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
25
|
+
<line x1="270.0" y1="804.0" x2="270.0" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
26
|
+
<line x1="680.4" y1="804.0" x2="680.4" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
27
|
+
<line x1="680.4" y1="804.0" x2="840.0" y2="804.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
28
|
+
<line x1="680.4" y1="636.8" x2="840.0" y2="636.8" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
29
|
+
<line x1="680.4" y1="804.0" x2="680.4" y2="636.8" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
30
|
+
<line x1="840.0" y1="804.0" x2="840.0" y2="636.8" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
31
|
+
<line x1="80.0" y1="439.2" x2="338.4" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
32
|
+
<line x1="80.0" y1="120.0" x2="338.4" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
33
|
+
<line x1="80.0" y1="439.2" x2="80.0" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
34
|
+
<line x1="338.4" y1="439.2" x2="338.4" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
35
|
+
<line x1="338.4" y1="439.2" x2="566.4" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
36
|
+
<line x1="338.4" y1="120.0" x2="566.4" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
37
|
+
<line x1="338.4" y1="439.2" x2="338.4" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
38
|
+
<line x1="566.4" y1="439.2" x2="566.4" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
39
|
+
<line x1="566.4" y1="439.2" x2="688.0" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
40
|
+
<line x1="566.4" y1="264.4" x2="688.0" y2="264.4" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
41
|
+
<line x1="566.4" y1="439.2" x2="566.4" y2="264.4" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
42
|
+
<line x1="688.0" y1="439.2" x2="688.0" y2="264.4" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
43
|
+
<line x1="566.4" y1="264.4" x2="688.0" y2="264.4" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
44
|
+
<line x1="566.4" y1="120.0" x2="688.0" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
45
|
+
<line x1="566.4" y1="264.4" x2="566.4" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
46
|
+
<line x1="688.0" y1="264.4" x2="688.0" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
47
|
+
<line x1="680.4" y1="636.8" x2="840.0" y2="636.8" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
48
|
+
<line x1="680.4" y1="439.2" x2="688.0" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
49
|
+
<line x1="680.4" y1="636.8" x2="680.4" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
50
|
+
<line x1="840.0" y1="636.8" x2="840.0" y2="439.2" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
51
|
+
<line x1="688.0" y1="120.0" x2="840.0" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
52
|
+
<line x1="688.0" y1="439.2" x2="688.0" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
53
|
+
<line x1="840.0" y1="439.2" x2="840.0" y2="120.0" stroke="#333" stroke-width="8.7" stroke-linecap="square"/>
|
|
54
|
+
<polygon points="80.0,804.0 840.0,804.0 840.0,120.0 80.0,120.0" fill="none" stroke="#111" stroke-width="17.5" stroke-linejoin="miter"/>
|
|
55
|
+
<text x="175.0" y="708.8" text-anchor="middle" font-weight="bold" font-size="12">Foyer</text>
|
|
56
|
+
<text x="175.0" y="721.8" text-anchor="middle" font-size="11" fill="#444">2.5×2.4 · 6.0 m²</text>
|
|
57
|
+
<text x="175.0" y="526.4" text-anchor="middle" font-weight="bold" font-size="12">Stair</text>
|
|
58
|
+
<text x="175.0" y="539.4" text-anchor="middle" font-size="11" fill="#444">2.5×2.4 · 6.0 m²</text>
|
|
59
|
+
<text x="475.2" y="617.6" text-anchor="middle" font-weight="bold" font-size="12">Living</text>
|
|
60
|
+
<text x="475.2" y="630.6" text-anchor="middle" font-size="11" fill="#444">5.4×4.8 · 25.9 m²</text>
|
|
61
|
+
<text x="760.2" y="716.4" text-anchor="middle" font-weight="bold" font-size="12">Prayer room</text>
|
|
62
|
+
<text x="760.2" y="729.4" text-anchor="middle" font-size="11" fill="#444">2.1×2.2 · 4.6 m²</text>
|
|
63
|
+
<text x="209.2" y="275.6" text-anchor="middle" font-weight="bold" font-size="12">Dining</text>
|
|
64
|
+
<text x="209.2" y="288.6" text-anchor="middle" font-size="11" fill="#444">3.4×4.2 · 14.3 m²</text>
|
|
65
|
+
<text x="452.4" y="275.6" text-anchor="middle" font-weight="bold" font-size="12">Kitchen</text>
|
|
66
|
+
<text x="452.4" y="288.6" text-anchor="middle" font-size="11" fill="#444">3×4.2 · 12.6 m²</text>
|
|
67
|
+
<text x="627.2" y="347.8" text-anchor="middle" font-weight="bold" font-size="12">Utility</text>
|
|
68
|
+
<text x="627.2" y="360.8" text-anchor="middle" font-size="11" fill="#444">1.6×2.3 · 3.7 m²</text>
|
|
69
|
+
<text x="627.2" y="188.2" text-anchor="middle" font-weight="bold" font-size="12">Toilet</text>
|
|
70
|
+
<text x="627.2" y="201.2" text-anchor="middle" font-size="11" fill="#444">1.6×1.9 · 3.0 m²</text>
|
|
71
|
+
<text x="764.0" y="275.6" text-anchor="middle" font-weight="bold" font-size="12">Hall</text>
|
|
72
|
+
<text x="764.0" y="288.6" text-anchor="middle" font-size="11" fill="#444">13.9 m²</text>
|
|
73
|
+
<line x1="384.0" y1="804.0" x2="566.4" y2="804.0" stroke="#fff" stroke-width="18.5"/>
|
|
74
|
+
<line x1="384.0" y1="798.2" x2="566.4" y2="798.2" stroke="#2b6cb0" stroke-width="1"/>
|
|
75
|
+
<line x1="384.0" y1="804.0" x2="566.4" y2="804.0" stroke="#2b6cb0" stroke-width="1"/>
|
|
76
|
+
<line x1="384.0" y1="809.8" x2="566.4" y2="809.8" stroke="#2b6cb0" stroke-width="1"/>
|
|
77
|
+
<line x1="80.0" y1="336.6" x2="80.0" y2="222.6" stroke="#fff" stroke-width="18.5"/>
|
|
78
|
+
<line x1="74.2" y1="336.6" x2="74.2" y2="222.6" stroke="#2b6cb0" stroke-width="1"/>
|
|
79
|
+
<line x1="80.0" y1="336.6" x2="80.0" y2="222.6" stroke="#2b6cb0" stroke-width="1"/>
|
|
80
|
+
<line x1="85.8" y1="336.6" x2="85.8" y2="222.6" stroke="#2b6cb0" stroke-width="1"/>
|
|
81
|
+
<line x1="406.8" y1="120.0" x2="498.0" y2="120.0" stroke="#fff" stroke-width="18.5"/>
|
|
82
|
+
<line x1="406.8" y1="114.2" x2="498.0" y2="114.2" stroke="#2b6cb0" stroke-width="1"/>
|
|
83
|
+
<line x1="406.8" y1="120.0" x2="498.0" y2="120.0" stroke="#2b6cb0" stroke-width="1"/>
|
|
84
|
+
<line x1="406.8" y1="125.8" x2="498.0" y2="125.8" stroke="#2b6cb0" stroke-width="1"/>
|
|
85
|
+
<line x1="840.0" y1="754.6" x2="840.0" y2="686.2" stroke="#fff" stroke-width="18.5"/>
|
|
86
|
+
<line x1="834.2" y1="754.6" x2="834.2" y2="686.2" stroke="#2b6cb0" stroke-width="1"/>
|
|
87
|
+
<line x1="840.0" y1="754.6" x2="840.0" y2="686.2" stroke="#2b6cb0" stroke-width="1"/>
|
|
88
|
+
<line x1="845.8" y1="754.6" x2="845.8" y2="686.2" stroke="#2b6cb0" stroke-width="1"/>
|
|
89
|
+
<line x1="604.4" y1="120.0" x2="650.0" y2="120.0" stroke="#fff" stroke-width="18.5"/>
|
|
90
|
+
<line x1="604.4" y1="114.2" x2="650.0" y2="114.2" stroke="#2b6cb0" stroke-width="1"/>
|
|
91
|
+
<line x1="604.4" y1="120.0" x2="650.0" y2="120.0" stroke="#2b6cb0" stroke-width="1"/>
|
|
92
|
+
<line x1="604.4" y1="125.8" x2="650.0" y2="125.8" stroke="#2b6cb0" stroke-width="1"/>
|
|
93
|
+
<line x1="137.0" y1="804.0" x2="213.0" y2="804.0" stroke="#fff" stroke-width="18.5"/>
|
|
94
|
+
<line x1="137.0" y1="804.0" x2="137.0" y2="728.0" stroke="#b5531a" stroke-width="1.5"/>
|
|
95
|
+
<path d="M 137.0 728.0 A 76.0 76.0 0 0 1 213.0 804.0" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>
|
|
96
|
+
<line x1="270.0" y1="747.0" x2="270.0" y2="678.6" stroke="#fff" stroke-width="18.5"/>
|
|
97
|
+
<line x1="270.0" y1="747.0" x2="338.4" y2="747.0" stroke="#b5531a" stroke-width="1.5"/>
|
|
98
|
+
<path d="M 338.4 747.0 A 68.4 68.4 0 0 0 270.0 678.6" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>
|
|
99
|
+
<line x1="140.8" y1="621.6" x2="209.2" y2="621.6" stroke="#fff" stroke-width="18.5"/>
|
|
100
|
+
<line x1="140.8" y1="621.6" x2="140.8" y2="553.2" stroke="#b5531a" stroke-width="1.5"/>
|
|
101
|
+
<path d="M 140.8 553.2 A 68.4 68.4 0 0 1 209.2 621.6" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>
|
|
102
|
+
<line x1="270.0" y1="439.2" x2="338.4" y2="439.2" stroke="#fff" stroke-width="18.5"/>
|
|
103
|
+
<line x1="270.0" y1="439.2" x2="270.0" y2="370.8" stroke="#b5531a" stroke-width="1.5"/>
|
|
104
|
+
<path d="M 270.0 370.8 A 68.4 68.4 0 0 1 338.4 439.2" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>
|
|
105
|
+
<line x1="680.4" y1="750.8" x2="680.4" y2="690.0" stroke="#fff" stroke-width="18.5"/>
|
|
106
|
+
<line x1="680.4" y1="750.8" x2="741.2" y2="750.8" stroke="#b5531a" stroke-width="1.5"/>
|
|
107
|
+
<path d="M 741.2 750.8 A 60.8 60.8 0 0 0 680.4 690.0" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>
|
|
108
|
+
<line x1="680.4" y1="572.2" x2="680.4" y2="503.8" stroke="#fff" stroke-width="18.5"/>
|
|
109
|
+
<line x1="680.4" y1="572.2" x2="748.8" y2="572.2" stroke="#b5531a" stroke-width="1.5"/>
|
|
110
|
+
<path d="M 748.8 572.2 A 68.4 68.4 0 0 0 680.4 503.8" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>
|
|
111
|
+
<line x1="338.4" y1="313.8" x2="338.4" y2="245.4" stroke="#fff" stroke-width="18.5"/>
|
|
112
|
+
<line x1="338.4" y1="313.8" x2="406.8" y2="313.8" stroke="#b5531a" stroke-width="1.5"/>
|
|
113
|
+
<path d="M 406.8 313.8 A 68.4 68.4 0 0 0 338.4 245.4" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>
|
|
114
|
+
<line x1="566.4" y1="378.4" x2="566.4" y2="317.6" stroke="#fff" stroke-width="18.5"/>
|
|
115
|
+
<line x1="566.4" y1="378.4" x2="627.2" y2="378.4" stroke="#b5531a" stroke-width="1.5"/>
|
|
116
|
+
<path d="M 627.2 378.4 A 60.8 60.8 0 0 0 566.4 317.6" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>
|
|
117
|
+
<line x1="688.0" y1="226.4" x2="688.0" y2="165.6" stroke="#fff" stroke-width="18.5"/>
|
|
118
|
+
<line x1="688.0" y1="226.4" x2="748.8" y2="226.4" stroke="#b5531a" stroke-width="1.5"/>
|
|
119
|
+
<path d="M 748.8 226.4 A 60.8 60.8 0 0 0 688.0 165.6" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>
|
|
120
|
+
<line x1="729.8" y1="120.0" x2="798.2" y2="120.0" stroke="#fff" stroke-width="18.5"/>
|
|
121
|
+
<line x1="729.8" y1="120.0" x2="729.8" y2="51.6" stroke="#b5531a" stroke-width="1.5"/>
|
|
122
|
+
<path d="M 729.8 51.6 A 68.4 68.4 0 0 1 798.2 120.0" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>
|
|
123
|
+
<line x1="80.0" y1="832.0" x2="840.0" y2="832.0" stroke="#555" stroke-width="0.8"/>
|
|
124
|
+
<line x1="76.0" y1="836.0" x2="84.0" y2="828.0" stroke="#555" stroke-width="1.2"/>
|
|
125
|
+
<line x1="836.0" y1="836.0" x2="844.0" y2="828.0" stroke="#555" stroke-width="1.2"/>
|
|
126
|
+
<text x="460.0" y="827.0" text-anchor="middle">10 m</text>
|
|
127
|
+
<line x1="52.0" y1="804.0" x2="52.0" y2="120.0" stroke="#555" stroke-width="0.8"/>
|
|
128
|
+
<line x1="48.0" y1="808.0" x2="56.0" y2="800.0" stroke="#555" stroke-width="1.2"/>
|
|
129
|
+
<line x1="48.0" y1="124.0" x2="56.0" y2="116.0" stroke="#555" stroke-width="1.2"/>
|
|
130
|
+
<text x="46.0" y="462.0" text-anchor="middle" transform="rotate(-90 46.0 462.0)">9 m</text>
|
|
131
|
+
<g transform="translate(920 40) rotate(15.0)"><polygon points="0,-20 7,9 0,4 -7,9" fill="#111"/><text x="0" y="-25" text-anchor="middle" font-weight="bold">N</text></g>
|
|
132
|
+
<rect x="80" y="890.0" width="76.0" height="6" fill="#111"/>
|
|
133
|
+
<rect x="156.0" y="890.0" width="76.0" height="6" fill="#fff" stroke="#111"/>
|
|
134
|
+
<text x="238.0" y="896.0">2 m</text>
|
|
135
|
+
</svg>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "floor-plan-layout",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lay out a schematic floor plan from a space programme and a footprint, and draw it as a dimensioned SVG — rooms placed as rectangles at real sizes, doors and windows on real walls, overall dimensions, a scale bar and north arrow — with a script that fails the plan when rooms overlap, a room spills outside the footprint, a door sits on no shared wall or a room can't be reached from the entrance. Use when an architect asks to \"draw a floor plan\", \"lay out these rooms\", \"make a plan for a 10 × 9 m footprint\", for a concept plan, a test fit, a zoning diagram or a dimensioned layout from room sizes.",
|
|
5
|
+
"entrypoint": "SKILL.md",
|
|
6
|
+
"deps": {
|
|
7
|
+
"npm": [],
|
|
8
|
+
"pip": []
|
|
9
|
+
},
|
|
10
|
+
"env": {
|
|
11
|
+
"required": [],
|
|
12
|
+
"optional": []
|
|
13
|
+
},
|
|
14
|
+
"related": [],
|
|
15
|
+
"tags": [
|
|
16
|
+
"architecture",
|
|
17
|
+
"floor-plan",
|
|
18
|
+
"layout",
|
|
19
|
+
"test-fit",
|
|
20
|
+
"zoning",
|
|
21
|
+
"svg",
|
|
22
|
+
"dimensions"
|
|
23
|
+
],
|
|
24
|
+
"permissions": {
|
|
25
|
+
"network": [],
|
|
26
|
+
"commands": [],
|
|
27
|
+
"files": "named-paths",
|
|
28
|
+
"notes": "Reads the plan.json it is given and writes the SVG to the path passed with --svg."
|
|
29
|
+
},
|
|
30
|
+
"model": {
|
|
31
|
+
"tier": "heavy",
|
|
32
|
+
"rationale": "Spatial layout is design judgment: zoning, adjacencies, daylight and circulation traded against areas, with a checker to iterate against."
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check a schematic floor plan and draw it as a dimensioned SVG. Stdlib only.
|
|
3
|
+
|
|
4
|
+
Reads a plan.json: the floor's outline, each room as one or more rectangles, the doors
|
|
5
|
+
between rooms (and to "outside"), and the windows. It fails a plan where
|
|
6
|
+
- two rooms overlap, or a room runs outside the outline
|
|
7
|
+
- a door does not sit on a wall its two rooms share (or on the outline, for "outside")
|
|
8
|
+
- a room cannot be reached from an outside door, or there is no outside door at all
|
|
9
|
+
and warns where
|
|
10
|
+
- a room's area is more than the tolerance (default 10 %) off its target
|
|
11
|
+
- a room is narrower than its min_width, or a door is narrower than min_door_width
|
|
12
|
+
- a room marked habitable has no window on the outline
|
|
13
|
+
- more than 5 % of the outline is not in any room
|
|
14
|
+
Then it draws the plan: outline, rooms with name, size and area, door swings, windows,
|
|
15
|
+
overall dimension strings, a scale bar and a north arrow.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python3 plan_check.py plan.json [--svg plan.svg]
|
|
19
|
+
Exit codes: 0 no failures, 1 a failure, 2 bad input.
|
|
20
|
+
"""
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import math
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
EPS = 1e-6
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InputError(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def number(v, what):
|
|
34
|
+
if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v):
|
|
35
|
+
raise InputError(f"{what} must be a number")
|
|
36
|
+
return float(v)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def poly_area(poly):
|
|
40
|
+
s = 0.0
|
|
41
|
+
for i, (x1, y1) in enumerate(poly):
|
|
42
|
+
x2, y2 = poly[(i + 1) % len(poly)]
|
|
43
|
+
s += x1 * y2 - x2 * y1
|
|
44
|
+
return abs(s) / 2
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def point_in_poly(p, poly):
|
|
48
|
+
"""True when p is inside or on the boundary."""
|
|
49
|
+
if on_outline(p, poly):
|
|
50
|
+
return True
|
|
51
|
+
x, y = p
|
|
52
|
+
inside = False
|
|
53
|
+
for i, (x1, y1) in enumerate(poly):
|
|
54
|
+
x2, y2 = poly[(i + 1) % len(poly)]
|
|
55
|
+
if (y1 > y) != (y2 > y):
|
|
56
|
+
xi = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
|
|
57
|
+
if xi > x:
|
|
58
|
+
inside = not inside
|
|
59
|
+
return inside
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def on_segment(p, a, b, tol=EPS * 10):
|
|
63
|
+
(x, y), (x1, y1), (x2, y2) = p, a, b
|
|
64
|
+
L = math.hypot(x2 - x1, y2 - y1)
|
|
65
|
+
if L < EPS:
|
|
66
|
+
return math.hypot(x - x1, y - y1) <= tol
|
|
67
|
+
cross = abs((x2 - x1) * (y - y1) - (y2 - y1) * (x - x1)) / L
|
|
68
|
+
dot = ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)) / L
|
|
69
|
+
return cross <= tol and -tol <= dot <= L + tol
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def on_outline(p, poly):
|
|
73
|
+
return any(on_segment(p, poly[i], poly[(i + 1) % len(poly)]) for i in range(len(poly)))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def rect_edges(r):
|
|
77
|
+
x, y, w, h = r
|
|
78
|
+
c = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
|
|
79
|
+
return [(c[i], c[(i + 1) % 4]) for i in range(4)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def on_rect_edge(p, r):
|
|
83
|
+
return any(on_segment(p, a, b) for a, b in rect_edges(r))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def overlap(r1, r2):
|
|
87
|
+
w = min(r1[0] + r1[2], r2[0] + r2[2]) - max(r1[0], r2[0])
|
|
88
|
+
h = min(r1[1] + r1[3], r2[1] + r2[3]) - max(r1[1], r2[1])
|
|
89
|
+
return w * h if w > EPS and h > EPS else 0.0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def rect_in_poly(r, poly):
|
|
93
|
+
x, y, w, h = r
|
|
94
|
+
corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
|
|
95
|
+
if not all(point_in_poly(c, poly) for c in corners):
|
|
96
|
+
return False
|
|
97
|
+
# an outline corner strictly inside the rectangle means the outline cuts into it
|
|
98
|
+
return not any(x + EPS < px < x + w - EPS and y + EPS < py < y + h - EPS for px, py in poly)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load(data):
|
|
102
|
+
if not isinstance(data, dict):
|
|
103
|
+
raise InputError("plan.json must be a JSON object")
|
|
104
|
+
units = data.get("units", "m")
|
|
105
|
+
if units not in ("m", "ft"):
|
|
106
|
+
raise InputError("`units` must be \"m\" or \"ft\"")
|
|
107
|
+
outline = data.get("outline")
|
|
108
|
+
if not isinstance(outline, list) or len(outline) < 3:
|
|
109
|
+
raise InputError("`outline` must list at least three [x, y] corners")
|
|
110
|
+
try:
|
|
111
|
+
outline = [(number(p[0], "outline x"), number(p[1], "outline y")) for p in outline]
|
|
112
|
+
except (TypeError, IndexError):
|
|
113
|
+
raise InputError("every corner in `outline` must be [x, y]")
|
|
114
|
+
rooms = []
|
|
115
|
+
seen = set()
|
|
116
|
+
for i, rm in enumerate(data.get("rooms") or [], 1):
|
|
117
|
+
if not isinstance(rm, dict) or not str(rm.get("name", "")).strip():
|
|
118
|
+
raise InputError(f"room {i} needs a `name`")
|
|
119
|
+
name = str(rm["name"]).strip()
|
|
120
|
+
if name.lower() in seen or name.lower() == "outside":
|
|
121
|
+
raise InputError(f"room name {name!r} is used twice or is reserved")
|
|
122
|
+
seen.add(name.lower())
|
|
123
|
+
rects = rm.get("rects") or ([rm["rect"]] if "rect" in rm else None)
|
|
124
|
+
if not isinstance(rects, list) or not rects:
|
|
125
|
+
raise InputError(f"{name}: give `rect` [x, y, width, depth] or `rects`")
|
|
126
|
+
clean = []
|
|
127
|
+
for r in rects:
|
|
128
|
+
if not isinstance(r, list) or len(r) != 4:
|
|
129
|
+
raise InputError(f"{name}: each rectangle is [x, y, width, depth]")
|
|
130
|
+
r = [number(v, f"{name}: rectangle") for v in r]
|
|
131
|
+
if r[2] <= EPS or r[3] <= EPS:
|
|
132
|
+
raise InputError(f"{name}: rectangle width and depth must be above zero")
|
|
133
|
+
clean.append(r)
|
|
134
|
+
t = rm.get("target_area")
|
|
135
|
+
mw = rm.get("min_width")
|
|
136
|
+
rooms.append({"name": name, "rects": clean,
|
|
137
|
+
"target": number(t, f"{name}: target_area") if t is not None else None,
|
|
138
|
+
"min_width": number(mw, f"{name}: min_width") if mw is not None else None,
|
|
139
|
+
"habitable": bool(rm.get("habitable", False))})
|
|
140
|
+
if not rooms:
|
|
141
|
+
raise InputError("`rooms` must list at least one room")
|
|
142
|
+
names = {r["name"].lower(): r["name"] for r in rooms}
|
|
143
|
+
doors = []
|
|
144
|
+
for i, d in enumerate(data.get("doors") or [], 1):
|
|
145
|
+
if not isinstance(d, dict):
|
|
146
|
+
raise InputError(f"door {i} must be an object")
|
|
147
|
+
btw = d.get("between")
|
|
148
|
+
if not isinstance(btw, list) or len(btw) != 2 or not all(isinstance(b, str) for b in btw):
|
|
149
|
+
raise InputError(f"door {i}: `between` must name two rooms (or a room and \"outside\")")
|
|
150
|
+
resolved = []
|
|
151
|
+
for b in btw:
|
|
152
|
+
if b.lower() == "outside":
|
|
153
|
+
resolved.append("outside")
|
|
154
|
+
elif b.lower() in names:
|
|
155
|
+
resolved.append(names[b.lower()])
|
|
156
|
+
else:
|
|
157
|
+
raise InputError(f"door {i}: no room called {b!r}")
|
|
158
|
+
at = d.get("at")
|
|
159
|
+
if not isinstance(at, list) or len(at) != 2:
|
|
160
|
+
raise InputError(f"door {i}: `at` must be the [x, y] of the door's centre")
|
|
161
|
+
doors.append({"between": resolved, "at": (number(at[0], "door x"), number(at[1], "door y")),
|
|
162
|
+
"width": number(d.get("width", 0.9 if units == "m" else 3.0), f"door {i}: width")})
|
|
163
|
+
windows = []
|
|
164
|
+
for i, w in enumerate(data.get("windows") or [], 1):
|
|
165
|
+
if not isinstance(w, dict) or str(w.get("room", "")).lower() not in names:
|
|
166
|
+
raise InputError(f"window {i}: `room` must name a room")
|
|
167
|
+
at = w.get("at")
|
|
168
|
+
if not isinstance(at, list) or len(at) != 2:
|
|
169
|
+
raise InputError(f"window {i}: `at` must be the [x, y] of the window's centre")
|
|
170
|
+
windows.append({"room": names[w["room"].lower()], "at": (number(at[0], "window x"), number(at[1], "window y")),
|
|
171
|
+
"width": number(w.get("width", 1.2 if units == "m" else 4.0), f"window {i}: width")})
|
|
172
|
+
default_door = 0.8 if units == "m" else 32 / 12
|
|
173
|
+
return {"units": units, "title": str(data.get("title", "Floor plan")), "outline": outline,
|
|
174
|
+
"rooms": rooms, "doors": doors, "windows": windows,
|
|
175
|
+
"min_door_width": number(data.get("min_door_width", default_door), "min_door_width"),
|
|
176
|
+
"tolerance": number(data.get("tolerance", 0.10), "tolerance"),
|
|
177
|
+
"north": number(data.get("north", 0), "north"),
|
|
178
|
+
"wall": number(data.get("wall", 0.2 if units == "m" else 0.5), "wall")}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def check(plan):
|
|
182
|
+
fails, warns = [], []
|
|
183
|
+
rooms, outline = plan["rooms"], plan["outline"]
|
|
184
|
+
by_name = {r["name"]: r for r in rooms}
|
|
185
|
+
for r in rooms:
|
|
186
|
+
r["area"] = sum(x[2] * x[3] for x in r["rects"])
|
|
187
|
+
r["area"] -= sum(overlap(a, b) for i, a in enumerate(r["rects"]) for b in r["rects"][i + 1:])
|
|
188
|
+
for i, a in enumerate(rooms):
|
|
189
|
+
for b in rooms[i + 1:]:
|
|
190
|
+
ov = sum(overlap(x, y) for x in a["rects"] for y in b["rects"])
|
|
191
|
+
if ov > EPS:
|
|
192
|
+
fails.append(f"{a['name']} and {b['name']} overlap by {ov:.2f}")
|
|
193
|
+
for r in rooms:
|
|
194
|
+
if not all(rect_in_poly(x, outline) for x in r["rects"]):
|
|
195
|
+
fails.append(f"{r['name']} runs outside the outline")
|
|
196
|
+
if r["target"]:
|
|
197
|
+
off = (r["area"] - r["target"]) / r["target"]
|
|
198
|
+
if abs(off) > plan["tolerance"] + EPS:
|
|
199
|
+
warns.append(f"{r['name']}: {r['area']:.1f} against a target of {r['target']:g} ({off * 100:+.0f} %)")
|
|
200
|
+
if r["min_width"] is not None:
|
|
201
|
+
narrow = min(min(x[2], x[3]) for x in r["rects"])
|
|
202
|
+
if narrow < r["min_width"] - EPS:
|
|
203
|
+
warns.append(f"{r['name']}: {narrow:g} wide, under its minimum width {r['min_width']:g}")
|
|
204
|
+
graph = {r["name"]: set() for r in rooms}
|
|
205
|
+
graph["outside"] = set()
|
|
206
|
+
for i, d in enumerate(plan["doors"], 1):
|
|
207
|
+
a, b = d["between"]
|
|
208
|
+
label = f"door {i} ({a} – {b})"
|
|
209
|
+
if a == b:
|
|
210
|
+
fails.append(f"{label} joins a room to itself")
|
|
211
|
+
continue
|
|
212
|
+
ok = True
|
|
213
|
+
for side in (a, b):
|
|
214
|
+
if side == "outside":
|
|
215
|
+
ok = ok and on_outline(d["at"], outline)
|
|
216
|
+
else:
|
|
217
|
+
ok = ok and any(on_rect_edge(d["at"], x) for x in by_name[side]["rects"])
|
|
218
|
+
if not ok:
|
|
219
|
+
fails.append(f"{label} at ({d['at'][0]:g}, {d['at'][1]:g}) is not on a wall both sides share")
|
|
220
|
+
continue
|
|
221
|
+
if d["width"] < plan["min_door_width"] - EPS:
|
|
222
|
+
warns.append(f"{label} is {d['width']:g} wide, under {plan['min_door_width']:.2f}")
|
|
223
|
+
graph[a].add(b)
|
|
224
|
+
graph[b].add(a)
|
|
225
|
+
if not graph["outside"]:
|
|
226
|
+
fails.append("no door to outside: the plan has no entrance")
|
|
227
|
+
else:
|
|
228
|
+
seen, todo = {"outside"}, ["outside"]
|
|
229
|
+
while todo:
|
|
230
|
+
for n in graph[todo.pop()]:
|
|
231
|
+
if n not in seen:
|
|
232
|
+
seen.add(n)
|
|
233
|
+
todo.append(n)
|
|
234
|
+
for r in rooms:
|
|
235
|
+
if r["name"] not in seen:
|
|
236
|
+
fails.append(f"{r['name']} cannot be reached from an entrance")
|
|
237
|
+
for w in plan["windows"]:
|
|
238
|
+
if not on_outline(w["at"], outline) or not any(on_rect_edge(w["at"], x) for x in by_name[w["room"]]["rects"]):
|
|
239
|
+
warns.append(f"window in {w['room']} at ({w['at'][0]:g}, {w['at'][1]:g}) is not on an outside wall of that room")
|
|
240
|
+
lit = {w["room"] for w in plan["windows"] if on_outline(w["at"], outline)}
|
|
241
|
+
for r in rooms:
|
|
242
|
+
if r["habitable"] and r["name"] not in lit:
|
|
243
|
+
warns.append(f"{r['name']} is habitable but has no window on an outside wall")
|
|
244
|
+
out_area = poly_area(outline)
|
|
245
|
+
room_area = sum(r["area"] for r in rooms)
|
|
246
|
+
spare = out_area - room_area
|
|
247
|
+
if out_area > 0 and spare / out_area > 0.05 + EPS:
|
|
248
|
+
warns.append(f"{spare:.1f} ({spare / out_area * 100:.0f} %) of the outline is in no room; label it (hall, stair, shaft) or remove it")
|
|
249
|
+
return {"fails": fails, "warns": warns, "outline_area": out_area, "room_area": room_area}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def report(plan, r):
|
|
253
|
+
a = "m²" if plan["units"] == "m" else "sq ft"
|
|
254
|
+
u = plan["units"]
|
|
255
|
+
L = [f"# {plan['title']}: plan check", "", f"| Room | Size ({u}) | Area ({a}) | Target ({a}) |", "|---|---|---:|---:|"]
|
|
256
|
+
for rm in plan["rooms"]:
|
|
257
|
+
size = " + ".join(f"{x[2]:g} × {x[3]:g}" for x in rm["rects"])
|
|
258
|
+
L.append(f"| {rm['name']} | {size} | {rm['area']:.1f} | {rm['target'] if rm['target'] is not None else ''} |")
|
|
259
|
+
L += [f"| **Rooms** | | **{r['room_area']:.1f}** | |",
|
|
260
|
+
f"| **Outline** | | **{r['outline_area']:.1f}** | |", ""]
|
|
261
|
+
if not r["fails"]:
|
|
262
|
+
L.append("PASS no overlaps, every door on a shared wall, every room reachable.")
|
|
263
|
+
L += [f"FAIL {f}" for f in r["fails"]] + [f"WARN {w}" for w in r["warns"]]
|
|
264
|
+
return "\n".join(L) + "\n"
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ---------- drawing ----------
|
|
268
|
+
|
|
269
|
+
def esc(t):
|
|
270
|
+
return str(t).replace("&", "&").replace("<", "<").replace(">", ">")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def svg(plan, r, draw=760):
|
|
274
|
+
o, u = plan["outline"], plan["units"]
|
|
275
|
+
xs, ys = [p[0] for p in o], [p[1] for p in o]
|
|
276
|
+
x0, y0, x1, y1 = min(xs), min(ys), max(xs), max(ys)
|
|
277
|
+
s = draw / max(x1 - x0, y1 - y0)
|
|
278
|
+
m = 80
|
|
279
|
+
W = (x1 - x0) * s + 2 * m + 40
|
|
280
|
+
H = (y1 - y0) * s + 2 * m + 70
|
|
281
|
+
am = "m²" if u == "m" else "sq ft"
|
|
282
|
+
|
|
283
|
+
def P(x, y):
|
|
284
|
+
return (m + (x - x0) * s, H - m - 30 - (y - y0) * s)
|
|
285
|
+
|
|
286
|
+
def pts(poly):
|
|
287
|
+
return " ".join("%.1f,%.1f" % P(*p) for p in poly)
|
|
288
|
+
|
|
289
|
+
wall = max(plan["wall"] * s, 2)
|
|
290
|
+
out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{W:.0f}" height="{H:.0f}" viewBox="0 0 {W:.0f} {H:.0f}" '
|
|
291
|
+
'font-family="Helvetica, Arial, sans-serif" font-size="11">',
|
|
292
|
+
f'<rect width="{W:.0f}" height="{H:.0f}" fill="#fff"/>',
|
|
293
|
+
f'<text x="20" y="28" font-size="16" font-weight="bold">{esc(plan["title"])}</text>',
|
|
294
|
+
f'<text x="20" y="46" fill="#444">Rooms {r["room_area"]:.1f} {am} · outline {r["outline_area"]:.1f} {am} · schematic, not for construction</text>']
|
|
295
|
+
fills = ["#eef4f1", "#f5f0e6", "#eef0f6", "#f6eef0", "#f0f6ee", "#f3f3f3"]
|
|
296
|
+
for i, rm in enumerate(plan["rooms"]):
|
|
297
|
+
for x, y, w, h in rm["rects"]:
|
|
298
|
+
X, Y = P(x, y + h)
|
|
299
|
+
out.append(f'<rect x="{X:.1f}" y="{Y:.1f}" width="{w * s:.1f}" height="{h * s:.1f}" fill="{fills[i % len(fills)]}"/>')
|
|
300
|
+
for rm in plan["rooms"]:
|
|
301
|
+
for (ax, ay), (bx, by) in room_walls(rm["rects"]):
|
|
302
|
+
A, B = P(ax, ay), P(bx, by)
|
|
303
|
+
out.append(f'<line x1="{A[0]:.1f}" y1="{A[1]:.1f}" x2="{B[0]:.1f}" y2="{B[1]:.1f}" stroke="#333" stroke-width="{wall / 2:.1f}" stroke-linecap="square"/>')
|
|
304
|
+
out.append(f'<polygon points="{pts(o)}" fill="none" stroke="#111" stroke-width="{wall:.1f}" stroke-linejoin="miter"/>')
|
|
305
|
+
for rm in plan["rooms"]:
|
|
306
|
+
x, y, w, h = max(rm["rects"], key=lambda q: q[2] * q[3])
|
|
307
|
+
X, Y = P(x + w / 2, y + h / 2)
|
|
308
|
+
size = " + ".join(f"{q[2]:g}×{q[3]:g}" for q in rm["rects"]) + f" · {rm['area']:.1f} {am}"
|
|
309
|
+
fs = 12 if min(w, h) * s > 60 else 9
|
|
310
|
+
if len(size) * (fs - 1) * 0.55 > w * s - 6:
|
|
311
|
+
size = f"{rm['area']:.1f} {am}"
|
|
312
|
+
out.append(f'<text x="{X:.1f}" y="{Y - 4:.1f}" text-anchor="middle" font-weight="bold" font-size="{fs}">{esc(rm["name"])}</text>')
|
|
313
|
+
out.append(f'<text x="{X:.1f}" y="{Y + 9:.1f}" text-anchor="middle" font-size="{fs - 1}" fill="#444">{size}</text>')
|
|
314
|
+
for w in plan["windows"]:
|
|
315
|
+
out += opening(P, s, w["at"], w["width"], plan, wall, window=True)
|
|
316
|
+
for d in plan["doors"]:
|
|
317
|
+
out += opening(P, s, d["at"], d["width"], plan, wall, window=False)
|
|
318
|
+
# overall dimension strings
|
|
319
|
+
bx, by = P(x0, y0)
|
|
320
|
+
ex, _ = P(x1, y0)
|
|
321
|
+
out += dim_line(bx, by + 28, ex, by + 28, f"{x1 - x0:g} {u}")
|
|
322
|
+
_, ty = P(x0, y1)
|
|
323
|
+
out += dim_line(bx - 28, by, bx - 28, ty, f"{y1 - y0:g} {u}", vertical=True)
|
|
324
|
+
out += north(W - 40, 40, plan["north"]) + scale(m, H - 18, s, max(x1 - x0, y1 - y0), u)
|
|
325
|
+
out.append("</svg>")
|
|
326
|
+
return "\n".join(out)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def opening(P, s, at, width, plan, wall, window):
|
|
330
|
+
"""A door (gap plus swing) or window (gap plus glazing lines), on whichever wall it sits."""
|
|
331
|
+
vertical = is_vertical(at, plan)
|
|
332
|
+
x, y = at
|
|
333
|
+
half = width / 2
|
|
334
|
+
a = P(x, y - half) if vertical else P(x - half, y)
|
|
335
|
+
b = P(x, y + half) if vertical else P(x + half, y)
|
|
336
|
+
out = [f'<line x1="{a[0]:.1f}" y1="{a[1]:.1f}" x2="{b[0]:.1f}" y2="{b[1]:.1f}" stroke="#fff" stroke-width="{wall + 1:.1f}"/>']
|
|
337
|
+
if window:
|
|
338
|
+
for off in (-wall / 3, 0, wall / 3):
|
|
339
|
+
dx, dy = (off, 0) if vertical else (0, off)
|
|
340
|
+
out.append(f'<line x1="{a[0] + dx:.1f}" y1="{a[1] + dy:.1f}" x2="{b[0] + dx:.1f}" y2="{b[1] + dy:.1f}" stroke="#2b6cb0" stroke-width="1"/>')
|
|
341
|
+
else:
|
|
342
|
+
r = width * s
|
|
343
|
+
if vertical:
|
|
344
|
+
leaf = (a[0] + r, a[1])
|
|
345
|
+
out.append(f'<line x1="{a[0]:.1f}" y1="{a[1]:.1f}" x2="{leaf[0]:.1f}" y2="{leaf[1]:.1f}" stroke="#b5531a" stroke-width="1.5"/>')
|
|
346
|
+
out.append(f'<path d="M {leaf[0]:.1f} {leaf[1]:.1f} A {r:.1f} {r:.1f} 0 0 0 {b[0]:.1f} {b[1]:.1f}" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>')
|
|
347
|
+
else:
|
|
348
|
+
leaf = (a[0], a[1] - r)
|
|
349
|
+
out.append(f'<line x1="{a[0]:.1f}" y1="{a[1]:.1f}" x2="{leaf[0]:.1f}" y2="{leaf[1]:.1f}" stroke="#b5531a" stroke-width="1.5"/>')
|
|
350
|
+
out.append(f'<path d="M {leaf[0]:.1f} {leaf[1]:.1f} A {r:.1f} {r:.1f} 0 0 1 {b[0]:.1f} {b[1]:.1f}" fill="none" stroke="#b5531a" stroke-width="0.8" stroke-dasharray="3 2"/>')
|
|
351
|
+
return out
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def room_walls(rects):
|
|
355
|
+
"""Each rectangle's edges, less the stretches it shares with another rectangle of the same room."""
|
|
356
|
+
walls = []
|
|
357
|
+
for i, (x, y, w, h) in enumerate(rects):
|
|
358
|
+
others = [q for j, q in enumerate(rects) if j != i]
|
|
359
|
+
for fixed, lo, hi, horizontal, side in ((y, x, x + w, True, -1), (y + h, x, x + w, True, 1),
|
|
360
|
+
(x, y, y + h, False, -1), (x + w, y, y + h, False, 1)):
|
|
361
|
+
cover = []
|
|
362
|
+
for ox, oy, ow, oh in others:
|
|
363
|
+
if horizontal and abs((oy + oh if side < 0 else oy) - fixed) < EPS:
|
|
364
|
+
cover.append((ox, ox + ow))
|
|
365
|
+
if not horizontal and abs((ox + ow if side < 0 else ox) - fixed) < EPS:
|
|
366
|
+
cover.append((oy, oy + oh))
|
|
367
|
+
pieces = [(lo, hi)]
|
|
368
|
+
for c0, c1 in cover:
|
|
369
|
+
nxt = []
|
|
370
|
+
for p0, p1 in pieces:
|
|
371
|
+
if c1 <= p0 + EPS or c0 >= p1 - EPS:
|
|
372
|
+
nxt.append((p0, p1))
|
|
373
|
+
continue
|
|
374
|
+
if c0 > p0 + EPS:
|
|
375
|
+
nxt.append((p0, c0))
|
|
376
|
+
if c1 < p1 - EPS:
|
|
377
|
+
nxt.append((c1, p1))
|
|
378
|
+
pieces = nxt
|
|
379
|
+
for p0, p1 in pieces:
|
|
380
|
+
walls.append(((p0, fixed), (p1, fixed)) if horizontal else ((fixed, p0), (fixed, p1)))
|
|
381
|
+
return walls
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def is_vertical(at, plan):
|
|
385
|
+
"""Whether the wall under a point runs north-south, judged from the rectangle edges and outline it sits on."""
|
|
386
|
+
segs = [e for rm in plan["rooms"] for q in rm["rects"] for e in rect_edges(q)]
|
|
387
|
+
o = plan["outline"]
|
|
388
|
+
segs += [(o[i], o[(i + 1) % len(o)]) for i in range(len(o))]
|
|
389
|
+
for a, b in segs:
|
|
390
|
+
if on_segment(at, a, b):
|
|
391
|
+
return abs(b[0] - a[0]) < abs(b[1] - a[1])
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def dim_line(x1, y1, x2, y2, label, vertical=False):
|
|
396
|
+
out = [f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" stroke="#555" stroke-width="0.8"/>']
|
|
397
|
+
for x, y in ((x1, y1), (x2, y2)):
|
|
398
|
+
out.append(f'<line x1="{x - 4:.1f}" y1="{y + 4:.1f}" x2="{x + 4:.1f}" y2="{y - 4:.1f}" stroke="#555" stroke-width="1.2"/>')
|
|
399
|
+
mx, my = (x1 + x2) / 2, (y1 + y2) / 2
|
|
400
|
+
if vertical:
|
|
401
|
+
out.append(f'<text x="{mx - 6:.1f}" y="{my:.1f}" text-anchor="middle" transform="rotate(-90 {mx - 6:.1f} {my:.1f})">{esc(label)}</text>')
|
|
402
|
+
else:
|
|
403
|
+
out.append(f'<text x="{mx:.1f}" y="{my - 5:.1f}" text-anchor="middle">{esc(label)}</text>')
|
|
404
|
+
return out
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def north(x, y, deg):
|
|
408
|
+
return [f'<g transform="translate({x:.0f} {y:.0f}) rotate({deg:.1f})"><polygon points="0,-20 7,9 0,4 -7,9" fill="#111"/>'
|
|
409
|
+
'<text x="0" y="-25" text-anchor="middle" font-weight="bold">N</text></g>']
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def scale(x, y, s, span, u):
|
|
413
|
+
step = 1.0
|
|
414
|
+
for mlt in (1, 2, 5):
|
|
415
|
+
for e in range(-1, 5):
|
|
416
|
+
v = mlt * 10 ** e
|
|
417
|
+
if step < v <= span / 5:
|
|
418
|
+
step = v
|
|
419
|
+
w = step * s
|
|
420
|
+
return [f'<rect x="{x}" y="{y - 6:.1f}" width="{w / 2:.1f}" height="6" fill="#111"/>',
|
|
421
|
+
f'<rect x="{x + w / 2:.1f}" y="{y - 6:.1f}" width="{w / 2:.1f}" height="6" fill="#fff" stroke="#111"/>',
|
|
422
|
+
f'<text x="{x + w + 6:.1f}" y="{y:.1f}">{step:g} {u}</text>']
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def main(argv=None):
|
|
426
|
+
ap = argparse.ArgumentParser(description="Check a schematic floor plan and draw it.")
|
|
427
|
+
ap.add_argument("plan")
|
|
428
|
+
ap.add_argument("--svg", help="write the dimensioned plan here")
|
|
429
|
+
args = ap.parse_args(argv)
|
|
430
|
+
try:
|
|
431
|
+
with open(args.plan, encoding="utf-8-sig") as f:
|
|
432
|
+
plan = load(json.load(f))
|
|
433
|
+
except (OSError, ValueError, UnicodeDecodeError) as e:
|
|
434
|
+
print(f"FAIL cannot read {args.plan}: {e}")
|
|
435
|
+
return 2
|
|
436
|
+
except InputError as e:
|
|
437
|
+
print(f"FAIL {e}")
|
|
438
|
+
return 2
|
|
439
|
+
r = check(plan)
|
|
440
|
+
print(report(plan, r), end="")
|
|
441
|
+
if args.svg:
|
|
442
|
+
with open(args.svg, "w", encoding="utf-8", newline="\n") as f:
|
|
443
|
+
f.write(svg(plan, r))
|
|
444
|
+
print(f"\nWrote {args.svg}")
|
|
445
|
+
return 1 if r["fails"] else 0
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
if __name__ == "__main__":
|
|
449
|
+
sys.exit(main())
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Ground floor, house on Plot 14",
|
|
3
|
+
"units": "m",
|
|
4
|
+
"north": 15,
|
|
5
|
+
"wall": 0.23,
|
|
6
|
+
"min_door_width": 0.8,
|
|
7
|
+
"outline": [[0, 0], [10, 0], [10, 9], [0, 9]],
|
|
8
|
+
"rooms": [
|
|
9
|
+
{"name": "Foyer", "rect": [0, 0, 2.5, 2.4], "target_area": 6},
|
|
10
|
+
{"name": "Stair", "rect": [0, 2.4, 2.5, 2.4]},
|
|
11
|
+
{"name": "Living", "rect": [2.5, 0, 5.4, 4.8], "target_area": 25.9, "min_width": 3.6, "habitable": true},
|
|
12
|
+
{"name": "Prayer room", "rect": [7.9, 0, 2.1, 2.2], "target_area": 4.5, "habitable": true},
|
|
13
|
+
{"name": "Dining", "rect": [0, 4.8, 3.4, 4.2], "target_area": 14, "habitable": true},
|
|
14
|
+
{"name": "Kitchen", "rect": [3.4, 4.8, 3.0, 4.2], "target_area": 12.6, "habitable": true},
|
|
15
|
+
{"name": "Utility", "rect": [6.4, 4.8, 1.6, 2.3], "target_area": 4},
|
|
16
|
+
{"name": "Toilet", "rect": [6.4, 7.1, 1.6, 1.9], "target_area": 3.2},
|
|
17
|
+
{"name": "Hall", "rects": [[7.9, 2.2, 2.1, 2.6], [8.0, 4.8, 2.0, 4.2]]}
|
|
18
|
+
],
|
|
19
|
+
"doors": [
|
|
20
|
+
{"between": ["outside", "Foyer"], "at": [1.25, 0], "width": 1.0},
|
|
21
|
+
{"between": ["Foyer", "Living"], "at": [2.5, 1.2], "width": 0.9},
|
|
22
|
+
{"between": ["Foyer", "Stair"], "at": [1.25, 2.4], "width": 0.9},
|
|
23
|
+
{"between": ["Living", "Dining"], "at": [2.95, 4.8], "width": 0.9},
|
|
24
|
+
{"between": ["Living", "Prayer room"], "at": [7.9, 1.1], "width": 0.8},
|
|
25
|
+
{"between": ["Living", "Hall"], "at": [7.9, 3.5], "width": 0.9},
|
|
26
|
+
{"between": ["Dining", "Kitchen"], "at": [3.4, 6.9], "width": 0.9},
|
|
27
|
+
{"between": ["Kitchen", "Utility"], "at": [6.4, 6.0], "width": 0.8},
|
|
28
|
+
{"between": ["Hall", "Toilet"], "at": [8.0, 8.0], "width": 0.8},
|
|
29
|
+
{"between": ["Hall", "outside"], "at": [9.0, 9], "width": 0.9}
|
|
30
|
+
],
|
|
31
|
+
"windows": [
|
|
32
|
+
{"room": "Living", "at": [5.2, 0], "width": 2.4},
|
|
33
|
+
{"room": "Dining", "at": [0, 6.9], "width": 1.5},
|
|
34
|
+
{"room": "Kitchen", "at": [4.9, 9], "width": 1.2},
|
|
35
|
+
{"room": "Prayer room", "at": [10, 1.1], "width": 0.9},
|
|
36
|
+
{"room": "Toilet", "at": [7.2, 9], "width": 0.6}
|
|
37
|
+
]
|
|
38
|
+
}
|