Files
PozzettiAndrea-ComfyUI-Geom…/web/viewer_multi.html
T
2026-02-12 22:02:40 +01:00

742 lines
28 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GeomPack VTK.js Multi Mesh Viewer</title>
<style>
body {
margin: 0;
overflow: hidden;
background: #2a2a2a;
font-family: monospace;
display: flex;
flex-direction: column;
height: 100vh;
}
#viewerContainer {
flex: 1;
position: relative;
min-height: 0;
display: grid;
gap: 2px;
padding: 2px;
box-sizing: border-box;
}
/* Grid layouts */
#viewerContainer.grid-1x1 { grid-template-columns: 1fr; grid-template-rows: 1fr; }
#viewerContainer.grid-1x2 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr; }
#viewerContainer.grid-1x3 { grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr; }
#viewerContainer.grid-2x2 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.viewport-container {
position: relative;
background: #1a1a1a;
min-height: 0;
overflow: hidden;
}
.viewport-label {
position: absolute;
top: 8px;
left: 8px;
color: #888;
font-size: 11px;
z-index: 10;
pointer-events: none;
background: rgba(0, 0, 0, 0.5);
padding: 4px 8px;
border-radius: 3px;
}
.viewport-inner {
width: 100%;
height: 100%;
}
#loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 14px;
text-align: center;
z-index: 1000;
pointer-events: none;
}
#controls {
background: #1a1a1a;
border-top: 1px solid #444;
padding: 8px 12px;
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
font-size: 12px;
color: #ccc;
min-height: 48px;
}
#controls button, #controls select {
background: #333;
color: #ccc;
border: 1px solid #555;
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
font-family: monospace;
cursor: pointer;
}
#controls button:hover { background: #444; }
#controls label {
display: flex;
align-items: center;
gap: 4px;
}
.divider {
width: 1px;
height: 20px;
background: #444;
margin: 0 8px;
}
.field-controls {
display: none;
}
.field-controls.active {
display: flex;
gap: 12px;
align-items: center;
}
</style>
</head>
<body>
<div id="viewerContainer" class="grid-1x1">
<!-- Viewports will be dynamically created -->
</div>
<div id="loading">Ready to load meshes</div>
<div id="controls">
<label>
<input type="checkbox" id="showEdges"> Edges
</label>
<span class="divider"></span>
<button id="viewPosX">+X</button>
<button id="viewNegX">-X</button>
<button id="viewPosY">+Y</button>
<button id="viewNegY">-Y</button>
<button id="viewPosZ">+Z</button>
<button id="viewNegZ">-Z</button>
<span class="divider"></span>
<button id="resetCamera">Reset Camera</button>
<!-- Field visualization controls -->
<div class="field-controls" id="fieldControls">
<span class="divider"></span>
<label>
Field:
<select id="fieldSelector">
<option value="none">None</option>
</select>
</label>
<label>
Colormap:
<select id="colormapSelector"></select>
</label>
</div>
</div>
<script src="./js/vtk-gltf.js"></script>
<script>
// Get VTK.js classes
const vtkFullScreenRenderWindow = vtk.Rendering.Misc.vtkFullScreenRenderWindow;
const vtkActor = vtk.Rendering.Core.vtkActor;
const vtkMapper = vtk.Rendering.Core.vtkMapper;
const vtkSTLReader = vtk.IO.Geometry.vtkSTLReader;
const vtkOBJReader = vtk.IO.Misc.vtkOBJReader;
const vtkXMLPolyDataReader = vtk.IO.XML.vtkXMLPolyDataReader;
const vtkPolyDataNormals = vtk.Filters.Core.vtkPolyDataNormals;
const vtkInteractorStyleTrackballCamera = vtk.Interaction.Style.vtkInteractorStyleTrackballCamera;
const vtkGLTFImporter = vtk.IO.Geometry.vtkGLTFImporter;
const vtkColorTransferFunction = vtk.Rendering.Core.vtkColorTransferFunction;
const vtkColorMaps = vtk.Rendering.Core.vtkColorTransferFunction.vtkColorMaps;
console.log('[GeomPack VTK Multi Viewer] Initializing...');
const viewerContainer = document.getElementById('viewerContainer');
const loading = document.getElementById('loading');
// Viewer state
let viewports = []; // Array of {container, fullScreenRenderer, renderer, renderWindow, interactor, actor, mapper, polydata, bounds, fieldNames}
let combinedBounds = null;
let numMeshes = 0;
let commonFields = []; // Fields common to all meshes
// Helper function to render with proper clipping range update
function renderWithClippingRangeUpdate(renderer, renderWindow) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
renderer.resetCameraClippingRange();
renderWindow.render();
});
});
}
// Create viewport containers dynamically
function createViewportContainers(count) {
// Clear existing
viewerContainer.innerHTML = '';
viewports = [];
// Set grid class
viewerContainer.className = '';
if (count === 1) {
viewerContainer.classList.add('grid-1x1');
} else if (count === 2) {
viewerContainer.classList.add('grid-1x2');
} else if (count === 3) {
viewerContainer.classList.add('grid-1x3');
} else {
viewerContainer.classList.add('grid-2x2');
}
// Create containers
for (let i = 0; i < count; i++) {
const container = document.createElement('div');
container.className = 'viewport-container';
container.id = `viewport-${i}`;
const label = document.createElement('div');
label.className = 'viewport-label';
label.textContent = `Mesh ${i + 1}`;
container.appendChild(label);
const inner = document.createElement('div');
inner.className = 'viewport-inner';
inner.id = `container-${i}`;
container.appendChild(inner);
viewerContainer.appendChild(container);
viewports.push({
container: inner,
fullScreenRenderer: null,
renderer: null,
renderWindow: null,
interactor: null,
actor: null,
mapper: null,
polydata: null,
bounds: null,
fieldNames: []
});
}
}
// Initialize VTK renderers for each viewport
function initializeViewports() {
viewports.forEach((vp, index) => {
vp.fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
background: [0.16, 0.16, 0.16],
container: vp.container,
});
vp.renderer = vp.fullScreenRenderer.getRenderer();
vp.renderWindow = vp.fullScreenRenderer.getRenderWindow();
vp.interactor = vp.renderWindow.getInteractor();
const interactorStyle = vtkInteractorStyleTrackballCamera.newInstance();
vp.interactor.setInteractorStyle(interactorStyle);
vp.actor = vtkActor.newInstance();
vp.mapper = vtkMapper.newInstance();
vp.actor.setMapper(vp.mapper);
// Configure appearance
const property = vp.actor.getProperty();
property.setEdgeVisibility(false);
property.setColor(0.6, 0.8, 1.0);
property.setAmbient(0.4);
property.setDiffuse(0.8);
property.setSpecular(0.3);
property.setSpecularPower(20);
property.setRepresentation(2);
vp.renderer.addActor(vp.actor);
vp.fullScreenRenderer.resize();
});
setupCameraSync();
}
// Camera synchronization across all viewports
let isSyncing = false;
let activeViewportIndex = -1;
function synchronizeCameras(sourceIndex) {
if (isSyncing) return;
isSyncing = true;
const sourceCamera = viewports[sourceIndex].renderer.getActiveCamera();
viewports.forEach((vp, index) => {
if (index !== sourceIndex) {
const targetCamera = vp.renderer.getActiveCamera();
targetCamera.setPosition(...sourceCamera.getPosition());
targetCamera.setFocalPoint(...sourceCamera.getFocalPoint());
targetCamera.setViewUp(...sourceCamera.getViewUp());
targetCamera.setParallelProjection(sourceCamera.getParallelProjection());
targetCamera.setClippingRange(...sourceCamera.getClippingRange());
targetCamera.setParallelScale(sourceCamera.getParallelScale());
targetCamera.setViewAngle(sourceCamera.getViewAngle());
renderWithClippingRangeUpdate(vp.renderer, vp.renderWindow);
}
});
isSyncing = false;
}
function setupCameraSync() {
viewports.forEach((vp, index) => {
vp.interactor.onStartAnimation(() => { activeViewportIndex = index; });
vp.interactor.onAnimation(() => {
if (activeViewportIndex === index) {
synchronizeCameras(index);
}
});
vp.interactor.onEndAnimation(() => {
if (activeViewportIndex === index) {
synchronizeCameras(index);
activeViewportIndex = -1;
}
});
});
// Mouse wheel sync
setTimeout(() => {
viewports.forEach((vp, index) => {
const canvas = vp.container.querySelector('canvas');
if (canvas) {
canvas.addEventListener('wheel', () => {
setTimeout(() => synchronizeCameras(index), 0);
});
}
});
}, 500);
}
// Field visualization functions
function applyFieldToMapper(mapper, polydata, fieldName, sharedRange = null) {
if (!fieldName || fieldName === 'none') {
mapper.setScalarVisibility(false);
return;
}
const isCellData = fieldName.startsWith('face.');
const actualFieldName = isCellData ? fieldName.substring(5) : fieldName;
const data = isCellData ? polydata.getCellData() : polydata.getPointData();
const scalarArray = data.getArrayByName(actualFieldName);
if (scalarArray) {
const range = scalarArray.getRange();
const [min, max] = sharedRange || range;
data.setActiveScalars(actualFieldName);
mapper.setScalarVisibility(true);
if (isCellData) {
mapper.setScalarModeToUseCellData();
} else {
mapper.setScalarModeToUsePointData();
}
mapper.setScalarRange(min, max);
// Apply colormap
try {
const colormapName = document.getElementById('colormapSelector')?.value || 'erdc_rainbow_bright';
const lookupTable = vtkColorTransferFunction.newInstance();
const preset = vtkColorMaps.getPresetByName(colormapName);
if (preset) {
lookupTable.applyColorMap(preset);
lookupTable.setMappingRange(min, max);
lookupTable.updateRange();
mapper.setLookupTable(lookupTable);
}
} catch (error) {
console.error(`[GeomPack VTK Multi Viewer] Error applying colormap:`, error);
}
mapper.update();
} else {
console.warn(`[GeomPack VTK Multi Viewer] Field "${fieldName}" not found in polydata`);
}
}
function populateColormapSelector() {
const selector = document.getElementById('colormapSelector');
if (!selector) return;
const presets = [
'erdc_rainbow_bright',
'Cool to Warm',
'Blue to Red Rainbow',
'X Ray',
'jet',
'hsv',
'hot',
'cool',
'rainbow',
'grayscale'
];
selector.innerHTML = '';
presets.forEach(preset => {
const option = document.createElement('option');
option.value = preset;
option.textContent = preset;
selector.appendChild(option);
});
}
function populateFieldSelector() {
// Find common fields across all meshes
if (viewports.length === 0) return;
// Start with first viewport's fields
commonFields = viewports[0].fieldNames ? [...viewports[0].fieldNames] : [];
// Intersect with all other viewports
for (let i = 1; i < viewports.length; i++) {
const vpFields = viewports[i].fieldNames || [];
commonFields = commonFields.filter(f => vpFields.includes(f));
}
console.log('[GeomPack VTK Multi Viewer] Common fields:', commonFields);
const selector = document.getElementById('fieldSelector');
if (!selector) return;
selector.innerHTML = '<option value="none">None</option>';
commonFields.forEach(field => {
const option = document.createElement('option');
option.value = field;
option.textContent = field;
selector.appendChild(option);
});
// Show field controls if any fields exist
const hasAnyFields = viewports.some(vp => vp.fieldNames && vp.fieldNames.length > 0);
if (hasAnyFields) {
document.getElementById('fieldControls').classList.add('active');
} else {
document.getElementById('fieldControls').classList.remove('active');
}
}
function applyFieldToAllViewports(fieldName) {
if (!fieldName || fieldName === 'none') {
// Disable scalar visibility for all
viewports.forEach(vp => {
if (vp.mapper) {
vp.mapper.setScalarVisibility(false);
vp.renderWindow.render();
}
});
return;
}
// Calculate shared range from all meshes
let sharedMin = Infinity;
let sharedMax = -Infinity;
const isCellData = fieldName.startsWith('face.');
const actualFieldName = isCellData ? fieldName.substring(5) : fieldName;
viewports.forEach(vp => {
if (vp.polydata) {
const data = isCellData ? vp.polydata.getCellData() : vp.polydata.getPointData();
const scalarArray = data.getArrayByName(actualFieldName);
if (scalarArray) {
const [min, max] = scalarArray.getRange();
sharedMin = Math.min(sharedMin, min);
sharedMax = Math.max(sharedMax, max);
}
}
});
const sharedRange = (sharedMin !== Infinity && sharedMax !== -Infinity) ? [sharedMin, sharedMax] : null;
// Apply to all viewports
viewports.forEach(vp => {
if (vp.mapper && vp.polydata) {
applyFieldToMapper(vp.mapper, vp.polydata, fieldName, sharedRange);
renderWithClippingRangeUpdate(vp.renderer, vp.renderWindow);
}
});
}
// Load mesh function
async function loadMesh(filepath, viewportIndex) {
const vp = viewports[viewportIndex];
console.log(`[GeomPack VTK Multi Viewer] Loading mesh ${viewportIndex + 1}:`, filepath);
try {
const isSTL = filepath.includes('.stl');
const isOBJ = filepath.includes('.obj');
const isVTP = filepath.includes('.vtp');
const isGLB = filepath.includes('.glb');
let reader;
if (isSTL) {
reader = vtkSTLReader.newInstance();
} else if (isOBJ) {
reader = vtkOBJReader.newInstance();
} else if (isVTP) {
reader = vtkXMLPolyDataReader.newInstance();
} else if (isGLB) {
reader = vtkGLTFImporter.newInstance();
} else {
throw new Error(`Unsupported file format: ${filepath}`);
}
const response = await fetch(filepath);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
if (isGLB) {
reader.parseAsArrayBuffer(arrayBuffer);
await reader.update();
const source = reader.getOutputData();
vp.mapper.setInputData(source);
vp.mapper.update();
vp.polydata = vp.mapper.getInputData();
vp.fieldNames = [];
} else {
if (isSTL) {
reader.parseAsArrayBuffer(arrayBuffer);
} else if (isOBJ) {
const text = new TextDecoder().decode(arrayBuffer);
reader.parseAsText(text);
} else if (isVTP) {
reader.parseAsArrayBuffer(arrayBuffer);
}
const polydata = reader.getOutputData();
if (!polydata) {
throw new Error('Failed to parse mesh data');
}
// Extract field names from VTP
vp.fieldNames = [];
if (isVTP) {
const pointData = polydata.getPointData();
const cellData = polydata.getCellData();
for (let i = 0; i < pointData.getNumberOfArrays(); i++) {
const array = pointData.getArray(i);
vp.fieldNames.push(array.getName());
}
for (let i = 0; i < cellData.getNumberOfArrays(); i++) {
const array = cellData.getArray(i);
vp.fieldNames.push(`face.${array.getName()}`);
}
console.log(`[GeomPack VTK Multi Viewer] Mesh ${viewportIndex + 1} fields:`, vp.fieldNames);
}
const normalsFilter = vtkPolyDataNormals.newInstance();
normalsFilter.setInputConnection(reader.getOutputPort());
vp.mapper.setInputConnection(normalsFilter.getOutputPort());
vp.mapper.update();
vp.polydata = vp.mapper.getInputData();
}
vp.bounds = vp.polydata.getBounds();
console.log(`[GeomPack VTK Multi Viewer] Mesh ${viewportIndex + 1} loaded, bounds:`, vp.bounds);
} catch (error) {
console.error(`[GeomPack VTK Multi Viewer] Error loading mesh ${viewportIndex + 1}:`, error);
throw error;
}
}
// Calculate combined bounds from all meshes
function updateCombinedBounds() {
combinedBounds = null;
viewports.forEach(vp => {
if (vp.bounds) {
if (!combinedBounds) {
combinedBounds = [...vp.bounds];
} else {
combinedBounds[0] = Math.min(combinedBounds[0], vp.bounds[0]);
combinedBounds[1] = Math.max(combinedBounds[1], vp.bounds[1]);
combinedBounds[2] = Math.min(combinedBounds[2], vp.bounds[2]);
combinedBounds[3] = Math.max(combinedBounds[3], vp.bounds[3]);
combinedBounds[4] = Math.min(combinedBounds[4], vp.bounds[4]);
combinedBounds[5] = Math.max(combinedBounds[5], vp.bounds[5]);
}
}
});
}
// Position camera at an angle
function positionCamera(renderer, bounds) {
const camera = renderer.getActiveCamera();
const center = [
(bounds[0] + bounds[1]) / 2,
(bounds[2] + bounds[3]) / 2,
(bounds[4] + bounds[5]) / 2
];
const maxDim = Math.max(
bounds[1] - bounds[0],
bounds[3] - bounds[2],
bounds[5] - bounds[4]
);
const distance = maxDim * 2.5;
camera.setPosition(
center[0] + distance,
center[1] + distance,
center[2] + distance
);
camera.setFocalPoint(center[0], center[1], center[2]);
camera.setViewUp(0, 0, 1);
renderer.resetCamera(bounds);
}
// Listen for messages from parent window
window.addEventListener('message', async (event) => {
if (event.data.type === 'LOAD_MULTI_MESH') {
const data = event.data;
console.log('[GeomPack VTK Multi Viewer] Received LOAD_MULTI_MESH message:', data);
numMeshes = data.numMeshes;
const meshFiles = data.meshFiles;
loading.textContent = 'Loading meshes...';
loading.style.display = 'block';
try {
// Create viewport containers
createViewportContainers(numMeshes);
// Initialize VTK renderers
initializeViewports();
// Load all meshes
await Promise.all(meshFiles.map((filepath, index) => loadMesh(filepath, index)));
// Update combined bounds and position cameras
updateCombinedBounds();
viewports.forEach(vp => {
positionCamera(vp.renderer, combinedBounds);
renderWithClippingRangeUpdate(vp.renderer, vp.renderWindow);
});
// Populate field and colormap selectors
populateColormapSelector();
populateFieldSelector();
loading.style.display = 'none';
console.log('[GeomPack VTK Multi Viewer] All meshes loaded successfully');
} catch (error) {
console.error('[GeomPack VTK Multi Viewer] Error loading meshes:', error);
loading.textContent = `Error: ${error.message}`;
loading.style.color = '#ff6b6b';
}
}
});
// Event handlers
document.getElementById('showEdges').addEventListener('change', (e) => {
const edgeViz = e.target.checked;
viewports.forEach(vp => {
vp.actor.getProperty().setEdgeVisibility(edgeViz);
vp.renderWindow.render();
});
});
document.getElementById('resetCamera').addEventListener('click', () => {
if (!combinedBounds) return;
viewports.forEach(vp => {
positionCamera(vp.renderer, combinedBounds);
renderWithClippingRangeUpdate(vp.renderer, vp.renderWindow);
});
});
// Camera view buttons
function setCameraView(direction) {
if (!combinedBounds) return;
const center = [
(combinedBounds[0] + combinedBounds[1]) / 2,
(combinedBounds[2] + combinedBounds[3]) / 2,
(combinedBounds[4] + combinedBounds[5]) / 2
];
const maxDim = Math.max(
combinedBounds[1] - combinedBounds[0],
combinedBounds[3] - combinedBounds[2],
combinedBounds[5] - combinedBounds[4]
);
const distance = maxDim * 2.5;
let position, viewUp;
switch(direction) {
case '+X': position = [center[0] + distance, center[1], center[2]]; viewUp = [0, 0, 1]; break;
case '-X': position = [center[0] - distance, center[1], center[2]]; viewUp = [0, 0, 1]; break;
case '+Y': position = [center[0], center[1] + distance, center[2]]; viewUp = [0, 0, 1]; break;
case '-Y': position = [center[0], center[1] - distance, center[2]]; viewUp = [0, 0, 1]; break;
case '+Z': position = [center[0], center[1], center[2] + distance]; viewUp = [0, 1, 0]; break;
case '-Z': position = [center[0], center[1], center[2] - distance]; viewUp = [0, 1, 0]; break;
}
viewports.forEach(vp => {
const camera = vp.renderer.getActiveCamera();
camera.setPosition(...position);
camera.setFocalPoint(...center);
camera.setViewUp(...viewUp);
vp.renderer.resetCamera();
renderWithClippingRangeUpdate(vp.renderer, vp.renderWindow);
});
}
document.getElementById('viewPosX').addEventListener('click', () => setCameraView('+X'));
document.getElementById('viewNegX').addEventListener('click', () => setCameraView('-X'));
document.getElementById('viewPosY').addEventListener('click', () => setCameraView('+Y'));
document.getElementById('viewNegY').addEventListener('click', () => setCameraView('-Y'));
document.getElementById('viewPosZ').addEventListener('click', () => setCameraView('+Z'));
document.getElementById('viewNegZ').addEventListener('click', () => setCameraView('-Z'));
// Field selector change
document.getElementById('fieldSelector').addEventListener('change', (e) => {
applyFieldToAllViewports(e.target.value);
});
// Colormap selector change
document.getElementById('colormapSelector').addEventListener('change', () => {
const currentField = document.getElementById('fieldSelector').value;
if (currentField && currentField !== 'none') {
applyFieldToAllViewports(currentField);
}
});
// Window resize
window.addEventListener('resize', () => {
viewports.forEach(vp => {
vp.fullScreenRenderer?.resize();
vp.renderWindow?.render();
});
});
console.log('[GeomPack VTK Multi Viewer] Ready');
</script>
</body>
</html>