382 lines
14 KiB
HTML
382 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>DepthAnythingV3 VTK Point Cloud Viewer</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background: #1a1a1a;
|
|
font-family: monospace;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
}
|
|
#viewerContainer {
|
|
flex: 1;
|
|
position: relative;
|
|
min-height: 0;
|
|
}
|
|
#container {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
}
|
|
#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;
|
|
background: rgba(0,0,0,0.8);
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
}
|
|
#error {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: white;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
z-index: 1000;
|
|
pointer-events: none;
|
|
background: rgba(180,0,0,0.9);
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
max-width: 80%;
|
|
display: none;
|
|
}
|
|
#info {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: rgba(0,0,0,0.7);
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-size: 12px;
|
|
z-index: 100;
|
|
}
|
|
#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 label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
#controls input[type="range"] {
|
|
width: 80px;
|
|
}
|
|
#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;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="viewerContainer">
|
|
<div id="container"></div>
|
|
<div id="loading">Ready to load point cloud</div>
|
|
<div id="error"></div>
|
|
<div id="info">
|
|
<div>Left: Rotate | Right: Pan</div>
|
|
<div>Scroll: Zoom</div>
|
|
<div id="point-count">Points: 0</div>
|
|
</div>
|
|
</div>
|
|
<div id="controls">
|
|
<label>
|
|
Point Size:
|
|
<input type="range" id="pointSize" min="1" max="20" value="3" step="1">
|
|
<span id="pointSizeValue">3</span>
|
|
</label>
|
|
<label style="width: 1px; height: 20px; background: #444; margin: 0 8px;"></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 style="width: 1px; height: 20px; background: #444; margin: 0 8px;"></label>
|
|
<button id="resetCamera">Reset View</button>
|
|
</div>
|
|
|
|
<script src="/extensions/ComfyUI-DepthAnythingV3/libs/vtk.min.lib"></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 vtkPLYReader = vtk.IO.Geometry.vtkPLYReader;
|
|
const vtkInteractorStyleTrackballCamera = vtk.Interaction.Style.vtkInteractorStyleTrackballCamera;
|
|
const vtkOrientationMarkerWidget = vtk.Interaction.Widgets.vtkOrientationMarkerWidget;
|
|
const vtkAnnotatedCubeActor = vtk.Rendering.Core.vtkAnnotatedCubeActor;
|
|
|
|
console.log('[DA3 VTK Viewer] Initializing...');
|
|
|
|
const loading = document.getElementById('loading');
|
|
const errorDiv = document.getElementById('error');
|
|
const container = document.getElementById('container');
|
|
const pointCountDiv = document.getElementById('point-count');
|
|
const pointSizeSlider = document.getElementById('pointSize');
|
|
const pointSizeValue = document.getElementById('pointSizeValue');
|
|
|
|
const MAX_POINTS = 2000000;
|
|
|
|
// Create full screen renderer
|
|
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
|
|
background: [0.1, 0.1, 0.1],
|
|
container: container,
|
|
});
|
|
|
|
fullScreenRenderer.resize();
|
|
|
|
const renderer = fullScreenRenderer.getRenderer();
|
|
const renderWindow = fullScreenRenderer.getRenderWindow();
|
|
const interactor = renderWindow.getInteractor();
|
|
|
|
// Set up trackball camera controls
|
|
const interactorStyle = vtkInteractorStyleTrackballCamera.newInstance();
|
|
interactor.setInteractorStyle(interactorStyle);
|
|
|
|
// Create actor and mapper
|
|
const actor = vtkActor.newInstance();
|
|
const mapper = vtkMapper.newInstance();
|
|
actor.setMapper(mapper);
|
|
|
|
// Configure for point cloud rendering
|
|
const property = actor.getProperty();
|
|
property.setPointSize(3);
|
|
property.setRepresentation(0); // Points mode
|
|
|
|
renderer.addActor(actor);
|
|
|
|
// Create axis orientation widget
|
|
const axes = vtkAnnotatedCubeActor.newInstance();
|
|
axes.setDefaultStyle({
|
|
fontStyle: 'bold',
|
|
fontFamily: 'Arial',
|
|
fontColor: 'white',
|
|
fontSizeScale: (res) => res / 2,
|
|
faceRotation: 0,
|
|
edgeThickness: 0.1,
|
|
edgeColor: 'white',
|
|
resolution: 400,
|
|
});
|
|
|
|
axes.setXPlusFaceProperty({ text: '+X', faceColor: '#ff5555' });
|
|
axes.setXMinusFaceProperty({ text: '-X', faceColor: '#ff5555' });
|
|
axes.setYPlusFaceProperty({ text: '+Y', faceColor: '#55ff55' });
|
|
axes.setYMinusFaceProperty({ text: '-Y', faceColor: '#55ff55' });
|
|
axes.setZPlusFaceProperty({ text: '+Z', faceColor: '#5555ff' });
|
|
axes.setZMinusFaceProperty({ text: '-Z', faceColor: '#5555ff' });
|
|
|
|
const orientationWidget = vtkOrientationMarkerWidget.newInstance({
|
|
actor: axes,
|
|
interactor: renderWindow.getInteractor(),
|
|
});
|
|
orientationWidget.setEnabled(true);
|
|
orientationWidget.setViewportCorner(vtkOrientationMarkerWidget.Corners.BOTTOM_LEFT);
|
|
orientationWidget.setViewportSize(0.15);
|
|
orientationWidget.setMinPixelSize(100);
|
|
orientationWidget.setMaxPixelSize(300);
|
|
|
|
let currentReader = null;
|
|
let meshBounds = null;
|
|
|
|
function showError(message) {
|
|
loading.style.display = 'none';
|
|
errorDiv.textContent = message;
|
|
errorDiv.style.display = 'block';
|
|
}
|
|
|
|
function hideError() {
|
|
errorDiv.style.display = 'none';
|
|
}
|
|
|
|
// Load point cloud function
|
|
async function loadPointCloud(filepath) {
|
|
console.log('[DA3 VTK Viewer] Loading:', filepath);
|
|
loading.textContent = 'Loading point cloud...';
|
|
loading.style.display = 'block';
|
|
hideError();
|
|
|
|
try {
|
|
currentReader = vtkPLYReader.newInstance();
|
|
|
|
const response = await fetch(filepath);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
console.log('[DA3 VTK Viewer] File size:', arrayBuffer.byteLength);
|
|
|
|
currentReader.parseAsArrayBuffer(arrayBuffer);
|
|
|
|
const polydata = currentReader.getOutputData();
|
|
if (!polydata) {
|
|
throw new Error('Failed to parse PLY file');
|
|
}
|
|
|
|
const numPoints = polydata.getNumberOfPoints();
|
|
console.log('[DA3 VTK Viewer] Loaded:', numPoints, 'points');
|
|
|
|
// Check point limit
|
|
if (numPoints > MAX_POINTS) {
|
|
throw new Error(`Point cloud too large: ${numPoints.toLocaleString()} points\nMaximum: ${MAX_POINTS.toLocaleString()} points`);
|
|
}
|
|
|
|
pointCountDiv.textContent = `Points: ${numPoints.toLocaleString()}`;
|
|
|
|
meshBounds = polydata.getBounds();
|
|
console.log('[DA3 VTK Viewer] Bounds:', meshBounds);
|
|
|
|
// Connect mapper directly to reader
|
|
mapper.setInputConnection(currentReader.getOutputPort());
|
|
mapper.update();
|
|
|
|
// Check if we have vertex colors
|
|
const scalars = polydata.getPointData().getScalars();
|
|
if (scalars) {
|
|
console.log('[DA3 VTK Viewer] Point colors found:', scalars.getName());
|
|
mapper.setScalarVisibility(true);
|
|
} else {
|
|
console.log('[DA3 VTK Viewer] No point colors, using white');
|
|
mapper.setScalarVisibility(false);
|
|
property.setColor(1.0, 1.0, 1.0);
|
|
}
|
|
|
|
// Reset camera
|
|
renderer.resetCamera();
|
|
|
|
const camera = renderer.getActiveCamera();
|
|
const center = [
|
|
(meshBounds[0] + meshBounds[1]) / 2,
|
|
(meshBounds[2] + meshBounds[3]) / 2,
|
|
(meshBounds[4] + meshBounds[5]) / 2
|
|
];
|
|
const maxDim = Math.max(
|
|
meshBounds[1] - meshBounds[0],
|
|
meshBounds[3] - meshBounds[2],
|
|
meshBounds[5] - meshBounds[4]
|
|
);
|
|
|
|
const distance = maxDim * 2.0;
|
|
camera.setPosition(
|
|
center[0] + distance * 0.5,
|
|
center[1] + distance * 0.5,
|
|
center[2] + distance
|
|
);
|
|
camera.setFocalPoint(center[0], center[1], center[2]);
|
|
camera.setViewUp(0, 1, 0);
|
|
|
|
renderWindow.render();
|
|
|
|
requestAnimationFrame(() => {
|
|
fullScreenRenderer.resize();
|
|
renderer.resetCameraClippingRange();
|
|
renderWindow.render();
|
|
interactor.enable();
|
|
});
|
|
|
|
loading.style.display = 'none';
|
|
console.log('[DA3 VTK Viewer] Load complete');
|
|
|
|
} catch (error) {
|
|
console.error('[DA3 VTK Viewer] Error:', error);
|
|
showError(error.message);
|
|
}
|
|
}
|
|
|
|
// Point size control
|
|
pointSizeSlider.addEventListener('input', () => {
|
|
const size = parseInt(pointSizeSlider.value);
|
|
pointSizeValue.textContent = size;
|
|
property.setPointSize(size);
|
|
renderWindow.render();
|
|
});
|
|
|
|
// Camera view controls
|
|
function setCameraView(position, up) {
|
|
if (!meshBounds) return;
|
|
const camera = renderer.getActiveCamera();
|
|
const center = [
|
|
(meshBounds[0] + meshBounds[1]) / 2,
|
|
(meshBounds[2] + meshBounds[3]) / 2,
|
|
(meshBounds[4] + meshBounds[5]) / 2
|
|
];
|
|
const maxDim = Math.max(
|
|
meshBounds[1] - meshBounds[0],
|
|
meshBounds[3] - meshBounds[2],
|
|
meshBounds[5] - meshBounds[4]
|
|
);
|
|
const distance = maxDim * 2.0;
|
|
|
|
camera.setPosition(
|
|
center[0] + position[0] * distance,
|
|
center[1] + position[1] * distance,
|
|
center[2] + position[2] * distance
|
|
);
|
|
camera.setFocalPoint(center[0], center[1], center[2]);
|
|
camera.setViewUp(up[0], up[1], up[2]);
|
|
renderer.resetCameraClippingRange();
|
|
renderWindow.render();
|
|
}
|
|
|
|
document.getElementById('viewPosX').addEventListener('click', () => setCameraView([1, 0, 0], [0, 1, 0]));
|
|
document.getElementById('viewNegX').addEventListener('click', () => setCameraView([-1, 0, 0], [0, 1, 0]));
|
|
document.getElementById('viewPosY').addEventListener('click', () => setCameraView([0, 1, 0], [0, 0, -1]));
|
|
document.getElementById('viewNegY').addEventListener('click', () => setCameraView([0, -1, 0], [0, 0, 1]));
|
|
document.getElementById('viewPosZ').addEventListener('click', () => setCameraView([0, 0, 1], [0, 1, 0]));
|
|
document.getElementById('viewNegZ').addEventListener('click', () => setCameraView([0, 0, -1], [0, 1, 0]));
|
|
document.getElementById('resetCamera').addEventListener('click', () => {
|
|
if (!meshBounds) return;
|
|
setCameraView([0.5, 0.5, 1], [0, 1, 0]);
|
|
});
|
|
|
|
// Handle resize
|
|
window.addEventListener('resize', () => {
|
|
fullScreenRenderer.resize();
|
|
renderWindow.render();
|
|
});
|
|
|
|
// Listen for messages from parent
|
|
window.addEventListener('message', (event) => {
|
|
console.log('[DA3 VTK Viewer] Received message:', event.data);
|
|
if (event.data.type === 'loadPointCloud' && event.data.url) {
|
|
loadPointCloud(event.data.url);
|
|
}
|
|
});
|
|
|
|
console.log('[DA3 VTK Viewer] Ready');
|
|
</script>
|
|
</body>
|
|
</html>
|