Files
PozzettiAndrea-ComfyUI-Geom…/javascript/viewer_dual_textured.html
T
PozzettiAndrea 9ebf36b571 Rename viewer bundles .js -> .mjs so they stop leaking into the main realm
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.
2026-08-13 11:41:01 +02:00

1465 lines
60 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GeomPack VTK.js Dual Textured 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: flex;
}
.viewport-container {
flex: 1;
position: relative;
background: #1a1a1a;
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;
}
#sideBySideContainer {
display: none;
gap: 2px;
width: 100%;
height: 100%;
}
#sideBySideContainer.active {
display: flex;
}
#overlayContainer {
display: none;
width: 100%;
height: 100%;
}
#overlayContainer.active {
display: block;
}
#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;
overflow-x: auto;
overflow-y: hidden;
}
#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;
}
#controls input[type="checkbox"] {
cursor: pointer;
}
.divider {
width: 1px;
height: 20px;
background: #444;
margin: 0 8px;
}
.field-controls {
display: none;
}
.field-controls.active {
display: flex;
gap: 12px;
}
/* Settings panel */
#settingsPanel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #1a1a1a;
border: 1px solid #555;
border-radius: 6px;
padding: 16px;
z-index: 2000;
display: none;
min-width: 280px;
max-height: 80%;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
#settingsPanel.visible {
display: block;
}
#settingsPanel h3 {
margin: 0 0 12px 0;
color: #ccc;
font-size: 14px;
font-weight: normal;
border-bottom: 1px solid #333;
padding-bottom: 8px;
}
#settingsPanel .setting-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
#settingsPanel .setting-row label {
flex: 0 0 100px;
color: #ccc;
font-size: 12px;
}
#settingsPanel .setting-row input[type="color"] {
flex: 1;
height: 32px;
border: 1px solid #555;
background: #333;
cursor: pointer;
}
#settingsPanel .setting-row input[type="number"] {
flex: 1;
background: #333;
color: #ccc;
border: 1px solid #555;
padding: 6px 8px;
border-radius: 3px;
font-size: 12px;
font-family: monospace;
}
#settingsPanel .setting-row select {
flex: 1;
background: #333;
color: #ccc;
border: 1px solid #555;
padding: 6px 8px;
border-radius: 3px;
font-size: 12px;
font-family: monospace;
}
#settingsPanel .button-row {
display: flex;
gap: 8px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #333;
}
#settingsPanel .button-row button {
flex: 1;
padding: 6px 12px;
font-size: 12px;
}
#settingsOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1999;
display: none;
}
#settingsOverlay.visible {
display: block;
}
#settingsButton {
background: #333;
width: 32px;
height: 32px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
</style>
</head>
<body>
<div id="viewerContainer">
<!-- Side-by-side layout -->
<div id="sideBySideContainer">
<div class="viewport-container" id="leftViewport">
<div class="viewport-label">Mesh 1</div>
<div id="leftContainer" style="width:100%;height:100%"></div>
</div>
<div class="viewport-container" id="rightViewport">
<div class="viewport-label">Mesh 2</div>
<div id="rightContainer" style="width:100%;height:100%"></div>
</div>
</div>
<!-- Overlay layout -->
<div id="overlayContainer">
<div id="singleContainer" style="width:100%;height:100%"></div>
</div>
<div id="loading">Ready to load meshes</div>
<!-- Settings overlay and panel -->
<div id="settingsOverlay"></div>
<div id="settingsPanel">
<h3>Appearance Settings</h3>
<div class="setting-row">
<label>Point Size:</label>
<input type="number" id="pointSize" min="1" max="20" step="1" value="10">
</div>
<div class="setting-row">
<label>Mesh Color:</label>
<input type="color" id="meshColor" value="#99ccff">
</div>
<div class="setting-row">
<label>Edge Width:</label>
<input type="number" id="edgeWidth" min="1" max="10" step="1" value="1">
</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="parallelProjectionSetting"> Parallel Projection
</label>
</div>
<div class="setting-row" style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #444;">
<label style="font-weight: bold; color: #aaa;">Colormap:</label>
</div>
<div class="setting-row">
<label>Preset:</label>
<select id="colormapSelector" style="width: 140px;"></select>
</div>
<div class="button-row">
<button id="applySettings">Apply</button>
<button id="cancelSettings">Cancel</button>
</div>
</div>
</div>
<div id="controls">
<!-- Settings button -->
<button id="settingsButton" title="Appearance Settings">[Settings]</button>
<label class="divider"></label>
<label>
<input type="checkbox" id="showEdges"> Edges
</label>
<label class="divider"></label>
<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>
<label class="divider"></label>
<button id="resetCamera">Reset Camera</button>
<!-- Field visualization controls -->
<div class="field-controls" id="fieldControls">
<label class="divider"></label>
<label>
Field Mode:
<select id="fieldMode">
<option value="none">None</option>
<option value="independent">Independent</option>
<option value="synchronized" selected>Synchronized</option>
</select>
</label>
<label id="field1Label" style="display:none;">
Mesh 1:
<select id="field1Selector"></select>
</label>
<label id="field2Label" style="display:none;">
Mesh 2:
<select id="field2Selector"></select>
</label>
<label id="sharedFieldLabel" style="display:none;">
Field:
<select id="sharedFieldSelector"></select>
</label>
</div>
</div>
<script src="./js/vtk-gltf.mjs"></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 vtkColorTransferFunction = vtk.Rendering.Core.vtkColorTransferFunction;
const vtkColorMaps = vtk.Rendering.Core.vtkColorTransferFunction.vtkColorMaps;
const vtkGLTFImporter = vtk.IO.Geometry.vtkGLTFImporter;
const loading = document.getElementById('loading');
const sideBySideContainer = document.getElementById('sideBySideContainer');
const overlayContainer = document.getElementById('overlayContainer');
// Viewer state
let viewerLayout = null; // 'side_by_side' or 'overlay'
let isInitialized = false; // Track if viewer has been initialized
let currentMesh1Path = null; // Track loaded mesh paths
let currentMesh2Path = null;
let currentMeshPath = null; // For overlay mode
let leftRenderer, rightRenderer, singleRenderer;
let leftRenderWindow, rightRenderWindow, singleRenderWindow;
let leftInteractor, rightInteractor, singleInteractor;
let leftFullScreenRenderer, rightFullScreenRenderer, singleFullScreenRenderer;
let leftActor, leftMapper, rightActor, rightMapper, singleActor, singleMapper;
let leftPolydata, rightPolydata, singlePolydata;
let leftActors = [], rightActors = [], singleActors = []; // GLB actors storage
let mesh1Bounds, mesh2Bounds, combinedBounds;
let fieldNames1 = [], fieldNames2 = [], commonFields = [];
// Camera state persistence
let _cameraWheelTimer = null;
let _pendingCameraState = null;
function sendCameraState() {
const cam = viewerLayout === 'side_by_side'
? leftRenderer?.getActiveCamera()
: singleRenderer?.getActiveCamera();
if (!cam) return;
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;
if (viewerLayout === 'side_by_side') {
[leftRenderer, rightRenderer].forEach(r => {
if (!r) return;
const cam = r.getActiveCamera();
cam.setPosition(...cs.position);
cam.setFocalPoint(...cs.focalPoint);
cam.setViewUp(...cs.viewUp);
r.resetCameraClippingRange();
});
} else if (singleRenderer) {
const cam = singleRenderer.getActiveCamera();
cam.setPosition(...cs.position);
cam.setFocalPoint(...cs.focalPoint);
cam.setViewUp(...cs.viewUp);
singleRenderer.resetCameraClippingRange();
}
return true;
} catch(e) { return false; }
}
// Helper function to render with proper clipping range update (double RAF pattern)
function renderWithClippingRangeUpdate(renderer, renderWindow) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
renderer.resetCameraClippingRange();
renderWindow.render();
});
});
}
// Initialize side-by-side layout
function initSideBySide() {
// Guard: prevent double initialization
if (leftFullScreenRenderer) {
return;
}
const leftContainer = document.getElementById('leftContainer');
const rightContainer = document.getElementById('rightContainer');
leftFullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
background: [0.16, 0.16, 0.16],
container: leftContainer,
});
rightFullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
background: [0.16, 0.16, 0.16],
container: rightContainer,
});
leftRenderer = leftFullScreenRenderer.getRenderer();
rightRenderer = rightFullScreenRenderer.getRenderer();
leftRenderWindow = leftFullScreenRenderer.getRenderWindow();
rightRenderWindow = rightFullScreenRenderer.getRenderWindow();
leftInteractor = leftRenderWindow.getInteractor();
rightInteractor = rightRenderWindow.getInteractor();
// Set up trackball camera controls
const leftInteractorStyle = vtkInteractorStyleTrackballCamera.newInstance();
const rightInteractorStyle = vtkInteractorStyleTrackballCamera.newInstance();
leftInteractor.setInteractorStyle(leftInteractorStyle);
rightInteractor.setInteractorStyle(rightInteractorStyle);
// Create actors and mappers
leftActor = vtkActor.newInstance();
rightActor = vtkActor.newInstance();
leftMapper = vtkMapper.newInstance();
rightMapper = vtkMapper.newInstance();
leftActor.setMapper(leftMapper);
rightActor.setMapper(rightMapper);
// Configure appearance
[leftActor.getProperty(), rightActor.getProperty()].forEach(property => {
property.setEdgeVisibility(false);
property.setColor(0.6, 0.8, 1.0); // Light blue
property.setAmbient(0.4);
property.setDiffuse(0.8);
property.setSpecular(0.3);
property.setSpecularPower(20);
property.setRepresentation(2);
});
leftRenderer.addActor(leftActor);
rightRenderer.addActor(rightActor);
leftFullScreenRenderer.resize();
rightFullScreenRenderer.resize();
setupCameraSync();
}
// Initialize overlay layout
function initOverlay() {
// Guard: prevent double initialization
if (singleFullScreenRenderer) {
return;
}
const singleContainer = document.getElementById('singleContainer');
singleFullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
background: [0.16, 0.16, 0.16],
container: singleContainer,
});
singleRenderer = singleFullScreenRenderer.getRenderer();
singleRenderWindow = singleFullScreenRenderer.getRenderWindow();
singleInteractor = singleRenderWindow.getInteractor();
const singleInteractorStyle = vtkInteractorStyleTrackballCamera.newInstance();
singleInteractor.setInteractorStyle(singleInteractorStyle);
singleActor = vtkActor.newInstance();
singleMapper = vtkMapper.newInstance();
singleActor.setMapper(singleMapper);
const property = singleActor.getProperty();
property.setEdgeVisibility(false);
property.setColor(0.6, 0.8, 1.0); // Light blue
property.setAmbient(0.4);
property.setDiffuse(0.8);
property.setSpecular(0.3);
property.setSpecularPower(20);
property.setRepresentation(2);
singleRenderer.addActor(singleActor);
singleFullScreenRenderer.resize();
// Camera state tracking for overlay mode
singleInteractor.onEndAnimation(() => sendCameraState());
setTimeout(() => {
const canvas = document.querySelector('#singleContainer canvas');
if (canvas) {
canvas.addEventListener('wheel', () => {
clearTimeout(_cameraWheelTimer);
_cameraWheelTimer = setTimeout(sendCameraState, 150);
});
}
}, 500);
}
// Camera synchronization
let isSyncing = false;
let isLeftAnimating = false;
let isRightAnimating = false;
function synchronizeCameras(sourceCamera, targetCamera) {
if (isSyncing) return;
isSyncing = true;
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());
isSyncing = false;
}
function setupCameraSync() {
// Animation-based sync
leftInteractor.onStartAnimation(() => { isLeftAnimating = true; });
leftInteractor.onAnimation(() => {
if (isLeftAnimating && !isRightAnimating) {
synchronizeCameras(leftRenderer.getActiveCamera(), rightRenderer.getActiveCamera());
renderWithClippingRangeUpdate(rightRenderer, rightRenderWindow);
}
});
leftInteractor.onEndAnimation(() => {
isLeftAnimating = false;
if (!isRightAnimating) {
synchronizeCameras(leftRenderer.getActiveCamera(), rightRenderer.getActiveCamera());
renderWithClippingRangeUpdate(rightRenderer, rightRenderWindow);
sendCameraState();
}
});
rightInteractor.onStartAnimation(() => { isRightAnimating = true; });
rightInteractor.onAnimation(() => {
if (isRightAnimating && !isLeftAnimating) {
synchronizeCameras(rightRenderer.getActiveCamera(), leftRenderer.getActiveCamera());
renderWithClippingRangeUpdate(leftRenderer, leftRenderWindow);
}
});
rightInteractor.onEndAnimation(() => {
isRightAnimating = false;
if (!isLeftAnimating) {
synchronizeCameras(rightRenderer.getActiveCamera(), leftRenderer.getActiveCamera());
renderWithClippingRangeUpdate(leftRenderer, leftRenderWindow);
sendCameraState();
}
});
// Mouse wheel zoom sync
setTimeout(() => {
const leftCanvas = document.querySelector('#leftContainer canvas');
const rightCanvas = document.querySelector('#rightContainer canvas');
if (leftCanvas) {
leftCanvas.addEventListener('wheel', () => {
setTimeout(() => {
if (!isRightAnimating) {
synchronizeCameras(leftRenderer.getActiveCamera(), rightRenderer.getActiveCamera());
renderWithClippingRangeUpdate(rightRenderer, rightRenderWindow);
}
}, 0);
clearTimeout(_cameraWheelTimer);
_cameraWheelTimer = setTimeout(sendCameraState, 150);
});
}
if (rightCanvas) {
rightCanvas.addEventListener('wheel', () => {
setTimeout(() => {
if (!isLeftAnimating) {
synchronizeCameras(rightRenderer.getActiveCamera(), leftRenderer.getActiveCamera());
renderWithClippingRangeUpdate(leftRenderer, leftRenderWindow);
}
}, 0);
clearTimeout(_cameraWheelTimer);
_cameraWheelTimer = setTimeout(sendCameraState, 150);
});
}
}, 500);
}
// Load mesh function
// Load GLB mesh using vtkGLTFImporter (texture mode)
async function loadGLBMesh(filepath, targetRenderer, isMesh1 = true, isOverlay = false) {
const meshLabel = isOverlay ? 'combined' : (isMesh1 ? '1' : '2');
try {
const importer = vtkGLTFImporter.newInstance();
importer.setRenderer(targetRenderer);
importer.setUrl(filepath);
// Wait for GLTF parsing
await new Promise((resolve, reject) => {
importer.onReady(() => {
resolve();
});
importer.loadData().catch(reject);
});
// Import actors
importer.importActors();
// Get actors and apply texture fixes
const actorsMap = importer.getActors();
const actors = Array.from(actorsMap.values());
let bounds = null;
actors.forEach((actor, actorIndex) => {
const mapper = actor.getMapper();
const property = actor.getProperty();
const input = mapper?.getInputData();
const numPoints = input?.getNumberOfPoints() || 0;
// Skip actors with no geometry - they serve no purpose and may cause rendering issues
if (numPoints === 0) {
targetRenderer.removeActor(actor);
return; // Skip this actor entirely
}
if (input && input.getNumberOfPoints() > 0) {
// Calculate bounds
const actorBounds = input.getBounds();
if (!bounds) {
bounds = actorBounds;
} else {
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])
];
}
// Check for vertex colors early
const scalars = input.getPointData()?.getScalars();
const hasVertexColors = scalars && scalars.getNumberOfComponents() >= 3;
let hasTexture = false;
// Apply texture fixes
if (property) {
// CRITICAL FIX: Remove metallicRoughnessTexture if present
if (typeof property.setRMTexture === 'function') {
property.setRMTexture(null);
}
if (typeof property.setMetallicRoughnessTexture === 'function') {
property.setMetallicRoughnessTexture(null);
}
// Try multiple texture getter methods for compatibility
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) {
actor.addTexture(texture);
hasTexture = true;
}
// Set material to non-metallic for proper texture display
property.setMetallic(0.0);
property.setRoughness(1.0);
property.setInterpolation(2);
}
// Configure mapper for rendering
if (mapper) {
if (hasVertexColors && !hasTexture) {
// Enable vertex colors
mapper.setScalarVisibility(true);
mapper.setScalarModeToUsePointData();
mapper.setColorModeToDirectScalars();
} else {
// Disable scalars for texture rendering
mapper.setScalarVisibility(false);
}
mapper.modified();
}
}
});
// Store bounds and actors based on mode
if (isOverlay) {
mesh1Bounds = bounds; // Used by updateCombinedBounds()
singleActors = actors;
} else if (isMesh1) {
mesh1Bounds = bounds;
leftActors = actors;
} else {
mesh2Bounds = bounds;
rightActors = actors;
}
} catch (error) {
console.error(`[GeomPack VTK Dual Textured] Error loading GLB ${meshLabel}:`, error);
throw error;
}
}
// Calculate combined bounds
function updateCombinedBounds() {
if (mesh1Bounds && mesh2Bounds) {
combinedBounds = [
Math.min(mesh1Bounds[0], mesh2Bounds[0]),
Math.max(mesh1Bounds[1], mesh2Bounds[1]),
Math.min(mesh1Bounds[2], mesh2Bounds[2]),
Math.max(mesh1Bounds[3], mesh2Bounds[3]),
Math.min(mesh1Bounds[4], mesh2Bounds[4]),
Math.max(mesh1Bounds[5], mesh2Bounds[5])
];
} else if (mesh1Bounds) {
combinedBounds = mesh1Bounds;
} else if (mesh2Bounds) {
combinedBounds = mesh2Bounds;
}
}
// 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);
}
// Field visualization
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;
// Check for degenerate range - if shared range provided (synchronized mode), don't disable
const epsilon = 1e-10;
const isDegenerate = Math.abs(max - min) < epsilon;
if (isDegenerate && !sharedRange) {
console.warn(`[GeomPack VTK Dual Viewer] Field "${fieldName}" has zero-width range [${min}, ${max}] - disabling visualization.`);
mapper.setScalarVisibility(false);
return;
}
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);
} else {
console.error(`[GeomPack VTK Dual Viewer] Colormap preset "${colormapName}" not found`);
}
} catch (error) {
console.error(`[GeomPack VTK Dual Viewer] Error applying colormap:`, error);
}
mapper.update();
} else {
console.warn(`[GeomPack VTK Dual Viewer] Field "${fieldName}" not found in polydata`);
}
}
// Populate colormap selector
function populateColormapSelector() {
const selector = document.getElementById('colormapSelector');
if (!selector) return;
// Only include VTK.js presets that are confirmed to exist
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);
});
}
// Field mode controls
function updateFieldModeUI() {
const fieldMode = document.getElementById('fieldMode').value;
const field1Label = document.getElementById('field1Label');
const field2Label = document.getElementById('field2Label');
const sharedFieldLabel = document.getElementById('sharedFieldLabel');
// Hide all
field1Label.style.display = 'none';
field2Label.style.display = 'none';
sharedFieldLabel.style.display = 'none';
if (fieldMode === 'independent' && viewerLayout === 'side_by_side') {
field1Label.style.display = 'flex';
field2Label.style.display = 'flex';
} else if (fieldMode === 'synchronized') {
sharedFieldLabel.style.display = 'flex';
}
}
// Populate field selectors
function populateFieldSelectors() {
commonFields = fieldNames1.filter(f => fieldNames2.includes(f));
const field1Selector = document.getElementById('field1Selector');
const field2Selector = document.getElementById('field2Selector');
const sharedFieldSelector = document.getElementById('sharedFieldSelector');
// Preserve current selections
const currentField1 = field1Selector?.value || 'none';
const currentField2 = field2Selector?.value || 'none';
const currentSharedField = sharedFieldSelector?.value || 'none';
if (field1Selector) {
field1Selector.innerHTML = '<option value="none">None</option>';
fieldNames1.forEach(field => {
const option = document.createElement('option');
option.value = field;
option.textContent = field;
if (field === currentField1) {
option.selected = true;
}
field1Selector.appendChild(option);
});
}
if (field2Selector) {
field2Selector.innerHTML = '<option value="none">None</option>';
fieldNames2.forEach(field => {
const option = document.createElement('option');
option.value = field;
option.textContent = field;
if (field === currentField2) {
option.selected = true;
}
field2Selector.appendChild(option);
});
}
if (sharedFieldSelector) {
sharedFieldSelector.innerHTML = '<option value="none">None</option>';
commonFields.forEach(field => {
const option = document.createElement('option');
option.value = field;
option.textContent = field;
if (field === currentSharedField) {
option.selected = true;
}
sharedFieldSelector.appendChild(option);
});
}
// Show field controls if fields exist
if (fieldNames1.length > 0 || fieldNames2.length > 0) {
document.getElementById('fieldControls').classList.add('active');
updateFieldModeUI();
}
}
// Listen for messages from parent window
window.addEventListener('message', async (event) => {
if (event.data.type === 'LOAD_DUAL_MESH') {
const data = event.data;
viewerLayout = data.layout;
loading.textContent = 'Loading textured meshes...';
loading.style.display = 'block';
try {
if (viewerLayout === 'side_by_side') {
// Side-by-side mode
sideBySideContainer.classList.add('active');
overlayContainer.classList.remove('active');
initSideBySide();
// Check if meshes have changed before reloading
const mesh1Changed = currentMesh1Path !== data.mesh1Filepath;
const mesh2Changed = currentMesh2Path !== data.mesh2Filepath;
if (mesh1Changed || leftActors.length === 0) {
// Clear previous actors
leftActors.forEach(actor => leftRenderer.removeActor(actor));
leftActors = [];
await loadGLBMesh(data.mesh1Filepath, leftRenderer, true);
currentMesh1Path = data.mesh1Filepath;
}
if (mesh2Changed || rightActors.length === 0) {
// Clear previous actors
rightActors.forEach(actor => rightRenderer.removeActor(actor));
rightActors = [];
await loadGLBMesh(data.mesh2Filepath, rightRenderer, false);
currentMesh2Path = data.mesh2Filepath;
}
updateCombinedBounds();
_pendingCameraState = data.cameraState || null;
if (!applyCameraState(_pendingCameraState)) {
positionCamera(leftRenderer, combinedBounds);
positionCamera(rightRenderer, combinedBounds);
synchronizeCameras(leftRenderer.getActiveCamera(), rightRenderer.getActiveCamera());
}
// Apply opacity values to all actors
if (data.opacity1 !== undefined) {
leftActors.forEach(actor => {
if (actor && actor.getProperty) {
actor.getProperty().setOpacity(data.opacity1);
}
});
}
if (data.opacity2 !== undefined) {
rightActors.forEach(actor => {
if (actor && actor.getProperty) {
actor.getProperty().setOpacity(data.opacity2);
}
});
}
renderWithClippingRangeUpdate(leftRenderer, leftRenderWindow);
renderWithClippingRangeUpdate(rightRenderer, rightRenderWindow);
} else {
// Overlay mode
sideBySideContainer.classList.remove('active');
overlayContainer.classList.add('active');
initOverlay();
// Check if mesh has changed before reloading
const meshChanged = currentMeshPath !== data.meshFilepath;
if (meshChanged || singleActors.length === 0) {
// Clear previous actors
singleActors.forEach(actor => singleRenderer.removeActor(actor));
singleActors = [];
await loadGLBMesh(data.meshFilepath, singleRenderer, true, true); // isOverlay = true
currentMeshPath = data.meshFilepath;
}
updateCombinedBounds();
_pendingCameraState = data.cameraState || null;
if (!applyCameraState(_pendingCameraState)) {
positionCamera(singleRenderer, combinedBounds);
}
// Apply average opacity for overlay mode to all actors
if (data.opacity1 !== undefined && data.opacity2 !== undefined) {
const avgOpacity = (data.opacity1 + data.opacity2) / 2.0;
singleActors.forEach(actor => {
if (actor && actor.getProperty) {
actor.getProperty().setOpacity(avgOpacity);
}
});
}
renderWithClippingRangeUpdate(singleRenderer, singleRenderWindow);
}
// Hide field controls in texture mode
const fieldControls = document.getElementById('fieldControls');
if (fieldControls) {
fieldControls.style.display = 'none';
}
// Apply showEdges from message data
const showEdges = data.showEdges || false;
if (viewerLayout === 'side_by_side') {
leftActors.forEach(actor => {
if (actor && actor.getProperty) {
actor.getProperty().setEdgeVisibility(showEdges);
}
});
rightActors.forEach(actor => {
if (actor && actor.getProperty) {
actor.getProperty().setEdgeVisibility(showEdges);
}
});
if (leftRenderWindow) leftRenderWindow.render();
if (rightRenderWindow) rightRenderWindow.render();
} else {
singleActors.forEach(actor => {
if (actor && actor.getProperty) {
actor.getProperty().setEdgeVisibility(showEdges);
}
});
if (singleRenderWindow) singleRenderWindow.render();
}
document.getElementById('showEdges').checked = showEdges;
// Mark as initialized
isInitialized = true;
loading.style.display = 'none';
} catch (error) {
console.error('[GeomPack VTK Dual Textured] Error loading meshes:', error);
loading.textContent = `Error: ${error.message}`;
loading.style.color = '#ff6b6b';
}
}
});
// Settings panel handlers
const settingsButton = document.getElementById('settingsButton');
const settingsPanel = document.getElementById('settingsPanel');
const settingsOverlay = document.getElementById('settingsOverlay');
const pointSizeInput = document.getElementById('pointSize');
const meshColorInput = document.getElementById('meshColor');
const edgeWidthInput = document.getElementById('edgeWidth');
const representationSettings = document.getElementById('representationSettings');
const parallelProjectionInput = document.getElementById('parallelProjectionSetting');
const applySettingsButton = document.getElementById('applySettings');
const cancelSettingsButton = document.getElementById('cancelSettings');
// Store current settings
let currentPointSize = 3;
let currentMeshColor = '#99ccff';
let currentEdgeWidth = 1;
let currentRepresentation = 2; // 2 = Surface
let currentParallelProjection = false;
// Open settings panel
settingsButton.addEventListener('click', () => {
// Update inputs with current values
pointSizeInput.value = currentPointSize;
meshColorInput.value = currentMeshColor;
edgeWidthInput.value = currentEdgeWidth;
representationSettings.value = currentRepresentation.toString();
parallelProjectionInput.checked = currentParallelProjection;
settingsPanel.classList.add('visible');
settingsOverlay.classList.add('visible');
});
// Close settings panel
function closeSettingsPanel() {
settingsPanel.classList.remove('visible');
settingsOverlay.classList.remove('visible');
}
// Close on overlay click
settingsOverlay.addEventListener('click', closeSettingsPanel);
// Cancel button
cancelSettingsButton.addEventListener('click', closeSettingsPanel);
// Apply settings
applySettingsButton.addEventListener('click', () => {
const newPointSize = parseFloat(pointSizeInput.value);
const newColor = meshColorInput.value;
const newEdgeWidth = parseFloat(edgeWidthInput.value);
const newRepresentation = parseInt(representationSettings.value);
const newParallelProjection = parallelProjectionInput.checked;
// Convert hex color to RGB (0-1 range)
const r = parseInt(newColor.substr(1, 2), 16) / 255;
const g = parseInt(newColor.substr(3, 2), 16) / 255;
const b = parseInt(newColor.substr(5, 2), 16) / 255;
// Apply settings to actors
if (viewerLayout === 'side_by_side') {
// Apply to all actors in side-by-side mode (GLB can have multiple actors)
leftActors.forEach(actor => {
if (actor && actor.getProperty) {
const property = actor.getProperty();
property.setPointSize(newPointSize);
property.setColor(r, g, b);
property.setLineWidth(newEdgeWidth);
property.setRepresentation(newRepresentation);
}
});
rightActors.forEach(actor => {
if (actor && actor.getProperty) {
const property = actor.getProperty();
property.setPointSize(newPointSize);
property.setColor(r, g, b);
property.setLineWidth(newEdgeWidth);
property.setRepresentation(newRepresentation);
}
});
// Apply parallel projection to both cameras
if (leftRenderer) leftRenderer.getActiveCamera().setParallelProjection(newParallelProjection);
if (rightRenderer) rightRenderer.getActiveCamera().setParallelProjection(newParallelProjection);
if (leftRenderWindow) leftRenderWindow.render();
if (rightRenderWindow) rightRenderWindow.render();
} else {
// Apply to all actors in overlay mode
singleActors.forEach(actor => {
if (actor && actor.getProperty) {
const property = actor.getProperty();
property.setPointSize(newPointSize);
property.setColor(r, g, b);
property.setLineWidth(newEdgeWidth);
property.setRepresentation(newRepresentation);
}
});
// Apply parallel projection to camera
if (singleRenderer) singleRenderer.getActiveCamera().setParallelProjection(newParallelProjection);
if (singleRenderWindow) singleRenderWindow.render();
}
// Update stored settings
currentPointSize = newPointSize;
currentMeshColor = newColor;
currentEdgeWidth = newEdgeWidth;
currentRepresentation = newRepresentation;
currentParallelProjection = newParallelProjection;
// Close panel
closeSettingsPanel();
});
// Event handlers
document.getElementById('showEdges').addEventListener('change', (e) => {
const edgeViz = e.target.checked;
if (viewerLayout === 'side_by_side') {
// Apply to all actors in left and right (GLB can have multiple actors)
leftActors.forEach(actor => {
if (actor && actor.getProperty) {
actor.getProperty().setEdgeVisibility(edgeViz);
}
});
rightActors.forEach(actor => {
if (actor && actor.getProperty) {
actor.getProperty().setEdgeVisibility(edgeViz);
}
});
if (leftRenderWindow) leftRenderWindow.render();
if (rightRenderWindow) rightRenderWindow.render();
} else {
// Apply to all actors in single overlay
singleActors.forEach(actor => {
if (actor && actor.getProperty) {
actor.getProperty().setEdgeVisibility(edgeViz);
}
});
if (singleRenderWindow) singleRenderWindow.render();
}
window.parent.postMessage({ type: 'WIDGET_UPDATE', widget: 'show_edges', value: e.target.checked }, '*');
});
document.getElementById('resetCamera').addEventListener('click', () => {
if (!combinedBounds) return;
if (viewerLayout === 'side_by_side') {
positionCamera(leftRenderer, combinedBounds);
positionCamera(rightRenderer, combinedBounds);
synchronizeCameras(leftRenderer.getActiveCamera(), rightRenderer.getActiveCamera());
renderWithClippingRangeUpdate(leftRenderer, leftRenderWindow);
renderWithClippingRangeUpdate(rightRenderer, rightRenderWindow);
} else {
positionCamera(singleRenderer, combinedBounds);
renderWithClippingRangeUpdate(singleRenderer, singleRenderWindow);
}
});
// Field mode change
document.getElementById('fieldMode').addEventListener('change', () => {
updateFieldModeUI();
});
// Field selection
document.getElementById('field1Selector')?.addEventListener('change', (e) => {
if (leftPolydata) {
applyFieldToMapper(leftMapper, leftPolydata, e.target.value);
renderWithClippingRangeUpdate(leftRenderer, leftRenderWindow);
}
});
document.getElementById('field2Selector')?.addEventListener('change', (e) => {
if (rightPolydata) {
applyFieldToMapper(rightMapper, rightPolydata, e.target.value);
renderWithClippingRangeUpdate(rightRenderer, rightRenderWindow);
}
});
document.getElementById('sharedFieldSelector')?.addEventListener('change', (e) => {
const fieldName = e.target.value;
window.parent.postMessage({
type: 'WIDGET_UPDATE',
widget: 'selected_field',
value: fieldName
}, '*');
if (viewerLayout === 'side_by_side') {
// Calculate shared range from both meshes for synchronized mode
let sharedRange = null;
if (fieldName && fieldName !== 'none' && leftPolydata && rightPolydata) {
const isCellData = fieldName.startsWith('face.');
const actualFieldName = isCellData ? fieldName.substring(5) : fieldName;
const leftData = isCellData ? leftPolydata.getCellData() : leftPolydata.getPointData();
const rightData = isCellData ? rightPolydata.getCellData() : rightPolydata.getPointData();
const leftArray = leftData.getArrayByName(actualFieldName);
const rightArray = rightData.getArrayByName(actualFieldName);
if (leftArray && rightArray) {
const [leftMin, leftMax] = leftArray.getRange();
const [rightMin, rightMax] = rightArray.getRange();
sharedRange = [Math.min(leftMin, rightMin), Math.max(leftMax, rightMax)];
}
}
if (leftPolydata) {
applyFieldToMapper(leftMapper, leftPolydata, fieldName, sharedRange);
}
if (rightPolydata) {
applyFieldToMapper(rightMapper, rightPolydata, fieldName, sharedRange);
}
renderWithClippingRangeUpdate(leftRenderer, leftRenderWindow);
renderWithClippingRangeUpdate(rightRenderer, rightRenderWindow);
}
});
document.getElementById('colormapSelector')?.addEventListener('change', () => {
window.parent.postMessage({
type: 'WIDGET_UPDATE',
widget: 'selected_colormap',
value: document.getElementById('colormapSelector').value
}, '*');
// Reapply current field selections with new colormap
const fieldMode = document.getElementById('fieldMode').value;
if (fieldMode === 'independent') {
const field1 = document.getElementById('field1Selector').value;
const field2 = document.getElementById('field2Selector').value;
if (leftPolydata) applyFieldToMapper(leftMapper, leftPolydata, field1);
if (rightPolydata) applyFieldToMapper(rightMapper, rightPolydata, field2);
renderWithClippingRangeUpdate(leftRenderer, leftRenderWindow);
renderWithClippingRangeUpdate(rightRenderer, rightRenderWindow);
} else if (fieldMode === 'synchronized') {
const field = document.getElementById('sharedFieldSelector').value;
// Calculate shared range from both meshes for synchronized mode
let sharedRange = null;
if (field && field !== 'none' && leftPolydata && rightPolydata) {
const isCellData = field.startsWith('face.');
const actualFieldName = isCellData ? field.substring(5) : field;
const leftData = isCellData ? leftPolydata.getCellData() : leftPolydata.getPointData();
const rightData = isCellData ? rightPolydata.getCellData() : rightPolydata.getPointData();
const leftArray = leftData.getArrayByName(actualFieldName);
const rightArray = rightData.getArrayByName(actualFieldName);
if (leftArray && rightArray) {
const [leftMin, leftMax] = leftArray.getRange();
const [rightMin, rightMax] = rightArray.getRange();
sharedRange = [Math.min(leftMin, rightMin), Math.max(leftMax, rightMax)];
}
}
if (leftPolydata) applyFieldToMapper(leftMapper, leftPolydata, field, sharedRange);
if (rightPolydata) applyFieldToMapper(rightMapper, rightPolydata, field, sharedRange);
if (viewerLayout === 'side_by_side') {
renderWithClippingRangeUpdate(leftRenderer, leftRenderWindow);
renderWithClippingRangeUpdate(rightRenderer, rightRenderWindow);
}
}
});
// 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;
}
if (viewerLayout === 'side_by_side') {
[leftRenderer.getActiveCamera(), rightRenderer.getActiveCamera()].forEach(camera => {
camera.setPosition(...position);
camera.setFocalPoint(...center);
camera.setViewUp(...viewUp);
});
leftRenderer.resetCamera();
rightRenderer.resetCamera();
renderWithClippingRangeUpdate(leftRenderer, leftRenderWindow);
renderWithClippingRangeUpdate(rightRenderer, rightRenderWindow);
} else {
const camera = singleRenderer.getActiveCamera();
camera.setPosition(...position);
camera.setFocalPoint(...center);
camera.setViewUp(...viewUp);
singleRenderer.resetCamera();
renderWithClippingRangeUpdate(singleRenderer, singleRenderWindow);
}
}
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'));
// Window resize
window.addEventListener('resize', () => {
if (viewerLayout === 'side_by_side') {
leftFullScreenRenderer?.resize();
rightFullScreenRenderer?.resize();
leftRenderWindow?.render();
rightRenderWindow?.render();
} else {
singleFullScreenRenderer?.resize();
singleRenderWindow?.render();
}
});
console.log('[GeomPack VTK Dual Textured Viewer] Ready');
console.log('[GeomPack VTK Dual Textured Viewer] Loaded successfully');
</script>
</body>
</html>