1#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
5
6use std::collections::BTreeSet;
22
23use super::model::Document;
24use super::{AutoRoute, Block, LineAnchor};
25use crate::store::{PinId, RectId};
26
27#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
36pub struct PinRef {
37 pub block: RectId,
38 pub pin: PinId,
39}
40
41impl PinRef {
42 fn resolve(owner: RectId, anchor: LineAnchor) -> Self {
45 match anchor {
46 LineAnchor::Port(pin) => PinRef { block: owner, pin },
47 LineAnchor::Pin { block, pin } => PinRef { block, pin },
48 }
49 }
50}
51
52impl std::fmt::Display for PinRef {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{}:{}", self.block, self.pin)
55 }
56}
57
58#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
60pub enum Change {
61 Document,
63 Block(RectId),
67 Pin(PinRef),
69 Route { from: PinRef, to: PinRef },
74}
75
76impl std::fmt::Display for Change {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 Change::Document => write!(f, "document"),
80 Change::Block(id) => write!(f, "{id}"),
81 Change::Pin(pin) => write!(f, "{pin}"),
82 Change::Route { from, to } => write!(f, "{from}->{to}"),
83 }
84 }
85}
86
87#[derive(Clone, PartialEq, Eq, Debug, Default)]
90pub struct ChangeSet(BTreeSet<Change>);
91
92impl ChangeSet {
93 pub fn is_empty(&self) -> bool {
94 self.0.is_empty()
95 }
96
97 pub fn names(&self) -> Vec<String> {
99 self.0.iter().map(Change::to_string).collect()
100 }
101
102 fn insert(&mut self, change: Change) {
103 self.0.insert(change);
104 }
105}
106
107pub fn changed(prev: &Document, next: &Document) -> ChangeSet {
112 let mut set = ChangeSet::default();
113 if document_itself_differs(prev, next) {
114 set.insert(Change::Document);
115 }
116 for id in prev.blocks.keys().chain(next.blocks.keys()).copied() {
117 match (prev.blocks.get(&id), next.blocks.get(&id)) {
118 (None, Some(_)) | (Some(_), None) => set.insert(Change::Block(id)),
121 (Some(before), Some(after)) => compare_block(id, before, after, &mut set),
122 (None, None) => unreachable!("the id came from one of the two maps"),
123 }
124 }
125 set
126}
127
128fn document_itself_differs(prev: &Document, next: &Document) -> bool {
134 let Document {
135 name,
136 top_id,
137 blocks: _,
138 } = prev;
139 name != &next.name || top_id != &next.top_id
140}
141
142fn compare_block(id: RectId, before: &Block, after: &Block, set: &mut ChangeSet) {
143 if block_itself_differs(before, after) {
144 set.insert(Change::Block(id));
145 }
146 for pin in before.pins.keys().chain(after.pins.keys()).copied() {
147 if before.pins.get(&pin) != after.pins.get(&pin) {
148 set.insert(Change::Pin(PinRef { block: id, pin }));
149 }
150 }
151 for route in before.routes.keys().chain(after.routes.keys()) {
152 let (was, now) = (before.routes.get(route), after.routes.get(route));
153 if was == now {
154 continue;
155 }
156 for r in [was, now].into_iter().flatten() {
159 set.insert(route_change(id, r));
160 }
161 }
162}
163
164fn route_change(owner: RectId, route: &AutoRoute) -> Change {
165 Change::Route {
166 from: PinRef::resolve(owner, route.start()),
167 to: PinRef::resolve(owner, route.finish()),
168 }
169}
170
171fn block_itself_differs(before: &Block, after: &Block) -> bool {
177 let Block {
178 inner,
179 decorations,
180 pins: _,
181 children,
182 routes: _,
183 texts,
184 comments,
185 images,
186 icon,
187 role,
188 locked,
189 } = before;
190 inner != &after.inner
191 || decorations != &after.decorations
192 || children != &after.children
193 || texts != &after.texts
194 || comments != &after.comments
195 || images != &after.images
196 || icon != &after.icon
197 || role != &after.role
198 || locked != &after.locked
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::document::schema_convert::{load, to_kdl};
205 use crate::presentation::store::IdMapExt as _;
206 use crate::store::{RectId, RouteId};
207
208 const SRC: &str = r#"
211top "b0"
212block "b0" x=0 y=0 w=40 h=30 {
213 pin "p1" "in" loc="w1"
214 pin "p2" "out" loc="e1"
215 route "p1" "b1:p1"
216 route "p2" "b2:p1"
217 children "b1" "b2"
218}
219block "b1" x=5 y=5 w=8 h=8 { pin "p1" "a" loc="e1" }
220block "b2" x=5 y=20 w=8 h=8 { pin "p1" "b" loc="e1" }
221"#;
222
223 fn doc() -> Document {
224 load(SRC, "t.kdl").expect("the fixture parses")
225 }
226
227 fn b(n: usize) -> RectId {
228 RectId::nth_default(n)
229 }
230
231 fn names(prev: &Document, next: &Document) -> Vec<String> {
232 changed(prev, next).names()
233 }
234
235 #[test]
236 fn an_unchanged_document_reports_nothing() {
237 let d = doc();
238 assert!(changed(&d, &d.clone()).is_empty());
239 }
240
241 #[test]
242 fn moving_a_block_reports_only_that_block() {
243 let before = doc();
244 let mut after = before.clone();
245 let block = after.blocks.get_mut(&b(1)).expect("b1");
246 block.inner.min.x += 3;
247 assert_eq!(names(&before, &after), vec!["b1"]);
248 }
249
250 #[test]
254 fn renaming_a_pin_reports_the_pin_not_the_block() {
255 let before = doc();
256 let mut after = before.clone();
257 let pin = *after.blocks[&b(1)].pins.keys().next().expect("a pin");
258 after
259 .blocks
260 .get_mut(&b(1))
261 .expect("b1")
262 .pins
263 .get_mut(&pin)
264 .expect("the pin")
265 .name = "renamed".to_string();
266 assert_eq!(names(&before, &after), vec![format!("b1:{pin}")]);
267 }
268
269 #[test]
272 fn adding_and_removing_a_block_reports_the_block_alone() {
273 let before = doc();
274 let mut after = before.clone();
275 after.blocks.shift_remove(&b(2));
278 after
279 .blocks
280 .get_mut(&b(0))
281 .expect("b0")
282 .children
283 .shift_remove(&b(2));
284 assert_eq!(names(&before, &after), vec!["b0", "b2"]);
285
286 let mut grown = before.clone();
287 let added = grown.blocks.insert_value(Block::default());
288 assert_eq!(names(&before, &grown), vec![added.to_string()]);
289 }
290
291 #[test]
294 fn an_annotation_reports_the_block_holding_it() {
295 use crate::document::TextBox;
296 let before = doc();
297 let mut after = before.clone();
298 after
299 .blocks
300 .get_mut(&b(1))
301 .expect("b1")
302 .texts
303 .insert_value(TextBox {
304 text: "a note".to_string(),
305 anchor: crate::document::GridPos { x: 1, y: 1 },
306 role: None,
307 });
308 assert_eq!(names(&before, &after), vec!["b1"]);
309 }
310
311 #[test]
312 fn renaming_the_document_reports_the_document() {
313 let before = doc();
314 let mut after = before.clone();
315 after.name = Some("CT Scanner".to_string());
316 assert_eq!(names(&before, &after), vec!["document"]);
317 }
318
319 #[test]
322 fn a_route_is_named_by_its_endpoints() {
323 let before = doc();
324 let mut after = before.clone();
325 let route = *after.blocks[&b(0)].routes.keys().next().expect("a route");
326 after
327 .blocks
328 .get_mut(&b(0))
329 .expect("b0")
330 .routes
331 .get_mut(&route)
332 .expect("the route")
333 .set_role(Some(3));
334
335 let reported = names(&before, &after);
336 assert_eq!(reported, vec!["b0:p1->b1:p1"]);
337 assert!(
338 !reported.iter().any(|n| n.contains(&route.to_string())),
339 "the route's session-local id leaked into {reported:?}"
340 );
341 }
342
343 #[test]
346 fn a_repointed_route_reports_both_names() {
347 let before = doc();
348 let mut after = before.clone();
349 let route = *after.blocks[&b(0)].routes.keys().next().expect("a route");
350 let elsewhere = LineAnchor::Pin {
351 block: b(2),
352 pin: crate::store::PinId::nth_default(1),
353 };
354 after
355 .blocks
356 .get_mut(&b(0))
357 .expect("b0")
358 .routes
359 .get_mut(&route)
360 .expect("the route")
361 .finish = elsewhere;
362
363 assert_eq!(
364 names(&before, &after),
365 vec!["b0:p1->b1:p1", "b0:p1->b2:p1"],
366 "both the old and the new endpoint should be findable"
367 );
368 }
369
370 #[test]
374 fn a_route_name_survives_a_reload_that_shifts_its_id() {
375 let full = doc();
376 let mut trimmed = full.clone();
377 let first = *trimmed.blocks[&b(0)].routes.keys().next().expect("a route");
378 trimmed
379 .blocks
380 .get_mut(&b(0))
381 .expect("b0")
382 .routes
383 .shift_remove(&first);
384
385 let reloaded = load(&to_kdl(&trimmed), "t.kdl").expect("the round trip parses");
386
387 let ids =
388 |d: &Document| -> Vec<RouteId> { d.blocks[&b(0)].routes.keys().copied().collect() };
389 assert_ne!(
391 ids(&trimmed),
392 ids(&reloaded),
393 "the surviving route kept its id, so nothing is being tested"
394 );
395
396 let recolor = |d: &Document| {
397 let mut next = d.clone();
398 let r = *next.blocks[&b(0)].routes.keys().next().expect("a route");
399 next.blocks
400 .get_mut(&b(0))
401 .expect("b0")
402 .routes
403 .get_mut(&r)
404 .expect("the route")
405 .set_role(Some(5));
406 next
407 };
408
409 assert_eq!(
410 names(&trimmed, &recolor(&trimmed)),
411 names(&reloaded, &recolor(&reloaded)),
412 "the same route reported under different names either side of a reload"
413 );
414 }
415
416 #[test]
420 fn a_pin_is_spelled_the_same_way_in_a_route_name_as_on_its_own() {
421 let before = doc();
422
423 let mut renamed = before.clone();
425 let pin = *renamed.blocks[&b(0)].pins.keys().next().expect("a pin");
426 renamed
427 .blocks
428 .get_mut(&b(0))
429 .expect("b0")
430 .pins
431 .get_mut(&pin)
432 .expect("the pin")
433 .name = "renamed".to_string();
434 let pin_name = names(&before, &renamed);
435
436 let mut recolored = before.clone();
439 let route = *recolored.blocks[&b(0)]
440 .routes
441 .keys()
442 .next()
443 .expect("a route");
444 assert_eq!(
445 recolored.blocks[&b(0)].routes[&route].start(),
446 LineAnchor::Port(pin),
447 "the fixture's first route should start at that port"
448 );
449 recolored
450 .blocks
451 .get_mut(&b(0))
452 .expect("b0")
453 .routes
454 .get_mut(&route)
455 .expect("the route")
456 .set_role(Some(3));
457 let route_name = names(&before, &recolored);
458
459 let token = &pin_name[0];
460 assert_eq!(token, &format!("b0:{pin}"));
461 assert!(
462 route_name[0].starts_with(&format!("{token}->")),
463 "searching for {token:?} would miss the route: {route_name:?}"
464 );
465 }
466}