1use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct Plan {
25 #[serde(default = "untitled")]
26 pub name: String,
27 #[serde(default)]
28 pub description: String,
29 #[serde(default)]
32 pub palette: Palette,
33 #[serde(default, alias = "regions")]
38 pub builds: Vec<Build>,
39 #[serde(default)]
42 pub needs: Vec<Need>,
43 #[serde(default)]
45 pub veils: Vec<Veil>,
46 #[serde(default)]
47 pub spawns: Vec<PlanSpawn>,
48 #[serde(default)]
49 pub lights: Vec<PlanLight>,
50 #[serde(default)]
52 pub signs: Vec<Sign>,
53}
54
55fn untitled() -> String {
56 "place".to_string()
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
60pub struct Palette {
61 #[serde(default)]
63 pub style: String,
64 #[serde(default)]
66 pub stone: String,
67 #[serde(default)]
68 pub wood: String,
69 #[serde(default)]
70 pub metal: String,
71 #[serde(default)]
73 pub sky: String,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct Build {
79 #[serde(default = "slab")]
82 pub shape: String,
83 #[serde(default)]
84 pub at: [f32; 3],
85 #[serde(default)]
87 pub yaw: f32,
88 #[serde(default)]
90 pub r: f32,
91 #[serde(default)]
92 pub w: f32,
93 #[serde(default)]
94 pub d: f32,
95 #[serde(default = "tenth")]
96 pub h: f32,
97 #[serde(default)]
100 pub material: String,
101 #[serde(default = "yes")]
103 pub solid: bool,
104 #[serde(default)]
105 pub name: String,
106}
107
108fn slab() -> String {
109 "slab".to_string()
110}
111fn tenth() -> f32 {
112 0.1
113}
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct Need {
118 pub kind: String,
120 #[serde(default)]
121 pub at: [f32; 3],
122 #[serde(default)]
124 pub yaw: f32,
125 #[serde(default)]
127 pub w: f32,
128 #[serde(default)]
129 pub h: f32,
130 #[serde(default)]
131 pub d: f32,
132 #[serde(default = "quarter")]
134 pub tol: f32,
135 #[serde(default)]
137 pub style: String,
138 #[serde(default)]
139 pub tags: Vec<String>,
140 #[serde(default = "yes")]
143 pub commission: bool,
144 #[serde(default = "yes")]
146 pub must: bool,
147 #[serde(default = "yes")]
149 pub solid: bool,
150 #[serde(default)]
151 pub name: String,
152}
153
154fn quarter() -> f32 {
155 0.25
156}
157fn yes() -> bool {
158 true
159}
160
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162pub struct Veil {
163 #[serde(default)]
164 pub at: [f32; 3],
165 #[serde(default)]
166 pub yaw: f32,
167 #[serde(default)]
169 pub to: String,
170 #[serde(default)]
171 pub label: String,
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct PlanSpawn {
176 #[serde(default)]
177 pub at: [f32; 3],
178 #[serde(default)]
179 pub yaw: f32,
180 #[serde(default)]
181 pub name: String,
182}
183
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct PlanLight {
186 #[serde(default)]
187 pub at: [f32; 3],
188 #[serde(default = "one_f")]
189 pub warm: f32,
190 #[serde(default = "eight")]
191 pub range: f32,
192 #[serde(default = "one_f")]
193 pub intensity: f32,
194 #[serde(default = "yes")]
196 pub fixture: bool,
197}
198
199fn one_f() -> f32 {
200 1.0
201}
202fn eight() -> f32 {
203 8.0
204}
205
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207pub struct Sign {
208 #[serde(default)]
209 pub at: [f32; 3],
210 #[serde(default)]
211 pub yaw: f32,
212 #[serde(default)]
213 pub text: String,
214 #[serde(default = "two")]
215 pub w: f32,
216 #[serde(default = "one_and_a_half")]
217 pub h: f32,
218}
219
220fn two() -> f32 {
221 2.0
222}
223fn one_and_a_half() -> f32 {
224 1.5
225}
226
227pub mod metric {
231 pub const DOOR_W: f32 = 1.1;
233 pub const DOOR_H: f32 = 2.3;
234 pub const CORRIDOR_W: f32 = 1.8;
236 pub const CEILING_MIN: f32 = DOOR_H * 1.6;
238 pub const VEIL_CLEAR: f32 = 3.0;
241 pub const PLAZA_MIN: f32 = 12.0;
243 pub const PLAZA_MAX: f32 = 20.0;
244 pub const LAMP_SPACING: f32 = 7.0;
246}
247
248impl Plan {
249 pub fn check(&self) -> Vec<String> {
252 let mut out = Vec::new();
253 if self.spawns.is_empty() {
254 out.push("no spawn: nobody can arrive".into());
255 }
256 for s in &self.spawns {
257 for v in &self.veils {
258 let d = dist(s.at, v.at);
259 if d < metric::VEIL_CLEAR {
260 out.push(format!(
261 "spawn '{}' is {d:.1} m from a veil — arrivals land inside the doorway (keep {:.1} m)",
262 s.name, metric::VEIL_CLEAR
263 ));
264 }
265 }
266 }
267 if !self
268 .builds
269 .iter()
270 .any(|b| matches!(b.shape.as_str(), "disc" | "slab"))
271 {
272 out.push("no ground: the place has no floor".into());
273 }
274 if self.lights.is_empty() && self.palette.sky.is_empty() {
275 out.push("no lights and no sky — this place renders dark".into());
276 }
277 for (i, a) in self.needs.iter().enumerate() {
279 for b in self.needs.iter().skip(i + 1) {
280 let clearance = ((a.w.max(a.d) + b.w.max(b.d)) / 2.0) * 0.6;
281 if a.solid && b.solid && dist(a.at, b.at) < clearance {
282 out.push(format!(
283 "'{}' and '{}' overlap at {:?}",
284 label(a, i),
285 label(b, i + 1),
286 a.at
287 ));
288 }
289 }
290 }
291 out
292 }
293}
294
295fn label(n: &Need, i: usize) -> String {
296 if n.name.is_empty() {
297 format!("{} #{i}", n.kind)
298 } else {
299 n.name.clone()
300 }
301}
302
303fn dist(a: [f32; 3], b: [f32; 3]) -> f32 {
304 let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
305 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn a_plan_is_a_bill_of_requirements_and_checks_itself() {
314 let p: Plan = serde_json::from_str(
315 r#"{ "name": "hall",
316 "palette": { "style": "classical", "stone": "marble", "sky": "dusk" },
317 "builds": [ { "shape": "disc", "r": 12, "material": "stone" } ],
318 "needs": [
319 { "kind": "column", "at": [8,0,0], "h": 5.2, "w": 0.9, "d": 0.9 },
320 { "kind": "column", "at": [-8,0,0], "h": 5.2, "w": 0.9, "d": 0.9 }
321 ],
322 "spawns": [ { "at": [0,0,10] } ],
323 "veils": [ { "at": [0,1.4,-11], "label": "Onward" } ],
324 "lights": [ { "at": [0,3,0] } ] }"#,
325 )
326 .unwrap();
327 assert_eq!(p.needs.len(), 2);
328 assert!(p.needs[0].commission, "commissioning is the default");
329 assert!(p.check().is_empty(), "a sound plan: {:?}", p.check());
330
331 let bad: Plan = serde_json::from_str(
333 r#"{ "needs": [ { "kind": "column", "at": [0,0,0], "w": 1, "d": 1 },
334 { "kind": "column", "at": [0.2,0,0], "w": 1, "d": 1 } ],
335 "spawns": [ { "at": [0,0,0] } ],
336 "veils": [ { "at": [0,1.4,1] } ] }"#,
337 )
338 .unwrap();
339 let found = bad.check();
340 assert!(found.iter().any(|f| f.contains("doorway")), "{found:?}");
341 assert!(found.iter().any(|f| f.contains("overlap")), "{found:?}");
342 assert!(found.iter().any(|f| f.contains("floor")), "{found:?}");
343 assert!(found.iter().any(|f| f.contains("dark")), "{found:?}");
344 }
345}