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:
co-authored by
Claude Opus 4.8
parent
330cb1a9e4
commit
44447baa40
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user