Skip to main content

long_screen/engine/
attachments.rs

1//! Fragment attachments: a fragment canvas that later evidence tied rigidly onto another canvas. The solve pass
2//! records them (`attach/<id>` rows) and follows the chains to compare a keyframe's raw canvas and pose with the
3//! canvas it is observed at today; the render pass walks the same chains.
4
5use crate::engine::pose_graph::{PoseGraph, PoseGraphError};
6use std::collections::{HashMap, HashSet};
7
8/// One `attach/<id>` row: every placement on `id` maps onto `target`, shifted by `(dx, dy)`.
9#[derive(Clone, Debug, PartialEq)]
10pub struct Attachment {
11    pub id: String,
12    pub target: String,
13    pub dx: f64,
14    pub dy: f64,
15    pub frame: u32,
16    pub confidence: f64,
17    /// The matched target keyframe's pose-graph node.
18    pub node: String,
19}
20
21/// Fragment id → its attachment.
22pub type Attachments = HashMap<String, Attachment>;
23
24/// The canvas `id` finally resolves to through its attachment chain (a cycle stops at the first repeat).
25pub fn resolve_target<'a>(attachments: &'a Attachments, mut id: &'a str) -> &'a str {
26    let mut seen = HashSet::new();
27    while let Some(a) = attachments.get(id) {
28        if !seen.insert(id) {
29            break;
30        }
31        id = &a.target;
32    }
33    id
34}
35
36/// The total rigid shift from `id` through its attachment chain to the canvas it resolves to.
37pub fn attachment_shift<'a>(attachments: &'a Attachments, mut id: &'a str) -> (f64, f64) {
38    let (mut x, mut y) = (0.0, 0.0);
39    let mut seen = HashSet::new();
40    while let Some(a) = attachments.get(id) {
41        if !seen.insert(id) {
42            break;
43        }
44        x += a.dx;
45        y += a.dy;
46        id = &a.target;
47    }
48    (x, y)
49}
50
51/// The render-time shift from `id` through its chain: each link's measured offset plus how far its anchor node has
52/// since moved under graph relaxation (as of the attach frame), so an attached fragment follows a target that a
53/// later loop closure moved. Summed link by link as `shift += dx + correction`.
54pub fn render_shift<'a>(
55    attachments: &'a Attachments,
56    mut id: &'a str,
57    graph: &PoseGraph,
58) -> Result<(f64, f64), PoseGraphError> {
59    let (mut x, mut y) = (0.0, 0.0);
60    let mut seen = HashSet::new();
61    while let Some(a) = attachments.get(id) {
62        if !seen.insert(id) {
63            break;
64        }
65        let (cx, cy) = graph.correction(&a.node, a.frame as f64)?;
66        x += a.dx + cx;
67        y += a.dy + cy;
68        id = &a.target;
69    }
70    Ok((x, y))
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    fn attach(id: &str, target: &str, dx: f64, dy: f64) -> (String, Attachment) {
78        (
79            id.to_string(),
80            Attachment {
81                id: id.to_string(),
82                target: target.to_string(),
83                dx,
84                dy,
85                frame: 0,
86                confidence: 1.0,
87                node: String::new(),
88            },
89        )
90    }
91
92    #[test]
93    fn an_unattached_canvas_resolves_to_itself_unshifted() {
94        let map = Attachments::new();
95        assert_eq!(resolve_target(&map, "a"), "a");
96        assert_eq!(attachment_shift(&map, "a"), (0.0, 0.0));
97    }
98
99    #[test]
100    fn a_chain_resolves_to_its_end_and_sums_its_shifts() {
101        let map: Attachments = [attach("a", "b", 1.0, 2.0), attach("b", "c", 10.0, 20.0)]
102            .into_iter()
103            .collect();
104        assert_eq!(resolve_target(&map, "a"), "c");
105        assert_eq!(attachment_shift(&map, "a"), (11.0, 22.0));
106        assert_eq!(resolve_target(&map, "b"), "c");
107    }
108
109    #[test]
110    fn the_render_shift_adds_how_far_each_anchor_node_moved() {
111        use crate::engine::pose_graph::PoseNode;
112        let node = |id: &str, moved: f64| PoseNode {
113            id: id.into(),
114            canvas_id: "t".into(),
115            frame: 0,
116            x: moved,
117            y: 0.0,
118            original_x: 0.0,
119            original_y: 0.0,
120            pinned: false,
121            next: None,
122        };
123        let mut map: Attachments = [attach("a", "b", 1.0, 2.0), attach("b", "c", 10.0, 20.0)]
124            .into_iter()
125            .collect();
126        map.get_mut("a").unwrap().node = "na".into();
127        map.get_mut("b").unwrap().node = "nb".into();
128        let mut g = PoseGraph::new();
129        g.hydrate_node(node("na", 3.0));
130        g.hydrate_node(node("nb", 0.5));
131        assert_eq!(
132            render_shift(&map, "a", &g),
133            Ok((1.0 + 3.0 + (10.0 + 0.5), 22.0))
134        );
135        assert_eq!(render_shift(&map, "c", &g), Ok((0.0, 0.0)));
136        g = PoseGraph::new();
137        assert_eq!(
138            render_shift(&map, "a", &g),
139            Err(PoseGraphError::MissingPose)
140        );
141    }
142
143    #[test]
144    fn a_cycle_stops_at_the_first_repeat() {
145        let map: Attachments = [attach("a", "b", 1.0, 0.0), attach("b", "a", 2.0, 0.0)]
146            .into_iter()
147            .collect();
148        assert_eq!(resolve_target(&map, "a"), "a");
149        assert_eq!(attachment_shift(&map, "a"), (3.0, 0.0));
150    }
151}