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.
1222 lines
49 KiB
HTML
1222 lines
49 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;
|
|
}
|
|
|
|
/* 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" class="grid-1x1">
|
|
<!-- Viewports will be dynamically created -->
|
|
|
|
<!-- 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"></select>
|
|
</div>
|
|
<div class="button-row">
|
|
<button id="applySettings">Apply</button>
|
|
<button id="cancelSettings">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="loading">Ready to load meshes</div>
|
|
|
|
<div id="controls">
|
|
<!-- Settings button -->
|
|
<button id="settingsButton" title="Appearance Settings">[Settings]</button>
|
|
|
|
<span class="divider"></span>
|
|
|
|
<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 id="channelLabel" style="display:none;">
|
|
Channel:
|
|
<select id="channelSelector">
|
|
<option value="magnitude">Magnitude</option>
|
|
</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 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');
|
|
|
|
// --- mesh download progress bar (aggregate across parallel loads) ---
|
|
let _dlBar, _dlFill, _dl = {};
|
|
function _ensureDlBar() {
|
|
if (_dlBar) return;
|
|
_dlBar = document.createElement('div');
|
|
_dlBar.style.cssText = 'position:absolute;bottom:12px;left:50%;transform:translateX(-50%);' +
|
|
'width:220px;height:8px;background:rgba(255,255,255,0.2);border-radius:4px;overflow:hidden;' +
|
|
'z-index:1001;display:none;';
|
|
_dlFill = document.createElement('div');
|
|
_dlFill.style.cssText = 'width:0%;height:100%;background:#00c8ff;transition:width .1s linear;';
|
|
_dlBar.appendChild(_dlFill);
|
|
document.body.appendChild(_dlBar);
|
|
}
|
|
function setDownloadBar(frac) {
|
|
_ensureDlBar();
|
|
if (frac == null || isNaN(frac)) { _dlBar.style.display = 'none'; return; }
|
|
_dlBar.style.display = 'block';
|
|
_dlFill.style.width = Math.max(0, Math.min(100, Math.round(frac * 100))) + '%';
|
|
}
|
|
function _dlReset() { _dl = {}; }
|
|
function _dlUpdate(key, loaded, total) {
|
|
_dl[key] = { loaded: loaded || 0, total: total || 0 };
|
|
let L = 0, T = 0;
|
|
for (const k in _dl) { L += _dl[k].loaded; T += _dl[k].total; }
|
|
if (T > 0) {
|
|
const f = Math.min(1, L / T);
|
|
setDownloadBar(f);
|
|
if (loading) loading.textContent = `Loading meshes… ${Math.round(f * 100)}%`;
|
|
}
|
|
}
|
|
async function fetchWithProgress(url, key) {
|
|
const resp = await fetch(url);
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
|
const total = Number(resp.headers.get('Content-Length')) || 0;
|
|
if (!resp.body || !total) { if (total) _dlUpdate(key, total, total); return resp.arrayBuffer(); }
|
|
const reader = resp.body.getReader();
|
|
const chunks = []; let loaded = 0; _dlUpdate(key, 0, total);
|
|
for (;;) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
chunks.push(value); loaded += value.length; _dlUpdate(key, loaded, total);
|
|
}
|
|
const out = new Uint8Array(loaded); let off = 0;
|
|
for (const c of chunks) { out.set(c, off); off += c.length; }
|
|
return out.buffer;
|
|
}
|
|
|
|
// 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
|
|
let fieldComponentMap = {}; // Maps field name -> number of components
|
|
|
|
// Send camera state to parent node widget (uses first viewport's camera)
|
|
let _cameraWheelTimer = null;
|
|
function sendCameraState() {
|
|
if (viewports.length === 0) return;
|
|
const cam = viewports[0].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;
|
|
viewports.forEach(vp => {
|
|
const cam = vp.renderer.getActiveCamera();
|
|
cam.setPosition(...cs.position);
|
|
cam.setFocalPoint(...cs.focalPoint);
|
|
cam.setViewUp(...cs.viewUp);
|
|
vp.renderer.resetCameraClippingRange();
|
|
vp.renderWindow.render();
|
|
});
|
|
return true;
|
|
} catch(e) { return false; }
|
|
}
|
|
|
|
// Helper function to render with proper clipping range update
|
|
function renderWithClippingRangeUpdate(renderer, renderWindow) {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
renderer.resetCameraClippingRange();
|
|
renderWindow.render();
|
|
});
|
|
});
|
|
}
|
|
|
|
// Create viewport containers dynamically. gridCols/gridRows are optional
|
|
// explicit grid dimensions (e.g. from a batch-input node that lets the user
|
|
// pick rows/cols directly); when absent, fall back to the fixed count-based
|
|
// layouts below so existing callers are unaffected.
|
|
function createViewportContainers(count, gridCols, gridRows) {
|
|
// Clear existing viewports but preserve settings panel elements
|
|
const overlay = document.getElementById('settingsOverlay');
|
|
const panel = document.getElementById('settingsPanel');
|
|
viewerContainer.innerHTML = '';
|
|
if (overlay) viewerContainer.appendChild(overlay);
|
|
if (panel) viewerContainer.appendChild(panel);
|
|
viewports = [];
|
|
|
|
viewerContainer.className = '';
|
|
viewerContainer.style.gridTemplateColumns = '';
|
|
viewerContainer.style.gridTemplateRows = '';
|
|
if (gridCols && gridRows) {
|
|
viewerContainer.style.gridTemplateColumns = `repeat(${gridCols}, 1fr)`;
|
|
viewerContainer.style.gridTemplateRows = `repeat(${gridRows}, 1fr)`;
|
|
} else 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;
|
|
sendCameraState();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Mouse wheel sync
|
|
setTimeout(() => {
|
|
viewports.forEach((vp, index) => {
|
|
const canvas = vp.container.querySelector('canvas');
|
|
if (canvas) {
|
|
canvas.addEventListener('wheel', () => {
|
|
setTimeout(() => synchronizeCameras(index), 0);
|
|
clearTimeout(_cameraWheelTimer);
|
|
_cameraWheelTimer = setTimeout(sendCameraState, 150);
|
|
});
|
|
}
|
|
});
|
|
}, 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 updateChannelSelector() {
|
|
const fieldName = document.getElementById('fieldSelector').value;
|
|
const channelSelector = document.getElementById('channelSelector');
|
|
const channelLabel = document.getElementById('channelLabel');
|
|
|
|
const numComponents = fieldComponentMap[fieldName] || 1;
|
|
|
|
if (!fieldName || fieldName === 'none' || numComponents <= 1) {
|
|
channelLabel.style.display = 'none';
|
|
channelSelector.value = 'magnitude';
|
|
return;
|
|
}
|
|
|
|
channelSelector.innerHTML = '<option value="magnitude">Magnitude</option>';
|
|
if (numComponents === 3) {
|
|
channelSelector.innerHTML += '<option value="0">X</option>';
|
|
channelSelector.innerHTML += '<option value="1">Y</option>';
|
|
channelSelector.innerHTML += '<option value="2">Z</option>';
|
|
} else {
|
|
for (let i = 0; i < numComponents; i++) {
|
|
channelSelector.innerHTML += `<option value="${i}">Component ${i}</option>`;
|
|
}
|
|
}
|
|
|
|
channelLabel.style.display = '';
|
|
}
|
|
|
|
function extractComponentAsField(polydata, fieldName, componentIdx) {
|
|
const vtkDataArray = vtk.Common.DataModel.vtkDataArray || vtk.Common.Core.vtkDataArray;
|
|
const isCellData = fieldName.startsWith('face.');
|
|
const actualName = isCellData ? fieldName.substring(5) : fieldName;
|
|
const data = isCellData ? polydata.getCellData() : polydata.getPointData();
|
|
const srcArray = data.getArrayByName(actualName);
|
|
|
|
if (!srcArray) return fieldName;
|
|
|
|
const numComponents = srcArray.getNumberOfComponents();
|
|
const numTuples = srcArray.getNumberOfTuples();
|
|
const srcData = srcArray.getData();
|
|
const extracted = new Float32Array(numTuples);
|
|
|
|
for (let i = 0; i < numTuples; i++) {
|
|
extracted[i] = srcData[i * numComponents + componentIdx];
|
|
}
|
|
|
|
const channelNames = numComponents === 3 ? ['X', 'Y', 'Z'] : null;
|
|
const suffix = channelNames ? channelNames[componentIdx] : `c${componentIdx}`;
|
|
const newName = `${actualName}_${suffix}`;
|
|
|
|
const existing = data.getArrayByName(newName);
|
|
if (!existing) {
|
|
const newArray = vtkDataArray.newInstance({
|
|
name: newName,
|
|
values: extracted,
|
|
numberOfComponents: 1,
|
|
});
|
|
data.addArray(newArray);
|
|
} else {
|
|
existing.setData(extracted);
|
|
}
|
|
|
|
return (isCellData ? 'face.' : '') + newName;
|
|
}
|
|
|
|
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>';
|
|
fieldComponentMap = {};
|
|
commonFields.forEach(field => {
|
|
const option = document.createElement('option');
|
|
option.value = field;
|
|
option.textContent = field;
|
|
selector.appendChild(option);
|
|
// Use component count from first viewport that has this field
|
|
for (const vp of viewports) {
|
|
if (vp.fieldComponentCounts && vp.fieldComponentCounts[field] != null) {
|
|
fieldComponentMap[field] = vp.fieldComponentCounts[field];
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Check if a specific component channel is selected
|
|
const channelValue = document.getElementById('channelSelector')?.value || 'magnitude';
|
|
const numComponents = fieldComponentMap[fieldName] || 1;
|
|
const useComponent = numComponents > 1 && channelValue !== 'magnitude';
|
|
const componentIdx = useComponent ? parseInt(channelValue) : -1;
|
|
|
|
// If extracting a component, do it for each viewport first
|
|
let effectiveFieldName = fieldName;
|
|
if (useComponent) {
|
|
viewports.forEach(vp => {
|
|
if (vp.polydata) {
|
|
effectiveFieldName = extractComponentAsField(vp.polydata, fieldName, componentIdx);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Calculate shared range from all meshes
|
|
let sharedMin = Infinity;
|
|
let sharedMax = -Infinity;
|
|
|
|
const isCellData = effectiveFieldName.startsWith('face.');
|
|
const actualFieldName = isCellData ? effectiveFieldName.substring(5) : effectiveFieldName;
|
|
|
|
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, effectiveFieldName, 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 arrayBuffer = await fetchWithProgress(filepath, viewportIndex);
|
|
|
|
if (isGLB) {
|
|
reader.parseAsArrayBuffer(arrayBuffer);
|
|
await reader.update();
|
|
const source = reader.getOutputData();
|
|
vp.mapper.setInputData(source);
|
|
vp.mapper.setScalarVisibility(false);
|
|
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 = [];
|
|
vp.fieldComponentCounts = {};
|
|
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());
|
|
vp.fieldComponentCounts[array.getName()] = array.getNumberOfComponents();
|
|
}
|
|
for (let i = 0; i < cellData.getNumberOfArrays(); i++) {
|
|
const array = cellData.getArray(i);
|
|
const name = `face.${array.getName()}`;
|
|
vp.fieldNames.push(name);
|
|
vp.fieldComponentCounts[name] = array.getNumberOfComponents();
|
|
}
|
|
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.setScalarVisibility(false);
|
|
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';
|
|
_dlReset();
|
|
|
|
try {
|
|
// Create viewport containers
|
|
createViewportContainers(numMeshes, data.gridCols, data.gridRows);
|
|
|
|
// Initialize VTK renderers
|
|
initializeViewports();
|
|
|
|
// Load all meshes
|
|
await Promise.all(meshFiles.map((filepath, index) => loadMesh(filepath, index)));
|
|
|
|
// Update combined bounds and position cameras
|
|
updateCombinedBounds();
|
|
|
|
// Restore camera state or auto-position
|
|
const _pendingCameraState = data.cameraState || null;
|
|
if (!applyCameraState(_pendingCameraState)) {
|
|
viewports.forEach(vp => {
|
|
positionCamera(vp.renderer, combinedBounds);
|
|
renderWithClippingRangeUpdate(vp.renderer, vp.renderWindow);
|
|
});
|
|
} else {
|
|
viewports.forEach(vp => {
|
|
renderWithClippingRangeUpdate(vp.renderer, vp.renderWindow);
|
|
});
|
|
}
|
|
|
|
// Populate field and colormap selectors
|
|
populateColormapSelector();
|
|
populateFieldSelector();
|
|
|
|
// Restore colormap from parent widget state
|
|
const selectedColormap = data.selectedColormap || "";
|
|
if (selectedColormap) {
|
|
const colormapSel = document.getElementById('colormapSelector');
|
|
if (colormapSel && Array.from(colormapSel.options).some(o => o.value === selectedColormap)) {
|
|
colormapSel.value = selectedColormap;
|
|
}
|
|
}
|
|
|
|
// Restore selected field, channel from parent widget state
|
|
const selectedField = data.selectedField || "";
|
|
if (selectedField) {
|
|
const fieldSel = document.getElementById('fieldSelector');
|
|
if (fieldSel && Array.from(fieldSel.options).some(o => o.value === selectedField)) {
|
|
fieldSel.value = selectedField;
|
|
updateChannelSelector();
|
|
// Restore channel
|
|
const selectedChannel = data.selectedChannel || "";
|
|
if (selectedChannel) {
|
|
const channelSel = document.getElementById('channelSelector');
|
|
if (channelSel && Array.from(channelSel.options).some(o => o.value === selectedChannel)) {
|
|
channelSel.value = selectedChannel;
|
|
}
|
|
}
|
|
applyFieldToAllViewports(selectedField);
|
|
}
|
|
}
|
|
|
|
// Apply showEdges from message data
|
|
const showEdges = data.showEdges || false;
|
|
viewports.forEach(vp => {
|
|
vp.actor.getProperty().setEdgeVisibility(showEdges);
|
|
vp.renderWindow.render();
|
|
});
|
|
document.getElementById('showEdges').checked = showEdges;
|
|
|
|
loading.style.display = 'none';
|
|
setDownloadBar(null);
|
|
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';
|
|
setDownloadBar(null);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Event handlers
|
|
document.getElementById('showEdges').addEventListener('change', (e) => {
|
|
const edgeViz = e.target.checked;
|
|
viewports.forEach(vp => {
|
|
vp.actor.getProperty().setEdgeVisibility(edgeViz);
|
|
vp.renderWindow.render();
|
|
});
|
|
|
|
window.parent.postMessage({ type: 'WIDGET_UPDATE', widget: 'show_edges', value: e.target.checked }, '*');
|
|
});
|
|
|
|
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) => {
|
|
updateChannelSelector();
|
|
applyFieldToAllViewports(e.target.value);
|
|
window.parent.postMessage({
|
|
type: 'WIDGET_UPDATE',
|
|
widget: 'selected_field',
|
|
value: e.target.value
|
|
}, '*');
|
|
});
|
|
|
|
// Channel selector change
|
|
document.getElementById('channelSelector').addEventListener('change', () => {
|
|
const currentField = document.getElementById('fieldSelector').value;
|
|
if (currentField && currentField !== 'none') {
|
|
applyFieldToAllViewports(currentField);
|
|
}
|
|
window.parent.postMessage({
|
|
type: 'WIDGET_UPDATE',
|
|
widget: 'selected_channel',
|
|
value: document.getElementById('channelSelector').value
|
|
}, '*');
|
|
});
|
|
|
|
// Colormap selector change
|
|
document.getElementById('colormapSelector').addEventListener('change', () => {
|
|
const currentField = document.getElementById('fieldSelector').value;
|
|
if (currentField && currentField !== 'none') {
|
|
applyFieldToAllViewports(currentField);
|
|
}
|
|
window.parent.postMessage({
|
|
type: 'WIDGET_UPDATE',
|
|
widget: 'selected_colormap',
|
|
value: document.getElementById('colormapSelector').value
|
|
}, '*');
|
|
});
|
|
|
|
// 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');
|
|
|
|
let currentPointSize = 3;
|
|
let currentMeshColor = '#99ccff';
|
|
let currentEdgeWidth = 1;
|
|
let currentRepresentation = 2;
|
|
let currentParallelProjection = false;
|
|
|
|
settingsButton.addEventListener('click', () => {
|
|
pointSizeInput.value = currentPointSize;
|
|
meshColorInput.value = currentMeshColor;
|
|
edgeWidthInput.value = currentEdgeWidth;
|
|
representationSettings.value = currentRepresentation.toString();
|
|
parallelProjectionInput.checked = currentParallelProjection;
|
|
|
|
settingsPanel.classList.add('visible');
|
|
settingsOverlay.classList.add('visible');
|
|
});
|
|
|
|
function closeSettingsPanel() {
|
|
settingsPanel.classList.remove('visible');
|
|
settingsOverlay.classList.remove('visible');
|
|
}
|
|
|
|
settingsOverlay.addEventListener('click', closeSettingsPanel);
|
|
cancelSettingsButton.addEventListener('click', closeSettingsPanel);
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
viewports.forEach(vp => {
|
|
if (!vp.actor) return;
|
|
const property = vp.actor.getProperty();
|
|
property.setPointSize(newPointSize);
|
|
property.setColor(r, g, b);
|
|
property.setLineWidth(newEdgeWidth);
|
|
property.setRepresentation(newRepresentation);
|
|
|
|
if (vp.renderer) {
|
|
vp.renderer.getActiveCamera().setParallelProjection(newParallelProjection);
|
|
vp.renderer.resetCameraClippingRange();
|
|
}
|
|
vp.renderWindow?.render();
|
|
});
|
|
|
|
currentPointSize = newPointSize;
|
|
currentMeshColor = newColor;
|
|
currentEdgeWidth = newEdgeWidth;
|
|
currentRepresentation = newRepresentation;
|
|
currentParallelProjection = newParallelProjection;
|
|
|
|
closeSettingsPanel();
|
|
});
|
|
|
|
// Window resize
|
|
window.addEventListener('resize', () => {
|
|
viewports.forEach(vp => {
|
|
vp.fullScreenRenderer?.resize();
|
|
vp.renderWindow?.render();
|
|
});
|
|
});
|
|
|
|
console.log('[GeomPack VTK Multi Viewer] Ready');
|
|
</script>
|
|
</body>
|
|
</html>
|