1use serde::{Deserialize, Serialize};
14
15use crate::{
16 BoundsSet, ElementId, ErrorCode, FileMakerError, LayoutTrace, Provenance, Rect,
17 ResolvedElement, ResolvedPage, ResolvedScene, ResourceLimits, Result, Unit,
18};
19
20#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
22pub struct ElementInspection {
23 pub page: usize,
25 pub id: ElementId,
27 pub bounds: BoundsSet,
29 pub collidable: bool,
31 pub layer: String,
33 pub z_index: i32,
35 pub provenance: Provenance,
37 pub layout_trace: LayoutTrace,
39 pub table_fragment: Option<usize>,
41 pub table_rows: Option<usize>,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
47pub struct PageInspection {
48 pub page: usize,
50 pub role: crate::PageRole,
52 pub exclusions: Vec<String>,
54 pub regions: Vec<String>,
56 pub safe: Option<Rect>,
58 pub elements: usize,
60 pub occupied: Option<Rect>,
62 pub overflow: Vec<ElementId>,
64}
65
66#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
68pub struct LayoutExplanation {
69 pub id: ElementId,
71 pub page: usize,
73 pub bounds: BoundsSet,
75 pub measured: bool,
77 pub patch_count: usize,
79 pub components: Vec<String>,
81 pub source: String,
83 pub decisions: Vec<String>,
85 pub trace: LayoutTrace,
87}
88
89pub struct SceneInspector<'a> {
91 scene: &'a ResolvedScene,
92}
93
94impl<'a> SceneInspector<'a> {
95 #[must_use]
97 pub const fn new(scene: &'a ResolvedScene) -> Self {
98 Self { scene }
99 }
100
101 pub fn inspect_element(&self, id: &ElementId) -> Result<ElementInspection> {
103 let (page, element) = self.find(id)?;
104 Ok(ElementInspection {
105 page,
106 id: element.id.clone(),
107 bounds: element.bounds,
108 collidable: element.collidable,
109 layer: element.layer.clone(),
110 z_index: element.z_index,
111 provenance: element.provenance.clone(),
112 layout_trace: element.layout_trace.clone(),
113 table_fragment: element.table.as_ref().map(|table| table.index),
114 table_rows: element.table.as_ref().map(|table| table.rows.len()),
115 })
116 }
117
118 pub fn inspect_page(&self, page: usize) -> Result<PageInspection> {
120 let page_ref = self.page(page)?;
121 let page_bounds = Rect::new(
122 Unit::ZERO,
123 Unit::ZERO,
124 page_ref.size.width,
125 page_ref.size.height,
126 )?;
127 let mut occupied: Option<Rect> = None;
128 let mut overflow = Vec::new();
129 for element in &page_ref.elements {
130 occupied = Some(match occupied {
131 Some(bounds) => bounds.union(element.bounds.layout)?,
132 None => element.bounds.layout,
133 });
134 if !contains_rect(page_bounds, element.bounds.visual)? {
135 overflow.push(element.id.clone());
136 }
137 }
138 Ok(PageInspection {
139 page,
140 role: page_ref.role,
141 exclusions: page_ref
142 .exclusions
143 .iter()
144 .map(|exclusion| exclusion.name.clone())
145 .collect(),
146 regions: page_ref
147 .regions
148 .iter()
149 .map(|region| region.name.clone())
150 .collect(),
151 safe: page_ref
152 .page_template
153 .as_ref()
154 .map(crate::PageTemplate::safe_bounds)
155 .transpose()?,
156 elements: page_ref.elements.len(),
157 occupied,
158 overflow,
159 })
160 }
161
162 pub fn explain_layout(&self, id: &ElementId) -> Result<LayoutExplanation> {
164 let (page, element) = self.find(id)?;
165 let mut decisions = vec![format!(
166 "layout=({}, {}, {}, {})",
167 element.bounds.layout.origin.x.raw(),
168 element.bounds.layout.origin.y.raw(),
169 element.bounds.layout.size.width.raw(),
170 element.bounds.layout.size.height.raw()
171 )];
172 if element.bounds.intrinsic != element.bounds.layout {
173 decisions.push(format!(
174 "measurement intrinsic=({}, {}, {}, {}) constrained by layout",
175 element.bounds.intrinsic.origin.x.raw(),
176 element.bounds.intrinsic.origin.y.raw(),
177 element.bounds.intrinsic.size.width.raw(),
178 element.bounds.intrinsic.size.height.raw()
179 ));
180 } else {
181 decisions.push("measurement matched proposed layout".to_owned());
182 }
183 decisions.push(format!(
184 "source x={:?} y={:?} width={:?} height={:?} region={:?}",
185 element.layout_trace.geometry.x,
186 element.layout_trace.geometry.y,
187 element.layout_trace.geometry.width,
188 element.layout_trace.geometry.height,
189 element.layout_trace.geometry.region
190 ));
191 if !element.layout_trace.geometry.anchors.is_empty() {
192 decisions.push(format!(
193 "anchors={:?}",
194 element.layout_trace.geometry.anchors
195 ));
196 }
197 decisions.push(format!(
198 "collision enabled={} bounds={:?} policy={:?} reflowed={}",
199 element.layout_trace.collision_policy.enabled,
200 element.layout_trace.collision_policy.bounds,
201 element.layout_trace.collision_policy.resolution,
202 element.layout_trace.reflowed
203 ));
204 if element.layout_trace.initial_page != page {
205 decisions.push(format!(
206 "page/reflow moved placement from page {} to page {}",
207 element.layout_trace.initial_page + 1,
208 page + 1
209 ));
210 } else {
211 decisions.push(format!("page/reflow retained page {}", page + 1));
212 }
213 if let Some(table) = &element.table {
214 decisions.push(format!(
215 "table fragment {} contains {} rows",
216 table.index,
217 table.rows.len()
218 ));
219 }
220 Ok(LayoutExplanation {
221 id: element.id.clone(),
222 page,
223 bounds: element.bounds,
224 measured: element.bounds.intrinsic != element.bounds.layout,
225 patch_count: element.provenance.patches.len(),
226 components: element.provenance.components.clone(),
227 source: element.provenance.source.clone(),
228 decisions,
229 trace: element.layout_trace.clone(),
230 })
231 }
232
233 pub fn query_free_regions(&self, page: usize, minimum: crate::Size) -> Result<Vec<Rect>> {
235 self.query_free_regions_bounded(page, minimum, &ResourceLimits::default())
236 }
237
238 pub fn query_free_regions_bounded(
240 &self,
241 page: usize,
242 minimum: crate::Size,
243 limits: &ResourceLimits,
244 ) -> Result<Vec<Rect>> {
245 crate::resolved::validate_scene_contract(self.scene, limits)?;
246 let page_ref = self.page(page)?;
247 let mut budget = crate::diagnostic_budget::DiagnosticBudget::new(limits)?;
248 let mut free = vec![Rect::new(
249 Unit::ZERO,
250 Unit::ZERO,
251 page_ref.size.width,
252 page_ref.size.height,
253 )?];
254 for element in page_ref
255 .elements
256 .iter()
257 .filter(|element| element.collidable)
258 {
259 let mut next = Vec::new();
260 for region in free {
261 budget.operation()?;
262 let pieces = subtract(region, element.bounds.collision)?;
263 budget.retained(next.len().saturating_add(pieces.len()))?;
264 next.extend(pieces);
265 }
266 free = next;
267 }
268 for exclusion in &page_ref.exclusions {
269 let mut next = Vec::new();
270 for region in free {
271 budget.operation()?;
272 let pieces = subtract(region, exclusion.bounds)?;
273 budget.retained(next.len().saturating_add(pieces.len()))?;
274 next.extend(pieces);
275 }
276 free = next;
277 }
278 free.retain(|region| {
279 region.size.width >= minimum.width && region.size.height >= minimum.height
280 });
281 free.sort_by_key(|region| {
282 (
283 region.origin.y,
284 region.origin.x,
285 region.size.height,
286 region.size.width,
287 )
288 });
289 Ok(free)
290 }
291
292 fn find(&self, id: &ElementId) -> Result<(usize, &ResolvedElement)> {
293 self.scene
294 .pages
295 .iter()
296 .find_map(|page| {
297 page.elements
298 .iter()
299 .find(|element| &element.id == id)
300 .map(|element| (page.index, element))
301 })
302 .ok_or_else(|| inspect_error(format!("element `{}` was not found", id.as_str())))
303 }
304
305 fn page(&self, index: usize) -> Result<&ResolvedPage> {
306 self.scene
307 .pages
308 .get(index)
309 .ok_or_else(|| inspect_error(format!("page {index} was not found")))
310 }
311}
312
313pub(crate) fn subtract(region: Rect, occupied: Rect) -> Result<Vec<Rect>> {
314 let Some(overlap) = region.intersection(occupied)? else {
315 return Ok(vec![region]);
316 };
317 let mut result = Vec::with_capacity(4);
318 if overlap.origin.y > region.origin.y {
319 result.push(Rect::new(
320 region.origin.x,
321 region.origin.y,
322 region.size.width,
323 overlap.origin.y.checked_sub(region.origin.y)?,
324 )?);
325 }
326 if overlap.bottom()? < region.bottom()? {
327 result.push(Rect::new(
328 region.origin.x,
329 overlap.bottom()?,
330 region.size.width,
331 region.bottom()?.checked_sub(overlap.bottom()?)?,
332 )?);
333 }
334 if overlap.origin.x > region.origin.x {
335 result.push(Rect::new(
336 region.origin.x,
337 overlap.origin.y,
338 overlap.origin.x.checked_sub(region.origin.x)?,
339 overlap.size.height,
340 )?);
341 }
342 if overlap.right()? < region.right()? {
343 result.push(Rect::new(
344 overlap.right()?,
345 overlap.origin.y,
346 region.right()?.checked_sub(overlap.right()?)?,
347 overlap.size.height,
348 )?);
349 }
350 Ok(result)
351}
352
353fn contains_rect(outer: Rect, inner: Rect) -> Result<bool> {
354 Ok(inner.origin.x >= outer.origin.x
355 && inner.origin.y >= outer.origin.y
356 && inner.right()? <= outer.right()?
357 && inner.bottom()? <= outer.bottom()?)
358}
359
360fn inspect_error(message: impl Into<String>) -> FileMakerError {
361 FileMakerError::new(ErrorCode::LayoutInvalid, message)
362}