Skip to content

Commit 2982b9c

Browse files
committed
Preserve saved auto-detect output settings across workflow reloads
When a workflow is reloaded, the auto-detect source dimensions may change technically (e.g. preview URLs, backend signatures) even though the source image itself did not change size. Previously, these technical changes could overwrite user-modified output dimensions. Introduce workflow-restore tracking state and a dedicated check that keeps the saved width/height when: - the workflow is being restored, and the newly detected dimensions match the restored source dimensions, or - the saved auto-detect properties match the newly detected dimensions. Hooks: - ResolutionMasterCanvas: added restoredAutoDetectDimensions and autoDetectWorkflowRestorePending state. - Node lifecycle: call prepareAutoDetectWorkflowRestore() during onConfigure so restore state is initialized at workflow load time. - Auto-detect methods: reset restore state when source changes or backend fallback occurs, and use shouldPreserveRestoredAutoDetectSettings() to decide whether to keep existing output. Tests: - Cover preserved output when technical signatures change but source size does not. - Cover clearing restore state when a genuinely different size is detected. - Cover onConfigure ordering in node lifecycle.
1 parent 5c1b3bc commit 2982b9c

5 files changed

Lines changed: 175 additions & 18 deletions

File tree

js/auto_detect/auto_detect_methods.js

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,62 @@ export const autoDetectMethods = {
101101
|| Math.round(Number(first.height) || 0) !== Math.round(Number(second.height) || 0);
102102
},
103103

