1use std::collections::BTreeMap;
4
5use sim_kernel::{Error, Expr, Result, Symbol};
6use sim_lib_scene::{AnchorSpace, Transform3};
7
8use crate::PanelPlacement;
9
10pub const XR_TRACKING_STATUS_NAMESPACE: &str = "stream/xr-tracking";
12
13pub const WORLD_ANCHOR_REASON_NAMESPACE: &str = "world-anchor";
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum VioTrackingStatus {
19 Stable6Dof,
21 Limited,
23 Lost,
25}
26
27impl VioTrackingStatus {
28 pub fn is_stable(self) -> bool {
30 matches!(self, Self::Stable6Dof)
31 }
32
33 pub fn to_symbol(self) -> Symbol {
35 match self {
36 Self::Stable6Dof => Symbol::qualified(XR_TRACKING_STATUS_NAMESPACE, "tracked"),
37 Self::Limited => Symbol::qualified(XR_TRACKING_STATUS_NAMESPACE, "limited"),
38 Self::Lost => Symbol::qualified(XR_TRACKING_STATUS_NAMESPACE, "lost"),
39 }
40 }
41
42 pub fn from_symbol(symbol: &Symbol) -> Result<Self> {
44 match (symbol.namespace.as_deref(), symbol.name.as_ref()) {
45 (Some(XR_TRACKING_STATUS_NAMESPACE), "tracked") => Ok(Self::Stable6Dof),
46 (Some(XR_TRACKING_STATUS_NAMESPACE), "limited") => Ok(Self::Limited),
47 (Some(XR_TRACKING_STATUS_NAMESPACE), "lost") => Ok(Self::Lost),
48 _ => Err(Error::HostError(format!(
49 "unknown Viture VIO tracking status {symbol}"
50 ))),
51 }
52 }
53
54 pub fn to_expr(self) -> Expr {
56 Expr::Symbol(self.to_symbol())
57 }
58
59 pub fn from_expr(expr: &Expr) -> Result<Self> {
61 let Expr::Symbol(symbol) = expr else {
62 return Err(Error::HostError(
63 "Viture VIO tracking status must be a symbol".to_owned(),
64 ));
65 };
66 Self::from_symbol(symbol)
67 }
68}
69
70#[derive(Clone, Debug, PartialEq)]
72pub struct WorldAnchorObservation {
73 pub anchor: Symbol,
75 pub transform: Transform3,
77}
78
79impl WorldAnchorObservation {
80 pub fn new(anchor: Symbol, transform: Transform3) -> Self {
82 Self { anchor, transform }
83 }
84}
85
86#[derive(Clone, Debug, PartialEq)]
88pub enum AnchorResolution {
89 World {
91 anchor: Symbol,
93 transform: Transform3,
95 },
96 HeadLocked {
98 anchor: Symbol,
100 transform: Transform3,
102 reason: Symbol,
104 },
105}
106
107impl AnchorResolution {
108 pub fn anchor(&self) -> &Symbol {
110 match self {
111 Self::World { anchor, .. } | Self::HeadLocked { anchor, .. } => anchor,
112 }
113 }
114
115 pub fn transform(&self) -> &Transform3 {
117 match self {
118 Self::World { transform, .. } | Self::HeadLocked { transform, .. } => transform,
119 }
120 }
121
122 pub fn anchor_space(&self) -> AnchorSpace {
124 match self {
125 Self::World { .. } => AnchorSpace::World,
126 Self::HeadLocked { .. } => AnchorSpace::Head,
127 }
128 }
129
130 pub fn reason(&self) -> Option<&Symbol> {
132 match self {
133 Self::World { .. } => None,
134 Self::HeadLocked { reason, .. } => Some(reason),
135 }
136 }
137}
138
139#[derive(Clone, Debug, Default, PartialEq)]
141pub struct WorldAnchorResolver {
142 anchors: BTreeMap<Symbol, Transform3>,
143}
144
145impl WorldAnchorResolver {
146 pub fn new(observed: impl IntoIterator<Item = WorldAnchorObservation>) -> Self {
148 let anchors = observed
149 .into_iter()
150 .map(|item| (item.anchor, item.transform))
151 .collect();
152 Self { anchors }
153 }
154
155 pub fn observe(&mut self, observation: WorldAnchorObservation) {
157 self.anchors
158 .insert(observation.anchor, observation.transform);
159 }
160
161 pub fn observed_transform(&self, anchor: &Symbol) -> Option<&Transform3> {
163 self.anchors.get(anchor)
164 }
165
166 pub fn resolve(
168 &self,
169 placement: &PanelPlacement,
170 status: VioTrackingStatus,
171 ) -> AnchorResolution {
172 let anchor = placement_anchor(placement);
173 if placement.space != AnchorSpace::World {
174 return head_locked(placement, anchor, reason("non-world-anchor"));
175 }
176 if !status.is_stable() {
177 return head_locked(placement, anchor, tracking_reason(status));
178 }
179 let Some(observed) = self.anchors.get(&anchor) else {
180 return head_locked(placement, anchor, reason("missing-world-anchor"));
181 };
182 AnchorResolution::World {
183 anchor,
184 transform: compose_transforms(observed, &placement.transform),
185 }
186 }
187}
188
189pub fn resolve_world_anchor(
191 placement: &PanelPlacement,
192 status: VioTrackingStatus,
193 resolver: &WorldAnchorResolver,
194) -> AnchorResolution {
195 resolver.resolve(placement, status)
196}
197
198fn placement_anchor(placement: &PanelPlacement) -> Symbol {
199 placement
200 .world_anchor
201 .clone()
202 .unwrap_or_else(|| placement.panel_id.clone())
203}
204
205fn head_locked(placement: &PanelPlacement, anchor: Symbol, reason: Symbol) -> AnchorResolution {
206 AnchorResolution::HeadLocked {
207 anchor,
208 transform: placement.transform.clone(),
209 reason,
210 }
211}
212
213fn tracking_reason(status: VioTrackingStatus) -> Symbol {
214 match status {
215 VioTrackingStatus::Stable6Dof => reason("stable"),
216 VioTrackingStatus::Limited => reason("unstable-vio"),
217 VioTrackingStatus::Lost => reason("lost-vio"),
218 }
219}
220
221fn reason(name: &'static str) -> Symbol {
222 Symbol::qualified(WORLD_ANCHOR_REASON_NAMESPACE, name)
223}
224
225fn compose_transforms(anchor: &Transform3, local: &Transform3) -> Transform3 {
226 let translated = rotate_vector(
227 normalize_quat(anchor.rotate_xyzw),
228 [
229 local.translate_m[0] * anchor.scale[0],
230 local.translate_m[1] * anchor.scale[1],
231 local.translate_m[2] * anchor.scale[2],
232 ],
233 );
234 Transform3::new(
235 [
236 anchor.translate_m[0] + translated[0],
237 anchor.translate_m[1] + translated[1],
238 anchor.translate_m[2] + translated[2],
239 ],
240 normalize_quat(quat_mul(anchor.rotate_xyzw, local.rotate_xyzw)),
241 [
242 anchor.scale[0] * local.scale[0],
243 anchor.scale[1] * local.scale[1],
244 anchor.scale[2] * local.scale[2],
245 ],
246 )
247}
248
249fn quat_mul(left: [f64; 4], right: [f64; 4]) -> [f64; 4] {
250 let [x1, y1, z1, w1] = normalize_quat(left);
251 let [x2, y2, z2, w2] = normalize_quat(right);
252 [
253 w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
254 w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
255 w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,
256 w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
257 ]
258}
259
260fn normalize_quat(quat: [f64; 4]) -> [f64; 4] {
261 let len =
262 (quat[0] * quat[0] + quat[1] * quat[1] + quat[2] * quat[2] + quat[3] * quat[3]).sqrt();
263 if len == 0.0 {
264 [0.0, 0.0, 0.0, 1.0]
265 } else {
266 [quat[0] / len, quat[1] / len, quat[2] / len, quat[3] / len]
267 }
268}
269
270fn rotate_vector(quat: [f64; 4], vector: [f64; 3]) -> [f64; 3] {
271 let qv = [quat[0], quat[1], quat[2]];
272 let uv = cross(qv, vector);
273 let uuv = cross(qv, uv);
274 [
275 vector[0] + 2.0 * (quat[3] * uv[0] + uuv[0]),
276 vector[1] + 2.0 * (quat[3] * uv[1] + uuv[1]),
277 vector[2] + 2.0 * (quat[3] * uv[2] + uuv[2]),
278 ]
279}
280
281fn cross(left: [f64; 3], right: [f64; 3]) -> [f64; 3] {
282 [
283 left[1] * right[2] - left[2] * right[1],
284 left[2] * right[0] - left[0] * right[2],
285 left[0] * right[1] - left[1] * right[0],
286 ]
287}