Merge main and preserve Startup script tasks across project changes

This commit is contained in:
dotCooCoo
2026-10-27 11:41:43 -07:00
628 changed files with 54760 additions and 43678 deletions
+25 -9
View File
@@ -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.
+1 -1
View File
@@ -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.
+4 -4
View File
@@ -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);
+8 -8
View File
@@ -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.
+1 -1
View File
@@ -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 {