ComfyUI auto-imports every web/**/*.js into the shared main page; the four big bundles (three, vtk-gltf, viewer-bundle-vtk, gsplat) only ever run inside the viewer iframes but were getting pulled into the main document too, where they wrote window.THREE / window.vtk / globalThis.* and could collide with any other 3D pack. The **/*.js glob only matches .js, so renaming to .mjs drops them from the auto-import while the iframes still load them via classic <script src> (mime resolves to text/javascript). window.* pollution from these bundles is gone from the main realm.
462 lines
18 KiB
HTML
462 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>GeomPack VTK.js Textured Viewer (Unified)</title>
|
|
<link rel="stylesheet" href="./js/viewer/ui/styles.css">
|
|
<style>
|
|
/* Minimal overrides for textured viewer */
|
|
#infoPanel {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
color: #ccc;
|
|
font-size: 11px;
|
|
z-index: 100;
|
|
}
|
|
#infoPanel div {
|
|
margin: 2px 0;
|
|
}
|
|
.info-label { color: #888; }
|
|
.info-value { color: #4fc3f7; }
|
|
.info-warning { color: #ffb74d; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="viewerContainer">
|
|
<div id="container"></div>
|
|
<div id="loading" class="loading">
|
|
<div class="spinner"></div>
|
|
<div class="loading-text">Ready to load textured mesh</div>
|
|
</div>
|
|
|
|
<!-- Info panel for texture/material status -->
|
|
<div id="infoPanel" style="display: none;">
|
|
<div><span class="info-label">Visual:</span> <span id="infoVisual" class="info-value">-</span></div>
|
|
<div><span class="info-label">Texture:</span> <span id="infoTexture" class="info-value">-</span></div>
|
|
<div><span class="info-label">Materials:</span> <span id="infoMaterial" class="info-value">-</span></div>
|
|
<div id="warningRow" style="display: none;"><span class="info-warning">PBR requires WebGPU</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="controls">
|
|
<!-- Settings button -->
|
|
<button id="settingsButton" class="settings-button" title="Appearance Settings">[Settings]</button>
|
|
|
|
<span class="control-separator"></span>
|
|
|
|
<label class="control-label">
|
|
<input type="checkbox" id="showEdges"> Edges
|
|
</label>
|
|
|
|
<span class="control-separator"></span>
|
|
|
|
<!-- Camera view buttons -->
|
|
<button class="view-button" data-view="+X">+X</button>
|
|
<button class="view-button" data-view="-X">-X</button>
|
|
<button class="view-button" data-view="+Y">+Y</button>
|
|
<button class="view-button" data-view="-Y">-Y</button>
|
|
<button class="view-button" data-view="+Z">+Z</button>
|
|
<button class="view-button" data-view="-Z">-Z</button>
|
|
|
|
<span class="control-separator"></span>
|
|
|
|
<button id="screenshot">Screenshot</button>
|
|
<button id="saveMesh" class="save-button">Save GLB</button>
|
|
</div>
|
|
|
|
<!-- Settings overlay and panel -->
|
|
<div id="settingsOverlay" class="settings-overlay"></div>
|
|
<div id="settingsPanel" class="settings-panel">
|
|
<h3>Appearance Settings</h3>
|
|
<div class="setting-row">
|
|
<label>Opacity:</label>
|
|
<input type="number" id="opacity" min="0" max="1" step="0.1" value="1.0">
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Render Mode:</label>
|
|
<select id="representationSettings">
|
|
<option value="2">Surface</option>
|
|
<option value="1">Wireframe</option>
|
|
<option value="0">Points</option>
|
|
</select>
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>
|
|
<input type="checkbox" id="parallelProjection"> Parallel Projection
|
|
</label>
|
|
</div>
|
|
<div class="button-row">
|
|
<button id="applySettings">Apply</button>
|
|
<button id="cancelSettings">Cancel</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Load VTK.js bundle with GLTFImporter support -->
|
|
<script src="./js/vtk-gltf.mjs"></script>
|
|
<!-- Load unified viewer bundle -->
|
|
<script src="./js/viewer-bundle-vtk.mjs"></script>
|
|
|
|
<script>
|
|
// Use the unified viewer module
|
|
const {
|
|
CameraController,
|
|
ActorManager,
|
|
TextureManager,
|
|
ScreenshotCapture,
|
|
AxisIndicator,
|
|
loadMesh,
|
|
detectFormat,
|
|
sendMeshLoaded,
|
|
sendError,
|
|
extractFilename,
|
|
getCenter,
|
|
getMaxDimension,
|
|
isValidBounds,
|
|
rgbToString
|
|
} = window.GeomPackViewer;
|
|
|
|
// DOM elements
|
|
const loading = document.getElementById('loading');
|
|
const loadingText = loading.querySelector('.loading-text');
|
|
const container = document.getElementById('container');
|
|
const infoPanel = document.getElementById('infoPanel');
|
|
|
|
// VTK setup
|
|
const vtkRenderWindow = vtk.Rendering.Core.vtkRenderWindow;
|
|
const vtkRenderer = vtk.Rendering.Core.vtkRenderer;
|
|
const vtkOpenGLRenderWindow = vtk.Rendering.OpenGL.vtkRenderWindow;
|
|
const vtkRenderWindowInteractor = vtk.Rendering.Core.vtkRenderWindowInteractor;
|
|
const vtkInteractorStyleTrackballCamera = vtk.Interaction.Style.vtkInteractorStyleTrackballCamera;
|
|
|
|
// Create render window
|
|
const renderWindow = vtkRenderWindow.newInstance();
|
|
const renderer = vtkRenderer.newInstance();
|
|
renderer.setBackground(0.16, 0.16, 0.16);
|
|
renderer.setTwoSidedLighting(true);
|
|
renderer.setUseShadows(false);
|
|
renderWindow.addRenderer(renderer);
|
|
|
|
// Create OpenGL view
|
|
const openGLRenderWindow = vtkOpenGLRenderWindow.newInstance();
|
|
openGLRenderWindow.setContainer(container);
|
|
renderWindow.addView(openGLRenderWindow);
|
|
|
|
// Resize to container
|
|
const { width, height } = container.getBoundingClientRect();
|
|
openGLRenderWindow.setSize(width, height);
|
|
|
|
// Create interactor
|
|
const interactor = vtkRenderWindowInteractor.newInstance();
|
|
interactor.setView(openGLRenderWindow);
|
|
interactor.initialize();
|
|
interactor.bindEvents(container);
|
|
|
|
const interactorStyle = vtkInteractorStyleTrackballCamera.newInstance();
|
|
interactor.setInteractorStyle(interactorStyle);
|
|
|
|
// Initialize modules
|
|
const cameraController = new CameraController(renderer, renderWindow, {
|
|
distanceMultiplier: 2.0 // Closer for texture detail
|
|
});
|
|
|
|
const actorManager = new ActorManager(renderWindow);
|
|
|
|
const textureManager = new TextureManager(vtk, renderWindow);
|
|
const screenshotCapture = new ScreenshotCapture(renderWindow);
|
|
|
|
// Create axis indicator
|
|
const axisIndicator = new AxisIndicator(vtk, interactor, {
|
|
enabled: true,
|
|
corner: 'BOTTOM_LEFT'
|
|
});
|
|
|
|
// State
|
|
let currentActors = [];
|
|
let meshBounds = null;
|
|
let currentFilename = null;
|
|
|
|
// Check GLTF support
|
|
const gltfSupported = vtk._gltfAvailable && vtk.IO.Geometry.vtkGLTFImporter !== null;
|
|
|
|
// Show loading
|
|
function showLoading(show, text = 'Loading textured mesh...') {
|
|
loading.style.display = show ? 'flex' : 'none';
|
|
loadingText.textContent = text;
|
|
}
|
|
|
|
// Send camera state to parent node widget
|
|
let _cameraWheelTimer = null;
|
|
function sendCameraState() {
|
|
const cam = renderer.getActiveCamera();
|
|
window.parent.postMessage({
|
|
type: 'WIDGET_UPDATE',
|
|
widget: 'camera_state',
|
|
value: JSON.stringify({
|
|
position: cam.getPosition(),
|
|
focalPoint: cam.getFocalPoint(),
|
|
viewUp: cam.getViewUp()
|
|
})
|
|
}, '*');
|
|
}
|
|
|
|
function applyCameraState(cameraState) {
|
|
if (!cameraState) return false;
|
|
try {
|
|
const cs = typeof cameraState === 'string' ? JSON.parse(cameraState) : cameraState;
|
|
if (!cs.position || !cs.focalPoint || !cs.viewUp) return false;
|
|
const cam = renderer.getActiveCamera();
|
|
cam.setPosition(...cs.position);
|
|
cam.setFocalPoint(...cs.focalPoint);
|
|
cam.setViewUp(...cs.viewUp);
|
|
renderer.resetCameraClippingRange();
|
|
return true;
|
|
} catch(e) { return false; }
|
|
}
|
|
|
|
interactor.onEndAnimation(() => sendCameraState());
|
|
container.addEventListener('wheel', () => {
|
|
clearTimeout(_cameraWheelTimer);
|
|
_cameraWheelTimer = setTimeout(sendCameraState, 150);
|
|
});
|
|
|
|
let _pendingCameraState = null;
|
|
|
|
// Load mesh function
|
|
async function loadTexturedMesh(filepath, metadata = {}) {
|
|
showLoading(true, 'Loading textured mesh...');
|
|
|
|
// Extract filename
|
|
const urlParams = new URLSearchParams(filepath.split('?')[1] || '');
|
|
currentFilename = urlParams.get('filename') || filepath.split('/').pop().split('?')[0];
|
|
|
|
// Update info panel
|
|
if (metadata.visual_kind) {
|
|
document.getElementById('infoVisual').textContent = metadata.visual_kind;
|
|
}
|
|
if (metadata.has_texture !== undefined) {
|
|
document.getElementById('infoTexture').textContent = metadata.has_texture ? 'Yes' : 'No';
|
|
}
|
|
if (metadata.has_material !== undefined) {
|
|
document.getElementById('infoMaterial').textContent = metadata.has_material ? 'Yes' : 'No';
|
|
}
|
|
infoPanel.style.display = 'block';
|
|
|
|
// Show warning if textures but no GLTF support
|
|
if (!gltfSupported && metadata.has_texture) {
|
|
document.getElementById('warningRow').style.display = 'block';
|
|
}
|
|
|
|
try {
|
|
// Clear previous actors
|
|
currentActors.forEach(actor => renderer.removeActor(actor));
|
|
currentActors = [];
|
|
|
|
// Load mesh using unified loader
|
|
const result = await loadMesh(filepath, vtk, {
|
|
renderer: renderer,
|
|
withTextures: true,
|
|
twoSidedLighting: true
|
|
});
|
|
|
|
currentActors = result.actors;
|
|
|
|
// Add actors to renderer (if not already added by GLTF loader)
|
|
result.actors.forEach(actor => {
|
|
if (!renderer.getActors().includes(actor)) {
|
|
renderer.addActor(actor);
|
|
}
|
|
});
|
|
|
|
// Apply texture fixes only for textured meshes (not vertex-colored ones)
|
|
if (result.hasTexture) {
|
|
textureManager.applyTextureFixesMultiple(result.actors);
|
|
textureManager.configureRenderer(renderer);
|
|
} else if (result.hasVertexColors) {
|
|
// For vertex-colored meshes, just configure renderer
|
|
textureManager.configureRenderer(renderer);
|
|
}
|
|
|
|
// Calculate bounds
|
|
if (result.actors.length > 0) {
|
|
meshBounds = result.actors[0].getBounds();
|
|
for (let i = 1; i < result.actors.length; i++) {
|
|
const b = result.actors[i].getBounds();
|
|
meshBounds = [
|
|
Math.min(meshBounds[0], b[0]),
|
|
Math.max(meshBounds[1], b[1]),
|
|
Math.min(meshBounds[2], b[2]),
|
|
Math.max(meshBounds[3], b[3]),
|
|
Math.min(meshBounds[4], b[4]),
|
|
Math.max(meshBounds[5], b[5])
|
|
];
|
|
}
|
|
}
|
|
|
|
// Position camera (restore saved state or auto-position)
|
|
if (!applyCameraState(_pendingCameraState)) {
|
|
renderer.resetCamera();
|
|
}
|
|
renderWindow.render();
|
|
|
|
// Second render for proper texture display
|
|
setTimeout(() => {
|
|
if (!_pendingCameraState) renderer.resetCamera();
|
|
_pendingCameraState = null;
|
|
renderWindow.render();
|
|
}, 100);
|
|
|
|
showLoading(false);
|
|
|
|
// Notify parent
|
|
sendMeshLoaded({
|
|
filename: currentFilename,
|
|
format: result.format,
|
|
hasTexture: result.hasTexture || false,
|
|
actors: result.actors.length
|
|
});
|
|
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('[GeomPack VTK Textured] Load failed:', error);
|
|
showLoading(true, 'Failed to load mesh: ' + error.message);
|
|
sendError(error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Camera view function
|
|
function setCameraView(direction) {
|
|
if (!meshBounds) return;
|
|
cameraController.setCameraView(direction, meshBounds);
|
|
renderWindow.render();
|
|
}
|
|
|
|
// Event listeners - Camera views
|
|
document.querySelectorAll('.view-button').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
setCameraView(button.dataset.view);
|
|
});
|
|
});
|
|
|
|
// Edges toggle
|
|
document.getElementById('showEdges').addEventListener('change', (e) => {
|
|
currentActors.forEach(actor => {
|
|
actor.getProperty().setEdgeVisibility(e.target.checked);
|
|
});
|
|
renderWindow.render();
|
|
window.parent.postMessage({ type: 'WIDGET_UPDATE', widget: 'show_edges', value: e.target.checked }, '*');
|
|
});
|
|
|
|
// Screenshot
|
|
document.getElementById('screenshot').addEventListener('click', async () => {
|
|
renderWindow.render();
|
|
await screenshotCapture.captureAndDownload(`screenshot_${Date.now()}.png`);
|
|
});
|
|
|
|
// Save mesh
|
|
document.getElementById('saveMesh').addEventListener('click', () => {
|
|
if (currentFilename) {
|
|
const link = document.createElement('a');
|
|
link.href = `/view/${currentFilename}`;
|
|
link.download = currentFilename;
|
|
link.click();
|
|
}
|
|
});
|
|
|
|
// Settings panel
|
|
const settingsButton = document.getElementById('settingsButton');
|
|
const settingsPanel = document.getElementById('settingsPanel');
|
|
const settingsOverlay = document.getElementById('settingsOverlay');
|
|
const applySettings = document.getElementById('applySettings');
|
|
const cancelSettings = document.getElementById('cancelSettings');
|
|
|
|
settingsButton.addEventListener('click', () => {
|
|
settingsPanel.classList.add('visible');
|
|
settingsOverlay.classList.add('visible');
|
|
});
|
|
|
|
cancelSettings.addEventListener('click', () => {
|
|
settingsPanel.classList.remove('visible');
|
|
settingsOverlay.classList.remove('visible');
|
|
});
|
|
|
|
settingsOverlay.addEventListener('click', () => {
|
|
settingsPanel.classList.remove('visible');
|
|
settingsOverlay.classList.remove('visible');
|
|
});
|
|
|
|
applySettings.addEventListener('click', () => {
|
|
const opacity = parseFloat(document.getElementById('opacity').value);
|
|
const representation = parseInt(document.getElementById('representationSettings').value);
|
|
const parallel = document.getElementById('parallelProjection').checked;
|
|
|
|
currentActors.forEach(actor => {
|
|
const property = actor.getProperty();
|
|
property.setOpacity(opacity);
|
|
property.setRepresentation(representation);
|
|
});
|
|
|
|
cameraController.setParallelProjection(parallel);
|
|
renderWindow.render();
|
|
|
|
settingsPanel.classList.remove('visible');
|
|
settingsOverlay.classList.remove('visible');
|
|
});
|
|
|
|
// Resize handling
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
const { width, height } = container.getBoundingClientRect();
|
|
if (width > 0 && height > 0) {
|
|
openGLRenderWindow.setSize(width, height);
|
|
renderWindow.render();
|
|
}
|
|
});
|
|
resizeObserver.observe(container);
|
|
|
|
// Expose for parent widget
|
|
window.loadMesh = loadTexturedMesh;
|
|
window.gltfSupported = gltfSupported;
|
|
|
|
// Listen for postMessage
|
|
window.addEventListener('message', async (event) => {
|
|
if (event.data.type === 'LOAD_MESH' && event.data.filepath) {
|
|
const filepath = event.data.filepath;
|
|
const metadata = event.data.metadata || {};
|
|
const showEdges = event.data.showEdges || false;
|
|
_pendingCameraState = event.data.cameraState || null;
|
|
|
|
const success = await loadTexturedMesh(filepath, metadata);
|
|
|
|
if (success) {
|
|
// Apply showEdges state from parent widget
|
|
const edgesCheckbox = document.getElementById('showEdges');
|
|
edgesCheckbox.checked = showEdges;
|
|
currentActors.forEach(actor => {
|
|
actor.getProperty().setEdgeVisibility(showEdges);
|
|
});
|
|
renderWindow.render();
|
|
}
|
|
|
|
if (window.parent && window.parent !== window) {
|
|
window.parent.postMessage({
|
|
type: success ? 'MESH_LOADED' : 'MESH_ERROR',
|
|
error: success ? null : 'Failed to load mesh'
|
|
}, '*');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Initial status
|
|
loadingText.textContent = gltfSupported ?
|
|
'Ready to load textured mesh (GLB/GLTF)' :
|
|
'Ready - GLTFImporter loading...';
|
|
console.log('[GeomPack VTK Textured Viewer] Loaded successfully');
|
|
</script>
|
|
</body>
|
|
</html>
|