104+
prepareAutoDetectWorkflowRestore() {
105+
this.restoredAutoDetectDimensions = null;
106+
this.autoDetectWorkflowRestorePending = !!this.node?.properties?.autoDetect;
107+
if (this.autoDetectWorkflowRestorePending) {
108+
log.debug('Prepared auto-detect workflow restore without recalculation', {
109+
nodeId: this.node?.id ?? null,
110+
savedWidth: this.widthWidget?.value ?? this.node?.properties?.valueX ?? null,
111+
savedHeight: this.heightWidget?.value ?? this.node?.properties?.valueY ?? null
112+
});
113+
}
114+
},
115+
116+
shouldPreserveRestoredAutoDetectSettings(dimensions) {
117+
if (!dimensions) return false;
118+
119+
const detectedWidth = Math.round(Number(dimensions.width) || 0);
120+
const detectedHeight = Math.round(Number(dimensions.height) || 0);
121+
122+
if (this.autoDetectWorkflowRestorePending) {
123+
this.autoDetectWorkflowRestorePending = false;
124+
this.restoredAutoDetectDimensions = {
125+
width: detectedWidth,
126+
height: detectedHeight
127+
};
128+
return true;
129+
}
130+
131+
const restored = this.restoredAutoDetectDimensions;
132+
133+
if (restored) {
134+
const matchesRestoredSource = restored.width === detectedWidth
135+
&& restored.height === detectedHeight;
136+
if (!matchesRestoredSource) {
137+
this.restoredAutoDetectDimensions = null;
138+
}
139+
return matchesRestoredSource;
140+
}
141+
142+
const props = this.node?.properties;
143+
const savedWidth = Math.round(Number(props?.autoDetectWidth) || 0);
144+
const savedHeight = Math.round(Number(props?.autoDetectHeight) || 0);
145+
const matchesSavedSource = !this.detectedDimensions
146+
&& savedWidth > 0
147+
&& savedHeight > 0
148+
&& savedWidth === detectedWidth
149+
&& savedHeight === detectedHeight;
150+
151+
if (matchesSavedSource) {
152+
this.restoredAutoDetectDimensions = {
153+
width: savedWidth,
154+
height: savedHeight
155+
};
156+
}
157+
return matchesSavedSource;
158+
},
159+
104160
shouldPreferBackendDimensions(frontendDimensions, backendDimensions, previousBackendTimestamp) {
105161
if (!frontendDimensions || !backendDimensions) return false;
106162
if (!this.haveDifferentDimensions(frontendDimensions, backendDimensions)) return false;
@@ -311,6 +367,8 @@ export const autoDetectMethods = {
311367

312368
const sourceNode = this.getConnectedSourceNode();
313369
if (shouldSuppressBackendFallback(sourceNode)) {
370+
this.autoDetectWorkflowRestorePending = false;
371+
this.restoredAutoDetectDimensions = null;
314372
this.setAutoDetectSource('frontend-empty');
315373
this.setRawAutoDetectDimensions(null);
316374
} else if (this.node.properties.autoDetectSource === 'frontend-empty') {
@@ -409,6 +467,8 @@ export const autoDetectMethods = {
409467
});
410468
}
411469
this.detectedDimensions = null;
470+
this.autoDetectWorkflowRestorePending = false;
471+
this.restoredAutoDetectDimensions = null;
412472
this.setAutoDetectSource('backend');
413473
this.setRawAutoDetectDimensions(null);
414474
return;
@@ -427,6 +487,8 @@ export const autoDetectMethods = {
427487
}
428488
this.clearLivePreviewPending();
429489
this.detectedDimensions = null;
490+
this.autoDetectWorkflowRestorePending = false;
491+
this.restoredAutoDetectDimensions = null;
430492
this.setAutoDetectSource('frontend-empty');
431493
this.setRawAutoDetectDimensions(null);
432494
return;
@@ -474,24 +536,21 @@ export const autoDetectMethods = {
474536
return;
475537
}
476538

477-
const props = node.properties;
478-
// Restore detected dimensions if the newly detected dimensions match the saved ones on first run.
479-
// This prevents overwriting user-modified/downsized settings upon reloading the workflow.
480-
if (!this.detectedDimensions && props && props.autoDetectWidth > 0 && props.autoDetectHeight > 0) {
481-
if (Math.round(Number(props.autoDetectWidth)) === dimensions.width &&
482-
Math.round(Number(props.autoDetectHeight)) === dimensions.height) {
483-
log.debug('Restoring auto-detect dimensions from saved properties without recalculation', {
484-
nodeId: node.id ?? null,
485-
width: dimensions.width,
486-
height: dimensions.height
487-
});
488-
this.detectedDimensions = dimensions;
489-
this.manuallySetByAutoFit = false;
490-
this.setAutoDetectSource(dimensions.source === 'frontend' ? 'frontend' : 'backend');
491-
this.setRawAutoDetectDimensions(dimensions);
492-
this.requestCanvasUpdate(true);
493-
return;
494-
}
539+
// Keep saved output settings while the restored source dimensions stay the same.
540+
// Preview URLs and frontend/backend signatures can change during workflow loading
541+
// without the source image itself changing size.
542+
if (this.shouldPreserveRestoredAutoDetectSettings(dimensions)) {
543+
log.debug('Keeping saved output settings for restored auto-detect dimensions', {
544+
nodeId: node.id ?? null,
545+
width: dimensions.width,
546+
height: dimensions.height
547+
});
548+
this.detectedDimensions = dimensions;
549+
this.manuallySetByAutoFit = false;
550+
this.setAutoDetectSource(dimensions.source === 'frontend' ? 'frontend' : 'backend');
551+
this.setRawAutoDetectDimensions(dimensions);
552+
this.requestCanvasUpdate(true);
553+
return;
495554
}
496555

497556
this.setAutoDetectSource(dimensions.source === 'frontend' ? 'frontend' : 'backend');

js/node/resolution_master_node_lifecycle.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,12 @@ export const nodeLifecycleMethods = {
885885
node.onPropertyChanged = function(property) {
886886
self.handlePropertyChange(property);
887887
};
888+
const origOnConfigure = node.onConfigure;
889+
node.onConfigure = function() {
890+
const result = origOnConfigure?.apply(this, arguments);
891+
self.prepareAutoDetectWorkflowRestore();
892+
return result;
893+
};
888894
const origOnConnectionsChange = node.onConnectionsChange;
889895
node.onConnectionsChange = function() {
890896
const result = origOnConnectionsChange?.apply(this, arguments);

js/resolution_master.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class ResolutionMasterCanvas {
5151
this.showTooltip = false;
5252
this.tooltipMousePos = null;
5353
this.detectedDimensions = null;
54+
this.restoredAutoDetectDimensions = null;
55+
this.autoDetectWorkflowRestorePending = false;
5456
this.lastBackendDimensionsTimestamp = null;
5557
this.autoDetectStartedAtMs = null;
5658
this.dimensionCheckInterval = null;

tests/js/auto_detect_methods.test.mjs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,88 @@ test("saved detected dimensions are restored without recalculating the workflow"
192192
});
193193

194194

195+
test("loading a workflow preserves saved output even when technical detected dimensions are stale", async () => {
196+
const sourceNode = {
197+
id: 9,
198+
imgs: [{ naturalWidth: 640, naturalHeight: 480, src: "workflow-preview.png" }],
199+
widgets: []
200+
};
201+
const state = createController(sourceNode);
202+
state.controller.node.properties.autoDetectWidth = 320;
203+
state.controller.node.properties.autoDetectHeight = 240;
204+
state.controller.widthWidget.value = 1024;
205+
state.controller.heightWidget.value = 768;
206+
state.controller.prepareAutoDetectWorkflowRestore();
207+
208+
await state.controller.checkForImageDimensions();
209+
210+
assert.equal(state.requests.length, 0);
211+
assert.equal(state.controller.widthWidget.value, 1024);
212+
assert.equal(state.controller.heightWidget.value, 768);
213+
assert.equal(state.controller.node.properties.autoDetectWidth, 640);
214+
assert.equal(state.controller.node.properties.autoDetectHeight, 480);
215+
assert.equal(state.controller.autoDetectWorkflowRestorePending, false);
216+
assert.deepEqual(state.controller.restoredAutoDetectDimensions, {
217+
width: 640,
218+
height: 480
219+
});
220+
});
221+
222+
223+
test("restored output settings survive same-size signature and source changes", async () => {
224+
const preview = {
225+
naturalWidth: 640,
226+
naturalHeight: 480,
227+
src: "saved-preview.png"
228+
};
229+
const sourceNode = {
230+
id: 9,
231+
imgs: [preview],
232+
widgets: []
233+
};
234+
const state = createController(sourceNode);
235+
state.controller.node.properties.autoDetectWidth = 640;
236+
state.controller.node.properties.autoDetectHeight = 480;
237+
238+
await state.controller.checkForImageDimensions();
239+
preview.src = "reloaded-preview.png";
240+
await state.controller.checkForImageDimensions();
241+
242+
assert.equal(state.requests.length, 0);
243+
assert.equal(state.controller.detectedDimensions.width, 640);
244+
assert.equal(state.controller.detectedDimensions.height, 480);
245+
assert.match(state.controller.detectedDimensions.signature, /reloaded-preview\.png/);
246+
247+
sourceNode.imgs = [];
248+
state.controller.getBackendDetectedDimensions = async () => ({
249+
width: 640,
250+
height: 480,
251+
source: "backend",
252+
timestamp: 2,
253+
signature: "backend:7:2:640x480"
254+
});
255+
await state.controller.checkForImageDimensions();
256+
257+
assert.equal(state.requests.length, 0);
258+
assert.equal(state.controller.node.properties.autoDetectSource, "backend");
259+
260+
state.controller.getBackendDetectedDimensions = async () => ({
261+
width: 800,
262+
height: 600,
263+
source: "backend",
264+
timestamp: 3,
265+
signature: "backend:7:3:800x600"
266+
});
267+
await state.controller.checkForImageDimensions();
268+
269+
assert.deepEqual(state.requests, [{
270+
action: "auto_detect",
271+
payload: { width: 800, height: 600 }
272+
}]);
273+
assert.equal(state.controller.restoredAutoDetectDimensions, null);
274+
});
275+
276+
195277
test("async source widget callbacks notify watchers and are restored on teardown", async () => {
196278
const state = createController();
197279
const originalCallback = async value => `loaded:${value}`;

tests/js/node_lifecycle.test.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ test("LiteGraph lifecycle hooks synchronize serialization and clean up on remova
162162
events.push("original-serialize");
163163
serialized.originalCalled = true;
164164
},
165+
onConfigure() {
166+
events.push("original-configure");
167+
},
165168
onRemoved() {
166169
events.push("original-remove");
167170
}
@@ -171,6 +174,7 @@ test("LiteGraph lifecycle hooks synchronize serialization and clean up on remova
171174
controller.installCanvasDragZoomBypass = () => {};
172175
controller.applyCompactSlotLabels = () => {};
173176
controller.installVueNodesCompatibilityWidget = () => {};
177+
controller.prepareAutoDetectWorkflowRestore = () => events.push("prepare-restore");
174178
controller.syncAutoDetectSourceState = () => events.push("sync-source");
175179
controller.syncBackendFallbackWidgets = () => events.push("sync-widgets");
176180
controller.stopAutoDetect = () => events.push("stop-auto-detect");
@@ -181,6 +185,7 @@ test("LiteGraph lifecycle hooks synchronize serialization and clean up on remova
181185
controller.initializeProperties();
182186

183187
controller.setupNode();
188+
controller.node.onConfigure({});
184189
const serialized = {};
185190
controller.node.onSerialize(serialized);
186191
controller.node.onRemoved();
@@ -189,6 +194,9 @@ test("LiteGraph lifecycle hooks synchronize serialization and clean up on remova
189194
assert.equal(heightWidget.hidden, true);
190195
assert.equal(serialized.originalCalled, true);
191196
assert.equal(serialized.properties.aux_id, "Azornes/Comfyui-Resolution-Master");
197+
const configureEventIndex = events.indexOf("original-configure");
198+
assert.notEqual(configureEventIndex, -1);
199+
assert.equal(events[configureEventIndex + 1], "prepare-restore");
192200
assert.deepEqual(events.slice(-6), [
193201
"sync-source",
194202
"sync-widgets",

0 commit comments

Comments
 (0)