skilldrop-cli 1.4.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 +8 -7
- package/bin/skilldrop.js +15 -10
- package/catalogue.json +13 -2
- package/contracts/catalogue.schema.json +2 -1
- package/model-routing.json +12 -8
- package/package.json +1 -1
- package/packs/ai-engineering/skills/judge-audit/SKILL.md +28 -11
- package/packs/ai-engineering/skills/judge-audit/evals/evals.json +30 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/12/judge_runs.csv +241 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/12/rosa_labels.csv +33 -0
- package/packs/ai-engineering/skills/judge-audit/evals/files/13/moderation_eval.csv +601 -0
- package/packs/ai-engineering/skills/judge-audit/manifest.json +2 -2
- package/packs/ai-engineering/skills/mcp-pairing/manifest.json +1 -1
- package/packs/ai-engineering/skills/mcp-pairing/reference.md +4 -3
- package/packs/ai-engineering/skills/subagent-design/SKILL.md +1 -0
- package/packs/ai-engineering/skills/subagent-design/examples/help-center-localization.md +146 -0
- package/packs/ai-engineering/skills/subagent-design/manifest.json +2 -2
- package/packs/api-builder/skills/prompt-caching-advisor/SKILL.md +4 -0
- package/packs/api-builder/skills/prompt-caching-advisor/examples/support-bot-block-order.md +120 -0
- package/packs/api-builder/skills/prompt-caching-advisor/manifest.json +2 -2
- 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/packs/dev-team/loops/release/LOOP.md +4 -4
- package/packs/dev-team/loops/release/loop.json +1 -1
- package/packs/dev-team/skills/bug-triage/SKILL.md +1 -0
- package/packs/dev-team/skills/bug-triage/examples/export-row-limit-escalation.md +126 -0
- package/packs/dev-team/skills/bug-triage/manifest.json +2 -2
- package/packs/product-manager/skills/prd-draft/SKILL.md +1 -0
- package/packs/product-manager/skills/prd-draft/examples/approver-delay-not-slack.md +142 -0
- package/packs/product-manager/skills/prd-draft/manifest.json +2 -2
- package/profiles.json +1 -1
- package/packs/ai-engineering/loops/adopt/LOOP.md +0 -120
- package/packs/ai-engineering/loops/adopt/evals/eval_queries.json +0 -10
- package/packs/ai-engineering/loops/adopt/evals/evals.json +0 -69
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/ai-ideas.md +0 -15
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/org-context.md +0 -27
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/staff-survey.csv +0 -11
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-sponsor-slice-blocked-by-client-data/ai-ideas.md +0 -13
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-sponsor-slice-blocked-by-client-data/firm-context.md +0 -23
- package/packs/ai-engineering/loops/adopt/loop.json +0 -53
- package/packs/career/loops/apply/LOOP.md +0 -97
- package/packs/career/loops/apply/evals/eval_queries.json +0 -9
- package/packs/career/loops/apply/evals/evals.json +0 -55
- package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/application-form.md +0 -10
- package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/job-posting.md +0 -33
- package/packs/career/loops/apply/evals/files/apply-candidate-decided-full-run/resume.md +0 -34
- package/packs/career/loops/apply/evals/files/apply-fit-gate-hard-requirement/cv.md +0 -27
- package/packs/career/loops/apply/evals/files/apply-fit-gate-hard-requirement/warehouse-supervisor.md +0 -30
- package/packs/career/loops/apply/loop.json +0 -52
- package/packs/product-manager/loops/discover/LOOP.md +0 -82
- package/packs/product-manager/loops/discover/evals/eval_queries.json +0 -30
- package/packs/product-manager/loops/discover/evals/evals.json +0 -54
- package/packs/product-manager/loops/discover/evals/files/discover-no-show-signal/interview-notes.md +0 -63
- package/packs/product-manager/loops/discover/evals/files/discover-no-show-signal/support-tickets.csv +0 -13
- package/packs/product-manager/loops/discover/evals/files/discover-vp-loyalty-no-evidence/vp-message.txt +0 -13
- package/packs/product-manager/loops/discover/loop.json +0 -41
- package/packs/sre-oncall/loops/operate/LOOP.md +0 -84
- package/packs/sre-oncall/loops/operate/evals/eval_queries.json +0 -30
- package/packs/sre-oncall/loops/operate/evals/evals.json +0 -53
- package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/alerts.log +0 -22
- package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/incident-chat.txt +0 -27
- package/packs/sre-oncall/loops/operate/evals/files/operate-export-service-first-outage/service-notes.md +0 -46
- package/packs/sre-oncall/loops/operate/evals/files/operate-incident-still-open/oncall-log.txt +0 -17
- package/packs/sre-oncall/loops/operate/evals/files/operate-incident-still-open/runbook-webhooks.md +0 -38
- package/packs/sre-oncall/loops/operate/loop.json +0 -41
|
@@ -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())
|