Reuse the decoded result and report silent adoption skips

Adopting a crop result previously downloaded the full-size image a second time
inside the crop layer. Pass the already-decoded image through, cutting one
decode round trip.

Adoption could also return without any feedback when the preview was still
loading, no target was selected, or the crop/layer state changed. Those cases now
show a visible message instead of failing silently.
This commit is contained in:
adachi
2026-10-17 13:57:03 +08:00
parent a5bd9624b8
commit da4a26c982
8 changed files with 27 additions and 11 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 0.7.2
- Adopting a result no longer decodes the full-size image a second time: the already-decoded image is reused for the crop layer, cutting one download/decode round trip.
- Show a visible message when adoption is skipped because the preview is still loading, no target is selected, or the crop/layer state changed, instead of failing silently.
- Bump the asset versions for the crop, comparison and result-adoption modules.
## 0.7.1
- Uploading a new source image now keeps the painted mask, undo/redo history and viewport, the same way adopting a result does; only the source pixels change. A first upload still starts with an empty mask.
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "adachi-inpaint-workbench"
version = "0.7.1"
version = "0.7.2"
description = "Inline mask painting and WebUI-style inpainting nodes for ComfyUI"
requires-python = ">=3.10"
license = { file = "LICENSE" }
+3 -1
View File
@@ -118,7 +118,9 @@ export class ImageCompareView {
async adopt() {
this.refreshTargets();
if (!this.ready || !this.targetSelect.value || this.busy) return;
if (this.busy) return;
if (!this.ready) { this.status(T("The result preview is still loading"), true); return; }
if (!this.targetSelect.value) { this.status(T("Select an inline mask to adopt into"), true); return; }
const target = this.targetSelect.value, result = this.results[this.index];
this.busy = true; this.updateButton(); this.status(T("Setting the result as the new source image…"));
try {
+1 -1
View File
@@ -1,6 +1,6 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { ImageCompareView } from "./compare.js?v=0.5.9";
import { ImageCompareView } from "./compare.js?v=0.7.2";
import { appendComparison } from "./compare_history.js?v=0.5.8";
import { routeWheelOutsideImage } from "./wheel_routing.js";
import { T } from "./i18n.js";
+3
View File
@@ -19,6 +19,9 @@ const ZH = {
"This result belongs to another crop; generate a result for the current region": "该结果来自其他选区,请为当前选区重新生成",
"Result dimensions must match the selected crop": "结果宽高必须与当前裁切区域一致",
"The crop changed while adopting the result": "采用过程中选区已改变,请重新生成结果",
"The result was not applied; the crop or layer state changed": "结果未应用,选区或图层状态已改变",
"The result preview is still loading": "结果预览仍在加载中",
"Select an inline mask to adopt into": "请选择要回填的内联蒙版",
"Painting tools and mask settings": "绘图工具与蒙版设置",
"Browser draft storage was full. The source was saved as an input file; the image, masks and layers are preserved.": "浏览器草稿空间不足,已将原图保存为图片文件;图像、蒙版和图层均已保留。",
"Editing mode": "编辑模式",
+7 -5
View File
@@ -1,7 +1,7 @@
import { CROP_PRESETS, cropAt } from "./crop_geometry.js?v=0.6.2";
import { T } from "./i18n.js";
import { syncCropDimensions } from "./parameter_bridge.js?v=0.6.4";
import { installResultAdoption } from "./result_adoption.js?v=0.6.4";
import { installResultAdoption } from "./result_adoption.js?v=0.7.2";
const fields = ["mask", "baseMask", "history", "future", "lastMask", "dirty"];
const canvas = (w, h) =>
@@ -505,7 +505,7 @@ export class InlineCropController {
this.sync();
this.editor.schedule();
}
async accept(record, {allowBusy = false} = {}) {
async accept(record, {allowBusy = false, image = null} = {}) {
if (
this.editor.destroyed ||
this.mode !== "crop" ||
@@ -520,7 +520,7 @@ export class InlineCropController {
this.resultTimer = setTimeout(() => {
const next = this.pendingResult;
this.pendingResult = null;
this.accept(next).catch((error) =>
this.accept(next, {allowBusy}).catch((error) =>
this.editor.status(error.message, true),
);
}, 50);
@@ -531,7 +531,9 @@ export class InlineCropController {
: record.image.filename;
if (name === this.widgets.layer_image.value) return;
const version = ++this.acceptVersion;
const image = await load(this.imageUrl(name));
// The adoption path already decoded this image once; reuse it instead of
// downloading and decoding the full-size result a second time.
const decoded = image || await load(this.imageUrl(name));
if (
this.editor.destroyed ||
version !== this.acceptVersion ||
@@ -539,7 +541,7 @@ export class InlineCropController {
this.mode !== "crop"
)
return;
this.layer = image;
this.layer = decoded;
this.widgets.layer_image.value = name;
if (!this.contexts.blend && this.target !== "blend")
await this.makeBlend(this.widgets.blend_data.value);
+1 -1
View File
@@ -4,7 +4,7 @@ import { InlineMaskEditor } from "./editor.js?v=0.7.1";
import { installInpaintWorkbench } from "./inpaint_node.js?v=0.6.1";
import { bindWidgetToNodeWidth } from "./widget_layout.js?v=0.5.5";
import { routeWheelOutsideImage } from "./wheel_routing.js";
import { InlineCropController } from "./inline_crop.js?v=0.6.9";
import { InlineCropController } from "./inline_crop.js?v=0.7.2";
import { canStoreDraft, prepareInlineDraft, syncInlineWidgets } from "./draft_guard.js?v=0.6.3";
import { T } from "./i18n.js";
import { removeLegacyFeedback } from "./legacy_feedback.js?v=0.6.4";
+5 -2
View File
@@ -40,9 +40,12 @@ export function installResultAdoption(node, crop) {
throw Error(T("The crop changed while adopting the result"));
}
const descriptor = {filename:uploaded.name, subfolder:uploaded.subfolder || "", type:"input"};
await crop.accept({job:region.job, image:descriptor}, {allowBusy:true});
await crop.accept({job:region.job, image:descriptor}, {allowBusy:true, image});
const name = descriptor.subfolder ? `${descriptor.subfolder}/${descriptor.filename}` : descriptor.filename;
if (crop.widgets.layer_image.value !== name) throw Error(T("The crop changed while adopting the result"));
if (crop.widgets.layer_image.value !== name) {
// accept() may have skipped silently (busy / changed job / duplicate).
throw Error(T("The result was not applied; the crop or layer state changed"));
}
editor.status("");
} finally {
URL.revokeObjectURL(objectUrl); crop.busy = false; crop.sync();