Movie Editor: i2v anchors drop on the Plan, not the timeline

The timeline is the rendered result, so setting up a generation must not touch it.
Removed the clip-level anchor drop and the timeline track's image->anchored-scene
drop. Image media now drops onto Plan cards (sets that scene's i2v anchor) or onto
empty Plan space (creates a new anchored scene). Video clips (finished results)
still drop on the timeline track. Plan-only image filter; teal drop-target hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-19 10:01:21 +03:00
co-authored by Claude Opus 4.8
parent 4f562cf0ff
commit 93c8068b90
3 changed files with 29 additions and 18 deletions
+22 -1
View File
@@ -60,6 +60,24 @@
const l = el("label", "plan-field"); l.append(el("span", null, labelText)); l.append(control); return l;
}
// i2v anchors are dropped on the PLAN (never the timeline). Accept image media only.
function attachAnchorDrop(node, st, onImage) {
node.addEventListener("dragover", (e) => {
if (!e.dataTransfer.types.includes("application/funpack-media")) return;
e.preventDefault(); e.dataTransfer.dropEffect = "copy"; node.classList.add("drop-target");
});
node.addEventListener("dragleave", () => node.classList.remove("drop-target"));
node.addEventListener("drop", (e) => {
node.classList.remove("drop-target");
const id = e.dataTransfer.getData("application/funpack-media");
if (!id) return;
const asset = (st.mediaBin || []).find((m) => m.id === id);
if (!asset || asset.kind !== "image") return; // images = anchors; videos go to the timeline
e.preventDefault(); e.stopPropagation();
onImage(id);
});
}
function cardEditor(st, sc) {
const root = rootOf(sc);
const ed = el("div", "plan-editor");
@@ -152,6 +170,7 @@
chev.onclick = (e) => { e.stopPropagation(); expandedId = isOpen ? null : sc.id; S.selectScene(sc.id); render(st); };
c.append(chev);
}
if (!S.isVideoClip(sc)) attachAnchorDrop(c, st, (id) => S.assignMediaToScene(sc.id, id));
c.onclick = () => { if (st.selectedSceneId !== sc.id) S.selectScene(sc.id); };
return c;
}
@@ -166,9 +185,11 @@
const rowEl = el("div", "plan-row");
scenes.forEach((sc, i) => rowEl.append(cardEl(st, sc, i)));
const add = el("button", "plan-add", "+ Add scene");
add.title = "Add a new scene to the plan";
add.title = "Add a new scene to the plan · or drop an image here to anchor a new scene";
add.onclick = () => S.addScene();
rowEl.append(add);
// Drop an image on empty plan space → a new scene anchored to it (cards stopPropagation).
attachAnchorDrop(rowEl, st, (id) => S.addImageClip(id));
body.append(rowEl);
if (expandedId) {
const sc = S.scene(expandedId);
+1
View File
@@ -1426,6 +1426,7 @@ body.tour-mode { overflow: hidden; }
}
.plan-card:hover { border-color: color-mix(in srgb, var(--amber) 50%, var(--line)); }
.plan-card.sel, .plan-card.open { border-color: var(--amber); }
.plan-card.drop-target, .plan-row.drop-target { outline: 2px dashed var(--teal); outline-offset: -2px; border-radius: 8px; }
.plan-thumb { width: 40px; height: 40px; flex: 0 0 40px; border-radius: 6px; overflow: hidden; background: var(--ink-3); display: flex; align-items: center; justify-content: center; }
.plan-thumb img { width: 100%; height: 100%; object-fit: cover; }
.plan-thumb-glyph { font-family: var(--font-mono); font-size: 15px; color: var(--faint); }
+6 -17
View File
@@ -600,16 +600,9 @@
});
}
// accept a media asset dragged from the bin → sets this clip's source
clip.addEventListener("dragover", (e) => { if (e.dataTransfer.types.includes("application/funpack-media")) { e.preventDefault(); clip.classList.add("drop-target"); } });
clip.addEventListener("dragleave", () => clip.classList.remove("drop-target"));
clip.addEventListener("drop", (e) => {
const id = e.dataTransfer.getData("application/funpack-media");
clip.classList.remove("drop-target");
// Dropping on a clip sets ITS anchor; stop the event reaching the track-level
// drop (which would otherwise also create a brand-new clip).
if (id) { e.preventDefault(); e.stopPropagation(); S.assignMediaToScene(scene.id, id); }
});
// i2v anchors are set on the PLAN, not here — the timeline is the rendered result.
// (Drop an image onto a Plan card to anchor that scene.) Video clips still drop on the
// timeline track below.
// i2v anchor thumbnail (image + mixed + generated_frame)
const mref = anchorMediaRef(scene, p);
@@ -1878,14 +1871,10 @@
const asset = (st.mediaBin || []).find((m) => m.id === id);
if (asset?.kind === "video") {
e.preventDefault();
S.addVideoClip(id);
} else if (asset?.kind === "image") {
// CapCut-style: drop an image on empty timeline → default-length generative
// scene anchored to it. (Dropping ON a clip sets that clip's anchor instead;
// that handler stops propagation so this one doesn't also fire.)
e.preventDefault();
S.addImageClip(id);
S.addVideoClip(id); // a finished video is a result — it belongs on the timeline
}
// Images are i2v ANCHORS — drop them on a Plan card (or the Plan strip) instead, so
// the timeline (the rendered result) is never changed by setting up a generation.
});
lay.forEach(({ seg, o, d }) => {
if (seg.kind === "gap") return;