1use serde::{Deserialize, Serialize};
14
15use crate::debug_geometry::{mask_free_regions, selected_bounds};
16use crate::{
17 Color, ElementId, ErrorCode, FileMakerError, Point, Rect, ResolvedElement, ResolvedPage,
18 ResolvedScene, ResourceLimits, Result, SceneInspector, Size, Unit,
19};
20
21#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum MaskView {
25 #[default]
27 CollisionMask,
28 LayoutBounds,
30 VisualBounds,
32 Combined,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
38pub struct DebugOverlayOptions {
39 pub grid: Option<Unit>,
41 pub ruler: bool,
43 pub ids: bool,
45 pub coordinates: bool,
47 pub bounds: bool,
49 pub anchors: bool,
51 pub regions: bool,
53 pub safe_area: bool,
55 pub collision: bool,
57 pub crosshair: bool,
59 pub view: MaskView,
61}
62
63#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
65#[serde(tag = "kind", rename_all = "snake_case")]
66pub enum DebugPrimitive {
67 Line {
69 from: Point,
71 to: Point,
73 color: Color,
75 },
76 Rect {
78 bounds: Rect,
80 color: Color,
82 },
83 Label {
85 origin: Point,
87 text: String,
89 color: Color,
91 },
92}
93
94#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
96pub struct DebugOverlay {
97 pub page: usize,
99 pub primitives: Vec<DebugPrimitive>,
101}
102
103impl DebugOverlay {
104 pub fn build(
106 scene: &ResolvedScene,
107 page: usize,
108 options: &DebugOverlayOptions,
109 ) -> Result<Self> {
110 Self::build_bounded(scene, page, options, &ResourceLimits::default())
111 }
112
113 pub fn build_bounded(
115 scene: &ResolvedScene,
116 page: usize,
117 options: &DebugOverlayOptions,
118 limits: &ResourceLimits,
119 ) -> Result<Self> {
120 crate::resolved::validate_scene_contract(scene, limits)?;
121 let page_ref = scene
122 .pages
123 .get(page)
124 .ok_or_else(|| debug_error("debug page was not found"))?;
125 if let Some(grid) = options.grid {
126 validate_grid(grid)?;
127 }
128 crate::debug_plan::validate_overlay(page_ref, options, limits)?;
129 let mut primitives = Vec::new();
130 if let Some(grid) = options.grid {
131 add_grid(&mut primitives, page_ref.size, grid)?;
132 }
133 add_page_geometry(&mut primitives, page_ref, options)?;
134 for element in &page_ref.elements {
135 add_element_geometry(&mut primitives, element, options)?;
136 }
137 if options.ruler {
138 add_ruler(
139 &mut primitives,
140 page_ref.size,
141 options.grid.unwrap_or(Unit::points(10)?),
142 )?;
143 }
144 Ok(Self { page, primitives })
145 }
146}
147
148fn add_page_geometry(
149 primitives: &mut Vec<DebugPrimitive>,
150 page: &ResolvedPage,
151 options: &DebugOverlayOptions,
152) -> Result<()> {
153 if options.safe_area {
154 if let Some(bounds) = page
155 .page_template
156 .as_ref()
157 .map(crate::PageTemplate::safe_bounds)
158 .transpose()?
159 {
160 add_named_rect(
161 primitives,
162 "safe",
163 bounds,
164 Color::Rgb { r: 0, g: 128, b: 0 },
165 );
166 }
167 }
168 if options.regions {
169 for region in &page.regions {
170 add_named_rect(
171 primitives,
172 &format!("region:{}", region.name),
173 region.bounds,
174 Color::Rgb {
175 r: 0,
176 g: 96,
177 b: 192,
178 },
179 );
180 }
181 }
182 if options.collision {
183 for element in page.elements.iter().filter(|element| element.collidable) {
184 add_named_rect(
185 primitives,
186 &format!("collision:{}", element.id.as_str()),
187 element.bounds.collision,
188 Color::Rgb {
189 r: 220,
190 g: 0,
191 b: 160,
192 },
193 );
194 }
195 for exclusion in &page.exclusions {
196 add_named_rect(
197 primitives,
198 &format!("exclusion:{}", exclusion.name),
199 exclusion.bounds,
200 Color::Rgb {
201 r: 160,
202 g: 0,
203 b: 220,
204 },
205 );
206 }
207 }
208 Ok(())
209}
210
211fn add_element_geometry(
212 primitives: &mut Vec<DebugPrimitive>,
213 element: &ResolvedElement,
214 options: &DebugOverlayOptions,
215) -> Result<()> {
216 if options.bounds {
217 for bounds in selected_bounds(element.bounds, options.view) {
218 primitives.push(DebugPrimitive::Rect {
219 bounds,
220 color: Color::Rgba {
221 r: 255,
222 g: 0,
223 b: 0,
224 a: 160,
225 },
226 });
227 }
228 }
229 if options.ids {
230 primitives.push(DebugPrimitive::Label {
231 origin: element.bounds.layout.origin,
232 text: element.id.as_str().to_owned(),
233 color: Color::Rgb { r: 180, g: 0, b: 0 },
234 });
235 }
236 if options.coordinates {
237 primitives.push(DebugPrimitive::Label {
238 origin: element.bounds.layout.origin,
239 text: format!(
240 "({:.3}, {:.3}) pt",
241 element.bounds.layout.origin.x.as_points_f64(),
242 element.bounds.layout.origin.y.as_points_f64()
243 ),
244 color: Color::Rgb { r: 0, g: 0, b: 0 },
245 });
246 }
247 if options.anchors {
248 for (edge, expression) in &element.layout_trace.geometry.anchors {
249 primitives.push(DebugPrimitive::Label {
250 origin: element.bounds.layout.origin,
251 text: format!("anchor:{edge}={expression}"),
252 color: Color::Rgb {
253 r: 128,
254 g: 64,
255 b: 0,
256 },
257 });
258 }
259 }
260 if options.crosshair {
261 add_crosshair(primitives, element.bounds.layout.origin)?;
262 }
263 Ok(())
264}
265
266fn add_grid(primitives: &mut Vec<DebugPrimitive>, size: Size, spacing: Unit) -> Result<()> {
267 let color = Color::Rgba {
268 r: 0,
269 g: 128,
270 b: 255,
271 a: 64,
272 };
273 let mut x = Unit::ZERO;
274 while x <= size.width {
275 primitives.push(DebugPrimitive::Line {
276 from: Point { x, y: Unit::ZERO },
277 to: Point { x, y: size.height },
278 color,
279 });
280 x = x.checked_add(spacing)?;
281 }
282 let mut y = Unit::ZERO;
283 while y <= size.height {
284 primitives.push(DebugPrimitive::Line {
285 from: Point { x: Unit::ZERO, y },
286 to: Point { x: size.width, y },
287 color,
288 });
289 y = y.checked_add(spacing)?;
290 }
291 Ok(())
292}
293
294fn validate_grid(grid: Unit) -> Result<()> {
295 for spacing in [1, 5, 10, 20] {
296 if grid == Unit::points(spacing)? {
297 return Ok(());
298 }
299 }
300 Err(debug_error("debug grid must be 1, 5, 10, or 20 points"))
301}
302
303fn add_ruler(primitives: &mut Vec<DebugPrimitive>, size: Size, spacing: Unit) -> Result<()> {
304 let mut x = Unit::ZERO;
305 while x <= size.width {
306 primitives.push(DebugPrimitive::Label {
307 origin: Point { x, y: Unit::ZERO },
308 text: format!("{:.3}", x.as_points_f64()),
309 color: Color::Rgb { r: 0, g: 0, b: 0 },
310 });
311 x = x.checked_add(spacing)?;
312 }
313 let mut y = Unit::ZERO;
314 while y <= size.height {
315 primitives.push(DebugPrimitive::Label {
316 origin: Point { x: Unit::ZERO, y },
317 text: format!("{:.3}", y.as_points_f64()),
318 color: Color::Rgb { r: 0, g: 0, b: 0 },
319 });
320 y = y.checked_add(spacing)?;
321 }
322 Ok(())
323}
324
325fn add_named_rect(primitives: &mut Vec<DebugPrimitive>, name: &str, bounds: Rect, color: Color) {
326 primitives.push(DebugPrimitive::Rect { bounds, color });
327 primitives.push(DebugPrimitive::Label {
328 origin: bounds.origin,
329 text: name.to_owned(),
330 color,
331 });
332}
333
334#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
336pub struct CollisionMask {
337 pub page: usize,
339 pub size: Size,
341 pub occupied: Vec<(ElementId, Rect)>,
343 pub free: Vec<Rect>,
345 pub collisions: Vec<(ElementId, ElementId, Rect)>,
347 pub overflow: Vec<ElementId>,
349}
350
351impl CollisionMask {
352 pub fn derive(scene: &ResolvedScene, page: usize, view: MaskView) -> Result<Self> {
354 Self::derive_bounded(scene, page, view, &ResourceLimits::default())
355 }
356
357 pub fn derive_bounded(
359 scene: &ResolvedScene,
360 page: usize,
361 view: MaskView,
362 limits: &ResourceLimits,
363 ) -> Result<Self> {
364 crate::resolved::validate_scene_contract(scene, limits)?;
365 let page_ref = scene
366 .pages
367 .get(page)
368 .ok_or_else(|| debug_error("mask page was not found"))?;
369 let mut budget = crate::diagnostic_budget::DiagnosticBudget::new(limits)?;
370 let mut occupied = Vec::new();
371 for element in page_ref
372 .elements
373 .iter()
374 .filter(|element| view != MaskView::CollisionMask || element.collidable)
375 {
376 let mut element_bounds = Vec::new();
377 for bounds in selected_bounds(element.bounds, view) {
378 if !element_bounds.contains(&bounds) {
379 budget.retained(occupied.len().saturating_add(1))?;
380 occupied.push((element.id.clone(), bounds));
381 element_bounds.push(bounds);
382 }
383 }
384 }
385 if matches!(view, MaskView::CollisionMask | MaskView::Combined) {
386 for exclusion in &page_ref.exclusions {
387 budget.retained(occupied.len().saturating_add(1))?;
388 occupied.push((
389 ElementId::new(format!("exclusion.{}", exclusion.name))?,
390 exclusion.bounds,
391 ));
392 }
393 }
394 let mut collisions = Vec::new();
395 for (index, (left_id, left)) in occupied.iter().enumerate() {
396 for (right_id, right) in &occupied[index + 1..] {
397 if left_id == right_id {
398 continue;
399 }
400 budget.operation()?;
401 if let Some(overlap) = left.intersection(*right)? {
402 budget.retained(collisions.len().saturating_add(1))?;
403 collisions.push((left_id.clone(), right_id.clone(), overlap));
404 }
405 }
406 }
407 let free = mask_free_regions(page_ref.size, &occupied, &mut budget)?;
408 let overflow = SceneInspector::new(scene).inspect_page(page)?.overflow;
409 Ok(Self {
410 page,
411 size: page_ref.size,
412 occupied,
413 free,
414 collisions,
415 overflow,
416 })
417 }
418
419 pub fn to_json(&self) -> Result<Vec<u8>> {
421 self.to_json_bounded(&ResourceLimits::default())
422 }
423
424 pub fn to_json_bounded(&self, limits: &ResourceLimits) -> Result<Vec<u8>> {
426 self.validate_limits(limits)?;
427 let size = crate::memory::serialized_size_pretty(self)?;
428 if size > limits.max_output_bytes {
429 return Err(FileMakerError::new(
430 ErrorCode::LimitExceeded,
431 "debug mask JSON exceeds the output budget",
432 ));
433 }
434 let mut bytes = Vec::with_capacity(size);
435 serde_json::to_writer_pretty(&mut bytes, self)
436 .map_err(|error| debug_error(format!("cannot encode mask JSON: {error}")))?;
437 debug_assert_eq!(bytes.len(), size);
438 Ok(bytes)
439 }
440
441 pub(crate) fn validate_limits(&self, limits: &ResourceLimits) -> Result<()> {
442 limits.validate()?;
443 let retained = self
444 .occupied
445 .len()
446 .checked_add(self.free.len())
447 .and_then(|count| count.checked_add(self.collisions.len()))
448 .and_then(|count| count.checked_add(self.overflow.len()))
449 .ok_or_else(|| {
450 FileMakerError::new(ErrorCode::LimitExceeded, "debug mask count overflow")
451 })?;
452 crate::diagnostic_budget::DiagnosticBudget::new(limits)?.retained(retained)
453 }
454}
455
456fn add_crosshair(primitives: &mut Vec<DebugPrimitive>, origin: Point) -> Result<()> {
457 let arm = Unit::points(3)?;
458 primitives.push(DebugPrimitive::Line {
459 from: Point {
460 x: origin.x.checked_sub(arm)?,
461 y: origin.y,
462 },
463 to: Point {
464 x: origin.x.checked_add(arm)?,
465 y: origin.y,
466 },
467 color: Color::Rgb {
468 r: 255,
469 g: 0,
470 b: 255,
471 },
472 });
473 primitives.push(DebugPrimitive::Line {
474 from: Point {
475 x: origin.x,
476 y: origin.y.checked_sub(arm)?,
477 },
478 to: Point {
479 x: origin.x,
480 y: origin.y.checked_add(arm)?,
481 },
482 color: Color::Rgb {
483 r: 255,
484 g: 0,
485 b: 255,
486 },
487 });
488 Ok(())
489}
490
491fn debug_error(message: impl Into<String>) -> FileMakerError {
492 FileMakerError::new(ErrorCode::LayoutInvalid, message)
493}