1use std::collections::BTreeSet;
17
18use thiserror::Error;
19
20use super::graph::ClippingGraph;
21use super::segment::SegId;
22use super::shape::{Closure, ShapeId};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
30pub enum StructureError {
31 #[error("shape {shape:?} links to segment {segment:?}, which is not in the arena")]
33 DanglingSegment {
34 shape: ShapeId,
36 segment: SegId,
38 },
39
40 #[error("segment {segment:?} is in shape {shape:?}'s list but belongs to {owner:?}")]
42 WrongParent {
43 shape: ShapeId,
45 segment: SegId,
47 owner: ShapeId,
49 },
50
51 #[error("segment {segment:?} is in more than one shape's list")]
53 SegmentInTwoLists {
54 segment: SegId,
56 },
57
58 #[error("segments {from:?} and {to:?} do not agree about the link between them")]
60 AsymmetricLink {
61 from: SegId,
63 to: SegId,
65 },
66
67 #[error("walking shape {shape:?}'s list did not return to its head within {bound} segments")]
71 WalkOverran {
72 shape: ShapeId,
74 bound: usize,
76 },
77
78 #[error("shape {shape:?} records {recorded} segments but its list holds {walked}")]
80 CountMismatch {
81 shape: ShapeId,
83 recorded: usize,
85 walked: usize,
87 },
88
89 #[error("shape {shape:?}'s segment offsets are not in order")]
92 OffsetsOutOfOrder {
93 shape: ShapeId,
95 },
96
97 #[error("segment {segment:?}'s offset is not the offset of its start point")]
99 OffsetDisagreesWithPoint {
100 segment: SegId,
102 },
103
104 #[error("segment {segment:?} is not indexed under the point it starts at")]
106 MissingFromIndex {
107 segment: SegId,
109 },
110
111 #[error("the shared-start index refers to segment {segment:?}, which is not in the arena")]
113 DanglingIndexEntry {
114 segment: SegId,
116 },
117
118 #[error("segment {segment:?} is indexed under a point it does not start at")]
121 MisfiledIndexEntry {
122 segment: SegId,
124 },
125
126 #[error("the shared-start index lists segment {segment:?} twice under one point")]
128 DuplicateIndexEntry {
129 segment: SegId,
131 },
132
133 #[error("the shared-start index holds a point with no segments")]
135 EmptyIndexEntry,
136
137 #[error("segment {segment:?} is alive but no shape's list reaches it")]
139 UnreachableSegment {
140 segment: SegId,
142 },
143
144 #[error("the shared-start index holds {indexed} segments but {live} are alive")]
146 IndexSizeMismatch {
147 indexed: usize,
149 live: usize,
151 },
152
153 #[error("segment {segment:?} is covered by shape {shape:?}, which no longer exists")]
156 CoveredByMissingShape {
157 segment: SegId,
159 shape: ShapeId,
161 },
162
163 #[error("shape {shape:?} covers part of its own outline")]
165 ShapeCoversItself {
166 shape: ShapeId,
168 },
169}
170
171impl ClippingGraph {
172 pub fn validate(&self) -> Result<(), StructureError> {
188 let mut reachable: BTreeSet<SegId> = BTreeSet::new();
189 for shape in self.shapes.keys() {
190 self.validate_shape(*shape, &mut reachable)?;
191 }
192
193 for (id, _) in self.arena.iter() {
197 if !reachable.contains(&id) {
198 return Err(StructureError::UnreachableSegment { segment: id });
199 }
200 }
201
202 self.validate_index()
203 }
204
205 fn validate_shape(
208 &self,
209 shape_id: ShapeId,
210 reachable: &mut BTreeSet<SegId>,
211 ) -> Result<(), StructureError> {
212 let Some(shape) = self.shapes.get(&shape_id) else {
213 return Ok(());
214 };
215 let Some(head) = shape.head else {
216 if shape.count != 0 {
217 return Err(StructureError::CountMismatch {
218 shape: shape_id,
219 recorded: shape.count,
220 walked: 0,
221 });
222 }
223 return Ok(());
224 };
225
226 let mut current = head;
227 let mut walked = 0_usize;
228 let mut decreases = 0_usize;
229 let mut decrease_is_the_wrap = false;
230
231 loop {
232 let segment = self
233 .arena
234 .get(current)
235 .ok_or(StructureError::DanglingSegment {
236 shape: shape_id,
237 segment: current,
238 })?;
239
240 if segment.parent != shape_id {
241 return Err(StructureError::WrongParent {
242 shape: shape_id,
243 segment: current,
244 owner: segment.parent,
245 });
246 }
247 if !reachable.insert(current) {
248 return Err(StructureError::SegmentInTwoLists { segment: current });
249 }
250 self.validate_links(shape_id, current)?;
251
252 if segment.start.to_bits() != shape.kind.point_to_offset(segment.point).to_bits() {
257 return Err(StructureError::OffsetDisagreesWithPoint { segment: current });
258 }
259 if !self
260 .shared_starts
261 .segments(segment.point)
262 .contains(¤t)
263 {
264 return Err(StructureError::MissingFromIndex { segment: current });
265 }
266
267 for covering in &segment.overlapping {
268 if *covering == shape_id {
269 return Err(StructureError::ShapeCoversItself { shape: shape_id });
270 }
271 if !self.shapes.contains_key(covering) {
272 return Err(StructureError::CoveredByMissingShape {
273 segment: current,
274 shape: *covering,
275 });
276 }
277 }
278
279 walked += 1;
280 if walked > shape.count {
281 return Err(StructureError::WalkOverran {
282 shape: shape_id,
283 bound: shape.count,
284 });
285 }
286
287 let next_start = self
288 .arena
289 .get(segment.next)
290 .map_or(segment.start, |next| next.start);
291 let wraps = segment.next == head;
292 if segment.start > next_start {
293 decreases += 1;
294 decrease_is_the_wrap = wraps;
295 }
296
297 current = segment.next;
298 if wraps {
299 break;
300 }
301 }
302
303 if walked != shape.count {
304 return Err(StructureError::CountMismatch {
305 shape: shape_id,
306 recorded: shape.count,
307 walked,
308 });
309 }
310 if decreases > 1 {
315 return Err(StructureError::OffsetsOutOfOrder { shape: shape_id });
316 }
317 if matches!(shape.closure, Closure::Open) && decreases == 1 && !decrease_is_the_wrap {
318 return Err(StructureError::OffsetsOutOfOrder { shape: shape_id });
319 }
320
321 Ok(())
322 }
323
324 fn validate_links(&self, shape_id: ShapeId, current: SegId) -> Result<(), StructureError> {
326 let segment = self
327 .arena
328 .get(current)
329 .ok_or(StructureError::DanglingSegment {
330 shape: shape_id,
331 segment: current,
332 })?;
333
334 let next = self
335 .arena
336 .get(segment.next)
337 .ok_or(StructureError::DanglingSegment {
338 shape: shape_id,
339 segment: segment.next,
340 })?;
341 if next.prev != current {
342 return Err(StructureError::AsymmetricLink {
343 from: current,
344 to: segment.next,
345 });
346 }
347
348 let previous = self
349 .arena
350 .get(segment.prev)
351 .ok_or(StructureError::DanglingSegment {
352 shape: shape_id,
353 segment: segment.prev,
354 })?;
355 if previous.next != current {
356 return Err(StructureError::AsymmetricLink {
357 from: segment.prev,
358 to: current,
359 });
360 }
361
362 Ok(())
363 }
364
365 fn validate_index(&self) -> Result<(), StructureError> {
369 for (key, segments) in self.shared_starts.iter() {
370 let [_, ..] = segments else {
371 return Err(StructureError::EmptyIndexEntry);
372 };
373 let mut distinct = BTreeSet::new();
374 for id in segments {
375 if !distinct.insert(*id) {
376 return Err(StructureError::DuplicateIndexEntry { segment: *id });
377 }
378 let segment = self
379 .arena
380 .get(*id)
381 .ok_or(StructureError::DanglingIndexEntry { segment: *id })?;
382 if segment.point.key() != key {
383 return Err(StructureError::MisfiledIndexEntry { segment: *id });
384 }
385 }
386 }
387
388 let indexed = self.shared_starts.len();
389 if indexed != self.arena.len() {
390 return Err(StructureError::IndexSizeMismatch {
391 indexed,
392 live: self.arena.len(),
393 });
394 }
395
396 Ok(())
397 }
398
399 pub(super) fn debug_validate(&self) {
405 if cfg!(debug_assertions) && self.deferred == 0 {
406 if let Err(error) = self.validate() {
407 panic!("the clipping structure is corrupt: {error}");
408 }
409 }
410 }
411
412 pub(crate) fn defer_validation(&mut self) {
427 if cfg!(debug_assertions) {
428 self.deferred += 1;
429 }
430 }
431
432 pub(crate) fn resume_validation(&mut self) {
435 if cfg!(debug_assertions) {
436 self.deferred = self.deferred.saturating_sub(1);
437 }
438 }
439
440 #[cfg(test)]
445 pub(crate) const fn validation_is_deferred(&self) -> bool {
446 self.deferred > 0
447 }
448}
449
450#[cfg(test)]
451mod tests {
452 #![allow(clippy::unwrap_used, clippy::expect_used)]
453
454 use super::StructureError;
455 use crate::Point;
456 use crate::clipping::{ClippingGraph, SegId, ShapeId, ShapeKind};
457
458 const BOARD: f64 = 20.0;
459
460 fn p(x: f64, y: f64) -> Point {
461 Point::new(x, y)
462 }
463
464 fn populated() -> (ClippingGraph, ShapeId, ShapeId) {
467 let mut graph = ClippingGraph::new(BOARD);
468 let edge = graph.shape_ids().next().unwrap();
469 let center = p(2.0, 10.0);
470 let zone = graph.add_dead_zone(center);
471 let circle = ShapeKind::dead_zone(center).circle().unwrap();
472
473 let crossing = graph.intersect(edge, circle).unwrap();
474 let edge_covered = graph.insert_or_get_existing(edge, crossing.entry).unwrap();
475 let zone_uncovered = graph.insert_or_get_existing(zone, crossing.entry).unwrap();
476 let edge_uncovered = graph.insert_or_get_existing(edge, crossing.exit).unwrap();
477 let zone_covered = graph.insert_or_get_existing(zone, crossing.exit).unwrap();
478 graph.add_overlapping(edge_covered, edge_uncovered, zone);
479 graph.add_overlapping(zone_covered, zone_uncovered, edge);
480
481 assert!(graph.validate().is_ok());
482 (graph, edge, zone)
483 }
484
485 fn head_of(graph: &ClippingGraph, shape: ShapeId) -> SegId {
486 graph.shape(shape).unwrap().head().unwrap()
487 }
488
489 #[test]
490 fn a_healthy_structure_validates() {
491 let (graph, _, _) = populated();
492 assert_eq!(graph.validate(), Ok(()));
493 }
494
495 #[test]
496 fn an_asymmetric_link_is_caught() {
497 let (mut graph, edge, _) = populated();
498 let head = head_of(&graph, edge);
499 let second = graph.segment(head).unwrap().next();
500 let third = graph.segment(second).unwrap().next();
501
502 graph.arena.get_mut(head).unwrap().next = third;
504
505 assert!(matches!(
506 graph.validate(),
507 Err(StructureError::AsymmetricLink { .. })
508 ));
509 }
510
511 #[test]
512 fn a_self_loop_part_way_round_the_list_is_caught() {
513 let (mut graph, edge, _) = populated();
514 let head = head_of(&graph, edge);
515 let second = graph.segment(head).unwrap().next();
516
517 graph.arena.get_mut(second).unwrap().next = second;
521 graph.arena.get_mut(second).unwrap().prev = second;
522
523 assert!(matches!(
524 graph.validate(),
525 Err(StructureError::AsymmetricLink { .. })
526 ));
527 }
528
529 #[test]
530 fn a_count_larger_than_the_list_is_caught() {
531 let (mut graph, edge, _) = populated();
532 graph.shapes.get_mut(&edge).unwrap().count += 1;
533
534 assert!(matches!(
535 graph.validate(),
536 Err(StructureError::CountMismatch { .. })
537 ));
538 }
539
540 #[test]
541 fn a_count_smaller_than_the_list_is_caught() {
542 let (mut graph, edge, _) = populated();
543 graph.shapes.get_mut(&edge).unwrap().count -= 1;
544
545 assert!(matches!(
548 graph.validate(),
549 Err(StructureError::WalkOverran { .. })
550 ));
551 }
552
553 #[test]
554 fn offsets_out_of_order_are_caught() {
555 let (mut graph, edge, _) = populated();
556 let head = head_of(&graph, edge);
557 let second = graph.segment(head).unwrap().next();
558 let third = graph.segment(second).unwrap().next();
559 let fourth = graph.segment(third).unwrap().next();
560
561 graph.arena.get_mut(head).unwrap().next = third;
565 graph.arena.get_mut(third).unwrap().prev = head;
566 graph.arena.get_mut(third).unwrap().next = second;
567 graph.arena.get_mut(second).unwrap().prev = third;
568 graph.arena.get_mut(second).unwrap().next = fourth;
569 graph.arena.get_mut(fourth).unwrap().prev = second;
570
571 assert!(matches!(
572 graph.validate(),
573 Err(StructureError::OffsetsOutOfOrder { .. })
574 ));
575 }
576
577 #[test]
578 fn an_open_shape_whose_head_is_not_its_lowest_offset_is_caught() {
579 let (mut graph, edge, _) = populated();
580 let head = head_of(&graph, edge);
581 let second = graph.segment(head).unwrap().next();
582
583 graph.shapes.get_mut(&edge).unwrap().head = Some(second);
587
588 assert!(matches!(
589 graph.validate(),
590 Err(StructureError::OffsetsOutOfOrder { .. })
591 ));
592 }
593
594 #[test]
595 fn a_segment_missing_from_the_index_is_caught() {
596 let (mut graph, edge, _) = populated();
597 let head = head_of(&graph, edge);
598 let point = graph.segment(head).unwrap().point();
599
600 graph.shared_starts.remove(point, head);
601
602 assert!(matches!(
603 graph.validate(),
604 Err(StructureError::MissingFromIndex { .. })
605 ));
606 }
607
608 #[test]
609 fn an_index_entry_pointing_at_nothing_is_caught() {
610 let (mut graph, _, _) = populated();
611 graph.shared_starts.add(p(3.0, 3.0), SegId::new(4_000));
612
613 assert!(matches!(
614 graph.validate(),
615 Err(StructureError::DanglingIndexEntry { .. })
616 ));
617 }
618
619 #[test]
620 fn an_index_entry_filed_under_the_wrong_point_is_caught() {
621 let (mut graph, edge, _) = populated();
622 let head = head_of(&graph, edge);
623 graph.shared_starts.add(p(3.0, 3.0), head);
624
625 assert!(matches!(
626 graph.validate(),
627 Err(StructureError::MisfiledIndexEntry { .. })
628 ));
629 }
630
631 #[test]
632 fn a_segment_no_shape_can_reach_is_caught() {
633 let (mut graph, edge, _) = populated();
634 let head = head_of(&graph, edge);
635 let second = graph.segment(head).unwrap().next();
636 let third = graph.segment(second).unwrap().next();
637
638 graph.arena.get_mut(head).unwrap().next = third;
640 graph.arena.get_mut(third).unwrap().prev = head;
641 graph.shapes.get_mut(&edge).unwrap().count -= 1;
642
643 assert!(matches!(
644 graph.validate(),
645 Err(StructureError::UnreachableSegment { .. })
646 ));
647 }
648
649 #[test]
650 fn a_segment_in_the_wrong_shapes_list_is_caught() {
651 let (mut graph, edge, zone) = populated();
652 let head = head_of(&graph, edge);
653 graph.arena.get_mut(head).unwrap().parent = zone;
654
655 assert!(matches!(
656 graph.validate(),
657 Err(StructureError::WrongParent { .. })
658 ));
659 }
660
661 #[test]
662 fn an_offset_that_does_not_match_its_point_is_caught() {
663 let (mut graph, edge, _) = populated();
664 let head = head_of(&graph, edge);
665 graph.arena.get_mut(head).unwrap().start += 1.0;
666
667 assert!(matches!(
668 graph.validate(),
669 Err(StructureError::OffsetDisagreesWithPoint { .. })
670 ));
671 }
672
673 #[test]
674 fn a_cover_by_a_shape_that_has_gone_is_caught() {
675 let (mut graph, _, zone) = populated();
676 graph.shapes.remove(&zone);
678
679 assert!(matches!(
680 graph.validate(),
681 Err(StructureError::CoveredByMissingShape { .. })
682 ));
683 }
684
685 #[test]
686 fn a_shape_covering_itself_is_caught() {
687 let (mut graph, edge, _) = populated();
688 let head = head_of(&graph, edge);
689 graph.arena.get_mut(head).unwrap().overlapping.insert(edge);
690
691 assert!(matches!(
692 graph.validate(),
693 Err(StructureError::ShapeCoversItself { .. })
694 ));
695 }
696
697 #[test]
698 #[cfg(debug_assertions)]
699 #[should_panic(expected = "the clipping structure is corrupt")]
700 fn a_mutation_on_a_corrupt_structure_panics() {
701 let (mut graph, edge, _) = populated();
702 let head = head_of(&graph, edge);
703 graph.shapes.get_mut(&edge).unwrap().count += 1;
704
705 graph.delete_segment(head);
707 }
708
709 #[test]
710 #[cfg(debug_assertions)]
711 fn a_deferred_mutation_does_not_check_and_the_damage_is_still_there() {
712 let (mut graph, edge, _) = populated();
713 let head = head_of(&graph, edge);
714 graph.shapes.get_mut(&edge).unwrap().count += 1;
715
716 graph.defer_validation();
719 graph.delete_segment(head);
720 graph.resume_validation();
721
722 assert!(graph.validate().is_err());
725 }
726
727 #[test]
728 #[cfg(debug_assertions)]
729 fn deferrals_nest_so_an_inner_resume_cannot_turn_the_check_back_on() {
730 let (mut graph, edge, _) = populated();
731 let head = head_of(&graph, edge);
732 graph.shapes.get_mut(&edge).unwrap().count += 1;
733
734 graph.defer_validation();
735 graph.defer_validation();
736 graph.resume_validation();
737
738 graph.delete_segment(head);
741
742 graph.resume_validation();
743 assert_eq!(graph.deferred, 0, "the deferrals balanced out");
744 }
745}