1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// =============================================================================
// #######
// ### ### F: collision.rs
// ## ## ## ## P: AppCore-Runtime
// ## ##
// C: 2026/08/30 05:00:00 by dnettoRaw
// ## ## ## ## U: 2026/08/30 05:00:00 by dnettoRaw
// ########### S: 1.0.2-rc
// =============================================================================
//! Defines bounded collision contracts and behavior for this crate.
use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::{Rect, Result};
/// Which resolved box participates in collision.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CollisionBounds {
/// Layout box.
#[default]
Layout,
/// Visual box including stroke/effects.
Visual,
/// Intrinsic content box.
Intrinsic,
}
/// Resolution applied when geometry overlaps.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CollisionResolution {
/// Move the lower-priority movable node forward in flow.
#[default]
Push,
/// Reject layout.
Error,
/// Accept overlap explicitly.
Overlay,
/// Move the candidate to the next page.
NextPage,
/// Reduce the candidate within its minimum bounds.
Shrink,
}
/// Inheritable collision policy.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CollisionPolicy {
/// Whether geometry participates.
pub enabled: bool,
/// Collision group.
pub group: String,
/// Groups collided with; empty means every group.
pub collides_with: BTreeSet<String>,
/// IDs ignored explicitly.
pub ignore: BTreeSet<String>,
/// Higher value wins movement conflict.
pub priority: i32,
/// Whether the resolver may reposition the node.
pub movable: bool,
/// Selected resolved box.
pub bounds: CollisionBounds,
/// Overlap resolution.
pub resolution: CollisionResolution,
}
impl Default for CollisionPolicy {
fn default() -> Self {
Self {
enabled: true,
group: "default".to_owned(),
collides_with: BTreeSet::new(),
ignore: BTreeSet::new(),
priority: 0,
movable: true,
bounds: CollisionBounds::Layout,
resolution: CollisionResolution::Push,
}
}
}
/// Indexed collision rule and geometry.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CollisionRule {
/// Stable element ID.
pub id: String,
/// Page-local collision bounds.
pub bounds: Rect,
/// Effective policy.
pub policy: CollisionPolicy,
/// Stable insertion sequence.
pub sequence: usize,
}
impl CollisionRule {
/// Whether this rule can collide with another effective rule.
#[must_use]
pub fn applies_to(&self, other: &Self) -> bool {
self.policy.enabled
&& other.policy.enabled
&& !self.policy.ignore.contains(&other.id)
&& !other.policy.ignore.contains(&self.id)
&& (self.policy.collides_with.is_empty()
|| self.policy.collides_with.contains(&other.policy.group))
&& (other.policy.collides_with.is_empty()
|| other.policy.collides_with.contains(&self.policy.group))
}
}
/// Deterministic spatial query contract.
pub trait SpatialIndex {
/// Inserts one resolved rule.
fn insert(&mut self, rule: CollisionRule) -> Result<()>;
/// Returns overlapping rules in stable insertion order.
fn query(&self, bounds: Rect) -> Result<Vec<&CollisionRule>>;
/// Number of indexed rules.
fn len(&self) -> usize;
/// Whether no rules are indexed.
fn is_empty(&self) -> bool {
self.len() == 0
}
}
/// Simple deterministic linear index; suitable baseline and test oracle.
#[derive(Clone, Debug, Default)]
pub struct LinearSpatialIndex {
rules: Vec<CollisionRule>,
}
impl SpatialIndex for LinearSpatialIndex {
fn insert(&mut self, rule: CollisionRule) -> Result<()> {
self.rules.push(rule);
self.rules.sort_by_key(|entry| entry.sequence);
Ok(())
}
fn query(&self, bounds: Rect) -> Result<Vec<&CollisionRule>> {
self.rules
.iter()
.filter_map(|rule| match rule.bounds.intersects(bounds) {
Ok(true) => Some(Ok(rule)),
Ok(false) => None,
Err(error) => Some(Err(error)),
})
.collect()
}
fn len(&self) -> usize {
self.rules.len()
}
}
impl LinearSpatialIndex {
pub(crate) fn first_applicable(
&self,
candidate: &CollisionRule,
comparisons: &mut usize,
maximum: usize,
) -> Result<Option<&CollisionRule>> {
for rule in &self.rules {
*comparisons = comparisons.checked_add(1).ok_or_else(|| {
crate::FileMakerError::new(
crate::ErrorCode::LimitExceeded,
"layout collision comparison count overflow",
)
})?;
if *comparisons > maximum {
return Err(crate::FileMakerError::new(
crate::ErrorCode::LimitExceeded,
"layout collision comparison budget exhausted",
));
}
if candidate.applies_to(rule) && rule.bounds.intersects(candidate.bounds)? {
return Ok(Some(rule));
}
}
Ok(None)
}
}