qualia_client_core/view_host/
session.rs1use 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 #[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
76pub 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}