Skip to main content

sim_lib_view_spatial/
encode.rs

1//! Surface codec implementation for spatial-capable glasses.
2
3use std::borrow::Cow;
4use std::sync::Arc;
5
6use sim_kernel::{Cx, Error, Expr, Result, Symbol};
7use sim_lib_view::{
8    Draft, Operation, PairCodec, SurfaceCaps, SurfaceCodec, UniversalEditor, UniversalView, View,
9    codec::reduce_for_caps,
10};
11use sim_lib_view_device::{DeviceSurfaceCapsExt, GlassesClass, glasses_class};
12
13use crate::glance_map::halo_glance_scene;
14use crate::layout::{arrange_spatial_panels, layout_expr};
15use crate::rank::rank_for_profile;
16
17/// The id under which the spatial glasses surface codec is registered.
18pub const SPATIAL_SURFACE_CODEC_ID: &str = "surface:spatial";
19
20/// Symbol form of [`SPATIAL_SURFACE_CODEC_ID`].
21pub fn surface_spatial_codec_symbol() -> Symbol {
22    Symbol::new(SPATIAL_SURFACE_CODEC_ID)
23}
24
25/// Capability-aware codec for glasses surfaces.
26pub struct SpatialSurfaceCodec {
27    editor: PairCodec,
28}
29
30impl Default for SpatialSurfaceCodec {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl SpatialSurfaceCodec {
37    /// Builds a spatial surface codec backed by the universal editor.
38    pub fn new() -> Self {
39        Self {
40            editor: PairCodec::new(
41                Arc::new(UniversalView),
42                Arc::new(UniversalEditor::writable()),
43            ),
44        }
45    }
46
47    fn source_scene(&self, cx: &mut Cx, value: &Expr) -> Result<Expr> {
48        let view_value = strip_layout_metadata(value);
49        let scene = UniversalView.encode(cx, view_value.as_ref())?;
50        validate("universal view produced invalid Scene", &scene)?;
51        Ok(scene)
52    }
53}
54
55impl SurfaceCodec for SpatialSurfaceCodec {
56    fn encode(&self, cx: &mut Cx, value: &Expr, caps: &SurfaceCaps) -> Result<Expr> {
57        let scene = self.source_scene(cx, value)?;
58        let profile = caps.device_profile();
59        let encoded = match glasses_class(&profile) {
60            Some(GlassesClass::Stereo6Dof) => rank_for_profile(
61                &arrange_spatial_panels(scene, layout_expr(value))?,
62                &profile,
63            )?,
64            Some(GlassesClass::MonoHud) => halo_glance_scene(&scene, &profile)?,
65            Some(GlassesClass::DisplayOnly) | None => reduce_for_caps(&scene, caps),
66        };
67        validate("spatial surface produced invalid Scene", &encoded)?;
68        Ok(encoded)
69    }
70
71    fn decode(&self, cx: &mut Cx, value: &Expr, intent: &Expr) -> Result<Draft> {
72        self.editor.decode(cx, value, intent)
73    }
74
75    fn commit(&self, cx: &mut Cx, draft: &Draft) -> Result<Operation> {
76        self.editor.commit(cx, draft)
77    }
78}
79
80fn validate(context: &str, scene: &Expr) -> Result<()> {
81    sim_lib_scene::validate_scene(scene)
82        .map_err(|err| Error::HostError(format!("{context}: {err}")))
83}
84
85fn strip_layout_metadata(value: &Expr) -> Cow<'_, Expr> {
86    let Expr::Map(entries) = value else {
87        return Cow::Borrowed(value);
88    };
89    let filtered = entries
90        .iter()
91        .filter(|(key, _)| !is_layout_metadata_key(key))
92        .cloned()
93        .collect::<Vec<_>>();
94    if filtered.len() == entries.len() {
95        Cow::Borrowed(value)
96    } else {
97        Cow::Owned(Expr::Map(filtered))
98    }
99}
100
101fn is_layout_metadata_key(key: &Expr) -> bool {
102    let name = match key {
103        Expr::Symbol(symbol) if symbol.namespace.is_none() => symbol.name.as_ref(),
104        Expr::String(text) => text.as_str(),
105        _ => return false,
106    };
107    matches!(
108        name,
109        "workspace-layout" | "spatial-workspace-layout" | "spatial-layout" | "layout"
110    )
111}