The viewer JS/HTML/bundles are now committed in javascript/ (declared via pyproject [tool.comfy] web = "javascript") instead of being copied from the shared comfy-3d-viewers pip package at prestartup. Consequences: - requirements.txt is now just comfy-env (host env installs nothing else, per the never-touch-the-host-env principle) - prestartup_script.py no longer copies viewers or web_src; it keeps only setup_env() and the input-asset copies - WEB_DIRECTORY attribute removed; the pyproject declaration is the single source (served at /extensions/comfyui-geometrypack/, keyed by project name) - web_src/ deleted; its 6 widgets now live in javascript/js/ as the tracked source, edited directly - viewer_pbr.html hardcoded-fallback URL updated to the new mount name Iframe URLs auto-detect the mount from import.meta.url, so the folder rename and mount-name change are transparent at runtime.
259 lines
11 KiB
JavaScript
259 lines
11 KiB
JavaScript
/**
|
|
* BVH Viewer Widget - Interactive BVH skeleton animation viewer
|
|
* Displays skeletal animations in BVH format with Three.js
|
|
*/
|
|
|
|
import { app } from "../../../../scripts/app.js";
|
|
|
|
console.log("[BVHViewer] Loading BVH Viewer extension");
|
|
|
|
// Node types that use this viewer
|
|
const BVH_VIEWER_NODES = [
|
|
{
|
|
nodeName: "BVHViewer",
|
|
extensionName: "comfy3d.bvhviewer",
|
|
logPrefix: "[BVHViewer]"
|
|
}
|
|
];
|
|
|
|
function detectExtensionFolder() {
|
|
try {
|
|
if (typeof import.meta !== 'undefined' && import.meta.url) {
|
|
const match = import.meta.url.match(/\/extensions\/([^\/]+)\//);
|
|
if (match) return match[1];
|
|
}
|
|
const scripts = document.getElementsByTagName('script');
|
|
for (let i = scripts.length - 1; i >= 0; i--) {
|
|
const src = scripts[i].src;
|
|
if (src) {
|
|
const match = src.match(/\/extensions\/([^\/]+)\//);
|
|
if (match) return match[1];
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('[BVHViewer] Could not detect extension folder:', e);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getViewerUrl(extensionFolder) {
|
|
return `/extensions/${extensionFolder}/viewer_bvh.html?v=` + Date.now();
|
|
}
|
|
|
|
function createBVHViewerExtension(config) {
|
|
const { extensionName, nodeName, logPrefix } = config;
|
|
|
|
console.log(`${logPrefix} Loading BVH viewer extension...`);
|
|
|
|
const extensionFolder = detectExtensionFolder();
|
|
if (!extensionFolder) {
|
|
console.error(`${logPrefix} Could not detect extension folder`);
|
|
return;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: extensionName,
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name !== nodeName) return;
|
|
|
|
console.log(`${logPrefix} Registering ${nodeName} node`);
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
|
|
nodeType.prototype.onNodeCreated = function() {
|
|
const result = onNodeCreated?.apply(this, arguments);
|
|
|
|
// Create container
|
|
const container = document.createElement("div");
|
|
container.style.cssText = "position: relative; width: 100%; background: #222;";
|
|
|
|
// Create iframe for Three.js viewer
|
|
const iframe = document.createElement("iframe");
|
|
iframe.style.cssText = "width: 100%; height: 600px; border: none; display: block; background: #1a1a1a;";
|
|
iframe.src = getViewerUrl(extensionFolder);
|
|
container.appendChild(iframe);
|
|
|
|
// Controls bar
|
|
const controlsBar = document.createElement("div");
|
|
controlsBar.style.cssText = "display: flex; flex-wrap: wrap; gap: 10px; padding: 10px; background: #252525; align-items: center; border-top: 1px solid #333;";
|
|
|
|
// Play/Pause button
|
|
const playButton = document.createElement("button");
|
|
playButton.textContent = "\u25B6";
|
|
playButton.style.cssText = "width: 30px; height: 30px; border: none; border-radius: 4px; background: #4a9eff; color: white; font-size: 14px; cursor: pointer; flex-shrink: 0;";
|
|
playButton.disabled = true;
|
|
controlsBar.appendChild(playButton);
|
|
|
|
// Frame slider
|
|
const frameSlider = document.createElement("input");
|
|
frameSlider.type = "range";
|
|
frameSlider.min = 0;
|
|
frameSlider.max = 100;
|
|
frameSlider.value = 0;
|
|
frameSlider.disabled = true;
|
|
frameSlider.style.cssText = "flex-grow: 1; height: 6px; min-width: 100px;";
|
|
controlsBar.appendChild(frameSlider);
|
|
|
|
// Frame counter
|
|
const frameCounter = document.createElement("div");
|
|
frameCounter.style.cssText = "padding: 4px 8px; background: rgba(0,0,0,0.3); color: #aaa; border-radius: 3px; font-size: 11px; font-family: monospace; min-width: 80px; text-align: center;";
|
|
frameCounter.textContent = "0 / 0";
|
|
controlsBar.appendChild(frameCounter);
|
|
|
|
// Separator
|
|
const sep = document.createElement("div");
|
|
sep.style.cssText = "width: 1px; height: 20px; background: #444; margin: 0 5px;";
|
|
controlsBar.appendChild(sep);
|
|
|
|
// Speed control container
|
|
const speedContainer = document.createElement("div");
|
|
speedContainer.style.cssText = "display: flex; align-items: center; gap: 5px;";
|
|
|
|
const speedLabel = document.createElement("span");
|
|
speedLabel.textContent = "Spd:";
|
|
speedLabel.style.cssText = "color: #aaa; font-size: 11px;";
|
|
speedContainer.appendChild(speedLabel);
|
|
|
|
const speedSlider = document.createElement("input");
|
|
speedSlider.type = "range";
|
|
speedSlider.min = 0.1;
|
|
speedSlider.max = 2.0;
|
|
speedSlider.step = 0.1;
|
|
speedSlider.value = 1.0;
|
|
speedSlider.style.cssText = "width: 60px; height: 4px;";
|
|
speedContainer.appendChild(speedSlider);
|
|
|
|
const speedValue = document.createElement("span");
|
|
speedValue.textContent = "1.0x";
|
|
speedValue.style.cssText = "color: #fff; font-size: 11px; min-width: 30px;";
|
|
speedContainer.appendChild(speedValue);
|
|
|
|
controlsBar.appendChild(speedContainer);
|
|
container.appendChild(controlsBar);
|
|
|
|
// State
|
|
this.bvhViewerState = {
|
|
iframe: iframe,
|
|
container: container,
|
|
playButton: playButton,
|
|
frameSlider: frameSlider,
|
|
frameCounter: frameCounter,
|
|
speedSlider: speedSlider,
|
|
speedValue: speedValue,
|
|
isPlaying: false,
|
|
currentFrame: 0,
|
|
totalFrames: 0,
|
|
bvhData: null,
|
|
viewerReady: false
|
|
};
|
|
|
|
// Add DOM widget
|
|
this.addDOMWidget("bvh_viewer", "customIframe", container);
|
|
|
|
// Play button handler
|
|
playButton.onclick = () => {
|
|
const state = this.bvhViewerState;
|
|
state.isPlaying = !state.isPlaying;
|
|
playButton.textContent = state.isPlaying ? "\u23F8" : "\u25B6";
|
|
iframe.contentWindow.postMessage({
|
|
type: state.isPlaying ? 'play' : 'pause'
|
|
}, '*');
|
|
};
|
|
|
|
// Frame slider handler
|
|
frameSlider.oninput = (e) => {
|
|
const frame = parseInt(e.target.value);
|
|
this.bvhViewerState.currentFrame = frame;
|
|
iframe.contentWindow.postMessage({ type: 'setFrame', frame: frame }, '*');
|
|
};
|
|
|
|
// Speed slider handler
|
|
speedSlider.oninput = (e) => {
|
|
const speed = parseFloat(e.target.value);
|
|
speedValue.textContent = speed.toFixed(1) + 'x';
|
|
iframe.contentWindow.postMessage({ type: 'setSpeed', speed: speed }, '*');
|
|
};
|
|
|
|
// Listen for messages from iframe
|
|
const messageHandler = (event) => {
|
|
if (event.source !== iframe.contentWindow) return;
|
|
|
|
const data = event.data;
|
|
const state = this.bvhViewerState;
|
|
|
|
if (data.type === 'VIEWER_READY') {
|
|
state.viewerReady = true;
|
|
if (state.bvhData) {
|
|
this.loadBVHData(state.bvhData.bvhContent, state.bvhData.bvhInfo);
|
|
}
|
|
} else if (data.type === 'playing') {
|
|
state.isPlaying = true;
|
|
playButton.textContent = "\u23F8";
|
|
} else if (data.type === 'paused') {
|
|
state.isPlaying = false;
|
|
playButton.textContent = "\u25B6";
|
|
} else if (data.type === 'frameChanged') {
|
|
state.currentFrame = data.frame;
|
|
state.totalFrames = data.totalFrames;
|
|
frameSlider.value = data.frame;
|
|
frameCounter.textContent = `${data.frame} / ${data.totalFrames}`;
|
|
} else if (data.type === 'looped') {
|
|
state.currentFrame = 0;
|
|
frameSlider.value = 0;
|
|
}
|
|
};
|
|
window.addEventListener('message', messageHandler);
|
|
|
|
// Cleanup on removal
|
|
const originalOnRemoved = this.onRemoved;
|
|
this.onRemoved = function() {
|
|
window.removeEventListener('message', messageHandler);
|
|
if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
|
|
};
|
|
|
|
// Handle data from backend
|
|
this.onExecuted = (message) => {
|
|
if (message?.bvh_content) {
|
|
const bvhContent = message.bvh_content[0];
|
|
const bvhInfo = message.bvh_info ? message.bvh_info[0] : {};
|
|
|
|
this.bvhViewerState.bvhData = { bvhContent, bvhInfo };
|
|
this.bvhViewerState.totalFrames = bvhInfo.num_frames || 0;
|
|
|
|
playButton.disabled = false;
|
|
frameSlider.disabled = false;
|
|
frameSlider.max = this.bvhViewerState.totalFrames - 1;
|
|
frameCounter.textContent = `0 / ${this.bvhViewerState.totalFrames}`;
|
|
|
|
if (this.bvhViewerState.viewerReady) {
|
|
this.loadBVHData(bvhContent, bvhInfo);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Method to load BVH data into viewer
|
|
this.loadBVHData = function(bvhContent, bvhInfo) {
|
|
if (iframe.contentWindow) {
|
|
iframe.contentWindow.postMessage({
|
|
type: 'loadBVH',
|
|
bvhContent: bvhContent,
|
|
bvhInfo: bvhInfo
|
|
}, '*');
|
|
}
|
|
};
|
|
|
|
this.setSize([Math.max(400, this.size[0] || 400), 720]);
|
|
return result;
|
|
};
|
|
}
|
|
});
|
|
|
|
console.log(`${logPrefix} Extension registered: ${extensionName}`);
|
|
}
|
|
|
|
// Auto-register all known BVH viewer node types
|
|
BVH_VIEWER_NODES.forEach(config => createBVHViewerExtension(config));
|
|
|
|
export { createBVHViewerExtension, detectExtensionFolder };
|