Files
1hew-ComfyUI-1hewNodes/web/js/load_ps.js
T
1hew d1dbe3ddfa v3.15.0 chore(release): 发布 Load PS 与批处理节点更新
整合 Load PS、批处理取样节点重构与文本列表解析增强,统一这次用户可见功能调整的发布入口。
2026-05-09 00:19:31 +08:00

490 lines
18 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { collectDropPayload } from "./core/media_utils.js";
import { registerExtensionOnce } from "./core/runtime.js";
import { attachCanvasUploadHandlers } from "./core/upload_drop_runtime.js";
function isValidPSFile(file) {
if (!file) return false;
const name = String(file.name || file.path || file || "").toLowerCase();
return name.endsWith(".psd") || name.endsWith(".psb");
}
function getDroppedPath(file) {
if (!file || typeof file !== "object") return "";
return String(file.path || file.fullPath || file.webkitRelativePath || "").trim();
}
function isUploadablePSFile(file) {
return Boolean(
file
&& typeof file === "object"
&& isValidPSFile(file)
&& typeof file.name === "string"
&& (typeof file.arrayBuffer === "function" || typeof file.stream === "function" || typeof file.size === "number")
);
}
function normalizeUrl(src) {
if (!src) return "";
try {
const url = new URL(src, window.location.href);
url.searchParams.delete("t");
return url.toString();
} catch {
return String(src);
}
}
function uploadPSFileWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
const form = new FormData();
form.append("files", file, file.name || "file.psd");
const xhr = new XMLHttpRequest();
xhr.open("POST", "/1hew/upload_ps", true);
xhr.upload.onprogress = (event) => {
if (!event.lengthComputable) {
onProgress?.(null);
return;
}
const percent = Math.max(0, Math.min(100, Math.round((event.loaded / event.total) * 100)));
onProgress?.(percent);
};
xhr.onload = () => {
if (xhr.status !== 200) {
reject(new Error(`upload failed: ${xhr.status}`));
return;
}
try {
resolve(JSON.parse(xhr.responseText || "{}"));
} catch (error) {
reject(error);
}
};
xhr.onerror = () => reject(new Error("upload failed"));
xhr.onabort = () => reject(new Error("upload aborted"));
xhr.send(form);
});
}
const loadPSNodes = new Set();
let globalDropBridgeInstalled = false;
const LOAD_PS_GLOBAL_DROP_HANDLED = "__comfy1hewLoadPSGlobalHandled";
function hasPSDFile(event) {
const files = Array.from(event?.dataTransfer?.files || []);
return files.some(isValidPSFile);
}
function firstPSDFile(event) {
return Array.from(event?.dataTransfer?.files || []).find(isValidPSFile) || null;
}
function firstPSDTextPath(event) {
const dataTransfer = event?.dataTransfer;
if (!dataTransfer) return "";
const types = Array.from(dataTransfer.types || []);
for (const type of types) {
if (!type || type === "Files") continue;
try {
const text = String(dataTransfer.getData(type) || "").trim();
const path = text.split(/\r?\n/).find((line) => isValidPSFile(line));
if (path) return path.replace(/^file:\/+/i, "");
} catch {}
}
return "";
}
function hasPotentialFileDrop(event) {
if (hasPSDFile(event)) return true;
const items = Array.from(event?.dataTransfer?.items || []);
return items.some((item) => item?.kind === "file");
}
async function getPSDFromDropEvent(event) {
const textPath = firstPSDTextPath(event);
if (textPath) return textPath;
const file = firstPSDFile(event);
if (file) return file;
try {
const payload = await collectDropPayload(event);
const pair = (payload?.pairs || []).find((item) => isValidPSFile(item?.file));
if (pair?.file) return pair.file;
} catch {}
return null;
}
function setWidgetValue(widget, value) {
if (!widget) return;
widget.value = value;
try {
widget.callback?.(value);
} catch {}
}
function clientToGraph(event) {
const canvas = app?.canvas;
const canvasEl = canvas?.canvas;
const ds = canvas?.ds;
if (!canvasEl || !ds) return null;
const rect = canvasEl.getBoundingClientRect();
return [
(event.clientX - rect.left) / ds.scale - ds.offset[0],
(event.clientY - rect.top) / ds.scale - ds.offset[1],
];
}
function nodeContainsPoint(node, point) {
if (!node || !point) return false;
const pos = node.pos || [0, 0];
const size = node.size || node._size || [0, 0];
return (
point[0] >= pos[0]
&& point[1] >= pos[1]
&& point[0] <= pos[0] + size[0]
&& point[1] <= pos[1] + size[1]
);
}
function getLoadPSNodeAtEvent(event) {
const point = clientToGraph(event);
const nodes = Array.from(loadPSNodes).filter((node) => node?.graph);
if (point) {
for (let i = nodes.length - 1; i >= 0; i -= 1) {
if (nodeContainsPoint(nodes[i], point)) {
return nodes[i];
}
}
}
return nodes.length === 1 ? nodes[0] : null;
}
function installGlobalDropBridge() {
if (globalDropBridgeInstalled) return;
globalDropBridgeInstalled = true;
window.addEventListener("dragover", (event) => {
if (!hasPotentialFileDrop(event)) return;
const node = getLoadPSNodeAtEvent(event);
if (!node) return;
event.preventDefault();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "copy";
}
}, true);
window.addEventListener("drop", (event) => {
if (!hasPotentialFileDrop(event)) return;
const node = getLoadPSNodeAtEvent(event);
if (!node || typeof node._comfy1hewUploadPSFile !== "function") return;
event.preventDefault();
if (event[LOAD_PS_GLOBAL_DROP_HANDLED]) return;
event[LOAD_PS_GLOBAL_DROP_HANDLED] = true;
void (async () => {
const file = await getPSDFromDropEvent(event);
if (file) {
await node._comfy1hewUploadPSFile(file);
}
})();
}, true);
}
registerExtensionOnce("__comfy1hewLoadPSExtensionRegistered", () => app.registerExtension({
name: "ComfyUI-1hewNodes.load_ps",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== "1hew_LoadPS") {
return;
}
installGlobalDropBridge();
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const r = onConfigure ? onConfigure.apply(this, arguments) : undefined;
this._comfy1hewLoadPSPendingPreview = true;
this._comfy1hewSchedulePSPreviewUpdate?.(0);
return r;
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const fileWidget = this.widgets.find((w) => w.name === "file");
const indexWidget = this.widgets.find((w) => w.name === "index");
const includeHiddenWidget = this.widgets.find((w) => w.name === "include_hidden");
const groupModeWidget = this.widgets.find((w) => w.name === "group_mode");
const outputModeWidget = this.widgets.find((w) => w.name === "output_mode");
const previewWidget = this.widgets.find((w) => w.name === "preview");
this._comfy1hewLoadPSUpdateTimer = null;
this._comfy1hewLoadPSLastImgSrc = undefined;
this._comfy1hewPreviewImageEl = null;
let uploadWidget = null;
let uploading = false;
const computeStateKey = () => {
const p = String(fileWidget ? fileWidget.value : "");
const i = String(indexWidget ? indexWidget.value : 0);
const h = String(includeHiddenWidget ? includeHiddenWidget.value : false);
const g = String(groupModeWidget ? groupModeWidget.value : "layer");
const o = String(outputModeWidget ? outputModeWidget.value : "all_layers");
const v = String(previewWidget ? previewWidget.value : false);
return `${p}||${i}||${h}||${g}||${o}||${v}`;
};
const clearPreview = () => {
this._comfy1hewPreviewImageEl = null;
this._comfy1hewLoadPSLastImgSrc = undefined;
this.imgs = null;
app?.graph?.setDirtyCanvas?.(true, true);
};
const updatePreview = async () => {
this._comfy1hewLoadPSReqId = (this._comfy1hewLoadPSReqId || 0) + 1;
const reqId = this._comfy1hewLoadPSReqId;
const file = String(fileWidget?.value || "").trim();
if (!previewWidget?.value) {
clearPreview();
return;
}
if (!file) {
clearPreview();
return;
}
const params = new URLSearchParams({
file,
index: String(indexWidget?.value ?? 0),
include_hidden: includeHiddenWidget?.value ? "true" : "false",
group_mode: String(groupModeWidget?.value || "layer"),
output_mode: String(outputModeWidget?.value || "all_layers"),
t: String(Date.now()),
});
const url = `/1hew/view_ps?${params.toString()}`;
const desiredNorm = normalizeUrl(url);
const currentNorm = normalizeUrl(this._comfy1hewLoadPSLastImgSrc);
if (currentNorm === desiredNorm && Array.isArray(this.imgs) && this.imgs[0]) {
app?.graph?.setDirtyCanvas?.(true, true);
return;
}
const img = new Image();
img.crossOrigin = "anonymous";
img.decoding = "async";
img.onload = () => {
if (reqId !== this._comfy1hewLoadPSReqId) return;
this._comfy1hewPreviewImageEl = img;
this.imgs = [img];
try {
const imageUrl = new URL(img.src);
imageUrl.searchParams.delete("t");
this._comfy1hewLoadPSLastImgSrc = imageUrl.toString();
} catch {
this._comfy1hewLoadPSLastImgSrc = img.src;
}
try {
this.setSizeForImage?.();
} catch {}
app?.graph?.setDirtyCanvas?.(true, true);
};
img.onerror = () => {
if (reqId !== this._comfy1hewLoadPSReqId) return;
this._comfy1hewPreviewImageEl = null;
this._comfy1hewLoadPSLastImgSrc = undefined;
this.imgs = null;
app?.graph?.setDirtyCanvas?.(true, true);
};
img.src = url;
};
const schedulePreviewUpdate = (delay = 0) => {
try {
if (this._comfy1hewLoadPSUpdateTimer) {
clearTimeout(this._comfy1hewLoadPSUpdateTimer);
}
} catch {}
this._comfy1hewLoadPSUpdateTimer = setTimeout(() => {
this._comfy1hewLoadPSUpdateTimer = null;
this._comfy1hewLoadPSPendingPreview = false;
this._comfy1hewLoadPSStateKey = computeStateKey();
void updatePreview();
}, delay);
};
this._comfy1hewSchedulePSPreviewUpdate = schedulePreviewUpdate;
this.updatePreview = updatePreview;
const applyFilePath = (path) => {
const finalPath = String(path || "").trim();
if (!finalPath) return false;
setWidgetValue(fileWidget, finalPath);
setWidgetValue(indexWidget, 0);
this._comfy1hewLoadPSStateKey = computeStateKey();
if (previewWidget?.value) {
schedulePreviewUpdate(0);
} else {
clearPreview();
}
app?.graph?.setDirtyCanvas?.(true, true);
return true;
};
const uploadFile = async (file) => {
if (!isValidPSFile(file) || uploading) return;
const directPath = typeof file === "string" ? file : getDroppedPath(file);
if (directPath && applyFilePath(directPath)) {
return;
}
if (!isUploadablePSFile(file)) {
if (uploadWidget) {
uploadWidget.name = "drop psd file only";
app?.graph?.setDirtyCanvas?.(true, true);
setTimeout(() => {
if (uploadWidget && uploadWidget.name === "drop psd file only") {
uploadWidget.name = "choose psd to upload";
app?.graph?.setDirtyCanvas?.(true, true);
}
}, 1200);
}
return;
}
uploading = true;
const oldName = uploadWidget?.name || "choose psd to upload";
try {
if (uploadWidget) {
uploadWidget.name = "uploading psd...0%";
}
app?.graph?.setDirtyCanvas?.(true, true);
const data = await uploadPSFileWithProgress(file, (percent) => {
if (!uploadWidget) return;
uploadWidget.name = percent === null
? "uploading psd..."
: `uploading psd...${percent}%`;
app?.graph?.setDirtyCanvas?.(true, true);
});
if (!data?.path) return;
applyFilePath(data.path);
} catch (error) {
console.error("[1hewNodes.load_ps] upload failed:", error);
if (uploadWidget) {
uploadWidget.name = "upload failed";
}
} finally {
uploading = false;
setTimeout(() => {
if (uploadWidget && uploadWidget.name === "upload failed") {
uploadWidget.name = oldName;
app?.graph?.setDirtyCanvas?.(true, true);
} else if (uploadWidget && uploadWidget.name.startsWith("uploading psd")) {
uploadWidget.name = oldName;
app?.graph?.setDirtyCanvas?.(true, true);
}
}, 1200);
}
app?.graph?.setDirtyCanvas?.(true, true);
};
this._comfy1hewUploadPSFile = uploadFile;
const fileInputEl = document.createElement("input");
fileInputEl.type = "file";
fileInputEl.accept = ".psd,.psb";
fileInputEl.style.display = "none";
const openFilePicker = () => {
try {
fileInputEl.value = "";
} catch {}
fileInputEl.click();
};
uploadWidget = this.addWidget(
"button",
"choose psd to upload",
"psd",
() => {
app.canvas.node_widget = null;
openFilePicker();
}
);
uploadWidget.serialize = false;
fileInputEl.addEventListener("change", async () => {
const file = fileInputEl.files && fileInputEl.files[0];
await uploadFile(file);
});
attachCanvasUploadHandlers({
node: this,
setDragPassthrough: () => {},
resetDragPassthrough: () => {},
onFileDrop: (file) => {
void uploadFile(file);
},
onEventDrop: (event) => {
if (event?.[LOAD_PS_GLOBAL_DROP_HANDLED]) {
return true;
}
void (async () => {
try {
const file = await getPSDFromDropEvent(event);
if (file) {
await uploadFile(file);
}
} catch {}
})();
return true;
},
});
const watchWidgets = [
fileWidget,
indexWidget,
includeHiddenWidget,
groupModeWidget,
outputModeWidget,
previewWidget,
];
for (const widget of watchWidgets) {
if (widget) widget.callback = () => schedulePreviewUpdate(0);
}
this._comfy1hewLoadPSStateKey = computeStateKey();
if (!this._comfy1hewLoadPSPoller) {
this._comfy1hewLoadPSPoller = setInterval(() => {
try {
const nextKey = computeStateKey();
if (nextKey === this._comfy1hewLoadPSStateKey) return;
this._comfy1hewLoadPSStateKey = nextKey;
schedulePreviewUpdate(0);
} catch {}
}, 200);
}
const onRemoved = this.onRemoved;
this.onRemoved = function () {
try {
loadPSNodes.delete(this);
if (this._comfy1hewLoadPSPoller) {
clearInterval(this._comfy1hewLoadPSPoller);
this._comfy1hewLoadPSPoller = null;
}
if (this._comfy1hewLoadPSUpdateTimer) {
clearTimeout(this._comfy1hewLoadPSUpdateTimer);
this._comfy1hewLoadPSUpdateTimer = null;
}
this._comfy1hewPreviewImageEl = null;
this.imgs = null;
} catch {}
return onRemoved ? onRemoved.apply(this, arguments) : undefined;
};
if ((fileWidget && fileWidget.value) || this._comfy1hewLoadPSPendingPreview) {
schedulePreviewUpdate(0);
}
loadPSNodes.add(this);
return r;
};
},
}));