Movie Editor: removing a scene from the plan keeps its generated clip on the timeline
'Remove from plan' (plan card) now: if the gen-unit has a render, mark it excluded (so generation + prompt skip it everywhere — reuses every existing !excluded filter, no audit) plus removed_from_plan (hide from the plan strip, override the single export skip so the clip still renders, and skip the 'excluded' dim styling). Ungenerated scenes are still a plain delete. Inspector shows a 'Restore to plan' banner on an off-plan scene; timeline marks it with a subtle dashed outline. This retires ghosts for the plan-removal path: the removed scene persists as a real result clip in the cut, not a separate ghost. New Scene.removed_from_plan field (persisted). Backend tests green (62). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
07eaec5690
commit
ea20c018f8
@@ -537,6 +537,9 @@ class Scene:
|
||||
scene_archive: Optional[dict] = None
|
||||
# Editorial pause (black/silent) after this clip before the next timeline segment.
|
||||
gap_after_sec: float = 0.0
|
||||
# Removed from the PLAN but its generated clip stays on the TIMELINE. Also `excluded`,
|
||||
# so generation/prompt skip it everywhere; export keeps it because it has a render.
|
||||
removed_from_plan: bool = False
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict) -> "Scene":
|
||||
@@ -566,6 +569,7 @@ class Scene:
|
||||
source_dur=d.get("source_dur"),
|
||||
scene_archive=d.get("scene_archive"),
|
||||
gap_after_sec=float(d.get("gap_after_sec") or 0),
|
||||
removed_from_plan=bool(d.get("removed_from_plan", False)),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
|
||||
@@ -265,6 +265,15 @@
|
||||
: "This scene has editorial cuts — Generate regens the whole uncut scene."));
|
||||
}
|
||||
|
||||
if (root.removed_from_plan) {
|
||||
const banner = el("div", "insp-hint");
|
||||
banner.append(el("span", null, "Removed from plan — its generated clip stays on the timeline. "));
|
||||
const restore = el("button", "btn ghost tiny", "Restore to plan");
|
||||
restore.onclick = () => S.restoreToPlan(scene.id);
|
||||
banner.append(restore);
|
||||
body.append(banner);
|
||||
}
|
||||
|
||||
const ta = el("textarea"); ta.rows = 4; ta.value = root.text || ""; ta.placeholder = "Describe this scene…"; ta.dataset.k = "sc-text";
|
||||
ta.oninput = () => S.patchSceneQuiet(scene.id, { text: ta.value });
|
||||
body.append(field("Prompt", ta));
|
||||
|
||||
@@ -147,9 +147,9 @@
|
||||
const adv = el("button", "btn ghost tiny", "Advanced ↗");
|
||||
adv.title = "Open full scene settings (guides, effects, transitions) in the inspector";
|
||||
adv.onclick = () => { S.selectScene(sc.id); if (window.DockLayout) window.DockLayout.set({ settings: true }); };
|
||||
const rm = el("button", "btn ghost tiny danger", "Remove");
|
||||
rm.title = "Remove this scene from the plan";
|
||||
rm.onclick = () => { if (confirm("Remove this scene from the plan?")) { expandedId = null; S.removeScene(sc.id); } };
|
||||
const rm = el("button", "btn ghost tiny danger", "Remove from plan");
|
||||
rm.title = "Remove from the plan. A generated clip stays on the timeline; an ungenerated scene is deleted.";
|
||||
rm.onclick = () => { expandedId = null; S.removeFromPlan(sc.id); };
|
||||
actions.append(gen, mvL, mvR, adv, rm);
|
||||
ed.append(actions);
|
||||
return ed;
|
||||
@@ -187,7 +187,7 @@
|
||||
if (collapseBtn) collapseBtn.textContent = collapsed ? "Show" : "Hide";
|
||||
clear(body);
|
||||
if (collapsed || !st.project) return;
|
||||
const scenes = st.project.scenes || [];
|
||||
const scenes = (st.project.scenes || []).filter((s) => !s.removed_from_plan);
|
||||
const rowEl = el("div", "plan-row");
|
||||
scenes.forEach((sc, i) => rowEl.append(cardEl(st, sc, i)));
|
||||
const add = el("button", "plan-add", "+ Add scene");
|
||||
|
||||
@@ -1329,6 +1329,35 @@
|
||||
notify(); scheduleSave();
|
||||
}
|
||||
|
||||
// Remove a scene from the PLAN. If its gen-unit has a generated clip, KEEP that clip on the
|
||||
// timeline: mark the unit `excluded` (so generation + prompt skip it everywhere) and
|
||||
// `removed_from_plan` (hide from the plan strip; export keeps it because it has a render).
|
||||
// If nothing was generated, it's a plain delete.
|
||||
function removeFromPlan(id) {
|
||||
if (!state.project) return;
|
||||
const s = scene(id); if (!s) return;
|
||||
const uid = genUnitId(s);
|
||||
const unit = (state.project.scenes || []).filter((x) => genUnitId(x) === uid);
|
||||
const rendered = isVideoClip(s) || unit.some((x) => state.sceneRenders[x.id] && state.sceneRenders[x.id].media);
|
||||
if (!rendered) { removeScene(id); return; }
|
||||
_historyRecord();
|
||||
unit.forEach((x) => { x.excluded = true; x.removed_from_plan = true; });
|
||||
syncGlobalPromptFromTimeline();
|
||||
notify(); scheduleSave();
|
||||
}
|
||||
|
||||
// Bring a removed-from-plan scene back into the plan (re-enable generation).
|
||||
function restoreToPlan(id) {
|
||||
if (!state.project) return;
|
||||
const s = scene(id); if (!s) return;
|
||||
const uid = genUnitId(s);
|
||||
_historyRecord();
|
||||
(state.project.scenes || []).filter((x) => genUnitId(x) === uid)
|
||||
.forEach((x) => { x.excluded = false; x.removed_from_plan = false; });
|
||||
syncGlobalPromptFromTimeline();
|
||||
notify(); scheduleSave();
|
||||
}
|
||||
|
||||
function removeSelectedScenes() {
|
||||
if (!state.project) return;
|
||||
let ids = [...new Set(state.selectedSceneIds || [])].filter((sid) => scene(sid));
|
||||
@@ -3122,7 +3151,7 @@
|
||||
const p = state.project;
|
||||
const out = [];
|
||||
for (const sc of orderedTimelineScenes()) {
|
||||
if (sc.excluded) continue;
|
||||
if (sc.excluded && !sc.removed_from_plan) continue; // removed-from-plan clips stay in the cut
|
||||
const r = state.sceneRenders[sc.id];
|
||||
const fps = (sc.fps_mode !== "project" && sc.fps != null ? sc.fps : p.frame_rate) || 25;
|
||||
const tFrames = sc.transition_frames || 0;
|
||||
@@ -3719,7 +3748,7 @@
|
||||
get, set, subscribe, notify, init,
|
||||
scheduleSaveFromHistory, notifyHistoryState,
|
||||
refreshProjectList, loadProject, newProject, deleteProject, downloadProject, importProject,
|
||||
patchProject, patchProjectQuiet, patchScene, patchSceneQuiet, flushSave, selectScene, addScene, removeScene, removeSelectedScenes, dismissGhost, moveScene, moveSceneTo, moveTimelineClip, scene,
|
||||
patchProject, patchProjectQuiet, patchScene, patchSceneQuiet, flushSave, selectScene, addScene, removeScene, removeSelectedScenes, removeFromPlan, restoreToPlan, dismissGhost, moveScene, moveSceneTo, moveTimelineClip, scene,
|
||||
addVideoClip, addImageClip, convertToVideo, convertToScene, isVideoClip, isGenerativeScene,
|
||||
sceneCharacterIds, toggleSceneCharacter,
|
||||
genUnitId, isGenSubclip, genUnitRoot, genUnitSceneIds,
|
||||
|
||||
@@ -1452,4 +1452,5 @@ body.tour-mode { overflow: hidden; }
|
||||
.plan-edit-prompt { width: 100%; box-sizing: border-box; }
|
||||
.plan-editor-note { font-size: 11px; color: var(--faint); margin: 8px 0; }
|
||||
.plan-editor-actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.clip.off-plan { outline: 1px dashed color-mix(in srgb, var(--teal) 45%, transparent); outline-offset: -2px; }
|
||||
|
||||
|
||||
@@ -537,7 +537,8 @@
|
||||
const srcType = src.type || "empty";
|
||||
const clip = el("div", "clip" + clipSelClass(st, scene.id)
|
||||
+ (S.isVideoClip(scene) ? " clip-video" : "")
|
||||
+ (scene.excluded ? " excluded" : "")
|
||||
+ (scene.excluded && !scene.removed_from_plan ? " excluded" : "")
|
||||
+ (scene.removed_from_plan ? " off-plan" : "")
|
||||
+ (hasRender(st, scene.id) ? " rendered" : (!scene.excluded && S.isGenerativeScene(scene) ? " pending" : ""))
|
||||
+ (hasRender(st, scene.id) && S.renderIsStale?.(scene.id) ? " stale-render" : "")
|
||||
+ (unitCuts > 1 ? " gen-cut" : "") + (subclip ? " subclip" : "")
|
||||
|
||||
Reference in New Issue
Block a user