feat(inpaint): paste/drag into the mask editor, richer Help, kept-strokes hint
- the open mask editor now accepts paste + drag-drop to load a source image (window-capture handlers gated on the overlay, preempting ComfyUI's own image-drop), same as the Load Image button. Unbound on close. - the saved-mask loader now shows "Kept your new strokes - the saved mask wasn't reloaded." when it skips a late mask load because you already started painting (was a silent skip). - the Help popup is rewritten into a short guide: what the node does, painting, crop-size modes, the seam (Softness + Blend mode Mask vs Whole crop), and keys. framework layout.mjs gains an optional helpTitle (defaults to the old "<name> - Shortcuts", so other editors are unchanged) so it reads "Inpaint Crop - Guide". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
11afe8369a
commit
718960a8bf
@@ -47,6 +47,7 @@ export function createEditorLayout(config) {
|
||||
onZoomOut,
|
||||
onZoomFit,
|
||||
helpContent = "",
|
||||
helpTitle = "", // optional; defaults to "<editorName> — Shortcuts" (back-compat)
|
||||
} = config;
|
||||
|
||||
// ── Overlay ──
|
||||
@@ -186,7 +187,7 @@ export function createEditorLayout(config) {
|
||||
if (helpContent) {
|
||||
helpPanel.innerHTML = `
|
||||
<div class="pxf-help-header">
|
||||
<h3>${editorName} — Shortcuts</h3>
|
||||
<h3>${helpTitle || `${editorName} — Shortcuts`}</h3>
|
||||
<button class="pxf-btn-sm" style="flex-shrink:0;">✕</button>
|
||||
</div>
|
||||
<div class="pxf-help-content">${helpContent}</div>
|
||||
|
||||
@@ -123,9 +123,10 @@ export class InpaintCropEditor {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._setStatus("No source loaded. Wire an IMAGE input and run the workflow once, or click Load Image.");
|
||||
this._setStatus("No source loaded. Wire an IMAGE input and run the workflow once, or click Load Image — or just paste / drag an image in here.");
|
||||
}
|
||||
this._bindKeys();
|
||||
this._bindDropPaste(); // paste / drag-drop an image straight into the editor
|
||||
}
|
||||
|
||||
_close() { this.layout?.unmount(); }
|
||||
@@ -141,17 +142,35 @@ export class InpaintCropEditor {
|
||||
onClose: () => this._close(),
|
||||
onUndo: () => this._doUndo(),
|
||||
onRedo: () => this._doRedo(),
|
||||
helpTitle: "Inpaint Crop — Guide",
|
||||
helpContent: `
|
||||
<b>Paint the mask:</b> drag on the image to mark the area to fix<br>
|
||||
<b>Brush / Erase:</b> pick a tool, or press <kbd>B</kbd> / <kbd>E</kbd><br>
|
||||
<b style="color:#f66744;">WHAT THIS DOES</b><br>
|
||||
Paint over the part of the image you want the model to redo. The node finds
|
||||
the box around your paint, adds a margin, and crops a clean piece to inpaint.
|
||||
The orange box is exactly what gets sent to the model.<br><br>
|
||||
|
||||
<b style="color:#f66744;">PAINTING</b><br>
|
||||
<b>Brush / Erase:</b> pick a tool or press <kbd>B</kbd> / <kbd>E</kbd> (hold <kbd>X</kbd> to flip briefly)<br>
|
||||
<b>Brush size:</b> <kbd>[</kbd> / <kbd>]</kbd> or the mouse wheel<br>
|
||||
<b>Toggle erase:</b> hold <kbd>X</kbd><br>
|
||||
<b>Show / hide mask:</b> <kbd>H</kbd><br>
|
||||
<b>Invert / Clear:</b> buttons in the sidebar<br>
|
||||
<b>Undo / Redo:</b> <kbd>Ctrl+Z</kbd> / <kbd>Ctrl+Shift+Z</kbd><br>
|
||||
The orange box is the crop that goes to the model. Adjust the context
|
||||
margin in the sidebar (or on the node) to include more surroundings.<br>
|
||||
<b>Save:</b> <kbd>Ctrl+S</kbd> · <b>Close:</b> <kbd>Escape</kbd>`,
|
||||
<b>Invert / Clear:</b> buttons in the sidebar (Invert = swap painted ↔ unpainted)<br>
|
||||
<b>Load a different image:</b> the Load Image button, or just <b>paste</b> / <b>drag</b> an image straight in here<br><br>
|
||||
|
||||
<b style="color:#f66744;">CROP SIZE</b> (sidebar + node)<br>
|
||||
<b>Keep shape:</b> scales your area so its long side hits Target, no stretching (best quality)<br>
|
||||
<b>Force square:</b> always a Target×Target square<br>
|
||||
<b>Free:</b> natural size, just rounded to the Multiple<br>
|
||||
<b>Context margin:</b> how much of the surroundings to include around your paint<br><br>
|
||||
|
||||
<b style="color:#f66744;">SEAM — HOW IT BLENDS BACK</b><br>
|
||||
<b>Softness:</b> how far the paste fades into the original at the edge (the live orange tint previews it)<br>
|
||||
<b>Mask grow:</b> expands the painted area a little before cropping<br>
|
||||
<b>Blend mode — Mask:</b> only the area you painted is replaced, the rest of the crop keeps the original (the normal inpaint)<br>
|
||||
<b>Blend mode — Whole crop:</b> the <i>entire</i> box is replaced with the model's version — use when the model also relit / changed the surroundings, or for an img2img pass<br><br>
|
||||
|
||||
<b style="color:#f66744;">KEYS</b><br>
|
||||
<b>Undo / Redo:</b> <kbd>Ctrl+Z</kbd> / <kbd>Ctrl+Shift+Z</kbd> ·
|
||||
<b>Save:</b> <kbd>Ctrl+S</kbd> · <b>Close:</b> <kbd>Escape</kbd>`,
|
||||
});
|
||||
this.layout = layout;
|
||||
layout.onSaveToDisk = () => { this._diskSavePending = true; this._save(); };
|
||||
|
||||
@@ -212,7 +212,10 @@ proto._loadMaskFromURL = function (url) {
|
||||
// bail if a newer open/load superseded this, OR the user already started working
|
||||
// (painting / has undo history / a painted bbox) - don't clobber fresh strokes.
|
||||
if (token !== this._loadToken) return;
|
||||
if (this._painting || this._undo?.length || this._bbox) return;
|
||||
if (this._painting || this._undo?.length || this._bbox) {
|
||||
this._setStatus("Kept your new strokes - the saved mask wasn't reloaded.");
|
||||
return;
|
||||
}
|
||||
this._ensureMaskCanvas();
|
||||
const tmp = document.createElement("canvas");
|
||||
tmp.width = this.imgW; tmp.height = this.imgH;
|
||||
@@ -334,9 +337,58 @@ proto._stopBrushHold = function () {
|
||||
if (this._holdRaf) { cancelAnimationFrame(this._holdRaf); this._holdRaf = null; }
|
||||
};
|
||||
|
||||
// ── paste / drag-drop a source image INTO the open editor ──────────────────
|
||||
// Loads the dropped/pasted image as the new source (same as the Load Image
|
||||
// button: _loadImageFromDataURL + onLoadImage to disconnect the upstream wire).
|
||||
// All three listen at window-capture, gated on the overlay being live, and
|
||||
// preventDefault + stop so ComfyUI doesn't spawn its own Load Image node behind us.
|
||||
proto._bindDropPaste = function () {
|
||||
const alive = () => !!this.el.overlay?.isConnected;
|
||||
const loadFile = (file) => {
|
||||
if (!file || !file.type?.startsWith("image/")) return false;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => { this._loadImageFromDataURL(ev.target.result); this.onLoadImage?.(); };
|
||||
reader.readAsDataURL(file);
|
||||
return true;
|
||||
};
|
||||
this._pasteHandler = (e) => {
|
||||
if (!alive()) return;
|
||||
const t = e.target;
|
||||
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
|
||||
const it = Array.from(e.clipboardData?.items || []).find((x) => x.type?.startsWith("image/"));
|
||||
if (!it) return;
|
||||
const blob = it.getAsFile();
|
||||
if (!blob) return;
|
||||
e.preventDefault(); e.stopImmediatePropagation();
|
||||
loadFile(blob);
|
||||
};
|
||||
this._dragOverHandler = (e) => {
|
||||
if (!alive()) return;
|
||||
if (e.dataTransfer?.types?.includes("Files")) { e.preventDefault(); e.stopImmediatePropagation(); }
|
||||
};
|
||||
this._dropHandler = (e) => {
|
||||
if (!alive()) return;
|
||||
const file = e.dataTransfer?.files?.[0];
|
||||
if (!file || !file.type?.startsWith("image/")) return;
|
||||
e.preventDefault(); e.stopImmediatePropagation();
|
||||
loadFile(file);
|
||||
};
|
||||
window.addEventListener("paste", this._pasteHandler, true);
|
||||
window.addEventListener("dragover", this._dragOverHandler, true);
|
||||
window.addEventListener("drop", this._dropHandler, true);
|
||||
};
|
||||
|
||||
proto._unbindDropPaste = function () {
|
||||
if (this._pasteHandler) window.removeEventListener("paste", this._pasteHandler, true);
|
||||
if (this._dragOverHandler) window.removeEventListener("dragover", this._dragOverHandler, true);
|
||||
if (this._dropHandler) window.removeEventListener("drop", this._dropHandler, true);
|
||||
this._pasteHandler = this._dragOverHandler = this._dropHandler = null;
|
||||
};
|
||||
|
||||
proto._unbindKeys = function () {
|
||||
this._stopBrushHold();
|
||||
this._detachStroke?.(); // detach any live stroke window-listeners (close mid-drag)
|
||||
this._unbindDropPaste();
|
||||
if (this._keyHandler) window.removeEventListener("keydown", this._keyHandler, { capture: true });
|
||||
if (this._keyUpHandler) window.removeEventListener("keyup", this._keyUpHandler, { capture: true });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user