appcore_filemaker/
collision.rs1use std::collections::BTreeSet;
14
15use serde::{Deserialize, Serialize};
16
17use crate::{Rect, Result};
18
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum CollisionBounds {
23 #[default]
25 Layout,
26 Visual,
28 Intrinsic,
30}
31
32#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum CollisionResolution {
36 #[default]
38 Push,
39 Error,
41 Overlay,
43 NextPage,
45 Shrink,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
51pub struct CollisionPolicy {
52 pub enabled: bool,
54 pub group: String,
56 pub collides_with: BTreeSet<String>,
58 pub ignore: BTreeSet<String>,
60 pub priority: i32,
62 pub movable: bool,
64 pub bounds: CollisionBounds,
66 pub resolution: CollisionResolution,
68}
69
70impl Default for CollisionPolicy {
71 fn default() -> Self {
72 Self {
73 enabled: true,
74 group: "default".to_owned(),
75 collides_with: BTreeSet::new(),
76 ignore: BTreeSet::new(),
77 priority: 0,
78 movable: true,
79 bounds: CollisionBounds::Layout,
80 resolution: CollisionResolution::Push,
81 }
82 }
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct CollisionRule {
88 pub id: String,
90 pub bounds: Rect,
92 pub policy: CollisionPolicy,
94 pub sequence: usize,
96}
97
98impl CollisionRule {
99 #[must_use]
101 pub fn applies_to(&self, other: &Self) -> bool {
102 self.policy.enabled
103 && other.policy.enabled
104 && !self.policy.ignore.contains(&other.id)
105 && !other.policy.ignore.contains(&self.id)
106 && (self.policy.collides_with.is_empty()
107 || self.policy.collides_with.contains(&other.policy.group))
108 && (other.policy.collides_with.is_empty()
109 || other.policy.collides_with.contains(&self.policy.group))
110 }
111}
112
113pub trait SpatialIndex {
115 fn insert(&mut self, rule: CollisionRule) -> Result<()>;
117 fn query(&self, bounds: Rect) -> Result<Vec<&CollisionRule>>;
119 fn len(&self) -> usize;
121 fn is_empty(&self) -> bool {
123 self.len() == 0
124 }
125}
126
127#[derive(Clone, Debug, Default)]
129pub struct LinearSpatialIndex {
130 rules: Vec<CollisionRule>,
131}
132
133impl SpatialIndex for LinearSpatialIndex {
134 fn insert(&mut self, rule: CollisionRule) -> Result<()> {
135 self.rules.push(rule);
136 self.rules.sort_by_key(|entry| entry.sequence);
137 Ok(())
138 }
139
140 fn query(&self, bounds: Rect) -> Result<Vec<&CollisionRule>> {
141 self.rules
142 .iter()
143 .filter_map(|rule| match rule.bounds.intersects(bounds) {
144 Ok(true) => Some(Ok(rule)),
145 Ok(false) => None,
146 Err(error) => Some(Err(error)),
147 })
148 .collect()
149 }
150
151 fn len(&self) -> usize {
152 self.rules.len()
153 }
154}
155
156impl LinearSpatialIndex {
157 pub(crate) fn first_applicable(
158 &self,
159 candidate: &CollisionRule,
160 comparisons: &mut usize,
161 maximum: usize,
162 ) -> Result<Option<&CollisionRule>> {
163 for rule in &self.rules {
164 *comparisons = comparisons.checked_add(1).ok_or_else(|| {
165 crate::FileMakerError::new(
166 crate::ErrorCode::LimitExceeded,
167 "layout collision comparison count overflow",
168 )
169 })?;
170 if *comparisons > maximum {
171 return Err(crate::FileMakerError::new(
172 crate::ErrorCode::LimitExceeded,
173 "layout collision comparison budget exhausted",
174 ));
175 }
176 if candidate.applies_to(rule) && rule.bounds.intersects(candidate.bounds)? {
177 return Ok(Some(rule));
178 }
179 }
180 Ok(None)
181 }
182}