Files
PozzettiAndrea-ComfyUI-Geom…/javascript/viewer_vtk_textured.html
T
PozzettiAndrea 414f759791 Vendor frontend JS into javascript/; drop the prestartup copy dance
The viewer JS/HTML/bundles are now committed in javascript/ (declared via
pyproject [tool.comfy] web = "javascript") instead of being copied from the
shared comfy-3d-viewers pip package at prestartup. Consequences:

- requirements.txt is now just comfy-env (host env installs nothing else,
  per the never-touch-the-host-env principle)
- prestartup_script.py no longer copies viewers or web_src; it keeps only
  setup_env() and the input-asset copies
- WEB_DIRECTORY attribute removed; the pyproject declaration is the single
  source (served at /extensions/comfyui-geometrypack/, keyed by project name)
- web_src/ deleted; its 6 widgets now live in javascript/js/ as the tracked
  source, edited directly
- viewer_pbr.html hardcoded-fallback URL updated to the new mount name

Iframe URLs auto-detect the mount from import.meta.url, so the folder rename
and mount-name change are transparent at runtime.
2026-08-12 17:37:09 +02:00

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.js"></script>
<!-- Load unified viewer bundle -->
<script src="./js/viewer-bundle-vtk.js"></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>