use std::collections::BTreeMap;
use sim_kernel::{Error, Expr, Result, Symbol};
use sim_lib_scene::{AnchorSpace, Transform3};
use crate::PanelPlacement;
pub const XR_TRACKING_STATUS_NAMESPACE: &str = "stream/xr-tracking";
pub const WORLD_ANCHOR_REASON_NAMESPACE: &str = "world-anchor";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VioTrackingStatus {
Stable6Dof,
Limited,
Lost,
}
impl VioTrackingStatus {
pub fn is_stable(self) -> bool {
matches!(self, Self::Stable6Dof)
}
pub fn to_symbol(self) -> Symbol {
match self {
Self::Stable6Dof => Symbol::qualified(XR_TRACKING_STATUS_NAMESPACE, "tracked"),
Self::Limited => Symbol::qualified(XR_TRACKING_STATUS_NAMESPACE, "limited"),
Self::Lost => Symbol::qualified(XR_TRACKING_STATUS_NAMESPACE, "lost"),
}
}
pub fn from_symbol(symbol: &Symbol) -> Result<Self> {
match (symbol.namespace.as_deref(), symbol.name.as_ref()) {
(Some(XR_TRACKING_STATUS_NAMESPACE), "tracked") => Ok(Self::Stable6Dof),
(Some(XR_TRACKING_STATUS_NAMESPACE), "limited") => Ok(Self::Limited),
(Some(XR_TRACKING_STATUS_NAMESPACE), "lost") => Ok(Self::Lost),
_ => Err(Error::HostError(format!(
"unknown Viture VIO tracking status {symbol}"
))),
}
}
pub fn to_expr(self) -> Expr {
Expr::Symbol(self.to_symbol())
}
pub fn from_expr(expr: &Expr) -> Result<Self> {
let Expr::Symbol(symbol) = expr else {
return Err(Error::HostError(
"Viture VIO tracking status must be a symbol".to_owned(),
));
};
Self::from_symbol(symbol)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct WorldAnchorObservation {
pub anchor: Symbol,
pub transform: Transform3,
}
impl WorldAnchorObservation {
pub fn new(anchor: Symbol, transform: Transform3) -> Self {
Self { anchor, transform }
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum AnchorResolution {
World {
anchor: Symbol,
transform: Transform3,
},
HeadLocked {
anchor: Symbol,
transform: Transform3,
reason: Symbol,
},
}
impl AnchorResolution {
pub fn anchor(&self) -> &Symbol {
match self {
Self::World { anchor, .. } | Self::HeadLocked { anchor, .. } => anchor,
}
}
pub fn transform(&self) -> &Transform3 {
match self {
Self::World { transform, .. } | Self::HeadLocked { transform, .. } => transform,
}
}
pub fn anchor_space(&self) -> AnchorSpace {
match self {
Self::World { .. } => AnchorSpace::World,
Self::HeadLocked { .. } => AnchorSpace::Head,
}
}
pub fn reason(&self) -> Option<&Symbol> {
match self {
Self::World { .. } => None,
Self::HeadLocked { reason, .. } => Some(reason),
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct WorldAnchorResolver {
anchors: BTreeMap<Symbol, Transform3>,
}
impl WorldAnchorResolver {
pub fn new(observed: impl IntoIterator<Item = WorldAnchorObservation>) -> Self {
let anchors = observed
.into_iter()
.map(|item| (item.anchor, item.transform))
.collect();
Self { anchors }
}
pub fn observe(&mut self, observation: WorldAnchorObservation) {
self.anchors
.insert(observation.anchor, observation.transform);
}
pub fn observed_transform(&self, anchor: &Symbol) -> Option<&Transform3> {
self.anchors.get(anchor)
}
pub fn resolve(
&self,
placement: &PanelPlacement,
status: VioTrackingStatus,
) -> AnchorResolution {
let anchor = placement_anchor(placement);
if placement.space != AnchorSpace::World {
return head_locked(placement, anchor, reason("non-world-anchor"));
}
if !status.is_stable() {
return head_locked(placement, anchor, tracking_reason(status));
}
let Some(observed) = self.anchors.get(&anchor) else {
return head_locked(placement, anchor, reason("missing-world-anchor"));
};
AnchorResolution::World {
anchor,
transform: compose_transforms(observed, &placement.transform),
}
}
}
pub fn resolve_world_anchor(
placement: &PanelPlacement,
status: VioTrackingStatus,
resolver: &WorldAnchorResolver,
) -> AnchorResolution {
resolver.resolve(placement, status)
}
fn placement_anchor(placement: &PanelPlacement) -> Symbol {
placement
.world_anchor
.clone()
.unwrap_or_else(|| placement.panel_id.clone())
}
fn head_locked(placement: &PanelPlacement, anchor: Symbol, reason: Symbol) -> AnchorResolution {
AnchorResolution::HeadLocked {
anchor,
transform: placement.transform.clone(),
reason,
}
}
fn tracking_reason(status: VioTrackingStatus) -> Symbol {
match status {
VioTrackingStatus::Stable6Dof => reason("stable"),
VioTrackingStatus::Limited => reason("unstable-vio"),
VioTrackingStatus::Lost => reason("lost-vio"),
}
}
fn reason(name: &'static str) -> Symbol {
Symbol::qualified(WORLD_ANCHOR_REASON_NAMESPACE, name)
}
fn compose_transforms(anchor: &Transform3, local: &Transform3) -> Transform3 {
let translated = rotate_vector(
normalize_quat(anchor.rotate_xyzw),
[
local.translate_m[0] * anchor.scale[0],
local.translate_m[1] * anchor.scale[1],
local.translate_m[2] * anchor.scale[2],
],
);
Transform3::new(
[
anchor.translate_m[0] + translated[0],
anchor.translate_m[1] + translated[1],
anchor.translate_m[2] + translated[2],
],
normalize_quat(quat_mul(anchor.rotate_xyzw, local.rotate_xyzw)),
[
anchor.scale[0] * local.scale[0],
anchor.scale[1] * local.scale[1],
anchor.scale[2] * local.scale[2],
],
)
}
fn quat_mul(left: [f64; 4], right: [f64; 4]) -> [f64; 4] {
let [x1, y1, z1, w1] = normalize_quat(left);
let [x2, y2, z2, w2] = normalize_quat(right);
[
w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2,
w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
]
}
fn normalize_quat(quat: [f64; 4]) -> [f64; 4] {
let len =
(quat[0] * quat[0] + quat[1] * quat[1] + quat[2] * quat[2] + quat[3] * quat[3]).sqrt();
if len == 0.0 {
[0.0, 0.0, 0.0, 1.0]
} else {
[quat[0] / len, quat[1] / len, quat[2] / len, quat[3] / len]
}
}
fn rotate_vector(quat: [f64; 4], vector: [f64; 3]) -> [f64; 3] {
let qv = [quat[0], quat[1], quat[2]];
let uv = cross(qv, vector);
let uuv = cross(qv, uv);
[
vector[0] + 2.0 * (quat[3] * uv[0] + uuv[0]),
vector[1] + 2.0 * (quat[3] * uv[1] + uuv[1]),
vector[2] + 2.0 * (quat[3] * uv[2] + uuv[2]),
]
}
fn cross(left: [f64; 3], right: [f64; 3]) -> [f64; 3] {
[
left[1] * right[2] - left[2] * right[1],
left[2] * right[0] - left[0] * right[2],
left[0] * right[1] - left[1] * right[0],
]
}