brep_app/plm/
thumbnail.rs1use std::cell::RefCell;
23use std::collections::BTreeMap;
24use std::future::Future;
25use std::pin::Pin;
26use std::rc::Rc;
27use std::sync::{Arc, Mutex};
28use std::task::{Context, Poll, Waker};
29
30use brep_render::engine_state::EngineState;
31use brep_render::thumbnail::{self as raster, Capture};
32
33use super::backend::SavedHook;
34use super::client::PlmClient;
35
36pub fn content_hash(document: &str) -> String {
38 use sha2::{Digest, Sha256};
39 Sha256::digest(document.as_bytes()).iter().map(|b| format!("{b:02x}")).collect()
40}
41
42#[derive(Debug, Clone, Default, PartialEq, serde::Serialize)]
44pub struct Stats {
45 pub staged: u32,
47 pub uploaded: u32,
49 pub skipped: u32,
51 pub failed: u32,
53 pub last_error: String,
55 pub last_render_ms: f64,
57}
58
59thread_local! {
60 static STAGED: RefCell<BTreeMap<String, Staged>> = RefCell::new(BTreeMap::new());
61 static WAITING: RefCell<Vec<Waiting>> = const { RefCell::new(Vec::new()) };
62 static STATS: RefCell<Stats> = RefCell::new(Stats::default());
63}
64
65pub fn stats() -> Stats {
67 STATS.with(|s| s.borrow().clone())
68}
69
70fn count(f: impl FnOnce(&mut Stats)) {
71 STATS.with(|s| f(&mut s.borrow_mut()));
72}
73
74struct Staged {
75 hash: String,
76 picture: Picture,
77}
78
79struct Waiting {
82 doc: u64,
83 after: u64,
84 picture: Picture,
85}
86
87#[derive(Clone, Default)]
90pub struct Picture(Arc<Shared>);
91
92#[derive(Default)]
93struct Shared {
94 capture: Mutex<Option<Capture>>,
95 result: Mutex<Option<Option<Vec<u8>>>>,
96 waker: Mutex<Option<Waker>>,
97}
98
99impl Picture {
100 fn start(&self, capture: Capture) {
102 #[cfg(not(target_arch = "wasm32"))]
103 {
104 let picture = self.clone();
105 std::thread::spawn(move || picture.fill(render(&capture)));
106 }
107 #[cfg(target_arch = "wasm32")]
108 {
109 *self.0.capture.lock().unwrap_or_else(|p| p.into_inner()) = Some(capture);
110 if let Some(waker) = self.0.waker.lock().unwrap_or_else(|p| p.into_inner()).take() {
111 waker.wake();
112 }
113 }
114 }
115
116 fn fill(&self, png: Option<Vec<u8>>) {
117 *self.0.result.lock().unwrap_or_else(|p| p.into_inner()) = Some(png);
118 if let Some(waker) = self.0.waker.lock().unwrap_or_else(|p| p.into_inner()).take() {
119 waker.wake();
120 }
121 }
122}
123
124impl Future for Picture {
125 type Output = Option<Vec<u8>>;
126
127 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
128 let capture = self.0.capture.lock().unwrap_or_else(|p| p.into_inner()).take();
129 if let Some(capture) = capture {
130 return Poll::Ready(render(&capture));
131 }
132 *self.0.waker.lock().unwrap_or_else(|p| p.into_inner()) = Some(cx.waker().clone());
133 match self.0.result.lock().unwrap_or_else(|p| p.into_inner()).take() {
134 Some(png) => Poll::Ready(png),
135 None => Poll::Pending,
136 }
137 }
138}
139
140fn render(capture: &Capture) -> Option<Vec<u8>> {
142 let started = crate::recovery::wall_clock();
143 let png = raster::render_png(capture, raster::SIZE);
144 let ms = (crate::recovery::wall_clock() - started) * 1000.0;
145 #[cfg(target_arch = "wasm32")]
148 count(|s| s.last_render_ms = ms);
149 #[cfg(not(target_arch = "wasm32"))]
150 LAST_RENDER_MS.store(ms.to_bits(), std::sync::atomic::Ordering::Relaxed);
151 png
152}
153
154#[cfg(not(target_arch = "wasm32"))]
155static LAST_RENDER_MS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
156
157pub fn stage(key: &str, document: &str, engine: &EngineState, doc: u64) {
161 let picture = Picture::default();
162 if engine.run_pending() {
163 WAITING.with(|w| w.borrow_mut().push(Waiting { doc, after: engine.applied_generation(), picture: picture.clone() }));
164 } else {
165 picture.start(raster::capture(&engine.scene));
166 }
167 count(|s| s.staged += 1);
168 STAGED.with(|s| s.borrow_mut().insert(key.to_string(), Staged { hash: content_hash(document), picture }));
169}
170
171pub fn stage_save(store: &dyn crate::store::ModelStore, name: &str, document: &str, engine: &EngineState, doc: u64) {
175 if store.plm_client().is_none() {
176 return;
177 }
178 if let Some(key) = crate::plm::uses::revision_key_in(name) {
179 stage(&key, document, engine, doc);
180 }
181}
182
183pub fn tick<'a>(engine_of: impl Fn(u64) -> Option<&'a EngineState>) {
187 if WAITING.with(|w| w.borrow().is_empty()) {
188 return;
189 }
190 WAITING.with(|w| {
191 w.borrow_mut().retain(|waiting| match engine_of(waiting.doc) {
192 None => {
193 waiting.picture.fill(None);
194 false
195 }
196 Some(engine) if engine.run_pending() || engine.applied_generation() == waiting.after => true,
197 Some(engine) => {
198 waiting.picture.start(raster::capture(&engine.scene));
199 false
200 }
201 })
202 });
203}
204
205fn take(key: &str, document: &str) -> Option<Picture> {
208 let hash = content_hash(document);
209 STAGED.with(|s| {
210 let mut staged = s.borrow_mut();
211 match staged.get(key) {
212 Some(entry) if entry.hash == hash => staged.remove(key).map(|e| e.picture),
213 _ => None,
214 }
215 })
216}
217
218pub(crate) fn upload_on_save() -> SavedHook {
221 Rc::new(|client, key, value| {
222 let key = crate::plm::uses::revision_key_in(&key).unwrap_or(key);
224 if let Some(picture) = take(&key, &value) {
225 let hash = content_hash(&value);
226 detach(async move { upload(&client, &key, &hash, picture).await });
227 }
228 Box::pin(async { Ok(()) })
229 })
230}
231
232pub(crate) fn chain(first: SavedHook, then: SavedHook) -> SavedHook {
235 Rc::new(move |client: Rc<PlmClient>, key: String, value: String| {
236 let before = first(client.clone(), key.clone(), value.clone());
237 let then = then.clone();
238 Box::pin(async move {
239 let answer = before.await;
240 let _ = then(client, key, value).await;
241 answer
242 })
243 })
244}
245
246async fn upload(client: &PlmClient, key: &str, hash: &str, picture: Picture) {
248 let Some(png) = picture.await else {
249 count(|s| s.skipped += 1);
250 return;
251 };
252 #[cfg(not(target_arch = "wasm32"))]
253 count(|s| s.last_render_ms = f64::from_bits(LAST_RENDER_MS.load(std::sync::atomic::Ordering::Relaxed)));
254 match put(client, key, hash, png).await {
255 Ok(()) => count(|s| s.uploaded += 1),
256 Err(why) => {
257 log::warn!("thumbnail of {key} not uploaded: {why}");
258 count(|s| {
259 s.failed += 1;
260 s.last_error = why;
261 });
262 }
263 }
264}
265
266pub async fn put(client: &PlmClient, key: &str, hash: &str, png: Vec<u8>) -> Result<(), String> {
269 let (part, revision) = revision_of(key)?;
270 let path = format!("{}/thumbnail?content_hash={hash}&renderer={}", super::identity::revision_path(&part, &revision), raster::RENDERER);
271 client.call_typed("PUT", &path, png, "image/png").await.map(|_| ()).map_err(|e| e.to_string())
272}
273
274fn revision_of(key: &str) -> Result<(String, String), String> {
275 super::family::key_ids(key).ok_or_else(|| format!("'{key}' is not a revision key"))
276}
277
278fn detach(task: impl Future<Output = ()> + 'static) {
279 #[cfg(target_arch = "wasm32")]
280 wasm_bindgen_futures::spawn_local(task);
281 #[cfg(not(target_arch = "wasm32"))]
282 super::native::spawn(task);
283}
284