Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a60d9e174 |
@@ -22,7 +22,7 @@ powershell -ExecutionPolicy Bypass -File .\install.ps1 -ComfyUIPath "D:\ComfyUI_
|
||||
|
||||
1. 将 `examples/01-inline-inpaint.json` 拖入 ComfyUI,选择自己的 checkpoint 喵。
|
||||
2. 在 **内联蒙版 / Inline Mask** 节点载入、拖放或粘贴图片,直接在画布上涂抹需要重绘的部分喵。
|
||||
3. 修改正负提示词,在 **内联蒙版** 图片下方选择重绘范围、采样分辨率、采样器与重绘幅度,然后运行喵。
|
||||
3. 修改正负提示词,在 **WebUI 局部重绘** 节点中选择画笔工具、重绘范围、采样分辨率、采样器与重绘幅度,然后运行喵。
|
||||
4. `image` 输出回贴后的完整原图,`crop` 输出实际采样画布,`mask` 输出该采样画布对应的蒙版喵。
|
||||
|
||||
**白色蒙版重绘,黑色保护;画布中橙色代表绘制区域** 喵。
|
||||
@@ -60,24 +60,25 @@ powershell -ExecutionPolicy Bypass -File .\install.ps1 -ComfyUIPath "D:\ComfyUI_
|
||||
| 原图 / 蒙版 / 叠加 | 切换预览,不改变输出 |
|
||||
|
||||
支持画笔压力和软笔刷;鼠标使用固定压力喵。
|
||||
笔径、硬度、笔触和叠加四个滑条位于图片编辑区下方喵。
|
||||
内联蒙版仅保留图片编辑区与左上角的小型 **载入原图** 按钮,载入后按原图比例调整画布高度以减少空边喵。
|
||||
画笔、橡皮、矩形、套索、移动、四个笔刷滑条、撤销重做、蒙版操作、显示切换、适应与原图嵌入选项,均显示在连接的 **WebUI 局部重绘** 节点中,直接控制原来的内联画布喵。
|
||||
图片上传一次,之后每笔在浏览器内更新蒙版,并在工作流及运行参数中保存 PNG 数据,无需每画一笔都保存、重新加载磁盘文件喵。
|
||||
鼠标位于图片区域时滚轮缩放图片;位于工具栏、按钮、滑条或参数面板时,滚轮遵循 ComfyUI 的画布导航设置喵。
|
||||
从 ComfyUI 画布开始的连续滚轮手势移入图片时,继续导航外部画布;停下后再在图片里滚动才缩放图片喵。
|
||||
超大图仍会消耗浏览器内存,PNG 编码耗时取决于分辨率和设备,不能保证所有图像都瞬间完成喵。
|
||||
|
||||
## 内联重绘参数
|
||||
## 画笔工具与重绘参数
|
||||
|
||||
内联蒙版连接 **WebUI 局部重绘** 的 `image` 输入后,采样参数自动移到内联蒙版底部,直接控制原采样节点的真实参数喵。
|
||||
内联蒙版连接 **WebUI 局部重绘** 的 `image` 输入后,原画笔控件自动显示在该重绘节点中;下方参数面板直接控制该节点的真实采样参数喵。
|
||||
宽度和高度各占一行,支持滑条及数值输入;重绘幅度也使用滑条,点击 **使用原图宽高** 可填入原图尺寸,按采样要求取最近的 8 的倍数喵。
|
||||
蒙版内外、填充、重绘范围、采样器、调度器、步数、CFG 和种子直接显示,蒙版处理、批量、Refiner 切换等位于 **蒙版、批量与高级设置** 中喵。
|
||||
如果一个内联蒙版连接多个采样节点,可在参数面板上方切换控制目标;断开连接后,采样节点恢复显示自身参数喵。
|
||||
外部连线控制的参数会禁用对应面板控件;模块化工作流仍使用各独立节点的参数喵。
|
||||
如果一张内联原图连接多个重绘节点,画笔控件放在第一个 WebUI 重绘节点中,各节点保留自己的参数面板;断开或删除该节点后,工具自动迁移到下一个可用节点喵。
|
||||
外部连线控制的参数会禁用对应面板控件;模块化工作流将画笔工具放在 **重绘预处理 / Inpaint Prepare** 节点中,采样参数仍在独立采样节点中调整喵。
|
||||
|
||||
## 图像对比与连续重绘
|
||||
|
||||
添加 **图像对比 / Image Compare** 节点,将内联蒙版的 `image` 接到 `original`,最终完整重绘图 `image` 接到 `result`,运行后即可对比喵。
|
||||
鼠标移入图片时,细竖线左边显示重绘结果、右边显示原图;移出后隐藏分割线,完整显示重绘结果,普通滚轮缩放图片;批量结果可在上方选择喵。
|
||||
鼠标移入图片时,细竖线左边显示重绘结果、右边显示原图;移出后隐藏分割线,完整显示重绘结果,普通滚轮缩放图片;批量结果选择、尺寸和适应按钮位于图片下方喵。
|
||||
满意后点击 **采用此结果**,插件会把当前选中的结果上传为新的原图,回填到上游内联蒙版,并清空旧蒙版,随后可以继续涂抹下一处喵。
|
||||
原图连接经过中间 IMAGE 节点时会向上寻找内联蒙版;若存在多个目标,可以在“回填到”中明确选择喵。
|
||||
回填只在点击按钮时发生,不会自动开始下一轮生成;`image` 输出仍传递整个结果批次,可接保存图像喵。
|
||||
@@ -146,6 +147,7 @@ Use `whole_image` to sample the full source extent, or `only_masked` to enlarge
|
||||
Sampling uses `width × height`; the final `image` output is composited at the original image resolution in both modes, while `crop` exposes the generated sampling canvas喵.
|
||||
|
||||
The editor supports brush/eraser, rectangles, freehand lasso, undo/redo, invert, mask import/export, brush size/hardness/opacity, zoom, pan, drag-and-drop and clipboard images喵.
|
||||
Inline Mask shows only the image and a small upload button; its original live painting controls and the sampling parameter panel are hosted in the connected WebUI Inpaint node (or Inpaint Prepare for modular workflows)喵.
|
||||
Paint with the left button; hold Alt or use the right button to erase temporarily, use Ctrl+wheel for brush size, zoom with the plain wheel, and pan while holding Space喵.
|
||||
Enable **Embed source in workflow** before saving JSON to share the source image along with its mask喵.
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# Git 版本与回滚
|
||||
|
||||
`v0.2.0` 是本次界面调整前的回滚点,`v0.3.0` 包含内联参数面板、左右对比调整和滚轮路由修复喵。
|
||||
`v0.2.0` 是最早的界面回滚点,`v0.3.0` 包含内联参数面板、左右对比调整和滚轮路由修复;`v0.4.0` 将画笔与参数集中到 WebUI 重绘节点,并精简原图与对比节点喵。
|
||||
本机安装目录与源码目录均保留 Git 历史;普通发布 ZIP 不含 `.git`,可另用 Git bundle 恢复历史喵。
|
||||
|
||||
在插件目录运行以下命令,先确认没有未保存改动,再切换版本喵。
|
||||
|
||||
```powershell
|
||||
git status --short
|
||||
git switch --detach v0.2.0
|
||||
git switch --detach v0.3.0
|
||||
```
|
||||
|
||||
恢复本版使用以下命令喵。
|
||||
|
||||
```powershell
|
||||
git switch --detach v0.3.0
|
||||
git switch --detach v0.4.0
|
||||
```
|
||||
|
||||
切换后重启 ComfyUI 并在浏览器按 `Ctrl+F5` 刷新喵。
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
- 0.3.0 验证工具栏和参数区域滚轮传递给外部画布,连续外部滚轮手势进入图片后不改变图片缩放,停下后的图片滚轮只缩放图片喵。
|
||||
- 0.3.0 对比节点实测左侧为结果、右侧为原图,移出后完整显示结果;分割线为一个物理像素,移除了圆形手柄、角标和底部说明,回填与批量选择仍通过喵。
|
||||
- 0.3.0 画笔浏览器交互 15 组通过,两种渲染模式的纵向拉伸和保存恢复通过,两个更新后的示例工作流连线及执行参数验证通过喵。
|
||||
- 0.4.0 在经典节点与 Nodes 2.0 验证内联画布只保留载入按钮,原画笔 DOM 控件迁移到 WebUI 节点;真实鼠标涂抹后可从重绘节点撤销,工具和滑条控制原画布,采样参数正确进入执行请求喵。
|
||||
- 0.4.0 验证按原图比例缩减画布空边、细高度条拉伸、保存重开保持尺寸、原图尺寸填写、断开重连、删除重建重绘节点,以及工具区域滚轮继续操作外部画布喵。
|
||||
- 0.4.0 图像对比顶部不再有工具栏,结果选择、尺寸和适应移到底部;真实 API 队列中的左右对比、移出显示完整结果、批量选择及结果回填通过喵。
|
||||
|
||||
采样路径还使用本机 ComfyUI 的真实 common_ksampler/KSampler 函数进行了噪声调度衔接检查;尚未加载实际 checkpoint 测试 GPU 生成质量或所有模型家族,不能将 CPU/接口测试视为模型效果验证喵。
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
],
|
||||
"size": [
|
||||
660,
|
||||
1276
|
||||
700
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
@@ -67,8 +67,7 @@
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "AdachiInlineMask",
|
||||
"adachi_inline_parameters": true
|
||||
"Node name for S&R": "AdachiInlineMask"
|
||||
},
|
||||
"widgets_values": [
|
||||
"",
|
||||
@@ -242,7 +241,7 @@
|
||||
],
|
||||
"size": [
|
||||
430,
|
||||
244
|
||||
1020
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
@@ -636,7 +635,7 @@
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"安达工作台 / Adachi Workbench\n1. 选择自己的 checkpoint 喵\n2. 在左侧节点载入原图并直接涂抹喵\n3. 在内联蒙版底部调整重绘参数喵\n4. width/height 是采样分辨率,最终 image 回贴为原图尺寸喵\n分享时勾选「原图嵌入工作流」喵"
|
||||
"安达工作台 / Adachi Workbench\n1. 选择自己的 checkpoint 喵\n2. 在左侧节点载入原图并直接涂抹喵\n3. 在 WebUI 重绘节点中选择画笔并调整重绘参数喵\n4. width/height 是采样分辨率,最终 image 回贴为原图尺寸喵\n分享时在重绘节点勾选「原图嵌入工作流」喵"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
|
||||
@@ -242,7 +242,7 @@
|
||||
],
|
||||
"size": [
|
||||
350,
|
||||
420
|
||||
763
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
@@ -500,7 +500,7 @@
|
||||
],
|
||||
"size": [
|
||||
330,
|
||||
310
|
||||
317.3125
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
|
||||
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 128 KiB |
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "comfyui-adachi-workbench"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
description = "Inline mask painting and WebUI-style inpainting nodes for ComfyUI"
|
||||
requires-python = ">=3.10"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
@@ -20,13 +20,12 @@ export class ImageCompareView {
|
||||
this.originals = []; this.results = []; this.index = 0;
|
||||
this.zoom = 1; this.pan = { x: 0, y: 0 }; this.split = .5;
|
||||
this.version = 0; this.ready = false;
|
||||
const head = element("div", "dd-head");
|
||||
this.dimensionsLabel = element("span", "dd-dimensions"); head.append(this.dimensionsLabel);
|
||||
this.dimensionsLabel = element("span", "dd-dimensions");
|
||||
const toolbar = element("div", "dd-row dd-compare-toolbar");
|
||||
toolbar.append(element("span", "dd-compare-label", "结果"));
|
||||
this.resultSelect = element("select"); this.resultSelect.setAttribute("aria-label", "选择重绘结果 / Choose result");
|
||||
this.resultSelect.onchange = () => { this.index = Number(this.resultSelect.value); this.options.onSelect?.(this.index); this.loadSelected(); };
|
||||
toolbar.append(this.resultSelect);
|
||||
toolbar.append(this.resultSelect, this.dimensionsLabel);
|
||||
const spacer = element("span", "dd-spacer"); toolbar.append(spacer);
|
||||
const fit = element("button", "", "适应"); fit.type = "button";
|
||||
fit.onclick = () => { this.zoom = 1; this.pan = { x: 0, y: 0 }; this.schedule(); }; toolbar.append(fit);
|
||||
@@ -48,7 +47,7 @@ export class ImageCompareView {
|
||||
this.adoptButton.onclick = () => this.adopt(); adoption.append(this.adoptButton);
|
||||
this.message = element("div", "dd-status", "鼠标移动竖线对比 · 滚轮缩放 · 中键拖动");
|
||||
this.message.hidden = true; this.message.setAttribute("role", "alert");
|
||||
this.root.append(head, toolbar, this.stage, adoption, this.message);
|
||||
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;
|
||||
|
||||
@@ -73,6 +73,25 @@
|
||||
.dd-comparer .dd-adoption select { flex: 1; }
|
||||
.dd-comparer .dd-compare-stage { min-height: 220px; }
|
||||
.dd-comparer .dd-compare-stage canvas { cursor: ew-resize; }
|
||||
.dd-inline-minimal { position: relative; border: 0; border-radius: 0; background: transparent; }
|
||||
.dd-inline-minimal .dd-stage { border: 0; min-height: 170px; flex-basis: 0; }
|
||||
.dd-inline-minimal > .dd-files { position: absolute; z-index: 2; left: 5px; top: 5px; padding: 0; }
|
||||
.dd-inline-minimal > .dd-files button { min-height: 23px; padding: 2px 6px; font-size: 11px; opacity: .85; }
|
||||
.dd-inline-minimal > .dd-files button:hover { opacity: 1; }
|
||||
.dd-inline-minimal > .dd-resize-handle { position: absolute; z-index: 2; bottom: 0; width: 100%; height: 5px; min-height: 5px; border: 0; color: transparent; background: transparent; }
|
||||
.dd-inline-minimal > .dd-resize-handle:hover { background: #ffc07966; }
|
||||
.dd-inline-minimal > .dd-status:not([hidden]) { position: absolute; bottom: 6px; left: 0; right: 0; background: #20242bee; }
|
||||
.dd-editor-controls { height: auto; flex: 0 0 auto; border: 0; border-radius: 0; background: transparent; overflow: visible; }
|
||||
.dd-editor-controls .dd-source-info { justify-content: space-between; }
|
||||
.dd-editor-controls .dd-filename { padding-left: 0; flex: 1; }
|
||||
.dd-workbench { height: 100%; border: 0; border-radius: 0; }
|
||||
.dd-workbench-tools { flex: 0 0 auto; }
|
||||
.dd-tools-placeholder { padding: 8px 12px; }
|
||||
.dd-comparer { border: 0; border-radius: 0; }
|
||||
.dd-comparer .dd-compare-stage { border: 0; }
|
||||
.dd-comparer .dd-compare-toolbar { padding-top: 5px; padding-bottom: 2px; }
|
||||
.dd-comparer .dd-compare-toolbar .dd-dimensions { margin-left: 5px; }
|
||||
.dd-comparer .dd-adoption { padding-top: 3px; padding-bottom: 5px; }
|
||||
.dd-comparer .dd-compare-captions { position: absolute; top: 8px; left: 8px; right: 8px; display: flex; justify-content: space-between; pointer-events: none; }
|
||||
.dd-comparer .dd-compare-captions span { background: #171d27cc; color: #f1e3cf; padding: 3px 7px; border-radius: 3px; font-size: 11px; }
|
||||
@container (max-width: 380px) { .dd-inpaint .dd-settings { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 3px 12px; } }
|
||||
|
||||
@@ -48,10 +48,20 @@ export class InlineMaskEditor {
|
||||
if (event.key === "Alt") { this.altErase = false; this.updateButtons(); this.schedule(); }
|
||||
});
|
||||
this.root.addEventListener("blur", () => { this.space = false; this.altErase = false; this.updateButtons(); this.schedule(); }, true);
|
||||
this.root.addEventListener("paste", (event) => {
|
||||
const paste = event => {
|
||||
const file = [...(event.clipboardData?.files || [])].find(f => f.type.startsWith("image/"));
|
||||
if (file) { event.preventDefault(); event.stopPropagation(); this.upload(file); }
|
||||
});
|
||||
};
|
||||
this.root.addEventListener("paste", paste);
|
||||
if (this.controls) {
|
||||
this.controls.addEventListener("paste", paste);
|
||||
this.controls.addEventListener("blur", () => { this.space = false; this.altErase = false; this.updateButtons(); this.schedule(); }, true);
|
||||
this.controls.addEventListener("keydown", event => this.keydown(event));
|
||||
this.controls.addEventListener("keyup", event => {
|
||||
if (event.code === "Space") { this.space = false; this.stage.classList.remove("dd-pan"); }
|
||||
if (event.key === "Alt") { this.altErase = false; this.updateButtons(); this.schedule(); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
button(parent, label, title, action, cls = "") {
|
||||
@@ -70,7 +80,7 @@ export class InlineMaskEditor {
|
||||
head.append(this.sizeLabel);
|
||||
this.root.append(head);
|
||||
const files = el("div", "dd-row dd-files");
|
||||
this.button(files, "载入原图", "上传图片,也可拖放或粘贴 / Upload image", () => this.imageInput.click(), "dd-primary");
|
||||
this.loadButton = this.button(files, "载入原图", "上传图片,也可拖放或粘贴 / Upload image", () => this.imageInput.click(), "dd-primary");
|
||||
this.filename = el("span", "dd-filename", "拖放图片到画布");
|
||||
files.append(this.filename);
|
||||
this.root.append(files);
|
||||
@@ -159,6 +169,16 @@ export class InlineMaskEditor {
|
||||
this.message.hidden = true;
|
||||
this.message.setAttribute("role", "alert");
|
||||
this.root.append(this.message);
|
||||
if (this.options.compact) {
|
||||
this.root.classList.add("dd-inline-minimal");
|
||||
this.empty.hidden = true;
|
||||
this.controls = el("div", "dd-inpaint dd-editor-controls");
|
||||
const info = el("div", "dd-row dd-source-info"); info.append(this.filename, this.sizeLabel);
|
||||
head.remove();
|
||||
this.controls.append(info, tools, settings, actions, footer, embedRow);
|
||||
// These are the original live controls; the sampler mounts this
|
||||
// same DOM tree so brush state and callbacks are preserved.
|
||||
}
|
||||
this.setTool("brush");
|
||||
this.updateButtons();
|
||||
}
|
||||
@@ -550,6 +570,6 @@ export class InlineMaskEditor {
|
||||
cancelAnimationFrame(this.frame); this.observer.disconnect();
|
||||
this.stopWheelCapture();
|
||||
for (const canvas of [this.mask, this.baseMask, this.overlay, this.canvas]) canvas.width = canvas.height = 0;
|
||||
this.history = []; this.future = []; this.image = null; this.root.remove();
|
||||
this.history = []; this.future = []; this.image = null; this.controls?.remove(); this.root.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { InlineMaskEditor } from "./editor.js";
|
||||
import { InpaintParameterPanel } from "./inpaint_panel.js";
|
||||
import { connectedSamplers, relocateWidgets } from "./parameter_bridge.js";
|
||||
import { installInpaintWorkbench } from "./inpaint_node.js";
|
||||
import { routeWheelOutsideImage } from "./wheel_routing.js";
|
||||
|
||||
const styleId = "adachi-inpaint-style";
|
||||
@@ -40,6 +39,7 @@ function imageUrl(value) {
|
||||
app.registerExtension({
|
||||
name: "Adachi.InlineInpaint",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (["AdachiWebUIInpaint", "AdachiInpaintPrepare"].includes(nodeData.name)) { installInpaintWorkbench(nodeType); return; }
|
||||
if (nodeData.name !== "AdachiInlineMask") return;
|
||||
const originalCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
@@ -59,7 +59,21 @@ app.registerExtension({
|
||||
let restoring = false;
|
||||
let restoreFrame = 0;
|
||||
let uploadVersion = 0;
|
||||
let panelBaseHeight = null;
|
||||
let fitFrame = 0;
|
||||
const fitNodeToImage = () => {
|
||||
cancelAnimationFrame(fitFrame);
|
||||
fitFrame = requestAnimationFrame(() => {
|
||||
if (!editor.ready || editor.destroyed || !node.graph) return;
|
||||
const rect = editor.canvas.getBoundingClientRect();
|
||||
const scale = app.canvas?.ds?.scale || 1;
|
||||
if (!rect.width || !rect.height) return;
|
||||
const overhead = node.size[1] - rect.height / scale;
|
||||
const height = overhead + rect.width / scale * editor.image.height / editor.image.width;
|
||||
node.setSize([node.size[0], Math.max(240, Math.min(4200, Math.ceil(height)))]);
|
||||
node.properties.adachi_compact_ui = true;
|
||||
editor.schedule();
|
||||
});
|
||||
};
|
||||
const restore = async () => {
|
||||
const embedded = sourceWidget?.value;
|
||||
const filename = imageWidget?.value;
|
||||
@@ -71,9 +85,10 @@ app.registerExtension({
|
||||
finally { restoring = false; }
|
||||
};
|
||||
const editor = new InlineMaskEditor({
|
||||
compact: true,
|
||||
onChange(data) { if (maskWidget && !restoring) maskWidget.value = data; },
|
||||
onDirty() { markChanged(); },
|
||||
onImage() { node.adachiParameters?.refresh(); },
|
||||
onImage() { if (!restoring || !node.properties.adachi_compact_ui) fitNodeToImage(); },
|
||||
onGraphWheel: event => app.canvas?.processMouseWheel(event),
|
||||
async onUpload(file) {
|
||||
const version = ++uploadVersion;
|
||||
@@ -101,33 +116,6 @@ app.registerExtension({
|
||||
});
|
||||
node.adachiEditor = editor;
|
||||
const stopWheelRouting = routeWheelOutsideImage(editor.root, editor.canvas, () => editor.ready);
|
||||
const panel = new InpaintParameterPanel({
|
||||
getTargets: () => connectedSamplers(node),
|
||||
getPreferred: () => node.properties.adachi_settings_target,
|
||||
onPreferred: id => { node.properties.adachi_settings_target = id; markChanged(); },
|
||||
getImage: () => editor.ready ? editor.image : null,
|
||||
onTargetChange: (previous, next) => {
|
||||
if (next && !node.properties.adachi_inline_parameters) panelBaseHeight = node.size[1];
|
||||
relocateWidgets(node, previous, next);
|
||||
},
|
||||
onDirty: markChanged,
|
||||
onHeight: () => {
|
||||
const parameters = node.adachiParameters;
|
||||
const panelHeight = parameters?.root.scrollHeight || 40;
|
||||
if (node.graph) {
|
||||
// Add the panel once when migrating an older workflow.
|
||||
// Reopening an already-migrated workflow must keep its saved size.
|
||||
const migrate = parameters?.target && panelHeight > 100 && !node.properties.adachi_inline_parameters;
|
||||
const migratedHeight = migrate ? (panelBaseHeight ?? node.size[1]) + Math.max(0, panelHeight - 40) : node.size[1];
|
||||
if (migrate) node.properties.adachi_inline_parameters = true;
|
||||
const height = Math.max(620 + panelHeight, migratedHeight);
|
||||
if (Math.abs(node.size[1] - height) > 1) node.setSize([node.size[0], height]);
|
||||
}
|
||||
editor.schedule();
|
||||
},
|
||||
});
|
||||
node.adachiParameters = panel;
|
||||
editor.root.append(panel.root);
|
||||
node.adachiUseResult = async file => {
|
||||
if (editor.destroyed) throw new Error("内联蒙版节点已移除");
|
||||
await editor.options.onUpload(file);
|
||||
@@ -137,7 +125,7 @@ app.registerExtension({
|
||||
const domWidget = node.addDOMWidget("adachi_canvas", "adachi_inline_mask", editor.root, {
|
||||
serialize: false,
|
||||
hideOnZoom: false,
|
||||
getMinHeight: () => 520 + (panel.root.scrollHeight || 40),
|
||||
getMinHeight: () => 170,
|
||||
getMaxHeight: () => 4096,
|
||||
});
|
||||
domWidget.serialize = false;
|
||||
@@ -154,7 +142,7 @@ app.registerExtension({
|
||||
editor.root.append(resizeHandle);
|
||||
let resizing = null;
|
||||
const resizeHeight = height => {
|
||||
node.setSize([node.size[0], Math.max(620, Math.min(4200, height))]);
|
||||
node.setSize([node.size[0], Math.max(240, Math.min(4200, height))]);
|
||||
node.graph?.setDirtyCanvas(true, true);
|
||||
editor.schedule();
|
||||
};
|
||||
@@ -201,20 +189,14 @@ app.registerExtension({
|
||||
const previousConfigure = node.onConfigure;
|
||||
node.onConfigure = function () {
|
||||
const configured = previousConfigure?.apply(this, arguments);
|
||||
cancelAnimationFrame(restoreFrame); restoreFrame = requestAnimationFrame(() => { restore(); panel.refresh(); });
|
||||
cancelAnimationFrame(restoreFrame); restoreFrame = requestAnimationFrame(restore);
|
||||
return configured;
|
||||
};
|
||||
const previousConnections = node.onConnectionsChange;
|
||||
node.onConnectionsChange = function () {
|
||||
const result = previousConnections?.apply(this, arguments);
|
||||
queueMicrotask(() => { if (!editor.destroyed) panel.refresh(); });
|
||||
return result;
|
||||
};
|
||||
const previousImageCallback = imageWidget.callback;
|
||||
imageWidget.callback = function () { previousImageCallback?.apply(this, arguments); restore(); };
|
||||
const previousRemoved = node.onRemoved;
|
||||
node.onRemoved = function () { cancelAnimationFrame(restoreFrame); panel.destroy(); stopWheelRouting(); editor.destroy(); return previousRemoved?.apply(this, arguments); };
|
||||
node.setSize([Math.max(510, node.size[0]), Math.max(650, node.size[1])]);
|
||||
node.onRemoved = function () { cancelAnimationFrame(restoreFrame); cancelAnimationFrame(fitFrame); stopWheelRouting(); editor.destroy(); return previousRemoved?.apply(this, arguments); };
|
||||
node.setSize([Math.max(380, node.size[0]), Math.max(480, node.size[1])]);
|
||||
restoreFrame = requestAnimationFrame(restore);
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { InpaintParameterPanel } from "./inpaint_panel.js";
|
||||
import { connectedEditor, connectedSamplers, relocateWidgets } from "./parameter_bridge.js";
|
||||
import { routeWheelOutsideImage } from "./wheel_routing.js";
|
||||
|
||||
export function installInpaintWorkbench(nodeType) {
|
||||
const created = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = created?.apply(this, arguments), node = this;
|
||||
const root = document.createElement("div"); root.className = "dd-inpaint dd-workbench";
|
||||
const tools = document.createElement("div"); tools.className = "dd-workbench-tools";
|
||||
const placeholder = document.createElement("div"); placeholder.className = "dd-param-placeholder dd-tools-placeholder";
|
||||
placeholder.textContent = "连接内联蒙版后显示画笔工具";
|
||||
tools.append(placeholder); root.append(tools);
|
||||
let source = null, mountedEditor = null, frame = 0, removed = false;
|
||||
const markChanged = () => {
|
||||
app.canvas?.emitBeforeChange?.(); app.canvas?.emitAfterChange?.();
|
||||
node.graph?.setDirtyCanvas(true, true);
|
||||
};
|
||||
const refreshEditor = () => {
|
||||
source = connectedEditor(node);
|
||||
const editor = source?.adachiEditor;
|
||||
// A live control tree has one host. Additional samplers still get
|
||||
// their own parameters and use the same source image dimensions.
|
||||
const canMount = editor?.controls && connectedSamplers(source)[0] === node;
|
||||
const next = canMount ? editor : null;
|
||||
if (next !== mountedEditor) {
|
||||
if (mountedEditor?.controls.parentElement === tools) mountedEditor.controls.remove();
|
||||
mountedEditor = next;
|
||||
if (next) tools.append(next.controls);
|
||||
scheduleSize();
|
||||
}
|
||||
placeholder.hidden = !!next;
|
||||
placeholder.textContent = editor ? "画笔工具位于连接此原图的另一个重绘节点" : "连接内联蒙版后显示画笔工具";
|
||||
};
|
||||
const contentHeight = () => (tools.scrollHeight || 30) + (panel?.root.scrollHeight || 620);
|
||||
const scheduleSize = () => {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = requestAnimationFrame(() => {
|
||||
if (removed || !node.graph || !root.isConnected) return;
|
||||
const required = node.computeSize()[1];
|
||||
if (Math.abs(node.size[1] - required) > 1) node.setSize([node.size[0], required]);
|
||||
node.graph.setDirtyCanvas(true, true);
|
||||
});
|
||||
};
|
||||
let panel;
|
||||
panel = new InpaintParameterPanel({
|
||||
getTargets: () => { refreshEditor(); return [node]; },
|
||||
getImage: () => source?.adachiEditor?.ready ? source.adachiEditor.image : null,
|
||||
onTargetChange: (previous, next) => relocateWidgets(node, previous, next),
|
||||
onDirty: markChanged, onHeight: scheduleSize,
|
||||
});
|
||||
root.append(panel.root);
|
||||
node.adachiParameters = panel; node.adachiWorkbench = { root, tools, get source() { return source; } };
|
||||
const stopWheelRouting = routeWheelOutsideImage(root, null, () => false);
|
||||
const widget = node.addDOMWidget("adachi_workbench", "adachi_inpaint_controls", root, {
|
||||
serialize: false, hideOnZoom: false, getMinHeight: contentHeight, getMaxHeight: () => Math.max(4096, contentHeight()),
|
||||
});
|
||||
widget.serialize = false;
|
||||
const observer = new ResizeObserver(scheduleSize); observer.observe(tools);
|
||||
const configured = node.onConfigure;
|
||||
node.onConfigure = function () { const result = configured?.apply(this, arguments); queueMicrotask(() => { if (!removed) { panel.refresh(); scheduleSize(); } }); return result; };
|
||||
const connections = node.onConnectionsChange;
|
||||
node.onConnectionsChange = function () { const result = connections?.apply(this, arguments); queueMicrotask(() => { if (!removed) panel.refresh(); }); return result; };
|
||||
const previousRemoved = node.onRemoved;
|
||||
node.onRemoved = function () {
|
||||
removed = true; cancelAnimationFrame(frame); observer.disconnect();
|
||||
if (mountedEditor?.controls.parentElement === tools) mountedEditor.controls.remove();
|
||||
panel.destroy(); stopWheelRouting(); root.remove();
|
||||
return previousRemoved?.apply(this, arguments);
|
||||
};
|
||||
node.setSize([Math.max(510, node.size[0]), node.size[1]]); scheduleSize();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
@@ -12,9 +12,8 @@ function setHiddenOption(widget, value) {
|
||||
if (value === undefined) delete widget.options.hidden; else widget.options.hidden = value;
|
||||
}
|
||||
|
||||
export function connectedSamplers(source) {
|
||||
const graph = source.graph; if (!graph) return [];
|
||||
const originatesFromSource = target => {
|
||||
export function connectedEditor(target) {
|
||||
const graph = target.graph; if (!graph) return null;
|
||||
const input = target.inputs?.find(input => input.name === "image");
|
||||
const link = graph.links?.[input?.link]; const queue = link ? [link.origin_id] : [], seen = new Set(), sources = new Set();
|
||||
while (queue.length && seen.size < 256) {
|
||||
@@ -27,9 +26,13 @@ export function connectedSamplers(source) {
|
||||
const next = graph.links?.[input.link]; if (next) queue.push(next.origin_id);
|
||||
}
|
||||
}
|
||||
return sources.size === 1 && sources.has(source.id);
|
||||
};
|
||||
return (graph._nodes || []).filter(node => node.type === "AdachiWebUIInpaint" && originatesFromSource(node));
|
||||
return sources.size === 1 ? graph.getNodeById([...sources][0]) : null;
|
||||
}
|
||||
|
||||
export function connectedSamplers(source) {
|
||||
return (source.graph?._nodes || [])
|
||||
.filter(node => ["AdachiWebUIInpaint", "AdachiInpaintPrepare"].includes(node.type) && connectedEditor(node) === source)
|
||||
.sort((a, b) => Number(b.type === "AdachiWebUIInpaint") - Number(a.type === "AdachiWebUIInpaint"));
|
||||
}
|
||||
|
||||
export function relocateWidgets(owner, previous, next) {
|
||||
|
||||