import { app } from "../../scripts/app.js"; import { InpaintParameterPanel } from "./inpaint_panel.js?v=adachi-name"; import { connectedEditor, connectedSamplers, relocateWidgets, syncCurrentDimensions } from "./parameter_bridge.js?v=0.8.2"; import { routeWheelOutsideImage } from "./wheel_routing.js"; import { T } from "./i18n.js"; import { bindWidgetToNodeWidth } from "./widget_layout.js?v=0.5.5"; 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"; if (node.comfyClass === "AdachiWebUIInpaint" || node.type === "AdachiWebUIInpaint") { const launch = document.createElement("button"); launch.type = "button"; launch.textContent = T("Open Adachi Workbench"); launch.className = "adachi-workspace-launch"; launch.onclick = event => { event.preventDefault(); event.stopPropagation(); document.dispatchEvent(new CustomEvent("adachi-open-workspace", {detail:{sampler:node}})); }; root.append(launch); } const fold = document.createElement("details"); fold.className = "dd-workbench-fold"; const summary = document.createElement("summary"); summary.textContent = T("Painting tools and mask settings"); summary.addEventListener("pointerdown", event => event.stopPropagation()); summary.addEventListener("click", event => event.stopPropagation()); fold.append(summary); 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 = T("Connect an inline mask to show the painting tools"); tools.append(placeholder); fold.append(tools); const actionHost = document.createElement("div"); actionHost.className = "dd-workbench-actions"; root.append(fold, actionHost); 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(); if (mountedEditor?.actions.parentElement === actionHost) mountedEditor.actions.remove(); mountedEditor = next; if (next) { tools.append(next.controls); actionHost.append(next.actions); } scheduleSize(); } placeholder.hidden = !!next; placeholder.textContent = editor ? T("The painting tools live in the other inpaint node connected to this image") : T("Connect an inline mask to show the painting tools"); }; const contentHeight = () => (root.querySelector('.adachi-workspace-launch')?.offsetHeight || 0) + fold.offsetHeight + actionHost.offsetHeight + (panel?.root.scrollHeight || 620); const scheduleSize = () => { cancelAnimationFrame(frame); frame = requestAnimationFrame(() => { if (removed || !node.graph || !root.isConnected || root.closest(".adachi-workspace")) 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; fold.open = !!node.properties.adachi_tools_open; fold.ontoggle = () => { if (node.properties.adachi_tools_open !== fold.open) { node.properties.adachi_tools_open = fold.open; markChanged(); } scheduleSize(); }; panel = new InpaintParameterPanel({ getTargets: () => { refreshEditor(); return [node]; }, getImage: () => source?.adachiEditor?.ready ? (source.adachiCrop?.mode === "crop" && source.adachiCrop.region.locked ? source.adachiCrop.region : source.adachiEditor.image) : null, onTargetChange: (previous, next) => relocateWidgets(node, previous, next), onDirty: markChanged, onHeight: scheduleSize, onMaskSettings: element => { fold.querySelector(".dd-param-mask-settings")?.remove(); fold.append(element); scheduleSize(); }, }); root.append(panel.root); node.adachiParameters = panel; node.adachiWorkbench = { root, tools, fold, actionHost, 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; bindWidgetToNodeWidth(node, widget, root); const observer = new ResizeObserver(scheduleSize); observer.observe(tools); observer.observe(fold); observer.observe(actionHost); const configured = node.onConfigure; node.onConfigure = function () { const result = configured?.apply(this, arguments); const legacyTitles = ["WebUI Inpaint", "WebUI 局部重绘", "WebUI 局部重绘 / WebUI Inpaint", "安达局部重绘工作台"]; if (node.type === "AdachiWebUIInpaint" && legacyTitles.includes(node.title)) { node.title = T("Adachi Inpaint Workbench"); } fold.open = !!node.properties.adachi_tools_open; queueMicrotask(() => { if (!removed) { panel.refresh(); scheduleSize(); } }); return result; }; const connections = node.onConnectionsChange; node.onConnectionsChange = function (type, slot, connected) { const result = connections?.apply(this, arguments); // Graph restoration also connects wires: never overwrite the sizes // saved in a workflow while it is being configured. const userImageConnection = !app.configuringGraph && type === 1 && connected && node.inputs?.[slot]?.name === "image"; queueMicrotask(() => { if (removed) return; panel.refresh(); if (userImageConnection) syncCurrentDimensions(connectedEditor(node), node); }); return result; }; const previousRemoved = node.onRemoved; node.onRemoved = function () { removed = true; cancelAnimationFrame(frame); observer.disconnect(); if (mountedEditor?.controls.parentElement === tools) mountedEditor.controls.remove(); if (mountedEditor?.actions.parentElement === actionHost) mountedEditor.actions.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; }; }