feat: Pin reference to slot RN in a reference's tile menu, beside Stop using as a reference

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-10-13 05:25:22 +03:00
co-authored by Claude Sonnet 5.5
parent 6634dd08e6
commit 03df8882f6
2 changed files with 36 additions and 1 deletions
+23
View File
@@ -63,3 +63,26 @@ test("a changed pin repaints the slot numbers on the tiles even when the list of
fire("project");
assert.deepEqual([flag("cat.png"), flag("dog.png")], ["R1", "R2"], "unpinned: back to list order");
});
test("a reference's tile menu pins it at the slot number it holds, for every scene, and takes the pin off again", async () => {
const doc = { fixed_references: {} };
const project = { selected: { id: "s1", references: ["m1", "m2"] }, project: doc, setScene: () => {}, edit: (fn) => fn(doc) };
const api = { media: async () => ({ media: [{ id: "m1", name: "cat.png", kind: "image" }, { id: "m2", name: "dog.png", kind: "image" }, { id: "m3", name: "owl.png", kind: "image" }] }) };
const host = document.createElement("div");
hub.setup({ host, app: { project, api, mediaPeek: {}, say: () => {}, on: () => () => {} } });
await new Promise((r) => setTimeout(r, 10));
const named = (n) => [...host.querySelectorAll(".cx-cell")].find((x) => x.textContent.includes(n));
const menuFor = (n) => {
document.querySelectorAll(".cx-popover").forEach((x) => x.remove());
named(n).dispatchEvent(new window.MouseEvent("contextmenu", { bubbles: true, cancelable: true }));
return [...document.querySelectorAll(".cx-menu-item")].map((b) => b.textContent);
};
assert.ok(menuFor("dog.png").includes("Pin reference to slot R2"));
assert.ok(!menuFor("owl.png").some((t) => t.includes("slot R")), "a picture that is no reference has nothing to pin");
menuFor("dog.png");
[...document.querySelectorAll(".cx-menu-item")].find((b) => b.textContent === "Pin reference to slot R2").click();
assert.deepEqual(doc.fixed_references, { m2: 2 });
assert.ok(menuFor("dog.png").includes("Unpin reference from slot R2"));
[...document.querySelectorAll(".cx-menu-item")].find((b) => b.textContent === "Unpin reference from slot R2").click();
assert.deepEqual(doc.fixed_references, {});
});
+13 -1
View File
@@ -32,12 +32,20 @@ export default {
p.setScene(sc.id, "source_image", item.id);
draw();
};
/** The slot number this picture holds in the selected scene, and whether it is pinned there: null when it is not one of its references. */
const slotOf = (id) => {
const sc = p.selected;
if (!sc) return null;
const plan = sceneReferences(sc, (p.project || {}).fixed_references), i = plan.ids.indexOf(id);
return i < 0 ? null : { n: referenceNumbers(plan.ids, plan.pins)[i], pinned: id in plan.pins };
};
const pinItem = (it) => { const s = slotOf(it.id); return s ? [{ id: "pinref", label: s.pinned ? `Unpin reference from slot R${s.n}` : `Pin reference to slot R${s.n}` }] : []; };
const extras = (it) => (app.mediaMenu || []).flatMap((f) => { try { return f(it, items.find((m) => m.id === it.id)) || []; } catch { return []; } }); // entries other features add to a tile's menu: { id, label, run }
const context = (it, e) => c.menu.context({ x: e.clientX, y: e.clientY, onPick: (id) => (act[id] ? act[id](it) : extras(it).find((x) => x.id === id)?.run()),
items: [{ id: "look", label: "Look at it on the monitor" }, ...(p.selected && p.selected.source_image === it.id ? [{ id: "unres", label: "Stop using as the selected scene's resolution source" }]
: [{ id: "res", label: "Use as the selected scene's resolution source", disabled: !p.selected || it.kind === "audio" || !items.some((m) => m.id === it.id && m.kind === "image") }]),
p.selected && (p.selected.references || []).includes(it.id) ? { id: "unref", label: "Stop using as a reference for the selected scene" }
: { id: "ref", label: "Use as a reference for the selected scene", disabled: !p.selected }, { id: "rename", label: "Rename…" },
: { id: "ref", label: "Use as a reference for the selected scene", disabled: !p.selected }, ...pinItem(it), { id: "rename", label: "Rename…" },
{ id: "export", label: "Save to your computer", disabled: !items.some((m) => m.id === it.id && m.kind !== "audio") }, ...extras(it).map(({ id, label, disabled }) => ({ id, label, disabled })), { separator: true }, { id: "delete", label: "Delete from the bin", danger: true }] });
/** Delete these from the bin, and nothing in this project may keep pointing at them. */
async function gone(ids) {
@@ -59,6 +67,10 @@ export default {
const act = {
look: (it) => peek(it), res: (it) => pick(it),
ref: (it) => p.setScene(p.selected.id, "references", [...(p.selected.references || []), it.id]),
pinref: (it) => { // pin at the number it holds now, for every scene, or take the pin off
const s = slotOf(it.id);
if (s) p.edit((pr) => { const next = { ...(pr.fixed_references || {}) }; if (s.pinned) delete next[it.id]; else next[it.id] = s.n; pr.fixed_references = next; return true; });
},
unref: (it) => p.setScene(p.selected.id, "references", (p.selected.references || []).filter((r) => r !== it.id)),
unres: () => p.setScene(p.selected.id, "source_image", ""),
rename: async (it) => {