Skip to main content

sim_lib_view_bridge/
glasses_review.rs

1//! Glasses projections for pending BRIDGE/FORGE warrant gates.
2//!
3//! A packet with a warrant is a human gate. This module projects that gate into
4//! the glasses-specific Scene shapes that the existing device paths already
5//! understand: a pinned spatial panel for Viture and a DEVICE_3 glance card for
6//! Halo. The decision itself remains an ordinary `intent/approve` or
7//! `intent/reject` value.
8
9use sim_codec_bridge::{BridgePacket, BridgeWarrant, content_id_string};
10use sim_kernel::{Error, Expr, Result, Symbol};
11use sim_lib_scene::{Anchor, AnchorSpace, Transform3};
12use sim_lib_view::SurfaceCaps;
13use sim_lib_view_device::{DeviceProfile, DeviceSurfaceCapsExt, GlanceReducer, GlassesClass};
14use sim_value::{access, build};
15
16/// Namespace used by the standard BRIDGE packet-review mission.
17pub const BRIDGE_WARRANT_REVIEW_MISSION_NAMESPACE: &str = "bridge";
18
19/// Name used by the standard BRIDGE packet-review mission.
20pub const BRIDGE_WARRANT_REVIEW_MISSION_NAME: &str = "packet-review";
21
22/// Stable id for the Viture spatial warrant-review panel.
23pub const VITURE_WARRANT_REVIEW_PANEL_ID: &str = "bridge-warrant-review";
24
25const HALO_GLYPH: &str = "OK/X";
26const REVIEW_TITLE: &str = "BRIDGE/FORGE warrant";
27
28/// Glasses-local review inputs while a BRIDGE/FORGE warrant is focused.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum BridgeGlassesReviewInput {
31    /// Viture gaze dwell followed by a stable nod approves the warrant.
32    VitureGazeDwellNod,
33    /// Viture head shake rejects the warrant.
34    VitureShake,
35    /// Halo double tap approves the warrant.
36    HaloDoubleTap,
37    /// Halo long press rejects the warrant.
38    HaloLongPress,
39}
40
41/// Standard warrant decision emitted by glasses review input.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum WarrantReviewDecision {
44    /// Approve the pending warrant gate.
45    Approve,
46    /// Reject the pending warrant gate.
47    Reject,
48}
49
50impl WarrantReviewDecision {
51    fn intent_kind(self) -> &'static str {
52        match self {
53            Self::Approve => "approve",
54            Self::Reject => "reject",
55        }
56    }
57}
58
59/// Returns the mission symbol used by BRIDGE/FORGE warrant decisions.
60pub fn warrant_review_mission() -> Symbol {
61    Symbol::qualified(
62        BRIDGE_WARRANT_REVIEW_MISSION_NAMESPACE,
63        BRIDGE_WARRANT_REVIEW_MISSION_NAME,
64    )
65}
66
67/// Renders `packet` as the pinned Viture center-front review panel.
68pub fn viture_warrant_review_panel(packet: &BridgePacket) -> Result<Expr> {
69    let context = WarrantReviewContext::new(packet)?;
70    let body = viture_review_card(packet, &context);
71    let panel = sim_lib_scene::panel(
72        VITURE_WARRANT_REVIEW_PANEL_ID,
73        body,
74        Anchor::new(AnchorSpace::Head, "center-front"),
75        Transform3::new([0.0, 0.0, -1.2], [0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0]),
76    );
77    let panel = mark_warrant_scene(panel, "warrant");
78    validate("invalid Viture warrant review panel", &panel)?;
79    Ok(panel)
80}
81
82/// Renders `packet` as the Viture spatial scene containing the review panel.
83pub fn viture_warrant_review_scene(packet: &BridgePacket) -> Result<Expr> {
84    let scene = sim_lib_scene::spatial(vec![viture_warrant_review_panel(packet)?]);
85    validate("invalid Viture warrant review scene", &scene)?;
86    Ok(scene)
87}
88
89/// Renders `packet` as a Halo `scene/glance` pager through the DEVICE_3 reducer.
90pub fn halo_warrant_glance_pager(packet: &BridgePacket, profile: &DeviceProfile) -> Result<Expr> {
91    WarrantReviewContext::new(packet)?;
92    let source = halo_warrant_source_scene(packet);
93    validate("invalid Halo warrant source scene", &source)?;
94    let glance = GlanceReducer.reduce(&source, profile)?;
95    validate("invalid Halo warrant glance pager", &glance)?;
96    Ok(glance)
97}
98
99/// Builds the standard warrant decision Intent for `packet`.
100pub fn warrant_review_intent(
101    packet: &BridgePacket,
102    decision: WarrantReviewDecision,
103    origin: sim_lib_intent::Origin,
104) -> Result<Expr> {
105    let context = WarrantReviewContext::new(packet)?;
106    let intent = sim_lib_intent::intent(
107        decision.intent_kind(),
108        origin,
109        vec![
110            ("mission", Expr::Symbol(warrant_review_mission())),
111            ("packet-cid", build::text(context.packet_cid.to_owned())),
112            ("warrant", warrant_expr(context.warrant)),
113        ],
114    );
115    sim_lib_intent::validate_intent(&intent)
116        .map_err(|error| Error::HostError(format!("invalid warrant review Intent: {error}")))?;
117    Ok(intent)
118}
119
120/// Converts a glasses-local review input into a standard warrant decision Intent.
121pub fn warrant_review_intent_from_glasses_input(
122    packet: &BridgePacket,
123    input: BridgeGlassesReviewInput,
124    origin: sim_lib_intent::Origin,
125) -> Result<Expr> {
126    warrant_review_intent(packet, decision_for_input(input), origin)
127}
128
129pub(crate) fn glasses_warrant_scene_for_caps(
130    packet: &BridgePacket,
131    caps: &SurfaceCaps,
132) -> Result<Option<Expr>> {
133    if packet.warrant.is_none() {
134        return Ok(None);
135    }
136    let profile = caps.device_profile();
137    match sim_lib_view_device::glasses_class(&profile) {
138        Some(GlassesClass::Stereo6Dof) => viture_warrant_review_scene(packet).map(Some),
139        Some(GlassesClass::MonoHud) => halo_warrant_glance_pager(packet, &profile).map(Some),
140        Some(GlassesClass::DisplayOnly) | None => Ok(None),
141    }
142}
143
144fn decision_for_input(input: BridgeGlassesReviewInput) -> WarrantReviewDecision {
145    match input {
146        BridgeGlassesReviewInput::VitureGazeDwellNod | BridgeGlassesReviewInput::HaloDoubleTap => {
147            WarrantReviewDecision::Approve
148        }
149        BridgeGlassesReviewInput::VitureShake | BridgeGlassesReviewInput::HaloLongPress => {
150            WarrantReviewDecision::Reject
151        }
152    }
153}
154
155struct WarrantReviewContext<'a> {
156    packet_cid: &'a str,
157    warrant: &'a BridgeWarrant,
158}
159
160impl<'a> WarrantReviewContext<'a> {
161    fn new(packet: &'a BridgePacket) -> Result<Self> {
162        Ok(Self {
163            packet_cid: packet.header.cid.as_deref().ok_or_else(|| {
164                Error::Eval("BRIDGE/FORGE warrant review requires a stamped packet".to_owned())
165            })?,
166            warrant: packet.warrant.as_ref().ok_or_else(|| {
167                Error::Eval("BRIDGE/FORGE warrant review requires a packet warrant".to_owned())
168            })?,
169        })
170    }
171}
172
173fn viture_review_card(packet: &BridgePacket, context: &WarrantReviewContext<'_>) -> Expr {
174    sim_lib_scene::node(
175        "box",
176        vec![
177            ("role", build::sym("bridge-warrant-review")),
178            ("title", build::text(REVIEW_TITLE)),
179            ("status", build::sym("warrant")),
180            ("warrant", Expr::Bool(true)),
181            ("bypass-budget", Expr::Bool(true)),
182            ("packet-cid", build::text(context.packet_cid.to_owned())),
183            ("mission", Expr::Symbol(warrant_review_mission())),
184            (
185                "children",
186                build::list(vec![
187                    sim_lib_scene::badge("warrant", "FORGE gate"),
188                    sim_lib_scene::text_node(format!(
189                        "move {} from {}",
190                        packet.header.move_kind.as_qualified_str(),
191                        packet.header.from
192                    )),
193                    sim_lib_scene::text_node(format!(
194                        "parts {} warrant parts {}",
195                        packet.body.len(),
196                        context.warrant.parts.len()
197                    )),
198                    sim_lib_scene::text_node(format!("packet {}", short_cid(context.packet_cid))),
199                    decision_button("Approve", WarrantReviewDecision::Approve),
200                    decision_button("Reject", WarrantReviewDecision::Reject),
201                ]),
202            ),
203        ],
204    )
205}
206
207fn halo_warrant_source_scene(packet: &BridgePacket) -> Expr {
208    let mut source = sim_lib_scene::node(
209        "stack",
210        vec![
211            ("dir", build::sym("column")),
212            ("title", build::text(REVIEW_TITLE)),
213            ("status", build::sym("critical")),
214            ("warrant", Expr::Bool(true)),
215            ("bypass-budget", Expr::Bool(true)),
216            (
217                "children",
218                build::list(vec![sim_lib_scene::node(
219                    "button",
220                    vec![
221                        ("label", build::text(HALO_GLYPH)),
222                        ("target", Expr::Symbol(warrant_review_mission())),
223                        ("control", build::sym("warrant-decision")),
224                        (
225                            "packet-cid",
226                            build::text(packet.header.cid.clone().unwrap_or_default()),
227                        ),
228                    ],
229                )]),
230            ),
231        ],
232    );
233    source = access::set(&source, "pager", build::sym("halo-glance"));
234    source
235}
236
237fn decision_button(label: &str, decision: WarrantReviewDecision) -> Expr {
238    sim_lib_scene::node(
239        "button",
240        vec![
241            ("label", build::text(label)),
242            ("target", Expr::Symbol(warrant_review_mission())),
243            ("control", build::sym(decision.intent_kind())),
244            (
245                "intent-kind",
246                Expr::Symbol(Symbol::qualified("intent", decision.intent_kind())),
247            ),
248        ],
249    )
250}
251
252fn mark_warrant_scene(scene: Expr, status: &str) -> Expr {
253    let scene = access::set(&scene, "status", build::sym(status));
254    let scene = access::set(&scene, "warrant", Expr::Bool(true));
255    let scene = access::set(&scene, "pinned", Expr::Bool(true));
256    access::set(&scene, "bypass-budget", Expr::Bool(true))
257}
258
259fn warrant_expr(warrant: &BridgeWarrant) -> Expr {
260    build::map(vec![
261        ("moves", build::text(content_id_string(&warrant.moves))),
262        ("frames", build::text(content_id_string(&warrant.frames))),
263        (
264            "parts",
265            build::list(
266                warrant
267                    .parts
268                    .iter()
269                    .map(|(kind, cid)| {
270                        build::map(vec![
271                            ("part-kind", Expr::Symbol(kind.clone())),
272                            ("cid", build::text(content_id_string(cid))),
273                        ])
274                    })
275                    .collect(),
276            ),
277        ),
278    ])
279}
280
281fn short_cid(cid: &str) -> &str {
282    cid.char_indices()
283        .nth(28)
284        .map(|(index, _)| &cid[..index])
285        .unwrap_or(cid)
286}
287
288fn validate(context: &str, scene: &Expr) -> Result<()> {
289    sim_lib_scene::validate_scene(scene)
290        .map_err(|error| Error::HostError(format!("{context}: {error}")))
291}