Pinned references feed the model input of their number per kind; a video's own sound with no input is said; preview rechecks after the upscale wait; lint covers rgb/hsl; tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tw9fjX7o2YfeGQE18MEtPu
This commit is contained in:
@@ -24,11 +24,11 @@ test("element CSS has no literal pixel sizes", () => {
|
||||
assert.equal(found, 0, `${found} literal px values in element CSS: use a --sp-* step or a component size from tokens.css`);
|
||||
});
|
||||
|
||||
test("element CSS has no literal colours outside token definitions", () => {
|
||||
test("element CSS has no hex, rgb() or hsl() colours outside token definitions", () => {
|
||||
const bad = [];
|
||||
for (const f of sheets()) {
|
||||
const css = readFileSync(f, "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
for (const decl of css.split(/[;{}]/)) if (!/^\s*--/.test(decl) && /#[0-9a-fA-F]{3,8}\b/.test(decl)) bad.push(`${f}: ${decl.trim().slice(0, 60)}`);
|
||||
for (const decl of css.split(/[;{}]/)) if (!/^\s*--/.test(decl) && /#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla)\(/.test(decl)) bad.push(`${f}: ${decl.trim().slice(0, 60)}`);
|
||||
}
|
||||
assert.deepEqual(bad, [], "literal colours in element CSS: use a token from tokens.css");
|
||||
});
|
||||
|
||||
@@ -173,9 +173,9 @@ export default {
|
||||
/** One run for the Training window: the pipeline as it is, its LoRA loader pointed at `lora`, the picked scene (else the first one) and the project's own settings.
|
||||
* Nothing is put on the timeline; resolves to {image} or {error}. */
|
||||
app.runner.preview = async ({ lora, strength = 1 }) => {
|
||||
if (busy || held || g.state().phase === QUEUED || g.state().phase === RUNNING) return { error: "A run is already going: the preview waits for it." };
|
||||
if (!p.project) return { error: "Open a project first: the preview uses its settings." };
|
||||
if (await upscaling()) return { error: "An upscale model is running. It shares ComfyUI's model memory, so the preview waits for it." };
|
||||
if (busy || held || g.state().phase === QUEUED || g.state().phase === RUNNING) return { error: "A run is already going: the preview waits for it." }; // after the wait: a Generate may have started meanwhile
|
||||
if (!p.project) return { error: "Open a project first: the preview uses its settings." };
|
||||
const frozen = structuredClone(app.pipeline.slots() || []);
|
||||
const slot = frozen.find((s) => s.node === "FunUILoraLoader" && !s.bypassed) || frozen.find((s) => s.node === "FunUILoraLoader");
|
||||
if (!slot) return { error: "This pipeline has no LoRA loader to try the LoRA in. Add one in Models & Pipeline." };
|
||||
|
||||
@@ -74,9 +74,11 @@ export default {
|
||||
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 };
|
||||
if (i < 0) return null;
|
||||
const nums = referenceNumbers(plan.ids, plan.pins), kindOf = (x) => (items.find((m) => m.id === x) || {}).kind;
|
||||
return { n: nums[i], ord: plan.ids.filter((r, j) => nums[j] <= nums[i] && kindOf(r) === kindOf(id)).length, pinned: id in plan.pins }; // ord: the number the tile shows (per kind)
|
||||
};
|
||||
const pinItem = (it) => { const s = slotOf(it.id); return s && s.n <= 16 ? [{ id: "pinref", label: s.pinned ? `Unpin reference from slot R${s.n}` : `Pin reference to slot R${s.n}` }] : []; };
|
||||
const pinItem = (it) => { const s = slotOf(it.id); return s && s.n <= 16 ? [{ id: "pinref", label: s.pinned ? `Unpin reference from slot R${s.ord}` : `Pin reference to slot R${s.ord}` }] : []; };
|
||||
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 aspect ratio source" }]
|
||||
|
||||
@@ -26,7 +26,8 @@ export function wireReferences(mediaIds, slots, reservedSlotIds, pins, kindOf, r
|
||||
// before it is switched off; the rest take the free numbers in the order they were added.
|
||||
const kind = (id) => (kindOf && kindOf(id)) || "image";
|
||||
const numbers = referenceNumbers(mediaIds, pins);
|
||||
const seen = {}; // how many of each kind went in: a video or audio input is numbered per kind
|
||||
// A video or sound input is numbered per kind, in the order of the reference numbers (a pinned one keeps its place), not of the list.
|
||||
const rank = (i) => 1 + mediaIds.filter((id, j) => j !== i && kind(id) === kind(mediaIds[i]) && numbers[j] < numbers[i]).length;
|
||||
mediaIds.forEach((mediaId, i) => {
|
||||
const at = `assets.reference_${numbers[i]}`;
|
||||
const slot = pool.find((s) => (s.roles || []).some((r) => r.at === at));
|
||||
@@ -37,8 +38,8 @@ export function wireReferences(mediaIds, slots, reservedSlotIds, pins, kindOf, r
|
||||
const role = slot.roles.find((r) => r.at === at);
|
||||
const k = kind(mediaId);
|
||||
const route = routeOf && routeOf(k, slot, role);
|
||||
if (k !== "image" && !route) { unwired += 1; left.push({ id: mediaId, at, why: `this pipeline has no input for a ${k}` }); return; }
|
||||
const n = seen[k] = (seen[k] || 0) + 1;
|
||||
if (k !== "image" && !route) { unwired += 1; left.push({ id: mediaId, at, why: `this pipeline has no input for ${/^[aeiou]/.test(k) ? "an" : "a"} ${k}` }); return; }
|
||||
const n = rank(i);
|
||||
if (route && n > route.names.length) { unwired += 1; left.push({ id: mediaId, at, why: `the model takes only ${route.names.length} ${k}${route.names.length > 1 ? "s" : ""}` }); return; }
|
||||
const wire = route ? { slot: route.slot, input: route.names[n - 1], output: route.output } : role.wireTo && { ...role.wireTo, output: 0 };
|
||||
// A destination can hold one value. Two references claiming the SAME slot or the SAME wire target are
|
||||
@@ -52,6 +53,7 @@ export function wireReferences(mediaIds, slots, reservedSlotIds, pins, kindOf, r
|
||||
// A video with a soundtrack also feeds the input paired with its own number (video 1's sound is audio 1).
|
||||
const track = k === "video" && hasSound && hasSound(mediaId) && routeOf && routeOf("videosound", slot, role);
|
||||
if (track && track.names[n - 1]) overrides[track.slot] = { ...overrides[track.slot], [track.names[n - 1]]: [slot.id, track.output] };
|
||||
else if (track) { unwired += 1; left.push({ id: mediaId, at, why: "the model has no free input for this video's own sound" }); }
|
||||
});
|
||||
return { overrides, unwired, left };
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ test("a dialog on screen is reported, and a landmark is found by name", () => {
|
||||
assert.equal(dom.landmark("menubar").className, "cx-frame-head");
|
||||
assert.equal(dom.landmark("zone:1"), document.querySelectorAll("section.cx-zone")[1]);
|
||||
assert.equal(dom.landmark("button:▶ Generate").tagName, "BUTTON");
|
||||
assert.equal(dom.landmark("button:Generating…|▶ Generate").tagName, "BUTTON"); // "a|b": either label
|
||||
document.querySelector("button").textContent = "Generating…";
|
||||
assert.equal(dom.landmark("button:▶ Generate|Generating…").tagName, "BUTTON");
|
||||
assert.equal(dom.landmark("button:▶ Generate"), null);
|
||||
assert.equal(dom.landmark("zone:9"), null);
|
||||
assert.equal(dom.landmark("nonsense"), null);
|
||||
});
|
||||
|
||||
@@ -188,3 +188,24 @@ test("fifteen references of mixed kinds all find an input: 9 pictures, 3 videos
|
||||
assert.deepEqual(overrides.r2v["ref_video_audios.ref_video_audio_2"], ["s12", 2]);
|
||||
assert.deepEqual(overrides.r2v["ref_audios.ref_audio_2"], ["s15", 2]);
|
||||
});
|
||||
|
||||
test("a pinned reference feeds the model input of its number, per kind, not of its place in the list", () => {
|
||||
const slot = (n) => ({ id: `ref_source_${n}`, node: "Loader", roles: [{ at: `assets.reference_${n}`, input: "media_id", wireTo: { slot: "r2v", input: `ref_images.ref_image_${n - 1}` } }] });
|
||||
const kinds = { pic1: "image", pic2: "image", snd1: "audio", snd2: "audio" };
|
||||
const routeOf = (k, s, role) => routeFor(k, R2V, LOADER, role.wireTo.slot);
|
||||
// pic1 is pinned to number 2, so it is listed first but is the SECOND picture; the same for the sound pinned to 2
|
||||
const { overrides, unwired } = wireReferences(["pic1", "snd1", "pic2", "snd2"], [slot(1), slot(2), slot(3), slot(4)], [], { pic1: 2, snd1: 4 }, (id) => kinds[id], routeOf);
|
||||
assert.equal(unwired, 0);
|
||||
assert.equal(overrides.r2v["ref_images.ref_image_1"][0], "ref_source_2"); // pic1 (number 2) is picture 2
|
||||
assert.equal(overrides.r2v["ref_images.ref_image_0"][0], "ref_source_1"); // pic2 takes the free number 1
|
||||
assert.equal(overrides.r2v["ref_audios.ref_audio_1"][0], "ref_source_4"); // snd1 (number 4) is sound 2
|
||||
assert.equal(overrides.r2v["ref_audios.ref_audio_0"][0], "ref_source_3");
|
||||
});
|
||||
|
||||
test("a video's own sound with no free input to go to is said, not dropped", () => {
|
||||
const slot = { id: "ref_source_1", node: "Loader", roles: [{ at: "assets.reference_1", input: "media_id", wireTo: { slot: "r2v", input: "x" } }] };
|
||||
const routeOf = (k, s, role) => (k === "video" ? { slot: "r2v", names: ["v0"], output: 1 } : k === "videosound" ? { slot: "r2v", names: [], output: 2 } : null);
|
||||
const r = wireReferences(["vid"], [slot], [], {}, () => "video", routeOf, () => true);
|
||||
assert.equal(r.unwired, 1);
|
||||
assert.match(r.left[0].why, /video's own sound/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user