From da4a26c9822d40a2bf04662d379333687d0902cd Mon Sep 17 00:00:00 2001 From: adachi Date: Sat, 17 Oct 2026 13:57:03 +0800 Subject: [PATCH] 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. --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- web/compare.js | 4 +++- web/compare_node.js | 2 +- web/i18n.js | 3 +++ web/inline_crop.js | 12 +++++++----- web/inline_inpaint.js | 2 +- web/result_adoption.js | 7 +++++-- 8 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adb1c39..37150a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/pyproject.toml b/pyproject.toml index 0e028b3..5ca66ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/web/compare.js b/web/compare.js index b15d0e0..9216168 100644 --- a/web/compare.js +++ b/web/compare.js @@ -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 { diff --git a/web/compare_node.js b/web/compare_node.js index f33c1cc..b88b152 100644 --- a/web/compare_node.js +++ b/web/compare_node.js @@ -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"; diff --git a/web/i18n.js b/web/i18n.js index 2672da8..d8ed49f 100644 --- a/web/i18n.js +++ b/web/i18n.js @@ -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": "编辑模式", diff --git a/web/inline_crop.js b/web/inline_crop.js index f2f0b03..f3ae142 100644 --- a/web/inline_crop.js +++ b/web/inline_crop.js @@ -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); diff --git a/web/inline_inpaint.js b/web/inline_inpaint.js index 8cbf979..7e0e7ae 100644 --- a/web/inline_inpaint.js +++ b/web/inline_inpaint.js @@ -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"; diff --git a/web/result_adoption.js b/web/result_adoption.js index a866e4d..2300445 100644 --- a/web/result_adoption.js +++ b/web/result_adoption.js @@ -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();