Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9e2b7c168 |
@@ -32,3 +32,5 @@ jobs:
|
||||
for source in pathlib.Path('web').glob('*.js'):
|
||||
subprocess.run(['node', '--input-type=module', '--check'], input=source.read_bytes(), check=True)
|
||||
PY
|
||||
- name: Check exclusive routing behavior
|
||||
run: node --experimental-default-type=module tests/test_exclusive_routes.mjs
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.6
|
||||
|
||||
- Follow KJNodes Get/Set routes and reroutes when deciding which exclusive input is enabled, including the setter's and real source node's bypass/mute states.
|
||||
- Support virtual routers using the current resolveVirtualOutput API, and safely reject routing cycles or missing setters.
|
||||
- Exclude inactive inputs from detached execution DTOs before frontend link resolution, avoiding missing-input errors when a virtual Get itself is bypassed; retain all editable/saved workflow wires.
|
||||
- Add dependency-free routing regression checks to CI and validate actual KJ Get/Set workflows with real CPU queue output pixels in both supported frontend versions/renderers.
|
||||
|
||||
## 0.5.5
|
||||
|
||||
- Bind the inline canvas, inpaint parameter panel and comparison widget width directly to the node's logical graph width. Cached screen-space measurements cannot shrink the widgets a second time at reduced graph zoom.
|
||||
|
||||
@@ -153,7 +153,8 @@ Both enabled, or neither connected/enabled, produces an error instead of silentl
|
||||
The node accepts conditioning, strings, images, masks, models, latents and other ComfyUI data types; both alternatives and the downstream socket should use the same data type.
|
||||
The model-free `examples/04-exclusive-switch.json` demonstrates two branches with two group presets.
|
||||
It has no switch widget. A small internal value is computed from the graph when queuing, including when a bypassed image/model transform would otherwise pass its input through.
|
||||
Place the selector outside the two controlled groups and connect each group's final node to it; any routing nodes between them should remain enabled.
|
||||
Place the selector outside the two controlled groups and connect each group's final node to it.
|
||||
KJNodes Get/Set routes and reroutes are supported: an ignored/muted Get, its corresponding Set, or the source behind those routing nodes disables that selector branch. Missing setters and virtual routing cycles also count as unavailable. Ordinary processing nodes remain independent active sources; the selector does not guess the meaning of arbitrary processing chains.
|
||||
The ignored branch is not requested by this selector; any separate preview/save outputs on that branch may still execute independently.
|
||||
For raw API prompts, omit the ignored input and omit the internal `active_inputs` field; exactly one of `input_a` / `input_b` must remain.
|
||||
|
||||
|
||||
+5
-4
@@ -1,10 +1,11 @@
|
||||
# Git versions and rollback
|
||||
|
||||
The current local version is **v0.5.5**. Both the installed plugin and the source repository retain local commits and tags. No GitHub push is part of this update.
|
||||
The current local version is **v0.5.6**. Both the installed plugin and the source repository retain local commits and tags. No GitHub push is part of this update.
|
||||
|
||||
| Tag | Contents |
|
||||
|---|---|
|
||||
| `v0.5.5` | Bind canvas/panel widths to node geometry and retain vertical resizing |
|
||||
| `v0.5.6` | Exclusive selector follows Get/Set and virtual routes correctly |
|
||||
| `v0.5.5` / `local-before-v0.5.6` | Bind canvas/panel widths to node geometry and retain vertical resizing |
|
||||
| `v0.5.4` / `local-before-v0.5.5` | Isolate Fit clicks and refresh editor assets on upgrade |
|
||||
| `v0.5.3` / `local-before-v0.5.4` | Fit beside Load image in the inline mask, plus the 5D VAE fix |
|
||||
| `v0.5.2` / `local-before-v0.5.3` | Single-frame 5D VAE support for image models, plus all previous features |
|
||||
@@ -17,13 +18,13 @@ To undo only this update, run in the plugin folder:
|
||||
|
||||
```powershell
|
||||
git status --short
|
||||
git switch --detach local-before-v0.5.5
|
||||
git switch --detach local-before-v0.5.6
|
||||
```
|
||||
|
||||
Return to this version with:
|
||||
|
||||
```powershell
|
||||
git switch --detach v0.5.5
|
||||
git switch --detach v0.5.6
|
||||
```
|
||||
|
||||
Restart ComfyUI and hard-refresh the browser (`Ctrl+F5`) after switching versions.
|
||||
|
||||
@@ -43,6 +43,10 @@ Environment: Windows, local ComfyUI 0.19.3, ComfyUI frontend 1.42.11, PyTorch 2.
|
||||
- 0.5.5: injecting a cached widget width equal to 35% of its node width reproduces the narrow-column shape on frontend 1.52.7 classic nodes. Before the fix, a 380px inline node had a 113px editor; afterwards its editor is 360px wide, with the remaining 20px reserved for native margins. Inpaint and comparison widgets recover their full widths as well.
|
||||
- 0.5.5: browser checks on frontend 1.42.11 and 1.52.7, classic nodes and Nodes 2.0, cover attempted stale width writes, resizing node widths to 800/450/1050px, independent height expansion, graph zoom at 0.3/0.6/1.15, Fit with mask preservation, and saved workflow reload. The simulated failure is covered; the remote server itself was not accessed.
|
||||
|
||||
- 0.5.6: inspected the supplied workflow read-only: selectors 532/533 are fed by KJ Get nodes p/p_0 and n/n_0, while the p_0/n_0 setters are bypassed. The previous direct-node check therefore incorrectly classified both branches as enabled.
|
||||
- 0.5.6: actual local KJNodes Set/Get frontend definitions were loaded only into isolated QA browsers (no KJ code is bundled). Across frontend 1.42.11/1.52.7 and classic/Nodes 2.0, 32 real CPU queue cases verify ignored setters, swapped states, ignored/muted real sources, ignored Get, explicit both/neither errors, and switching after workflow reload. Output image pixels match the selected branch; generating prompts does not change editable connections.
|
||||
- 0.5.6: dependency-free routing tests cover Get/Set, reroutes, current virtual-output APIs, mute/bypass, missing setters/inputs and cycles. All 29 backend CPU tests continue to pass.
|
||||
|
||||
The sampling path was also checked for noise-schedule continuity against the real `common_ksampler`/`KSampler` functions in the local ComfyUI; an actual checkpoint has not been loaded to test GPU image quality or every model family, so CPU/interface tests must not be treated as model-quality verification.
|
||||
|
||||
References used during implementation:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "adachi-inpaint-workbench"
|
||||
version = "0.5.5"
|
||||
version = "0.5.6"
|
||||
description = "Inline mask painting and WebUI-style inpainting nodes for ComfyUI"
|
||||
requires-python = ">=3.10"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { branchIsEnabled } from "../web/exclusive_routes.js";
|
||||
|
||||
const nodes = new Map(), links = new Map();
|
||||
const graph = {getNodeById: id => nodes.get(id), getLink: id => links.get(id)};
|
||||
let sequence = 0;
|
||||
function node(type, options = {}) {
|
||||
const value = {id: ++sequence, type, mode: 0, inputs: [], graph, ...options};
|
||||
nodes.set(value.id, value); return value;
|
||||
}
|
||||
function connect(source, target, name = "value") {
|
||||
const id = ++sequence;
|
||||
links.set(id, {origin_id: source.id, origin_slot: 0});
|
||||
target.inputs.push({name, link: id});
|
||||
}
|
||||
const a = node("CLIPTextEncode"), b = node("CLIPTextEncode");
|
||||
const sa = node("SetNode", {isVirtualNode: true}), sb = node("SetNode", {isVirtualNode: true});
|
||||
connect(a, sa); connect(b, sb);
|
||||
const ga = node("GetNode", {isVirtualNode: true, findSetter: () => sa});
|
||||
const gb = node("GetNode", {isVirtualNode: true, findSetter: () => sb});
|
||||
const switchNode = node("AdachiExclusiveSwitch");
|
||||
connect(ga, switchNode, "input_a"); connect(gb, switchNode, "input_b");
|
||||
const state = () => Number(branchIsEnabled(switchNode, "input_a")) | (Number(branchIsEnabled(switchNode, "input_b")) << 1);
|
||||
assert.equal(state(), 3, "both real branches enabled");
|
||||
sa.mode = 4; assert.equal(state(), 2, "ignored Set disables A despite enabled Get");
|
||||
sa.mode = 0; sb.mode = 4; assert.equal(state(), 1, "reverse ignored Set selects A");
|
||||
sb.mode = 0; a.mode = 4; assert.equal(state(), 2, "ignored encoder is found beyond Get/Set");
|
||||
a.mode = 0; b.mode = 2; assert.equal(state(), 1, "muted encoder is also disabled");
|
||||
b.mode = 0; ga.mode = 4; assert.equal(state(), 2, "ignored Get is disabled"); ga.mode = 0;
|
||||
const reroute = node("Reroute"); connect(a, reroute); sa.inputs = []; connect(reroute, sa);
|
||||
reroute.mode = 4; assert.equal(state(), 2, "ignored reroute behind Set is disabled");
|
||||
reroute.mode = 0; a.mode = 4; assert.equal(state(), 2, "enabled reroute follows ignored source"); a.mode = 0;
|
||||
sa.inputs = []; connect(ga, sa); assert.equal(state(), 2, "virtual routing cycle is disabled safely");
|
||||
ga.findSetter = () => null; assert.equal(state(), 2, "missing setter is disabled");
|
||||
const modern = node("VirtualRouter", {isVirtualNode: true, resolveVirtualOutput: () => ({node: a, slot: 0})});
|
||||
const routed = node("AdachiExclusiveSwitch"); connect(modern, routed, "input_a");
|
||||
assert.equal(branchIsEnabled(routed, "input_a"), true, "modern virtual output API is followed");
|
||||
a.mode = 4;
|
||||
assert.equal(branchIsEnabled(routed, "input_a"), false, "modern virtual output respects ignored source");
|
||||
assert.equal(branchIsEnabled({...switchNode, graph: null}, "input_a"), false);
|
||||
assert.equal(branchIsEnabled(switchNode, "not_connected"), false);
|
||||
console.log("Exclusive routing: Get/Set, bypass/mute, reroutes, cycles and missing inputs passed");
|
||||
@@ -0,0 +1,35 @@
|
||||
const disabled = node => node.mode === 2 || node.mode === 4;
|
||||
const inputLink = (graph, input) => input?.link == null ? null
|
||||
: graph.getLink?.(input.link) ?? graph.links?.[input.link];
|
||||
|
||||
/** Follow routing nodes without treating ordinary processing nodes as routers. */
|
||||
export function branchIsEnabled(node, name) {
|
||||
const graph = node.graph;
|
||||
if (!graph) return false;
|
||||
const seen = new Set();
|
||||
function visit(source, slot = 0) {
|
||||
if (!source || seen.has(source) || disabled(source)) return false;
|
||||
seen.add(source);
|
||||
const sourceGraph = source.graph || graph;
|
||||
const follow = link => link ? visit(sourceGraph.getNodeById(link.origin_id), link.origin_slot) : false;
|
||||
|
||||
// KJ Get nodes have no physical inputs. Follow their actual setter so
|
||||
// its ignored state is checked too; getInputLink alone skips the Set.
|
||||
if (source.isVirtualNode && typeof source.findSetter === "function") {
|
||||
return visit(source.findSetter(sourceGraph), slot);
|
||||
}
|
||||
if (source.type === "Reroute" || source.type === "SetNode") {
|
||||
return follow(inputLink(sourceGraph, source.inputs?.[slot] || source.inputs?.[0]));
|
||||
}
|
||||
if (source.isVirtualNode && typeof source.resolveVirtualOutput === "function") {
|
||||
const resolved = source.resolveVirtualOutput(slot);
|
||||
if (resolved?.node) return visit(resolved.node, resolved.slot ?? 0);
|
||||
}
|
||||
if (source.isVirtualNode && typeof source.getInputLink === "function") {
|
||||
return follow(source.getInputLink(slot));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const link = inputLink(graph, node.inputs?.find(input => input.name === name));
|
||||
return link ? visit(graph.getNodeById(link.origin_id), link.origin_slot) : false;
|
||||
}
|
||||
+17
-20
@@ -1,22 +1,5 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
function branchIsEnabled(node, name) {
|
||||
const graph = node.graph;
|
||||
let input = node.inputs?.find(input => input.name === name);
|
||||
const visited = new Set();
|
||||
while (graph && input?.link != null) {
|
||||
const link = graph.getLink?.(input.link) ?? graph.links?.[input.link];
|
||||
const source = link && graph.getNodeById(link.origin_id);
|
||||
if (!source || visited.has(source.id)) return false;
|
||||
visited.add(source.id);
|
||||
// A bypassed transform normally passes its own input through in ComfyUI.
|
||||
// For this exclusive selector the original branch is explicitly disabled.
|
||||
if (source.mode === 2 || source.mode === 4) return false;
|
||||
if (source.type !== "Reroute") return true;
|
||||
input = source.inputs?.[0];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
import { branchIsEnabled } from "./exclusive_routes.js?v=0.5.6";
|
||||
|
||||
app.registerExtension({
|
||||
name: "AdachiWorkbench.ExclusiveSwitch",
|
||||
@@ -32,8 +15,22 @@ app.registerExtension({
|
||||
widget.options ??= {};
|
||||
widget.options.hidden = true;
|
||||
widget.computeSize = () => [0, -4];
|
||||
widget.serializeValue = () => Number(branchIsEnabled(this, "input_a"))
|
||||
| (Number(branchIsEnabled(this, "input_b")) << 1);
|
||||
widget.serializeValue = executionNode => {
|
||||
const flags = Number(branchIsEnabled(this, "input_a"))
|
||||
| (Number(branchIsEnabled(this, "input_b")) << 1);
|
||||
// Recent frontends resolve ignored virtual Get nodes before
|
||||
// backend lazy execution, sometimes trying a nonexistent input.
|
||||
// Drop only inactive links in the detached execution DTO;
|
||||
// the editable graph and saved workflow keep all their wires.
|
||||
if (executionNode !== this && executionNode?.node === this) {
|
||||
for (const input of executionNode.inputs || []) {
|
||||
const bit = input.name === "input_a" ? 1 : input.name === "input_b" ? 2 : 0;
|
||||
if (bit && !(flags & bit) && "linkId" in input
|
||||
&& !this.inputs.includes(input)) input.linkId = null;
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
};
|
||||
}
|
||||
const internalSlot = this.inputs?.findIndex(input => input.name === "active_inputs");
|
||||
if (internalSlot >= 0) this.removeInput(internalSlot);
|
||||
|
||||
Reference in New Issue
Block a user