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.
1245 lines
48 KiB
HTML
1245 lines
48 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>GeomPack VTK.js Dual Mesh Slider 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;
|
|
}
|
|
/* Two stacked render containers */
|
|
.render-layer {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
#mesh1Container {
|
|
z-index: 1;
|
|
}
|
|
#mesh2Container {
|
|
z-index: 2;
|
|
/* Will be clipped by JavaScript */
|
|
}
|
|
#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;
|
|
}
|
|
|
|
/* Slider overlay */
|
|
#sliderOverlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: 100;
|
|
}
|
|
#sliderLine {
|
|
position: absolute;
|
|
top: 0;
|
|
width: 2px;
|
|
height: 100%;
|
|
background: #00e0ff;
|
|
pointer-events: auto;
|
|
cursor: ew-resize;
|
|
box-shadow: 0 0 8px rgba(0, 224, 255, 0.5);
|
|
}
|
|
#sliderHandle {
|
|
position: absolute;
|
|
width: 20px;
|
|
height: 20px;
|
|
background: #00e0ff;
|
|
border: 2px solid white;
|
|
border-radius: 50%;
|
|
transform: translate(-50%, -50%);
|
|
pointer-events: auto;
|
|
cursor: ew-resize;
|
|
transition: transform 0.1s, box-shadow 0.1s;
|
|
box-shadow: 0 0 8px rgba(0, 224, 255, 0.5);
|
|
}
|
|
#sliderHandle:hover, #sliderHandle.dragging {
|
|
transform: translate(-50%, -50%) scale(1.2);
|
|
box-shadow: 0 0 15px rgba(0, 224, 255, 0.8);
|
|
}
|
|
.mesh-label {
|
|
position: absolute;
|
|
top: 8px;
|
|
color: white;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
text-shadow: 2px 2px 4px black;
|
|
pointer-events: none;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
padding: 4px 10px;
|
|
border-radius: 4px;
|
|
z-index: 101;
|
|
}
|
|
#labelMesh1 {
|
|
left: 12px;
|
|
}
|
|
#labelMesh2 {
|
|
right: 12px;
|
|
}
|
|
|
|
#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;
|
|
}
|
|
#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;
|
|
}
|
|
|
|
/* 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">
|
|
<!-- Bottom layer: Mesh 1 (fully visible) -->
|
|
<div id="mesh1Container" class="render-layer"></div>
|
|
<!-- Top layer: Mesh 2 (clipped by slider) -->
|
|
<div id="mesh2Container" class="render-layer"></div>
|
|
|
|
<!-- Slider overlay -->
|
|
<div id="sliderOverlay">
|
|
<div id="sliderLine"></div>
|
|
<div id="sliderHandle"></div>
|
|
<div class="mesh-label" id="labelMesh1">Mesh 1</div>
|
|
<div class="mesh-label" id="labelMesh2">Mesh 2</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">
|
|
<option value="erdc_rainbow_bright">Rainbow</option>
|
|
<option value="Cool to Warm">Cool to Warm</option>
|
|
<option value="Viridis (matplotlib)">Viridis</option>
|
|
<option value="Plasma (matplotlib)">Plasma</option>
|
|
<option value="Grayscale">Grayscale</option>
|
|
</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>
|
|
|
|
<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>
|
|
<button id="resetSlider">Reset Slider</button>
|
|
|
|
<span class="divider"></span>
|
|
|
|
<label id="fieldLabel" style="display:none;">
|
|
Field:
|
|
<select id="fieldSelector"></select>
|
|
</label>
|
|
</div>
|
|
|
|
<script src="./js/vtk-gltf.mjs"></script>
|
|
<script src="./js/viewer-bundle-vtk.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;
|
|
|
|
console.log('[GeomPack VTK Slider Viewer] Initializing...');
|
|
|
|
// Get FieldVisualization from viewer bundle
|
|
const { FieldVisualization } = window.GeomPackViewer || {};
|
|
|
|
const loading = document.getElementById('loading');
|
|
const mesh1Container = document.getElementById('mesh1Container');
|
|
const mesh2Container = document.getElementById('mesh2Container');
|
|
const sliderOverlay = document.getElementById('sliderOverlay');
|
|
const sliderLine = document.getElementById('sliderLine');
|
|
const sliderHandle = document.getElementById('sliderHandle');
|
|
|
|
// Viewer state
|
|
let renderer1, renderer2, renderWindow1, renderWindow2;
|
|
let fullScreenRenderer1, fullScreenRenderer2;
|
|
let interactor1, interactor2;
|
|
let mesh1Actor, mesh1Mapper, mesh2Actor, mesh2Mapper;
|
|
let mesh1Bounds, mesh2Bounds, combinedBounds;
|
|
let sliderPos = 0.5; // 0 = left edge, 1 = right edge
|
|
let isDragging = false;
|
|
|
|
// Field visualization state
|
|
let fieldVisualization1 = null;
|
|
let fieldVisualization2 = null;
|
|
let mesh1Polydata = null;
|
|
let mesh2Polydata = null;
|
|
|
|
// GLTF actors (stored separately since they replace the pre-made actors)
|
|
let mesh1GltfActors = null;
|
|
let mesh2GltfActors = null;
|
|
|
|
// Camera state persistence
|
|
let _cameraWheelTimer = null;
|
|
let _pendingCameraState = null;
|
|
|
|
function sendCameraState() {
|
|
const cam = renderer1?.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;
|
|
[renderer1, renderer2].forEach(r => {
|
|
if (!r) return;
|
|
const cam = r.getActiveCamera();
|
|
cam.setPosition(...cs.position);
|
|
cam.setFocalPoint(...cs.focalPoint);
|
|
cam.setViewUp(...cs.viewUp);
|
|
r.resetCameraClippingRange();
|
|
});
|
|
return true;
|
|
} catch(e) { return false; }
|
|
}
|
|
|
|
// Camera sync state
|
|
let isSyncing = false;
|
|
let isAnimating1 = false;
|
|
let isAnimating2 = false;
|
|
|
|
// Initialize both viewers
|
|
function initViewers() {
|
|
if (fullScreenRenderer1) return;
|
|
|
|
// Renderer 1 (mesh 1 - bottom layer)
|
|
fullScreenRenderer1 = vtkFullScreenRenderWindow.newInstance({
|
|
background: [0.16, 0.16, 0.16],
|
|
container: mesh1Container,
|
|
});
|
|
renderer1 = fullScreenRenderer1.getRenderer();
|
|
renderWindow1 = fullScreenRenderer1.getRenderWindow();
|
|
interactor1 = renderWindow1.getInteractor();
|
|
|
|
const style1 = vtkInteractorStyleTrackballCamera.newInstance();
|
|
interactor1.setInteractorStyle(style1);
|
|
|
|
mesh1Actor = vtkActor.newInstance();
|
|
mesh1Mapper = vtkMapper.newInstance();
|
|
mesh1Actor.setMapper(mesh1Mapper);
|
|
|
|
const prop1 = mesh1Actor.getProperty();
|
|
prop1.setColor(0.3, 0.5, 0.95); // Blue
|
|
prop1.setEdgeVisibility(false);
|
|
prop1.setAmbient(0.4);
|
|
prop1.setDiffuse(0.8);
|
|
prop1.setSpecular(0.3);
|
|
prop1.setSpecularPower(20);
|
|
|
|
renderer1.addActor(mesh1Actor);
|
|
|
|
// Renderer 2 (mesh 2 - top layer, will be clipped)
|
|
fullScreenRenderer2 = vtkFullScreenRenderWindow.newInstance({
|
|
background: [0.16, 0.16, 0.16],
|
|
container: mesh2Container,
|
|
});
|
|
renderer2 = fullScreenRenderer2.getRenderer();
|
|
renderWindow2 = fullScreenRenderer2.getRenderWindow();
|
|
interactor2 = renderWindow2.getInteractor();
|
|
|
|
const style2 = vtkInteractorStyleTrackballCamera.newInstance();
|
|
interactor2.setInteractorStyle(style2);
|
|
|
|
mesh2Actor = vtkActor.newInstance();
|
|
mesh2Mapper = vtkMapper.newInstance();
|
|
mesh2Actor.setMapper(mesh2Mapper);
|
|
|
|
const prop2 = mesh2Actor.getProperty();
|
|
prop2.setColor(0.3, 0.85, 0.4); // Green
|
|
prop2.setEdgeVisibility(false);
|
|
prop2.setAmbient(0.4);
|
|
prop2.setDiffuse(0.8);
|
|
prop2.setSpecular(0.3);
|
|
prop2.setSpecularPower(20);
|
|
|
|
renderer2.addActor(mesh2Actor);
|
|
|
|
fullScreenRenderer1.resize();
|
|
fullScreenRenderer2.resize();
|
|
|
|
setupCameraSync();
|
|
|
|
// Initialize field visualization if available
|
|
if (FieldVisualization) {
|
|
fieldVisualization1 = new FieldVisualization(vtk, renderWindow1);
|
|
fieldVisualization2 = new FieldVisualization(vtk, renderWindow2);
|
|
console.log('[GeomPack VTK Slider Viewer] Field visualization initialized');
|
|
}
|
|
|
|
console.log('[GeomPack VTK Slider Viewer] Viewers initialized');
|
|
}
|
|
|
|
// Camera synchronization
|
|
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() {
|
|
// Sync from renderer 1 to renderer 2
|
|
interactor1.onStartAnimation(() => { isAnimating1 = true; });
|
|
interactor1.onAnimation(() => {
|
|
if (isAnimating1 && !isAnimating2) {
|
|
synchronizeCameras(renderer1.getActiveCamera(), renderer2.getActiveCamera());
|
|
renderer2.resetCameraClippingRange();
|
|
renderWindow2.render();
|
|
}
|
|
});
|
|
interactor1.onEndAnimation(() => {
|
|
isAnimating1 = false;
|
|
if (!isAnimating2) {
|
|
synchronizeCameras(renderer1.getActiveCamera(), renderer2.getActiveCamera());
|
|
renderer2.resetCameraClippingRange();
|
|
renderWindow2.render();
|
|
sendCameraState();
|
|
}
|
|
});
|
|
|
|
// Sync from renderer 2 to renderer 1
|
|
interactor2.onStartAnimation(() => { isAnimating2 = true; });
|
|
interactor2.onAnimation(() => {
|
|
if (isAnimating2 && !isAnimating1) {
|
|
synchronizeCameras(renderer2.getActiveCamera(), renderer1.getActiveCamera());
|
|
renderer1.resetCameraClippingRange();
|
|
renderWindow1.render();
|
|
}
|
|
});
|
|
interactor2.onEndAnimation(() => {
|
|
isAnimating2 = false;
|
|
if (!isAnimating1) {
|
|
synchronizeCameras(renderer2.getActiveCamera(), renderer1.getActiveCamera());
|
|
renderer1.resetCameraClippingRange();
|
|
renderWindow1.render();
|
|
sendCameraState();
|
|
}
|
|
});
|
|
|
|
// Mouse wheel sync
|
|
setTimeout(() => {
|
|
const canvas1 = mesh1Container.querySelector('canvas');
|
|
const canvas2 = mesh2Container.querySelector('canvas');
|
|
|
|
if (canvas1) {
|
|
canvas1.addEventListener('wheel', () => {
|
|
setTimeout(() => {
|
|
if (!isAnimating2) {
|
|
synchronizeCameras(renderer1.getActiveCamera(), renderer2.getActiveCamera());
|
|
renderer2.resetCameraClippingRange();
|
|
renderWindow2.render();
|
|
}
|
|
}, 0);
|
|
clearTimeout(_cameraWheelTimer);
|
|
_cameraWheelTimer = setTimeout(sendCameraState, 150);
|
|
});
|
|
}
|
|
|
|
if (canvas2) {
|
|
canvas2.addEventListener('wheel', () => {
|
|
setTimeout(() => {
|
|
if (!isAnimating1) {
|
|
synchronizeCameras(renderer2.getActiveCamera(), renderer1.getActiveCamera());
|
|
renderer1.resetCameraClippingRange();
|
|
renderWindow1.render();
|
|
}
|
|
}, 0);
|
|
clearTimeout(_cameraWheelTimer);
|
|
_cameraWheelTimer = setTimeout(sendCameraState, 150);
|
|
});
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
// Load mesh function
|
|
async function loadMesh(filepath, targetMapper, isMesh1 = true) {
|
|
console.log(`[GeomPack VTK Slider Viewer] Loading mesh ${isMesh1 ? '1' : '2'}:`, 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) {
|
|
// GLTFImporter requires using its own actors - we can't extract polydata
|
|
// So we need to replace the pre-made actors with the GLTF actors
|
|
const targetRenderer = isMesh1 ? renderer1 : renderer2;
|
|
const targetActor = isMesh1 ? mesh1Actor : mesh2Actor;
|
|
|
|
// Clean up any previous GLTF actors
|
|
const prevGltfActors = isMesh1 ? mesh1GltfActors : mesh2GltfActors;
|
|
if (prevGltfActors) {
|
|
prevGltfActors.forEach(actor => targetRenderer.removeActor(actor));
|
|
}
|
|
|
|
const importer = vtkGLTFImporter.newInstance();
|
|
importer.setRenderer(targetRenderer);
|
|
importer.setUrl(filepath);
|
|
|
|
// Wait for GLTF parsing using onReady callback
|
|
await new Promise((resolve, reject) => {
|
|
importer.onReady(() => resolve());
|
|
importer.loadData().catch(reject);
|
|
});
|
|
|
|
// Import actors - this adds them to the renderer
|
|
importer.importActors();
|
|
const actorsMap = importer.getActors();
|
|
const actors = Array.from(actorsMap.values());
|
|
|
|
console.log(`[GeomPack VTK Slider Viewer] GLTF actors found: ${actors.length}`);
|
|
|
|
if (actors.length === 0) {
|
|
throw new Error('No actors found in GLTF');
|
|
}
|
|
|
|
// Remove the pre-made actor (it has no data)
|
|
targetRenderer.removeActor(targetActor);
|
|
|
|
// Calculate bounds from all GLTF actors and apply vertex color settings
|
|
let bounds = null;
|
|
actors.forEach((actor, idx) => {
|
|
const mapper = actor.getMapper();
|
|
const property = actor.getProperty();
|
|
const input = mapper?.getInputData();
|
|
|
|
if (input) {
|
|
const actorBounds = actor.getBounds();
|
|
if (!bounds) {
|
|
bounds = [...actorBounds];
|
|
} else {
|
|
bounds[0] = Math.min(bounds[0], actorBounds[0]);
|
|
bounds[1] = Math.max(bounds[1], actorBounds[1]);
|
|
bounds[2] = Math.min(bounds[2], actorBounds[2]);
|
|
bounds[3] = Math.max(bounds[3], actorBounds[3]);
|
|
bounds[4] = Math.min(bounds[4], actorBounds[4]);
|
|
bounds[5] = Math.max(bounds[5], actorBounds[5]);
|
|
}
|
|
|
|
// Check for vertex colors
|
|
const scalars = input.getPointData()?.getScalars();
|
|
if (scalars && scalars.getNumberOfComponents() >= 3) {
|
|
mapper.setScalarVisibility(true);
|
|
mapper.setScalarModeToUsePointData();
|
|
mapper.setColorModeToDirectScalars();
|
|
console.log(`[GeomPack VTK Slider Viewer] Mesh ${isMesh1 ? '1' : '2'} actor ${idx} has vertex colors`);
|
|
}
|
|
|
|
// Apply material fixes for better rendering
|
|
if (property) {
|
|
if (typeof property.setMetallic === 'function') property.setMetallic(0.0);
|
|
if (typeof property.setRoughness === 'function') property.setRoughness(1.0);
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`[GeomPack VTK Slider Viewer] Mesh ${isMesh1 ? '1' : '2'} bounds:`, bounds);
|
|
|
|
if (isMesh1) {
|
|
mesh1Bounds = bounds;
|
|
mesh1Polydata = null; // GLTF doesn't provide simple polydata access
|
|
mesh1GltfActors = actors; // Store for later reference
|
|
} else {
|
|
mesh2Bounds = bounds;
|
|
mesh2Polydata = null;
|
|
mesh2GltfActors = actors;
|
|
}
|
|
} 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');
|
|
}
|
|
|
|
const normalsFilter = vtkPolyDataNormals.newInstance();
|
|
normalsFilter.setInputConnection(reader.getOutputPort());
|
|
targetMapper.setInputConnection(normalsFilter.getOutputPort());
|
|
targetMapper.setScalarVisibility(false);
|
|
targetMapper.update();
|
|
|
|
const mapperPolydata = targetMapper.getInputData();
|
|
|
|
if (isMesh1) {
|
|
mesh1Bounds = mapperPolydata.getBounds();
|
|
mesh1Polydata = mapperPolydata;
|
|
} else {
|
|
mesh2Bounds = mapperPolydata.getBounds();
|
|
mesh2Polydata = mapperPolydata;
|
|
}
|
|
}
|
|
|
|
// Populate field selectors after both meshes are loaded
|
|
if (mesh1Polydata && mesh2Polydata && fieldVisualization1) {
|
|
populateFieldSelectors();
|
|
}
|
|
|
|
console.log(`[GeomPack VTK Slider Viewer] Mesh ${isMesh1 ? '1' : '2'} Loaded successfully`);
|
|
} catch (error) {
|
|
console.error(`[GeomPack VTK Slider Viewer] Error loading mesh ${isMesh1 ? '1' : '2'}:`, 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];
|
|
}
|
|
}
|
|
|
|
// Populate field selectors from polydata
|
|
function populateFieldSelectors() {
|
|
if (!fieldVisualization1 || !mesh1Polydata || !mesh2Polydata) return;
|
|
|
|
const fieldSelector = document.getElementById('fieldSelector');
|
|
const fieldLabel = document.getElementById('fieldLabel');
|
|
|
|
// Extract fields from both meshes
|
|
const fields1 = fieldVisualization1.extractFields(mesh1Polydata);
|
|
const fields2 = fieldVisualization1.extractFields(mesh2Polydata);
|
|
|
|
// Find common fields between both meshes
|
|
const commonFields = fields1.allFields.filter(f1 =>
|
|
fields2.allFields.some(f2 => f2.fullName === f1.fullName)
|
|
);
|
|
|
|
console.log(`[GeomPack VTK Slider Viewer] Found ${commonFields.length} common fields:`, commonFields.map(f => f.displayName));
|
|
|
|
if (commonFields.length > 0) {
|
|
fieldSelector.innerHTML = '<option value="">None</option>';
|
|
commonFields.forEach(field => {
|
|
const option = document.createElement('option');
|
|
option.value = field.fullName;
|
|
option.textContent = field.displayName;
|
|
fieldSelector.appendChild(option);
|
|
});
|
|
|
|
fieldLabel.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
// Apply field visualization to both meshes
|
|
function applyFieldVisualization(fieldName, colormap) {
|
|
if (!fieldVisualization1 || !fieldVisualization2) return;
|
|
|
|
if (!fieldName) {
|
|
// Reset to solid colors
|
|
const prop1 = mesh1Actor.getProperty();
|
|
const prop2 = mesh2Actor.getProperty();
|
|
prop1.setColor(0.3, 0.5, 0.95); // Blue
|
|
prop2.setColor(0.3, 0.85, 0.4); // Green
|
|
mesh1Mapper.setScalarVisibility(false);
|
|
mesh2Mapper.setScalarVisibility(false);
|
|
} else {
|
|
// Calculate synchronized range for both meshes
|
|
const range = fieldVisualization1.calculateSynchronizedRange(
|
|
[mesh1Polydata, mesh2Polydata], fieldName
|
|
);
|
|
|
|
// Apply to mesh 1
|
|
fieldVisualization1.applyField(mesh1Mapper, mesh1Polydata, fieldName, {
|
|
colormap: colormap,
|
|
range: range
|
|
});
|
|
|
|
// Apply to mesh 2
|
|
fieldVisualization2.applyField(mesh2Mapper, mesh2Polydata, fieldName, {
|
|
colormap: colormap,
|
|
range: range
|
|
});
|
|
}
|
|
|
|
renderWindow1.render();
|
|
renderWindow2.render();
|
|
}
|
|
|
|
// Field selector event handlers
|
|
document.getElementById('fieldSelector').addEventListener('change', (e) => {
|
|
const fieldName = e.target.value;
|
|
const colormap = document.getElementById('colormapSelector').value;
|
|
applyFieldVisualization(fieldName, colormap);
|
|
window.parent.postMessage({
|
|
type: 'WIDGET_UPDATE',
|
|
widget: 'selected_field',
|
|
value: fieldName
|
|
}, '*');
|
|
});
|
|
|
|
document.getElementById('colormapSelector').addEventListener('change', (e) => {
|
|
const fieldName = document.getElementById('fieldSelector').value;
|
|
const colormap = e.target.value;
|
|
if (fieldName) {
|
|
applyFieldVisualization(fieldName, colormap);
|
|
}
|
|
window.parent.postMessage({
|
|
type: 'WIDGET_UPDATE',
|
|
widget: 'selected_colormap',
|
|
value: e.target.value
|
|
}, '*');
|
|
});
|
|
|
|
// Update slider UI and CSS clip-path
|
|
function updateSlider() {
|
|
const containerRect = mesh1Container.getBoundingClientRect();
|
|
const xPixel = containerRect.width * sliderPos;
|
|
|
|
// Update slider line and handle position
|
|
sliderLine.style.left = `${xPixel}px`;
|
|
sliderHandle.style.left = `${xPixel}px`;
|
|
sliderHandle.style.top = `${containerRect.height / 2}px`;
|
|
|
|
// CSS clip-path: inset(top right bottom left)
|
|
// Clip mesh2 container to show only the RIGHT portion (from slider to right edge)
|
|
const clipLeft = sliderPos * 100;
|
|
mesh2Container.style.clipPath = `inset(0 0 0 ${clipLeft}%)`;
|
|
}
|
|
|
|
// Position camera at an angle
|
|
function positionCamera(bounds) {
|
|
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;
|
|
|
|
// Set both cameras to same position
|
|
[renderer1.getActiveCamera(), renderer2.getActiveCamera()].forEach(camera => {
|
|
camera.setPosition(
|
|
center[0] + distance,
|
|
center[1] + distance,
|
|
center[2] + distance
|
|
);
|
|
camera.setFocalPoint(center[0], center[1], center[2]);
|
|
camera.setViewUp(0, 0, 1);
|
|
});
|
|
|
|
renderer1.resetCamera(bounds);
|
|
renderer2.resetCamera(bounds);
|
|
renderer1.resetCameraClippingRange();
|
|
renderer2.resetCameraClippingRange();
|
|
}
|
|
|
|
// Slider drag handling
|
|
function startDrag(e) {
|
|
isDragging = true;
|
|
sliderHandle.classList.add('dragging');
|
|
e.preventDefault();
|
|
}
|
|
|
|
function onDrag(e) {
|
|
if (!isDragging) return;
|
|
|
|
const containerRect = mesh1Container.getBoundingClientRect();
|
|
let clientX;
|
|
|
|
if (e.touches) {
|
|
clientX = e.touches[0].clientX;
|
|
} else {
|
|
clientX = e.clientX;
|
|
}
|
|
|
|
const x = clientX - containerRect.left;
|
|
sliderPos = Math.max(0, Math.min(1, x / containerRect.width));
|
|
|
|
updateSlider();
|
|
}
|
|
|
|
function endDrag() {
|
|
isDragging = false;
|
|
sliderHandle.classList.remove('dragging');
|
|
}
|
|
|
|
// Set up slider event listeners
|
|
sliderHandle.addEventListener('mousedown', startDrag);
|
|
sliderLine.addEventListener('mousedown', startDrag);
|
|
document.addEventListener('mousemove', onDrag);
|
|
document.addEventListener('mouseup', endDrag);
|
|
|
|
// Touch support
|
|
sliderHandle.addEventListener('touchstart', startDrag);
|
|
sliderLine.addEventListener('touchstart', startDrag);
|
|
document.addEventListener('touchmove', onDrag);
|
|
document.addEventListener('touchend', endDrag);
|
|
|
|
// Click on overlay to jump slider
|
|
sliderOverlay.addEventListener('click', (e) => {
|
|
if (e.target === sliderOverlay) {
|
|
const containerRect = mesh1Container.getBoundingClientRect();
|
|
const x = e.clientX - containerRect.left;
|
|
sliderPos = Math.max(0, Math.min(1, x / containerRect.width));
|
|
updateSlider();
|
|
}
|
|
});
|
|
|
|
// Listen for messages from parent window
|
|
window.addEventListener('message', async (event) => {
|
|
if (event.data.type === 'LOAD_DUAL_MESH') {
|
|
const data = event.data;
|
|
|
|
console.log('[GeomPack VTK Slider Viewer] Received LOAD_DUAL_MESH message:', data);
|
|
|
|
loading.textContent = 'Loading meshes...';
|
|
loading.style.display = 'block';
|
|
|
|
try {
|
|
initViewers();
|
|
|
|
await loadMesh(data.mesh1Filepath, mesh1Mapper, true);
|
|
await loadMesh(data.mesh2Filepath, mesh2Mapper, false);
|
|
|
|
updateCombinedBounds();
|
|
|
|
// Reset slider to center
|
|
sliderPos = 0.5;
|
|
updateSlider();
|
|
|
|
_pendingCameraState = data.cameraState || null;
|
|
if (!applyCameraState(_pendingCameraState)) {
|
|
positionCamera(combinedBounds);
|
|
}
|
|
|
|
// Apply opacity values (handle both regular actors and GLTF actors)
|
|
if (data.opacity1 !== undefined) {
|
|
if (mesh1GltfActors) {
|
|
mesh1GltfActors.forEach(actor => actor.getProperty().setOpacity(data.opacity1));
|
|
} else {
|
|
mesh1Actor.getProperty().setOpacity(data.opacity1);
|
|
}
|
|
}
|
|
if (data.opacity2 !== undefined) {
|
|
if (mesh2GltfActors) {
|
|
mesh2GltfActors.forEach(actor => actor.getProperty().setOpacity(data.opacity2));
|
|
} else {
|
|
mesh2Actor.getProperty().setOpacity(data.opacity2);
|
|
}
|
|
}
|
|
|
|
// Apply showEdges from message data
|
|
const showEdges = data.showEdges || false;
|
|
if (mesh1GltfActors) {
|
|
mesh1GltfActors.forEach(actor => actor.getProperty().setEdgeVisibility(showEdges));
|
|
} else {
|
|
mesh1Actor.getProperty().setEdgeVisibility(showEdges);
|
|
}
|
|
if (mesh2GltfActors) {
|
|
mesh2GltfActors.forEach(actor => actor.getProperty().setEdgeVisibility(showEdges));
|
|
} else {
|
|
mesh2Actor.getProperty().setEdgeVisibility(showEdges);
|
|
}
|
|
document.getElementById('showEdges').checked = showEdges;
|
|
|
|
// 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 from parent widget state
|
|
const selectedField = data.selectedField || "";
|
|
if (selectedField && fieldVisualization1 && mesh1Polydata && mesh2Polydata) {
|
|
const fieldSelector = document.getElementById('fieldSelector');
|
|
if (fieldSelector && Array.from(fieldSelector.options).some(o => o.value === selectedField)) {
|
|
fieldSelector.value = selectedField;
|
|
const colormap = document.getElementById('colormapSelector').value;
|
|
applyFieldVisualization(selectedField, colormap);
|
|
}
|
|
}
|
|
|
|
renderWindow1.render();
|
|
renderWindow2.render();
|
|
|
|
loading.style.display = 'none';
|
|
console.log('[GeomPack VTK Slider Viewer] Meshes Loaded successfully');
|
|
|
|
} catch (error) {
|
|
console.error('[GeomPack VTK Slider Viewer] Error loading meshes:', error);
|
|
loading.textContent = `Error: ${error.message}`;
|
|
loading.style.color = '#ff6b6b';
|
|
}
|
|
}
|
|
});
|
|
|
|
// Control event handlers
|
|
document.getElementById('showEdges').addEventListener('change', (e) => {
|
|
const edgeViz = e.target.checked;
|
|
// Handle both regular actors and GLTF actors
|
|
if (mesh1GltfActors) {
|
|
mesh1GltfActors.forEach(actor => actor.getProperty().setEdgeVisibility(edgeViz));
|
|
} else {
|
|
mesh1Actor.getProperty().setEdgeVisibility(edgeViz);
|
|
}
|
|
if (mesh2GltfActors) {
|
|
mesh2GltfActors.forEach(actor => actor.getProperty().setEdgeVisibility(edgeViz));
|
|
} else {
|
|
mesh2Actor.getProperty().setEdgeVisibility(edgeViz);
|
|
}
|
|
renderWindow1.render();
|
|
renderWindow2.render();
|
|
|
|
window.parent.postMessage({ type: 'WIDGET_UPDATE', widget: 'show_edges', value: e.target.checked }, '*');
|
|
});
|
|
|
|
document.getElementById('resetCamera').addEventListener('click', () => {
|
|
if (!combinedBounds) return;
|
|
positionCamera(combinedBounds);
|
|
renderWindow1.render();
|
|
renderWindow2.render();
|
|
});
|
|
|
|
document.getElementById('resetSlider').addEventListener('click', () => {
|
|
sliderPos = 0.5;
|
|
updateSlider();
|
|
});
|
|
|
|
// 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;
|
|
}
|
|
|
|
[renderer1.getActiveCamera(), renderer2.getActiveCamera()].forEach(camera => {
|
|
camera.setPosition(...position);
|
|
camera.setFocalPoint(...center);
|
|
camera.setViewUp(...viewUp);
|
|
});
|
|
|
|
renderer1.resetCamera();
|
|
renderer2.resetCamera();
|
|
renderer1.resetCameraClippingRange();
|
|
renderer2.resetCameraClippingRange();
|
|
renderWindow1.render();
|
|
renderWindow2.render();
|
|
}
|
|
|
|
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'));
|
|
|
|
// 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;
|
|
|
|
// Apply to both mesh actors
|
|
[mesh1Actor, mesh2Actor].forEach(actor => {
|
|
if (!actor) return;
|
|
const property = actor.getProperty();
|
|
property.setPointSize(newPointSize);
|
|
property.setColor(r, g, b);
|
|
property.setLineWidth(newEdgeWidth);
|
|
property.setRepresentation(newRepresentation);
|
|
});
|
|
|
|
// Apply to GLTF actors if present
|
|
[mesh1GltfActors, mesh2GltfActors].forEach(actors => {
|
|
if (!actors) return;
|
|
actors.forEach(actor => {
|
|
const property = actor.getProperty();
|
|
property.setPointSize(newPointSize);
|
|
property.setLineWidth(newEdgeWidth);
|
|
property.setRepresentation(newRepresentation);
|
|
});
|
|
});
|
|
|
|
// Apply parallel projection
|
|
[renderer1, renderer2].forEach(renderer => {
|
|
if (!renderer) return;
|
|
renderer.getActiveCamera().setParallelProjection(newParallelProjection);
|
|
renderer.resetCameraClippingRange();
|
|
});
|
|
|
|
currentPointSize = newPointSize;
|
|
currentMeshColor = newColor;
|
|
currentEdgeWidth = newEdgeWidth;
|
|
currentRepresentation = newRepresentation;
|
|
currentParallelProjection = newParallelProjection;
|
|
|
|
renderWindow1?.render();
|
|
renderWindow2?.render();
|
|
closeSettingsPanel();
|
|
});
|
|
|
|
// Window resize
|
|
window.addEventListener('resize', () => {
|
|
fullScreenRenderer1?.resize();
|
|
fullScreenRenderer2?.resize();
|
|
updateSlider();
|
|
renderWindow1?.render();
|
|
renderWindow2?.render();
|
|
});
|
|
|
|
console.log('[GeomPack VTK Slider Viewer] Ready');
|
|
</script>
|
|
</body>
|
|
</html>
|