- Image editor: layers with masks/blend modes, lightweight groups, Camera Raw scoped to the selected layer, full-frame i2i, Copy Merged, Photoshop keyboard shortcuts, and a large batch of Photoshop-parity bug fixes. - LoadImage right-click "Open in PromptChain editor" bridge (edit -> lineage child re-pointed onto the node). - Krea 2 Turbo/Realism templates and editor modal integration; remove the krea2 Text-to-Image 3D Regional template. - ControlNet depth + 3D-pose: wire Poser true-depth straight into Apply ControlNet on pose paths. - Sidebar: drag-to-LoadImage now uploads to the input root so the widget value is a valid bare filename; "Copy as path" emits native Windows backslashes. - Contributor/relicensing docs and packaging tweaks.
226 lines
8.5 KiB
JavaScript
226 lines
8.5 KiB
JavaScript
/**
|
|
* Asset Drop Handler
|
|
*
|
|
* Intercepts drag-and-drop from the PromptChain sidebar to ComfyUI nodes.
|
|
* When an asset (image/video) is dragged from the sidebar, we:
|
|
* 1. Detect the drop target node
|
|
* 2. Fetch the asset file from userdata
|
|
* 3. Upload it via /upload/image
|
|
* 4. Update the node's widget value
|
|
*/
|
|
|
|
import { api } from "../../../scripts/api.js";
|
|
import { app } from "../../../scripts/app.js";
|
|
|
|
// Debug flag - set to true for verbose logging
|
|
const DEBUG = false;
|
|
const log = (...args) => { if (DEBUG) console.log("[PromptChain:AssetDrop]", ...args); };
|
|
|
|
// Track if we're currently dragging an asset
|
|
let currentAssetDrag = null;
|
|
|
|
/**
|
|
* Handle dragover on the canvas/graph area.
|
|
* Sets appropriate drop effect for asset drags.
|
|
*/
|
|
function handleDragOver(e) {
|
|
// Check if this is a PromptChain asset drag
|
|
if (!e.dataTransfer.types.includes("application/x-promptchain-asset")) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = "copy";
|
|
}
|
|
|
|
/**
|
|
* Handle drop on the canvas.
|
|
* If dropping on a node that accepts images, upload and set the value.
|
|
*/
|
|
async function handleDrop(e) {
|
|
// Check if this is a PromptChain asset drag
|
|
if (!e.dataTransfer.types.includes("application/x-promptchain-asset")) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Parse the asset data
|
|
let assetData;
|
|
try {
|
|
const dataStr = e.dataTransfer.getData("application/x-promptchain-asset");
|
|
assetData = JSON.parse(dataStr);
|
|
} catch (err) {
|
|
console.error("[PromptChain] Failed to parse asset drag data:", err);
|
|
return;
|
|
}
|
|
|
|
if (!assetData || assetData.type !== "asset" || !assetData.path) {
|
|
console.warn("[PromptChain] Invalid asset drag data:", assetData);
|
|
return;
|
|
}
|
|
|
|
// Find the node under the drop point
|
|
const canvas = app.canvas;
|
|
if (!canvas) {
|
|
console.warn("[PromptChain] No canvas available");
|
|
return;
|
|
}
|
|
|
|
// Convert screen coordinates to canvas coordinates
|
|
const rect = canvas.canvas.getBoundingClientRect();
|
|
const canvasX = (e.clientX - rect.left) / canvas.ds.scale - canvas.ds.offset[0];
|
|
const canvasY = (e.clientY - rect.top) / canvas.ds.scale - canvas.ds.offset[1];
|
|
|
|
const scope = assetData.scope || "output";
|
|
|
|
// Smart switch: dropping an output image onto a STRICTLY EMPTY graph restores
|
|
// its embedded ComfyUI workflow (the litegraph "workflow" PNG chunk) instead of
|
|
// adding a Load Image node — mirroring ComfyUI's native PNG file-drop. Gated to
|
|
// zero nodes because loadGraphData clean-wipes the canvas (same as the native
|
|
// drop), so on a populated graph that would silently destroy the user's work.
|
|
// The endpoint already exists (inpaint/upscale use it) and NaN-defuses the JSON.
|
|
if (!app.graph?._nodes?.length && /\.(png|webp)$/i.test(assetData.path)) {
|
|
try {
|
|
const wfResp = await api.fetchApi(
|
|
`/promptchain/image-workflow?scope=${scope}&path=${encodeURIComponent(assetData.path)}`
|
|
);
|
|
if (wfResp.ok) {
|
|
const { workflow } = await wfResp.json();
|
|
if (workflow) {
|
|
await app.loadGraphData(workflow, true, true, assetData.name || assetData.path.split("/").pop());
|
|
app.extensionManager?.toast?.add?.({ severity: "success", summary: "Restored workflow from image", life: 2500 });
|
|
return;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// any failure (no workflow chunk, fetch error) falls through to Load Image
|
|
log("workflow restore failed; falling back to Load Image", err);
|
|
}
|
|
}
|
|
|
|
try {
|
|
// fetch image via browse preview
|
|
const fetchResponse = await api.fetchApi(
|
|
`/promptchain/browse/preview?scope=${scope}&path=${encodeURIComponent(assetData.path)}`
|
|
);
|
|
if (!fetchResponse.ok) throw new Error(`Failed to fetch: ${fetchResponse.status}`);
|
|
const blob = await fetchResponse.blob();
|
|
|
|
// Upload into the input ROOT, not a subfolder. Stock LoadImage's INPUT_TYPES
|
|
// only enumerates top-level input files, so a "subfolder/name" combo value is
|
|
// never in its option list — the node loads flagged/errored even though the
|
|
// backend's VALIDATE_INPUTS (exists_annotated_filepath) still finds the file
|
|
// and the graph runs. A bare filename keeps the combo valid.
|
|
const filename = assetData.path.split("/").pop();
|
|
const file = new File([blob], filename, { type: blob.type });
|
|
const formData = new FormData();
|
|
formData.append("image", file);
|
|
formData.append("type", "input");
|
|
formData.append("overwrite", "true");
|
|
const uploadResponse = await api.fetchApi("/upload/image", { method: "POST", body: formData });
|
|
if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.status}`);
|
|
const result = await uploadResponse.json();
|
|
const uploadedPath = result.subfolder ? `${result.subfolder}/${result.name}` : result.name;
|
|
|
|
// find target node, or create a new LoadImage
|
|
let node = app.graph.getNodeOnPos(canvasX, canvasY);
|
|
let imageWidget = node ? findImageWidget(node) : null;
|
|
|
|
if (!imageWidget) {
|
|
// no suitable target — create LoadImage at drop position
|
|
const LG = window.LiteGraph || globalThis.LiteGraph;
|
|
if (!LG || !app.graph) return;
|
|
node = LG.createNode("LoadImage");
|
|
if (!node) return;
|
|
node.pos = [canvasX, canvasY];
|
|
app.graph.add(node);
|
|
imageWidget = findImageWidget(node);
|
|
}
|
|
|
|
if (imageWidget) {
|
|
if (imageWidget.options?.values && !imageWidget.options.values.includes(uploadedPath)) {
|
|
imageWidget.options.values.push(uploadedPath);
|
|
}
|
|
imageWidget.value = uploadedPath;
|
|
imageWidget.callback?.(uploadedPath);
|
|
}
|
|
|
|
node.setDirtyCanvas(true, true);
|
|
canvas.selectNode(node);
|
|
app.graph.setDirtyCanvas(true, true);
|
|
|
|
// notify sidebar that input folder changed (uploaded a file)
|
|
window.dispatchEvent(new CustomEvent("promptchain:input-changed", {
|
|
detail: { name: result.name, subfolder: result.subfolder || "" },
|
|
}));
|
|
|
|
} catch (err) {
|
|
console.error("[PromptChain] Failed to drop asset:", err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find an image upload widget on a node.
|
|
* Looks for combo widgets with image_upload flag.
|
|
*/
|
|
function findImageWidget(node) {
|
|
if (!node.widgets) return null;
|
|
|
|
for (const widget of node.widgets) {
|
|
// Check for image_upload flag in widget options
|
|
if (widget.options?.image_upload === true) {
|
|
return widget;
|
|
}
|
|
|
|
// Also check if this is a COMBO widget named "image" (common pattern)
|
|
if (widget.type === "combo" && widget.name === "image") {
|
|
return widget;
|
|
}
|
|
|
|
// Check for LoadImage pattern - first combo widget
|
|
if (node.type === "LoadImage" && widget.type === "combo") {
|
|
return widget;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Install global drag-and-drop handlers for asset drops.
|
|
* Call this once during extension setup.
|
|
*/
|
|
export function setupAssetDropHandler() {
|
|
// We need to intercept at the document level to catch drops on the canvas
|
|
// The canvas is inside several nested elements
|
|
|
|
const waitForCanvas = () => {
|
|
const canvasEl = document.querySelector("canvas.lgraphcanvas") ||
|
|
document.querySelector("#graph-canvas canvas") ||
|
|
app.canvas?.canvas;
|
|
|
|
if (!canvasEl) {
|
|
requestAnimationFrame(waitForCanvas);
|
|
return;
|
|
}
|
|
|
|
// Add listeners to the canvas element
|
|
canvasEl.addEventListener("dragover", handleDragOver, true);
|
|
canvasEl.addEventListener("drop", handleDrop, true);
|
|
|
|
// Also add to parent containers to ensure we catch the event
|
|
let parent = canvasEl.parentElement;
|
|
while (parent && parent !== document.body) {
|
|
parent.addEventListener("dragover", handleDragOver, true);
|
|
parent.addEventListener("drop", handleDrop, true);
|
|
parent = parent.parentElement;
|
|
}
|
|
|
|
log("Asset drop handler installed");
|
|
};
|
|
|
|
requestAnimationFrame(waitForCanvas);
|
|
}
|