Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a38504ea7 |
@@ -34,3 +34,5 @@ jobs:
|
||||
PY
|
||||
- name: Check exclusive routing behavior
|
||||
run: node --experimental-default-type=module tests/test_exclusive_routes.mjs
|
||||
- name: Check comparison result history
|
||||
run: node --experimental-default-type=module tests/test_compare_history.mjs
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.7
|
||||
|
||||
- Accumulate comparison results across runs, select the newest result and preserve each result's matching original, including after workflow reload.
|
||||
- Keep comparison images permanently fitted and centered in the canvas; remove independent wheel zoom and middle/Space image panning.
|
||||
- Route wheel events over the full comparison node, including the picture, to ComfyUI workflow navigation.
|
||||
- Preserve hover comparison, the full-result view on pointer leave, batch selection and adoption with mask/history retention.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -104,11 +104,13 @@ Parameters driven by an external link disable the matching panel control; a modu
|
||||
## Comparison and chained inpainting
|
||||
|
||||
Add an **Image Compare** node and connect Inline Mask's `image` to `original` and the final full-size generated `image` to `result`, then run.
|
||||
With the pointer over the picture, the thin divider shows the generated result on the left and the original on the right; leaving the picture hides the divider and shows the full result, and the plain wheel zooms. Batch selection, dimensions and the fit button sit below the picture.
|
||||
With the pointer over the picture, the thin divider shows the generated result on the left and the original on the right; leaving the picture hides the divider and shows the full result, and the image always remains fitted and centered in its canvas. Scrolling over the comparison image navigates the ComfyUI workflow background using the current navigation settings; comparison images do not zoom or pan independently. Batch selection, dimensions and the fit button sit below the picture.
|
||||
When you are happy, click **Adopt this result**: the plugin uploads the selected result as the new source image and writes it back into the upstream inline editor while preserving the painted mask, current undo/redo history and viewport. Use **Clear** when you want to start a new mask; a normal **Load image** upload still starts a fresh mask.
|
||||
To preserve mask coordinates, an adopted result must match the source dimensions; a mismatched result is rejected without changing the current source or mask. The mask remains embedded in saved workflow data; command-level undo history is retained during adoption in the current session.
|
||||
If the original is routed through intermediate IMAGE nodes the plugin walks upstream to find the inline editor; when several candidates exist you can pick one explicitly under **Adopt into**.
|
||||
Adoption only happens when you click the button and never starts the next generation automatically; the `image` output still passes the whole result batch, so you can feed a Save Image node.
|
||||
The result list accumulates images across runs and selects the newest result automatically. Each result keeps its original from that run, including when adopting results for further inpainting. Saving the workflow also saves this history's preview references and selection.
|
||||
|
||||
Comparison previews live in ComfyUI's temp directory and may need a re-run after cleanup or restart; an adopted source image is stored in `input/adachi-inpaint/` and does not depend on temp previews.
|
||||
|
||||
## Group bypass presets
|
||||
|
||||
+5
-4
@@ -1,10 +1,11 @@
|
||||
# Git versions and rollback
|
||||
|
||||
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.
|
||||
The current local version is **v0.5.7**. Both the installed plugin and the source repository retain local commits and tags. This update is also pushed to the GitHub main branch.
|
||||
|
||||
| Tag | Contents |
|
||||
|---|---|
|
||||
| `v0.5.6` | Exclusive selector follows Get/Set and virtual routes correctly |
|
||||
| `v0.5.7` | Fixed comparison images; wheel navigates the workflow; retain result history across runs |
|
||||
| `v0.5.6` / `local-before-v0.5.7` | 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 |
|
||||
@@ -18,13 +19,13 @@ To undo only this update, run in the plugin folder:
|
||||
|
||||
```powershell
|
||||
git status --short
|
||||
git switch --detach local-before-v0.5.6
|
||||
git switch --detach local-before-v0.5.7
|
||||
```
|
||||
|
||||
Return to this version with:
|
||||
|
||||
```powershell
|
||||
git switch --detach v0.5.6
|
||||
git switch --detach v0.5.7
|
||||
```
|
||||
|
||||
Restart ComfyUI and hard-refresh the browser (`Ctrl+F5`) after switching versions.
|
||||
|
||||
@@ -47,6 +47,9 @@ Environment: Windows, local ComfyUI 0.19.3, ComfyUI frontend 1.42.11, PyTorch 2.
|
||||
- 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.
|
||||
|
||||
- 0.5.7: actual browser wheel events over a real queued comparison image change workflow navigation while the comparison transform stays fitted and centered. Verified on frontends 1.42.11 and 1.52.7 in classic nodes and Nodes 2.0, with left-result/right-original pixels, hover/leave behavior, batch selection, adoption preserving mask/history, workflow reload, rejected mismatched adoption and ordinary-upload mask reset.
|
||||
- 0.5.7: three real queued two-image batches accumulate 2, 4 and 6 results and select the newest result. After adoption, old results retain their old original and new results use the adopted original; all six results survive workflow reload in both frontend versions/renderers. Dependency-free tests additionally cover legacy batch mapping, empty outputs and immutable appending.
|
||||
|
||||
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.6"
|
||||
version = "0.5.7"
|
||||
description = "Inline mask painting and WebUI-style inpainting nodes for ComfyUI"
|
||||
requires-python = ">=3.10"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { appendComparison } from "../web/compare_history.js";
|
||||
|
||||
const first = {originals: ["source-a"], results: ["result-a"]};
|
||||
const second = appendComparison(first, {originals: ["source-b"], results: ["result-b", "result-c"]});
|
||||
assert.deepEqual(second, {originals: ["source-a", "source-b", "source-b"], results: ["result-a", "result-b", "result-c"]});
|
||||
assert.deepEqual(first, {originals: ["source-a"], results: ["result-a"]}, "Do not mutate saved history");
|
||||
const legacy = {originals: ["a", "b"], results: [1, 2, 3, 4]};
|
||||
assert.deepEqual(appendComparison(legacy, {originals: ["c"], results: [5]}), {
|
||||
originals: ["a", "a", "b", "b", "c"], results: [1, 2, 3, 4, 5],
|
||||
});
|
||||
assert.deepEqual(appendComparison(undefined, first), first);
|
||||
assert.deepEqual(appendComparison(first, {}), first);
|
||||
assert.deepEqual(appendComparison({}, {results: ["orphan"]}), {originals: [], results: []});
|
||||
assert.deepEqual(appendComparison(JSON.parse(JSON.stringify(second)), first).results, ["result-a", "result-b", "result-c", "result-a"]);
|
||||
console.log("Comparison history checks passed");
|
||||
+11
-40
@@ -1,5 +1,4 @@
|
||||
/* Original hover-comparison UI, MIT licensed. */
|
||||
import { registerImageWheel } from "./wheel_gestures.js";
|
||||
import { T } from "./i18n.js";
|
||||
const element = (tag, className, text) => {
|
||||
const node = document.createElement(tag);
|
||||
@@ -19,7 +18,7 @@ export class ImageCompareView {
|
||||
this.options = options;
|
||||
this.root = element("div", "dd-inpaint dd-comparer");
|
||||
this.originals = []; this.results = []; this.index = 0;
|
||||
this.zoom = 1; this.pan = { x: 0, y: 0 }; this.split = .5;
|
||||
this.split = .5;
|
||||
this.version = 0; this.ready = false;
|
||||
this.dimensionsLabel = element("span", "dd-dimensions");
|
||||
const toolbar = element("div", "dd-row dd-compare-toolbar");
|
||||
@@ -29,7 +28,7 @@ export class ImageCompareView {
|
||||
toolbar.append(this.resultSelect, this.dimensionsLabel);
|
||||
const spacer = element("span", "dd-spacer"); toolbar.append(spacer);
|
||||
const fit = element("button", "", T("Fit")); fit.type = "button";
|
||||
fit.onclick = () => { this.zoom = 1; this.pan = { x: 0, y: 0 }; this.schedule(); }; toolbar.append(fit);
|
||||
fit.onclick = event => { event.preventDefault(); event.stopPropagation(); this.schedule(); }; toolbar.append(fit);
|
||||
this.stage = element("div", "dd-stage dd-compare-stage");
|
||||
this.canvas = document.createElement("canvas"); this.canvas.tabIndex = 0;
|
||||
this.canvas.setAttribute("aria-label", T("Result left, original right; hover to compare"));
|
||||
@@ -46,34 +45,22 @@ export class ImageCompareView {
|
||||
adoption.append(this.targetSelect);
|
||||
this.adoptButton = element("button", "dd-primary", T("Adopt this result")); this.adoptButton.type = "button";
|
||||
this.adoptButton.onclick = () => this.adopt(); adoption.append(this.adoptButton);
|
||||
this.message = element("div", "dd-status", T("Move to compare · wheel to zoom · middle-drag to pan"));
|
||||
this.message = element("div", "dd-status", T("Result left, original right; hover to compare"));
|
||||
this.message.hidden = true; this.message.setAttribute("role", "alert");
|
||||
this.root.append(this.stage, toolbar, adoption, this.message);
|
||||
this.canvas.addEventListener("pointermove", event => this.move(event));
|
||||
this.canvas.addEventListener("pointerdown", event => {
|
||||
if (!this.ready) return;
|
||||
if (!this.ready || event.button !== 0) return;
|
||||
event.preventDefault(); event.stopPropagation(); this.canvas.focus({ preventScroll: true });
|
||||
if (event.button === 1 || this.space) {
|
||||
this.drag = { id: event.pointerId, x: event.clientX, y: event.clientY, pan: { ...this.pan } };
|
||||
this.canvas.setPointerCapture(event.pointerId);
|
||||
} else this.move(event);
|
||||
this.move(event);
|
||||
});
|
||||
const finish = event => {
|
||||
if (!this.drag || this.drag.id !== event.pointerId) return;
|
||||
const id = this.drag.id; this.drag = null;
|
||||
if (this.canvas.hasPointerCapture(id)) this.canvas.releasePointerCapture(id);
|
||||
};
|
||||
for (const name of ["pointerup", "pointercancel", "lostpointercapture"]) this.canvas.addEventListener(name, finish);
|
||||
this.stopWheelCapture = registerImageWheel(this.canvas, () => this.ready, event => this.wheel(event), options.onGraphWheel);
|
||||
this.canvas.addEventListener("wheel", event => this.wheel(event), { passive: false });
|
||||
this.canvas.addEventListener("keydown", event => {
|
||||
if (event.code === "Space") { event.preventDefault(); event.stopPropagation(); this.space = true; }
|
||||
else if (["ArrowLeft", "ArrowRight"].includes(event.key)) {
|
||||
if (["ArrowLeft", "ArrowRight"].includes(event.key)) {
|
||||
event.preventDefault(); event.stopPropagation();
|
||||
this.split = Math.max(0, Math.min(1, this.split + (event.key === "ArrowRight" ? .02 : -.02))); this.schedule();
|
||||
}
|
||||
});
|
||||
this.canvas.addEventListener("keyup", event => { if (event.code === "Space") this.space = false; });
|
||||
this.canvas.addEventListener("blur", () => { this.space = false; });
|
||||
this.observer = new ResizeObserver(() => this.schedule()); this.observer.observe(this.stage);
|
||||
this.refreshTargets(); this.updateButton();
|
||||
}
|
||||
@@ -140,36 +127,21 @@ export class ImageCompareView {
|
||||
|
||||
transform() {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
if (this.viewport?.width && this.viewport?.height && rect.width && rect.height) {
|
||||
this.pan.x *= rect.width / this.viewport.width;
|
||||
this.pan.y *= rect.height / this.viewport.height;
|
||||
}
|
||||
this.viewport = { width: rect.width, height: rect.height };
|
||||
const scale = this.before ? Math.min(rect.width / this.before.width, rect.height / this.before.height) * this.zoom : 1;
|
||||
return { width: rect.width, height: rect.height, scale, x: (rect.width - (this.before?.width || 0) * scale) / 2 + this.pan.x, y: (rect.height - (this.before?.height || 0) * scale) / 2 + this.pan.y };
|
||||
const scale = this.before ? Math.min(rect.width / this.before.width, rect.height / this.before.height) : 1;
|
||||
return { width: rect.width, height: rect.height, scale, x: (rect.width - (this.before?.width || 0) * scale) / 2, y: (rect.height - (this.before?.height || 0) * scale) / 2 };
|
||||
}
|
||||
|
||||
position(event) { const rect = this.canvas.getBoundingClientRect(), t = this.transform(); return { x: (event.clientX - rect.left - t.x) / t.scale, y: (event.clientY - rect.top - t.y) / t.scale }; }
|
||||
|
||||
wheel(event) {
|
||||
if (!this.ready) return;
|
||||
event.preventDefault(); event.stopPropagation();
|
||||
const before = this.position(event), rect = this.canvas.getBoundingClientRect();
|
||||
this.zoom = Math.max(.1, Math.min(32, this.zoom * Math.exp(-event.deltaY * .0015)));
|
||||
const t = this.transform();
|
||||
this.pan.x += event.clientX - rect.left - (t.x + before.x * t.scale);
|
||||
this.pan.y += event.clientY - rect.top - (t.y + before.y * t.scale);
|
||||
this.schedule();
|
||||
this.options.onGraphWheel?.(event);
|
||||
}
|
||||
|
||||
move(event) {
|
||||
if (!this.ready) return;
|
||||
this.hovered = true;
|
||||
if (this.drag) {
|
||||
event.preventDefault(); event.stopPropagation();
|
||||
this.pan.x = this.drag.pan.x + event.clientX - this.drag.x;
|
||||
this.pan.y = this.drag.pan.y + event.clientY - this.drag.y;
|
||||
} else this.split = Math.max(0, Math.min(1, this.position(event).x / this.before.width));
|
||||
this.split = Math.max(0, Math.min(1, this.position(event).x / this.before.width));
|
||||
this.schedule();
|
||||
}
|
||||
|
||||
@@ -197,7 +169,6 @@ export class ImageCompareView {
|
||||
|
||||
destroy() {
|
||||
this.destroyed = true; ++this.version; cancelAnimationFrame(this.frame); this.observer.disconnect();
|
||||
this.stopWheelCapture();
|
||||
this.before = this.after = null; this.canvas.width = this.canvas.height = 0; this.root.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Store one matching original per result, including batches from older workflows.
|
||||
export function appendComparison(previous, incoming) {
|
||||
const pairs = data => {
|
||||
const originals = Array.isArray(data?.originals) ? data.originals : [];
|
||||
const results = Array.isArray(data?.results) ? data.results : [];
|
||||
if (!originals.length || !results.length) return { originals: [], results: [] };
|
||||
const repetitions = results.length / originals.length;
|
||||
return {
|
||||
originals: results.map((_, index) => originals[Math.min(originals.length - 1, Math.floor(index / repetitions))]),
|
||||
results,
|
||||
};
|
||||
};
|
||||
const before = pairs(previous), next = pairs(incoming);
|
||||
return { originals: [...before.originals, ...next.originals], results: [...before.results, ...next.results] };
|
||||
}
|
||||
+9
-4
@@ -1,6 +1,7 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { ImageCompareView } from "./compare.js";
|
||||
import { ImageCompareView } from "./compare.js?v=0.5.7";
|
||||
import { appendComparison } from "./compare_history.js?v=0.5.7";
|
||||
import { routeWheelOutsideImage } from "./wheel_routing.js";
|
||||
import { T } from "./i18n.js";
|
||||
import { bindWidgetToNodeWidth } from "./widget_layout.js?v=0.5.5";
|
||||
@@ -51,7 +52,7 @@ app.registerExtension({
|
||||
},
|
||||
});
|
||||
node.adachiCompare = compare;
|
||||
const stopWheelRouting = routeWheelOutsideImage(compare.root, compare.canvas, () => compare.ready);
|
||||
const stopWheelRouting = routeWheelOutsideImage(compare.root, null, () => false);
|
||||
const widget = node.addDOMWidget("adachi_compare", "adachi_compare_view", compare.root, {
|
||||
serialize: false, hideOnZoom: false, getMinHeight: () => 420, getMaxHeight: () => 4096,
|
||||
});
|
||||
@@ -60,9 +61,13 @@ app.registerExtension({
|
||||
const executed = node.onExecuted;
|
||||
node.onExecuted = function (output) {
|
||||
const result = executed?.apply(this, arguments);
|
||||
const data = { originals: output?.adachi_originals || [], results: output?.adachi_results || [] };
|
||||
if (!output?.adachi_originals?.length || !output?.adachi_results?.length) return result;
|
||||
const data = appendComparison(node.properties.adachi_compare, {
|
||||
originals: output.adachi_originals, results: output.adachi_results,
|
||||
});
|
||||
node.properties.adachi_compare = data;
|
||||
compare.setData(data, node.properties.adachi_result_index || 0);
|
||||
node.properties.adachi_result_index = data.results.length - 1;
|
||||
compare.setData(data, node.properties.adachi_result_index);
|
||||
return result;
|
||||
};
|
||||
const configured = node.onConfigure;
|
||||
|
||||
Reference in New Issue
Block a user