Skip to main content

qualia_client_core/view_host/
mod.rs

1//! App-global entity-view session composition (desktop-wide, not browser-only).
2//!
3//! Pure filter/layout from `qualia_core_db::entity_view`; this module binds storage + session.
4
5mod session;
6
7pub use session::{
8    morph_flatten, morph_spatialize, project_library_for_observer, project_web_locus, MorphMode,
9    ViewSession,
10};
11
12use qualia_core_db::entity_view::{Circumstance, EntityId, ObserverStatus, PresentationLevel};
13use serde::{Deserialize, Serialize};
14use std::sync::Mutex;
15
16/// Process-wide session for webizen-desktop (shell, studio, browser share one).
17static SESSION: Mutex<Option<ViewSession>> = Mutex::new(None);
18
19pub fn with_session<R>(f: impl FnOnce(&mut ViewSession) -> R) -> R {
20    let mut g = SESSION.lock().unwrap_or_else(|e| e.into_inner());
21    if g.is_none() {
22        *g = Some(ViewSession::default());
23    }
24    f(g.as_mut().expect("session just set"))
25}
26
27pub fn get_session_snapshot() -> ViewSession {
28    with_session(|s| s.clone())
29}
30
31pub fn set_observer(status: ObserverStatus) {
32    with_session(|s| s.observer = status);
33}
34
35pub fn set_presentation_level(level: u8) {
36    with_session(|s| s.presentation_level = PresentationLevel::from_u8(level));
37}
38
39pub fn select_entity(entity_id: u64) {
40    with_session(|s| {
41        s.selection.clear();
42        s.selection.push(EntityId::from_raw(entity_id));
43    });
44}
45
46/// Select by URI/DID/asset (stable EntityId via from_uri); also sets attention_url.
47pub fn select_entity_uri(uri: &str) {
48    let id = EntityId::from_uri(uri);
49    with_session(|s| {
50        s.selection.clear();
51        s.selection.push(id);
52        s.attention_url = Some(uri.to_string());
53    });
54}
55
56pub fn clear_selection() {
57    with_session(|s| {
58        s.selection.clear();
59        s.attention_url = None;
60    });
61}
62
63/// Set circumstance (role / audience / quorum / environment / evaluatory).
64pub fn set_circumstance(c: Circumstance) {
65    with_session(|s| s.circumstance = c);
66}
67
68/// DTO for Tauri / studio JSON.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct ProjectLibraryRequest {
71    pub storage_path: String,
72    pub section: Option<String>,
73    pub observer: Option<String>,
74    pub presentation_level: Option<u8>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ProjectWebLocusRequest {
79    pub url: String,
80    pub observer: Option<String>,
81}
82
83pub fn parse_observer(s: &str) -> ObserverStatus {
84    match s.trim().to_ascii_lowercase().as_str() {
85        "peer" => ObserverStatus::Peer,
86        "guardian" => ObserverStatus::Guardian,
87        "steward" => ObserverStatus::Steward,
88        "public" | "anonymous" => ObserverStatus::Public,
89        "instrument" | "agent" => ObserverStatus::Instrument,
90        "auditor" => ObserverStatus::Auditor,
91        _ => ObserverStatus::Principal,
92    }
93}
94
95/// Project library from disk path for given observer (updates session).
96pub fn project_library_json(
97    storage_path: &str,
98    section: Option<&str>,
99    observer: ObserverStatus,
100    level: PresentationLevel,
101) -> Result<serde_json::Value, String> {
102    with_session(|s| {
103        s.observer = observer;
104        s.presentation_level = level;
105        let result = project_library_for_observer(storage_path, section, observer, level)?;
106        s.last_projection = Some(result.clone());
107        s.morph_mode = MorphMode::Both;
108        serde_json::to_value(&result).map_err(|e| e.to_string())
109    })
110}
111
112pub fn project_web_locus_json(
113    url: &str,
114    observer: ObserverStatus,
115) -> Result<serde_json::Value, String> {
116    with_session(|s| {
117        s.observer = observer;
118        let card = project_web_locus(url, observer);
119        if let Some(id) = card.get("entity_id").and_then(|v| v.as_u64()) {
120            s.selection.clear();
121            s.selection.push(EntityId::from_raw(id));
122        }
123        s.attention_url = Some(url.to_string());
124        Ok(card)
125    })
126}
127
128pub fn morph_json(mode: &str) -> Result<serde_json::Value, String> {
129    with_session(|s| {
130        let Some(ref proj) = s.last_projection else {
131            return Err("no projection yet - call view_project_library first".into());
132        };
133        match mode.trim().to_ascii_lowercase().as_str() {
134            "flatten" | "flat" => {
135                s.morph_mode = MorphMode::Flatten;
136                Ok(morph_flatten(proj))
137            }
138            "spatialize" | "spatial" | "scene" => {
139                s.morph_mode = MorphMode::Spatialize;
140                Ok(morph_spatialize(proj))
141            }
142            "both" => {
143                s.morph_mode = MorphMode::Both;
144                serde_json::to_value(proj).map_err(|e| e.to_string())
145            }
146            _ => Err(format!(
147                "unknown morph mode '{mode}' (flatten|spatialize|both)"
148            )),
149        }
150    })
151}
152
153/// Nearest scene node in last projection by normalized (x,y) in 0..1 (controller pick).
154/// Returns entity_id when found; also updates selection.
155pub fn pick_scene_node_at(nx: f64, ny: f64, max_dist: f64) -> Option<u64> {
156    with_session(|s| {
157        let proj = s.last_projection.as_ref()?;
158        let mut best: Option<(f64, u64)> = None;
159        for n in &proj.scene_nodes {
160            if n.entity_id == 0 {
161                continue;
162            }
163            let dx = n.x - nx;
164            let dy = n.y - ny;
165            let d = (dx * dx + dy * dy).sqrt();
166            if d <= max_dist {
167                if best.map(|(bd, _)| d < bd).unwrap_or(true) {
168                    best = Some((d, n.entity_id));
169                }
170            }
171        }
172        let id = best.map(|(_, id)| id)?;
173        s.selection.clear();
174        s.selection.push(EntityId::from_raw(id));
175        Some(id)
176    })
177}