diff --git a/javascript/js/bvh_viewer.js b/javascript/js/bvh_viewer.js
deleted file mode 100644
index b7231fa9..00000000
--- a/javascript/js/bvh_viewer.js
+++ /dev/null
@@ -1,258 +0,0 @@
-/**
- * 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 };
diff --git a/javascript/js/compare_skeleton_widget.js b/javascript/js/compare_skeleton_widget.js
deleted file mode 100644
index 0cc1fb21..00000000
--- a/javascript/js/compare_skeleton_widget.js
+++ /dev/null
@@ -1,219 +0,0 @@
-/**
- * ComfyUI Compare Skeletons Widget
- *
- * Widget for UniRigCompareSkeletons node - displays two FBX skeletons side-by-side
- * with synced camera rotation and cross-bone highlighting.
- */
-
-import { app } from "../../../../scripts/app.js";
-import { detectExtensionFolder, getViewerUrl } from "./mesh_preview_fbx.js";
-
-console.log("[UniRig Compare] Loading compare skeleton widget...");
-
-const extensionFolder = detectExtensionFolder();
-if (!extensionFolder) {
- console.error("[UniRig Compare] Could not detect extension folder");
-}
-
-console.log("[UniRig Compare] Detected extension folder:", extensionFolder);
-
-app.registerExtension({
- name: "unirig.compareskeletons",
-
- async beforeRegisterNodeDef(nodeType, nodeData, app) {
- if (nodeData.name === "CompareSkeletons" || nodeData.name === "UniRigCompareSkeletons") {
- console.log("[UniRig Compare] Registering Compare Skeletons node");
-
- const onNodeCreated = nodeType.prototype.onNodeCreated;
- nodeType.prototype.onNodeCreated = function() {
- const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
-
- console.log("[UniRig Compare] Node created, adding compare viewer widget");
-
- // Create iframe for comparison viewer
- const iframe = document.createElement("iframe");
- iframe.style.width = "100%";
- iframe.style.flex = "1 1 0";
- iframe.style.minHeight = "0";
- iframe.style.border = "none";
- iframe.style.backgroundColor = "#2a2a2a";
-
- // Load viewer from file URL
- const viewerUrl = getViewerUrl(extensionFolder, "viewer_fbx_compare.html");
- iframe.src = viewerUrl;
- console.log("[UniRig Compare] Setting iframe src to:", viewerUrl);
-
- iframe.onload = () => {
- console.log("[UniRig Compare] Iframe loaded successfully");
- };
- iframe.onerror = (e) => {
- console.error("[UniRig Compare] Iframe failed to load:", e);
- };
-
- // Add widget
- const widget = this.addDOMWidget("preview", "FBX_COMPARE_PREVIEW", iframe, {
- getValue() { return ""; },
- setValue(v) { }
- });
-
- console.log("[UniRig Compare] Widget created:", widget);
-
- // Set widget size - wider for side-by-side view
- widget.computeSize = function(width) {
- const w = width || 800;
- const h = w * 0.75; // Wider aspect ratio for dual view
- return [w, h];
- };
-
- widget.element = iframe;
-
- // Store iframe reference
- this.compareViewerIframe = iframe;
- this.compareViewerReady = false;
-
- // Listen for ready message from iframe
- const onMessage = (event) => {
- if (event.data && event.data.type === 'VIEWER_READY') {
- console.log("[UniRig Compare] Viewer iframe is ready!");
- this.compareViewerReady = true;
- }
- };
- window.addEventListener('message', onMessage.bind(this));
-
- const notifyIframeResize = () => {
- if (iframe.contentWindow) {
- const rect = iframe.getBoundingClientRect();
- iframe.contentWindow.postMessage({
- type: 'RESIZE',
- width: rect.width,
- height: rect.height
- }, '*');
- }
- };
-
- this.onResize = function(size) {
- const isVueNodes = iframe.closest('[data-node-id]') !== null ||
- document.querySelector('.vue-graph-canvas') !== null;
-
- if (!isVueNodes && size && size[1]) {
- const nodeHeight = size[1];
- const headerHeight = 70;
- const availableHeight = Math.max(200, nodeHeight - headerHeight);
- iframe.style.height = availableHeight + 'px';
- }
-
- requestAnimationFrame(() => {
- notifyIframeResize();
- });
- };
-
- let resizeTimeout = null;
- let lastSize = { width: 0, height: 0 };
- const resizeObserver = new ResizeObserver((entries) => {
- const entry = entries[0];
- const newWidth = entry.contentRect.width;
- const newHeight = entry.contentRect.height;
-
- if (Math.abs(newWidth - lastSize.width) < 1 && Math.abs(newHeight - lastSize.height) < 1) {
- return;
- }
- lastSize = { width: newWidth, height: newHeight };
-
- if (resizeTimeout) {
- clearTimeout(resizeTimeout);
- }
- resizeTimeout = setTimeout(() => {
- notifyIframeResize();
- }, 50);
- });
- resizeObserver.observe(iframe);
-
- const originalOnRemoved = this.onRemoved;
- this.onRemoved = function() {
- resizeObserver.disconnect();
- if (resizeTimeout) {
- clearTimeout(resizeTimeout);
- }
- window.removeEventListener('message', onMessage);
- if (originalOnRemoved) {
- originalOnRemoved.apply(this, arguments);
- }
- };
-
- // Set initial node size (wider for dual view)
- this.setSize([800, 600]);
-
- // Handle execution
- const onExecuted = this.onExecuted;
- this.onExecuted = function(message) {
- console.log("[UniRig Compare] onExecuted called with message:", message);
- onExecuted?.apply(this, arguments);
-
- // The message contains both FBX file paths
- const leftFile = message?.fbx_file_left?.[0];
- const rightFile = message?.fbx_file_right?.[0];
-
- if (leftFile || rightFile) {
- console.log("[UniRig Compare] Loading FBX files - Left:", leftFile, "Right:", rightFile);
-
- // Build file paths
- const buildFilepath = (filename) => {
- if (!filename) return null;
- if (!filename.includes('/') && !filename.includes('\\')) {
- return `${window.location.origin}/view?filename=${encodeURIComponent(filename)}&type=output&subfolder=`;
- } else {
- const basename = filename.split(/[/\\]/).pop();
- return `${window.location.origin}/view?filename=${encodeURIComponent(basename)}&type=output&subfolder=`;
- }
- };
-
- const filepathLeft = buildFilepath(leftFile);
- const filepathRight = buildFilepath(rightFile);
-
- // Send message to iframe
- const sendMessage = () => {
- if (iframe.contentWindow) {
- console.log("[UniRig Compare] Sending postMessage to iframe");
- iframe.contentWindow.postMessage({
- type: "LOAD_FBX_COMPARE",
- filepathLeft: filepathLeft,
- filepathRight: filepathRight,
- timestamp: Date.now()
- }, "*");
- } else {
- console.error("[UniRig Compare] Iframe contentWindow not available");
- }
- };
-
- // Wait for iframe to be ready, or use timeout as fallback
- if (this.compareViewerReady) {
- sendMessage();
- } else {
- const checkReady = setInterval(() => {
- if (this.compareViewerReady) {
- clearInterval(checkReady);
- sendMessage();
- }
- }, 50);
-
- // Fallback timeout after 2 seconds
- setTimeout(() => {
- clearInterval(checkReady);
- if (!this.compareViewerReady) {
- console.warn("[UniRig Compare] Iframe not ready after 2s, sending anyway");
- sendMessage();
- }
- }, 2000);
- }
- } else {
- console.log("[UniRig Compare] No FBX files in message data. Keys:", Object.keys(message || {}));
- }
- };
-
- return r;
- };
- }
- }
-});
-
-console.log("[UniRig Compare] Compare skeleton widget registered");
diff --git a/javascript/js/compare_smpl_bvh.js b/javascript/js/compare_smpl_bvh.js
deleted file mode 100644
index 0f821a41..00000000
--- a/javascript/js/compare_smpl_bvh.js
+++ /dev/null
@@ -1,161 +0,0 @@
-/**
- * Compare SMPL to BVH Viewer Widget
- * Side-by-side comparison of SMPL mesh and BVH skeleton using split view
- */
-
-import { app } from "../../../../scripts/app.js";
-
-console.log("[CompareSMPLBVH] Loading Compare SMPL BVH extension");
-
-// Node types that use this viewer
-const COMPARE_VIEWER_NODES = [
- {
- nodeName: "CompareSMPLtoBVH",
- extensionName: "comfy3d.comparesmplbvh",
- logPrefix: "[CompareSMPLBVH]",
- smplMeshApiPath: "/motioncapture/smpl_mesh"
- }
-];
-
-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('[CompareSMPLBVH] Could not detect extension folder:', e);
- }
- return null;
-}
-
-function getViewerUrl(extensionFolder) {
- return `/extensions/${extensionFolder}/viewer_compare_smpl_bvh.html?v=` + Date.now();
-}
-
-function createCompareViewerExtension(config) {
- const { extensionName, nodeName, logPrefix, smplMeshApiPath } = config;
-
- console.log(`${logPrefix} Loading Compare SMPL BVH 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);
-
- const container = document.createElement("div");
- container.style.width = "100%";
- container.style.height = "100%";
- container.style.background = "#222";
- container.style.display = "flex";
- container.style.flexDirection = "column";
-
- const iframe = document.createElement("iframe");
- iframe.style.width = "100%";
- iframe.style.height = "100%";
- iframe.style.border = "none";
- iframe.style.flexGrow = "1";
- iframe.src = getViewerUrl(extensionFolder);
- container.appendChild(iframe);
-
- // Store references
- this.compareViewerIframe = iframe;
- this.compareViewerReady = false;
- this.compareDataToLoad = null;
-
- // Add widget with computeSize for automatic resizing
- const widget = this.addDOMWidget("compare_viewer", "iframe", container);
- widget.computeSize = (w) => [w, 500];
-
- // Set an initial size for the node
- this.setSize([this.size[0], 500 + 30]);
-
- // Listen for viewer ready message
- const messageHandler = (event) => {
- if (event.source !== iframe.contentWindow) return;
- if (event.data.type === 'VIEWER_READY') {
- this.compareViewerReady = true;
- if (this.compareDataToLoad) {
- this.loadCompareData(this.compareDataToLoad);
- }
- }
- };
- window.addEventListener('message', messageHandler);
-
- // Cleanup on removal
- const originalOnRemoved = this.onRemoved;
- this.onRemoved = function() {
- window.removeEventListener('message', messageHandler);
- if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
- };
-
- // Method to load compare data
- this.loadCompareData = function(data) {
- if (!iframe.contentWindow) {
- this.compareDataToLoad = data;
- return;
- }
-
- if (!this.compareViewerReady) {
- this.compareDataToLoad = data;
- return;
- }
-
- iframe.contentWindow.postMessage({
- type: 'loadData',
- smplFilename: data.smplFilename,
- bvhContent: data.bvhContent,
- smplMeshApiPath: smplMeshApiPath
- }, '*');
- this.compareDataToLoad = null;
- };
-
- this.onExecuted = (msg) => {
- if (msg?.smpl_mesh_filename && msg?.bvh_content) {
- const data = {
- smplFilename: msg.smpl_mesh_filename[0],
- bvhContent: msg.bvh_content[0]
- };
-
- if (this.compareViewerReady) {
- this.loadCompareData(data);
- } else {
- this.compareDataToLoad = data;
- }
- }
- };
-
- return result;
- };
- }
- });
-
- console.log(`${logPrefix} Extension registered: ${extensionName}`);
-}
-
-// Auto-register all known compare viewer node types
-COMPARE_VIEWER_NODES.forEach(config => createCompareViewerExtension(config));
-
-export { createCompareViewerExtension, detectExtensionFolder };
diff --git a/javascript/js/debug_skeleton_widget.js b/javascript/js/debug_skeleton_widget.js
deleted file mode 100644
index d63b2090..00000000
--- a/javascript/js/debug_skeleton_widget.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createFBXPreviewExtension } from "./mesh_preview_fbx.js";
-
-// Register UniRigDebugSkeleton with the debug viewer
-createFBXPreviewExtension({
- nodeName: "UniRigDebugSkeleton",
- extensionName: "unirig.debugskeleton",
- logPrefix: "[UniRig Debug]",
- fbxExportApiPath: "/unirig/export_posed_fbx",
- viewerFile: "viewer_fbx_debug.html"
-});
diff --git a/javascript/js/fbx_animation_viewer.js b/javascript/js/fbx_animation_viewer.js
deleted file mode 100644
index ab2f9cd7..00000000
--- a/javascript/js/fbx_animation_viewer.js
+++ /dev/null
@@ -1,315 +0,0 @@
-/**
- * FBX Animation Viewer Widget - Interactive animation playback with Three.js
- * Controls animation playback, speed, looping, and skeleton visibility
- */
-
-import { app } from "../../../../scripts/app.js";
-
-console.log("[FBXAnimationViewer] Loading FBX Animation Viewer extension");
-
-// Node types that use this viewer
-const FBX_ANIMATION_NODES = [
- {
- nodeName: "FBXAnimationViewer",
- extensionName: "comfy3d.fbxanimationviewer",
- logPrefix: "[FBXAnimationViewer]"
- }
-];
-
-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('[FBXAnimationViewer] Could not detect extension folder:', e);
- }
- return null;
-}
-
-function getViewerUrl(extensionFolder) {
- return `/extensions/${extensionFolder}/viewer_fbx_animation.html?v=` + Date.now();
-}
-
-function createFBXAnimationViewerExtension(config) {
- const { extensionName, nodeName, logPrefix } = config;
-
- console.log(`${logPrefix} Loading FBX Animation 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);
-
- // Main Container
- const mainContainer = document.createElement("div");
- mainContainer.style.cssText = "width: 100%; height: 100%; display: flex; flex-direction: column; overflow: hidden; background: #1a1a1a; border: 1px solid #444; box-sizing: border-box;";
-
- // Viewer Area (Iframe)
- const viewerArea = document.createElement("div");
- viewerArea.style.cssText = "position: relative; flex-grow: 1; min-height: 200px; overflow: hidden;";
-
- const iframe = document.createElement("iframe");
- iframe.style.cssText = "display: block; width: 100%; height: 100%; border: none;";
- iframe.src = getViewerUrl(extensionFolder);
-
- viewerArea.appendChild(iframe);
- mainContainer.appendChild(viewerArea);
-
- // Controls Area
- const controlsContainer = document.createElement("div");
- controlsContainer.style.cssText = "flex-shrink: 0; background: #2a2a2a; border-top: 1px solid #444; padding: 10px; box-sizing: border-box; color: white; font-size: 12px; font-family: Arial, sans-serif;";
-
- controlsContainer.innerHTML = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- mainContainer.appendChild(controlsContainer);
-
- // Add Single Widget
- const widget = this.addDOMWidget("fbx_viewer_unified", "main", mainContainer, {
- serialize: false,
- hideOnZoom: false
- });
-
- widget.computeSize = (width) => [width, 500];
-
- // Get control elements
- const playPauseBtn = controlsContainer.querySelector('#playPauseBtn');
- const resetBtn = controlsContainer.querySelector('#resetBtn');
- const timeline = controlsContainer.querySelector('#timeline');
- const speedControl = controlsContainer.querySelector('#speedControl');
- const loopCheckbox = controlsContainer.querySelector('#loop');
- const animationSelect = controlsContainer.querySelector('#animationSelect');
- const showSkeleton = controlsContainer.querySelector('#showSkeleton');
- const showMesh = controlsContainer.querySelector('#showMesh');
- const xraySkeleton = controlsContainer.querySelector('#xraySkeleton');
- const resetCamera = controlsContainer.querySelector('#resetCamera');
- const currentTimeEl = controlsContainer.querySelector('#currentTime');
- const currentFrameEl = controlsContainer.querySelector('#currentFrame');
- const totalFramesEl = controlsContainer.querySelector('#totalFrames');
-
- // Store references
- this.animationViewerIframe = iframe;
- this.animationViewerReady = false;
- this.animationControls = {
- playPauseBtn, resetBtn, timeline, speedControl, loopCheckbox, animationSelect,
- showSkeleton, showMesh, xraySkeleton, resetCamera,
- currentTimeEl, currentFrameEl, totalFramesEl
- };
-
- // Wire up controls
- playPauseBtn.addEventListener('click', () => {
- iframe.contentWindow.postMessage({ type: 'PLAY_PAUSE' }, '*');
- });
-
- resetBtn.addEventListener('click', () => {
- iframe.contentWindow.postMessage({ type: 'RESET' }, '*');
- });
-
- timeline.addEventListener('input', (e) => {
- const progress = parseFloat(e.target.value) / 100;
- iframe.contentWindow.postMessage({ type: 'SET_TIMELINE', progress }, '*');
- });
-
- speedControl.addEventListener('change', (e) => {
- iframe.contentWindow.postMessage({ type: 'SET_SPEED', speed: parseFloat(e.target.value) }, '*');
- });
-
- loopCheckbox.addEventListener('change', (e) => {
- iframe.contentWindow.postMessage({ type: 'SET_LOOP', loop: e.target.checked }, '*');
- });
-
- animationSelect.addEventListener('change', (e) => {
- iframe.contentWindow.postMessage({ type: 'CHANGE_ANIMATION', index: parseInt(e.target.value) }, '*');
- });
-
- showSkeleton.addEventListener('change', (e) => {
- iframe.contentWindow.postMessage({ type: 'TOGGLE_SKELETON', visible: e.target.checked }, '*');
- });
-
- showMesh.addEventListener('change', (e) => {
- iframe.contentWindow.postMessage({ type: 'TOGGLE_MESH', visible: e.target.checked }, '*');
- });
-
- xraySkeleton.addEventListener('change', (e) => {
- iframe.contentWindow.postMessage({ type: 'TOGGLE_XRAY', xray: e.target.checked }, '*');
- });
-
- resetCamera.addEventListener('click', () => {
- iframe.contentWindow.postMessage({ type: 'RESET_CAMERA' }, '*');
- });
-
- // Listen for messages from iframe
- const messageHandler = (event) => {
- if (event.source !== iframe.contentWindow) return;
-
- const { type, ...data } = event.data;
-
- switch(type) {
- case 'VIEWER_READY':
- this.animationViewerReady = true;
- if (this.fbxPathToLoad) {
- this.loadAnimationInViewer(this.fbxPathToLoad);
- }
- break;
-
- case 'ANIMATIONS_LOADED':
- animationSelect.innerHTML = '';
- data.animations.forEach(anim => {
- const option = document.createElement('option');
- option.value = anim.index;
- option.textContent = anim.name;
- animationSelect.appendChild(option);
- });
-
- playPauseBtn.disabled = false;
- resetBtn.disabled = false;
- timeline.disabled = false;
- speedControl.disabled = false;
- loopCheckbox.disabled = false;
- animationSelect.disabled = data.animations.length <= 1;
- break;
-
- case 'TIME_UPDATE':
- if (currentTimeEl) currentTimeEl.textContent = data.time.toFixed(2) + 's';
- if (currentFrameEl) currentFrameEl.textContent = data.frame;
- if (totalFramesEl) totalFramesEl.textContent = data.totalFrames;
- timeline.value = data.progress;
- break;
-
- case 'PLAY_STATE_CHANGED':
- playPauseBtn.textContent = data.isPlaying ? '\u23F8 Pause' : '\u25B6 Play';
- break;
-
- case 'ANIMATION_CHANGED':
- const fps = 30;
- if (totalFramesEl) totalFramesEl.textContent = Math.floor(data.duration * fps);
- playPauseBtn.textContent = '\u23F8 Pause';
- break;
-
- case 'NO_ANIMATIONS':
- playPauseBtn.textContent = 'No Animation';
- playPauseBtn.disabled = true;
- break;
- }
- };
- window.addEventListener('message', messageHandler);
-
- // Cleanup on removal
- const originalOnRemoved = this.onRemoved;
- this.onRemoved = function() {
- window.removeEventListener('message', messageHandler);
- if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
- };
-
- // Set initial node size
- const nodeWidth = Math.max(512, this.size[0] || 512);
- this.setSize([nodeWidth, 500]);
-
- return result;
- };
-
- // Add method to load FBX
- nodeType.prototype.loadAnimationInViewer = function(fbxPath) {
- if (!this.animationViewerIframe || !this.animationViewerIframe.contentWindow) {
- this.fbxPathToLoad = fbxPath;
- return;
- }
-
- if (!this.animationViewerReady) {
- this.fbxPathToLoad = fbxPath;
- return;
- }
-
- let relativePath = fbxPath;
- if (fbxPath.includes('/output/')) {
- relativePath = fbxPath.split('/output/')[1];
- } else if (fbxPath.includes('/input/')) {
- relativePath = fbxPath.split('/input/')[1];
- } else {
- relativePath = fbxPath.split('/').pop();
- }
-
- const viewPath = window.location.origin + "/view?filename=" + encodeURIComponent(relativePath);
-
- this.animationViewerIframe.contentWindow.postMessage({
- type: 'LOAD_FBX',
- path: viewPath
- }, '*');
- this.fbxPathToLoad = null;
- };
-
- // Override onExecuted to load FBX
- const onExecuted = nodeType.prototype.onExecuted;
- nodeType.prototype.onExecuted = function(message) {
- if (onExecuted) onExecuted.apply(this, arguments);
-
- if (message?.fbx_path?.[0]) {
- const fbxPath = message.fbx_path[0];
- this.loadAnimationInViewer(fbxPath);
- }
- };
- }
- });
-
- console.log(`${logPrefix} Extension registered: ${extensionName}`);
-}
-
-// Auto-register all known FBX animation viewer node types
-FBX_ANIMATION_NODES.forEach(config => createFBXAnimationViewerExtension(config));
-
-export { createFBXAnimationViewerExtension, detectExtensionFolder };
diff --git a/javascript/js/gaussian_preview.js b/javascript/js/gaussian_preview.js
index b2e6a12b..e9ba6cff 100644
--- a/javascript/js/gaussian_preview.js
+++ b/javascript/js/gaussian_preview.js
@@ -15,7 +15,7 @@ const EXTENSION_FOLDER = (() => {
console.log("[GeomPack Gaussian] Loading extension...");
app.registerExtension({
- name: "geompack.gaussianpreview",
+ name: "geometrypack.gaussianpreview",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "GeomPackPreviewGaussian") {
@@ -101,6 +101,9 @@ app.registerExtension({
// Listen for messages from iframe
window.addEventListener('message', async (event) => {
+ // Without this check, every open viewer instance's listener
+ // fires for every iframe's messages, not just its own.
+ if (event.source !== iframe.contentWindow) return;
// Handle screenshot messages
if (event.data.type === 'SCREENSHOT' && event.data.image) {
try {
diff --git a/javascript/js/load_mesh_batch_path.js b/javascript/js/load_mesh_batch_path.js
index 45eed1fe..990bce14 100644
--- a/javascript/js/load_mesh_batch_path.js
+++ b/javascript/js/load_mesh_batch_path.js
@@ -15,7 +15,7 @@ function pathStem(value) {
}
app.registerExtension({
- name: "geompack.loadmeshbatch.path",
+ name: "geometrypack.loadmeshbatch.path",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "GeomPackLoadMeshBatch") return;
console.log(`${TAG} registering for GeomPackLoadMeshBatch`);
@@ -48,16 +48,21 @@ app.registerExtension({
});
widget.computeSize = (w) => [w, 30];
- // Floating dropdown attached to body (avoids canvas clipping/transform).
+ // Floating dropdown as a manual POPOVER inside our own widget DOM:
+ // showPopover() renders it in the browser's top layer, which
+ // escapes the canvas transform/clipping WITHOUT appending anything
+ // to the shared document.body (isolation: we never touch DOM we
+ // don't own). UA popover styles are reset (inset/margin/padding).
const menu = document.createElement("div");
- menu.style.cssText = "position:fixed;z-index:10000;max-height:240px;overflow-y:auto;" +
+ menu.popover = "manual";
+ menu.style.cssText = "position:fixed;inset:auto;margin:0;padding:0;max-height:240px;overflow-y:auto;" +
"background:#1e1e1e;border:1px solid #555;border-radius:4px;" +
- "box-shadow:0 4px 14px rgba(0,0,0,0.55);font:12px monospace;display:none;";
- document.body.appendChild(menu);
+ "box-shadow:0 4px 14px rgba(0,0,0,0.55);font:12px monospace;";
+ wrap.appendChild(menu);
let items = [];
let lastDir = null;
- const hide = () => { menu.style.display = "none"; };
+ const hide = () => { if (menu.matches(":popover-open")) menu.hidePopover(); };
const place = () => {
const b = input.getBoundingClientRect();
menu.style.left = b.left + "px";
@@ -84,8 +89,8 @@ app.registerExtension({
});
menu.appendChild(row);
}
+ if (!menu.matches(":popover-open")) menu.showPopover();
place();
- menu.style.display = "block";
};
const fetchDir = async (force) => {
const [dir, rem] = pathStem(input.value);
diff --git a/javascript/js/load_mesh_upload.js b/javascript/js/load_mesh_upload.js
index c2df112b..6ed00231 100644
--- a/javascript/js/load_mesh_upload.js
+++ b/javascript/js/load_mesh_upload.js
@@ -67,7 +67,7 @@ function selectValue(node, val) {
}
app.registerExtension({
- name: "geompack.loadmesh.upload",
+ name: "geometrypack.loadmesh.upload",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "GeomPackLoadMesh") return;
console.log(`${TAG} registering for GeomPackLoadMesh`);
@@ -142,8 +142,10 @@ app.registerExtension({
const input = document.createElement("input");
input.type = "file"; input.accept = ACCEPT; input.multiple = true; input.style.display = "none";
input.addEventListener("change", async () => { await uploadList(input.files); input.value = ""; });
- document.body.appendChild(input);
- node._gpFileInput = input; // removed in onRemoved below
+ // Deliberately NOT attached to the DOM: input.click() opens the
+ // picker from a detached element in all modern browsers, and
+ // staying detached keeps us out of the shared document entirely.
+ node._gpFileInput = input;
node.addWidget("button", "⬆ upload / drop mesh", null, () => { console.log(`${TAG} upload button clicked`); input.click(); });
// inline View 3D
diff --git a/javascript/js/mesh_preview.js b/javascript/js/mesh_preview.js
index 12859e66..ce235b92 100644
--- a/javascript/js/mesh_preview.js
+++ b/javascript/js/mesh_preview.js
@@ -15,7 +15,7 @@ const EXTENSION_FOLDER = (() => {
console.log("[GeomPack] Loading mesh preview extension...");
app.registerExtension({
- name: "geompack.meshpreview",
+ name: "geometrypack.meshpreview",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "GeomPackPreviewMesh") {
diff --git a/javascript/js/mesh_preview_dual.js b/javascript/js/mesh_preview_dual.js
index cf68189d..8a1457f8 100644
--- a/javascript/js/mesh_preview_dual.js
+++ b/javascript/js/mesh_preview_dual.js
@@ -11,7 +11,7 @@ import { buildDualMeshInfoHTML, formatExtents } from "./utils/formatting.js";
import { createViewerManager, createErrorHandler, buildViewUrl, createLoadDualMeshMessage } from "./utils/postMessage.js";
app.registerExtension({
- name: "geompack.meshpreview.dual",
+ name: "geometrypack.meshpreview.dual",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "GeomPackPreviewMeshDual") {
@@ -68,7 +68,7 @@ app.registerExtension({
const viewerManager = createViewerManager(iframe, "[GeomPack Dual]");
// Listen for error messages
- window.addEventListener('message', createErrorHandler(infoPanel, "[GeomPack Dual]"));
+ window.addEventListener('message', createErrorHandler(infoPanel, "[GeomPack Dual]", iframe));
// Set initial node size
this.setSize([768, 680]);
diff --git a/javascript/js/mesh_preview_fbx.js b/javascript/js/mesh_preview_fbx.js
deleted file mode 100644
index b900468c..00000000
--- a/javascript/js/mesh_preview_fbx.js
+++ /dev/null
@@ -1,325 +0,0 @@
-/**
- * ComfyUI FBX Rigged Mesh Preview Widget
- *
- * Generic widget for FBX preview nodes. Loads viewer from file (not inline).
- * Used by UniRig, SAM3DBody, and other extensions that preview FBX files.
- *
- * This file auto-registers for all known FBX preview node types when loaded.
- * Extensions copy this file from the comfy-3d-viewers package at startup.
- */
-
-import { app } from "../../../../scripts/app.js";
-
-// Auto-register for all known FBX preview node types
-const FBX_PREVIEW_NODES = [
- {
- nodeName: "UniRigPreviewRiggedMesh",
- extensionName: "unirig.fbxpreview",
- logPrefix: "[UniRig]",
- fbxExportApiPath: "/unirig/export_posed_fbx"
- },
- {
- nodeName: "SAM3DBodyPreviewRiggedMesh",
- extensionName: "sam3dbody.meshpreview",
- logPrefix: "[SAM3DBody]"
- // Note: SAM3DBody doesn't have FBX export API
- },
- {
- nodeName: "MocapPreviewRiggedMesh",
- extensionName: "motioncapture.meshpreview",
- logPrefix: "[MotionCapture]"
- },
- {
- nodeName: "FBXPreview",
- extensionName: "motioncapture.fbxpreview.legacy",
- logPrefix: "[MotionCapture]"
- }
-];
-
-/**
- * Detect the extension folder name from the current script URL
- */
-function detectExtensionFolder() {
- try {
- // Try to detect from import.meta.url if available
- if (typeof import.meta !== 'undefined' && import.meta.url) {
- const match = import.meta.url.match(/\/extensions\/([^\/]+)\//);
- if (match) {
- return match[1];
- }
- }
-
- // Fallback: look at currently loading script
- 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('[FBX Preview] Could not detect extension folder:', e);
- }
- return null;
-}
-
-/**
- * Get the viewer URL for the FBX viewer HTML file
- * @param {string} extensionFolder - The extension folder name
- * @returns {string} The URL to the viewer HTML
- */
-function getViewerUrl(extensionFolder, viewerFile = "viewer_fbx.html") {
- // Add cache-busting timestamp
- return `/extensions/${extensionFolder}/${viewerFile}?v=` + Date.now();
-}
-
-/**
- * Create and register an FBX preview extension
- * @param {Object} config - Configuration object
- * @param {string} config.extensionName - Unique extension name (e.g., "unirig.fbxpreview")
- * @param {string} config.nodeName - Node type name to hook (e.g., "UniRigPreviewRiggedMesh")
- * @param {string} config.logPrefix - Log prefix for console messages (e.g., "[UniRig]")
- * @param {string} [config.extensionFolder] - Override extension folder detection
- * @param {string} [config.fbxExportApiPath] - API path for FBX export (e.g., "/unirig/export_posed_fbx")
- */
-export function createFBXPreviewExtension(config) {
- const {
- extensionName,
- nodeName,
- logPrefix,
- extensionFolder: configExtensionFolder,
- fbxExportApiPath
- } = config;
-
- console.log(`${logPrefix} Loading FBX mesh preview extension...`);
-
- // Detect extension folder if not provided
- const extensionFolder = configExtensionFolder || detectExtensionFolder();
- if (!extensionFolder) {
- console.error(`${logPrefix} Could not detect extension folder`);
- return;
- }
- console.log(`${logPrefix} Detected extension folder: ${extensionFolder}`);
-
- app.registerExtension({
- name: extensionName,
-
- async beforeRegisterNodeDef(nodeType, nodeData, app) {
- if (nodeData.name === nodeName) {
- console.log(`${logPrefix} Registering Preview Rigged Mesh node`);
-
- const onNodeCreated = nodeType.prototype.onNodeCreated;
- nodeType.prototype.onNodeCreated = function() {
- const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
-
- console.log(`${logPrefix} Node created, adding FBX viewer widget`);
-
- // Create iframe for FBX viewer
- const iframe = document.createElement("iframe");
- iframe.style.width = "100%";
- iframe.style.flex = "1 1 0";
- iframe.style.minHeight = "0";
- iframe.style.border = "none";
- iframe.style.backgroundColor = "#2a2a2a";
-
- // Load viewer from file URL (not inline)
- const viewerUrl = getViewerUrl(extensionFolder);
- iframe.src = viewerUrl;
- console.log(`${logPrefix} Setting iframe src to: ${viewerUrl}`);
-
- // Add load event listener
- iframe.onload = () => {
- console.log(`${logPrefix} Iframe loaded successfully`);
- };
- iframe.onerror = (e) => {
- console.error(`${logPrefix} Iframe failed to load:`, e);
- };
-
- // Add widget
- const widget = this.addDOMWidget("preview", "FBX_PREVIEW", iframe, {
- getValue() { return ""; },
- setValue(v) { }
- });
-
- console.log(`${logPrefix} Widget created:`, widget);
-
- // Set widget size - allow flexible height
- widget.computeSize = function(width) {
- const w = width || 512;
- const h = w * 1.5; // Taller than wide to accommodate controls
- return [w, h];
- };
-
- widget.element = iframe;
-
- // Store iframe reference
- this.fbxViewerIframe = iframe;
- this.fbxViewerReady = false;
-
- // Listen for ready message from iframe
- const onMessage = (event) => {
- if (event.data && event.data.type === 'VIEWER_READY') {
- console.log(`${logPrefix} Viewer iframe is ready!`);
- this.fbxViewerReady = true;
- }
- };
- window.addEventListener('message', onMessage.bind(this));
-
- const notifyIframeResize = () => {
- if (iframe.contentWindow) {
- const rect = iframe.getBoundingClientRect();
- iframe.contentWindow.postMessage({
- type: 'RESIZE',
- width: rect.width,
- height: rect.height
- }, '*');
- }
- };
-
- this.onResize = function(size) {
- const isVueNodes = iframe.closest('[data-node-id]') !== null ||
- document.querySelector('.vue-graph-canvas') !== null;
-
- if (!isVueNodes && size && size[1]) {
- const nodeHeight = size[1];
- const headerHeight = 70;
- const availableHeight = Math.max(200, nodeHeight - headerHeight);
- iframe.style.height = availableHeight + 'px';
- }
-
- requestAnimationFrame(() => {
- notifyIframeResize();
- });
- };
-
- let resizeTimeout = null;
- let lastSize = { width: 0, height: 0 };
- const resizeObserver = new ResizeObserver((entries) => {
- const entry = entries[0];
- const newWidth = entry.contentRect.width;
- const newHeight = entry.contentRect.height;
-
- if (Math.abs(newWidth - lastSize.width) < 1 && Math.abs(newHeight - lastSize.height) < 1) {
- return;
- }
- lastSize = { width: newWidth, height: newHeight };
-
- if (resizeTimeout) {
- clearTimeout(resizeTimeout);
- }
- resizeTimeout = setTimeout(() => {
- notifyIframeResize();
- }, 50);
- });
- resizeObserver.observe(iframe);
-
- const originalOnRemoved = this.onRemoved;
- this.onRemoved = function() {
- resizeObserver.disconnect();
- if (resizeTimeout) {
- clearTimeout(resizeTimeout);
- }
- window.removeEventListener('message', onMessage);
- if (originalOnRemoved) {
- originalOnRemoved.apply(this, arguments);
- }
- };
-
- // Set initial node size (taller to accommodate controls)
- this.setSize([512, 768]);
-
- // Handle execution
- const onExecuted = this.onExecuted;
- this.onExecuted = function(message) {
- console.log(`${logPrefix} onExecuted called with message:`, message);
- onExecuted?.apply(this, arguments);
-
- // The message contains the FBX file path
- if (message?.fbx_file && message.fbx_file[0]) {
- const filename = message.fbx_file[0];
- console.log(`${logPrefix} Loading FBX: ${filename}`);
-
- // Try different path formats based on filename
- let filepath;
- let folderType = 'output'; // default
-
- // Detect folder type from path
- if (filename.includes('/input/') || filename.includes('\\input\\')) {
- folderType = 'input';
- }
-
- // Extract basename if full path provided
- const basename = filename.includes('/') || filename.includes('\\')
- ? filename.split(/[/\\]/).pop()
- : filename;
-
- filepath = `${window.location.origin}/view?filename=${encodeURIComponent(basename)}&type=${folderType}&subfolder=`;
-
- // Send message to iframe
- const sendMessage = () => {
- if (iframe.contentWindow) {
- console.log(`${logPrefix} Sending postMessage to iframe: ${filepath}`);
- const messageData = {
- type: "LOAD_FBX",
- filepath: filepath,
- timestamp: Date.now()
- };
- // Include custom FBX export API path if configured
- if (fbxExportApiPath) {
- messageData.fbxExportApiPath = fbxExportApiPath;
- }
- iframe.contentWindow.postMessage(messageData, "*");
- } else {
- console.error(`${logPrefix} Iframe contentWindow not available`);
- }
- };
-
- // Wait for iframe to be ready, or use timeout as fallback
- if (this.fbxViewerReady) {
- sendMessage();
- } else {
- const checkReady = setInterval(() => {
- if (this.fbxViewerReady) {
- clearInterval(checkReady);
- sendMessage();
- }
- }, 50);
-
- // Fallback timeout after 2 seconds
- setTimeout(() => {
- clearInterval(checkReady);
- if (!this.fbxViewerReady) {
- console.warn(`${logPrefix} Iframe not ready after 2s, sending anyway`);
- sendMessage();
- }
- }, 2000);
- }
- } else {
- console.log(`${logPrefix} No fbx_file in message data. Keys:`, Object.keys(message || {}));
- }
- };
-
- return r;
- };
- }
- }
- });
-
- console.log(`${logPrefix} FBX mesh preview extension registered`);
-}
-
-// Export detectExtensionFolder for use by other modules
-export { detectExtensionFolder, getViewerUrl };
-
-// Self-executing: register all known FBX preview node types
-console.log("[FBX Preview] Auto-registering for all known FBX preview node types...");
-FBX_PREVIEW_NODES.forEach(config => {
- try {
- createFBXPreviewExtension(config);
- } catch (e) {
- // Extension already registered by another custom node copy - skip
- }
-});
diff --git a/javascript/js/mesh_preview_multi.js b/javascript/js/mesh_preview_multi.js
index ed58973e..2ad21d33 100644
--- a/javascript/js/mesh_preview_multi.js
+++ b/javascript/js/mesh_preview_multi.js
@@ -15,7 +15,7 @@ const EXTENSION_FOLDER = (() => {
console.log('[GeomPack Multi JS] Loading mesh_preview_multi.js extension');
app.registerExtension({
- name: "geompack.meshpreview.multi",
+ name: "geometrypack.meshpreview.multi",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "GeomPackPreviewMeshMulti") {
diff --git a/javascript/js/mesh_preview_uv.js b/javascript/js/mesh_preview_uv.js
index ca773c09..f92eabbd 100644
--- a/javascript/js/mesh_preview_uv.js
+++ b/javascript/js/mesh_preview_uv.js
@@ -15,7 +15,7 @@ const EXTENSION_FOLDER = (() => {
console.log("[GeomPack] Loading UV mesh preview extension...");
app.registerExtension({
- name: "geompack.meshpreviewuv",
+ name: "geometrypack.meshpreviewuv",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "GeomPackPreviewMeshUV") {
diff --git a/javascript/js/mesh_preview_vtk.js b/javascript/js/mesh_preview_vtk.js
index f56caf4c..479c9b2b 100644
--- a/javascript/js/mesh_preview_vtk.js
+++ b/javascript/js/mesh_preview_vtk.js
@@ -11,7 +11,7 @@ import { createScreenshotHandler } from "./utils/screenshot.js";
import { createViewerManager, createErrorHandler, buildViewUrl } from "./utils/postMessage.js";
app.registerExtension({
- name: "geompack.meshpreview.vtk",
+ name: "geometrypack.meshpreview.vtk",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "GeomPackPreviewMeshVTK") {
@@ -76,8 +76,8 @@ app.registerExtension({
const viewerManager = createViewerManager(iframe, "[GeomPack VTK]");
// Listen for screenshot and error messages
- window.addEventListener('message', createScreenshotHandler('vtk-screenshot'));
- window.addEventListener('message', createErrorHandler(infoPanel, "[GeomPack VTK]"));
+ window.addEventListener('message', createScreenshotHandler('vtk-screenshot', iframe));
+ window.addEventListener('message', createErrorHandler(infoPanel, "[GeomPack VTK]", iframe));
// Set initial node size
this.setSize([512, 640]);
diff --git a/javascript/js/mesh_preview_vtk_batch.js b/javascript/js/mesh_preview_vtk_batch.js
index c42f0b63..46710396 100644
--- a/javascript/js/mesh_preview_vtk_batch.js
+++ b/javascript/js/mesh_preview_vtk_batch.js
@@ -13,7 +13,7 @@ const EXTENSION_FOLDER = (() => {
})();
app.registerExtension({
- name: "geompack.meshpreview.vtk.batch",
+ name: "geometrypack.meshpreview.vtk.batch",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "GeomPackPreviewMeshVTKBatch") {
diff --git a/javascript/js/mesh_preview_vtk_textured.js b/javascript/js/mesh_preview_vtk_textured.js
index f1e53290..33b9595e 100644
--- a/javascript/js/mesh_preview_vtk_textured.js
+++ b/javascript/js/mesh_preview_vtk_textured.js
@@ -15,7 +15,7 @@ const EXTENSION_FOLDER = (() => {
console.log("[GeomPack] Loading VTK.js textured mesh preview extension...");
app.registerExtension({
- name: "geompack.meshpreview.vtk.textured",
+ name: "geometrypack.meshpreview.vtk.textured",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "GeomPackPreviewMeshVTKWithTexture") {
@@ -83,6 +83,9 @@ app.registerExtension({
// Listen for messages from iframe
window.addEventListener('message', async (event) => {
+ // Without this check, every open viewer instance's listener
+ // fires for every iframe's messages, not just its own.
+ if (event.source !== iframe.contentWindow) return;
// Handle screenshot messages
if (event.data.type === 'SCREENSHOT' && event.data.image) {
console.log('[GeomPack VTK Textured] Received screenshot from iframe');
diff --git a/javascript/js/pointcloud_preview_vtk.js b/javascript/js/pointcloud_preview_vtk.js
deleted file mode 100644
index 57ef7f50..00000000
--- a/javascript/js/pointcloud_preview_vtk.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * VTK.js Point Cloud Preview Widget
- * For point cloud visualization nodes (SAM3, DepthAnythingV3, etc.)
- */
-
-import { app } from "../../../scripts/app.js";
-import { EXTENSION_FOLDER, getViewerUrl } from "./utils/extensionFolder.js";
-import { createContainer, createIframe, createInfoPanel, createWidgetOptions } from "./utils/uiComponents.js";
-import { createViewerManager, createErrorHandler, buildViewUrl } from "./utils/postMessage.js";
-
-const POINTCLOUD_NODES = ["SAM3D_PreviewPointCloud", "DA3_PreviewPointCloud"];
-
-app.registerExtension({
- name: "comfy3d.pointcloud.vtk",
-
- async beforeRegisterNodeDef(nodeType, nodeData, app) {
- if (POINTCLOUD_NODES.includes(nodeData.name)) {
-
- const onNodeCreated = nodeType.prototype.onNodeCreated;
- nodeType.prototype.onNodeCreated = function() {
- const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
-
- // Create container for viewer + info panel
- const container = createContainer();
-
- // Create iframe for VTK.js viewer
- const iframe = createIframe(getViewerUrl("viewer_vtk"));
-
- // Create info panel
- const infoPanel = createInfoPanel("Point cloud will appear after execution");
-
- // Add iframe and info panel to container
- container.appendChild(iframe);
- container.appendChild(infoPanel);
-
- // Add widget
- const widget = this.addDOMWidget("preview_vtk", "POINTCLOUD_PREVIEW_VTK", container, createWidgetOptions());
- widget.computeSize = () => [512, 580];
-
- // Store references
- this._vtkIframe = iframe;
- this._infoPanel = infoPanel;
-
- // Create viewer manager
- const viewerManager = createViewerManager(iframe, "[PointCloud VTK]");
-
- // Listen for error messages
- window.addEventListener('message', createErrorHandler(infoPanel, "[PointCloud VTK]"));
-
- // Set initial node size
- this.setSize([512, 580]);
-
- return r;
- };
-
- // Handle execution
- const onExecuted = nodeType.prototype.onExecuted;
- nodeType.prototype.onExecuted = function(message) {
- onExecuted?.apply(this, arguments);
-
- // Get file path from ui.file_path
- let filePath = null;
- if (message?.ui?.file_path) {
- filePath = Array.isArray(message.ui.file_path) ? message.ui.file_path[0] : message.ui.file_path;
- } else if (message?.file_path) {
- filePath = Array.isArray(message.file_path) ? message.file_path[0] : message.file_path;
- }
-
- if (filePath && filePath.trim() !== '') {
- // Normalize path
- filePath = filePath.replace(/\\/g, '/').trim();
-
- // Build URL for ComfyUI view endpoint
- const outputMatch = filePath.match(/(?:^|\/)(output|input)\/(.+)$/);
- let url;
- if (outputMatch) {
- const [, type, relativePath] = outputMatch;
- const pathParts = relativePath.split('/');
- const filename = pathParts.pop();
- const subfolder = pathParts.join('/');
- url = `/view?filename=${encodeURIComponent(filename)}&type=${type}&subfolder=${encodeURIComponent(subfolder)}`;
- } else {
- const filename = filePath.split('/').pop();
- url = `/view?filename=${encodeURIComponent(filename)}&type=output&subfolder=`;
- }
-
- // Update info panel
- if (this._infoPanel) {
- const filename = filePath.split('/').pop();
- this._infoPanel.innerHTML = `File: ${filename}`;
- }
-
- // Send message to iframe
- if (this._vtkIframe?.contentWindow) {
- const sendMessage = () => {
- this._vtkIframe.contentWindow.postMessage({
- type: 'LOAD_MESH',
- filepath: url
- }, '*');
- };
-
- if (this._vtkIframe.contentDocument?.readyState === 'complete') {
- sendMessage();
- } else {
- this._vtkIframe.addEventListener('load', () => setTimeout(sendMessage, 100), { once: true });
- }
- }
- }
- };
- }
- }
-});
diff --git a/javascript/js/preview_mesh_batch_render.js b/javascript/js/preview_mesh_batch_render.js
index 94f4c62d..a47cf1ae 100644
--- a/javascript/js/preview_mesh_batch_render.js
+++ b/javascript/js/preview_mesh_batch_render.js
@@ -14,13 +14,13 @@ function toggleWidget(w, show) {
w.type = w.__gpOrig.type;
w.computeSize = w.__gpOrig.computeSize;
} else {
- w.type = "geompack_hidden"; // unknown type -> not drawn
+ w.type = "geometrypack_hidden"; // unknown type -> not drawn
w.computeSize = () => [0, -4]; // collapse (cancels the 4px row gap)
}
}
app.registerExtension({
- name: "geompack.previewmeshbatchrender.ui",
+ name: "geometrypack.previewmeshbatchrender.ui",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "GeomPackPreviewMeshBatchRender") return;
console.log(`${TAG} registering for GeomPackPreviewMeshBatchRender`);
diff --git a/javascript/js/preview_mesh_boundaries.js b/javascript/js/preview_mesh_boundaries.js
index 089655c2..2fc28102 100644
--- a/javascript/js/preview_mesh_boundaries.js
+++ b/javascript/js/preview_mesh_boundaries.js
@@ -26,7 +26,7 @@ function viewUrl(name) {
}
app.registerExtension({
- name: "geompack.previewmeshboundaries",
+ name: "geometrypack.previewmeshboundaries",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "GeomPackPreviewMeshBoundaries") return;
console.log(`${TAG} registering for GeomPackPreviewMeshBoundaries`);
diff --git a/javascript/js/preview_mesh_multi_batch.js b/javascript/js/preview_mesh_multi_batch.js
index b4b9ebd6..b0c75895 100644
--- a/javascript/js/preview_mesh_multi_batch.js
+++ b/javascript/js/preview_mesh_multi_batch.js
@@ -15,7 +15,7 @@ const EXTENSION_FOLDER = (() => {
console.log('[GeomPack Multi Batch JS] Loading preview_mesh_multi_batch.js extension');
app.registerExtension({
- name: "geompack.meshpreview.multibatch",
+ name: "geometrypack.meshpreview.multibatch",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "GeomPackPreviewMeshMultiBatch") {
diff --git a/javascript/js/preview_mesh_slicer.js b/javascript/js/preview_mesh_slicer.js
index 1285f6b0..4f4a6f44 100644
--- a/javascript/js/preview_mesh_slicer.js
+++ b/javascript/js/preview_mesh_slicer.js
@@ -24,7 +24,7 @@ function viewUrl(name) {
}
app.registerExtension({
- name: "geompack.previewmeshslicer",
+ name: "geometrypack.previewmeshslicer",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "GeomPackPreviewMeshSlicer") return;
console.log(`${TAG} registering for GeomPackPreviewMeshSlicer`);
diff --git a/javascript/js/text_report.js b/javascript/js/text_report.js
index 161239c3..f71781c9 100644
--- a/javascript/js/text_report.js
+++ b/javascript/js/text_report.js
@@ -92,7 +92,7 @@ const TOGGLE_HEIGHT = 28;
const DEFAULT_CONTENT_HEIGHT = 60;
app.registerExtension({
- name: "geompack.text_report",
+ name: "geometrypack.text_report",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (!TEXT_REPORT_NODES.includes(nodeData.name)) return;
diff --git a/javascript/js/ultimate_mesh_inspection.js b/javascript/js/ultimate_mesh_inspection.js
index 318df2af..94e8fc90 100644
--- a/javascript/js/ultimate_mesh_inspection.js
+++ b/javascript/js/ultimate_mesh_inspection.js
@@ -15,7 +15,7 @@ const EXTENSION_FOLDER = (() => {
})();
app.registerExtension({
- name: "geompack.ultimate_mesh_inspection",
+ name: "geometrypack.ultimate_mesh_inspection",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "GeomPackUltimateMeshInspection") return;
diff --git a/javascript/js/utils/index.js b/javascript/js/utils/index.js
deleted file mode 100644
index a2b1baba..00000000
--- a/javascript/js/utils/index.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * ComfyUI 3D Viewer Utilities
- * Shared utilities for ComfyUI 3D viewer integration
- */
-
-export * from './extensionFolder.js';
-export * from './screenshot.js';
-export * from './uiComponents.js';
-export * from './formatting.js';
-export * from './analysisPanel.js';
-export * from './postMessage.js';
diff --git a/javascript/js/utils/postMessage.js b/javascript/js/utils/postMessage.js
index 31e19c1c..d74fa1c6 100644
--- a/javascript/js/utils/postMessage.js
+++ b/javascript/js/utils/postMessage.js
@@ -182,10 +182,14 @@ export function createViewerManager(iframe, logPrefix = "[Viewer]") {
* Create an error message handler for iframe errors
* @param {HTMLElement} infoPanel - The info panel to display errors
* @param {string} logPrefix - Logging prefix
+ * @param {HTMLIFrameElement} iframe - Only accept messages from this iframe
* @returns {Function} Event handler function
*/
-export function createErrorHandler(infoPanel, logPrefix = "[Viewer]") {
+export function createErrorHandler(infoPanel, logPrefix = "[Viewer]", iframe = null) {
return (event) => {
+ // Provenance guard: without it this fires for EVERY pack's iframe
+ // messages in the shared page, not just ours.
+ if (iframe && event.source !== iframe.contentWindow) return;
if (event.data.type === 'MESH_ERROR' && event.data.error) {
console.error(`${logPrefix} Error from viewer:`, event.data.error);
if (infoPanel) {
diff --git a/javascript/js/utils/screenshot.js b/javascript/js/utils/screenshot.js
index 2b9d09e4..fffa6d74 100644
--- a/javascript/js/utils/screenshot.js
+++ b/javascript/js/utils/screenshot.js
@@ -55,11 +55,15 @@ export async function uploadScreenshot(dataUrl, prefix = 'screenshot') {
/**
* Create a message handler for screenshot events from iframe
* @param {string} prefix - Filename prefix for screenshots
+ * @param {HTMLIFrameElement} iframe - Only accept messages from this iframe
* @param {Function} onError - Error callback
* @returns {Function} Event handler function
*/
-export function createScreenshotHandler(prefix = 'screenshot', onError = console.error) {
+export function createScreenshotHandler(prefix = 'screenshot', iframe = null, onError = console.error) {
return async (event) => {
+ // Provenance guard: without it this fires for EVERY pack's iframe
+ // messages in the shared page, not just ours.
+ if (iframe && event.source !== iframe.contentWindow) return;
if (event.data.type === 'SCREENSHOT' && event.data.image) {
try {
await uploadScreenshot(event.data.image, prefix);
diff --git a/javascript/js/viewer/core/ActorManager.js b/javascript/js/viewer/core/ActorManager.js
deleted file mode 100644
index 0ce7c26f..00000000
--- a/javascript/js/viewer/core/ActorManager.js
+++ /dev/null
@@ -1,276 +0,0 @@
-/**
- * ActorManager - Unified actor/property management for VTK viewers
- *
- * Handles actor configuration, appearance settings, edge visibility,
- * and material properties. Works with both single actors and actor arrays.
- */
-
-import { hexToRgb } from '../utils/ColorUtils.js';
-
-/**
- * Default actor appearance settings
- */
-const DEFAULT_SETTINGS = {
- color: [1.0, 1.0, 1.0], // White
- edgeColor: [0.0, 0.0, 0.0], // Black edges
- ambient: 0.4,
- diffuse: 0.8,
- specular: 0.3,
- specularPower: 20,
- edgeVisibility: false,
- lineWidth: 1,
- pointSize: 5,
- representation: 2, // 0=Points, 1=Wireframe, 2=Surface
- opacity: 1.0
-};
-
-/**
- * Representation mode constants
- */
-export const Representation = {
- POINTS: 0,
- WIREFRAME: 1,
- SURFACE: 2
-};
-
-export class ActorManager {
- /**
- * Create an ActorManager
- * @param {Object} renderWindow - VTK render window for triggering renders
- */
- constructor(renderWindow) {
- this.renderWindow = renderWindow;
- this.actors = [];
- this.settings = { ...DEFAULT_SETTINGS };
- }
-
- /**
- * Add an actor to be managed
- * @param {Object} actor - VTK actor instance
- */
- addActor(actor) {
- this.actors.push(actor);
- this.configureActor(actor, this.settings);
- }
-
- /**
- * Remove an actor from management
- * @param {Object} actor - VTK actor instance
- */
- removeActor(actor) {
- const index = this.actors.indexOf(actor);
- if (index > -1) {
- this.actors.splice(index, 1);
- }
- }
-
- /**
- * Clear all managed actors
- */
- clearActors() {
- this.actors = [];
- }
-
- /**
- * Set actors to manage (replaces existing)
- * @param {Object[]} actors - Array of VTK actor instances
- */
- setActors(actors) {
- this.actors = Array.isArray(actors) ? actors : [actors];
- this.actors.forEach(actor => this.configureActor(actor, this.settings));
- }
-
- /**
- * Configure a single actor with settings
- * @param {Object} actor - VTK actor instance
- * @param {Object} settings - Settings object
- */
- configureActor(actor, settings = {}) {
- const merged = { ...this.settings, ...settings };
- const property = actor.getProperty();
-
- if (!property) {
- console.warn('[ActorManager] Actor has no property object');
- return;
- }
-
- // Color (supports hex string or RGB array)
- if (merged.color) {
- const rgb = typeof merged.color === 'string'
- ? hexToRgb(merged.color)
- : merged.color;
- property.setColor(...rgb);
- }
-
- // Edge color
- if (merged.edgeColor) {
- const rgb = typeof merged.edgeColor === 'string'
- ? hexToRgb(merged.edgeColor)
- : merged.edgeColor;
- property.setEdgeColor(...rgb);
- }
-
- // Lighting properties
- if (merged.ambient !== undefined) property.setAmbient(merged.ambient);
- if (merged.diffuse !== undefined) property.setDiffuse(merged.diffuse);
- if (merged.specular !== undefined) property.setSpecular(merged.specular);
- if (merged.specularPower !== undefined) property.setSpecularPower(merged.specularPower);
-
- // Edge and line properties
- if (merged.edgeVisibility !== undefined) property.setEdgeVisibility(merged.edgeVisibility);
- if (merged.lineWidth !== undefined) property.setLineWidth(merged.lineWidth);
-
- // Point properties
- if (merged.pointSize !== undefined) property.setPointSize(merged.pointSize);
-
- // Representation mode
- if (merged.representation !== undefined) property.setRepresentation(merged.representation);
-
- // Opacity
- if (merged.opacity !== undefined) property.setOpacity(merged.opacity);
- }
-
- /**
- * Apply settings to all managed actors
- * @param {Object} settings - Settings object
- * @param {boolean} render - Whether to trigger a render (default: true)
- */
- applySettings(settings, render = true) {
- this.settings = { ...this.settings, ...settings };
- this.actors.forEach(actor => this.configureActor(actor, this.settings));
- if (render) {
- this.renderWindow.render();
- }
- }
-
- /**
- * Set edge visibility on all managed actors
- * @param {boolean} visible - Whether edges should be visible
- */
- setEdgeVisibility(visible) {
- this.applySettings({ edgeVisibility: visible });
- }
-
- /**
- * Set color on all managed actors
- * @param {string|number[]} color - Hex string or RGB array (0-1 range)
- */
- setColor(color) {
- this.applySettings({ color });
- }
-
- /**
- * Set opacity on all managed actors
- * @param {number} opacity - Opacity value (0-1)
- */
- setOpacity(opacity) {
- this.applySettings({ opacity });
- }
-
- /**
- * Set representation mode on all managed actors
- * @param {number} mode - 0=Points, 1=Wireframe, 2=Surface
- */
- setRepresentation(mode) {
- this.applySettings({ representation: mode });
- }
-
- /**
- * Set line width for edges/wireframe
- * @param {number} width - Line width in pixels
- */
- setLineWidth(width) {
- this.applySettings({ lineWidth: width });
- }
-
- /**
- * Set point size for point cloud rendering
- * @param {number} size - Point size in pixels
- */
- setPointSize(size) {
- this.applySettings({ pointSize: size });
- }
-
- /**
- * Get current settings
- * @returns {Object} Current settings object
- */
- getSettings() {
- return { ...this.settings };
- }
-
- /**
- * Apply settings from a settings panel (handles hex colors and number parsing)
- * @param {Object} panelValues - Values from settings panel inputs
- */
- applyPanelSettings(panelValues) {
- const settings = {};
-
- if (panelValues.meshColor !== undefined) {
- settings.color = panelValues.meshColor;
- }
- if (panelValues.pointSize !== undefined) {
- settings.pointSize = parseFloat(panelValues.pointSize);
- }
- if (panelValues.edgeWidth !== undefined) {
- settings.lineWidth = parseFloat(panelValues.edgeWidth);
- }
- if (panelValues.representation !== undefined) {
- settings.representation = parseInt(panelValues.representation, 10);
- }
- if (panelValues.opacity !== undefined) {
- settings.opacity = parseFloat(panelValues.opacity);
- }
-
- this.applySettings(settings);
- }
-
- /**
- * Configure for textured mode (disables scalar visibility, sets material)
- * @param {Object} actor - VTK actor instance
- */
- configureForTexture(actor) {
- const property = actor.getProperty();
- const mapper = actor.getMapper();
-
- if (property) {
- // Material settings for proper texture display
- if (typeof property.setMetallic === 'function') {
- property.setMetallic(0.0);
- }
- if (typeof property.setRoughness === 'function') {
- property.setRoughness(1.0);
- }
- if (typeof property.setInterpolation === 'function') {
- property.setInterpolation(2); // Phong shading
- }
-
- // Remove metallic/roughness texture if present (common GLTF issue)
- if (typeof property.setRMTexture === 'function') {
- property.setRMTexture(null);
- }
- if (typeof property.setMetallicRoughnessTexture === 'function') {
- property.setMetallicRoughnessTexture(null);
- }
- }
-
- if (mapper) {
- mapper.setScalarVisibility(false);
- mapper.modified();
- }
- }
-
- /**
- * Enable/disable scalar (field) visualization on mapper
- * @param {Object} mapper - VTK mapper instance
- * @param {boolean} enabled - Whether to show scalar colors
- */
- setScalarVisibility(mapper, enabled) {
- if (mapper) {
- mapper.setScalarVisibility(enabled);
- mapper.modified();
- }
- }
-}
-
-export default ActorManager;
diff --git a/javascript/js/viewer/core/BaseViewer.js b/javascript/js/viewer/core/BaseViewer.js
deleted file mode 100644
index 09c8384a..00000000
--- a/javascript/js/viewer/core/BaseViewer.js
+++ /dev/null
@@ -1,657 +0,0 @@
-/**
- * BaseViewer - Foundation class for VTK viewers
- *
- * Provides core lifecycle management, VTK setup, and feature composition.
- * Subclasses configure features and behavior through options.
- */
-
-import { CameraController } from './CameraController.js';
-import { ActorManager, Representation } from './ActorManager.js';
-import { SettingsPanel } from '../ui/SettingsPanel.js';
-import { ControlsBar, StandardControls } from '../ui/ControlsBar.js';
-import { loadMesh } from '../loaders/LoaderFactory.js';
-import { ScreenshotCapture } from '../features/ScreenshotCapture.js';
-import { AxisIndicator } from '../features/AxisIndicator.js';
-import { FieldVisualization } from '../features/FieldVisualization.js';
-import { TextureManager } from '../features/TextureManager.js';
-import { sendMeshLoaded, sendError, createMessageListener, extractFilename } from '../utils/MessageHandler.js';
-import { getCenter, getMaxDimension, isValidBounds } from '../utils/BoundsUtils.js';
-
-/**
- * Default viewer configuration
- */
-const DEFAULT_CONFIG = {
- // Appearance
- backgroundColor: [0.15, 0.15, 0.15],
- defaultMeshColor: [0.6, 0.8, 1.0],
- edgeColor: [0.2, 0.2, 0.2],
- edgeWidth: 1.0,
- showEdges: false,
-
- // Camera
- cameraDistanceMultiplier: 2.5,
- parallelProjection: false,
-
- // Lighting
- twoSidedLighting: false,
-
- // Features (enabled/disabled)
- enableFields: true,
- enableTextures: false,
- enableScreenshot: true,
- enableAxisIndicator: false,
- enableSettings: true,
- enableControls: true,
-
- // UI
- showLoadingOverlay: true,
- showInfoOverlay: true
-};
-
-export class BaseViewer {
- /**
- * Create a BaseViewer
- * @param {HTMLElement} container - Container element for the viewer
- * @param {Object} config - Configuration options
- */
- constructor(container, config = {}) {
- this.container = container;
- this.config = { ...DEFAULT_CONFIG, ...config };
-
- // VTK objects (initialized in initialize())
- this.vtk = null;
- this.renderWindow = null;
- this.renderer = null;
- this.interactor = null;
- this.openGLRenderWindow = null;
-
- // Feature modules
- this.cameraController = null;
- this.actorManager = null;
- this.settingsPanel = null;
- this.controlsBar = null;
- this.screenshotCapture = null;
- this.axisIndicator = null;
- this.fieldVisualization = null;
- this.textureManager = null;
-
- // State
- this.actors = [];
- this.polyDatas = [];
- this.currentFilename = null;
- this.isInitialized = false;
-
- // UI elements
- this.loadingOverlay = null;
- this.infoOverlay = null;
-
- // Message listener cleanup
- this._messageCleanup = null;
- }
-
- /**
- * Initialize the viewer
- * Must be called before loading meshes
- */
- async initialize() {
- if (this.isInitialized) {
- console.warn('[BaseViewer] Already initialized');
- return;
- }
-
- // Get VTK from global
- this.vtk = window.vtk;
- if (!this.vtk) {
- throw new Error('VTK.js not loaded. Include vtk-gltf.js before initializing viewer.');
- }
-
- // Create VTK rendering pipeline
- this._createRenderingPipeline();
-
- // Initialize feature modules
- this._initializeFeatures();
-
- // Create UI elements
- this._createUI();
-
- // Setup resize handling
- this._setupResizeHandler();
-
- // Setup message listener for loading meshes
- this._setupMessageListener();
-
- this.isInitialized = true;
- console.log('[BaseViewer] Initialized');
- }
-
- /**
- * Create VTK rendering pipeline
- * @private
- */
- _createRenderingPipeline() {
- // Create render window
- this.renderWindow = this.vtk.Rendering.Core.vtkRenderWindow.newInstance();
-
- // Create renderer
- this.renderer = this.vtk.Rendering.Core.vtkRenderer.newInstance();
- this.renderer.setBackground(...this.config.backgroundColor);
- this.renderWindow.addRenderer(this.renderer);
-
- // Two-sided lighting
- if (this.config.twoSidedLighting) {
- this.renderer.setTwoSidedLighting(true);
- }
-
- // Create OpenGL render window
- this.openGLRenderWindow = this.vtk.Rendering.OpenGL.vtkRenderWindow.newInstance();
- this.openGLRenderWindow.setContainer(this.container);
- this.renderWindow.addView(this.openGLRenderWindow);
-
- // Create interactor
- this.interactor = this.vtk.Rendering.Core.vtkRenderWindowInteractor.newInstance();
- this.interactor.setView(this.openGLRenderWindow);
- this.interactor.initialize();
- this.interactor.bindEvents(this.container);
-
- // Set interactor style
- const interactorStyle = this.vtk.Interaction.Style.vtkInteractorStyleTrackballCamera.newInstance();
- this.interactor.setInteractorStyle(interactorStyle);
-
- // Size to container
- const { width, height } = this.container.getBoundingClientRect();
- this.openGLRenderWindow.setSize(width, height);
- }
-
- /**
- * Initialize feature modules
- * @private
- */
- _initializeFeatures() {
- // Camera controller
- this.cameraController = new CameraController(this.renderer, {
- distanceMultiplier: this.config.cameraDistanceMultiplier,
- parallelProjection: this.config.parallelProjection
- });
-
- // Actor manager
- this.actorManager = new ActorManager(this.vtk, {
- defaultColor: this.config.defaultMeshColor,
- edgeColor: this.config.edgeColor,
- edgeWidth: this.config.edgeWidth,
- showEdges: this.config.showEdges
- });
-
- // Screenshot capture
- if (this.config.enableScreenshot) {
- this.screenshotCapture = new ScreenshotCapture(this.renderWindow);
- }
-
- // Axis indicator
- if (this.config.enableAxisIndicator) {
- this.axisIndicator = new AxisIndicator(this.vtk, this.interactor, {
- enabled: true
- });
- }
-
- // Field visualization
- if (this.config.enableFields) {
- this.fieldVisualization = new FieldVisualization(this.vtk, this.renderWindow);
- }
-
- // Texture manager
- if (this.config.enableTextures) {
- this.textureManager = new TextureManager(this.vtk, this.renderWindow);
- }
- }
-
- /**
- * Create UI elements
- * @private
- */
- _createUI() {
- // Loading overlay
- if (this.config.showLoadingOverlay) {
- this.loadingOverlay = this._createLoadingOverlay();
- }
-
- // Info overlay
- if (this.config.showInfoOverlay) {
- this.infoOverlay = this._createInfoOverlay();
- }
-
- // Settings panel
- if (this.config.enableSettings && this.config.settingsConfig) {
- this.settingsPanel = new SettingsPanel(this.config.settingsConfig);
- this.settingsPanel.onApply((settings) => this._onSettingsApply(settings));
- }
-
- // Controls bar
- if (this.config.enableControls && this.config.controlsConfig) {
- this.controlsBar = new ControlsBar(this.config.controlsConfig);
- this._setupControlsHandlers();
- }
- }
-
- /**
- * Create loading overlay element
- * @private
- * @returns {HTMLElement}
- */
- _createLoadingOverlay() {
- const overlay = document.createElement('div');
- overlay.id = 'loading';
- overlay.className = 'loading';
- overlay.innerHTML = `
-
- Loading mesh...
-
- `;
- this.container.parentElement.appendChild(overlay);
- return overlay;
- }
-
- /**
- * Update the download progress bar.
- * @param {number|null} fraction - 0..1, or null for indeterminate (hides the bar)
- */
- setProgress(fraction) {
- if (!this.loadingOverlay) return;
- const wrap = this.loadingOverlay.querySelector('.loading-progress');
- const bar = this.loadingOverlay.querySelector('.loading-progress-bar');
- const textEl = this.loadingOverlay.querySelector('.loading-text');
- if (!wrap || !bar) return;
- if (fraction == null || isNaN(fraction)) {
- wrap.style.display = 'none'; // indeterminate -> spinner only
- return;
- }
- const pct = Math.max(0, Math.min(100, Math.round(fraction * 100)));
- wrap.style.display = 'block';
- bar.style.width = pct + '%';
- if (textEl) textEl.textContent = `Downloading mesh… ${pct}%`;
- }
-
- /**
- * Create info overlay element
- * @private
- * @returns {HTMLElement}
- */
- _createInfoOverlay() {
- const overlay = document.createElement('div');
- overlay.id = 'meshInfo';
- overlay.className = 'mesh-info';
- this.container.parentElement.appendChild(overlay);
- return overlay;
- }
-
- /**
- * Setup resize handler
- * @private
- */
- _setupResizeHandler() {
- const resizeObserver = new ResizeObserver(() => {
- const { width, height } = this.container.getBoundingClientRect();
- if (width > 0 && height > 0) {
- this.openGLRenderWindow.setSize(width, height);
- this.renderWindow.render();
- }
- });
- resizeObserver.observe(this.container);
- this._resizeObserver = resizeObserver;
- }
-
- /**
- * Setup message listener for postMessage API
- * @private
- */
- _setupMessageListener() {
- this._messageCleanup = createMessageListener((data) => {
- if (data.type === 'loadMesh' && data.url) {
- this.loadMesh(data.url);
- } else if (data.type === 'screenshot') {
- this.captureScreenshot();
- }
- });
- }
-
- /**
- * Setup controls bar handlers
- * @private
- */
- _setupControlsHandlers() {
- if (!this.controlsBar) return;
-
- // Camera view buttons
- this.controlsBar.onAction('camera-view', (direction) => {
- this.setCameraView(direction);
- });
-
- // Screenshot button
- this.controlsBar.onAction('screenshot', () => {
- this.captureScreenshot();
- });
-
- // Settings button
- this.controlsBar.onAction('settings', () => {
- if (this.settingsPanel) {
- this.settingsPanel.open();
- }
- });
-
- // Edges toggle
- this.controlsBar.onAction('edges-toggle', (show) => {
- this.setEdgeVisibility(show);
- });
- }
-
- /**
- * Handle settings panel apply
- * @private
- * @param {Object} settings - Applied settings
- */
- _onSettingsApply(settings) {
- // Apply to actor manager
- if (this.actorManager) {
- this.actorManager.applySettings(this.actors, settings);
- }
-
- // Apply background color
- if (settings.backgroundColor) {
- this.renderer.setBackground(...settings.backgroundColor);
- }
-
- this.render();
- }
-
- /**
- * Load a mesh from URL
- * @param {string} url - Mesh file URL
- * @returns {Promise