skilldrop-cli 1.5.0 → 1.7.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 +4 -1
- package/catalogue.json +25 -2
- package/model-routing.json +20 -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/packs/event-planner/pack.json +61 -0
- package/packs/event-planner/skills/run-of-show/SKILL.md +141 -0
- package/packs/event-planner/skills/run-of-show/evals/eval_queries.json +30 -0
- package/packs/event-planner/skills/run-of-show/evals/evals.json +103 -0
- package/packs/event-planner/skills/run-of-show/evals/files/1/agenda.md +16 -0
- package/packs/event-planner/skills/run-of-show/evals/files/2/schedule.csv +8 -0
- package/packs/event-planner/skills/run-of-show/evals/files/3/notes.txt +3 -0
- package/packs/event-planner/skills/run-of-show/evals/files/4/sessions_export.csv +25 -0
- package/packs/event-planner/skills/run-of-show/evals/files/4/venue_and_crew.txt +27 -0
- package/packs/event-planner/skills/run-of-show/evals/files/5/runsheet_v4.csv +28 -0
- package/packs/event-planner/skills/run-of-show/evals/files/5/venue_and_suppliers.txt +22 -0
- package/packs/event-planner/skills/run-of-show/evals/files/6/acts_from_booker.csv +26 -0
- package/packs/event-planner/skills/run-of-show/evals/files/6/venue_rooms_and_crew.txt +14 -0
- package/packs/event-planner/skills/run-of-show/evals/files/7/runsheet_draft_v4.csv +25 -0
- package/packs/event-planner/skills/run-of-show/evals/files/7/venue_and_fixed_times.txt +24 -0
- package/packs/event-planner/skills/run-of-show/examples/product-launch.md +77 -0
- package/packs/event-planner/skills/run-of-show/examples/product-launch.svg +58 -0
- package/packs/event-planner/skills/run-of-show/manifest.json +44 -0
- package/packs/event-planner/skills/run-of-show/scripts/show_check.py +295 -0
- package/packs/event-planner/skills/run-of-show/templates/show.json +15 -0
- package/packs/event-planner/skills/seating-plan/SKILL.md +152 -0
- package/packs/event-planner/skills/seating-plan/evals/eval_queries.json +30 -0
- package/packs/event-planner/skills/seating-plan/evals/evals.json +134 -0
- package/packs/event-planner/skills/seating-plan/evals/files/1/guests.csv +31 -0
- package/packs/event-planner/skills/seating-plan/evals/files/2/table-plan.txt +8 -0
- package/packs/event-planner/skills/seating-plan/evals/files/3/brief.txt +4 -0
- package/packs/event-planner/skills/seating-plan/evals/files/3/guests.csv +49 -0
- package/packs/event-planner/skills/seating-plan/evals/files/4/rsvp_export.csv +127 -0
- package/packs/event-planner/skills/seating-plan/evals/files/4/seating_notes.txt +21 -0
- package/packs/event-planner/skills/seating-plan/evals/files/5/organiser_notes.txt +10 -0
- package/packs/event-planner/skills/seating-plan/evals/files/5/registrations.csv +93 -0
- package/packs/event-planner/skills/seating-plan/evals/files/5/table_plan_v3.csv +94 -0
- package/packs/event-planner/skills/seating-plan/evals/files/6/bridgets_notes.txt +30 -0
- package/packs/event-planner/skills/seating-plan/evals/files/6/ticket_export.csv +117 -0
- package/packs/event-planner/skills/seating-plan/evals/files/6/venue_floorplan_notes.txt +14 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/keep_apart_CONFIDENTIAL.txt +9 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/prom_seating_v3.csv +101 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/ticket_list.csv +101 -0
- package/packs/event-planner/skills/seating-plan/evals/files/7/venue_info.txt +16 -0
- package/packs/event-planner/skills/seating-plan/evals/files/8/registrations_export.csv +147 -0
- package/packs/event-planner/skills/seating-plan/evals/files/8/room_layout.md +22 -0
- package/packs/event-planner/skills/seating-plan/evals/files/8/seating_requests.txt +31 -0
- package/packs/event-planner/skills/seating-plan/evals/files/9/guest_list.csv +84 -0
- package/packs/event-planner/skills/seating-plan/evals/files/9/notes.txt +16 -0
- package/packs/event-planner/skills/seating-plan/evals/files/9/table_plan.csv +85 -0
- package/packs/event-planner/skills/seating-plan/examples/wedding-dinner.md +61 -0
- package/packs/event-planner/skills/seating-plan/examples/wedding-dinner.svg +39 -0
- package/packs/event-planner/skills/seating-plan/manifest.json +44 -0
- package/packs/event-planner/skills/seating-plan/scripts/seating_check.py +324 -0
- package/packs/event-planner/skills/seating-plan/templates/seating.json +28 -0
- package/packs/experience-design/pack.json +5 -0
- package/packs/experience-design/skills/locale-check/SKILL.md +139 -0
- package/packs/experience-design/skills/locale-check/evals/eval_queries.json +38 -0
- package/packs/experience-design/skills/locale-check/evals/evals.json +104 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/budget.json +1 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/ar.json +16 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/de.json +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/en.json +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/1/locales/pl.json +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values/strings.xml +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values-es/strings.xml +11 -0
- package/packs/experience-design/skills/locale-check/evals/files/2/app/src/main/res/values-fr/strings.xml +12 -0
- package/packs/experience-design/skills/locale-check/evals/files/3/po/ja.po +20 -0
- package/packs/experience-design/skills/locale-check/evals/files/3/po/messages.pot +23 -0
- package/packs/experience-design/skills/locale-check/evals/files/3/po/ru.po +22 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/budgets.json +15 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/ar.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/de.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/en.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/fr.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/4/locales/pl.json +265 -0
- package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values/strings.xml +77 -0
- package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values-es/strings.xml +63 -0
- package/packs/experience-design/skills/locale-check/evals/files/5/app/src/main/res/values-ja/strings.xml +61 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/cs.po +605 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/marlin.pot +602 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/pt_BR.po +602 -0
- package/packs/experience-design/skills/locale-check/evals/files/6/po/ru.po +608 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/en.lproj/Localizable.strings +160 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/en.lproj/Localizable.stringsdict +70 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/he.lproj/Localizable.strings +157 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/he.lproj/Localizable.stringsdict +86 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/it.lproj/Localizable.strings +160 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/it.lproj/Localizable.stringsdict +68 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/ko.lproj/Localizable.strings +160 -0
- package/packs/experience-design/skills/locale-check/evals/files/7/Tidewatch/Resources/ko.lproj/Localizable.stringsdict +62 -0
- package/packs/experience-design/skills/locale-check/examples/settings-release.md +107 -0
- package/packs/experience-design/skills/locale-check/manifest.json +45 -0
- package/packs/experience-design/skills/locale-check/scripts/locale_check.py +1105 -0
- package/packs/operations-manager/pack.json +60 -0
- package/packs/operations-manager/skills/shift-roster/SKILL.md +166 -0
- package/packs/operations-manager/skills/shift-roster/evals/eval_queries.json +38 -0
- package/packs/operations-manager/skills/shift-roster/evals/evals.json +135 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/1/shifts.csv +3 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/1/staff.csv +8 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/2/rota.csv +22 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/2/staff.csv +6 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/3/team.md +14 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/4/availability_export_wk47.csv +23 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/4/rota_notes.txt +20 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/5/rota_draft_16-29nov.csv +107 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/5/staff_list.csv +17 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/6/availability_notes.txt +24 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/6/peoplehub_staff_export.csv +14 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/6/rota_template.csv +25 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/7/leave_and_availability.txt +18 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/7/rota_wc_30nov.csv +47 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/7/staff_list.csv +15 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/8/availability_form_responses.csv +22 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/8/cover_requirements.csv +15 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/8/staff_export.csv +29 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/9/leave_and_restrictions.txt +16 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/9/rota_export_07-13Dec.csv +85 -0
- package/packs/operations-manager/skills/shift-roster/evals/files/9/staff_list.csv +27 -0
- package/packs/operations-manager/skills/shift-roster/examples/cafe-week.md +72 -0
- package/packs/operations-manager/skills/shift-roster/examples/cafe-week.svg +123 -0
- package/packs/operations-manager/skills/shift-roster/manifest.json +34 -0
- package/packs/operations-manager/skills/shift-roster/scripts/roster_check.py +633 -0
- package/packs/operations-manager/skills/shift-roster/templates/roster.json +22 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Seated dinners and event days that hold together: a table plan where every guest has one seat and every together, keep-apart and access request holds, and a run of show where no room or person is booked twice and every changeover has the time it needs, each checked by a script and drawn before anyone prints it.",
|
|
3
|
+
"display_name": "Event planner",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"events",
|
|
6
|
+
"event planning",
|
|
7
|
+
"seating plan",
|
|
8
|
+
"table plan",
|
|
9
|
+
"wedding",
|
|
10
|
+
"run of show",
|
|
11
|
+
"running order",
|
|
12
|
+
"agenda",
|
|
13
|
+
"conference"
|
|
14
|
+
],
|
|
15
|
+
"links": {
|
|
16
|
+
"documentation": "https://sananthanarayan.github.io/skilldrop/packs/event-planner/"
|
|
17
|
+
},
|
|
18
|
+
"maintainers": [
|
|
19
|
+
{
|
|
20
|
+
"name": "sananthanarayan"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"first-value": {
|
|
24
|
+
"audience-posture": "non-technical",
|
|
25
|
+
"prerequisites": [
|
|
26
|
+
"The guest list with households and requests, or the day's sessions with rooms and times",
|
|
27
|
+
"Python 3 on the machine, which the checking and drawing scripts run on"
|
|
28
|
+
],
|
|
29
|
+
"starter-task": "Get a checked seating plan from a guest list and a few requests",
|
|
30
|
+
"starter-prompt": "Seat these 36 wedding guests at four tables of 10 (the head table seats 6). Keep each household together, keep Dan and Priya apart, and Grandma Rose uses a wheelchair, so she needs table 1, which is step-free. Here's the list: <paste it>.",
|
|
31
|
+
"verification": "seating-plan replies with a table of who sits where, names any request it couldn't meet and why, lists dietary needs per table, and saves a drawing of the room with names at each table, after a check that every guest has one seat and every request holds.",
|
|
32
|
+
"recovery": "If seating-plan does not activate, ask for it by name (\"use seating-plan\"). If a request can't be met, say which one may give, and ask again."
|
|
33
|
+
},
|
|
34
|
+
"requires": [
|
|
35
|
+
"core"
|
|
36
|
+
],
|
|
37
|
+
"journey": {
|
|
38
|
+
"when": "You're running a seated dinner or a day of sessions, and want a plan that won't fall apart on the day.",
|
|
39
|
+
"you_bring": [
|
|
40
|
+
"the guest list, with households, access needs and dietary needs",
|
|
41
|
+
"the tables: how many, seats each, which are step-free",
|
|
42
|
+
"who must sit together and who must be kept apart",
|
|
43
|
+
"the day's sessions, rooms, speakers and venue hours"
|
|
44
|
+
],
|
|
45
|
+
"steps": [
|
|
46
|
+
{
|
|
47
|
+
"skill": "run-of-show",
|
|
48
|
+
"you_do": "Give it the sessions, rooms, speakers and the venue's hours.",
|
|
49
|
+
"you_get": "A running order with every changeover and break, checked so no room or person is booked twice, and a timeline per room.",
|
|
50
|
+
"decide": "Accept the moves it proposes for any clash, or name what may give."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"skill": "seating-plan",
|
|
54
|
+
"you_do": "Give it the guest list, the tables and the requests.",
|
|
55
|
+
"you_get": "A table-by-table plan with every request held or named, the caterer's list per table, and a drawing of the room.",
|
|
56
|
+
"decide": "Print the place cards, or ask for another arrangement."
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"you_end_with": "A running order the crew can work from and a seating plan the venue can set, both checked."
|
|
60
|
+
}
|
|
61
|
+
}
|