Skip to main content

scena/scene/
annotations.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::diagnostics::LookupError;
6
7use super::{CameraKey, NodeKey, Scene, Transform, Vec3};
8
9pub const SCENE_ANNOTATION_PROJECTION_SCHEMA_V1: &str = "scena.annotation_projection.v1";
10
11#[derive(Debug, Clone, PartialEq)]
12pub struct AnnotationAnchor {
13    id: String,
14    target: AnnotationAnchorTarget,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum AnnotationAnchorTarget {
19    World { position: Vec3 },
20    Node { node: NodeKey, local_offset: Vec3 },
21}
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct AnnotationProjectionReportV1 {
25    pub schema: String,
26    pub coordinate_space: String,
27    pub viewport_width: u32,
28    pub viewport_height: u32,
29    pub annotations: Vec<AnnotationProjectionV1>,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct AnnotationProjectionV1 {
34    pub id: String,
35    #[serde(default)]
36    pub node_handle: Option<u64>,
37    pub x: f32,
38    pub y: f32,
39    pub visible: bool,
40}
41
42impl AnnotationAnchor {
43    pub fn world(id: impl Into<String>, position: Vec3) -> Self {
44        Self {
45            id: id.into(),
46            target: AnnotationAnchorTarget::World { position },
47        }
48    }
49
50    pub fn node(id: impl Into<String>, node: NodeKey, local_offset: Vec3) -> Self {
51        Self {
52            id: id.into(),
53            target: AnnotationAnchorTarget::Node { node, local_offset },
54        }
55    }
56
57    pub fn id(&self) -> &str {
58        &self.id
59    }
60
61    pub const fn target(&self) -> AnnotationAnchorTarget {
62        self.target
63    }
64
65    pub(crate) const fn target_node(&self) -> Option<NodeKey> {
66        match self.target {
67            AnnotationAnchorTarget::Node { node, .. } => Some(node),
68            AnnotationAnchorTarget::World { .. } => None,
69        }
70    }
71}
72
73impl AnnotationProjectionReportV1 {
74    pub fn to_schema_json(&self) -> serde_json::Value {
75        serde_json::to_value(self).expect("annotation projection report is JSON-serializable")
76    }
77}
78
79impl Scene {
80    pub fn set_annotation_anchor(&mut self, anchor: AnnotationAnchor) -> Result<(), LookupError> {
81        if let Some(node) = anchor.target_node()
82            && !self.nodes.contains_key(node)
83        {
84            return Err(LookupError::NodeNotFound(node));
85        }
86
87        let changed = self.annotations.get(anchor.id()) != Some(&anchor);
88        self.annotations.insert(anchor.id.clone(), anchor);
89        if changed {
90            self.structure_revision = self.structure_revision.saturating_add(1);
91        }
92        Ok(())
93    }
94
95    pub fn clear_annotation_anchor(&mut self, id: &str) -> bool {
96        let removed = self.annotations.remove(id).is_some();
97        if removed {
98            self.structure_revision = self.structure_revision.saturating_add(1);
99        }
100        removed
101    }
102
103    pub fn annotation_anchor(&self, id: &str) -> Option<&AnnotationAnchor> {
104        self.annotations.get(id)
105    }
106
107    pub fn annotation_anchors(&self) -> impl Iterator<Item = &AnnotationAnchor> {
108        self.annotations.values()
109    }
110
111    pub fn annotation_projection_report(
112        &self,
113        camera: CameraKey,
114        viewport_width: u32,
115        viewport_height: u32,
116    ) -> Result<AnnotationProjectionReportV1, LookupError> {
117        if viewport_width == 0 || viewport_height == 0 {
118            return Err(LookupError::InvalidViewport {
119                width: viewport_width,
120                height: viewport_height,
121            });
122        }
123        if !self.cameras.contains_key(camera) {
124            return Err(LookupError::CameraNotFound(camera));
125        }
126
127        let annotations = self
128            .annotations
129            .values()
130            .map(|anchor| self.project_annotation(anchor, camera, viewport_width, viewport_height))
131            .collect::<Result<Vec<_>, _>>()?;
132
133        Ok(AnnotationProjectionReportV1 {
134            schema: SCENE_ANNOTATION_PROJECTION_SCHEMA_V1.to_owned(),
135            coordinate_space: "viewport_pixels".to_owned(),
136            viewport_width,
137            viewport_height,
138            annotations,
139        })
140    }
141
142    pub fn annotation_projection_report_with_node_handles(
143        &self,
144        camera: CameraKey,
145        viewport_width: u32,
146        viewport_height: u32,
147        node_handles: &BTreeMap<NodeKey, u64>,
148    ) -> Result<AnnotationProjectionReportV1, LookupError> {
149        let mut report =
150            self.annotation_projection_report(camera, viewport_width, viewport_height)?;
151        for projection in &mut report.annotations {
152            projection.node_handle = self
153                .annotation_anchor(&projection.id)
154                .and_then(|anchor| anchor.target_node())
155                .and_then(|node| node_handles.get(&node).copied());
156        }
157        Ok(report)
158    }
159
160    fn project_annotation(
161        &self,
162        anchor: &AnnotationAnchor,
163        camera: CameraKey,
164        viewport_width: u32,
165        viewport_height: u32,
166    ) -> Result<AnnotationProjectionV1, LookupError> {
167        let world_position = match anchor.target {
168            AnnotationAnchorTarget::World { position } => position,
169            AnnotationAnchorTarget::Node { node, local_offset } => {
170                let world_transform = self
171                    .world_transform(node)
172                    .ok_or(LookupError::NodeNotFound(node))?;
173                Transform::compose(world_transform, Transform::at(local_offset)).translation
174            }
175        };
176
177        let projected =
178            self.project_world_point(camera, world_position, viewport_width, viewport_height)?;
179        let (x, y, visible) = match projected {
180            Some(point) => (
181                point.x,
182                point.y,
183                point.ndc_x.abs() <= 1.0 && point.ndc_y.abs() <= 1.0,
184            ),
185            None => (0.0, 0.0, false),
186        };
187
188        Ok(AnnotationProjectionV1 {
189            id: anchor.id.clone(),
190            node_handle: None,
191            x,
192            y,
193            visible,
194        })
195    }
196}