Skip to main content

qualia_core_db/entity_view/
projection.rs

1//! Flat + scene projection IR for mindware HID (cold-path friendly serde types).
2
3use super::entity_id::{EntityId, EntityKind};
4use super::observer::{AffordanceBits, RepresentationWing};
5use serde::{Deserialize, Serialize};
6
7/// Presentation morphology level P0-P6 (see presentation-morphology plan).
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
9#[repr(u8)]
10pub enum PresentationLevel {
11    #[default]
12    Document = 0,
13    AppHabitat = 1,
14    SpatialDesk = 2,
15    StageWorld = 3,
16    EmbodiedWorld = 4,
17    Infosphere = 5,
18    MultiSensory = 6,
19}
20
21impl PresentationLevel {
22    pub fn from_u8(v: u8) -> Self {
23        match v {
24            1 => Self::AppHabitat,
25            2 => Self::SpatialDesk,
26            3 => Self::StageWorld,
27            4 => Self::EmbodiedWorld,
28            5 => Self::Infosphere,
29            6 => Self::MultiSensory,
30            _ => Self::Document,
31        }
32    }
33
34    pub fn as_u8(self) -> u8 {
35        self as u8
36    }
37}
38
39/// Card descriptor for flat HID (Dioxus / browser chrome lists).
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct FlatCard {
42    pub entity_id: u64,
43    pub kind: EntityKind,
44    pub title: String,
45    pub excerpt: String,
46    pub wing: RepresentationWing,
47    pub affordance_bits: u8,
48    pub honesty: String,
49    pub uri: String,
50}
51
52/// Scene node projection (maps to webizen-render SceneNode fields).
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct SceneNodeProj {
55    pub entity_id: u64,
56    pub id: String,
57    pub x: f64,
58    pub y: f64,
59    pub z: f64,
60    pub color: String,
61    pub radius: f64,
62    pub alpha: f64,
63    pub affordance_bits: u8,
64}
65
66/// Combined projection result for one observer session.
67#[derive(Debug, Clone, Serialize, Deserialize, Default)]
68pub struct ProjectionResult {
69    pub observer: String,
70    pub presentation_level: u8,
71    pub flat: Vec<FlatCard>,
72    pub scene_nodes: Vec<SceneNodeProj>,
73    pub hidden_count: u32,
74}
75
76/// Layout entity with optional geo for pin placement.
77#[derive(Debug, Clone, Copy)]
78pub struct LayoutInput {
79    pub entity_id: EntityId,
80    pub lat: Option<f32>,
81    pub lon: Option<f32>,
82    pub affordances: AffordanceBits,
83    pub wing: RepresentationWing,
84}
85
86/// Wing → presentation colour (cinema-readable, not neon random).
87pub fn wing_color(wing: RepresentationWing) -> &'static str {
88    match wing {
89        RepresentationWing::Private => "#a78bfa", // violet
90        RepresentationWing::Offered => "#34d399", // emerald
91        RepresentationWing::Commons => "#38bdf8", // sky
92    }
93}
94
95/// Place nodes: geo pins when lat/lon present; otherwise golden-angle manifold field.
96/// Writes up to `out.len()` nodes; returns count.
97pub fn layout_scene_nodes(inputs: &[LayoutInput], out: &mut [SceneNodeProj]) -> usize {
98    let mut n = 0;
99    let mut field_i = 0usize;
100    let field_total = inputs
101        .iter()
102        .filter(|i| i.lat.is_none() || i.lon.is_none())
103        .count()
104        .max(1);
105    for inp in inputs {
106        if n >= out.len() {
107            break;
108        }
109        let (x, y, z) = if let (Some(lat), Some(lon)) = (inp.lat, inp.lon) {
110            // Equirectangular sketch in 0..1 for 2D map morph.
111            let xn = ((lon as f64) + 180.0) / 360.0;
112            let yn = 1.0 - ((lat as f64) + 90.0) / 180.0;
113            (xn.clamp(0.05, 0.95), yn.clamp(0.05, 0.95), 0.15)
114        } else {
115            // Golden-angle disk → slight depth for spatial field (prestige morph).
116            let i = field_i as f64;
117            let tot = field_total as f64;
118            field_i += 1;
119            let golden = std::f64::consts::PI * (3.0 - 5.0_f64.sqrt());
120            let r = ((i + 0.5) / tot).sqrt() * 0.38;
121            let theta = i * golden;
122            let xn = 0.5 + r * theta.cos();
123            let yn = 0.5 + r * theta.sin() * 0.72;
124            let zn = 0.2 + (i / tot) * 0.55;
125            (
126                xn.clamp(0.08, 0.92),
127                yn.clamp(0.08, 0.92),
128                zn.clamp(0.05, 0.95),
129            )
130        };
131        let bits = inp.affordances.pack();
132        let radius = 5.5
133            + if inp.affordances.can_edit { 2.0 } else { 0.0 }
134            + if inp.affordances.can_share { 1.0 } else { 0.0 };
135        out[n] = SceneNodeProj {
136            entity_id: inp.entity_id.raw(),
137            id: format!("{:016x}", inp.entity_id.raw()),
138            x,
139            y,
140            z,
141            color: wing_color(inp.wing).into(),
142            radius,
143            alpha: (0.5 + z * 0.5).clamp(0.45, 1.0),
144            affordance_bits: bits,
145        };
146        n += 1;
147    }
148    n
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::entity_view::entity_id::EntityId;
155    use crate::entity_view::observer::AffordanceBits;
156
157    #[test]
158    fn layout_uses_geo_when_present() {
159        let inputs = [LayoutInput {
160            entity_id: EntityId::from_uri("urn:pin"),
161            lat: Some(0.0),
162            lon: Some(0.0),
163            affordances: AffordanceBits::FULL,
164            wing: RepresentationWing::Commons,
165        }];
166        let mut out: Vec<SceneNodeProj> = (0..2)
167            .map(|_| SceneNodeProj {
168                entity_id: 0,
169                id: String::new(),
170                x: 0.0,
171                y: 0.0,
172                z: 0.0,
173                color: String::new(),
174                radius: 0.0,
175                alpha: 0.0,
176                affordance_bits: 0,
177            })
178            .collect();
179        let n = layout_scene_nodes(&inputs, &mut out);
180        assert_eq!(n, 1);
181        assert!((out[0].x - 0.5).abs() < 0.01);
182    }
183}