1use std::cell::RefCell;
32use std::collections::BTreeMap;
33use std::future::Future;
34use std::pin::Pin;
35use std::rc::Rc;
36use std::sync::{Arc, Mutex};
37use std::task::{Context, Poll, Waker};
38
39use brep_render::engine_state::EngineState;
40use brep_render::thumbnail::{self as raster, Capture};
41
42use super::backend::SavedHook;
43use super::client::PlmClient;
44
45pub fn content_hash(document: &str) -> String {
47 use sha2::{Digest, Sha256};
48 Sha256::digest(document.as_bytes()).iter().map(|b| format!("{b:02x}")).collect()
49}
50
51#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
53pub struct Stats {
54 pub staged: u32,
56 pub uploaded: u32,
58 pub skipped: u32,
60 pub failed: u32,
62 pub last_error: String,
64 pub last_render_ms: f64,
66}
67
68thread_local! {
69 static STAGED: RefCell<BTreeMap<String, Staged>> = RefCell::new(BTreeMap::new());
70 static WAITING: RefCell<Vec<Waiting>> = const { RefCell::new(Vec::new()) };
71 static STATS: RefCell<Stats> = RefCell::new(Stats::default());
72}
73
74pub fn stats() -> Stats {
76 STATS.with(|s| s.borrow().clone())
77}
78
79fn count(f: impl FnOnce(&mut Stats)) {
80 STATS.with(|s| f(&mut s.borrow_mut()));
81}
82
83struct Staged {
84 hash: String,
85 picture: Picture,
86}
87
88struct Waiting {
91 doc: u64,
92 after: u64,
93 picture: Picture,
94}
95
96#[derive(Clone, Default)]
99pub struct Picture(Arc<Shared>);
100
101struct Shared {
102 size: u32,
104 capture: Mutex<Option<Capture>>,
105 result: Mutex<Option<Option<Vec<u8>>>>,
106 waker: Mutex<Option<Waker>>,
107}
108
109impl Default for Shared {
110 fn default() -> Self {
111 Self { size: raster::SIZE, capture: Mutex::new(None), result: Mutex::new(None), waker: Mutex::new(None) }
112 }
113}
114
115impl Picture {
116 fn sized(size: u32) -> Self {
118 Self(Arc::new(Shared { size, ..Shared::default() }))
119 }
120
121 fn peek(&self) -> Option<Option<Vec<u8>>> {
124 let capture = self.0.capture.lock().unwrap_or_else(|p| p.into_inner()).take();
125 if let Some(capture) = capture {
126 return Some(render(&capture, self.0.size));
127 }
128 self.0.result.lock().unwrap_or_else(|p| p.into_inner()).take()
129 }
130
131 fn start(&self, capture: Capture) {
133 #[cfg(not(target_arch = "wasm32"))]
134 {
135 let picture = self.clone();
136 let size = self.0.size;
137 std::thread::spawn(move || picture.fill(render(&capture, size)));
138 }
139 #[cfg(target_arch = "wasm32")]
140 {
141 *self.0.capture.lock().unwrap_or_else(|p| p.into_inner()) = Some(capture);
142 if let Some(waker) = self.0.waker.lock().unwrap_or_else(|p| p.into_inner()).take() {
143 waker.wake();
144 }
145 }
146 }
147
148 fn fill(&self, png: Option<Vec<u8>>) {
149 *self.0.result.lock().unwrap_or_else(|p| p.into_inner()) = Some(png);
150 if let Some(waker) = self.0.waker.lock().unwrap_or_else(|p| p.into_inner()).take() {
151 waker.wake();
152 }
153 }
154}
155
156impl Future for Picture {
157 type Output = Option<Vec<u8>>;
158
159 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
160 let capture = self.0.capture.lock().unwrap_or_else(|p| p.into_inner()).take();
161 if let Some(capture) = capture {
162 return Poll::Ready(render(&capture, self.0.size));
163 }
164 *self.0.waker.lock().unwrap_or_else(|p| p.into_inner()) = Some(cx.waker().clone());
165 match self.0.result.lock().unwrap_or_else(|p| p.into_inner()).take() {
166 Some(png) => Poll::Ready(png),
167 None => Poll::Pending,
168 }
169 }
170}
171
172fn render(capture: &Capture, size: u32) -> Option<Vec<u8>> {
174 let started = crate::recovery::wall_clock();
175 let png = raster::render_png(capture, size);
176 let ms = (crate::recovery::wall_clock() - started) * 1000.0;
177 if size == raster::SIZE {
180 #[cfg(target_arch = "wasm32")]
183 count(|s| s.last_render_ms = ms);
184 #[cfg(not(target_arch = "wasm32"))]
185 LAST_RENDER_MS.store(ms.to_bits(), std::sync::atomic::Ordering::Relaxed);
186 }
187 png
188}
189
190#[cfg(not(target_arch = "wasm32"))]
191static LAST_RENDER_MS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
192
193pub fn stage(key: &str, document: &str, engine: &EngineState, doc: u64) {
197 let picture = picture_of(engine, doc, raster::SIZE);
198 count(|s| s.staged += 1);
199 STAGED.with(|s| s.borrow_mut().insert(key.to_string(), Staged { hash: content_hash(document), picture }));
200}
201
202fn picture_of(engine: &EngineState, doc: u64, size: u32) -> Picture {
205 let picture = Picture::sized(size);
206 if engine.run_pending() {
207 WAITING.with(|w| w.borrow_mut().push(Waiting { doc, after: engine.applied_generation(), picture: picture.clone() }));
208 } else {
209 picture.start(raster::capture(&engine.scene));
210 }
211 picture
212}
213
214pub fn stage_save(store: &dyn crate::store::ModelStore, name: &str, document: &str, engine: &EngineState, doc: u64) {
219 stage_recent(name, engine, doc);
220 if store.plm_client().is_none() {
221 return;
222 }
223 if let Some(key) = crate::plm::uses::revision_key_in(name) {
224 stage(&key, document, engine, doc);
225 }
226}
227
228pub fn tick<'a>(engine_of: impl Fn(u64) -> Option<&'a EngineState>) {
232 if WAITING.with(|w| w.borrow().is_empty()) {
233 return;
234 }
235 WAITING.with(|w| {
236 w.borrow_mut().retain(|waiting| match engine_of(waiting.doc) {
237 None => {
238 waiting.picture.fill(None);
239 false
240 }
241 Some(engine) if engine.run_pending() || engine.applied_generation() == waiting.after => true,
242 Some(engine) => {
243 waiting.picture.start(raster::capture(&engine.scene));
244 false
245 }
246 })
247 });
248}
249
250fn take(key: &str, document: &str) -> Option<Picture> {
253 let hash = content_hash(document);
254 STAGED.with(|s| {
255 let mut staged = s.borrow_mut();
256 match staged.get(key) {
257 Some(entry) if entry.hash == hash => staged.remove(key).map(|e| e.picture),
258 _ => None,
259 }
260 })
261}
262
263pub(crate) fn upload_on_save() -> SavedHook {
266 Rc::new(|client, key, value| {
267 let key = crate::plm::uses::revision_key_in(&key).unwrap_or(key);
269 if let Some(picture) = take(&key, &value) {
270 let hash = content_hash(&value);
271 detach(async move { upload(&client, &key, &hash, picture).await });
272 }
273 Box::pin(async { Ok(()) })
274 })
275}
276
277pub(crate) fn chain(first: SavedHook, then: SavedHook) -> SavedHook {
280 Rc::new(move |client: Rc<PlmClient>, key: String, value: String| {
281 let before = first(client.clone(), key.clone(), value.clone());
282 let then = then.clone();
283 Box::pin(async move {
284 let answer = before.await;
285 let _ = then(client, key, value).await;
286 answer
287 })
288 })
289}
290
291async fn upload(client: &PlmClient, key: &str, hash: &str, picture: Picture) {
293 let Some(png) = picture.await else {
294 count(|s| s.skipped += 1);
295 return;
296 };
297 #[cfg(not(target_arch = "wasm32"))]
298 count(|s| s.last_render_ms = f64::from_bits(LAST_RENDER_MS.load(std::sync::atomic::Ordering::Relaxed)));
299 match put(client, key, hash, png).await {
300 Ok(()) => count(|s| s.uploaded += 1),
301 Err(why) => {
302 log::warn!("thumbnail of {key} not uploaded: {why}");
303 count(|s| {
304 s.failed += 1;
305 s.last_error = why;
306 });
307 }
308 }
309}
310
311pub async fn put(client: &PlmClient, key: &str, hash: &str, png: Vec<u8>) -> Result<(), String> {
314 let (part, revision) = revision_of(key)?;
315 let path = format!("{}/thumbnail?content_hash={hash}&renderer={}", super::identity::revision_path(&part, &revision), raster::RENDERER);
316 client.call_typed("PUT", &path, png, "image/png").await.map(|_| ()).map_err(|e| e.to_string())
317}
318
319fn revision_of(key: &str) -> Result<(String, String), String> {
320 super::family::key_ids(key).ok_or_else(|| format!("'{key}' is not a revision key"))
321}
322
323fn detach(task: impl Future<Output = ()> + 'static) {
324 #[cfg(target_arch = "wasm32")]
325 wasm_bindgen_futures::spawn_local(task);
326 #[cfg(not(target_arch = "wasm32"))]
327 super::native::spawn(task);
328}
329
330use crate::store::{ModelStore, RECENT_THUMBNAILS_KEY};
335use eframe::egui;
336
337pub const RECENT_SIZE: u32 = 80;
342pub const RECENT_SIDE: f32 = 40.0;
344
345#[derive(Debug, Clone, Default, PartialEq)]
347pub struct RecentStats {
348 pub staged: u32,
350 pub stored: u32,
352 pub skipped: u32,
354}
355
356struct Recent {
358 name: String,
359 picture: Picture,
360}
361
362thread_local! {
363 static RECENT: RefCell<Vec<Recent>> = const { RefCell::new(Vec::new()) };
364 static RECENT_STATS: RefCell<RecentStats> = RefCell::new(RecentStats::default());
365 static RECENT_TEXTURES: RefCell<Option<RecentTextures>> = const { RefCell::new(None) };
366}
367
368pub fn recent_stats() -> RecentStats {
370 RECENT_STATS.with(|s| s.borrow().clone())
371}
372
373fn count_recent(f: impl FnOnce(&mut RecentStats)) {
374 RECENT_STATS.with(|s| f(&mut s.borrow_mut()));
375}
376
377pub fn stage_recent(name: &str, engine: &EngineState, doc: u64) {
381 let picture = picture_of(engine, doc, RECENT_SIZE);
382 count_recent(|s| s.staged += 1);
383 RECENT.with(|r| r.borrow_mut().push(Recent { name: name.to_string(), picture }));
384}
385
386fn recent_map(store: &dyn ModelStore) -> BTreeMap<String, String> {
388 store
389 .read(RECENT_THUMBNAILS_KEY)
390 .and_then(|text| serde_json::from_str(&text).ok())
391 .unwrap_or_default()
392}
393
394pub fn persist_recent(store: &dyn ModelStore) {
399 let done: Vec<(String, Option<Vec<u8>>)> = RECENT.with(|r| {
400 let mut pending = r.borrow_mut();
401 let mut done = Vec::new();
402 pending.retain(|entry| match entry.picture.peek() {
403 Some(png) => {
404 done.push((entry.name.clone(), png));
405 false
406 }
407 None => true,
408 });
409 done
410 });
411 if done.is_empty() {
412 return;
413 }
414 let recent = crate::panels::file::recent_documents(store);
415 let mut map = recent_map(store);
416 map.retain(|name, _| recent.contains(name));
417 for (name, png) in done {
418 match png {
419 Some(png) if recent.contains(&name) => {
420 map.insert(name, base64_encode(&png));
421 count_recent(|s| s.stored += 1);
422 }
423 Some(_) => count_recent(|s| s.skipped += 1),
424 None => {
425 map.remove(&name);
427 count_recent(|s| s.skipped += 1);
428 }
429 }
430 }
431 let text = serde_json::to_string(&map).unwrap_or_else(|_| "{}".into());
432 if let Err(why) = store.write(RECENT_THUMBNAILS_KEY, &text) {
433 log::warn!("recent thumbnails not stored: {why}");
434 }
435}
436
437struct RecentTextures {
440 generation: u64,
441 textures: Rc<BTreeMap<String, (String, egui::TextureHandle)>>,
442}
443
444pub fn recent_textures(ctx: &egui::Context, store: &dyn ModelStore) -> Rc<BTreeMap<String, (String, egui::TextureHandle)>> {
447 let generation = store.mutation_generation();
448 RECENT_TEXTURES.with(|cache| {
449 let mut cache = cache.borrow_mut();
450 if let Some(cached) = cache.as_ref().filter(|c| c.generation == generation) {
451 return cached.textures.clone();
452 }
453 let previous = cache.take().map(|c| c.textures).unwrap_or_default();
454 let mut textures = BTreeMap::new();
455 for (name, encoded) in recent_map(store) {
456 if let Some((kept, texture)) = previous.get(&name).filter(|(kept, _)| *kept == encoded) {
457 textures.insert(name, (kept.clone(), texture.clone()));
458 continue;
459 }
460 let Some(image) = base64_decode(&encoded).and_then(|png| decode_png(&png)) else { continue };
461 let texture = ctx.load_texture(format!("recent-thumbnail:{name}"), image, egui::TextureOptions::LINEAR);
462 textures.insert(name, (encoded, texture));
463 }
464 let textures = Rc::new(textures);
465 *cache = Some(RecentTextures { generation, textures: textures.clone() });
466 textures
467 })
468}
469
470fn decode_png(bytes: &[u8]) -> Option<egui::ColorImage> {
472 let mut reader = image::ImageReader::with_format(std::io::Cursor::new(bytes), image::ImageFormat::Png);
473 let mut limits = image::Limits::default();
474 limits.max_image_width = Some(512);
475 limits.max_image_height = Some(512);
476 limits.max_alloc = Some(4 * 1024 * 1024);
477 reader.limits(limits);
478 let image = reader.decode().ok()?.to_rgba8();
479 Some(egui::ColorImage::from_rgba_unmultiplied([image.width() as usize, image.height() as usize], image.as_raw()))
480}
481
482const BASE64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
483
484fn base64_encode(bytes: &[u8]) -> String {
486 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
487 for chunk in bytes.chunks(3) {
488 let n = chunk.iter().enumerate().fold(0u32, |n, (i, b)| n | (u32::from(*b) << (16 - 8 * i)));
489 for i in 0..4 {
490 if i <= chunk.len() {
491 out.push(BASE64[((n >> (18 - 6 * i)) & 63) as usize] as char);
492 } else {
493 out.push('=');
494 }
495 }
496 }
497 out
498}
499
500fn base64_decode(text: &str) -> Option<Vec<u8>> {
501 let text = text.trim_end_matches('=');
502 let mut out = Vec::with_capacity(text.len() * 3 / 4);
503 let mut acc = 0u32;
504 let mut bits = 0u32;
505 for c in text.bytes() {
506 let value = BASE64.iter().position(|b| *b == c)? as u32;
507 acc = (acc << 6) | value;
508 bits += 6;
509 if bits >= 8 {
510 bits -= 8;
511 out.push(((acc >> bits) & 0xff) as u8);
512 }
513 }
514 Some(out)
515}
516