diff --git a/CHANGELOG.md b/CHANGELOG.md index 99fbe1e..3a29013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.6.5 + +- Keep crop size presets, width/height swap and custom dimensions available after locking a region. Changing dimensions automatically unlocks it for a new selection. +- Merge an adopted layer before changing the region, and retain the old selection if merging fails. + ## 0.6.4 - Use a pure black dashed crop outline and automatically synchronize connected inpaint resolution when locking a crop. Linked resolution inputs remain under their upstream node's control. diff --git a/README.md b/README.md index cbfd26c..c30a25d 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Upload, drop or paste a source image and paint on **Inline Mask** itself. Mask s Choose **Mask** or **Crop & Blend** at the upper right of Inline Mask. Both modes receive images through **Adopt this result** in Image Compare; Inline Mask has no result input socket. -In Crop & Blend mode, choose one of six 64-aligned presets spanning 512–2048 pixels per side, including 1024 × 1536 (or custom dimensions in multiples of 64). The dashed rectangle follows the pointer by its upper-left corner; left-click locks it. The outside is darkened. A new region starts with an empty repaint mask. Paint inside the rectangle; brush and shape operations stay within its bounds. Use **◌ repaint mask** to return to repaint-mask editing. The position stays fixed for repeated passes. The outline is a pure black dashed rectangle, and locking it automatically fills the connected Inpaint node's width and height from the crop, using the same rules as Use source size. +In Crop & Blend mode, choose one of six 64-aligned presets spanning 512–2048 pixels per side, including 1024 × 1536 (or custom dimensions in multiples of 64). The dashed rectangle follows the pointer by its upper-left corner; left-click locks it. The outside is darkened. A new region starts with an empty repaint mask. Paint inside the rectangle; brush and shape operations stay within its bounds. Use **◌ repaint mask** to return to repaint-mask editing. The position stays fixed for repeated passes. Size presets remain editable while locked; changing the preset, orientation or custom dimensions cancels the current selection and returns to pointer placement, merging any adopted layer first. The outline is a pure black dashed rectangle, and locking it automatically fills the connected Inpaint node's width and height from the crop, using the same rules as Use source size. | Connection | Purpose | | --- | --- | diff --git a/README.zh-CN.md b/README.zh-CN.md index 64dd129..f37a59b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -62,7 +62,7 @@ 在内联蒙版右上角选择**蒙版**或**裁切融合**。两种模式都通过图像对比节点的**采用此结果**回传图片;内联蒙版不再提供结果输入口。 -裁切融合模式保留六组常用预设,宽高均为 64 的倍数,范围 512~2048(含两端);可用交换按钮切换横竖方向,自定义尺寸按 64 像素递增。虚线框的左上角跟随鼠标,左键点击锁定,框外压暗。新选区的蒙版为空,需要自己在框内涂抹;画笔和形状操作限制在选区内,点击 **◌ 重绘蒙版**可返回编辑重绘范围。选区框为纯黑虚线;点击锁定时,相连 Inpaint 节点会自动填写裁片宽高,等同于点一次「使用原图宽高」。选区位置固定,支持反复重绘同一处。 +裁切融合模式保留六组常用预设,宽高均为 64 的倍数,范围 512~2048(含两端);可用交换按钮切换横竖方向,自定义尺寸按 64 像素递增。虚线框的左上角跟随鼠标,左键点击锁定,框外压暗。新选区的蒙版为空,需要自己在框内涂抹;画笔和形状操作限制在选区内,点击 **◌ 重绘蒙版**可返回编辑重绘范围。选区框为纯黑虚线;点击锁定时,相连 Inpaint 节点会自动填写裁片宽高,等同于点一次「使用原图宽高」。选区位置固定,支持反复重绘同一处。选定后仍可切换预设、交换横竖或修改自定义宽高;修改会自动取消当前选区,重新跟随鼠标定位,已有采用图层会先合并保留。 | 连线 | 用途 | | --- | --- | diff --git a/ROLLBACK.md b/ROLLBACK.md index 92bf5bc..25fc318 100644 --- a/ROLLBACK.md +++ b/ROLLBACK.md @@ -1,11 +1,13 @@ # Git versions and rollback -The current version is **v0.6.4**. Git history lives in the source repository. The installed custom-node folder may be a ZIP/file copy without its own `.git`; do not run rollback commands in ComfyUI's parent repository. +The current version is **v0.6.5**. Git history lives in the source repository. The installed custom-node folder may be a ZIP/file copy without its own `.git`; do not run rollback commands in ComfyUI's parent repository. Before installing, the installer saves the previous plugin directory under `ComfyUI/user/adachi-workbench-backups/`. Restore that ZIP to roll back a copied installation. For a Git clone, use the tags below. | Tag | Contents | |---|---| +| `v0.6.5` | Change crop dimensions while selected, automatically return to selection | +| `local-before-v0.6.5` | Source before editable locked-crop dimensions | | `v0.6.4` | Black crop frame, automatic dimensions and comparison-only result adoption | | `local-before-v0.6.4` | Source before the comparison-adoption update, including v0.6.3 draft preflight fixes | | `v0.6.2` | Draft recovery, collapsible controls, retired selection tools and revised crop behavior | @@ -33,13 +35,13 @@ To undo only this update, run in the plugin folder: ```powershell git status --short -git switch --detach local-before-v0.6.4 +git switch --detach local-before-v0.6.5 ``` Return to this version with: ```powershell -git switch --detach v0.6.4 +git switch --detach v0.6.5 ``` Restart ComfyUI and hard-refresh the browser (`Ctrl+F5`) after switching versions. diff --git a/VERIFICATION.md b/VERIFICATION.md index 530d6b4..48c4a21 100644 --- a/VERIFICATION.md +++ b/VERIFICATION.md @@ -1,5 +1,7 @@ # Verification record +- 0.6.5: classic and Nodes 2.0 browser checks on frontend 1.52.7 verify changing a locked preset, pointer-following replacement selection, automatic inpaint dimensions after relocking, preservation of an adopted patch by merging, retention of the old selection/resolution after an upload failure, and reselection through swap/custom controls. Frontend syntax and diff whitespace checks pass. + - 0.6.4 comparison-adoption update: 48 backend tests pass, including removal of the result socket, ignoring obsolete feedback links, and generating-region metadata on comparison previews. Resolution-sync checks cover multiple connected targets, linked-input protection and source-size bounds. Real CPU comparison queues and browser interactions on isolated port 8196 with frontend 1.52.7 pass in classic nodes and Nodes 2.0: black dashed frame, automatic inpaint dimensions, no automatic stitching, manual crop adoption with retained repaint mask, independent fusion editing, reload, stale-result rejection, normal-mode mask preservation, and migration of the old wired example. Existing draft preflight/storage, comparison history, geometry and frontend syntax checks also pass. No complete model generation is claimed by these lightweight image probes. - 0.6.3: reproduced the sticky native draft failure on real frontend builds 1.42.11 and 1.52.7: after a 12 Mi-character draft fails, even a two-character draft returns false until the page reloads. Isolated lightweight API fixtures (no model queues or production writes) verify oversized workflow loading before configuration, exact source byte hashes, repaint mask pixels, stale named-snapshot cleanup, repeated mode changes and subsequent successful native draft writes in classic nodes and Nodes 2.0 on both builds. Unit checks additionally cover nested subgraph sources, named-only snapshots, fitting/blocked storage, failed uploads and unchanged masks/layers. This does not claim inspection of the user's live browser console or a complete model generation. diff --git a/pyproject.toml b/pyproject.toml index 2f25f47..20e2e05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "adachi-inpaint-workbench" -version = "0.6.4" +version = "0.6.5" description = "Inline mask painting and WebUI-style inpainting nodes for ComfyUI" requires-python = ">=3.10" license = { file = "LICENSE" } diff --git a/web/inline_crop.js b/web/inline_crop.js index 92c830f..d8a06d7 100644 --- a/web/inline_crop.js +++ b/web/inline_crop.js @@ -199,22 +199,19 @@ export class InlineCropController { custom.value = "custom"; custom.textContent = T("Custom"); this.preset.append(custom); - this.preset.onchange = () => { - if (this.preset.value !== "custom") - [this.cropWidth, this.cropHeight] = this.preset.value - .split("x") - .map(Number); - this.custom.hidden = this.preset.value !== "custom"; - this.sizeChanged(); - }; + this.preset.onchange = () => this.run(() => { + const custom = this.preset.value === "custom"; + const [width, height] = custom + ? [this.cropWidth, this.cropHeight] + : this.preset.value.split("x").map(Number); + return this.changeSize(width, height, {custom}); + }); this.bar.append(this.preset); const button = (text, title, action) => e.button(this.bar, text, T(title), () => this.run(action)); - this.swapButton = button("⇄", "Swap crop dimensions", () => { - [this.cropWidth, this.cropHeight] = [this.cropHeight, this.cropWidth]; - this.syncPreset(); - this.sizeChanged(); - }); + this.swapButton = button("⇄", "Swap crop dimensions", () => + this.changeSize(this.cropHeight, this.cropWidth), + ); this.reselectButton = button("↖", "Choose another region", () => this.reselect(), ); @@ -246,11 +243,11 @@ export class InlineCropController { "aria-label", T(key === "cropWidth" ? "Width" : "Height"), ); - input.onchange = () => { - this[key] = Math.max(64, Math.min(16384, Math.round(Number(input.value) / 64) * 64)); - input.value = this[key]; - this.sizeChanged(); - }; + input.onchange = () => this.run(() => { + const value = Math.max(64, Math.min(16384, Math.round(Number(input.value) / 64) * 64)); + return this.changeSize(key === "cropWidth" ? value : this.cropWidth, + key === "cropHeight" ? value : this.cropHeight, {custom:true}); + }); this.custom.append(input); } this.bar.append(this.custom); @@ -278,10 +275,10 @@ export class InlineCropController { this.maskButton.classList.toggle("dd-active", this.target === "mask"); this.blendButton.classList.toggle("dd-active", this.target === "blend"); this.modeSelect.disabled = !!this.busy; - this.preset.disabled = !!this.region?.locked; - this.swapButton.disabled = !!this.region?.locked; + this.preset.disabled = !!this.busy; + this.swapButton.disabled = !!this.busy; for (const input of this.custom.children) - input.disabled = !!this.region?.locked; + input.disabled = !!this.busy; } setMode(mode) { if ( @@ -337,6 +334,22 @@ export class InlineCropController { this.editor.schedule(); this.syncPreset(); } + async changeSize(width, height, {custom = false} = {}) { + if (this.busy || this.editor.stroke) { this.syncPreset(); return; } + try { + // Use the same merge-before-reselect behavior as the reselect button. + if (this.region.locked) await this.reselect(); + this.cropWidth = width; + this.cropHeight = height; + this.sizeChanged(); + if (custom) { this.preset.value = "custom"; this.custom.hidden = false; } + this.sync(); + this.changed(); + } catch (error) { + this.syncPreset(); + throw error; + } + } reset() { this.layer = null; this.widgets.layer_image.value = ""; diff --git a/web/inline_inpaint.js b/web/inline_inpaint.js index d93d2a7..7630a94 100644 --- a/web/inline_inpaint.js +++ b/web/inline_inpaint.js @@ -4,7 +4,7 @@ import { InlineMaskEditor } from "./editor.js?v=0.6.2"; 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.4"; +import { InlineCropController } from "./inline_crop.js?v=0.6.5"; 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"; @@ -162,7 +162,7 @@ app.registerExtension({ markChanged(); }, }); - editor.root.dataset.adachiVersion = "0.6.4"; + editor.root.dataset.adachiVersion = "0.6.5"; const beforeModeChange = () => { if (draftRecovery) return draftRecovery; const embedded = sourceWidget?.value;