Skip to main content

qualia_client_core/view_host/
session.rs

1//! Session + projectors: library storage - filtered projection.
2
3use qualia_core_db::entity_view::{
4    decide_view, layout_scene_nodes, Circumstance, EntityId, EntityKind, EntityViewMeta, FlatCard,
5    LayoutInput, ObserverStatus, PresentationLevel, ProjectionResult, SceneNodeProj,
6    SensitivityClass,
7};
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11use crate::wellfair::hypermedia_store::{CommonsVisibility, HypermediaStore, LibraryEntry};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
14#[serde(rename_all = "snake_case")]
15pub enum MorphMode {
16    Flatten,
17    Spatialize,
18    #[default]
19    Both,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ViewSession {
24    pub observer: ObserverStatus,
25    pub presentation_level: PresentationLevel,
26    pub selection: Vec<EntityId>,
27    pub morph_mode: MorphMode,
28    pub attention_url: Option<String>,
29    /// Spatio-social-temporal circumstance (partial: design + session hooks).
30    #[serde(default)]
31    pub circumstance: Circumstance,
32    #[serde(skip)]
33    pub last_projection: Option<ProjectionResult>,
34}
35
36impl Default for ViewSession {
37    fn default() -> Self {
38        Self {
39            observer: ObserverStatus::Principal,
40            presentation_level: PresentationLevel::AppHabitat,
41            selection: Vec::new(),
42            morph_mode: MorphMode::Both,
43            attention_url: None,
44            circumstance: Circumstance::private_sanctuary(),
45            last_projection: None,
46        }
47    }
48}
49
50fn entry_to_meta(e: &LibraryEntry) -> EntityViewMeta {
51    let sens = SensitivityClass::parse(&e.sensitivity);
52    let is_secret = e.is_secret() || e.section == "secret" || sens.is_high();
53    let commons = matches!(
54        e.commons_visibility,
55        CommonsVisibility::Peers | CommonsVisibility::Commons
56    ) || e.section == "commons";
57    let peer = matches!(
58        e.commons_visibility,
59        CommonsVisibility::Peers | CommonsVisibility::Commons
60    );
61    EntityViewMeta {
62        entity_id: EntityId::from_uri(&e.asset_uri),
63        kind: EntityKind::Asset,
64        sensitivity: sens,
65        is_secret,
66        commons_visible: commons && !is_secret,
67        peer_offered: peer && !is_secret,
68    }
69}
70
71fn entry_title(e: &LibraryEntry) -> String {
72    let u = e.asset_uri.as_str();
73    u.rsplit(['/', ':']).next().unwrap_or(u).to_string()
74}
75
76/// Project library section for observer into flat + scene nodes.
77pub fn project_library_for_observer(
78    storage_path: &str,
79    section: Option<&str>,
80    observer: ObserverStatus,
81    level: PresentationLevel,
82) -> Result<ProjectionResult, String> {
83    let root = std::path::Path::new(storage_path);
84    let store = HypermediaStore::open(root).map_err(|e| e.to_string())?;
85    let entries = match section {
86        Some(s) if !s.is_empty() && s != "all" => store
87            .by_section(crate::wellfair::hypermedia_store::LibrarySection::parse(s))
88            .map_err(|e| e.to_string())?,
89        _ => store.all().map_err(|e| e.to_string())?,
90    };
91
92    let mut flat = Vec::new();
93    let mut layout_in = Vec::new();
94    let mut hidden = 0u32;
95
96    for e in &entries {
97        let meta = entry_to_meta(e);
98        let decision = decide_view(observer, &meta);
99        if !decision.visible {
100            hidden += 1;
101            continue;
102        }
103        let excerpt = e.excerpt.chars().take(160).collect::<String>();
104        flat.push(FlatCard {
105            entity_id: meta.entity_id.raw(),
106            kind: meta.kind,
107            title: entry_title(e),
108            excerpt,
109            wing: decision.wing,
110            affordance_bits: decision.affordances.pack(),
111            honesty: if e.topics.iter().any(|t| t.contains("seed")) {
112                "partial".into()
113            } else {
114                "present".into()
115            },
116            uri: e.asset_uri.clone(),
117        });
118        layout_in.push(LayoutInput {
119            entity_id: meta.entity_id,
120            lat: e.lat,
121            lon: e.lon,
122            affordances: decision.affordances,
123            wing: decision.wing,
124        });
125    }
126
127    let mut scene_buf: Vec<SceneNodeProj> = (0..layout_in.len().max(1))
128        .map(|_| SceneNodeProj {
129            entity_id: 0,
130            id: String::new(),
131            x: 0.0,
132            y: 0.0,
133            z: 0.0,
134            color: String::new(),
135            radius: 0.0,
136            alpha: 0.0,
137            affordance_bits: 0,
138        })
139        .collect();
140    let n = layout_scene_nodes(&layout_in, &mut scene_buf);
141    scene_buf.truncate(n);
142
143    Ok(ProjectionResult {
144        observer: format!("{observer:?}").to_ascii_lowercase(),
145        presentation_level: level.as_u8(),
146        flat,
147        scene_nodes: scene_buf,
148        hidden_count: hidden,
149    })
150}
151
152pub fn project_web_locus(url: &str, observer: ObserverStatus) -> serde_json::Value {
153    let id = EntityId::from_uri(url);
154    let meta = EntityViewMeta {
155        entity_id: id,
156        kind: EntityKind::WebLocus,
157        sensitivity: SensitivityClass::Public,
158        is_secret: false,
159        commons_visible: true,
160        peer_offered: true,
161    };
162    let d = decide_view(observer, &meta);
163    json!({
164        "entity_id": id.raw(),
165        "kind": "web_locus",
166        "uri": url,
167        "visible": d.visible,
168        "wing": d.wing,
169        "affordance_bits": d.affordances.pack(),
170        "title": url,
171        "honesty": "present",
172    })
173}
174
175pub fn morph_flatten(proj: &ProjectionResult) -> serde_json::Value {
176    json!({
177        "morph": "flatten",
178        "observer": proj.observer,
179        "presentation_level": proj.presentation_level,
180        "flat": proj.flat,
181        "hidden_count": proj.hidden_count,
182    })
183}
184
185pub fn morph_spatialize(proj: &ProjectionResult) -> serde_json::Value {
186    json!({
187        "morph": "spatialize",
188        "observer": proj.observer,
189        "presentation_level": proj.presentation_level,
190        "scene_nodes": proj.scene_nodes,
191        "hidden_count": proj.hidden_count,
192    })
193}