1pub use a7n_proto::v1::Edge;
6use a7n_proto::{MachineId, ProtoError, limits, v1};
7
8#[cfg(test)]
9mod tests;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct Placement {
13 pub id: MachineId,
14 pub x: i32,
15 pub y: i32,
16 pub width: u32,
17 pub height: u32,
18 pub blocked: u16,
19}
20
21const SEGMENTS_PER_EDGE: u16 = 3;
22const BLOCKED_MASK: u32 = (1 << (4 * SEGMENTS_PER_EDGE)) - 1;
23
24#[must_use]
26pub fn third_at(along: f32) -> u8 {
27 let along = along.clamp(0.0, 1.0);
28 if along < 1.0 / f32::from(SEGMENTS_PER_EDGE) {
29 0
30 } else if along < 2.0 / f32::from(SEGMENTS_PER_EDGE) {
31 1
32 } else {
33 2
34 }
35}
36
37impl Placement {
38 #[must_use]
39 pub fn is_blocked(&self, edge: Edge, third: u8) -> bool {
40 self.blocked & Self::segment_mask(edge, third) != 0
41 }
42
43 pub fn toggle_blocked(&mut self, edge: Edge, third: u8) {
44 self.blocked ^= Self::segment_mask(edge, third);
45 }
46
47 fn segment_mask(edge: Edge, third: u8) -> u16 {
48 if u16::from(third) >= SEGMENTS_PER_EDGE {
49 return 0;
50 }
51 let index = match edge {
52 Edge::Left => 0,
53 Edge::Top => 1,
54 Edge::Right => 2,
55 Edge::Bottom => 3,
56 Edge::Unspecified => return 0,
57 };
58 1 << (index * SEGMENTS_PER_EDGE + u16::from(third))
59 }
60
61 fn blocks(&self, edge: Edge, along: f32) -> bool {
62 self.is_blocked(edge, third_at(along))
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum Corner {
68 TopLeft,
69 TopRight,
70 BottomLeft,
71 BottomRight,
72}
73
74#[derive(Debug, thiserror::Error)]
75pub enum LayoutError {
76 #[error(transparent)]
77 InvalidId(#[from] ProtoError),
78 #[error("layout exceeds the machine limit")]
79 TooManyMachines,
80 #[error("machine rectangle has zero size")]
81 ZeroSize,
82 #[error("layout contains a duplicate machine id")]
83 DuplicateId,
84 #[error("machine rectangle exceeds the canvas coordinate range")]
85 CoordinateOverflow,
86 #[error("machine rectangles overlap")]
87 Overlap,
88 #[error("layout version is exhausted")]
89 VersionExhausted,
90}
91
92pub fn validate(placements: &[Placement]) -> Result<(), LayoutError> {
97 if placements.len() > limits::LAYOUT_MACHINES_MAX {
98 return Err(LayoutError::TooManyMachines);
99 }
100 for (index, p) in placements.iter().enumerate() {
101 if p.width == 0 || p.height == 0 {
102 return Err(LayoutError::ZeroSize);
103 }
104 if right(p) > i64::from(i32::MAX) || bottom(p) > i64::from(i32::MAX) {
105 return Err(LayoutError::CoordinateOverflow);
106 }
107 for other in &placements[..index] {
108 if p.id == other.id {
109 return Err(LayoutError::DuplicateId);
110 }
111 if i64::from(p.x) < right(other)
112 && i64::from(other.x) < right(p)
113 && i64::from(p.y) < bottom(other)
114 && i64::from(other.y) < bottom(p)
115 {
116 return Err(LayoutError::Overlap);
117 }
118 }
119 }
120 Ok(())
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct Layout {
125 version: u64,
126 by: MachineId,
127 placements: Vec<Placement>,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum Merge {
132 Unchanged,
133 Replaced { lost_own_edit: bool },
134}
135
136impl Layout {
137 #[must_use]
138 pub fn empty(own_id: MachineId) -> Self {
139 Self {
140 version: 0,
141 by: own_id,
142 placements: Vec::new(),
143 }
144 }
145
146 #[must_use]
147 pub fn version(&self) -> u64 {
148 self.version
149 }
150
151 #[must_use]
152 pub fn by(&self) -> MachineId {
153 self.by
154 }
155
156 #[must_use]
157 pub fn placements(&self) -> &[Placement] {
158 &self.placements
159 }
160
161 #[must_use]
164 pub fn nth(&self, n: u8, eligible: impl Fn(MachineId) -> bool) -> Option<MachineId> {
165 let index = usize::from(n.checked_sub(1)?);
166 let mut placements: Vec<_> = self.placements.iter().collect();
167 placements.sort_unstable_by_key(|p| (p.y, p.x));
168 placements
169 .get(index)
170 .filter(|p| eligible(p.id))
171 .map(|p| p.id)
172 }
173
174 pub fn edit(&self, placements: Vec<Placement>, own_id: MachineId) -> Result<Self, LayoutError> {
177 validate(&placements)?;
178 let version = self
179 .version
180 .checked_add(1)
181 .ok_or(LayoutError::VersionExhausted)?;
182 Ok(Self {
183 version,
184 by: own_id,
185 placements,
186 })
187 }
188
189 pub fn merge(&mut self, incoming: Self, own_id: MachineId) -> Merge {
190 if (incoming.version, incoming.by) <= (self.version, self.by) {
191 return Merge::Unchanged;
192 }
193 let lost_own_edit = self.by == own_id && self.version == incoming.version;
194 *self = incoming;
195 Merge::Replaced { lost_own_edit }
196 }
197
198 #[must_use]
199 pub fn to_proto(&self) -> v1::Layout {
200 v1::Layout {
201 version: self.version,
202 by: self.by.as_bytes().to_vec(),
203 machines: self
204 .placements
205 .iter()
206 .map(|p| v1::MachineRect {
207 id: p.id.as_bytes().to_vec(),
208 x: p.x,
209 y: p.y,
210 width: p.width,
211 height: p.height,
212 blocked: u32::from(p.blocked),
213 })
214 .collect(),
215 }
216 }
217
218 #[must_use]
220 #[allow(clippy::cast_possible_truncation)] pub fn exit(
222 &self,
223 from: MachineId,
224 edge: Edge,
225 along: f32,
226 eligible: impl Fn(MachineId) -> bool,
227 ) -> Option<(MachineId, v1::Landing)> {
228 if along.is_nan() {
229 return None;
230 }
231 let source = self.placements.iter().find(|p| p.id == from)?;
232 if source.blocks(edge, along) {
233 return None;
234 }
235 let horizontal = matches!(edge, Edge::Top | Edge::Bottom);
236 let (start, size) = if horizontal {
237 (source.x, source.width)
238 } else {
239 (source.y, source.height)
240 };
241 let point = f64::from(start) + f64::from(size) * f64::from(along.clamp(0.0, 1.0));
242 for target in &self.placements {
243 if target.id == from || !eligible(target.id) || !touches(source, target, edge) {
244 continue;
245 }
246 let (start, size) = if horizontal {
247 (target.x, target.width)
248 } else {
249 (target.y, target.height)
250 };
251 let offset = (point - f64::from(start)) / f64::from(size);
252 if (0.0..1.0).contains(&offset) {
253 return Some((
254 target.id,
255 v1::Landing {
256 edge: opposite(edge) as i32,
257 offset: offset as f32,
258 ..v1::Landing::default()
259 },
260 ));
261 }
262 }
263 None
264 }
265
266 #[must_use]
269 pub fn exit_corner(
270 &self,
271 from: MachineId,
272 edge: Edge,
273 corner: Corner,
274 eligible: impl Fn(MachineId) -> bool,
275 ) -> Option<(MachineId, v1::Landing)> {
276 let source = self.placements.iter().find(|p| p.id == from)?;
277 let at_right = matches!(corner, Corner::TopRight | Corner::BottomRight);
278 let at_bottom = matches!(corner, Corner::BottomLeft | Corner::BottomRight);
279 let at_end = match edge {
280 Edge::Left if !at_right => at_bottom,
281 Edge::Right if at_right => at_bottom,
282 Edge::Top if !at_bottom => at_right,
283 Edge::Bottom if at_bottom => at_right,
284 _ => return None,
285 };
286 if source.blocks(edge, if at_end { 1.0 } else { 0.0 }) {
287 return None;
288 }
289 let target = self.placements.iter().find(|target| {
290 target.id != from
291 && eligible(target.id)
292 && if at_right {
293 right(source) == i64::from(target.x)
294 } else {
295 i64::from(source.x) == right(target)
296 }
297 && if at_bottom {
298 bottom(source) == i64::from(target.y)
299 } else {
300 i64::from(source.y) == bottom(target)
301 }
302 })?;
303 Some((
304 target.id,
305 v1::Landing {
306 edge: opposite(edge) as i32,
307 offset: if at_end { 0.0 } else { 1.0 },
308 ..v1::Landing::default()
309 },
310 ))
311 }
312}
313
314impl TryFrom<&v1::Layout> for Layout {
315 type Error = LayoutError;
316
317 fn try_from(value: &v1::Layout) -> Result<Self, Self::Error> {
318 if value.machines.len() > limits::LAYOUT_MACHINES_MAX {
319 return Err(LayoutError::TooManyMachines);
320 }
321 if value.version > limits::COUNTER_MAX {
322 return Err(ProtoError::CounterTooLarge.into());
323 }
324 let by = MachineId::try_from(value.by.as_slice())?;
325 let placements = value
326 .machines
327 .iter()
328 .map(|p| {
329 Ok(Placement {
330 id: MachineId::try_from(p.id.as_slice())?,
331 x: p.x,
332 y: p.y,
333 width: p.width,
334 height: p.height,
335 blocked: u16::try_from(p.blocked & BLOCKED_MASK)
336 .expect("masked segments fit u16"),
337 })
338 })
339 .collect::<Result<Vec<_>, LayoutError>>()?;
340 validate(&placements)?;
341 Ok(Self {
342 version: value.version,
343 by,
344 placements,
345 })
346 }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub struct SharedEdge {
351 pub from: MachineId,
352 pub to: MachineId,
353 pub edge: Edge,
354}
355
356#[must_use]
357pub fn shared_edges(placements: &[Placement]) -> Vec<SharedEdge> {
358 let mut edges = Vec::new();
359 for from in placements {
360 for to in placements {
361 if from.id == to.id {
362 continue;
363 }
364 for edge in [Edge::Left, Edge::Right, Edge::Top, Edge::Bottom] {
365 let overlap = if matches!(edge, Edge::Left | Edge::Right) {
366 i64::from(from.y).max(i64::from(to.y)) < bottom(from).min(bottom(to))
367 } else {
368 i64::from(from.x).max(i64::from(to.x)) < right(from).min(right(to))
369 };
370 if overlap && touches(from, to, edge) {
371 edges.push(SharedEdge {
372 from: from.id,
373 to: to.id,
374 edge,
375 });
376 }
377 }
378 }
379 }
380 edges
381}
382
383fn right(p: &Placement) -> i64 {
384 i64::from(p.x) + i64::from(p.width)
385}
386fn bottom(p: &Placement) -> i64 {
387 i64::from(p.y) + i64::from(p.height)
388}
389
390fn touches(from: &Placement, to: &Placement, edge: Edge) -> bool {
391 match edge {
392 Edge::Left => i64::from(from.x) == right(to),
393 Edge::Right => right(from) == i64::from(to.x),
394 Edge::Top => i64::from(from.y) == bottom(to),
395 Edge::Bottom => bottom(from) == i64::from(to.y),
396 Edge::Unspecified => false,
397 }
398}
399
400fn opposite(edge: Edge) -> Edge {
401 match edge {
402 Edge::Left => Edge::Right,
403 Edge::Right => Edge::Left,
404 Edge::Top => Edge::Bottom,
405 Edge::Bottom => Edge::Top,
406 Edge::Unspecified => Edge::Unspecified,
407 }
408}