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}