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 = ` -
- - -
- -
- -
- 0 / 0 - 0.00s -
-
- -
- - -
- -
- - - - -
- - - `; - - 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} Load result with polyData, actors, fields - */ - async loadMesh(url) { - this.showLoading(true, 'Downloading mesh…'); - this.setProgress(0); - - try { - // Clear existing actors - this.clearScene(); - - // Detect format and load (with download progress) - const result = await loadMesh(url, this.vtk, { - renderer: this.renderer, - onProgress: (frac) => this.setProgress(frac) - }); - - // Store state - this.actors = result.actors; - this.polyDatas = result.polyData ? [result.polyData] : []; - this.currentFilename = extractFilename(url); - - // Add actors to renderer - result.actors.forEach(actor => { - this.renderer.addActor(actor); - }); - - // Configure actors - this._configureLoadedActors(result); - - // Setup camera - this._setupCameraForMesh(); - - // Update UI - this._updateInfoOverlay(result); - this.showLoading(false); - - // Notify parent - sendMeshLoaded({ - filename: this.currentFilename, - format: result.format, - actors: result.actors.length, - hasTexture: result.hasTexture || false, - hasVertexColors: result.hasVertexColors || false, - fields: result.fields || [] - }); - - this.render(); - return result; - - } catch (error) { - console.error('[BaseViewer] Error loading mesh:', error); - this.showLoading(false); - sendError(error.message); - throw error; - } - } - - /** - * Configure actors after loading - * @private - * @param {Object} result - Load result - */ - _configureLoadedActors(result) { - // Apply texture fixes if enabled - if (this.config.enableTextures && this.textureManager && result.hasTexture) { - this.textureManager.applyTextureFixesMultiple(this.actors); - this.textureManager.configureRenderer(this.renderer); - } else if (result.hasVertexColors) { - // Mesh has vertex colors - don't override with default color - // Just configure edges if needed, colors are already set up by loader - this.actors.forEach(actor => { - const property = actor.getProperty(); - if (property) { - if (this.config.showEdges) { - property.setEdgeVisibility(true); - if (this.config.edgeColor) { - property.setEdgeColor(...this.config.edgeColor); - } - } - } - }); - } else { - // Apply standard actor configuration - this.actorManager.configureActors(this.actors, { - color: this.config.defaultMeshColor, - showEdges: this.config.showEdges, - edgeColor: this.config.edgeColor - }); - } - } - - /** - * Setup camera position for loaded mesh - * @private - */ - _setupCameraForMesh() { - if (this.actors.length === 0) return; - - // Get combined bounds - let bounds = this.actors[0].getBounds(); - for (let i = 1; i < this.actors.length; i++) { - const actorBounds = this.actors[i].getBounds(); - bounds = [ - Math.min(bounds[0], actorBounds[0]), - Math.max(bounds[1], actorBounds[1]), - Math.min(bounds[2], actorBounds[2]), - Math.max(bounds[3], actorBounds[3]), - Math.min(bounds[4], actorBounds[4]), - Math.max(bounds[5], actorBounds[5]) - ]; - } - - if (isValidBounds(bounds)) { - this.cameraController.positionInitialCamera(bounds); - } - } - - /** - * Update info overlay with mesh information - * @private - * @param {Object} result - Load result - */ - _updateInfoOverlay(result) { - if (!this.infoOverlay) return; - - const lines = [`File: ${this.currentFilename}`]; - - if (result.polyData) { - const numPoints = result.polyData.getNumberOfPoints(); - const numCells = result.polyData.getNumberOfCells(); - lines.push(`Points: ${numPoints.toLocaleString()}`); - lines.push(`Cells: ${numCells.toLocaleString()}`); - } - - if (result.hasTexture) { - lines.push('Textured: Yes'); - } - - this.infoOverlay.innerHTML = lines.join('
'); - } - - /** - * Clear the scene - */ - clearScene() { - this.actors.forEach(actor => { - this.renderer.removeActor(actor); - }); - this.actors = []; - this.polyDatas = []; - } - - /** - * Set camera view direction - * @param {string} direction - '+X', '-X', '+Y', '-Y', '+Z', '-Z' - */ - setCameraView(direction) { - if (this.actors.length === 0) return; - - const bounds = this.actors[0].getBounds(); - this.cameraController.setCameraView(direction, bounds); - this.render(); - } - - /** - * Set edge visibility - * @param {boolean} visible - */ - setEdgeVisibility(visible) { - this.actors.forEach(actor => { - this.actorManager.setEdgeVisibility(actor, visible); - }); - this.render(); - } - - /** - * Capture screenshot - * @returns {Promise} Base64 image data - */ - async captureScreenshot() { - if (this.screenshotCapture) { - return this.screenshotCapture.captureAndSend(); - } - } - - /** - * Show/hide loading overlay - * @param {boolean} show - * @param {string} text - Optional loading text - */ - showLoading(show, text = 'Loading mesh...') { - if (this.loadingOverlay) { - this.loadingOverlay.style.display = show ? 'flex' : 'none'; - const textEl = this.loadingOverlay.querySelector('.loading-text'); - if (textEl) { - textEl.textContent = text; - } - if (!show) { - // reset the progress bar for the next load - const wrap = this.loadingOverlay.querySelector('.loading-progress'); - const bar = this.loadingOverlay.querySelector('.loading-progress-bar'); - if (wrap) wrap.style.display = 'none'; - if (bar) bar.style.width = '0%'; - } - } - } - - /** - * Render the scene - */ - render() { - this.renderWindow.render(); - } - - /** - * Resize the viewer - * @param {number} width - * @param {number} height - */ - resize(width, height) { - this.openGLRenderWindow.setSize(width, height); - this.render(); - } - - /** - * Get current actors - * @returns {Object[]} - */ - getActors() { - return this.actors; - } - - /** - * Get renderer - * @returns {Object} - */ - getRenderer() { - return this.renderer; - } - - /** - * Cleanup and destroy viewer - */ - destroy() { - // Cleanup message listener - if (this._messageCleanup) { - this._messageCleanup(); - } - - // Cleanup resize observer - if (this._resizeObserver) { - this._resizeObserver.disconnect(); - } - - // Cleanup axis indicator - if (this.axisIndicator) { - this.axisIndicator.destroy(); - } - - // Clear scene - this.clearScene(); - - // Delete VTK objects - if (this.interactor) { - this.interactor.unbindEvents(); - } - if (this.openGLRenderWindow) { - this.openGLRenderWindow.delete(); - } - if (this.renderWindow) { - this.renderWindow.delete(); - } - - // Remove UI elements - if (this.loadingOverlay && this.loadingOverlay.parentElement) { - this.loadingOverlay.parentElement.removeChild(this.loadingOverlay); - } - if (this.infoOverlay && this.infoOverlay.parentElement) { - this.infoOverlay.parentElement.removeChild(this.infoOverlay); - } - - console.log('[BaseViewer] Destroyed'); - } -} - -export default BaseViewer; diff --git a/javascript/js/viewer/core/CameraController.js b/javascript/js/viewer/core/CameraController.js deleted file mode 100644 index 7858f421..00000000 --- a/javascript/js/viewer/core/CameraController.js +++ /dev/null @@ -1,271 +0,0 @@ -/** - * CameraController - Unified camera management for VTK viewers - * - * Handles camera positioning, view presets, and projection modes. - * Configurable distance multiplier allows different viewers to use different defaults. - */ - -import { getCenter, getMaxDimension } from '../utils/BoundsUtils.js'; - -/** - * View direction configurations - * Maps direction strings to camera position offsets and viewUp vectors - */ -const VIEW_CONFIGS = { - '+X': { offset: [1, 0, 0], viewUp: [0, 0, 1] }, - '-X': { offset: [-1, 0, 0], viewUp: [0, 0, 1] }, - '+Y': { offset: [0, 1, 0], viewUp: [0, 0, 1] }, - '-Y': { offset: [0, -1, 0], viewUp: [0, 0, 1] }, - '+Z': { offset: [0, 0, 1], viewUp: [0, 1, 0] }, - '-Z': { offset: [0, 0, -1], viewUp: [0, 1, 0] } -}; - -/** - * Default camera configuration - */ -const DEFAULT_CONFIG = { - distanceMultiplier: 2.5, // Standard viewers use 2.5, textured uses 2.0 - initialViewOffset: [1, 1, 1], // Diagonal view for initial positioning - initialViewUp: [0, 0, 1] -}; - -export class CameraController { - /** - * Create a CameraController - * @param {Object} renderer - VTK renderer instance - * @param {Object} renderWindow - VTK render window instance - * @param {Object} config - Configuration options - * @param {number} config.distanceMultiplier - Camera distance = maxDim * multiplier (default: 2.5) - */ - constructor(renderer, renderWindow, config = {}) { - this.renderer = renderer; - this.renderWindow = renderWindow; - this.config = { ...DEFAULT_CONFIG, ...config }; - this.currentBounds = null; - } - - /** - * Get the active camera from the renderer - * @returns {Object} VTK camera instance - */ - getCamera() { - return this.renderer.getActiveCamera(); - } - - /** - * Store bounds for later use (e.g., view presets) - * @param {number[]} bounds - VTK bounds [xmin, xmax, ymin, ymax, zmin, zmax] - */ - setBounds(bounds) { - this.currentBounds = bounds; - } - - /** - * Position camera for initial viewing of mesh - * @param {number[]} bounds - VTK bounds [xmin, xmax, ymin, ymax, zmin, zmax] - */ - positionInitialCamera(bounds) { - this.currentBounds = bounds; - - const camera = this.getCamera(); - const center = getCenter(bounds); - const maxDim = getMaxDimension(bounds); - const distance = maxDim * this.config.distanceMultiplier; - - const offset = this.config.initialViewOffset; - const position = [ - center[0] + offset[0] * distance, - center[1] + offset[1] * distance, - center[2] + offset[2] * distance - ]; - - camera.setPosition(...position); - camera.setFocalPoint(...center); - camera.setViewUp(...this.config.initialViewUp); - - this.renderer.resetCamera(); - this.renderWindow.render(); - } - - /** - * Set camera to a specific axis-aligned view - * @param {string} direction - One of '+X', '-X', '+Y', '-Y', '+Z', '-Z' - * @param {number[]} bounds - Optional bounds, uses stored bounds if not provided - */ - setCameraView(direction, bounds = null) { - const activeBounds = bounds || this.currentBounds; - if (!activeBounds) { - console.warn('[CameraController] No bounds available for setCameraView'); - return; - } - - const viewConfig = VIEW_CONFIGS[direction]; - if (!viewConfig) { - console.warn(`[CameraController] Unknown view direction: ${direction}`); - return; - } - - const camera = this.getCamera(); - const center = getCenter(activeBounds); - const maxDim = getMaxDimension(activeBounds); - const distance = maxDim * this.config.distanceMultiplier; - - const position = [ - center[0] + viewConfig.offset[0] * distance, - center[1] + viewConfig.offset[1] * distance, - center[2] + viewConfig.offset[2] * distance - ]; - - camera.setPosition(...position); - camera.setFocalPoint(...center); - camera.setViewUp(...viewConfig.viewUp); - - this.renderer.resetCamera(); - this.renderWindow.render(); - } - - /** - * Reset camera to fit the current bounds - */ - resetCamera() { - if (this.currentBounds) { - this.renderer.resetCamera(this.currentBounds); - } else { - this.renderer.resetCamera(); - } - this.renderWindow.render(); - } - - /** - * Reset camera clipping range (useful after mesh updates) - */ - resetClippingRange() { - this.renderer.resetCameraClippingRange(); - this.renderWindow.render(); - } - - /** - * Set parallel (orthographic) vs perspective projection - * @param {boolean} enabled - True for parallel, false for perspective - */ - setParallelProjection(enabled) { - const camera = this.getCamera(); - camera.setParallelProjection(enabled); - this.renderWindow.render(); - } - - /** - * Get current parallel projection state - * @returns {boolean} True if parallel projection is enabled - */ - getParallelProjection() { - return this.getCamera().getParallelProjection(); - } - - /** - * Synchronize this camera with another camera - * Useful for side-by-side viewers - * @param {Object} sourceCamera - Camera to copy state from - */ - synchronizeFrom(sourceCamera) { - const camera = this.getCamera(); - camera.setPosition(...sourceCamera.getPosition()); - camera.setFocalPoint(...sourceCamera.getFocalPoint()); - camera.setViewUp(...sourceCamera.getViewUp()); - camera.setParallelProjection(sourceCamera.getParallelProjection()); - camera.setClippingRange(...sourceCamera.getClippingRange()); - camera.setParallelScale(sourceCamera.getParallelScale()); - camera.setViewAngle(sourceCamera.getViewAngle()); - } - - /** - * Get camera state for serialization - * @returns {Object} Camera state object - */ - getState() { - const camera = this.getCamera(); - return { - position: camera.getPosition(), - focalPoint: camera.getFocalPoint(), - viewUp: camera.getViewUp(), - parallelProjection: camera.getParallelProjection(), - clippingRange: camera.getClippingRange(), - parallelScale: camera.getParallelScale(), - viewAngle: camera.getViewAngle() - }; - } - - /** - * Restore camera state from serialized object - * @param {Object} state - Camera state object from getState() - */ - setState(state) { - const camera = this.getCamera(); - if (state.position) camera.setPosition(...state.position); - if (state.focalPoint) camera.setFocalPoint(...state.focalPoint); - if (state.viewUp) camera.setViewUp(...state.viewUp); - if (state.parallelProjection !== undefined) camera.setParallelProjection(state.parallelProjection); - if (state.clippingRange) camera.setClippingRange(...state.clippingRange); - if (state.parallelScale !== undefined) camera.setParallelScale(state.parallelScale); - if (state.viewAngle !== undefined) camera.setViewAngle(state.viewAngle); - this.renderWindow.render(); - } - - /** - * Focus camera on a specific 3D point - * Moves the camera to look at the point while maintaining relative view direction - * @param {number[]} point - [x, y, z] coordinates to focus on - * @param {number} viewRadius - Optional radius around point to view (defaults to 1/10 of mesh size) - */ - focusOnPoint(point, viewRadius = null) { - const camera = this.getCamera(); - - // Get current camera direction (from focal point to camera position) - const currentFocalPoint = camera.getFocalPoint(); - const currentPosition = camera.getPosition(); - const direction = [ - currentPosition[0] - currentFocalPoint[0], - currentPosition[1] - currentFocalPoint[1], - currentPosition[2] - currentFocalPoint[2] - ]; - - // Normalize direction - const length = Math.sqrt(direction[0]**2 + direction[1]**2 + direction[2]**2); - if (length > 0) { - direction[0] /= length; - direction[1] /= length; - direction[2] /= length; - } else { - // Default to looking from +Z if no current direction - direction[0] = 0; - direction[1] = 0; - direction[2] = 1; - } - - // Calculate view distance - let distance; - if (viewRadius !== null) { - distance = viewRadius * this.config.distanceMultiplier; - } else if (this.currentBounds) { - const maxDim = getMaxDimension(this.currentBounds); - distance = maxDim * 0.3; // Closer view for focused point - } else { - distance = 10; // Fallback - } - - // Position camera along the direction from the point - const newPosition = [ - point[0] + direction[0] * distance, - point[1] + direction[1] * distance, - point[2] + direction[2] * distance - ]; - - camera.setFocalPoint(...point); - camera.setPosition(...newPosition); - - this.renderer.resetCameraClippingRange(); - this.renderWindow.render(); - } -} - -export default CameraController; diff --git a/javascript/js/viewer/features/AxisIndicator.js b/javascript/js/viewer/features/AxisIndicator.js deleted file mode 100644 index 095f23fc..00000000 --- a/javascript/js/viewer/features/AxisIndicator.js +++ /dev/null @@ -1,242 +0,0 @@ -/** - * AxisIndicator - Orientation widget for VTK viewers - * - * Creates and manages an annotated cube axis indicator that shows - * the current camera orientation. - */ - -import { rgbToString } from '../utils/ColorUtils.js'; - -/** - * Default axis colors - */ -const DEFAULT_COLORS = { - x: '#ff5555', // Red for X axis - y: '#55ff55', // Green for Y axis - z: '#5555ff' // Blue for Z axis -}; - -/** - * Default widget configuration - */ -const DEFAULT_CONFIG = { - enabled: false, - corner: 'BOTTOM_LEFT', // BOTTOM_LEFT, BOTTOM_RIGHT, TOP_LEFT, TOP_RIGHT - viewportSize: 0.15, - minPixelSize: 100, - maxPixelSize: 300, - faceColor: rgbToString(0.5, 0.5, 0.5) -}; - -export class AxisIndicator { - /** - * Create an AxisIndicator - * @param {Object} vtk - VTK.js global object - * @param {Object} interactor - VTK interactor (from renderWindow.getInteractor()) - * @param {Object} config - Configuration options - */ - constructor(vtk, interactor, config = {}) { - this.vtk = vtk; - this.interactor = interactor; - this.config = { ...DEFAULT_CONFIG, ...config }; - - // VTK classes - this.vtkOrientationMarkerWidget = vtk.Interaction.Widgets.vtkOrientationMarkerWidget; - this.vtkAnnotatedCubeActor = vtk.Rendering.Core.vtkAnnotatedCubeActor; - - // Widget state - this.widget = null; - this.cubeActor = null; - this.enabled = this.config.enabled; - - this._createWidget(); - } - - /** - * Create the orientation widget - * @private - */ - _createWidget() { - // Create annotated cube actor - this.cubeActor = this.vtkAnnotatedCubeActor.newInstance(); - - // Set default style - this.cubeActor.setDefaultStyle({ - text: '+X', - fontStyle: 'bold', - fontFamily: 'Arial', - fontColor: 'white', - fontSizeScale: (res) => res / 2, - faceColor: this.config.faceColor, - faceRotation: 0, - edgeThickness: 0.1, - edgeColor: 'white', - resolution: 400, - }); - - // Customize each face with axis colors - this.cubeActor.setXPlusFaceProperty({ text: '+X', faceColor: DEFAULT_COLORS.x }); - this.cubeActor.setXMinusFaceProperty({ text: '-X', faceColor: DEFAULT_COLORS.x }); - this.cubeActor.setYPlusFaceProperty({ text: '+Y', faceColor: DEFAULT_COLORS.y }); - this.cubeActor.setYMinusFaceProperty({ text: '-Y', faceColor: DEFAULT_COLORS.y }); - this.cubeActor.setZPlusFaceProperty({ text: '+Z', faceColor: DEFAULT_COLORS.z }); - this.cubeActor.setZMinusFaceProperty({ text: '-Z', faceColor: DEFAULT_COLORS.z }); - - // Create orientation marker widget - this.widget = this.vtkOrientationMarkerWidget.newInstance({ - actor: this.cubeActor, - interactor: this.interactor, - }); - - // Configure widget position and size - this._applyCornerConfig(); - this.widget.setViewportSize(this.config.viewportSize); - this.widget.setMinPixelSize(this.config.minPixelSize); - this.widget.setMaxPixelSize(this.config.maxPixelSize); - - // Set initial enabled state - this.widget.setEnabled(this.enabled); - } - - /** - * Apply corner configuration - * @private - */ - _applyCornerConfig() { - const Corners = this.vtkOrientationMarkerWidget.Corners; - const cornerMap = { - 'BOTTOM_LEFT': Corners.BOTTOM_LEFT, - 'BOTTOM_RIGHT': Corners.BOTTOM_RIGHT, - 'TOP_LEFT': Corners.TOP_LEFT, - 'TOP_RIGHT': Corners.TOP_RIGHT - }; - - const corner = cornerMap[this.config.corner] || Corners.BOTTOM_LEFT; - this.widget.setViewportCorner(corner); - } - - /** - * Enable the axis indicator - */ - enable() { - this.enabled = true; - if (this.widget) { - this.widget.setEnabled(true); - } - } - - /** - * Disable the axis indicator - */ - disable() { - this.enabled = false; - if (this.widget) { - this.widget.setEnabled(false); - } - } - - /** - * Toggle the axis indicator - * @returns {boolean} New enabled state - */ - toggle() { - if (this.enabled) { - this.disable(); - } else { - this.enable(); - } - return this.enabled; - } - - /** - * Set enabled state - * @param {boolean} enabled - Whether to enable the indicator - */ - setEnabled(enabled) { - if (enabled) { - this.enable(); - } else { - this.disable(); - } - } - - /** - * Check if enabled - * @returns {boolean} - */ - isEnabled() { - return this.enabled; - } - - /** - * Set the corner position - * @param {string} corner - 'BOTTOM_LEFT', 'BOTTOM_RIGHT', 'TOP_LEFT', 'TOP_RIGHT' - */ - setCorner(corner) { - this.config.corner = corner; - this._applyCornerConfig(); - } - - /** - * Set viewport size (0-1 range) - * @param {number} size - Viewport size as fraction - */ - setViewportSize(size) { - this.config.viewportSize = size; - if (this.widget) { - this.widget.setViewportSize(size); - } - } - - /** - * Set axis colors - * @param {Object} colors - Color object with x, y, z properties (hex strings) - */ - setAxisColors(colors) { - if (colors.x) { - this.cubeActor.setXPlusFaceProperty({ faceColor: colors.x }); - this.cubeActor.setXMinusFaceProperty({ faceColor: colors.x }); - } - if (colors.y) { - this.cubeActor.setYPlusFaceProperty({ faceColor: colors.y }); - this.cubeActor.setYMinusFaceProperty({ faceColor: colors.y }); - } - if (colors.z) { - this.cubeActor.setZPlusFaceProperty({ faceColor: colors.z }); - this.cubeActor.setZMinusFaceProperty({ faceColor: colors.z }); - } - } - - /** - * Get the underlying VTK widget - * @returns {Object} VTK orientation marker widget - */ - getWidget() { - return this.widget; - } - - /** - * Get the cube actor - * @returns {Object} VTK annotated cube actor - */ - getCubeActor() { - return this.cubeActor; - } - - /** - * Cleanup resources - */ - destroy() { - if (this.widget) { - this.widget.setEnabled(false); - this.widget.delete(); - this.widget = null; - } - if (this.cubeActor) { - this.cubeActor.delete(); - this.cubeActor = null; - } - } -} - -export default AxisIndicator; diff --git a/javascript/js/viewer/features/FieldVisualization.js b/javascript/js/viewer/features/FieldVisualization.js deleted file mode 100644 index 05159123..00000000 --- a/javascript/js/viewer/features/FieldVisualization.js +++ /dev/null @@ -1,352 +0,0 @@ -/** - * FieldVisualization - Scalar field coloring for VTK viewers - * - * Handles scalar field visualization including: - * - Field detection (point/cell data) - * - Colormap application - * - Range calculation (auto/manual, single/synchronized) - */ - -/** - * Popular colormaps to show at top of selector - */ -const POPULAR_COLORMAPS = [ - 'erdc_rainbow_bright', - 'Cool to Warm', - 'Viridis (matplotlib)', - 'Plasma (matplotlib)', - 'Inferno (matplotlib)', - 'Magma (matplotlib)', - 'jet', - 'rainbow' -]; - -export class FieldVisualization { - /** - * Create a FieldVisualization instance - * @param {Object} vtk - VTK.js global object - * @param {Object} renderWindow - VTK render window for triggering renders - */ - constructor(vtk, renderWindow) { - this.vtk = vtk; - this.renderWindow = renderWindow; - - // VTK classes - this.vtkColorTransferFunction = vtk.Rendering.Core.vtkColorTransferFunction; - this.vtkColorMaps = vtk.Rendering.Core.vtkColorTransferFunction.vtkColorMaps; - - // State - this.currentField = null; - this.currentColormap = 'erdc_rainbow_bright'; - this.autoRange = true; - this.manualRange = [0, 1]; - } - - /** - * Extract field information from polydata - * @param {Object} polyData - VTK polydata object - * @returns {Object} Field information with pointFields and cellFields arrays - */ - extractFields(polyData) { - if (!polyData) { - return { pointFields: [], cellFields: [], allFields: [] }; - } - - // Fields to hide from dropdown (VTK internal arrays that aren't useful to visualize) - const HIDDEN_FIELDS = ['Attribute', 'vtkOriginalPointIds', 'vtkOriginalCellIds']; - - const pointData = polyData.getPointData(); - const cellData = polyData.getCellData(); - - const pointFields = []; - const cellFields = []; - - // Extract point (vertex) data arrays - for (let i = 0; i < pointData.getNumberOfArrays(); i++) { - const array = pointData.getArray(i); - const name = array.getName(); - // Skip hidden/internal fields - if (HIDDEN_FIELDS.includes(name)) continue; - const range = array.getRange(); - pointFields.push({ - name, - displayName: `${name} (vertex)`, - type: 'point', - fullName: `point:${name}`, - numComponents: array.getNumberOfComponents(), - range: [range[0], range[1]] - }); - } - - // Extract cell (face) data arrays - for (let i = 0; i < cellData.getNumberOfArrays(); i++) { - const array = cellData.getArray(i); - const name = array.getName(); - // Skip hidden/internal fields - if (HIDDEN_FIELDS.includes(name)) continue; - const range = array.getRange(); - cellFields.push({ - name, - displayName: `${name} (face)`, - type: 'cell', - fullName: `cell:${name}`, - numComponents: array.getNumberOfComponents(), - range: [range[0], range[1]] - }); - } - - const allFields = [...pointFields, ...cellFields]; - - return { - pointFields, - cellFields, - allFields, - hasFields: allFields.length > 0 - }; - } - - /** - * Apply a scalar field to a mapper - * @param {Object} mapper - VTK mapper - * @param {Object} polyData - VTK polydata - * @param {string} fieldName - Field name (e.g., "point:temperature" or "cell:pressure") - * @param {Object} options - Options - * @param {number[]} options.range - Manual range [min, max] (overrides auto) - * @param {string} options.colormap - Colormap name - * @param {boolean} options.render - Whether to trigger render (default: true) - */ - applyField(mapper, polyData, fieldName, options = {}) { - const { - range = null, - colormap = this.currentColormap, - render = true - } = options; - - if (!fieldName || fieldName === 'none' || !polyData) { - this.disableScalarVisualization(mapper, render); - return; - } - - // Parse field name - const isCellData = fieldName.startsWith('cell:'); - const actualFieldName = isCellData - ? fieldName.substring(5) - : fieldName.startsWith('point:') - ? fieldName.substring(6) - : fieldName; - - // Get data array - const data = isCellData ? polyData.getCellData() : polyData.getPointData(); - const scalarArray = data.getArrayByName(actualFieldName); - - if (!scalarArray) { - console.warn(`[FieldVisualization] Field "${actualFieldName}" not found`); - this.disableScalarVisualization(mapper, render); - return; - } - - // Get range - const dataRange = scalarArray.getRange(); - const [minVal, maxVal] = range || (this.autoRange ? dataRange : this.manualRange); - - // Check for degenerate range - const epsilon = 1e-10; - if (Math.abs(maxVal - minVal) < epsilon && !range) { - console.warn(`[FieldVisualization] Field "${actualFieldName}" has zero-width range`); - this.disableScalarVisualization(mapper, render); - return; - } - - // Set active scalars - data.setActiveScalars(actualFieldName); - - // Configure mapper - mapper.setScalarVisibility(true); - if (isCellData) { - mapper.setScalarModeToUseCellData(); - } else { - mapper.setScalarModeToUsePointData(); - } - mapper.setScalarRange(minVal, maxVal); - - // Apply colormap - this.applyColormap(mapper, colormap, minVal, maxVal); - - // Update state - this.currentField = fieldName; - this.currentColormap = colormap; - - // Update mapper to apply changes - mapper.update(); - - if (render) { - this.renderWindow.render(); - } - - console.log(`[FieldVisualization] Applied field "${actualFieldName}" with range [${minVal.toFixed(3)}, ${maxVal.toFixed(3)}]`); - } - - /** - * Apply a colormap to a mapper - * @param {Object} mapper - VTK mapper - * @param {string} colormapName - Colormap preset name - * @param {number} minVal - Range minimum - * @param {number} maxVal - Range maximum - */ - applyColormap(mapper, colormapName, minVal, maxVal) { - try { - const lookupTable = this.vtkColorTransferFunction.newInstance(); - const preset = this.vtkColorMaps.getPresetByName(colormapName); - - if (preset) { - lookupTable.applyColorMap(preset); - lookupTable.setMappingRange(minVal, maxVal); - lookupTable.updateRange(); - mapper.setLookupTable(lookupTable); - } else { - console.warn(`[FieldVisualization] Colormap "${colormapName}" not found`); - } - } catch (error) { - console.error('[FieldVisualization] Error applying colormap:', error); - } - } - - /** - * Disable scalar visualization on a mapper - * @param {Object} mapper - VTK mapper - * @param {boolean} render - Whether to trigger render - */ - disableScalarVisualization(mapper, render = true) { - if (mapper) { - mapper.setScalarVisibility(false); - } - this.currentField = null; - if (render) { - this.renderWindow.render(); - } - } - - /** - * Get available colormap presets - * @param {boolean} popularFirst - Put popular colormaps at top (default: true) - * @returns {Array} Array of {value, label, isPopular} objects - */ - getColormapOptions(popularFirst = true) { - const allPresets = this.vtkColorMaps.rgbPresetNames || []; - const options = []; - - if (popularFirst) { - // Add popular colormaps first - POPULAR_COLORMAPS.forEach(name => { - if (allPresets.includes(name)) { - options.push({ value: name, label: name, isPopular: true }); - } - }); - - // Add separator - options.push({ value: '', label: '----------', disabled: true }); - - // Add remaining colormaps - allPresets - .filter(name => !POPULAR_COLORMAPS.includes(name)) - .sort() - .forEach(name => { - options.push({ value: name, label: name, isPopular: false }); - }); - } else { - allPresets.sort().forEach(name => { - options.push({ value: name, label: name, isPopular: POPULAR_COLORMAPS.includes(name) }); - }); - } - - return options; - } - - /** - * Set auto range mode - * @param {boolean} auto - Whether to use auto range - */ - setAutoRange(auto) { - this.autoRange = auto; - } - - /** - * Set manual range - * @param {number} min - Range minimum - * @param {number} max - Range maximum - */ - setManualRange(min, max) { - this.manualRange = [min, max]; - } - - /** - * Calculate synchronized range for multiple polydatas - * @param {Object[]} polydatas - Array of VTK polydata objects - * @param {string} fieldName - Field name - * @returns {number[]} Combined range [min, max] - */ - calculateSynchronizedRange(polydatas, fieldName) { - let globalMin = Infinity; - let globalMax = -Infinity; - - const isCellData = fieldName.startsWith('cell:'); - const actualFieldName = isCellData - ? fieldName.substring(5) - : fieldName.startsWith('point:') - ? fieldName.substring(6) - : fieldName; - - polydatas.forEach(polyData => { - if (!polyData) return; - - const data = isCellData ? polyData.getCellData() : polyData.getPointData(); - const scalarArray = data?.getArrayByName(actualFieldName); - - if (scalarArray) { - const range = scalarArray.getRange(); - globalMin = Math.min(globalMin, range[0]); - globalMax = Math.max(globalMax, range[1]); - } - }); - - if (globalMin === Infinity || globalMax === -Infinity) { - return [0, 1]; // Default range - } - - return [globalMin, globalMax]; - } - - /** - * Find common fields between multiple polydatas - * @param {Object[]} polydatas - Array of VTK polydata objects - * @returns {Object} Object with commonPointFields, commonCellFields, commonAllFields - */ - findCommonFields(polydatas) { - if (!polydatas || polydatas.length === 0) { - return { commonPointFields: [], commonCellFields: [], commonAllFields: [] }; - } - - // Get fields from first polydata - const firstFields = this.extractFields(polydatas[0]); - let commonPointFields = firstFields.pointFields.map(f => f.name); - let commonCellFields = firstFields.cellFields.map(f => f.name); - - // Intersect with remaining polydatas - for (let i = 1; i < polydatas.length; i++) { - const fields = this.extractFields(polydatas[i]); - const pointNames = fields.pointFields.map(f => f.name); - const cellNames = fields.cellFields.map(f => f.name); - - commonPointFields = commonPointFields.filter(n => pointNames.includes(n)); - commonCellFields = commonCellFields.filter(n => cellNames.includes(n)); - } - - return { - commonPointFields, - commonCellFields, - commonAllFields: [...commonPointFields, ...commonCellFields] - }; - } -} - -export default FieldVisualization; diff --git a/javascript/js/viewer/features/ScreenshotCapture.js b/javascript/js/viewer/features/ScreenshotCapture.js deleted file mode 100644 index 27b90a35..00000000 --- a/javascript/js/viewer/features/ScreenshotCapture.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * ScreenshotCapture - Screenshot functionality for VTK viewers - * - * Handles capturing screenshots from VTK render windows and - * sending them via postMessage or triggering downloads. - */ - -import { sendScreenshot } from '../utils/MessageHandler.js'; - -export class ScreenshotCapture { - /** - * Create a ScreenshotCapture instance - * @param {Object} renderWindow - VTK render window - */ - constructor(renderWindow) { - this.renderWindow = renderWindow; - } - - /** - * Capture a screenshot as a base64 PNG data URL - * @returns {Promise} Base64 PNG data URL - */ - async capture() { - const images = await this.renderWindow.captureImages(); - return images[0]; // Returns base64 PNG data URL - } - - /** - * Capture and send screenshot to parent window via postMessage - * @returns {Promise} - */ - async captureAndSend() { - try { - const image = await this.capture(); - sendScreenshot(image); - console.log('[ScreenshotCapture] Screenshot captured and sent to parent'); - } catch (error) { - console.error('[ScreenshotCapture] Error capturing screenshot:', error); - throw error; - } - } - - /** - * Capture and download screenshot as a file - * @param {string} filename - Download filename (default: "screenshot.png") - * @returns {Promise} - */ - async captureAndDownload(filename = 'screenshot.png') { - try { - const image = await this.capture(); - - // Create download link - const link = document.createElement('a'); - link.href = image; - link.download = filename; - link.style.display = 'none'; - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - console.log('[ScreenshotCapture] Screenshot downloaded:', filename); - } catch (error) { - console.error('[ScreenshotCapture] Error downloading screenshot:', error); - throw error; - } - } - - /** - * Capture screenshot and return as Blob - * @returns {Promise} PNG Blob - */ - async captureAsBlob() { - const image = await this.capture(); - - // Convert base64 to blob - const response = await fetch(image); - return response.blob(); - } - - /** - * Generate a timestamp-based filename - * @param {string} prefix - Filename prefix (default: "screenshot") - * @param {string} extension - File extension (default: "png") - * @returns {string} Filename with timestamp - */ - static generateFilename(prefix = 'screenshot', extension = 'png') { - const now = new Date(); - const timestamp = now.toISOString() - .replace(/[:.]/g, '-') - .replace('T', '_') - .slice(0, 19); - return `${prefix}_${timestamp}.${extension}`; - } -} - -export default ScreenshotCapture; diff --git a/javascript/js/viewer/features/TextureManager.js b/javascript/js/viewer/features/TextureManager.js deleted file mode 100644 index dccd7d94..00000000 --- a/javascript/js/viewer/features/TextureManager.js +++ /dev/null @@ -1,213 +0,0 @@ -/** - * TextureManager - Texture handling for GLTF/GLB meshes - * - * Manages texture extraction, material fixes, and rendering configuration - * for properly displaying textured meshes in VTK.js. - */ - -/** - * Texture information structure - * @typedef {Object} TextureInfo - * @property {boolean} hasTexture - Whether a texture was found - * @property {boolean} hasMaterial - Whether PBR material properties exist - * @property {string} textureType - Type of texture found (baseColor, diffuse, etc.) - */ - -export class TextureManager { - /** - * Create a TextureManager - * @param {Object} vtk - VTK.js global object - * @param {Object} renderWindow - VTK render window - */ - constructor(vtk, renderWindow) { - this.vtk = vtk; - this.renderWindow = renderWindow; - } - - /** - * Apply texture fixes to an actor - * This fixes common issues with GLTF texture rendering in VTK.js - * @param {Object} actor - VTK actor - * @returns {TextureInfo} Texture information - */ - applyTextureFixes(actor) { - const property = actor.getProperty(); - const mapper = actor.getMapper(); - const info = { - hasTexture: false, - hasMaterial: false, - textureType: null - }; - - if (!property) { - return info; - } - - // CRITICAL FIX: Remove metallicRoughnessTexture if present - // VTK.js GLTFImporter may use this instead of baseColorTexture - if (typeof property.setRMTexture === 'function') { - property.setRMTexture(null); - } - if (typeof property.setMetallicRoughnessTexture === 'function') { - property.setMetallicRoughnessTexture(null); - } - - // Try multiple methods to get the base color texture - let texture = null; - - if (typeof property.getBaseColorTexture === 'function') { - texture = property.getBaseColorTexture(); - if (texture) info.textureType = 'baseColor'; - } - if (!texture && typeof property.getDiffuseTexture === 'function') { - texture = property.getDiffuseTexture(); - if (texture) info.textureType = 'diffuse'; - } - if (!texture && typeof property.getTexture === 'function') { - texture = property.getTexture(); - if (texture) info.textureType = 'generic'; - } - - if (texture) { - info.hasTexture = true; - actor.addTexture(texture); - } - - // Check for PBR material properties - if (typeof property.getMetallic === 'function' || - typeof property.getRoughness === 'function') { - info.hasMaterial = true; - } - - // Set material to non-metallic 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 - } - - // Configure mapper for texture rendering - if (mapper) { - mapper.setScalarVisibility(false); - mapper.modified(); - } - - return info; - } - - /** - * Apply texture fixes to multiple actors - * @param {Object[]} actors - Array of VTK actors - * @returns {TextureInfo} Combined texture information - */ - applyTextureFixesMultiple(actors) { - const combinedInfo = { - hasTexture: false, - hasMaterial: false, - texturedActors: 0 - }; - - actors.forEach(actor => { - const info = this.applyTextureFixes(actor); - if (info.hasTexture) { - combinedInfo.hasTexture = true; - combinedInfo.texturedActors++; - } - if (info.hasMaterial) { - combinedInfo.hasMaterial = true; - } - }); - - return combinedInfo; - } - - /** - * Configure renderer for texture display - * @param {Object} renderer - VTK renderer - */ - configureRenderer(renderer) { - // Enable two-sided lighting for better texture visibility - if (typeof renderer.setTwoSidedLighting === 'function') { - renderer.setTwoSidedLighting(true); - } - - // Disable shadows (can interfere with texture display) - if (typeof renderer.setUseShadows === 'function') { - renderer.setUseShadows(false); - } - } - - /** - * Set actor appearance for non-textured display (solid color) - * @param {Object} actor - VTK actor - * @param {number[]} color - RGB color array [0-1 range] - */ - setActorColor(actor, color = [1.0, 1.0, 1.0]) { - const property = actor.getProperty(); - const mapper = actor.getMapper(); - - if (property) { - property.setColor(...color); - } - - if (mapper) { - mapper.setScalarVisibility(false); - } - } - - /** - * Extract texture information from actor without modifying it - * @param {Object} actor - VTK actor - * @returns {TextureInfo} Texture information - */ - getTextureInfo(actor) { - const property = actor.getProperty(); - const info = { - hasTexture: false, - hasMaterial: false, - textureType: null - }; - - if (!property) { - return info; - } - - // Check for textures - if (typeof property.getBaseColorTexture === 'function' && property.getBaseColorTexture()) { - info.hasTexture = true; - info.textureType = 'baseColor'; - } else if (typeof property.getDiffuseTexture === 'function' && property.getDiffuseTexture()) { - info.hasTexture = true; - info.textureType = 'diffuse'; - } else if (typeof property.getTexture === 'function' && property.getTexture()) { - info.hasTexture = true; - info.textureType = 'generic'; - } - - // Check for material - if (typeof property.getMetallic === 'function' || - typeof property.getRoughness === 'function') { - info.hasMaterial = true; - } - - return info; - } - - /** - * Check if GLTF texture support is available - * @returns {boolean} - */ - isTextureSupported() { - try { - return this.vtk.IO.Geometry.vtkGLTFImporter !== undefined; - } catch { - return false; - } - } -} - -export default TextureManager; diff --git a/javascript/js/viewer/index.js b/javascript/js/viewer/index.js deleted file mode 100644 index aa68bad1..00000000 --- a/javascript/js/viewer/index.js +++ /dev/null @@ -1,199 +0,0 @@ -/** - * GeomPack VTK Viewer - Main Entry Point - * - * This module exports the ViewerFactory and all viewer components - * for creating unified VTK viewers. - */ - -// Utils -import ColorUtils from './utils/ColorUtils.js'; -import BoundsUtils from './utils/BoundsUtils.js'; -import MessageHandler from './utils/MessageHandler.js'; - -// Core -import CameraController from './core/CameraController.js'; -import ActorManager, { Representation } from './core/ActorManager.js'; -import BaseViewer from './core/BaseViewer.js'; - -// UI -import SettingsPanel, { SettingTypes } from './ui/SettingsPanel.js'; -import ControlsBar, { ControlTypes, StandardControls } from './ui/ControlsBar.js'; - -// Loaders -import LoaderFactory, { FormatTypes } from './loaders/LoaderFactory.js'; -import STLLoader from './loaders/STLLoader.js'; -import OBJLoader from './loaders/OBJLoader.js'; -import VTPLoader from './loaders/VTPLoader.js'; -import GLTFLoader from './loaders/GLTFLoader.js'; - -// Features -import FieldVisualization from './features/FieldVisualization.js'; -import TextureManager from './features/TextureManager.js'; -import ScreenshotCapture from './features/ScreenshotCapture.js'; -import AxisIndicator from './features/AxisIndicator.js'; - -// Viewers -import StandardViewer, { createStandardViewer } from './viewers/StandardViewer.js'; -import TexturedViewer, { createTexturedViewer } from './viewers/TexturedViewer.js'; -import DualViewer, { createDualViewer, DualLayoutMode } from './viewers/DualViewer.js'; - -// Re-export utilities -export { ColorUtils, BoundsUtils, MessageHandler }; - -// Re-export core -export { CameraController, ActorManager, Representation, BaseViewer }; - -// Re-export UI -export { SettingsPanel, SettingTypes, ControlsBar, ControlTypes, StandardControls }; - -// Re-export loaders -export { LoaderFactory, FormatTypes, STLLoader, OBJLoader, VTPLoader, GLTFLoader }; - -// Re-export features -export { FieldVisualization, TextureManager, ScreenshotCapture, AxisIndicator }; - -// Re-export viewers -export { StandardViewer, TexturedViewer, DualViewer, DualLayoutMode }; -export { createStandardViewer, createTexturedViewer, createDualViewer }; - -// Export individual utility functions for convenience -export { rgbToString, hexToRgb, rgbToHex } from './utils/ColorUtils.js'; -export { getCenter, getMaxDimension, getDimensions, combineBounds, isValidBounds } from './utils/BoundsUtils.js'; -export { - MessageTypes, - sendToParent, - sendError, - sendScreenshot, - sendMeshLoaded, - createMessageListener, - extractFilename -} from './utils/MessageHandler.js'; - -// Export loader factory functions -export { - loadMesh, - loadMeshes, - detectFormat, - requiresRenderer, - supportsTextures, - supportsFields, - getLoaderConfig -} from './loaders/LoaderFactory.js'; - -/** - * Viewer types for ViewerFactory - */ -export const ViewerTypes = { - STANDARD: 'standard', - TEXTURED: 'textured', - DUAL: 'dual' -}; - -/** - * ViewerFactory - Creates viewer instances - * - * Usage: - * const viewer = ViewerFactory.create('standard', containerEl); - * await viewer.initialize(); - * await viewer.loadMesh('/path/to/mesh.stl'); - */ -export const ViewerFactory = { - /** - * Create a viewer instance - * @param {string} type - Viewer type ('standard', 'textured', 'dual') - * @param {HTMLElement|HTMLElement[]} container - Container element(s) - * @param {Object} options - Viewer options - * @returns {BaseViewer|DualViewer} Viewer instance - */ - create(type, container, options = {}) { - console.log(`[GeomPack Viewer] Creating ${type} viewer`); - - switch (type) { - case ViewerTypes.STANDARD: - case 'standard': - return new StandardViewer(container, options); - - case ViewerTypes.TEXTURED: - case 'textured': - return new TexturedViewer(container, options); - - case ViewerTypes.DUAL: - case 'dual': - // Dual viewer expects two containers - if (Array.isArray(container)) { - return new DualViewer(container[0], container[1], options); - } else if (options.containerB) { - return new DualViewer(container, options.containerB, options); - } else { - throw new Error('DualViewer requires two containers'); - } - - default: - console.warn(`[GeomPack Viewer] Unknown viewer type: ${type}, falling back to standard`); - return new StandardViewer(container, options); - } - }, - - /** - * Create and initialize a viewer - * @param {string} type - Viewer type - * @param {HTMLElement|HTMLElement[]} container - Container element(s) - * @param {Object} options - Viewer options - * @returns {Promise} Initialized viewer - */ - async createAndInit(type, container, options = {}) { - const viewer = this.create(type, container, options); - await viewer.initialize(); - return viewer; - } -}; - -// Default export -export default { - // Utils - ColorUtils, - BoundsUtils, - MessageHandler, - - // Core - CameraController, - ActorManager, - Representation, - BaseViewer, - - // UI - SettingsPanel, - SettingTypes, - ControlsBar, - ControlTypes, - StandardControls, - - // Loaders - LoaderFactory, - FormatTypes, - STLLoader, - OBJLoader, - VTPLoader, - GLTFLoader, - - // Features - FieldVisualization, - TextureManager, - ScreenshotCapture, - AxisIndicator, - - // Viewers - StandardViewer, - TexturedViewer, - DualViewer, - DualLayoutMode, - ViewerTypes, - - // Factory functions - createStandardViewer, - createTexturedViewer, - createDualViewer, - - // Factory - ViewerFactory -}; diff --git a/javascript/js/viewer/loaders/GLTFLoader.js b/javascript/js/viewer/loaders/GLTFLoader.js deleted file mode 100644 index b589f853..00000000 --- a/javascript/js/viewer/loaders/GLTFLoader.js +++ /dev/null @@ -1,299 +0,0 @@ -/** - * GLTFLoader - GLTF/GLB mesh file loader with texture support - * - * Loads GLTF/GLB files using VTK.js vtkGLTFImporter. - * Includes critical texture fixes for proper rendering: - * - Removes metallicRoughnessTexture (causes rendering issues) - * - Sets material to non-metallic for proper texture display - * - Enables two-sided lighting - * - * This loader properly handles textured meshes, unlike simplified implementations. - */ - -/** - * Check if GLTFImporter is available - * @param {Object} vtk - VTK.js global object - * @returns {boolean} - */ -export function isGLTFSupported(vtk) { - try { - return vtk.IO.Geometry.vtkGLTFImporter !== undefined; - } catch { - return false; - } -} - -/** - * Load a GLTF/GLB file with full texture support - * @param {string} filepath - URL to the GLTF/GLB file - * @param {Object} vtk - VTK.js global object - * @param {Object} renderer - VTK renderer (required for GLTF import) - * @param {Object} options - Loading options - * @param {boolean} options.applyTextureFixes - Apply material fixes for proper texture display (default: true) - * @param {boolean} options.twoSidedLighting - Enable two-sided lighting (default: true) - * @returns {Object} Result object with actors and metadata - */ -export async function loadGLTF(filepath, vtk, renderer, options = {}) { - const { - applyTextureFixes = true, - twoSidedLighting = true - } = options; - - const vtkGLTFImporter = vtk.IO.Geometry.vtkGLTFImporter; - - if (!vtkGLTFImporter) { - throw new Error('GLTFImporter not available in this VTK.js version'); - } - - // Enable two-sided lighting for better texture visibility - if (twoSidedLighting && renderer.setTwoSidedLighting) { - renderer.setTwoSidedLighting(true); - } - - const importer = vtkGLTFImporter.newInstance(); - - // CRITICAL: Set renderer FIRST - importActors() needs this - importer.setRenderer(renderer); - - // Set the URL - importer.setUrl(filepath); - - // Wait for GLTF parsing to complete using onReady callback - await new Promise((resolve, reject) => { - importer.onReady(() => { - resolve(); - }); - - // Start loading (this triggers parsing) - importer.loadData().catch(reject); - }); - - // Import actors (creates VTK actors and adds them to renderer) - importer.importActors(); - - // Get actors from the importer (returns Map) - const actorsMap = importer.getActors(); - const actors = Array.from(actorsMap.values()); - - // Track texture and vertex color information - let hasTexture = false; - let hasMaterial = false; - let hasVertexColors = false; - - // Process actors and apply texture fixes - if (applyTextureFixes) { - actors.forEach((actor, index) => { - const mapper = actor.getMapper(); - const property = actor.getProperty(); - const input = mapper?.getInputData(); - - if (input && input.getNumberOfPoints() > 0) { - // Check for vertex colors (may be stored as scalars or as named array COLOR_0) - const pointData = input.getPointData(); - let scalars = pointData?.getScalars(); - - // If no default scalars, check for COLOR_0 array (GLTF standard) - if (!scalars && pointData) { - const numArrays = pointData.getNumberOfArrays(); - for (let i = 0; i < numArrays; i++) { - const arr = pointData.getArray(i); - const name = arr?.getName() || ''; - if (name.toUpperCase().includes('COLOR') && arr.getNumberOfComponents() >= 3) { - scalars = arr; - // Set as active scalars so VTK renders them - pointData.setScalars(arr); - break; - } - } - } - - if (scalars && scalars.getNumberOfComponents() >= 3) { - hasVertexColors = true; - console.log('[GLTFLoader] Found vertex colors:', scalars.getName(), 'components:', scalars.getNumberOfComponents()); - } - - if (property) { - // CRITICAL FIX: Remove metallicRoughnessTexture if present - // VTK.js GLTFImporter may use this instead of baseColorTexture - if (typeof property.setRMTexture === 'function') { - property.setRMTexture(null); - } - if (typeof property.setMetallicRoughnessTexture === 'function') { - property.setMetallicRoughnessTexture(null); - } - - // Try multiple methods to get the base color texture - let texture = null; - if (typeof property.getBaseColorTexture === 'function') { - texture = property.getBaseColorTexture(); - } - if (!texture && typeof property.getDiffuseTexture === 'function') { - texture = property.getDiffuseTexture(); - } - if (!texture && typeof property.getTexture === 'function') { - texture = property.getTexture(); - } - - if (texture) { - hasTexture = true; - actor.addTexture(texture); - } - - // Check for material - if (property.getMetallic !== undefined || property.getRoughness !== undefined) { - hasMaterial = true; - } - - // Set material to non-metallic 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 - } - } - - // Configure mapper for rendering - if (mapper) { - console.log('[GLTFLoader] Configuring mapper - hasVertexColors:', hasVertexColors, 'hasTexture:', hasTexture); - if (hasVertexColors && !hasTexture) { - // Enable scalar visibility to show vertex colors - console.log('[GLTFLoader] Enabling vertex color rendering'); - mapper.setScalarVisibility(true); - mapper.setScalarModeToUsePointData(); - mapper.setColorModeToDirectScalars(); - - // Set actor color to white so vertex colors show through - if (property) { - console.log('[GLTFLoader] Setting actor color to white'); - property.setColor(1.0, 1.0, 1.0); - // Also set PBR base color factor for GLTF materials - if (typeof property.setBaseColorFactor === 'function') { - console.log('[GLTFLoader] Setting PBR baseColorFactor to white'); - property.setBaseColorFactor([1.0, 1.0, 1.0, 1.0]); - } - // Set diffuse and ambient explicitly - if (typeof property.setDiffuseColor === 'function') { - property.setDiffuseColor(1.0, 1.0, 1.0); - } - if (typeof property.setAmbientColor === 'function') { - property.setAmbientColor(1.0, 1.0, 1.0); - } - if (typeof property.setAmbient === 'function') { - property.setAmbient(0.2); - } - if (typeof property.setDiffuse === 'function') { - property.setDiffuse(0.8); - } - } - } else { - // Disable scalars for texture rendering - mapper.setScalarVisibility(false); - } - mapper.modified(); - } - } - }); - } - - // Calculate combined bounds from all actors - let bounds = null; - if (actors.length > 0) { - bounds = [Infinity, -Infinity, Infinity, -Infinity, Infinity, -Infinity]; - actors.forEach(actor => { - const actorBounds = actor.getBounds(); - bounds[0] = Math.min(bounds[0], actorBounds[0]); - bounds[1] = Math.max(bounds[1], actorBounds[1]); - bounds[2] = Math.min(bounds[2], actorBounds[2]); - bounds[3] = Math.max(bounds[3], actorBounds[3]); - bounds[4] = Math.min(bounds[4], actorBounds[4]); - bounds[5] = Math.max(bounds[5], actorBounds[5]); - }); - } - - // Calculate total points/polys - let totalPoints = 0; - let totalPolys = 0; - actors.forEach(actor => { - const mapper = actor.getMapper(); - const input = mapper?.getInputData(); - if (input) { - totalPoints += input.getNumberOfPoints(); - totalPolys += input.getNumberOfPolys(); - } - }); - - return { - actors, - importer, - bounds, - hasTexture, - hasVertexColors, - metadata: { - format: 'GLTF', - numActors: actors.length, - numPoints: totalPoints, - numPolys: totalPolys, - bounds, - hasTexture, - hasVertexColors, - hasMaterial - } - }; -} - -/** - * Simplified GLTF load for non-textured display (e.g., field visualization) - * Uses parseAsArrayBuffer instead of URL loading for simpler cases - * @param {ArrayBuffer} arrayBuffer - File data as ArrayBuffer - * @param {Object} vtk - VTK.js global object - * @returns {Object} Result object with polyData and metadata - */ -export async function loadGLTFSimple(arrayBuffer, vtk) { - const vtkGLTFImporter = vtk.IO.Geometry.vtkGLTFImporter; - - if (!vtkGLTFImporter) { - throw new Error('GLTFImporter not available'); - } - - const importer = vtkGLTFImporter.newInstance(); - importer.parseAsArrayBuffer(arrayBuffer); - await importer.update(); - - const polyData = importer.getOutputData(); - if (!polyData) { - throw new Error('Failed to extract polydata from GLTF'); - } - - return { - polyData, - importer, - metadata: { - format: 'GLTF', - numPoints: polyData.getNumberOfPoints(), - numPolys: polyData.getNumberOfPolys(), - bounds: polyData.getBounds() - } - }; -} - -/** - * Check if a filepath is a GLTF/GLB file - * @param {string} filepath - File path or URL - * @returns {boolean} - */ -export function canLoadGLTF(filepath) { - const lower = filepath.toLowerCase(); - return lower.includes('.glb') || lower.includes('.gltf'); -} - -export default { - load: loadGLTF, - loadSimple: loadGLTFSimple, - canLoad: canLoadGLTF, - isSupported: isGLTFSupported, - format: 'GLTF' -}; diff --git a/javascript/js/viewer/loaders/LoaderFactory.js b/javascript/js/viewer/loaders/LoaderFactory.js deleted file mode 100644 index 4c82d6aa..00000000 --- a/javascript/js/viewer/loaders/LoaderFactory.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * LoaderFactory - Auto-detect and load mesh files - * - * Provides a unified interface for loading different mesh formats. - * Automatically detects file type and uses the appropriate loader. - */ - -import { loadSTL, canLoadSTL } from './STLLoader.js'; -import { loadOBJ, canLoadOBJ } from './OBJLoader.js'; -import { loadVTP, canLoadVTP } from './VTPLoader.js'; -import { loadGLTF, loadGLTFSimple, canLoadGLTF, isGLTFSupported } from './GLTFLoader.js'; - -/** - * Supported format types - */ -export const FormatTypes = { - STL: 'STL', - OBJ: 'OBJ', - VTP: 'VTP', - GLTF: 'GLTF', - GLB: 'GLB' -}; - -/** - * Detect file format from filepath - * @param {string} filepath - File path or URL - * @returns {string|null} Format type or null if unknown - */ -export function detectFormat(filepath) { - const lower = filepath.toLowerCase(); - - if (lower.includes('.stl')) return FormatTypes.STL; - if (lower.includes('.obj')) return FormatTypes.OBJ; - if (lower.includes('.vtp')) return FormatTypes.VTP; - if (lower.includes('.glb')) return FormatTypes.GLB; - if (lower.includes('.gltf')) return FormatTypes.GLTF; - - return null; -} - -/** - * Check if a format requires special handling (e.g., GLTF needs renderer) - * @param {string} format - Format type - * @returns {boolean} - */ -export function requiresRenderer(format) { - return format === FormatTypes.GLTF || format === FormatTypes.GLB; -} - -/** - * Check if a format supports textures - * @param {string} format - Format type - * @returns {boolean} - */ -export function supportsTextures(format) { - return format === FormatTypes.GLTF || format === FormatTypes.GLB; -} - -/** - * Check if a format supports scalar fields - * @param {string} format - Format type - * @returns {boolean} - */ -export function supportsFields(format) { - return format === FormatTypes.VTP; -} - -/** - * Fetch file data as ArrayBuffer - * @param {string} filepath - URL to fetch - * @returns {Promise} - */ -async function fetchFile(filepath, onProgress = null) { - const response = await fetch(filepath); - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - const total = Number(response.headers.get('Content-Length')) || 0; - // Fall back to a plain (indeterminate) read if we can't stream or size it. - if (!response.body || !total || typeof onProgress !== 'function') { - if (typeof onProgress === 'function') onProgress(null); - return response.arrayBuffer(); - } - const reader = response.body.getReader(); - const chunks = []; - let loaded = 0; - onProgress(0, 0, total); - for (;;) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - loaded += value.length; - onProgress(Math.min(1, loaded / total), loaded, total); - } - const out = new Uint8Array(loaded); - let offset = 0; - for (const c of chunks) { out.set(c, offset); offset += c.length; } - return out.buffer; -} - -/** - * Load a mesh file with automatic format detection - * @param {string} filepath - URL to the mesh file - * @param {Object} vtk - VTK.js global object - * @param {Object} options - Loading options - * @param {Object} options.renderer - VTK renderer (required for GLTF with textures) - * @param {boolean} options.withTextures - Load GLTF with full texture support (default: true) - * @param {boolean} options.twoSidedLighting - Enable two-sided lighting for GLTF (default: true) - * @returns {Promise} Result object with polyData/actors and metadata - */ -export async function loadMesh(filepath, vtk, options = {}) { - const { - renderer = null, - withTextures = true, - twoSidedLighting = true, - onProgress = null - } = options; - - const format = detectFormat(filepath); - - if (!format) { - throw new Error(`Unsupported file format: ${filepath}`); - } - - console.log(`[LoaderFactory] Loading ${format} file:`, filepath); - - // GLTF/GLB with texture support requires renderer and URL-based loading - // (VTK fetches internally, so we can't byte-track it -> indeterminate). - if ((format === FormatTypes.GLTF || format === FormatTypes.GLB) && withTextures && renderer) { - if (typeof onProgress === 'function') onProgress(null); - return loadGLTF(filepath, vtk, renderer, { - applyTextureFixes: true, - twoSidedLighting - }); - } - - // For all other cases, fetch the file first (with download progress) - const arrayBuffer = await fetchFile(filepath, onProgress); - - switch (format) { - case FormatTypes.STL: - return loadSTL(arrayBuffer, vtk); - - case FormatTypes.OBJ: - return loadOBJ(arrayBuffer, vtk); - - case FormatTypes.VTP: - return loadVTP(arrayBuffer, vtk); - - case FormatTypes.GLTF: - case FormatTypes.GLB: - // Simple GLTF load without texture support - return loadGLTFSimple(arrayBuffer, vtk); - - default: - throw new Error(`No loader available for format: ${format}`); - } -} - -/** - * Load multiple mesh files - * @param {string[]} filepaths - Array of URLs to mesh files - * @param {Object} vtk - VTK.js global object - * @param {Object} options - Loading options (same as loadMesh) - * @returns {Promise} Array of result objects - */ -export async function loadMeshes(filepaths, vtk, options = {}) { - return Promise.all(filepaths.map(fp => loadMesh(fp, vtk, options))); -} - -/** - * Create a loader configuration for a specific format - * Useful for manual loader selection - * @param {string} format - Format type - * @returns {Object} Loader configuration - */ -export function getLoaderConfig(format) { - switch (format) { - case FormatTypes.STL: - return { - load: loadSTL, - canLoad: canLoadSTL, - requiresRenderer: false, - supportsTextures: false, - supportsFields: false - }; - case FormatTypes.OBJ: - return { - load: loadOBJ, - canLoad: canLoadOBJ, - requiresRenderer: false, - supportsTextures: false, - supportsFields: false - }; - case FormatTypes.VTP: - return { - load: loadVTP, - canLoad: canLoadVTP, - requiresRenderer: false, - supportsTextures: false, - supportsFields: true - }; - case FormatTypes.GLTF: - case FormatTypes.GLB: - return { - load: loadGLTF, - loadSimple: loadGLTFSimple, - canLoad: canLoadGLTF, - isSupported: isGLTFSupported, - requiresRenderer: true, - supportsTextures: true, - supportsFields: false - }; - default: - return null; - } -} - -export default { - loadMesh, - loadMeshes, - detectFormat, - requiresRenderer, - supportsTextures, - supportsFields, - getLoaderConfig, - FormatTypes -}; diff --git a/javascript/js/viewer/loaders/OBJLoader.js b/javascript/js/viewer/loaders/OBJLoader.js deleted file mode 100644 index 3f388d20..00000000 --- a/javascript/js/viewer/loaders/OBJLoader.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * OBJLoader - OBJ mesh file loader - * - * Loads Wavefront OBJ files using VTK.js vtkOBJReader. - * Includes normals computation for proper shading. - */ - -/** - * Load an OBJ file and return polydata - * @param {ArrayBuffer} arrayBuffer - File data as ArrayBuffer - * @param {Object} vtk - VTK.js global object - * @returns {Object} Result object with polyData and metadata - */ -export async function loadOBJ(arrayBuffer, vtk) { - const vtkOBJReader = vtk.IO.Misc.vtkOBJReader; - const vtkPolyDataNormals = vtk.Filters.Core.vtkPolyDataNormals; - - const reader = vtkOBJReader.newInstance(); - - // OBJ files are text-based - const text = new TextDecoder().decode(arrayBuffer); - reader.parseAsText(text); - - const rawPolyData = reader.getOutputData(); - if (!rawPolyData) { - throw new Error('Failed to parse OBJ data'); - } - - // Apply normals filter for proper shading - const normalsFilter = vtkPolyDataNormals.newInstance(); - normalsFilter.setInputConnection(reader.getOutputPort()); - normalsFilter.update(); - - const polyData = normalsFilter.getOutputData(); - - return { - polyData, - reader, - normalsFilter, - outputPort: normalsFilter.getOutputPort(), - metadata: { - format: 'OBJ', - numPoints: polyData.getNumberOfPoints(), - numPolys: polyData.getNumberOfPolys(), - bounds: polyData.getBounds() - } - }; -} - -/** - * Check if a filepath is an OBJ file - * @param {string} filepath - File path or URL - * @returns {boolean} - */ -export function canLoadOBJ(filepath) { - return filepath.toLowerCase().includes('.obj'); -} - -export default { - load: loadOBJ, - canLoad: canLoadOBJ, - format: 'OBJ' -}; diff --git a/javascript/js/viewer/loaders/STLLoader.js b/javascript/js/viewer/loaders/STLLoader.js deleted file mode 100644 index 801341dc..00000000 --- a/javascript/js/viewer/loaders/STLLoader.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * STLLoader - STL mesh file loader - * - * Loads binary or ASCII STL files using VTK.js vtkSTLReader. - * Includes normals computation for proper shading. - */ - -/** - * Load an STL file and return polydata - * @param {ArrayBuffer} arrayBuffer - File data as ArrayBuffer - * @param {Object} vtk - VTK.js global object - * @returns {Object} Result object with polyData and metadata - */ -export async function loadSTL(arrayBuffer, vtk) { - const vtkSTLReader = vtk.IO.Geometry.vtkSTLReader; - const vtkPolyDataNormals = vtk.Filters.Core.vtkPolyDataNormals; - - const reader = vtkSTLReader.newInstance(); - reader.parseAsArrayBuffer(arrayBuffer); - - const rawPolyData = reader.getOutputData(); - if (!rawPolyData) { - throw new Error('Failed to parse STL data'); - } - - // Apply normals filter for proper shading - const normalsFilter = vtkPolyDataNormals.newInstance(); - normalsFilter.setInputConnection(reader.getOutputPort()); - normalsFilter.update(); - - const polyData = normalsFilter.getOutputData(); - - return { - polyData, - reader, - normalsFilter, - outputPort: normalsFilter.getOutputPort(), - metadata: { - format: 'STL', - numPoints: polyData.getNumberOfPoints(), - numPolys: polyData.getNumberOfPolys(), - bounds: polyData.getBounds() - } - }; -} - -/** - * Check if a filepath is an STL file - * @param {string} filepath - File path or URL - * @returns {boolean} - */ -export function canLoadSTL(filepath) { - return filepath.toLowerCase().includes('.stl'); -} - -export default { - load: loadSTL, - canLoad: canLoadSTL, - format: 'STL' -}; diff --git a/javascript/js/viewer/loaders/VTPLoader.js b/javascript/js/viewer/loaders/VTPLoader.js deleted file mode 100644 index 5f20eaaa..00000000 --- a/javascript/js/viewer/loaders/VTPLoader.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * VTPLoader - VTK XML PolyData file loader - * - * Loads VTP files using VTK.js vtkXMLPolyDataReader. - * VTP files can contain scalar fields (vertex/face attributes). - * Includes normals computation for proper shading. - */ - -/** - * Extract field information from polydata - * @param {Object} polyData - VTK polydata object - * @returns {Object} Field information - */ -function extractFieldInfo(polyData) { - const pointData = polyData.getPointData(); - const cellData = polyData.getCellData(); - - const pointFields = []; - const cellFields = []; - - // Extract point (vertex) data arrays - for (let i = 0; i < pointData.getNumberOfArrays(); i++) { - const array = pointData.getArray(i); - const name = array.getName(); - const range = array.getRange(); - pointFields.push({ - name, - type: 'point', - numComponents: array.getNumberOfComponents(), - range: [range[0], range[1]] - }); - } - - // Extract cell (face) data arrays - for (let i = 0; i < cellData.getNumberOfArrays(); i++) { - const array = cellData.getArray(i); - const name = array.getName(); - const range = array.getRange(); - cellFields.push({ - name, - type: 'cell', - numComponents: array.getNumberOfComponents(), - range: [range[0], range[1]] - }); - } - - return { - pointFields, - cellFields, - hasFields: pointFields.length > 0 || cellFields.length > 0 - }; -} - -/** - * Load a VTP file and return polydata - * @param {ArrayBuffer} arrayBuffer - File data as ArrayBuffer - * @param {Object} vtk - VTK.js global object - * @returns {Object} Result object with polyData, fields, and metadata - */ -export async function loadVTP(arrayBuffer, vtk) { - const vtkXMLPolyDataReader = vtk.IO.XML.vtkXMLPolyDataReader; - const vtkPolyDataNormals = vtk.Filters.Core.vtkPolyDataNormals; - - const reader = vtkXMLPolyDataReader.newInstance(); - reader.parseAsArrayBuffer(arrayBuffer); - - const rawPolyData = reader.getOutputData(); - if (!rawPolyData) { - throw new Error('Failed to parse VTP data'); - } - - // Extract field info BEFORE normals filter (preserves original data) - const fieldInfo = extractFieldInfo(rawPolyData); - - // Apply normals filter for proper shading - const normalsFilter = vtkPolyDataNormals.newInstance(); - normalsFilter.setInputConnection(reader.getOutputPort()); - normalsFilter.update(); - - const polyData = normalsFilter.getOutputData(); - - // Extract field info again from filtered data (should be same) - const filteredFieldInfo = extractFieldInfo(polyData); - - return { - polyData, - reader, - normalsFilter, - outputPort: normalsFilter.getOutputPort(), - fields: filteredFieldInfo, - metadata: { - format: 'VTP', - numPoints: polyData.getNumberOfPoints(), - numPolys: polyData.getNumberOfPolys(), - bounds: polyData.getBounds(), - pointFields: filteredFieldInfo.pointFields.map(f => f.name), - cellFields: filteredFieldInfo.cellFields.map(f => f.name), - hasFields: filteredFieldInfo.hasFields - } - }; -} - -/** - * Check if a filepath is a VTP file - * @param {string} filepath - File path or URL - * @returns {boolean} - */ -export function canLoadVTP(filepath) { - return filepath.toLowerCase().includes('.vtp'); -} - -export default { - load: loadVTP, - canLoad: canLoadVTP, - format: 'VTP' -}; diff --git a/javascript/js/viewer/ui/ControlsBar.js b/javascript/js/viewer/ui/ControlsBar.js deleted file mode 100644 index 731f9b3e..00000000 --- a/javascript/js/viewer/ui/ControlsBar.js +++ /dev/null @@ -1,398 +0,0 @@ -/** - * ControlsBar - Configurable controls bar for VTK viewers - * - * Generates and manages the bottom controls bar with camera buttons, - * toggles, selectors, and action buttons. - */ - -/** - * Control types - */ -export const ControlTypes = { - BUTTON: 'button', - CHECKBOX: 'checkbox', - SELECT: 'select', - DIVIDER: 'divider', - LABEL: 'label' -}; - -/** - * Standard control configurations - */ -export const StandardControls = { - settingsButton: { - id: 'settingsButton', - type: ControlTypes.BUTTON, - label: '[Settings]', - title: 'Appearance Settings' - }, - divider: { - type: ControlTypes.DIVIDER - }, - edgesToggle: { - id: 'showEdges', - type: ControlTypes.CHECKBOX, - label: 'Edges' - }, - cameraButtons: { - type: 'camera_group', - buttons: ['+X', '-X', '+Y', '-Y', '+Z', '-Z'] - }, - screenshotButton: { - id: 'screenshot', - type: ControlTypes.BUTTON, - label: 'Screenshot' - }, - saveMeshButton: { - id: 'saveMesh', - type: ControlTypes.BUTTON, - label: 'Save Mesh', - className: 'save-button' - }, - saveGLBButton: { - id: 'saveGLB', - type: ControlTypes.BUTTON, - label: 'Save GLB', - className: 'save-button' - }, - resetCameraButton: { - id: 'resetCamera', - type: ControlTypes.BUTTON, - label: 'Reset Camera' - }, - fieldSelector: { - id: 'fieldSelector', - type: ControlTypes.SELECT, - label: 'Field', - wrapperClass: 'field-label', - hidden: true - }, - colormapSelector: { - id: 'colormapSelector', - type: ControlTypes.SELECT, - label: 'Colormap', - wrapperClass: 'colormap-label', - hidden: true - } -}; - -export class ControlsBar { - /** - * Create a ControlsBar - * @param {Object} options - Configuration options - * @param {HTMLElement} options.container - Container element for the controls bar - * @param {Object} options.handlers - Event handlers keyed by control ID - * @param {Array} options.controls - Array of control configurations - */ - constructor(options = {}) { - this.container = options.container; - this.handlers = options.handlers || {}; - this.controls = options.controls || this._getDefaultControls(); - this.elements = {}; - } - - /** - * Get default controls for standard viewer - * @private - */ - _getDefaultControls() { - return [ - StandardControls.settingsButton, - StandardControls.divider, - StandardControls.edgesToggle, - StandardControls.divider, - StandardControls.cameraButtons, - StandardControls.divider, - StandardControls.fieldSelector, - StandardControls.colormapSelector, - StandardControls.screenshotButton, - StandardControls.saveMeshButton - ]; - } - - /** - * Generate HTML for the controls bar - * @returns {string} HTML string - */ - generateHTML() { - let html = ''; - - this.controls.forEach(control => { - if (!control) return; - - switch (control.type) { - case ControlTypes.BUTTON: - html += this._generateButton(control); - break; - case ControlTypes.CHECKBOX: - html += this._generateCheckbox(control); - break; - case ControlTypes.SELECT: - html += this._generateSelect(control); - break; - case ControlTypes.DIVIDER: - html += ''; - break; - case ControlTypes.LABEL: - html += `${control.text || ''}`; - break; - case 'camera_group': - html += this._generateCameraGroup(control); - break; - default: - if (typeof control.html === 'string') { - html += control.html; - } - } - }); - - return html; - } - - /** - * Generate button HTML - * @private - */ - _generateButton(control) { - const className = control.className || ''; - const style = control.id === 'settingsButton' ? 'width:32px;height:32px;padding:0;font-size:16px;' : ''; - const styleAttr = style ? `style="${style}"` : ''; - const classAttr = className ? `class="${className}"` : ''; - - return ``; - } - - /** - * Generate checkbox HTML - * @private - */ - _generateCheckbox(control) { - const checked = control.checked ? 'checked' : ''; - return ` - - `; - } - - /** - * Generate select HTML - * @private - */ - _generateSelect(control) { - const wrapperClass = control.wrapperClass || ''; - const hiddenStyle = control.hidden ? 'style="display:none;"' : ''; - const options = (control.options || []).map(opt => - `` - ).join(''); - - return ` - - `; - } - - /** - * Generate camera view buttons group - * @private - */ - _generateCameraGroup(control) { - const buttons = control.buttons || ['+X', '-X', '+Y', '-Y', '+Z', '-Z']; - return buttons.map(dir => { - const id = `view${dir.replace('+', 'Pos').replace('-', 'Neg')}`; - return ``; - }).join(''); - } - - /** - * Render the controls bar into the container - */ - render() { - if (!this.container) { - console.warn('[ControlsBar] No container specified'); - return; - } - - this.container.innerHTML = this.generateHTML(); - this._cacheElements(); - this._setupEventListeners(); - } - - /** - * Cache references to control elements - * @private - */ - _cacheElements() { - this.controls.forEach(control => { - if (control && control.id) { - this.elements[control.id] = document.getElementById(control.id); - } - }); - - // Also cache camera buttons - ['viewPosX', 'viewNegX', 'viewPosY', 'viewNegY', 'viewPosZ', 'viewNegZ'].forEach(id => { - const el = document.getElementById(id); - if (el) this.elements[id] = el; - }); - } - - /** - * Setup event listeners based on handlers - * @private - */ - _setupEventListeners() { - // Standard handlers - Object.keys(this.handlers).forEach(id => { - const element = this.elements[id]; - const handler = this.handlers[id]; - - if (element && handler) { - if (element.type === 'checkbox') { - element.addEventListener('change', (e) => handler(e.target.checked, e)); - } else if (element.tagName === 'SELECT') { - element.addEventListener('change', (e) => handler(e.target.value, e)); - } else { - element.addEventListener('click', handler); - } - } - }); - - // Camera buttons (special handling) - if (this.handlers.cameraView) { - const cameraDirections = ['+X', '-X', '+Y', '-Y', '+Z', '-Z']; - cameraDirections.forEach(dir => { - const id = `view${dir.replace('+', 'Pos').replace('-', 'Neg')}`; - const element = this.elements[id]; - if (element) { - element.addEventListener('click', () => this.handlers.cameraView(dir)); - } - }); - } - } - - /** - * Get an element by ID - * @param {string} id - Element ID - * @returns {HTMLElement|null} - */ - getElement(id) { - return this.elements[id] || document.getElementById(id); - } - - /** - * Show a control element - * @param {string} id - Element ID - */ - show(id) { - const wrapper = document.getElementById(`${id}Label`) || this.elements[id]; - if (wrapper) { - wrapper.style.display = 'flex'; - } - } - - /** - * Hide a control element - * @param {string} id - Element ID - */ - hide(id) { - const wrapper = document.getElementById(`${id}Label`) || this.elements[id]; - if (wrapper) { - wrapper.style.display = 'none'; - } - } - - /** - * Enable a control element - * @param {string} id - Element ID - */ - enable(id) { - const element = this.elements[id]; - if (element) { - element.disabled = false; - } - } - - /** - * Disable a control element - * @param {string} id - Element ID - */ - disable(id) { - const element = this.elements[id]; - if (element) { - element.disabled = true; - } - } - - /** - * Set button text - * @param {string} id - Button ID - * @param {string} text - New text - */ - setButtonText(id, text) { - const element = this.elements[id]; - if (element) { - element.textContent = text; - } - } - - /** - * Set button style (e.g., for save button states) - * @param {string} id - Button ID - * @param {Object} style - Style properties - */ - setButtonStyle(id, style) { - const element = this.elements[id]; - if (element) { - Object.assign(element.style, style); - } - } - - /** - * Populate a select element with options - * @param {string} id - Select element ID - * @param {Array} options - Array of {value, label, selected?} objects - * @param {boolean} clearFirst - Whether to clear existing options first - */ - populateSelect(id, options, clearFirst = true) { - const element = this.elements[id] || document.getElementById(id); - if (!element || element.tagName !== 'SELECT') return; - - if (clearFirst) { - element.innerHTML = ''; - } - - options.forEach(opt => { - const option = document.createElement('option'); - option.value = opt.value; - option.textContent = opt.label; - if (opt.selected) option.selected = true; - if (opt.disabled) option.disabled = true; - element.appendChild(option); - }); - } - - /** - * Add a handler for a control - * @param {string} id - Control ID - * @param {Function} handler - Event handler function - */ - addHandler(id, handler) { - this.handlers[id] = handler; - - // If already rendered, attach immediately - const element = this.elements[id]; - if (element) { - if (element.type === 'checkbox') { - element.addEventListener('change', (e) => handler(e.target.checked, e)); - } else if (element.tagName === 'SELECT') { - element.addEventListener('change', (e) => handler(e.target.value, e)); - } else { - element.addEventListener('click', handler); - } - } - } -} - -export default ControlsBar; diff --git a/javascript/js/viewer/ui/SettingsPanel.js b/javascript/js/viewer/ui/SettingsPanel.js deleted file mode 100644 index af39ae18..00000000 --- a/javascript/js/viewer/ui/SettingsPanel.js +++ /dev/null @@ -1,383 +0,0 @@ -/** - * SettingsPanel - Configurable settings modal for VTK viewers - * - * Generates and manages a settings panel with configurable fields. - * Different viewers can specify which settings to show. - */ - -/** - * Available setting types - */ -export const SettingTypes = { - NUMBER: 'number', - COLOR: 'color', - SELECT: 'select', - CHECKBOX: 'checkbox' -}; - -/** - * Default settings configuration - */ -const DEFAULT_SETTINGS_CONFIG = [ - { - id: 'pointSize', - label: 'Point Size', - type: SettingTypes.NUMBER, - min: 1, - max: 20, - step: 1, - default: 5 - }, - { - id: 'meshColor', - label: 'Mesh Color', - type: SettingTypes.COLOR, - default: '#ffffff' - }, - { - id: 'edgeWidth', - label: 'Edge Width', - type: SettingTypes.NUMBER, - min: 1, - max: 10, - step: 1, - default: 1 - }, - { - id: 'representation', - label: 'Render Mode', - type: SettingTypes.SELECT, - options: [ - { value: '2', label: 'Surface' }, - { value: '1', label: 'Wireframe' }, - { value: '0', label: 'Points' } - ], - default: '2' - }, - { - id: 'parallelProjection', - label: 'Parallel Projection', - type: SettingTypes.CHECKBOX, - default: false - }, - { - id: 'showAxisIndicator', - label: 'Show Axis Indicator', - type: SettingTypes.CHECKBOX, - default: false - } -]; - -/** - * Colormap range settings (optional, for field visualization) - */ -const COLORMAP_SETTINGS_CONFIG = [ - { - id: 'autoColorRange', - label: 'Auto Range', - type: SettingTypes.CHECKBOX, - default: true, - section: 'Colormap Range' - }, - { - id: 'colormapMin', - label: 'Min', - type: SettingTypes.NUMBER, - step: 'any', - placeholder: 'Auto', - default: '', - section: 'Colormap Range' - }, - { - id: 'colormapMax', - label: 'Max', - type: SettingTypes.NUMBER, - step: 'any', - placeholder: 'Auto', - default: '', - section: 'Colormap Range' - } -]; - -export class SettingsPanel { - /** - * Create a SettingsPanel - * @param {Object} options - Configuration options - * @param {HTMLElement} options.container - Container element for the panel - * @param {Function} options.onApply - Callback when settings are applied - * @param {string[]} options.enabledSettings - Array of setting IDs to show (null = all) - * @param {boolean} options.includeColormapSettings - Whether to include colormap range settings - */ - constructor(options = {}) { - this.container = options.container || document.body; - this.onApply = options.onApply || (() => {}); - this.enabledSettings = options.enabledSettings || null; - this.includeColormapSettings = options.includeColormapSettings || false; - - this.panelElement = null; - this.overlayElement = null; - this.currentValues = {}; - - // Build settings config - this.settingsConfig = this._buildSettingsConfig(); - - // Initialize default values - this.settingsConfig.forEach(setting => { - this.currentValues[setting.id] = setting.default; - }); - } - - /** - * Build the settings configuration based on enabled settings - * @private - */ - _buildSettingsConfig() { - let config = [...DEFAULT_SETTINGS_CONFIG]; - - if (this.includeColormapSettings) { - config = config.concat(COLORMAP_SETTINGS_CONFIG); - } - - if (this.enabledSettings) { - config = config.filter(s => this.enabledSettings.includes(s.id)); - } - - return config; - } - - /** - * Generate the HTML for the settings panel - * @returns {string} HTML string - */ - generateHTML() { - let currentSection = null; - let html = ` -
-
-

Appearance Settings

- `; - - this.settingsConfig.forEach(setting => { - // Add section header if needed - if (setting.section && setting.section !== currentSection) { - currentSection = setting.section; - html += ` -
- -
- `; - } - - html += this._generateSettingRow(setting); - }); - - html += ` -
- - -
-
- `; - - return html; - } - - /** - * Generate HTML for a single setting row - * @private - */ - _generateSettingRow(setting) { - let inputHtml = ''; - - switch (setting.type) { - case SettingTypes.NUMBER: - inputHtml = ` - - `; - break; - - case SettingTypes.COLOR: - inputHtml = ``; - break; - - case SettingTypes.SELECT: - inputHtml = `'; - break; - - case SettingTypes.CHECKBOX: - const checked = setting.default ? 'checked' : ''; - return ` -
- -
- `; - } - - return ` -
- - ${inputHtml} -
- `; - } - - /** - * Render the panel into the container - */ - render() { - // Create wrapper if needed - const wrapper = document.createElement('div'); - wrapper.innerHTML = this.generateHTML(); - - // Find or create panel elements in container - this.overlayElement = wrapper.querySelector('#settingsOverlay'); - this.panelElement = wrapper.querySelector('#settingsPanel'); - - // Append to container - this.container.appendChild(this.overlayElement); - this.container.appendChild(this.panelElement); - - // Setup event listeners - this._setupEventListeners(); - } - - /** - * Attach to existing panel elements in the DOM - * @param {HTMLElement} panel - The settings panel element - * @param {HTMLElement} overlay - The overlay element - */ - attachToExisting(panel, overlay) { - this.panelElement = panel; - this.overlayElement = overlay; - this._setupEventListeners(); - } - - /** - * Setup event listeners - * @private - */ - _setupEventListeners() { - // Apply button - const applyBtn = this.panelElement.querySelector('#applySettings'); - if (applyBtn) { - applyBtn.addEventListener('click', () => this._handleApply()); - } - - // Cancel button - const cancelBtn = this.panelElement.querySelector('#cancelSettings'); - if (cancelBtn) { - cancelBtn.addEventListener('click', () => this.close()); - } - - // Overlay click - if (this.overlayElement) { - this.overlayElement.addEventListener('click', () => this.close()); - } - } - - /** - * Handle apply button click - * @private - */ - _handleApply() { - const values = this.getValues(); - this.currentValues = { ...values }; - this.onApply(values); - this.close(); - } - - /** - * Open the settings panel - */ - open() { - // Update inputs with current values - this.setValues(this.currentValues); - - if (this.panelElement) { - this.panelElement.classList.add('visible'); - } - if (this.overlayElement) { - this.overlayElement.classList.add('visible'); - } - } - - /** - * Close the settings panel - */ - close() { - if (this.panelElement) { - this.panelElement.classList.remove('visible'); - } - if (this.overlayElement) { - this.overlayElement.classList.remove('visible'); - } - } - - /** - * Get current values from all inputs - * @returns {Object} Object with setting id -> value pairs - */ - getValues() { - const values = {}; - - this.settingsConfig.forEach(setting => { - const element = document.getElementById(setting.id); - if (!element) return; - - if (setting.type === SettingTypes.CHECKBOX) { - values[setting.id] = element.checked; - } else { - values[setting.id] = element.value; - } - }); - - return values; - } - - /** - * Set values on all inputs - * @param {Object} values - Object with setting id -> value pairs - */ - setValues(values) { - this.settingsConfig.forEach(setting => { - const element = document.getElementById(setting.id); - if (!element || values[setting.id] === undefined) return; - - if (setting.type === SettingTypes.CHECKBOX) { - element.checked = values[setting.id]; - } else { - element.value = values[setting.id]; - } - }); - } - - /** - * Update stored current values - * @param {Object} values - Partial values to update - */ - updateCurrentValues(values) { - this.currentValues = { ...this.currentValues, ...values }; - } - - /** - * Get current stored values - * @returns {Object} Current values - */ - getCurrentValues() { - return { ...this.currentValues }; - } -} - -export default SettingsPanel; diff --git a/javascript/js/viewer/utils/BoundsUtils.js b/javascript/js/viewer/utils/BoundsUtils.js deleted file mode 100644 index 0236ea1f..00000000 --- a/javascript/js/viewer/utils/BoundsUtils.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Bounds calculation utilities for VTK viewers - */ - -/** - * Get the center point of bounds - * @param {number[]} bounds - VTK bounds array [xmin, xmax, ymin, ymax, zmin, zmax] - * @returns {number[]} Center point [x, y, z] - */ -export function getCenter(bounds) { - return [ - (bounds[0] + bounds[1]) / 2, - (bounds[2] + bounds[3]) / 2, - (bounds[4] + bounds[5]) / 2 - ]; -} - -/** - * Get the maximum dimension of bounds - * @param {number[]} bounds - VTK bounds array [xmin, xmax, ymin, ymax, zmin, zmax] - * @returns {number} Maximum dimension - */ -export function getMaxDimension(bounds) { - return Math.max( - bounds[1] - bounds[0], - bounds[3] - bounds[2], - bounds[5] - bounds[4] - ); -} - -/** - * Get all three dimensions of bounds - * @param {number[]} bounds - VTK bounds array [xmin, xmax, ymin, ymax, zmin, zmax] - * @returns {number[]} Dimensions [width, height, depth] - */ -export function getDimensions(bounds) { - return [ - bounds[1] - bounds[0], - bounds[3] - bounds[2], - bounds[5] - bounds[4] - ]; -} - -/** - * Combine multiple bounds into a single bounding box - * @param {number[][]} boundsArray - Array of VTK bounds - * @returns {number[]} Combined bounds [xmin, xmax, ymin, ymax, zmin, zmax] - */ -export function combineBounds(boundsArray) { - if (!boundsArray || boundsArray.length === 0) { - return [0, 0, 0, 0, 0, 0]; - } - - const combined = [...boundsArray[0]]; - - for (let i = 1; i < boundsArray.length; i++) { - const b = boundsArray[i]; - combined[0] = Math.min(combined[0], b[0]); // xmin - combined[1] = Math.max(combined[1], b[1]); // xmax - combined[2] = Math.min(combined[2], b[2]); // ymin - combined[3] = Math.max(combined[3], b[3]); // ymax - combined[4] = Math.min(combined[4], b[4]); // zmin - combined[5] = Math.max(combined[5], b[5]); // zmax - } - - return combined; -} - -/** - * Check if bounds are valid (non-empty) - * @param {number[]} bounds - VTK bounds array - * @returns {boolean} True if bounds are valid - */ -export function isValidBounds(bounds) { - if (!bounds || bounds.length !== 6) return false; - - // Check for NaN or Infinity - for (const val of bounds) { - if (!Number.isFinite(val)) return false; - } - - // Check that max >= min for all dimensions - return bounds[1] >= bounds[0] && - bounds[3] >= bounds[2] && - bounds[5] >= bounds[4]; -} - -export default { - getCenter, - getMaxDimension, - getDimensions, - combineBounds, - isValidBounds -}; diff --git a/javascript/js/viewer/utils/ColorUtils.js b/javascript/js/viewer/utils/ColorUtils.js deleted file mode 100644 index 1e2debdc..00000000 --- a/javascript/js/viewer/utils/ColorUtils.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Color utility functions for VTK viewers - */ - -/** - * Convert RGB values (0-1 range) to CSS rgb() string - * @param {number} r - Red component (0-1) - * @param {number} g - Green component (0-1) - * @param {number} b - Blue component (0-1) - * @returns {string} CSS rgb string like "rgb(255, 128, 0)" - */ -export function rgbToString(r, g, b) { - const r255 = Math.round(r * 255); - const g255 = Math.round(g * 255); - const b255 = Math.round(b * 255); - return `rgb(${r255}, ${g255}, ${b255})`; -} - -/** - * Convert hex color string to RGB array (0-1 range) - * @param {string} hex - Hex color like "#ffffff" or "#fff" - * @returns {number[]} RGB array [r, g, b] with values 0-1 - */ -export function hexToRgb(hex) { - // Remove # if present - hex = hex.replace(/^#/, ''); - - // Handle shorthand hex (#fff -> #ffffff) - if (hex.length === 3) { - hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; - } - - const r = parseInt(hex.substr(0, 2), 16) / 255; - const g = parseInt(hex.substr(2, 2), 16) / 255; - const b = parseInt(hex.substr(4, 2), 16) / 255; - - return [r, g, b]; -} - -/** - * Convert RGB array (0-1 range) to hex color string - * @param {number} r - Red component (0-1) - * @param {number} g - Green component (0-1) - * @param {number} b - Blue component (0-1) - * @returns {string} Hex color like "#ffffff" - */ -export function rgbToHex(r, g, b) { - const toHex = (c) => { - const hex = Math.round(c * 255).toString(16); - return hex.length === 1 ? '0' + hex : hex; - }; - return '#' + toHex(r) + toHex(g) + toHex(b); -} - -export default { - rgbToString, - hexToRgb, - rgbToHex -}; diff --git a/javascript/js/viewer/utils/MessageHandler.js b/javascript/js/viewer/utils/MessageHandler.js deleted file mode 100644 index 4e2de8cb..00000000 --- a/javascript/js/viewer/utils/MessageHandler.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * PostMessage communication utilities for VTK viewers - */ - -/** - * Message types used by the viewer system - */ -export const MessageTypes = { - // Incoming messages (from parent) - LOAD_MESH: 'LOAD_MESH', - LOAD_DUAL_MESH: 'LOAD_DUAL_MESH', - - // Outgoing messages (to parent) - MESH_LOADED: 'MESH_LOADED', - MESH_ERROR: 'MESH_ERROR', - SCREENSHOT: 'SCREENSHOT' -}; - -/** - * Send a message to the parent window - * @param {string} type - Message type - * @param {Object} data - Additional data to send - */ -export function sendToParent(type, data = {}) { - if (window.parent && window.parent !== window) { - window.parent.postMessage({ - type, - ...data - }, '*'); - } -} - -/** - * Send an error message to the parent window - * @param {string|Error} error - Error message or Error object - */ -export function sendError(error) { - const message = error instanceof Error ? error.message : error; - sendToParent(MessageTypes.MESH_ERROR, { error: message }); -} - -/** - * Send a screenshot to the parent window - * @param {string} imageDataUrl - Base64 PNG data URL - */ -export function sendScreenshot(imageDataUrl) { - sendToParent(MessageTypes.SCREENSHOT, { - image: imageDataUrl, - timestamp: Date.now() - }); -} - -/** - * Send mesh loaded confirmation to parent - */ -export function sendMeshLoaded() { - sendToParent(MessageTypes.MESH_LOADED, { error: null }); -} - -/** - * Create a message listener for the viewer - * @param {Object} handlers - Object mapping message types to handler functions - * @returns {Function} Cleanup function to remove the listener - */ -export function createMessageListener(handlers) { - const listener = (event) => { - const { type } = event.data || {}; - - if (type && handlers[type]) { - handlers[type](event.data); - } - }; - - window.addEventListener('message', listener); - - // Return cleanup function - return () => { - window.removeEventListener('message', listener); - }; -} - -/** - * Extract filename from a filepath URL - * @param {string} filepath - URL or path to extract filename from - * @returns {string} Extracted filename - */ -export function extractFilename(filepath) { - const urlParams = new URLSearchParams(filepath.split('?')[1] || ''); - return urlParams.get('filename') || filepath.split('/').pop().split('?')[0]; -} - -export default { - MessageTypes, - sendToParent, - sendError, - sendScreenshot, - sendMeshLoaded, - createMessageListener, - extractFilename -}; diff --git a/javascript/js/viewer/viewers/DualViewer.js b/javascript/js/viewer/viewers/DualViewer.js deleted file mode 100644 index 8eee2d94..00000000 --- a/javascript/js/viewer/viewers/DualViewer.js +++ /dev/null @@ -1,551 +0,0 @@ -/** - * DualViewer - VTK viewer for dual mesh comparison - * - * Supports side-by-side and overlay layouts with: - * - Camera synchronization between viewports - * - Synchronized scalar field ranges - * - Multiple layout modes (side-by-side, overlay, split) - * - Independent and linked settings - */ - -import { CameraController } from '../core/CameraController.js'; -import { ActorManager } from '../core/ActorManager.js'; -import { loadMesh, supportsTextures } from '../loaders/LoaderFactory.js'; -import { FieldVisualization } from '../features/FieldVisualization.js'; -import { TextureManager } from '../features/TextureManager.js'; -import { ScreenshotCapture } from '../features/ScreenshotCapture.js'; -import { sendMeshLoaded, sendError, createMessageListener, extractFilename } from '../utils/MessageHandler.js'; -import { combineBounds, isValidBounds } from '../utils/BoundsUtils.js'; - -/** - * Layout modes for dual viewer - */ -export const LayoutMode = { - SIDE_BY_SIDE: 'side-by-side', - OVERLAY: 'overlay', - VERTICAL: 'vertical' -}; - -/** - * Dual viewer configuration - */ -const DUAL_CONFIG = { - // Camera - cameraDistanceMultiplier: 2.5, - - // Layout - layout: LayoutMode.SIDE_BY_SIDE, - - // Sync options - syncCameras: true, - syncFields: true, - - // Appearance - backgroundColor: [0.15, 0.15, 0.15], - defaultMeshColorA: [0.6, 0.8, 1.0], - defaultMeshColorB: [0.6, 0.8, 1.0], - showEdges: false, - - // Features - enableFields: true, - enableTextures: true -}; - -export class DualViewer { - /** - * Create a DualViewer - * @param {HTMLElement} containerA - Container for first viewport - * @param {HTMLElement} containerB - Container for second viewport - * @param {Object} config - Configuration options - */ - constructor(containerA, containerB, config = {}) { - this.containerA = containerA; - this.containerB = containerB; - this.config = { ...DUAL_CONFIG, ...config }; - - // VTK objects - this.vtk = null; - - // Viewports (each has its own renderer, render window, etc.) - this.viewportA = null; - this.viewportB = null; - - // Feature modules (shared) - this.actorManager = null; - this.fieldVisualization = null; - - // State - this.layout = this.config.layout; - this.syncCameras = this.config.syncCameras; - this.syncFields = this.config.syncFields; - this.isInitialized = false; - - // Camera sync state - this._cameraObserver = null; - this._isUpdatingCamera = false; - - // Message listener cleanup - this._messageCleanup = null; - } - - /** - * Initialize the dual viewer - */ - async initialize() { - if (this.isInitialized) return; - - this.vtk = window.vtk; - if (!this.vtk) { - throw new Error('VTK.js not loaded'); - } - - // Create both viewports - this.viewportA = this._createViewport(this.containerA, 'A'); - this.viewportB = this._createViewport(this.containerB, 'B'); - - // Initialize shared modules - this.actorManager = new ActorManager(this.vtk, { - defaultColor: this.config.defaultMeshColorA - }); - this.fieldVisualization = new FieldVisualization(this.vtk, this.viewportA.renderWindow); - - // Setup camera sync if enabled - if (this.syncCameras) { - this._setupCameraSync(); - } - - // Setup resize handlers - this._setupResizeHandlers(); - - // Setup message listener - this._setupMessageListener(); - - this.isInitialized = true; - console.log('[DualViewer] Initialized'); - } - - /** - * Create a viewport - * @private - * @param {HTMLElement} container - * @param {string} id - * @returns {Object} - */ - _createViewport(container, id) { - const renderWindow = this.vtk.Rendering.Core.vtkRenderWindow.newInstance(); - const renderer = this.vtk.Rendering.Core.vtkRenderer.newInstance(); - renderer.setBackground(...this.config.backgroundColor); - renderWindow.addRenderer(renderer); - - const openGLRenderWindow = this.vtk.Rendering.OpenGL.vtkRenderWindow.newInstance(); - openGLRenderWindow.setContainer(container); - renderWindow.addView(openGLRenderWindow); - - const interactor = this.vtk.Rendering.Core.vtkRenderWindowInteractor.newInstance(); - interactor.setView(openGLRenderWindow); - interactor.initialize(); - interactor.bindEvents(container); - - const interactorStyle = this.vtk.Interaction.Style.vtkInteractorStyleTrackballCamera.newInstance(); - interactor.setInteractorStyle(interactorStyle); - - const { width, height } = container.getBoundingClientRect(); - openGLRenderWindow.setSize(width, height); - - const cameraController = new CameraController(renderer, { - distanceMultiplier: this.config.cameraDistanceMultiplier - }); - - // Create texture manager for this viewport - const textureManager = new TextureManager(this.vtk, renderWindow); - - return { - id, - container, - renderWindow, - renderer, - openGLRenderWindow, - interactor, - interactorStyle, - cameraController, - textureManager, - actors: [], - polyData: null, - filename: null - }; - } - - /** - * Setup camera synchronization between viewports - * @private - */ - _setupCameraSync() { - const syncCamera = (sourceViewport, targetViewport) => { - if (this._isUpdatingCamera) return; - this._isUpdatingCamera = true; - - const sourceCamera = sourceViewport.renderer.getActiveCamera(); - const targetCamera = targetViewport.renderer.getActiveCamera(); - - targetCamera.setPosition(...sourceCamera.getPosition()); - targetCamera.setFocalPoint(...sourceCamera.getFocalPoint()); - targetCamera.setViewUp(...sourceCamera.getViewUp()); - targetCamera.setParallelScale(sourceCamera.getParallelScale()); - - targetViewport.renderWindow.render(); - this._isUpdatingCamera = false; - }; - - // Subscribe to camera modified events - const cameraA = this.viewportA.renderer.getActiveCamera(); - const cameraB = this.viewportB.renderer.getActiveCamera(); - - cameraA.onModified(() => { - if (this.syncCameras) { - syncCamera(this.viewportA, this.viewportB); - } - }); - - cameraB.onModified(() => { - if (this.syncCameras) { - syncCamera(this.viewportB, this.viewportA); - } - }); - } - - /** - * Setup resize handlers for both viewports - * @private - */ - _setupResizeHandlers() { - const handleResize = (viewport) => { - const { width, height } = viewport.container.getBoundingClientRect(); - if (width > 0 && height > 0) { - viewport.openGLRenderWindow.setSize(width, height); - viewport.renderWindow.render(); - } - }; - - this._resizeObserverA = new ResizeObserver(() => handleResize(this.viewportA)); - this._resizeObserverB = new ResizeObserver(() => handleResize(this.viewportB)); - - this._resizeObserverA.observe(this.viewportA.container); - this._resizeObserverB.observe(this.viewportB.container); - } - - /** - * Setup message listener - * @private - */ - _setupMessageListener() { - this._messageCleanup = createMessageListener((data) => { - switch (data.type) { - case 'loadMeshA': - this.loadMesh('A', data.url); - break; - case 'loadMeshB': - this.loadMesh('B', data.url); - break; - case 'loadMesh': - // Load to both or specified viewport - if (data.viewport) { - this.loadMesh(data.viewport, data.url); - } else { - this.loadMesh('A', data.url); - } - break; - case 'setLayout': - this.setLayout(data.layout); - break; - case 'setCameraSync': - this.setCameraSync(data.enabled); - break; - case 'setField': - this.applyField(data.field, data.options); - break; - case 'screenshot': - this.captureScreenshot(); - break; - } - }); - } - - /** - * Load mesh into a viewport - * @param {string} viewport - 'A' or 'B' - * @param {string} url - Mesh URL - * @returns {Promise} - */ - async loadMesh(viewport, url) { - const vp = viewport === 'A' ? this.viewportA : this.viewportB; - const defaultColor = viewport === 'A' - ? this.config.defaultMeshColorA - : this.config.defaultMeshColorB; - - try { - // Clear existing actors - vp.actors.forEach(actor => vp.renderer.removeActor(actor)); - vp.actors = []; - - // Load mesh - const result = await loadMesh(url, this.vtk, { - renderer: vp.renderer - }); - - vp.actors = result.actors; - vp.polyData = result.polyData; - vp.filename = extractFilename(url); - - // Add actors to renderer - result.actors.forEach(actor => { - vp.renderer.addActor(actor); - }); - - // Configure actors based on format - if (result.hasTexture && this.config.enableTextures) { - // Apply texture fixes for GLTF/GLB - vp.textureManager.applyTextureFixesMultiple(result.actors); - vp.textureManager.configureRenderer(vp.renderer); - } else if (result.hasVertexColors) { - // Mesh has vertex colors - don't override with default color - // Just configure edges if needed, colors are already set up by loader - result.actors.forEach(actor => { - const property = actor.getProperty(); - if (property && this.config.showEdges) { - property.setEdgeVisibility(true); - } - }); - } else { - // Standard actor configuration - apply default color - this.actorManager.configureActors(result.actors, { - color: defaultColor, - showEdges: this.config.showEdges - }); - } - - // Position camera - this._positionCameraForViewport(vp); - - // Render - vp.renderWindow.render(); - - // Notify parent - sendMeshLoaded({ - viewport, - filename: vp.filename, - format: result.format, - hasTexture: result.hasTexture || false, - hasVertexColors: result.hasVertexColors || false - }); - - return result; - - } catch (error) { - console.error(`[DualViewer] Error loading mesh ${viewport}:`, error); - sendError(`Failed to load mesh ${viewport}: ${error.message}`); - throw error; - } - } - - /** - * Position camera for a viewport - * @private - */ - _positionCameraForViewport(viewport) { - if (viewport.actors.length === 0) return; - - let bounds = viewport.actors[0].getBounds(); - for (let i = 1; i < viewport.actors.length; i++) { - bounds = combineBounds(bounds, viewport.actors[i].getBounds()); - } - - if (isValidBounds(bounds)) { - viewport.cameraController.positionInitialCamera(bounds); - } - } - - /** - * Apply field visualization to both viewports - * @param {string} fieldName - Field name - * @param {Object} options - Options including colormap, range - */ - applyField(fieldName, options = {}) { - if (!this.config.enableFields) return; - - // Calculate synchronized range if enabled - let range = options.range; - if (this.syncFields && !range) { - const polydatas = [ - this.viewportA.polyData, - this.viewportB.polyData - ].filter(Boolean); - - if (polydatas.length > 0) { - range = this.fieldVisualization.calculateSynchronizedRange(polydatas, fieldName); - } - } - - // Apply to viewport A - if (this.viewportA.polyData && this.viewportA.actors.length > 0) { - const mapperA = this.viewportA.actors[0].getMapper(); - this.fieldVisualization.applyField(mapperA, this.viewportA.polyData, fieldName, { - ...options, - range, - render: false - }); - } - - // Apply to viewport B - if (this.viewportB.polyData && this.viewportB.actors.length > 0) { - const mapperB = this.viewportB.actors[0].getMapper(); - // Need a separate FieldVisualization for viewport B's render window - const fieldVizB = new FieldVisualization(this.vtk, this.viewportB.renderWindow); - fieldVizB.applyField(mapperB, this.viewportB.polyData, fieldName, { - ...options, - range, - render: false - }); - } - - // Render both - this.render(); - } - - /** - * Set layout mode - * @param {string} layout - LayoutMode value - */ - setLayout(layout) { - this.layout = layout; - // Layout changes are handled by CSS classes on the containers - // This method just updates the state; the parent should update CSS - console.log(`[DualViewer] Layout set to: ${layout}`); - } - - /** - * Set camera sync enabled - * @param {boolean} enabled - */ - setCameraSync(enabled) { - this.syncCameras = enabled; - console.log(`[DualViewer] Camera sync: ${enabled}`); - } - - /** - * Set field range sync enabled - * @param {boolean} enabled - */ - setFieldSync(enabled) { - this.syncFields = enabled; - } - - /** - * Set camera view for both viewports - * @param {string} direction - '+X', '-X', etc. - */ - setCameraView(direction) { - // Get combined bounds from both viewports - const allActors = [...this.viewportA.actors, ...this.viewportB.actors]; - if (allActors.length === 0) return; - - let bounds = allActors[0].getBounds(); - for (let i = 1; i < allActors.length; i++) { - bounds = combineBounds(bounds, allActors[i].getBounds()); - } - - // Apply to viewport A (will sync to B if enabled) - this.viewportA.cameraController.setCameraView(direction, bounds); - - // If sync is disabled, also update B - if (!this.syncCameras) { - this.viewportB.cameraController.setCameraView(direction, bounds); - } - - this.render(); - } - - /** - * Capture screenshot of both viewports - * @returns {Promise} - */ - async captureScreenshot() { - const screenshotA = new ScreenshotCapture(this.viewportA.renderWindow); - const screenshotB = new ScreenshotCapture(this.viewportB.renderWindow); - - const [imageA, imageB] = await Promise.all([ - screenshotA.capture(), - screenshotB.capture() - ]); - - // Send combined screenshot info - window.parent?.postMessage({ - type: 'screenshot', - data: { - viewportA: imageA, - viewportB: imageB - } - }, '*'); - - return { viewportA: imageA, viewportB: imageB }; - } - - /** - * Render both viewports - */ - render() { - this.viewportA.renderWindow.render(); - this.viewportB.renderWindow.render(); - } - - /** - * Get viewport - * @param {string} id - 'A' or 'B' - * @returns {Object} - */ - getViewport(id) { - return id === 'A' ? this.viewportA : this.viewportB; - } - - /** - * Cleanup and destroy - */ - destroy() { - if (this._messageCleanup) { - this._messageCleanup(); - } - - if (this._resizeObserverA) { - this._resizeObserverA.disconnect(); - } - if (this._resizeObserverB) { - this._resizeObserverB.disconnect(); - } - - // Destroy viewports - [this.viewportA, this.viewportB].forEach(vp => { - if (!vp) return; - vp.actors.forEach(actor => vp.renderer.removeActor(actor)); - vp.interactor.unbindEvents(); - vp.openGLRenderWindow.delete(); - vp.renderWindow.delete(); - }); - - console.log('[DualViewer] Destroyed'); - } -} - -/** - * Create a DualViewer instance - * @param {HTMLElement} containerA - * @param {HTMLElement} containerB - * @param {Object} config - * @returns {DualViewer} - */ -export function createDualViewer(containerA, containerB, config = {}) { - const viewer = new DualViewer(containerA, containerB, config); - viewer.initialize(); - return viewer; -} - -export { LayoutMode as DualLayoutMode }; -export default DualViewer; diff --git a/javascript/js/viewer/viewers/StandardViewer.js b/javascript/js/viewer/viewers/StandardViewer.js deleted file mode 100644 index b8df80a2..00000000 --- a/javascript/js/viewer/viewers/StandardViewer.js +++ /dev/null @@ -1,241 +0,0 @@ -/** - * StandardViewer - VTK viewer for standard mesh preview - * - * Supports STL, OBJ, VTP formats with: - * - Scalar field visualization - * - Colormap selection - * - Settings panel (color, edges, lighting) - * - Screenshot capture - */ - -import { BaseViewer } from '../core/BaseViewer.js'; -import { SettingTypes } from '../ui/SettingsPanel.js'; -import { ControlTypes, StandardControls } from '../ui/ControlsBar.js'; - -/** - * Standard viewer configuration - */ -const STANDARD_CONFIG = { - // Camera - cameraDistanceMultiplier: 2.5, - - // Features - enableFields: true, - enableTextures: false, - enableScreenshot: true, - enableAxisIndicator: false, - enableSettings: true, - enableControls: true, - - // Appearance - backgroundColor: [0.15, 0.15, 0.15], - defaultMeshColor: [0.6, 0.8, 1.0], - showEdges: false -}; - -export class StandardViewer extends BaseViewer { - /** - * Create a StandardViewer - * @param {HTMLElement} container - Container element - * @param {Object} config - Additional configuration - */ - constructor(container, config = {}) { - // Merge with standard config - const mergedConfig = { - ...STANDARD_CONFIG, - ...config, - // Settings panel configuration - settingsConfig: { - title: 'Settings', - fields: [ - { - type: SettingTypes.COLOR, - id: 'meshColor', - label: 'Mesh Color', - defaultValue: config.defaultMeshColor || STANDARD_CONFIG.defaultMeshColor - }, - { - type: SettingTypes.CHECKBOX, - id: 'showEdges', - label: 'Show Edges', - defaultValue: config.showEdges || STANDARD_CONFIG.showEdges - }, - { - type: SettingTypes.COLOR, - id: 'edgeColor', - label: 'Edge Color', - defaultValue: config.edgeColor || [0.2, 0.2, 0.2] - }, - { - type: SettingTypes.COLOR, - id: 'backgroundColor', - label: 'Background', - defaultValue: config.backgroundColor || STANDARD_CONFIG.backgroundColor - } - ] - }, - // Controls bar configuration - controlsConfig: { - controls: [ - StandardControls.CAMERA_VIEWS, - StandardControls.SCREENSHOT, - StandardControls.SETTINGS - ] - } - }; - - super(container, mergedConfig); - - // Field visualization state - this.currentField = null; - this.currentColormap = 'erdc_rainbow_bright'; - this.availableFields = []; - } - - /** - * Load mesh and extract field information - * @param {string} url - Mesh file URL - * @returns {Promise} - */ - async loadMesh(url) { - const result = await super.loadMesh(url); - - // Extract field information for VTP files - if (this.fieldVisualization && this.polyDatas.length > 0) { - const fieldInfo = this.fieldVisualization.extractFields(this.polyDatas[0]); - this.availableFields = fieldInfo.allFields; - - // Notify parent about available fields - if (this.availableFields.length > 0) { - window.parent?.postMessage({ - type: 'fieldsAvailable', - fields: this.availableFields.map(f => ({ - name: f.fullName, - displayName: f.displayName, - type: f.type, - range: f.range - })) - }, '*'); - } - } - - return result; - } - - /** - * Apply scalar field visualization - * @param {string} fieldName - Field name (e.g., 'point:temperature') - * @param {Object} options - Visualization options - */ - applyField(fieldName, options = {}) { - if (!this.fieldVisualization || this.polyDatas.length === 0) { - console.warn('[StandardViewer] Field visualization not available'); - return; - } - - const mapper = this.actors[0]?.getMapper(); - if (!mapper) return; - - this.fieldVisualization.applyField( - mapper, - this.polyDatas[0], - fieldName, - { - colormap: options.colormap || this.currentColormap, - range: options.range, - render: true - } - ); - - this.currentField = fieldName; - if (options.colormap) { - this.currentColormap = options.colormap; - } - } - - /** - * Set colormap - * @param {string} colormap - Colormap name - */ - setColormap(colormap) { - this.currentColormap = colormap; - if (this.currentField) { - this.applyField(this.currentField, { colormap }); - } - } - - /** - * Clear field visualization (show solid color) - */ - clearField() { - if (this.actors.length > 0) { - const mapper = this.actors[0].getMapper(); - if (mapper) { - this.fieldVisualization?.disableScalarVisualization(mapper, true); - } - } - this.currentField = null; - } - - /** - * Get available colormap options - * @returns {Array} - */ - getColormapOptions() { - return this.fieldVisualization?.getColormapOptions() || []; - } - - /** - * Get available fields - * @returns {Array} - */ - getAvailableFields() { - return this.availableFields; - } - - /** - * Setup message listener with field support - * @private - * @override - */ - _setupMessageListener() { - super._setupMessageListener(); - - // Add field-specific message handling - window.addEventListener('message', (event) => { - const data = event.data; - if (!data || !data.type) return; - - switch (data.type) { - case 'setField': - this.applyField(data.field, { - colormap: data.colormap, - range: data.range - }); - break; - - case 'setColormap': - this.setColormap(data.colormap); - break; - - case 'clearField': - this.clearField(); - break; - } - }); - } -} - -/** - * Create a StandardViewer instance - * @param {HTMLElement} container - * @param {Object} config - * @returns {StandardViewer} - */ -export function createStandardViewer(container, config = {}) { - const viewer = new StandardViewer(container, config); - viewer.initialize(); - return viewer; -} - -export default StandardViewer; diff --git a/javascript/js/viewer/viewers/TexturedViewer.js b/javascript/js/viewer/viewers/TexturedViewer.js deleted file mode 100644 index 84441cc0..00000000 --- a/javascript/js/viewer/viewers/TexturedViewer.js +++ /dev/null @@ -1,161 +0,0 @@ -/** - * TexturedViewer - VTK viewer for textured GLTF/GLB meshes - * - * Specialized for displaying textured 3D models with proper: - * - Material handling (metallic=0, roughness=1) - * - Texture extraction and application - * - Two-sided lighting - * - Closer camera distance for better texture viewing - */ - -import { BaseViewer } from '../core/BaseViewer.js'; -import { SettingTypes } from '../ui/SettingsPanel.js'; -import { StandardControls } from '../ui/ControlsBar.js'; - -/** - * Textured viewer configuration - */ -const TEXTURED_CONFIG = { - // Camera - closer for texture detail - cameraDistanceMultiplier: 2.0, - - // Lighting - twoSidedLighting: true, - - // Features - enableFields: false, - enableTextures: true, - enableScreenshot: true, - enableAxisIndicator: false, - enableSettings: true, - enableControls: true, - - // Appearance - backgroundColor: [0.15, 0.15, 0.15], - defaultMeshColor: [1.0, 1.0, 1.0], - showEdges: false -}; - -export class TexturedViewer extends BaseViewer { - /** - * Create a TexturedViewer - * @param {HTMLElement} container - Container element - * @param {Object} config - Additional configuration - */ - constructor(container, config = {}) { - // Merge with textured config - const mergedConfig = { - ...TEXTURED_CONFIG, - ...config, - // Settings panel configuration - settingsConfig: { - title: 'Settings', - fields: [ - { - type: SettingTypes.COLOR, - id: 'backgroundColor', - label: 'Background', - defaultValue: config.backgroundColor || TEXTURED_CONFIG.backgroundColor - }, - { - type: SettingTypes.CHECKBOX, - id: 'showEdges', - label: 'Show Edges', - defaultValue: false - } - ] - }, - // Controls bar configuration - controlsConfig: { - controls: [ - StandardControls.CAMERA_VIEWS, - StandardControls.SCREENSHOT, - StandardControls.SETTINGS - ] - } - }; - - super(container, mergedConfig); - - // Texture state - this.textureInfo = null; - } - - /** - * Configure loaded actors for textured display - * @private - * @override - */ - _configureLoadedActors(result) { - if (this.textureManager && result.actors.length > 0) { - // Apply texture fixes to all actors - this.textureInfo = this.textureManager.applyTextureFixesMultiple(result.actors); - - // Configure renderer for texture display - this.textureManager.configureRenderer(this.renderer); - - console.log('[TexturedViewer] Texture info:', this.textureInfo); - - // If no textures found, apply default white color - if (!this.textureInfo.hasTexture) { - result.actors.forEach(actor => { - this.textureManager.setActorColor(actor, this.config.defaultMeshColor); - }); - } - } - } - - /** - * Update info overlay with texture information - * @private - * @override - */ - _updateInfoOverlay(result) { - if (!this.infoOverlay) return; - - const lines = [`File: ${this.currentFilename}`]; - - if (result.actors) { - lines.push(`Actors: ${result.actors.length}`); - } - - if (this.textureInfo) { - lines.push(`Textured: ${this.textureInfo.hasTexture ? 'Yes' : 'No'}`); - if (this.textureInfo.texturedActors > 0) { - lines.push(`Textured actors: ${this.textureInfo.texturedActors}`); - } - } - - this.infoOverlay.innerHTML = lines.join('
'); - } - - /** - * Get texture information - * @returns {Object|null} - */ - getTextureInfo() { - return this.textureInfo; - } - - /** - * Check if current mesh has textures - * @returns {boolean} - */ - hasTextures() { - return this.textureInfo?.hasTexture || false; - } -} - -/** - * Create a TexturedViewer instance - * @param {HTMLElement} container - * @param {Object} config - * @returns {TexturedViewer} - */ -export function createTexturedViewer(container, config = {}) { - const viewer = new TexturedViewer(container, config); - viewer.initialize(); - return viewer; -} - -export default TexturedViewer; diff --git a/javascript/js/warp_mesh_field_select.js b/javascript/js/warp_mesh_field_select.js index 3b9cc3d7..d051e0cc 100644 --- a/javascript/js/warp_mesh_field_select.js +++ b/javascript/js/warp_mesh_field_select.js @@ -10,7 +10,7 @@ import { app } from "../../../scripts/app.js"; app.registerExtension({ - name: "geompack.warp_mesh_field_select", + name: "geometrypack.warp_mesh_field_select", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name !== "GeomPackWarpMesh") return; diff --git a/javascript/viewer_bvh.html b/javascript/viewer_bvh.html deleted file mode 100644 index 121c140b..00000000 --- a/javascript/viewer_bvh.html +++ /dev/null @@ -1,277 +0,0 @@ - - - - - - - -
Loading BVH animation...
- -
- - - - - - diff --git a/javascript/viewer_compare_smpl_bvh.html b/javascript/viewer_compare_smpl_bvh.html deleted file mode 100644 index ad055edd..00000000 --- a/javascript/viewer_compare_smpl_bvh.html +++ /dev/null @@ -1,354 +0,0 @@ - - - - - - - -
-
-
Waiting for data...
-
SMPL (Mesh)
-
BVH (Skeleton)
-
-
- - -
0 / 0
-
-
- - - - - - diff --git a/javascript/viewer_fbx.html b/javascript/viewer_fbx.html deleted file mode 100644 index 363b71b6..00000000 --- a/javascript/viewer_fbx.html +++ /dev/null @@ -1,1123 +0,0 @@ - - - - - FBX Viewer - - - -
- -
Ready to load rigged mesh
-
- -
-
FBX Viewer
- -
- - - -
- -
- - -
- -
- - - -
- -
- - - -
- -
No mesh loaded
-
- - - - - diff --git a/javascript/viewer_fbx_animation.html b/javascript/viewer_fbx_animation.html deleted file mode 100644 index 6acfc5c0..00000000 --- a/javascript/viewer_fbx_animation.html +++ /dev/null @@ -1,316 +0,0 @@ - - - - - - - -
Loading animated FBX...
-
- - - - - - diff --git a/javascript/viewer_fbx_compare.html b/javascript/viewer_fbx_compare.html deleted file mode 100644 index 2687fc99..00000000 --- a/javascript/viewer_fbx_compare.html +++ /dev/null @@ -1,1007 +0,0 @@ - - - - - FBX Compare Viewer - Side-by-Side Skeleton Comparison - - - -
-
-
Left Skeleton
- -
Ready to load skeletons
-
-
-
Right Skeleton
- -
-
- - -
-

Bone Comparison

-
-
Bone
-
-
-
-
-
Source
-
-
-
-
-
Left Position
-
-
-
-
-
Right Position
-
-
-
-
-
Left Rotation
-
-
-
-
-
Right Rotation
-
-
-
-
-
-
- -
-
FBX Compare Viewer - Side-by-Side Skeleton Comparison
- -
- - - - - - -
- -
-
- - - 1.0x -
-
- - - 1.0x -
- -
- -
No skeletons loaded
-
- - - - diff --git a/javascript/viewer_fbx_debug.html b/javascript/viewer_fbx_debug.html deleted file mode 100644 index 24e96179..00000000 --- a/javascript/viewer_fbx_debug.html +++ /dev/null @@ -1,1936 +0,0 @@ - - - - - FBX Debug Viewer - Bone Roll & Animation - - - -
- -
Ready to load rigged mesh
-
- - -
-

Bone Info

-
-
Name
-
-
-
Parent
-
-
-
Index
-
-
-
-
-
Roll Angle (Z-rotation)
-
-
-
-
-
Local Position
-
- X: - - Y: - - Z: - -
-
Local Rotation (Euler XYZ)
-
- X: - - Y: - - Z: - -
-
Local Quaternion
-
-
-
Local Scale
-
-
-
-
-
World Position
-
- X: - - Y: - - Z: - -
-
World Rotation (Euler XYZ)
-
- X: - - Y: - - Z: - -
-
-
-
Bone Length
-
-
-
Children
-
-
-
-
- -
-
FBX Debug Viewer - Bone Roll & Animation Debugging
- - -
-
Animation Playback
-
- -
-
-
- - - - - -
- - 0.00s / 0.00s -
-
-
- - - 1.0x -
- -
-
- - -
-
Visualization
-
- - - - - -
-
- - -
-
Size Controls
-
-
- - - 1.0x -
-
- - - 1.0x -
-
- - - 1.0x -
-
-
- - -
-
Bone Filter
-
- - 0/0 bones -
-
- - -
-
Scene
-
- - - -
-
- - -
- - - - - -
- -
No mesh loaded
-
- - - -