572 lines
21 KiB
HTML
572 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>GeomPack VTK.js Side-by-Side 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;
|
|
gap: 2px;
|
|
}
|
|
.viewport {
|
|
flex: 1;
|
|
position: relative;
|
|
background: #1a1a1a;
|
|
}
|
|
.viewport-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
}
|
|
.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;
|
|
}
|
|
#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;
|
|
}
|
|
#controls button {
|
|
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;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="viewerContainer">
|
|
<div class="viewport" id="leftViewport">
|
|
<div class="viewport-label">Left</div>
|
|
<div class="viewport-container" id="leftContainer"></div>
|
|
</div>
|
|
<div class="viewport" id="rightViewport">
|
|
<div class="viewport-label">Right</div>
|
|
<div class="viewport-container" id="rightContainer"></div>
|
|
</div>
|
|
<div id="loading">Ready to load meshes</div>
|
|
</div>
|
|
<div id="controls">
|
|
<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>
|
|
<label>
|
|
<input type="checkbox" id="parallelProjection"> Parallel
|
|
</label>
|
|
</div>
|
|
|
|
<script src="/extensions/ComfyUI-GeometryPack/js/vtk.js"></script>
|
|
|
|
<script>
|
|
// Get VTK.js classes from global vtk object
|
|
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;
|
|
|
|
console.log('[GeomPack VTK Side-by-Side Viewer] Initializing...');
|
|
|
|
const loading = document.getElementById('loading');
|
|
const leftContainer = document.getElementById('leftContainer');
|
|
const rightContainer = document.getElementById('rightContainer');
|
|
|
|
// Create two independent renderers
|
|
const leftFullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
|
|
background: [0.16, 0.16, 0.16],
|
|
container: leftContainer,
|
|
});
|
|
|
|
const rightFullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
|
|
background: [0.16, 0.16, 0.16],
|
|
container: rightContainer,
|
|
});
|
|
|
|
// Get renderers and render windows
|
|
const leftRenderer = leftFullScreenRenderer.getRenderer();
|
|
const rightRenderer = rightFullScreenRenderer.getRenderer();
|
|
const leftRenderWindow = leftFullScreenRenderer.getRenderWindow();
|
|
const rightRenderWindow = rightFullScreenRenderer.getRenderWindow();
|
|
const leftInteractor = leftRenderWindow.getInteractor();
|
|
const rightInteractor = rightRenderWindow.getInteractor();
|
|
|
|
// Set up trackball camera controls for both
|
|
const leftInteractorStyle = vtkInteractorStyleTrackballCamera.newInstance();
|
|
const rightInteractorStyle = vtkInteractorStyleTrackballCamera.newInstance();
|
|
leftInteractor.setInteractorStyle(leftInteractorStyle);
|
|
rightInteractor.setInteractorStyle(rightInteractorStyle);
|
|
|
|
// Create actors and mappers for both meshes
|
|
const leftActor = vtkActor.newInstance();
|
|
const rightActor = vtkActor.newInstance();
|
|
const leftMapper = vtkMapper.newInstance();
|
|
const rightMapper = vtkMapper.newInstance();
|
|
leftActor.setMapper(leftMapper);
|
|
rightActor.setMapper(rightMapper);
|
|
|
|
// Configure appearance for both actors
|
|
const leftProperty = leftActor.getProperty();
|
|
const rightProperty = rightActor.getProperty();
|
|
|
|
[leftProperty, rightProperty].forEach(property => {
|
|
property.setEdgeVisibility(false);
|
|
property.setColor(1.0, 1.0, 1.0);
|
|
property.setAmbient(0.4);
|
|
property.setDiffuse(0.8);
|
|
property.setSpecular(0.3);
|
|
property.setSpecularPower(20);
|
|
property.setRepresentation(2); // Surface rendering
|
|
});
|
|
|
|
// Add actors to their respective scenes
|
|
leftRenderer.addActor(leftActor);
|
|
rightRenderer.addActor(rightActor);
|
|
|
|
// Immediately resize both renderers
|
|
leftFullScreenRenderer.resize();
|
|
rightFullScreenRenderer.resize();
|
|
|
|
// Camera synchronization
|
|
let isSyncing = false; // Prevent infinite sync loops
|
|
let leftMeshBounds = null;
|
|
let rightMeshBounds = null;
|
|
let combinedBounds = null;
|
|
|
|
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());
|
|
|
|
isSyncing = false;
|
|
}
|
|
|
|
// Track animation state
|
|
let isLeftAnimating = false;
|
|
let isRightAnimating = false;
|
|
|
|
// Set up real-time bidirectional camera sync
|
|
leftInteractor.onStartAnimation(() => {
|
|
isLeftAnimating = true;
|
|
});
|
|
|
|
leftInteractor.onAnimation(() => {
|
|
if (isLeftAnimating && !isRightAnimating) {
|
|
const leftCamera = leftRenderer.getActiveCamera();
|
|
const rightCamera = rightRenderer.getActiveCamera();
|
|
synchronizeCameras(leftCamera, rightCamera);
|
|
rightRenderWindow.render();
|
|
}
|
|
});
|
|
|
|
leftInteractor.onEndAnimation(() => {
|
|
isLeftAnimating = false;
|
|
});
|
|
|
|
rightInteractor.onStartAnimation(() => {
|
|
isRightAnimating = true;
|
|
});
|
|
|
|
rightInteractor.onAnimation(() => {
|
|
if (isRightAnimating && !isLeftAnimating) {
|
|
const leftCamera = leftRenderer.getActiveCamera();
|
|
const rightCamera = rightRenderer.getActiveCamera();
|
|
synchronizeCameras(rightCamera, leftCamera);
|
|
leftRenderWindow.render();
|
|
}
|
|
});
|
|
|
|
rightInteractor.onEndAnimation(() => {
|
|
isRightAnimating = false;
|
|
});
|
|
|
|
console.log('[GeomPack VTK Side-by-Side Viewer] Camera synchronization enabled');
|
|
|
|
// Calculate combined bounds from both meshes
|
|
function updateCombinedBounds() {
|
|
if (leftMeshBounds && rightMeshBounds) {
|
|
combinedBounds = [
|
|
Math.min(leftMeshBounds[0], rightMeshBounds[0]),
|
|
Math.max(leftMeshBounds[1], rightMeshBounds[1]),
|
|
Math.min(leftMeshBounds[2], rightMeshBounds[2]),
|
|
Math.max(leftMeshBounds[3], rightMeshBounds[3]),
|
|
Math.min(leftMeshBounds[4], rightMeshBounds[4]),
|
|
Math.max(leftMeshBounds[5], rightMeshBounds[5])
|
|
];
|
|
console.log('[GeomPack VTK Side-by-Side Viewer] Combined bounds:', combinedBounds);
|
|
} else if (leftMeshBounds) {
|
|
combinedBounds = leftMeshBounds;
|
|
} else if (rightMeshBounds) {
|
|
combinedBounds = rightMeshBounds;
|
|
}
|
|
}
|
|
|
|
// Load mesh function
|
|
async function loadMesh(filepath, side) {
|
|
const isLeft = side === 'left';
|
|
const renderer = isLeft ? leftRenderer : rightRenderer;
|
|
const mapper = isLeft ? leftMapper : rightMapper;
|
|
const renderWindow = isLeft ? leftRenderWindow : rightRenderWindow;
|
|
|
|
console.log(`[GeomPack VTK Side-by-Side Viewer] Loading ${side} mesh:`, filepath);
|
|
loading.textContent = `Loading ${side} mesh...`;
|
|
loading.style.display = 'block';
|
|
|
|
try {
|
|
// Determine file type
|
|
const isSTL = filepath.includes('.stl');
|
|
const isOBJ = filepath.includes('.obj');
|
|
const isVTP = filepath.includes('.vtp');
|
|
|
|
console.log(`[GeomPack VTK Side-by-Side Viewer] ${side} file type - STL:`, isSTL, 'OBJ:', isOBJ, 'VTP:', isVTP);
|
|
|
|
// Create appropriate reader
|
|
let reader;
|
|
if (isSTL) {
|
|
reader = vtkSTLReader.newInstance();
|
|
} else if (isOBJ) {
|
|
reader = vtkOBJReader.newInstance();
|
|
} else if (isVTP) {
|
|
reader = vtkXMLPolyDataReader.newInstance();
|
|
} else {
|
|
throw new Error(`Unsupported file format: ${filepath}`);
|
|
}
|
|
|
|
// Fetch the file
|
|
const response = await fetch(filepath);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
// Parse the file
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
if (isSTL) {
|
|
reader.parseAsArrayBuffer(arrayBuffer);
|
|
} else if (isOBJ) {
|
|
const text = new TextDecoder().decode(arrayBuffer);
|
|
reader.parseAsText(text);
|
|
} else if (isVTP) {
|
|
reader.parseAsArrayBuffer(arrayBuffer);
|
|
}
|
|
|
|
// Get output data
|
|
const polydata = reader.getOutputData();
|
|
if (!polydata) {
|
|
throw new Error('Failed to parse mesh data');
|
|
}
|
|
|
|
const numPoints = polydata.getNumberOfPoints();
|
|
const numPolys = polydata.getNumberOfPolys();
|
|
console.log(`[GeomPack VTK Side-by-Side Viewer] ${side} mesh loaded:`, numPoints, 'points,', numPolys, 'polygons');
|
|
|
|
// Store bounds
|
|
const bounds = polydata.getBounds();
|
|
if (isLeft) {
|
|
leftMeshBounds = bounds;
|
|
} else {
|
|
rightMeshBounds = bounds;
|
|
}
|
|
updateCombinedBounds();
|
|
|
|
// Set up pipeline with normals filter
|
|
const normalsFilter = vtkPolyDataNormals.newInstance();
|
|
normalsFilter.setInputConnection(reader.getOutputPort());
|
|
mapper.setInputConnection(normalsFilter.getOutputPort());
|
|
mapper.update();
|
|
|
|
console.log(`[GeomPack VTK Side-by-Side Viewer] ${side} pipeline connected`);
|
|
|
|
// Reset camera to fit mesh (use combined bounds for consistent view)
|
|
const cameraBounds = combinedBounds || bounds;
|
|
renderer.resetCamera(cameraBounds);
|
|
|
|
// Position camera at an angle
|
|
const camera = renderer.getActiveCamera();
|
|
const center = [
|
|
(cameraBounds[0] + cameraBounds[1]) / 2,
|
|
(cameraBounds[2] + cameraBounds[3]) / 2,
|
|
(cameraBounds[4] + cameraBounds[5]) / 2
|
|
];
|
|
const maxDim = Math.max(
|
|
cameraBounds[1] - cameraBounds[0],
|
|
cameraBounds[3] - cameraBounds[2],
|
|
cameraBounds[5] - cameraBounds[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);
|
|
|
|
// Synchronize cameras after loading
|
|
if (isLeft && rightMeshBounds) {
|
|
synchronizeCameras(camera, rightRenderer.getActiveCamera());
|
|
} else if (!isLeft && leftMeshBounds) {
|
|
synchronizeCameras(camera, leftRenderer.getActiveCamera());
|
|
}
|
|
|
|
// Render
|
|
renderWindow.render();
|
|
|
|
// Force resize
|
|
requestAnimationFrame(() => {
|
|
if (isLeft) {
|
|
leftFullScreenRenderer.resize();
|
|
} else {
|
|
rightFullScreenRenderer.resize();
|
|
}
|
|
renderer.resetCameraClippingRange();
|
|
renderWindow.render();
|
|
});
|
|
|
|
// Hide loading message if both meshes are loaded
|
|
if (leftMeshBounds && rightMeshBounds) {
|
|
loading.style.display = 'none';
|
|
console.log('[GeomPack VTK Side-by-Side Viewer] Both meshes loaded successfully');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`[GeomPack VTK Side-by-Side Viewer] Error loading ${side} mesh:`, error);
|
|
loading.textContent = `Error loading ${side} mesh: ${error.message}`;
|
|
loading.style.color = '#ff6b6b';
|
|
|
|
if (window.parent) {
|
|
window.parent.postMessage({
|
|
type: 'MESH_ERROR',
|
|
error: error.message,
|
|
side: side
|
|
}, '*');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Listen for messages from parent window
|
|
window.addEventListener('message', (event) => {
|
|
console.log('[GeomPack VTK Side-by-Side Viewer] Received postMessage:', event.data);
|
|
|
|
if (event.data.type === 'LOAD_MESHES') {
|
|
console.log('[GeomPack VTK Side-by-Side Viewer] Loading meshes:', event.data);
|
|
if (event.data.leftFilepath) {
|
|
loadMesh(event.data.leftFilepath, 'left');
|
|
}
|
|
if (event.data.rightFilepath) {
|
|
loadMesh(event.data.rightFilepath, 'right');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle window resize
|
|
window.addEventListener('resize', () => {
|
|
leftFullScreenRenderer.resize();
|
|
rightFullScreenRenderer.resize();
|
|
leftRenderWindow.render();
|
|
rightRenderWindow.render();
|
|
});
|
|
|
|
// Control event handlers
|
|
document.getElementById('showEdges').addEventListener('change', (e) => {
|
|
leftProperty.setEdgeVisibility(e.target.checked);
|
|
rightProperty.setEdgeVisibility(e.target.checked);
|
|
leftRenderWindow.render();
|
|
rightRenderWindow.render();
|
|
});
|
|
|
|
document.getElementById('parallelProjection').addEventListener('change', (e) => {
|
|
const leftCamera = leftRenderer.getActiveCamera();
|
|
const rightCamera = rightRenderer.getActiveCamera();
|
|
leftCamera.setParallelProjection(e.target.checked);
|
|
rightCamera.setParallelProjection(e.target.checked);
|
|
leftRenderWindow.render();
|
|
rightRenderWindow.render();
|
|
});
|
|
|
|
// Camera view buttons
|
|
function setCameraView(direction) {
|
|
if (!combinedBounds) return;
|
|
|
|
const bounds = combinedBounds;
|
|
const leftCamera = leftRenderer.getActiveCamera();
|
|
const rightCamera = rightRenderer.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;
|
|
|
|
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;
|
|
}
|
|
|
|
// Set both cameras
|
|
[leftCamera, rightCamera].forEach(camera => {
|
|
camera.setPosition(...position);
|
|
camera.setFocalPoint(...center);
|
|
camera.setViewUp(...viewUp);
|
|
});
|
|
|
|
leftRenderer.resetCamera();
|
|
rightRenderer.resetCamera();
|
|
leftRenderWindow.render();
|
|
rightRenderWindow.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'));
|
|
|
|
document.getElementById('resetCamera').addEventListener('click', () => {
|
|
if (!combinedBounds) return;
|
|
|
|
const leftCamera = leftRenderer.getActiveCamera();
|
|
const rightCamera = rightRenderer.getActiveCamera();
|
|
|
|
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;
|
|
|
|
[leftCamera, rightCamera].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);
|
|
});
|
|
|
|
leftRenderer.resetCamera();
|
|
rightRenderer.resetCamera();
|
|
leftRenderWindow.render();
|
|
rightRenderWindow.render();
|
|
});
|
|
|
|
console.log('[GeomPack VTK Side-by-Side Viewer] Ready');
|
|
</script>
|
|
</body>
|
|
</html>
|