749 lines
30 KiB
JavaScript
749 lines
30 KiB
JavaScript
var __defProp = Object.defineProperty;
|
||
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
||
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
||
import { app } from "../../../scripts/app.js";
|
||
import { api } from "../../../scripts/api.js";
|
||
const POST_MESSAGE_ORIGIN = window.location.origin;
|
||
const MODEL_3D_NODES = ["Load3D", "Preview3D", "SaveGLB"];
|
||
const IMAGE_NODES = ["LoadImage"];
|
||
function isModel3DNode(node) {
|
||
var _a;
|
||
if (!node || typeof node !== "object") return false;
|
||
const n = node;
|
||
const typedNode = node;
|
||
const nodeClass = (_a = typedNode == null ? void 0 : typedNode.constructor) == null ? void 0 : _a.comfyClass;
|
||
if (nodeClass && MODEL_3D_NODES.includes(nodeClass)) return true;
|
||
if (n.widgets) {
|
||
const has3DWidget = n.widgets.some(
|
||
(w) => w.name === "model_file" || w.name === "mesh" || w.name === "3d_model" || w.value && typeof w.value === "string" && /\.(glb|gltf|fbx|obj)$/i.test(w.value)
|
||
);
|
||
if (has3DWidget) return true;
|
||
}
|
||
return false;
|
||
}
|
||
function getModelUrlFromNode(node) {
|
||
var _a, _b, _c, _d;
|
||
const nodeClass = (_a = node.constructor) == null ? void 0 : _a.comfyClass;
|
||
if (nodeClass === "Preview3D") {
|
||
const lastModelFile = (_b = node.properties) == null ? void 0 : _b["Last Time Model File"];
|
||
if (lastModelFile) return buildModelUrl(lastModelFile, "output");
|
||
}
|
||
if ((_c = node.images) == null ? void 0 : _c[0]) {
|
||
const model = node.images[0];
|
||
const params = new URLSearchParams({
|
||
filename: model.filename,
|
||
type: model.type || "input",
|
||
subfolder: model.subfolder || ""
|
||
});
|
||
return api.apiURL(`/view?${params.toString()}`);
|
||
}
|
||
const modelWidget = (_d = node.widgets) == null ? void 0 : _d.find(
|
||
(w) => w.name === "model_file" || w.name === "mesh" || w.name === "3d_model" || w.name === "model"
|
||
);
|
||
if (modelWidget == null ? void 0 : modelWidget.value) return buildModelUrl(modelWidget.value, "input");
|
||
return null;
|
||
}
|
||
function buildModelUrl(value, defaultType) {
|
||
const match = value.match(/^(.+?)(?:\s*\[(\w+)\])?$/);
|
||
if (!match) return null;
|
||
const fullPath = match[1];
|
||
const type = match[2] || defaultType;
|
||
const lastSlash = fullPath.lastIndexOf("/");
|
||
const subfolder = lastSlash > -1 ? fullPath.substring(0, lastSlash) : "";
|
||
const filename = lastSlash > -1 ? fullPath.substring(lastSlash + 1) : fullPath;
|
||
const params = new URLSearchParams({ filename, type, subfolder });
|
||
return api.apiURL(`/view?${params.toString()}`);
|
||
}
|
||
function captureFromMesh2MotionIframe(iframe) {
|
||
return new Promise((resolve, reject) => {
|
||
var _a;
|
||
const timeout = setTimeout(() => {
|
||
window.removeEventListener("message", handler);
|
||
reject(new Error("Mesh2Motion capture timeout"));
|
||
}, 15e3);
|
||
const handler = (event) => {
|
||
var _a2;
|
||
if (event.origin !== POST_MESSAGE_ORIGIN || event.source !== iframe.contentWindow) return;
|
||
if (((_a2 = event.data) == null ? void 0 : _a2.type) === "mesh2motion:captureResult") {
|
||
clearTimeout(timeout);
|
||
window.removeEventListener("message", handler);
|
||
resolve(event.data.data);
|
||
}
|
||
};
|
||
window.addEventListener("message", handler);
|
||
(_a = iframe.contentWindow) == null ? void 0 : _a.postMessage({ type: "mesh2motion:capture" }, POST_MESSAGE_ORIGIN);
|
||
});
|
||
}
|
||
function captureVideoFromMesh2MotionIframe(iframe, presetName, width, height) {
|
||
return new Promise((resolve, reject) => {
|
||
var _a;
|
||
const timeout = setTimeout(() => {
|
||
window.removeEventListener("message", handler);
|
||
reject(new Error("Video capture timeout (120s)"));
|
||
}, 12e4);
|
||
const handler = (event) => {
|
||
var _a2;
|
||
if (event.origin !== POST_MESSAGE_ORIGIN || event.source !== iframe.contentWindow) return;
|
||
if (((_a2 = event.data) == null ? void 0 : _a2.type) === "mesh2motion:captureVideoResult") {
|
||
clearTimeout(timeout);
|
||
window.removeEventListener("message", handler);
|
||
const result = event.data.data;
|
||
if (result.error) {
|
||
reject(new Error(result.error));
|
||
} else {
|
||
resolve({ videoPath: result.videoPath, fps: result.fps });
|
||
}
|
||
}
|
||
};
|
||
window.addEventListener("message", handler);
|
||
(_a = iframe.contentWindow) == null ? void 0 : _a.postMessage({
|
||
type: "mesh2motion:captureVideoFrames",
|
||
data: { presetName, width, height }
|
||
}, POST_MESSAGE_ORIGIN);
|
||
});
|
||
}
|
||
async function uploadMesh2MotionTempImage(dataUrl) {
|
||
const blob = await fetch(dataUrl).then((r) => r.blob());
|
||
const name = `mesh2motion_${Date.now()}.png`;
|
||
const file = new File([blob], name, { type: "image/png" });
|
||
const body = new FormData();
|
||
body.append("image", file);
|
||
body.append("subfolder", "mesh2motion");
|
||
body.append("type", "temp");
|
||
const resp = await api.fetchApi("/upload/image", {
|
||
method: "POST",
|
||
body
|
||
});
|
||
if (resp.status !== 200) {
|
||
throw new Error(`Upload failed: ${resp.status}`);
|
||
}
|
||
return await resp.json();
|
||
}
|
||
const PROJECT_STATE_VERSION = 2;
|
||
const PROJECT_KEY = "mesh2motion_project";
|
||
function loadProject(props) {
|
||
if (!props) return null;
|
||
const v2 = props[PROJECT_KEY];
|
||
if (v2 && typeof v2 === "object" && v2.version === PROJECT_STATE_VERSION) {
|
||
return v2;
|
||
}
|
||
return null;
|
||
}
|
||
function saveProject(node, state) {
|
||
if (!node.properties) node.properties = {};
|
||
node.properties[PROJECT_KEY] = state;
|
||
}
|
||
function createMesh2MotionExploreWidget(node) {
|
||
const container = document.createElement("div");
|
||
container.style.cssText = "width:100%;height:100%;position:relative;overflow:hidden;";
|
||
const iframe = document.createElement("iframe");
|
||
iframe.src = "/mesh2motion/index-comfyui.html?comfyui=true&theme=dark";
|
||
iframe.style.cssText = "width:100%;height:100%;border:none;display:block;";
|
||
iframe.allow = "cross-origin-isolated";
|
||
container.appendChild(iframe);
|
||
node._mesh2motionIframe = iframe;
|
||
node._mesh2motionReady = false;
|
||
const readyHandler = (event) => {
|
||
var _a, _b, _c, _d;
|
||
if (event.origin !== POST_MESSAGE_ORIGIN || event.source !== iframe.contentWindow) return;
|
||
if (((_a = event.data) == null ? void 0 : _a.type) === "mesh2motion:ready") {
|
||
node._mesh2motionReady = true;
|
||
const project = loadProject(node.properties);
|
||
if (project) {
|
||
(_b = iframe.contentWindow) == null ? void 0 : _b.postMessage(
|
||
{ type: "mesh2motion:restoreProject", data: project },
|
||
POST_MESSAGE_ORIGIN
|
||
);
|
||
}
|
||
sendInitialBooleanStates();
|
||
sendPreviewState();
|
||
}
|
||
if (((_c = event.data) == null ? void 0 : _c.type) === "mesh2motion:projectStateChanged" && ((_d = event.data) == null ? void 0 : _d.data)) {
|
||
saveProject(node, event.data.data);
|
||
}
|
||
};
|
||
window.addEventListener("message", readyHandler);
|
||
const origOnRemovedExplore = node.onRemoved;
|
||
node.onRemoved = function() {
|
||
window.removeEventListener("message", readyHandler);
|
||
origOnRemovedExplore == null ? void 0 : origOnRemovedExplore.call(this);
|
||
};
|
||
const BOOLEAN_WIDGETS = [
|
||
{ widget: "show_skeleton", type: "mesh2motion:setShowSkeleton" },
|
||
{ widget: "mirror_animations", type: "mesh2motion:setMirrorAnimations" },
|
||
{ widget: "checker_room", type: "mesh2motion:setCheckerRoom" }
|
||
];
|
||
const hookBooleanWidget = (widgetName, messageType) => {
|
||
var _a;
|
||
const widget = (_a = node.widgets) == null ? void 0 : _a.find((w2) => w2.name === widgetName);
|
||
if (widget) {
|
||
const origCallback = widget.callback;
|
||
widget.callback = (value) => {
|
||
var _a2;
|
||
origCallback == null ? void 0 : origCallback(value);
|
||
if (node._mesh2motionReady) {
|
||
(_a2 = iframe.contentWindow) == null ? void 0 : _a2.postMessage(
|
||
{ type: messageType, data: { value } },
|
||
POST_MESSAGE_ORIGIN
|
||
);
|
||
}
|
||
};
|
||
}
|
||
};
|
||
const sendInitialBooleanStates = () => {
|
||
var _a, _b;
|
||
for (const { widget: name, type } of BOOLEAN_WIDGETS) {
|
||
const w2 = (_a = node.widgets) == null ? void 0 : _a.find((x) => x.name === name);
|
||
if (!w2) continue;
|
||
(_b = iframe.contentWindow) == null ? void 0 : _b.postMessage(
|
||
{ type, data: { value: !!w2.value } },
|
||
POST_MESSAGE_ORIGIN
|
||
);
|
||
}
|
||
};
|
||
const sendPreviewState = () => {
|
||
var _a, _b, _c, _d;
|
||
if (!node._mesh2motionReady) return;
|
||
const previewWidget = (_a = node.widgets) == null ? void 0 : _a.find((w2) => w2.name === "preview_output");
|
||
const widthWidget = (_b = node.widgets) == null ? void 0 : _b.find((w2) => w2.name === "width");
|
||
const heightWidget = (_c = node.widgets) == null ? void 0 : _c.find((w2) => w2.name === "height");
|
||
(_d = iframe.contentWindow) == null ? void 0 : _d.postMessage({
|
||
type: "mesh2motion:setPreviewOverlay",
|
||
data: {
|
||
enabled: !!(previewWidget == null ? void 0 : previewWidget.value),
|
||
width: (widthWidget == null ? void 0 : widthWidget.value) ?? 1024,
|
||
height: (heightWidget == null ? void 0 : heightWidget.value) ?? 1024
|
||
}
|
||
}, POST_MESSAGE_ORIGIN);
|
||
};
|
||
const hookPreviewWidgets = () => {
|
||
var _a, _b, _c;
|
||
const widthWidget = (_a = node.widgets) == null ? void 0 : _a.find((w2) => w2.name === "width");
|
||
const heightWidget = (_b = node.widgets) == null ? void 0 : _b.find((w2) => w2.name === "height");
|
||
const previewWidget = (_c = node.widgets) == null ? void 0 : _c.find((w2) => w2.name === "preview_output");
|
||
if (widthWidget) {
|
||
widthWidget.callback = () => {
|
||
sendPreviewState();
|
||
};
|
||
}
|
||
if (heightWidget) {
|
||
heightWidget.callback = () => {
|
||
sendPreviewState();
|
||
};
|
||
}
|
||
if (previewWidget) {
|
||
previewWidget.callback = () => {
|
||
sendPreviewState();
|
||
};
|
||
}
|
||
};
|
||
node.addDOMWidget("mesh2motion_view", "mesh2motion-explore", container, {
|
||
getMinHeight: () => 450,
|
||
hideOnZoom: false,
|
||
serialize: false
|
||
});
|
||
const hookHiddenWidget = (widgetName, serializeFn) => {
|
||
var _a;
|
||
const widget = (_a = node.widgets) == null ? void 0 : _a.find((w2) => w2.name === widgetName);
|
||
if (widget) {
|
||
widget.serializeValue = serializeFn;
|
||
}
|
||
};
|
||
setTimeout(() => {
|
||
hookBooleanWidget("show_skeleton", "mesh2motion:setShowSkeleton");
|
||
hookBooleanWidget("mirror_animations", "mesh2motion:setMirrorAnimations");
|
||
hookBooleanWidget("checker_room", "mesh2motion:setCheckerRoom");
|
||
hookPreviewWidgets();
|
||
hookHiddenWidget("image", async () => {
|
||
try {
|
||
const dataUrl = await captureFromMesh2MotionIframe(node._mesh2motionIframe);
|
||
const result = await uploadMesh2MotionTempImage(dataUrl);
|
||
return `mesh2motion/${result.name} [temp]`;
|
||
} catch (err) {
|
||
console.error("[Mesh2Motion] Capture failed:", err);
|
||
return "";
|
||
}
|
||
});
|
||
const computeVideoSignature = () => {
|
||
var _a, _b;
|
||
const project = (_a = node.properties) == null ? void 0 : _a[PROJECT_KEY];
|
||
const presetFile = (project == null ? void 0 : project.cameraPreset) ?? null;
|
||
if (!presetFile) return null;
|
||
const w2 = (name) => {
|
||
var _a2, _b2;
|
||
return (_b2 = (_a2 = node.widgets) == null ? void 0 : _a2.find((x) => x.name === name)) == null ? void 0 : _b2.value;
|
||
};
|
||
return JSON.stringify({
|
||
presetFile,
|
||
skeleton: (project == null ? void 0 : project.skeleton) ?? null,
|
||
timeline: (project == null ? void 0 : project.timeline) ?? null,
|
||
tuning: ((_b = project == null ? void 0 : project.presetTuning) == null ? void 0 : _b[presetFile]) ?? null,
|
||
animationIndex: (project == null ? void 0 : project.animationIndex) ?? 0,
|
||
width: w2("width"),
|
||
height: w2("height"),
|
||
fps: w2("fps"),
|
||
showSkel: !!w2("show_skeleton"),
|
||
mirror: !!w2("mirror_animations"),
|
||
checker: !!w2("checker_room")
|
||
});
|
||
};
|
||
hookHiddenWidget("video_frames", async () => {
|
||
var _a, _b, _c;
|
||
const project = (_a = node.properties) == null ? void 0 : _a[PROJECT_KEY];
|
||
const presetFile = (project == null ? void 0 : project.cameraPreset) ?? null;
|
||
if (!presetFile) return "";
|
||
const signature = computeVideoSignature();
|
||
if (signature != null && signature === node._mesh2motionVideoSig && typeof node._mesh2motionVideoSerialized === "string" && node._mesh2motionVideoSerialized) {
|
||
return node._mesh2motionVideoSerialized;
|
||
}
|
||
const fileName = presetFile.split("/").pop() ?? "";
|
||
const presetId = fileName.replace(/\.json$/, "");
|
||
if (!presetId) return "";
|
||
const widthWidget = (_b = node.widgets) == null ? void 0 : _b.find((w3) => w3.name === "width");
|
||
const heightWidget = (_c = node.widgets) == null ? void 0 : _c.find((w3) => w3.name === "height");
|
||
const w2 = (widthWidget == null ? void 0 : widthWidget.value) ?? 1024;
|
||
const h2 = (heightWidget == null ? void 0 : heightWidget.value) ?? 1024;
|
||
try {
|
||
const result = await captureVideoFromMesh2MotionIframe(
|
||
node._mesh2motionIframe,
|
||
presetId,
|
||
w2,
|
||
h2
|
||
);
|
||
const serialized = JSON.stringify({ video: result.videoPath, fps: result.fps });
|
||
if (signature != null) {
|
||
node._mesh2motionVideoSig = signature;
|
||
node._mesh2motionVideoSerialized = serialized;
|
||
}
|
||
return serialized;
|
||
} catch (err) {
|
||
console.error("[Mesh2Motion] Video capture failed:", err);
|
||
return "";
|
||
}
|
||
});
|
||
}, 100);
|
||
const [w, h] = node.size;
|
||
node.setSize([Math.max(w, 500), Math.max(h, 700)]);
|
||
}
|
||
const CSS = `
|
||
.mesh2motion-dialog-overlay {
|
||
position: fixed; inset: 0;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
z-index: 10000;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.mesh2motion-dialog {
|
||
width: 95vw; height: 95vh;
|
||
background: #1e1e1e; color: #e0e0e0;
|
||
border: 1px solid #444; border-radius: 6px;
|
||
display: flex; flex-direction: column; overflow: hidden;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
||
}
|
||
.mesh2motion-dialog-header {
|
||
display: flex; align-items: center; gap: 12px;
|
||
padding: 10px 14px; background: #2a2a2a;
|
||
border-bottom: 1px solid #444; flex: 0 0 auto;
|
||
}
|
||
.mesh2motion-dialog-title { font-weight: 600; font-size: 14px; flex: 1 1 auto; }
|
||
.mesh2motion-dialog-btn {
|
||
background: #3a3a3a; color: #eee; border: 1px solid #555;
|
||
border-radius: 4px; padding: 6px 12px; cursor: pointer;
|
||
font-size: 13px;
|
||
}
|
||
.mesh2motion-dialog-btn:hover:not(:disabled) { background: #4a4a4a; }
|
||
.mesh2motion-dialog-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||
.mesh2motion-dialog-btn-primary { background: #2563eb; border-color: #3b82f6; }
|
||
.mesh2motion-dialog-btn-primary:hover:not(:disabled) { background: #1d4ed8; }
|
||
.mesh2motion-dialog-close {
|
||
background: transparent; border: none; color: #bbb;
|
||
cursor: pointer; font-size: 20px; line-height: 1; padding: 0 6px;
|
||
}
|
||
.mesh2motion-dialog-close:hover { color: #fff; }
|
||
.mesh2motion-dialog-body { flex: 1 1 auto; position: relative; }
|
||
.mesh2motion-dialog-iframe { width: 100%; height: 100%; border: 0; display: block; }
|
||
.mesh2motion-dialog-status {
|
||
font-size: 12px; color: #8bc; margin-right: 8px; min-width: 0;
|
||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
}
|
||
`;
|
||
class Mesh2MotionDialog {
|
||
constructor() {
|
||
__publicField(this, "overlay", null);
|
||
__publicField(this, "dialog", null);
|
||
__publicField(this, "iframe", null);
|
||
__publicField(this, "titleEl", null);
|
||
__publicField(this, "statusEl", null);
|
||
__publicField(this, "saveBtn", null);
|
||
__publicField(this, "saveImageBtn", null);
|
||
__publicField(this, "styleInjected", false);
|
||
__publicField(this, "currentPage", "explore");
|
||
__publicField(this, "currentNode", null);
|
||
__publicField(this, "editorReady", false);
|
||
__publicField(this, "pendingModelUrl", null);
|
||
__publicField(this, "saveInFlight", false);
|
||
__publicField(this, "saveImageInFlight", false);
|
||
__publicField(this, "onMessage", (event) => {
|
||
if (event.origin !== POST_MESSAGE_ORIGIN) return;
|
||
if (!this.iframe || event.source !== this.iframe.contentWindow) return;
|
||
const { type, data } = event.data || {};
|
||
switch (type) {
|
||
case "mesh2motion:ready":
|
||
this.editorReady = true;
|
||
this.updateSaveButtonsEnabled(true);
|
||
if (this.statusEl) this.statusEl.textContent = "";
|
||
if (this.pendingModelUrl) {
|
||
this.post("comfyui:loadModel", { url: this.pendingModelUrl });
|
||
this.pendingModelUrl = null;
|
||
}
|
||
break;
|
||
case "mesh2motion:modelLoaded":
|
||
break;
|
||
case "mesh2motion:export":
|
||
if ((data == null ? void 0 : data.modelData) && (data == null ? void 0 : data.filename)) {
|
||
void this.handleModelExport(data.modelData, data.filename);
|
||
}
|
||
break;
|
||
case "mesh2motion:imageExport":
|
||
if ((data == null ? void 0 : data.imageDataUrl) && (data == null ? void 0 : data.filename)) {
|
||
void this.handleImageExport(
|
||
data.imageDataUrl,
|
||
data.filename,
|
||
Number(data.width) || 512,
|
||
Number(data.height) || 512
|
||
);
|
||
}
|
||
break;
|
||
case "mesh2motion:error":
|
||
console.error("[Mesh2Motion] iframe error:", data == null ? void 0 : data.message);
|
||
if (this.statusEl) this.statusEl.textContent = String((data == null ? void 0 : data.message) ?? "Error");
|
||
this.saveInFlight = false;
|
||
this.saveImageInFlight = false;
|
||
this.updateSaveButtonsEnabled(this.editorReady);
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
openExplore(node) {
|
||
this.currentPage = "explore";
|
||
this.currentNode = node;
|
||
this.show();
|
||
}
|
||
openCreate(node) {
|
||
this.currentPage = "create";
|
||
this.currentNode = node;
|
||
this.pendingModelUrl = node ? getModelUrlFromNode(node) : null;
|
||
this.show();
|
||
}
|
||
close() {
|
||
this.editorReady = false;
|
||
this.pendingModelUrl = null;
|
||
this.currentNode = null;
|
||
this.saveInFlight = false;
|
||
this.saveImageInFlight = false;
|
||
if (this.overlay) {
|
||
this.overlay.remove();
|
||
this.overlay = null;
|
||
this.dialog = null;
|
||
this.iframe = null;
|
||
this.titleEl = null;
|
||
this.statusEl = null;
|
||
this.saveBtn = null;
|
||
this.saveImageBtn = null;
|
||
}
|
||
}
|
||
// ── Build & show ────────────────────────────────────────────────────
|
||
show() {
|
||
this.injectStyles();
|
||
this.buildUI();
|
||
this.updateSaveButtonsEnabled(false);
|
||
if (this.statusEl) this.statusEl.textContent = "Loading editor…";
|
||
const pageFile = this.currentPage === "explore" ? "index-comfyui-window.html" : "create-comfyui-window.html";
|
||
const params = new URLSearchParams({ window: "true", theme: "dark" });
|
||
if (this.iframe) this.iframe.src = `/mesh2motion/${pageFile}?${params.toString()}`;
|
||
if (this.titleEl) {
|
||
const hint = this.currentPage === "explore" ? "Explore" : "Create";
|
||
this.titleEl.textContent = `Mesh2Motion — ${hint}`;
|
||
}
|
||
}
|
||
injectStyles() {
|
||
if (this.styleInjected) return;
|
||
const style = document.createElement("style");
|
||
style.textContent = CSS;
|
||
document.head.appendChild(style);
|
||
this.styleInjected = true;
|
||
}
|
||
buildUI() {
|
||
if (this.overlay) return;
|
||
this.overlay = document.createElement("div");
|
||
this.overlay.className = "mesh2motion-dialog-overlay";
|
||
this.overlay.addEventListener("click", (ev) => {
|
||
if (ev.target === this.overlay) this.close();
|
||
});
|
||
this.dialog = document.createElement("div");
|
||
this.dialog.className = "mesh2motion-dialog";
|
||
const header = document.createElement("div");
|
||
header.className = "mesh2motion-dialog-header";
|
||
this.titleEl = document.createElement("span");
|
||
this.titleEl.className = "mesh2motion-dialog-title";
|
||
header.appendChild(this.titleEl);
|
||
this.statusEl = document.createElement("span");
|
||
this.statusEl.className = "mesh2motion-dialog-status";
|
||
header.appendChild(this.statusEl);
|
||
this.saveImageBtn = document.createElement("button");
|
||
this.saveImageBtn.className = "mesh2motion-dialog-btn";
|
||
this.saveImageBtn.textContent = "Save Image";
|
||
this.saveImageBtn.addEventListener("click", () => this.requestImageExport());
|
||
header.appendChild(this.saveImageBtn);
|
||
this.saveBtn = document.createElement("button");
|
||
this.saveBtn.className = "mesh2motion-dialog-btn mesh2motion-dialog-btn-primary";
|
||
this.saveBtn.textContent = "Save Model";
|
||
this.saveBtn.addEventListener("click", () => this.requestExport());
|
||
header.appendChild(this.saveBtn);
|
||
const closeBtn = document.createElement("button");
|
||
closeBtn.className = "mesh2motion-dialog-close";
|
||
closeBtn.setAttribute("aria-label", "Close");
|
||
closeBtn.textContent = "✕";
|
||
closeBtn.addEventListener("click", () => this.close());
|
||
header.appendChild(closeBtn);
|
||
this.dialog.appendChild(header);
|
||
const body = document.createElement("div");
|
||
body.className = "mesh2motion-dialog-body";
|
||
this.iframe = document.createElement("iframe");
|
||
this.iframe.className = "mesh2motion-dialog-iframe";
|
||
this.iframe.allow = "cross-origin-isolated";
|
||
body.appendChild(this.iframe);
|
||
this.dialog.appendChild(body);
|
||
this.overlay.appendChild(this.dialog);
|
||
document.body.appendChild(this.overlay);
|
||
window.addEventListener("message", this.onMessage);
|
||
}
|
||
post(type, data) {
|
||
var _a, _b;
|
||
(_b = (_a = this.iframe) == null ? void 0 : _a.contentWindow) == null ? void 0 : _b.postMessage({ type, data }, POST_MESSAGE_ORIGIN);
|
||
}
|
||
updateSaveButtonsEnabled(enabled) {
|
||
if (this.saveBtn) this.saveBtn.disabled = !enabled || this.saveInFlight;
|
||
if (this.saveImageBtn) this.saveImageBtn.disabled = !enabled || this.saveImageInFlight;
|
||
}
|
||
requestExport() {
|
||
if (!this.editorReady || this.saveInFlight) return;
|
||
this.saveInFlight = true;
|
||
this.updateSaveButtonsEnabled(true);
|
||
if (this.statusEl) this.statusEl.textContent = "Exporting model…";
|
||
this.post("comfyui:requestExport");
|
||
}
|
||
requestImageExport() {
|
||
if (!this.editorReady || this.saveImageInFlight) return;
|
||
this.saveImageInFlight = true;
|
||
this.updateSaveButtonsEnabled(true);
|
||
if (this.statusEl) this.statusEl.textContent = "Open the export overlay in the editor, then confirm…";
|
||
this.post("comfyui:requestImageExport");
|
||
}
|
||
// ── Back-to-ComfyUI upload + node injection ─────────────────────────
|
||
async handleModelExport(modelData, filename) {
|
||
try {
|
||
const blob = new Blob([modelData], { type: "model/gltf-binary" });
|
||
const finalFilename = filename || `mesh2motion-${Date.now()}.glb`;
|
||
const formData = new FormData();
|
||
formData.append("image", blob, finalFilename);
|
||
formData.append("type", "input");
|
||
formData.append("subfolder", "mesh2motion");
|
||
formData.append("overwrite", "true");
|
||
const resp = await api.fetchApi("/upload/image", { method: "POST", body: formData });
|
||
if (!resp.ok) throw new Error(`Upload failed: ${resp.status}`);
|
||
const result = await resp.json();
|
||
const node = this.currentNode;
|
||
if (node && "constructor" in node) this.injectModelIntoNode(node, result);
|
||
if (this.statusEl) this.statusEl.textContent = "Model saved ✓";
|
||
this.close();
|
||
} catch (err) {
|
||
console.error("[Mesh2Motion] Failed to save model:", err);
|
||
if (this.statusEl) this.statusEl.textContent = "Save failed — see console";
|
||
} finally {
|
||
this.saveInFlight = false;
|
||
this.updateSaveButtonsEnabled(this.editorReady);
|
||
}
|
||
}
|
||
injectModelIntoNode(node, result) {
|
||
var _a, _b, _c;
|
||
const widgetValue = result.subfolder ? `${result.subfolder}/${result.name} [input]` : `${result.name} [input]`;
|
||
node.images = [{
|
||
filename: result.name,
|
||
subfolder: result.subfolder || "",
|
||
type: "input"
|
||
}];
|
||
const modelWidget = (_a = node.widgets) == null ? void 0 : _a.find(
|
||
(w) => w.name === "model_file" || w.name === "mesh" || w.name === "3d_model" || w.name === "model"
|
||
);
|
||
if (modelWidget) {
|
||
if (((_b = modelWidget.options) == null ? void 0 : _b.values) && !modelWidget.options.values.includes(widgetValue)) {
|
||
modelWidget.options.values.push(widgetValue);
|
||
}
|
||
modelWidget.value = widgetValue;
|
||
(_c = modelWidget.callback) == null ? void 0 : _c.call(modelWidget, widgetValue);
|
||
}
|
||
app.graph.setDirtyCanvas(true, true);
|
||
}
|
||
async handleImageExport(imageDataUrl, filename, width, height) {
|
||
try {
|
||
const response = await fetch(imageDataUrl);
|
||
const blob = await response.blob();
|
||
const finalFilename = filename || `mesh2motion-render-${Date.now()}.png`;
|
||
const formData = new FormData();
|
||
formData.append("image", blob, finalFilename);
|
||
formData.append("type", "input");
|
||
formData.append("subfolder", "mesh2motion");
|
||
formData.append("overwrite", "true");
|
||
const resp = await api.fetchApi("/upload/image", { method: "POST", body: formData });
|
||
if (!resp.ok) throw new Error(`Upload failed: ${resp.status}`);
|
||
const result = await resp.json();
|
||
const imageNode = this.resolveImageNode();
|
||
if (imageNode) {
|
||
await this.injectImageIntoNode(imageNode, result, imageDataUrl);
|
||
if (this.statusEl) this.statusEl.textContent = `Image saved → LoadImage (${width}×${height})`;
|
||
} else {
|
||
if (this.statusEl) this.statusEl.textContent = `Image saved (no LoadImage node) — ${result.name}`;
|
||
}
|
||
} catch (err) {
|
||
console.error("[Mesh2Motion] Failed to export image:", err);
|
||
if (this.statusEl) this.statusEl.textContent = "Image export failed — see console";
|
||
} finally {
|
||
this.saveImageInFlight = false;
|
||
this.updateSaveButtonsEnabled(this.editorReady);
|
||
}
|
||
}
|
||
resolveImageNode() {
|
||
var _a, _b, _c, _d;
|
||
if (this.currentNode) {
|
||
const n = this.currentNode;
|
||
if (((_a = n.constructor) == null ? void 0 : _a.comfyClass) && IMAGE_NODES.includes(n.constructor.comfyClass)) {
|
||
return this.currentNode;
|
||
}
|
||
}
|
||
const graph = app.graph;
|
||
if (!graph) return null;
|
||
const canvas = (_b = graph.list_of_graphcanvas) == null ? void 0 : _b[0];
|
||
const selected = canvas == null ? void 0 : canvas.selected_nodes;
|
||
if (selected) {
|
||
for (const id of Object.keys(selected)) {
|
||
const node = graph.getNodeById(Number(id));
|
||
const cls = (_c = node == null ? void 0 : node.constructor) == null ? void 0 : _c.comfyClass;
|
||
if (cls && IMAGE_NODES.includes(cls)) return node;
|
||
}
|
||
}
|
||
for (const node of graph._nodes || []) {
|
||
const cls = (_d = node == null ? void 0 : node.constructor) == null ? void 0 : _d.comfyClass;
|
||
if (cls && IMAGE_NODES.includes(cls)) return node;
|
||
}
|
||
return null;
|
||
}
|
||
async injectImageIntoNode(imageNode, result, imageDataUrl) {
|
||
var _a, _b, _c;
|
||
const widgetValue = result.subfolder ? `${result.subfolder}/${result.name}` : result.name;
|
||
imageNode.images = [{
|
||
filename: result.name,
|
||
subfolder: result.subfolder || "",
|
||
type: "input"
|
||
}];
|
||
const imageWidget = (_a = imageNode.widgets) == null ? void 0 : _a.find((w) => w.name === "image");
|
||
if (imageWidget) {
|
||
if (((_b = imageWidget.options) == null ? void 0 : _b.values) && !imageWidget.options.values.includes(widgetValue)) {
|
||
imageWidget.options.values.push(widgetValue);
|
||
}
|
||
imageWidget.value = widgetValue;
|
||
if (imageNode.widgets_values && imageNode.widgets) {
|
||
const idx = imageNode.widgets.findIndex((w) => w.name === "image");
|
||
if (idx >= 0) imageNode.widgets_values[idx] = widgetValue;
|
||
}
|
||
if (imageNode.properties) imageNode.properties["image"] = widgetValue;
|
||
(_c = imageWidget.callback) == null ? void 0 : _c.call(imageWidget, widgetValue);
|
||
}
|
||
const img = new Image();
|
||
img.crossOrigin = "anonymous";
|
||
img.src = imageDataUrl;
|
||
await new Promise((resolve, reject) => {
|
||
img.onload = () => resolve(null);
|
||
img.onerror = reject;
|
||
}).catch(() => {
|
||
});
|
||
imageNode.imgs = [img];
|
||
app.graph.setDirtyCanvas(true, true);
|
||
}
|
||
}
|
||
const dialog = new Mesh2MotionDialog();
|
||
function openMesh2MotionExplore(node = null) {
|
||
dialog.openExplore(node);
|
||
}
|
||
function openMesh2MotionCreate(node = null) {
|
||
dialog.openCreate(node);
|
||
}
|
||
const { ComfyButton } = window.comfyAPI.button;
|
||
app.registerExtension({
|
||
name: "ComfyUI.Mesh2Motion",
|
||
setup() {
|
||
var _a;
|
||
(_a = app.menu) == null ? void 0 : _a.settingsGroup.append(
|
||
new ComfyButton({
|
||
icon: "human-male",
|
||
tooltip: "Mesh2Motion — rig, pose, and render characters",
|
||
content: "Mesh2Motion",
|
||
action: () => openMesh2MotionExplore(null)
|
||
})
|
||
);
|
||
},
|
||
nodeCreated(node) {
|
||
var _a;
|
||
if (((_a = node.constructor) == null ? void 0 : _a.comfyClass) === "Mesh2MotionExplore") {
|
||
createMesh2MotionExploreWidget(node);
|
||
}
|
||
},
|
||
// Right-click menu entries on compatible nodes:
|
||
// Image nodes (LoadImage) → Explore window
|
||
// 3D model nodes (Load3D / Preview3D / ...) → Create window, pre-loading
|
||
// the node's current model
|
||
getNodeMenuItems(node) {
|
||
var _a;
|
||
const typedNode = node;
|
||
const nodeClass = (_a = typedNode == null ? void 0 : typedNode.constructor) == null ? void 0 : _a.comfyClass;
|
||
if (nodeClass && IMAGE_NODES.includes(nodeClass)) {
|
||
return [
|
||
null,
|
||
{
|
||
content: "Open in Mesh2Motion",
|
||
callback: () => {
|
||
openMesh2MotionExplore(node);
|
||
}
|
||
}
|
||
];
|
||
}
|
||
if (nodeClass && MODEL_3D_NODES.includes(nodeClass)) {
|
||
return [
|
||
null,
|
||
{
|
||
content: "Open in Mesh2Motion",
|
||
callback: () => {
|
||
openMesh2MotionCreate(node);
|
||
}
|
||
}
|
||
];
|
||
}
|
||
if (isModel3DNode(node)) {
|
||
return [
|
||
null,
|
||
{
|
||
content: "Open in Mesh2Motion",
|
||
callback: () => {
|
||
openMesh2MotionCreate(node);
|
||
}
|
||
}
|
||
];
|
||
}
|
||
return [];
|
||
}
|
||
});
|
||
export {
|
||
openMesh2MotionCreate,
|
||
openMesh2MotionExplore
|
||
};
|
||
//# sourceMappingURL=main.js.map
|