delvewright_dsl/siteplan/
measure.rs1use super::*;
4
5enum Measured {
11 Value(f64),
12 Unresolved(Diagnostic),
13}
14
15pub(super) fn identities(
30 c: &Campaign,
31 plan: &SitePlanContent,
32 placed: &[Placed<'_>],
33 d: &mut Vec<Diagnostic>,
34) {
35 let facts: BTreeMap<&str, &crate::layout::BriefFact> = c
36 .geometry_brief
37 .as_ref()
38 .map(|b| {
39 b.content
40 .facts
41 .iter()
42 .map(|f| (f.id.0.as_str(), f))
43 .collect()
44 })
45 .unwrap_or_default();
46
47 if facts.is_empty() || plan.identities.is_empty() {
48 let empty = match (facts.is_empty(), plan.identities.is_empty()) {
49 (true, true) => "the brief states no fact and the plan declares no identity",
50 (true, false) => "the brief states no fact",
51 _ => "the plan declares no identity",
52 };
53 d.push(Diagnostic::warning(
54 DW_IDENTITY_EMPTY,
55 "site-plan",
56 "/content/identities",
57 format!(
58 "the identity gate binds nothing: {empty}. This is what holds the whole map to \
59 the design somebody wrote down — with either side empty, the plan may say \
60 anything at all and every check above will still pass, because none of them \
61 has an opinion about how big the map was meant to be. It is a warning rather \
62 than a refusal so that a deliberately minimal plan stays compilable; it is \
63 printed every run so that the emptiness is never quietly a pass."
64 ),
65 ));
66 }
67
68 let by_node: BTreeMap<&str, &Placed<'_>> =
69 placed.iter().map(|p| (p.plan.node.0.as_str(), p)).collect();
70 let datums: BTreeMap<&str, i64> = plan.datums.iter().map(|x| (x.id.0.as_str(), x.y)).collect();
71
72 for (i, id) in plan.identities.iter().enumerate() {
73 let Some(fact) = facts.get(id.fact.0.as_str()) else {
74 d.push(Diagnostic::error(
75 DW_PLAN_AGREEMENT,
76 "site-plan",
77 format!("/content/identities/{i}/fact"),
78 format!(
79 "this identity holds the map to `{f}`, which the geometry brief states no \
80 fact for. An identity binds to a number the brief WROTE DOWN — that is what \
81 makes it a design being kept rather than an assertion the plan makes about \
82 itself.",
83 f = id.fact,
84 ),
85 ));
86 continue;
87 };
88 let measured = measure(&id.measure, plan, &by_node, &datums, i);
89 let value = match measured {
90 Measured::Value(v) => v,
91 Measured::Unresolved(diag) => {
92 d.push(diag);
93 continue;
94 }
95 };
96 if id.cmp.holds(value, fact.value) {
97 continue;
98 }
99 d.push(Diagnostic::error(
100 DW_IDENTITY_FALSE,
101 "site-plan",
102 format!("/content/identities/{i}"),
103 format!(
104 "the plan does not keep `{f}`: {what} measures {value}, and the brief asks for \
105 {cmp} {want}{unit}. The brief's sentence was: \"{note}\". Either move the \
106 geometry until the number is true, or change the brief's fact — in the brief, \
107 where the design is written down, so that the change is a decision somebody \
108 took rather than a plan that drifted.",
109 f = id.fact,
110 what = describe(&id.measure),
111 cmp = id.cmp.as_str(),
112 want = fact.value,
113 unit = fact
114 .unit
115 .as_ref()
116 .map(|u| format!(" {u}"))
117 .unwrap_or_default(),
118 note = fact.note,
119 ),
120 ));
121 }
122}
123
124fn measure(
126 m: &Measure,
127 plan: &SitePlanContent,
128 by_node: &BTreeMap<&str, &Placed<'_>>,
129 datums: &BTreeMap<&str, i64>,
130 i: usize,
131) -> Measured {
132 let missing_node = |node: &NodeId| {
133 Measured::Unresolved(Diagnostic::error(
134 DW_PLAN_AGREEMENT,
135 "site-plan",
136 format!("/content/identities/{i}/measure"),
137 format!(
138 "this identity measures `{node}`, which this plan embeds no box for. A measure \
139 is taken off the geometry, so it can only name a place the plan actually put \
140 somewhere."
141 ),
142 ))
143 };
144 match m {
145 Measure::RegionExtent { axis } => {
146 Measured::Value(f64::from(plan.region.extent[axis.index()].get()))
147 }
148 Measure::BoxExtent { node, axis } => match by_node.get(node.0.as_str()) {
149 Some(p) => Measured::Value(f64::from(p.plan.extent[axis.index()].get())),
150 None => missing_node(node),
151 },
152 Measure::BoxHeight { node } => match by_node.get(node.0.as_str()) {
153 Some(p) => Measured::Value(f64::from(p.clearance)),
154 None => missing_node(node),
155 },
156 Measure::DistanceXz { from, to } => {
157 let (Some(a), Some(b)) = (by_node.get(from.0.as_str()), by_node.get(to.0.as_str()))
158 else {
159 return missing_node(if by_node.contains_key(from.0.as_str()) {
160 to
161 } else {
162 from
163 });
164 };
165 let (ax, az) = a.centre_xz();
166 let (bx, bz) = b.centre_xz();
167 Measured::Value(((bx - ax).powi(2) + (bz - az).powi(2)).sqrt())
168 }
169 Measure::DatumY { datum } => match datums.get(datum.0.as_str()) {
170 Some(y) => Measured::Value(*y as f64),
171 None => Measured::Unresolved(Diagnostic::error(
172 crate::codes::DANGLING_REF,
173 "site-plan",
174 format!("/content/identities/{i}/measure"),
175 format!(
176 "this identity measures `{datum}`, which this plan declares no `datums[]` \
177 entry for."
178 ),
179 )),
180 },
181 }
182}
183
184fn describe(m: &Measure) -> String {
186 match m {
187 Measure::RegionExtent { axis } => {
188 format!("the region's extent on {}", axis.as_str())
189 }
190 Measure::BoxExtent { node, axis } => {
191 format!("`{node}`'s footprint on {}", axis.as_str())
192 }
193 Measure::BoxHeight { node } => format!("`{node}`'s headroom"),
194 Measure::DistanceXz { from, to } => {
195 format!("the horizontal distance from `{from}` to `{to}`")
196 }
197 Measure::DatumY { datum } => format!("the plane `{datum}`"),
198 }
199}
200
201impl Axis {
202 fn index(self) -> usize {
203 match self {
204 Axis::X => 0,
205 Axis::Y => 1,
206 Axis::Z => 2,
207 }
208 }
209
210 fn as_str(self) -> &'static str {
211 match self {
212 Axis::X => "x",
213 Axis::Y => "y",
214 Axis::Z => "z",
215 }
216 }
217}
218
219impl PlanAxis {
220 fn index(self) -> usize {
221 match self {
222 PlanAxis::X => 0,
223 PlanAxis::Z => 1,
224 }
225 }
226
227 fn as_str(self) -> &'static str {
228 match self {
229 PlanAxis::X => "x",
230 PlanAxis::Z => "z",
231 }
232 }
233}
234
235impl VolumeRole {
236 #[must_use]
238 pub fn as_str(self) -> &'static str {
239 match self {
240 VolumeRole::Massif => "massif",
241 VolumeRole::Ground => "ground",
242 VolumeRole::Clearance => "clearance",
243 }
244 }
245}