mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 05:27:08 -06:00
Merge main and preserve Startup script tasks across project changes
This commit is contained in:
@@ -86,27 +86,39 @@ pub struct WebApp {
|
||||
}
|
||||
|
||||
impl eframe::App for WebApp {
|
||||
/// Take dropped files out of `raw_input` before eframe can replay them on a hidden tab, read them asynchronously
|
||||
/// through `open_or_import`, then run the shared app's input hook.
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
|
||||
// Files dropped on the page are input: read them here, where the browser's drop event is
|
||||
// still the current pass's, not from a later (possibly logic-only) tick.
|
||||
//
|
||||
// The list is *taken* out of the raw input: a hidden tab runs `logic` and eframe appends
|
||||
// the same prepared raw input back for the next pass, so leaving the files there would
|
||||
// import them again on every hidden logic tick.
|
||||
let dropped_files = std::mem::take(&mut raw_input.dropped_files);
|
||||
let dropped: Vec<web_sys::File> = dropped_files.iter().filter_map(|f| f.web_file().cloned()).collect();
|
||||
if !dropped.is_empty() {
|
||||
files::read_files(dropped, |paths| post(move |app| open_or_import(app, paths)));
|
||||
}
|
||||
self.app.raw_input_hook(ctx, raw_input);
|
||||
}
|
||||
|
||||
/// The browser host's logic tick: run queued UI actions, tick the shared app, poll API replies, flush Render
|
||||
/// Queue downloads, persist the session snapshot when enabled, publish GPU/frame-worker/disk-cache info and close
|
||||
/// the IME pass.
|
||||
fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
let actions = ACTIONS.with(|a| std::mem::take(&mut *a.borrow_mut()));
|
||||
for f in actions {
|
||||
f(&mut self.app);
|
||||
}
|
||||
// Files dropped on the page: read them, then open/import.
|
||||
let dropped: Vec<web_sys::File> = ctx.input(|i| i.raw.dropped_files.iter().filter_map(|f| f.web_file().cloned()).collect());
|
||||
if !dropped.is_empty() {
|
||||
files::read_files(dropped, |paths| post(move |app| open_or_import(app, paths)));
|
||||
}
|
||||
self.app.logic(ctx, frame);
|
||||
self.app.tick(ctx, frame);
|
||||
api::poll_replies();
|
||||
// Render Queue outputs written this frame (here or by a worker) → downloads.
|
||||
files::flush_downloads();
|
||||
// What a reload comes back to.
|
||||
if self.persist {
|
||||
persist::snapshot(&self.app.session, ctx.input(|i| i.time), 1.0);
|
||||
// The app's monotonic clock: `ctx.input(|i| i.time)` is stale in a logic-only tick.
|
||||
persist::snapshot(&self.app.session, self.app.now(), 1.0);
|
||||
}
|
||||
api::set_info("gpu", json!({"compositor": self.app.gpu_adapter(), "viewerOnGpu": self.app.viewer_on_gpu()}));
|
||||
api::set_info("frameWorkers", frames::stats());
|
||||
@@ -114,6 +126,7 @@ impl eframe::App for WebApp {
|
||||
let d = &self.app.session.prefs.disk;
|
||||
diskcache::configure(d.disk_cache_enabled, d.disk_cache_max_gb, storage::quota());
|
||||
api::set_info("diskCache", diskcache::stats());
|
||||
self.app.finish_ime_logic(ctx);
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
@@ -195,7 +208,9 @@ pub async fn start(canvas_id: String) -> Result<(), JsValue> {
|
||||
let home = query_flag("home");
|
||||
// Browser storage: settings, recent projects, imported media, saved projects, auto-saves and
|
||||
// the last session come back.
|
||||
let t0 = js_sys::Date::now();
|
||||
// Monotonic (performance.now) rather than the wall clock: a system-time jump must not turn
|
||||
// the storage-load report negative or huge.
|
||||
let t0 = web_time::Instant::now();
|
||||
let stored = match persist::load(&query_value("storage").unwrap_or_default()).await {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
@@ -203,7 +218,7 @@ pub async fn start(canvas_id: String) -> Result<(), JsValue> {
|
||||
0
|
||||
}
|
||||
};
|
||||
api::set_info("storage", json!({"backend": persist::backend(), "entries": stored, "loadMs": js_sys::Date::now() - t0}));
|
||||
api::set_info("storage", json!({"backend": persist::backend(), "entries": stored, "loadMs": t0.elapsed().as_secs_f64() * 1000.0}));
|
||||
audio::install_unlock();
|
||||
browse::restore();
|
||||
diskcache::init();
|
||||
@@ -214,6 +229,7 @@ pub async fn start(canvas_id: String) -> Result<(), JsValue> {
|
||||
canvas,
|
||||
eframe::WebOptions::default(),
|
||||
Box::new(move |cc| {
|
||||
EffectcraftApp::install_ime_reset_hooks(&cc.egui_ctx);
|
||||
CTX.with(|c| *c.borrow_mut() = Some(cc.egui_ctx.clone()));
|
||||
// The page owns this shared presentation device; worker devices retain their
|
||||
// separate owned-device handlers. No browser error-scope future is blocked on.
|
||||
|
||||
@@ -52,7 +52,7 @@ struct State {
|
||||
static STATE: LazyLock<Mutex<State>> = LazyLock::new(Mutex::default);
|
||||
|
||||
fn state() -> std::sync::MutexGuard<'static, State> {
|
||||
STATE.lock().unwrap_or_else(|e| e.into_inner())
|
||||
STATE.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// The web app's Media Browser.
|
||||
|
||||
@@ -48,14 +48,14 @@ pub fn list() -> Vec<(String, usize)> {
|
||||
|
||||
/// The next write of `path` goes to browser storage only (no download).
|
||||
pub fn save_quietly(path: &str) {
|
||||
QUIET.lock().unwrap_or_else(|e| e.into_inner()).push(path.to_string());
|
||||
QUIET.lock().unwrap_or_else(std::sync::PoisonError::into_inner).push(path.to_string());
|
||||
}
|
||||
|
||||
/// A rendered file (from the page's exporter or a worker): kept in the table unpersisted and
|
||||
/// downloaded at the next [`flush_downloads`].
|
||||
pub fn add_output(path: &str, data: Arc<[u8]>) {
|
||||
STORE.put_file(path, data, false);
|
||||
OUTPUTS.lock().unwrap_or_else(|e| e.into_inner()).push(path.to_string());
|
||||
OUTPUTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner).push(path.to_string());
|
||||
}
|
||||
|
||||
/// The table path for a picked/dropped file name.
|
||||
@@ -120,7 +120,7 @@ impl Services for WebServices {
|
||||
fn write_file(&self, path: &str, data: &[u8]) -> std::io::Result<()> {
|
||||
put(path, data.into());
|
||||
let quiet = {
|
||||
let mut q = QUIET.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut q = QUIET.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let n = q.len();
|
||||
q.retain(|p| p != path);
|
||||
q.len() != n
|
||||
@@ -184,7 +184,7 @@ pub fn export_sink() -> Arc<effectcraft_export::Sink> {
|
||||
|
||||
/// Download the render outputs written since the last call.
|
||||
pub fn flush_downloads() {
|
||||
let paths = std::mem::take(&mut *OUTPUTS.lock().unwrap_or_else(|e| e.into_inner()));
|
||||
let paths = std::mem::take(&mut *OUTPUTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner));
|
||||
let mut out: Vec<(String, Arc<[u8]>)> = paths.into_iter().filter_map(|p| get(&p).map(|d| (file_name(&p).to_string(), d))).collect();
|
||||
out.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
out.dedup_by(|a, b| a.0 == b.0);
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
//!
|
||||
//! | Desktop | Web |
|
||||
//! |---|---|
|
||||
//! | file system (`FsServices`, media reads, export writes) | a virtual file table ([`files`]) persisted to the Origin Private File System / IndexedDB ([`persist`], [`store`]) |
|
||||
//! | file system (`FsServices`, media reads, export writes) | a virtual file table (`files`) persisted to the Origin Private File System / IndexedDB (`persist`, [`store`]) |
|
||||
//! | config directory (settings, shortcuts, recent projects, auto-saves) | the same store ([`store::WebConfig`]) |
|
||||
//! | rfd file dialogs | `<input type=file>` pickers; drop files on the page |
|
||||
//! | Media Browser on the file system | browser storage and folders opened with the File System Access API ([`browse`]) |
|
||||
//! | Media Browser on the file system | browser storage and folders opened with the File System Access API (`browse`) |
|
||||
//! | written files (Save, Render Queue) | browser downloads (several render files as one `.zip`) |
|
||||
//! | frame render threads | frame workers, each with a project replica fed by diffs and its own WebGPU device ([`frames`]); `Frames::pump` on the UI thread without workers |
|
||||
//! | disk cache folder | frames in the Origin Private File System, written by the frame workers ([`diskcache`]) |
|
||||
//! | (none) | the storage manager: usage, quota, persistence, clearing ([`storage`]) |
|
||||
//! | Render Queue / analysis threads | Web Workers running their own engine instance ([`worker`]) |
|
||||
//! | cpal audio output | Web Audio: an AudioWorklet fed from the preview mixdown ([`audio`]) |
|
||||
//! | TCP control channel / MCP | `window.effectcraft` JavaScript API ([`api`]) |
|
||||
//! | frame render threads | frame workers, each with a project replica fed by diffs and its own WebGPU device (`frames`); `Frames::pump` on the UI thread without workers |
|
||||
//! | disk cache folder | frames in the Origin Private File System, written by the frame workers (`diskcache`) |
|
||||
//! | (none) | the storage manager: usage, quota, persistence, clearing (`storage`) |
|
||||
//! | Render Queue / analysis threads | Web Workers running their own engine instance (`worker`) |
|
||||
//! | cpal audio output | Web Audio: an AudioWorklet fed from the preview mixdown (`audio`) |
|
||||
//! | TCP control channel / MCP | `window.effectcraft` JavaScript API (`api`) |
|
||||
//!
|
||||
//! [`store`] is portable (unit-tested natively); everything else is `wasm32`-only.
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ impl Default for Store {
|
||||
|
||||
impl Store {
|
||||
pub fn lock(&self) -> std::sync::MutexGuard<'_, Mirror> {
|
||||
self.mirror.lock().unwrap_or_else(|e| e.into_inner())
|
||||
self.mirror.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn now(&self) -> f64 {
|
||||
|
||||
Reference in New Issue
Block a user