qualia_client_core/chora/
api.rs1use std::path::Path;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use ed25519_dalek::Signer;
7use qualia_core_db::domains::geospatial::canvas_query::query_region;
8use qualia_core_db::domains::geospatial::render_surface::{
9 build_surface_descriptor, RenderSurfaceDescriptor, BACKEND_WEBGPU,
10};
11use qualia_core_db::domains::geospatial::spatial::{SpatialElement, SpatiotemporalQuadTree};
12use qualia_core_db::domains::geospatial::spatial_sync::{
13 merge_planted_assets, PlantedAsset, SpatialSyncCell,
14};
15use qualia_core_db::query::spawn_decay::spawn_decay_alpha;
16
17use crate::canvas_state;
18use crate::canvas_store;
19use crate::canvas_world;
20use crate::wellfair::api::WebizenHostApi;
21use crate::wellfair::blob_store::BlobStore;
22use crate::wellfair::sync_protocol::SyncOperation;
23
24fn now_unix() -> u64 {
25 SystemTime::now()
26 .duration_since(UNIX_EPOCH)
27 .map(|d| d.as_secs())
28 .unwrap_or(0)
29}
30
31impl WebizenHostApi {
32 pub fn set_temporal_slice(&self, t_value: f64) -> Result<(), String> {
34 let storage_root = self.chora_storage_root();
35 let mut state = canvas_state::load(storage_root);
36 state.temporal_t = t_value.max(0.0) as u64;
37 canvas_state::save(storage_root, &state).map_err(|e| e.to_string())
38 }
39
40 pub fn get_temporal_slice(&self) -> u64 {
42 canvas_state::load(self.chora_storage_root()).temporal_t
43 }
44
45 fn canvas_store(&self) -> Result<canvas_store::CanvasWorldStore, String> {
46 canvas_store::CanvasWorldStore::open(self.chora_storage_root()).map_err(|e| e.to_string())
47 }
48
49 pub fn list_canvas_worlds(&self) -> Result<Vec<serde_json::Value>, String> {
51 let store = self.canvas_store()?;
52 let worlds = store.list().map_err(|e| e.to_string())?;
53 Ok(worlds
54 .iter()
55 .map(|w| {
56 serde_json::json!({
57 "id": w.id,
58 "title": w.title,
59 "layerCount": w.layer_stack.len(),
60 "assetCount": w.assets.len(),
61 "temporalRange": w.temporal_range,
62 "origin": { "lat": w.origin_lat, "lon": w.origin_lon },
63 })
64 })
65 .collect())
66 }
67
68 pub fn get_canvas_world(&self, world_id: &str) -> Result<serde_json::Value, String> {
70 let store = self.canvas_store()?;
71 let world = store
72 .get(world_id)
73 .map_err(|e| e.to_string())?
74 .ok_or_else(|| format!("world not found: {world_id}"))?;
75 serde_json::to_value(&world).map_err(|e| e.to_string())
76 }
77
78 pub fn save_canvas_world(&self, config_json: &str) -> Result<(), String> {
80 let config: canvas_world::CanvasWorldConfig =
81 serde_json::from_str(config_json).map_err(|e| e.to_string())?;
82 self.canvas_store()?
83 .upsert(config, now_unix())
84 .map_err(|e| e.to_string())
85 }
86
87 pub fn delete_canvas_world(&self, world_id: &str) -> Result<bool, String> {
89 self.canvas_store()?
90 .remove(world_id)
91 .map_err(|e| e.to_string())
92 }
93
94 pub fn seed_canvas_demo(&self) -> Result<bool, String> {
96 self.canvas_store()?
97 .seed_if_empty(now_unix())
98 .map_err(|e| e.to_string())
99 }
100
101 pub fn seed_flagship_worlds(&self) -> Result<u32, String> {
103 let seeded = super::seed_all_flagships(&self.canvas_store()?, now_unix())
104 .map_err(|e| e.to_string())?;
105 Ok(seeded as u32)
106 }
107
108 pub fn set_active_canvas_world(&self, world_id: &str) -> Result<(), String> {
110 let store = self.canvas_store()?;
111 if store.get(world_id).map_err(|e| e.to_string())?.is_none() {
112 return Err(format!("world not found: {world_id}"));
113 }
114 let storage_root = self.chora_storage_root();
115 let mut state = canvas_state::load(storage_root);
116 state.active_world_id = world_id.to_string();
117 canvas_state::save(storage_root, &state).map_err(|e| e.to_string())
118 }
119
120 pub fn canvas_navigation_state(&self) -> serde_json::Value {
122 let state = canvas_state::load(self.chora_storage_root());
123 serde_json::json!({
124 "activeWorldId": state.active_world_id,
125 "temporalT": state.temporal_t,
126 "rampSecs": state.ramp_secs,
127 })
128 }
129
130 pub fn query_canvas_region(
132 &self,
133 x1: f64,
134 y1: f64,
135 x2: f64,
136 y2: f64,
137 ) -> Result<Vec<serde_json::Value>, String> {
138 let nav = canvas_state::load(self.chora_storage_root());
139 let store = self.canvas_store()?;
140 let world = store
141 .get(&nav.active_world_id)
142 .map_err(|e| e.to_string())?
143 .ok_or_else(|| format!("active world not found: {}", nav.active_world_id))?;
144
145 let mut tree = SpatiotemporalQuadTree::new((-180.0, -90.0, 180.0, 90.0));
146 for (i, asset) in world.assets.iter().enumerate() {
147 let vf = asset.valid_from.unwrap_or(0);
148 let vu = asset.valid_until.unwrap_or(u64::MAX);
149 let alpha = spawn_decay_alpha(
150 nav.temporal_t,
151 vf,
152 asset.valid_until,
153 nav.ramp_secs,
154 nav.ramp_secs,
155 );
156 if alpha <= 0.0 {
157 continue;
158 }
159 let (lat, lon) = match (asset.lat, asset.lon) {
160 (Some(la), Some(lo)) => (la, lo),
161 _ => (world.origin_lat, world.origin_lon),
162 };
163 let bounds = (lon - 0.01, lat - 0.01, lon + 0.01, lat + 0.01);
164 tree.insert(SpatialElement {
165 h3_index: i as u64,
166 bounds,
167 t0: vf,
168 t1: vu,
169 });
170 }
171
172 let ramp = nav.ramp_secs;
173 let t0 = nav.temporal_t.saturating_sub(ramp);
174 let t1 = nav.temporal_t.saturating_add(ramp);
175 let hits = query_region(&tree, (x1, y1, x2, y2), (t0, t1));
176
177 Ok(hits
178 .iter()
179 .filter_map(|&idx| world.assets.get(idx as usize))
180 .map(|a| {
181 let vf = a.valid_from.unwrap_or(0);
182 let alpha = spawn_decay_alpha(
183 nav.temporal_t,
184 vf,
185 a.valid_until,
186 nav.ramp_secs,
187 nav.ramp_secs,
188 );
189 serde_json::json!({
190 "assetId": a.asset_id,
191 "licence": a.licence,
192 "lat": a.lat,
193 "lon": a.lon,
194 "alpha": alpha,
195 })
196 })
197 .collect())
198 }
199
200 pub fn request_asset_stream(&self, asset_id: &str) -> Result<Vec<u8>, String> {
206 let asset_id = asset_id.trim();
207 if asset_id.is_empty() {
208 return Err("asset_id must not be empty".into());
209 }
210
211 if asset_id.starts_with("urn:") {
212 return Err(format!(
213 "remote urn asset fetch is not implemented offline: {asset_id}"
214 ));
215 }
216
217 let storage_root = self.chora_storage_root();
218 if asset_id.len() == 64 && asset_id.bytes().all(|b| b.is_ascii_hexdigit()) {
219 let store = BlobStore::open(storage_root).map_err(|e| e.to_string())?;
220 return store
221 .get(asset_id)
222 .map_err(|e| e.to_string())?
223 .ok_or_else(|| format!("blob not found for hash {asset_id}"));
224 }
225
226 let path = if asset_id.starts_with("local://") {
227 let rel = asset_id.trim_start_matches("local://");
228 storage_root.join("wellfair/canvas").join(rel)
229 } else if Path::new(asset_id).is_absolute() {
230 Path::new(asset_id).to_path_buf()
231 } else {
232 storage_root.join(asset_id)
233 };
234
235 std::fs::read(&path).map_err(|e| format!("failed to read asset '{asset_id}': {e}"))
236 }
237
238 pub fn get_render_surface(&self) -> Result<String, String> {
240 const DEFAULT_WIDTH: u32 = 1280;
241 const DEFAULT_HEIGHT: u32 = 720;
242
243 let storage_root = self.chora_storage_root();
244 let nav = canvas_state::load(storage_root);
245 let store = self.canvas_store()?;
246 let world = store
247 .get(&nav.active_world_id)
248 .map_err(|e| e.to_string())?
249 .unwrap_or_else(canvas_world::CanvasWorldConfig::default);
250
251 let desc: RenderSurfaceDescriptor = build_surface_descriptor(
252 DEFAULT_WIDTH,
253 DEFAULT_HEIGHT,
254 BACKEND_WEBGPU,
255 nav.temporal_t,
256 &nav.active_world_id,
257 world.origin_lat,
258 world.origin_lon,
259 )?;
260
261 serde_json::to_string(&desc).map_err(|e| e.to_string())
262 }
263
264 pub fn publish_planted_asset(&self, asset: PlantedAsset) -> Result<(), String> {
266 let payload_summary = serde_json::to_string(&asset).map_err(|e| e.to_string())?;
267 let op = SyncOperation::new(
268 uuid::Uuid::new_v4().to_string(),
269 format!("urn:qualia:spatial_plant:{}", asset.asset_id),
270 "spatial_plant",
271 self.chora_owner_did().to_string(),
272 "Public",
273 payload_summary,
274 asset.lamport,
275 now_unix() as u32,
276 );
277 let signature = self.chora_signing_key().sign(&op.signing_payload());
278 let signed_op = op.with_signature(hex::encode(signature.to_bytes()));
279
280 self.admit_sync_operation(&signed_op).map(|_| ())
282 }
283
284 pub fn pull_spatial_assets(&self, cell_id: u64) -> Result<Vec<PlantedAsset>, String> {
286 let ops = self.validated_sync_operations()?;
287 let mut local_assets = Vec::new();
288
289 for op in ops {
290 if op.kind == "spatial_plant" {
291 if let Ok(asset) = serde_json::from_str::<PlantedAsset>(&op.payload_summary) {
292 if asset.cell_id == cell_id {
293 local_assets.push(asset);
294 }
295 }
296 }
297 }
298
299 let final_assets = merge_planted_assets(&local_assets, &[]);
303
304 Ok(final_assets.into_iter().filter(|a| !a.deleted).collect())
306 }
307
308 pub fn sync_cell(&self, cell_id: u64) -> Result<String, String> {
310 let assets = self.pull_spatial_assets(cell_id)?;
311 let cell = SpatialSyncCell::from_assets(cell_id, &assets);
312 serde_json::to_string(&cell).map_err(|e| e.to_string())
313 }
314}