Movie Editor: don't rebuild the editor on generation progress ticks

A long generation polled progress every ~700ms via set()->notify(), rebuilding the whole
editor (inspector/timeline/player) each time — so saving rendered frames to the bin or
adding effects got interrupted constantly. Progress ticks now use updateGenProgress(),
which mutates state.gen quietly and fires a 'funpack-gen-progress' DOM event; the player
updates the readout/progress bar in place. Full notify() is reserved for state transitions
(queued/done/error), so the editor stays interactive throughout a run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-08 01:39:53 +03:00
co-authored by Claude Opus 4.8
parent 330cb1a9e4
commit 44447baa40
2 changed files with 36 additions and 14 deletions
+24 -11
View File
@@ -257,6 +257,20 @@
let _fpsCur = 25;
let _tcEl = null; // timecode span (updated live, not rebuilt)
let _playBtnEl = null; // play/pause button (glyph updated live, not rebuilt)
let _genMsgEl = null, _genBarEl = null, _genFillEl = null; // gen readout, updated live
// Update the generation readout in place on progress ticks (no store re-render, so the
// editor stays interactive — frames can be saved, effects added — while a job runs).
window.addEventListener("funpack-gen-progress", (e) => {
const g = (e && e.detail) || {};
if (_genMsgEl && g.msg != null) _genMsgEl.textContent = g.msg || g.state || "";
if (_genFillEl && _genBarEl) {
if (g.maxStep > 0) {
_genBarEl.style.visibility = "";
_genFillEl.style.width = Math.min(100, Math.round((g.step / g.maxStep) * 100)) + "%";
}
}
});
// Lightweight per-tick update: never rebuilds the buttons (rebuilding mid-click ate the
// mousedown/mouseup, so play/pause/stop stopped responding during playback).
@@ -404,22 +418,21 @@
empty.append(el("div", "pj-meta", "Generate from the menu or timeline"));
canvas.append(empty);
}
// generation progress overlay (visible even when there's already media)
// generation progress overlay (visible even when there's already media). Built once per
// store render; live progress ticks update it in place (see the gen-progress listener),
// so the rest of the editor isn't rebuilt while a generation runs.
_genMsgEl = _genBarEl = _genFillEl = null;
if (["queuing", "running", "pending", "error"].includes(gen.state)) {
const busy = gen.state !== "error";
const ro = el("div", "gen-readout" + (gen.state === "error" ? " error" : ""));
ro.append(el("span", "pulse"));
ro.append(el("span", null, gen.msg || gen.state));
// step progress bar
if (busy && gen.maxStep > 0) {
const bar = el("div", "gen-bar");
const fill = el("div", "gen-bar-fill");
fill.style.width = Math.min(100, Math.round((gen.step / gen.maxStep) * 100)) + "%";
bar.append(fill);
ro.append(bar);
}
// interrupt
_genMsgEl = el("span", null, gen.msg || gen.state); ro.append(_genMsgEl);
if (busy) {
const bar = el("div", "gen-bar"); _genBarEl = bar;
_genFillEl = el("div", "gen-bar-fill");
_genFillEl.style.width = gen.maxStep > 0 ? Math.min(100, Math.round((gen.step / gen.maxStep) * 100)) + "%" : "0%";
if (!(gen.maxStep > 0)) bar.style.visibility = "hidden";
bar.append(_genFillEl); ro.append(bar);
const stop = el("button", "gen-interrupt", "■ Interrupt");
stop.title = "Stop the current generation";
stop.onclick = () => S.interrupt();
+12 -3
View File
@@ -28,6 +28,15 @@
function notify() { listeners.forEach((fn) => { try { fn(state); } catch (e) { console.error(e); } }); }
function subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); }
function set(patch) { Object.assign(state, patch); notify(); }
// Update generation progress WITHOUT a full notify(): a store notify rebuilds the whole
// editor (inspector/timeline/player), which during a long generation kept interrupting the
// user (couldn't save frames to the bin or add effects). Progress ticks instead mutate
// state.gen quietly and fire a DOM event the player listens to, updating only the readout.
function updateGenProgress(patch) {
Object.assign(state.gen, patch);
try { window.dispatchEvent(new CustomEvent("funpack-gen-progress", { detail: state.gen })); } catch (_) {}
}
function get() { return state; }
// ── project lifecycle ──────────────────────────────────────────────────────
@@ -388,7 +397,7 @@
// "Interrupted" and the run loop stops.
async function interrupt() {
_interrupted = true;
set({ gen: { ...state.gen, msg: "Interrupting…" } });
updateGenProgress({ msg: "Interrupting…" });
try { await API.interrupt(); } catch (_) {}
}
@@ -445,7 +454,7 @@
if (_interrupted) return;
try {
const pr = await API.progress();
if (pr && pr.max > 0) set({ gen: { ...state.gen, step: pr.value, maxStep: pr.max } });
if (pr && pr.max > 0) updateGenProgress({ step: pr.value, maxStep: pr.max });
} catch (_) {}
}, 700);
pollTimer = setInterval(async () => {
@@ -478,7 +487,7 @@
return;
}
const step = (state.gen.maxStep > 0) ? ` · step ${state.gen.step}/${state.gen.maxStep}` : "";
set({ gen: { ...state.gen, state: s.state, msg: `${prefix} ${_elapsed()}${step}` } });
updateGenProgress({ state: s.state, msg: `${prefix} ${_elapsed()}${step}` });
}
} catch (e) {
_clearGenTimers();