666 lines
26 KiB
HTML
666 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>GeomPack VTK.js Viewer (Unified)</title>
|
|
<link rel="stylesheet" href="./js/viewer/ui/styles.css">
|
|
</head>
|
|
<body>
|
|
<div id="viewerContainer">
|
|
<div id="container"></div>
|
|
<div id="loading" class="loading">
|
|
<div class="spinner"></div>
|
|
<div class="loading-text">Ready to load mesh</div>
|
|
</div>
|
|
|
|
<!-- Settings overlay and panel -->
|
|
<div id="settingsOverlay" class="settings-overlay"></div>
|
|
<div id="settingsPanel" class="settings-panel">
|
|
<h3>Appearance Settings</h3>
|
|
<div class="setting-row">
|
|
<label>Point Size:</label>
|
|
<input type="number" id="pointSize" min="1" max="20" step="1" value="5">
|
|
</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="parallelProjection"> Parallel Projection
|
|
</label>
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>
|
|
<input type="checkbox" id="showAxisIndicator"> Show Axis Indicator
|
|
</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 Range:</label>
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>
|
|
<input type="checkbox" id="autoColorRange" checked> Auto Range
|
|
</label>
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Min:</label>
|
|
<input type="number" id="colormapMin" step="any" placeholder="Auto" style="width: 120px;">
|
|
</div>
|
|
<div class="setting-row">
|
|
<label>Max:</label>
|
|
<input type="number" id="colormapMax" step="any" placeholder="Auto" style="width: 120px;">
|
|
</div>
|
|
<div class="button-row">
|
|
<button id="applySettings">Apply</button>
|
|
<button id="cancelSettings">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="controls">
|
|
<button id="settingsButton" class="settings-button" title="Appearance Settings">⚙</button>
|
|
|
|
<span class="control-separator"></span>
|
|
|
|
<label class="control-label">
|
|
<input type="checkbox" id="showEdges"> Edges
|
|
</label>
|
|
|
|
<span class="control-separator"></span>
|
|
|
|
<!-- Camera view buttons -->
|
|
<button class="view-button" data-view="+X">+X</button>
|
|
<button class="view-button" data-view="-X">-X</button>
|
|
<button class="view-button" data-view="+Y">+Y</button>
|
|
<button class="view-button" data-view="-Y">-Y</button>
|
|
<button class="view-button" data-view="+Z">+Z</button>
|
|
<button class="view-button" data-view="-Z">-Z</button>
|
|
|
|
<span class="control-separator"></span>
|
|
|
|
<label id="fieldLabel" class="control-label" style="display:none;">
|
|
Field:
|
|
<select id="fieldSelector"></select>
|
|
</label>
|
|
<label id="colormapLabel" class="control-label" style="display:none;">
|
|
Colormap:
|
|
<select id="colormapSelector"></select>
|
|
</label>
|
|
|
|
<button id="screenshot">Screenshot</button>
|
|
<button id="saveMesh" class="save-button">Save Mesh</button>
|
|
</div>
|
|
|
|
<!-- Load VTK.js bundle -->
|
|
<script src="./js/vtk-gltf.js"></script>
|
|
<!-- Load unified viewer bundle -->
|
|
<script src="./js/viewer-bundle.js"></script>
|
|
|
|
<script>
|
|
// Use unified viewer modules
|
|
const {
|
|
CameraController,
|
|
ActorManager,
|
|
FieldVisualization,
|
|
ScreenshotCapture,
|
|
AxisIndicator,
|
|
loadMesh,
|
|
detectFormat,
|
|
sendMeshLoaded,
|
|
sendError,
|
|
extractFilename,
|
|
hexToRgb,
|
|
rgbToString
|
|
} = window.GeomPackViewer;
|
|
|
|
console.log('[GeomPack VTK Viewer] Initializing unified viewer...');
|
|
|
|
// DOM elements
|
|
const loading = document.getElementById('loading');
|
|
const loadingText = loading.querySelector('.loading-text');
|
|
const container = document.getElementById('container');
|
|
|
|
// VTK setup
|
|
const vtkRenderWindow = vtk.Rendering.Core.vtkRenderWindow;
|
|
const vtkRenderer = vtk.Rendering.Core.vtkRenderer;
|
|
const vtkOpenGLRenderWindow = vtk.Rendering.OpenGL.vtkRenderWindow;
|
|
const vtkRenderWindowInteractor = vtk.Rendering.Core.vtkRenderWindowInteractor;
|
|
const vtkInteractorStyleTrackballCamera = vtk.Interaction.Style.vtkInteractorStyleTrackballCamera;
|
|
const vtkActor = vtk.Rendering.Core.vtkActor;
|
|
const vtkMapper = vtk.Rendering.Core.vtkMapper;
|
|
const vtkPolyDataNormals = vtk.Filters.Core.vtkPolyDataNormals;
|
|
|
|
// Create render window
|
|
const renderWindow = vtkRenderWindow.newInstance();
|
|
const renderer = vtkRenderer.newInstance();
|
|
renderer.setBackground(0.16, 0.16, 0.16);
|
|
renderWindow.addRenderer(renderer);
|
|
|
|
// Create OpenGL view
|
|
const openGLRenderWindow = vtkOpenGLRenderWindow.newInstance();
|
|
openGLRenderWindow.setContainer(container);
|
|
renderWindow.addView(openGLRenderWindow);
|
|
|
|
// Resize to container
|
|
const { width, height } = container.getBoundingClientRect();
|
|
openGLRenderWindow.setSize(width, height);
|
|
|
|
// Create interactor
|
|
const interactor = vtkRenderWindowInteractor.newInstance();
|
|
interactor.setView(openGLRenderWindow);
|
|
interactor.initialize();
|
|
interactor.bindEvents(container);
|
|
|
|
const interactorStyle = vtkInteractorStyleTrackballCamera.newInstance();
|
|
interactor.setInteractorStyle(interactorStyle);
|
|
|
|
// Initialize modules
|
|
const cameraController = new CameraController(renderer, renderWindow, {
|
|
distanceMultiplier: 2.5
|
|
});
|
|
|
|
const actorManager = new ActorManager(renderWindow);
|
|
|
|
const fieldVisualization = new FieldVisualization(vtk, renderWindow);
|
|
const screenshotCapture = new ScreenshotCapture(renderWindow);
|
|
|
|
// Create axis indicator (disabled by default)
|
|
const axisIndicator = new AxisIndicator(vtk, interactor, {
|
|
enabled: false,
|
|
corner: 'BOTTOM_LEFT'
|
|
});
|
|
|
|
// Create single actor/mapper for mesh
|
|
const actor = vtkActor.newInstance();
|
|
const mapper = vtkMapper.newInstance();
|
|
actor.setMapper(mapper);
|
|
|
|
// Configure default appearance
|
|
const property = actor.getProperty();
|
|
property.setEdgeVisibility(false);
|
|
property.setColor(0.6, 0.8, 1.0); // Light blue
|
|
property.setAmbient(0.4);
|
|
property.setDiffuse(0.8);
|
|
property.setSpecular(0.3);
|
|
property.setSpecularPower(20);
|
|
property.setRepresentation(2);
|
|
|
|
renderer.addActor(actor);
|
|
|
|
// State
|
|
let currentReader = null;
|
|
let currentFilename = null;
|
|
let meshBounds = null;
|
|
let currentPolyData = null;
|
|
let isCurrentDataPointCloud = false;
|
|
|
|
// Settings state
|
|
let currentSettings = {
|
|
pointSize: 5,
|
|
meshColor: '#99ccff',
|
|
edgeWidth: 1,
|
|
representation: 2,
|
|
parallelProjection: false,
|
|
showAxisIndicator: false
|
|
};
|
|
|
|
// Show/hide loading
|
|
function showLoading(show, text = 'Loading mesh...') {
|
|
loading.style.display = show ? 'flex' : 'none';
|
|
loadingText.textContent = text;
|
|
}
|
|
|
|
// Load mesh function
|
|
async function loadMeshFile(filepath, lineWidth = 1.0) {
|
|
console.log('[GeomPack VTK Viewer] Loading:', filepath);
|
|
showLoading(true, 'Loading mesh...');
|
|
|
|
// Extract filename
|
|
const urlParams = new URLSearchParams(filepath.split('?')[1] || '');
|
|
currentFilename = urlParams.get('filename') || filepath.split('/').pop().split('?')[0];
|
|
|
|
try {
|
|
// Determine file type and create reader
|
|
const isSTL = filepath.includes('.stl');
|
|
const isOBJ = filepath.includes('.obj');
|
|
const isVTP = filepath.includes('.vtp');
|
|
|
|
if (!isSTL && !isOBJ && !isVTP) {
|
|
throw new Error('Unsupported format. Use STL, OBJ, or VTP.');
|
|
}
|
|
|
|
// Fetch file
|
|
const response = await fetch(filepath);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
|
|
// Create appropriate reader
|
|
if (isSTL) {
|
|
currentReader = vtk.IO.Geometry.vtkSTLReader.newInstance();
|
|
currentReader.parseAsArrayBuffer(arrayBuffer);
|
|
} else if (isOBJ) {
|
|
currentReader = vtk.IO.Misc.vtkOBJReader.newInstance();
|
|
const text = new TextDecoder().decode(arrayBuffer);
|
|
currentReader.parseAsText(text);
|
|
} else if (isVTP) {
|
|
currentReader = vtk.IO.XML.vtkXMLPolyDataReader.newInstance();
|
|
currentReader.parseAsArrayBuffer(arrayBuffer);
|
|
}
|
|
|
|
const polydata = currentReader.getOutputData();
|
|
if (!polydata) {
|
|
throw new Error('Failed to parse mesh data');
|
|
}
|
|
|
|
currentPolyData = polydata;
|
|
meshBounds = polydata.getBounds();
|
|
|
|
// Detect point cloud (no polygons)
|
|
const numPolys = polydata.getNumberOfPolys();
|
|
isCurrentDataPointCloud = numPolys === 0;
|
|
|
|
if (isCurrentDataPointCloud) {
|
|
console.log('[GeomPack VTK Viewer] Detected point cloud, skipping normals filter');
|
|
// Connect mapper directly to polydata (no normals filter for point clouds)
|
|
mapper.setInputData(polydata);
|
|
|
|
// Lock representation to Points mode
|
|
property.setRepresentation(0);
|
|
property.setPointSize(currentSettings.pointSize);
|
|
} else {
|
|
// For meshes, use normals filter for proper shading
|
|
const normalsFilter = vtkPolyDataNormals.newInstance();
|
|
normalsFilter.setInputData(polydata);
|
|
normalsFilter.update();
|
|
// Use the filtered polydata so scalars are preserved
|
|
const filteredPolyData = normalsFilter.getOutputData();
|
|
mapper.setInputData(filteredPolyData);
|
|
currentPolyData = filteredPolyData; // Update reference for field visualization
|
|
}
|
|
mapper.update();
|
|
|
|
property.setLineWidth(lineWidth);
|
|
|
|
// Populate field selectors
|
|
populateFieldSelectors(polydata);
|
|
|
|
// Position camera
|
|
cameraController.positionInitialCamera(meshBounds);
|
|
|
|
// Render
|
|
renderWindow.render();
|
|
|
|
// Force resize and re-render
|
|
requestAnimationFrame(() => {
|
|
openGLRenderWindow.setSize(container.clientWidth, container.clientHeight);
|
|
renderer.resetCameraClippingRange();
|
|
renderWindow.render();
|
|
interactor.enable();
|
|
});
|
|
|
|
showLoading(false);
|
|
console.log('[GeomPack VTK Viewer] Mesh loaded:', polydata.getNumberOfPoints(), 'points');
|
|
|
|
} catch (error) {
|
|
console.error('[GeomPack VTK Viewer] Error:', error);
|
|
showLoading(true, `Error: ${error.message}`);
|
|
loading.style.color = '#ff6b6b';
|
|
|
|
if (window.parent) {
|
|
window.parent.postMessage({ type: 'MESH_ERROR', error: error.message }, '*');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Populate field selectors from polydata
|
|
function populateFieldSelectors(polydata) {
|
|
const fieldSelector = document.getElementById('fieldSelector');
|
|
const fieldLabel = document.getElementById('fieldLabel');
|
|
const colormapSelector = document.getElementById('colormapSelector');
|
|
const colormapLabel = document.getElementById('colormapLabel');
|
|
|
|
fieldSelector.innerHTML = '<option value="">None</option>';
|
|
|
|
// Extract fields using FieldVisualization module
|
|
const fieldInfo = fieldVisualization.extractFields(polydata);
|
|
|
|
if (fieldInfo.hasFields) {
|
|
fieldInfo.allFields.forEach(field => {
|
|
const option = document.createElement('option');
|
|
option.value = field.fullName;
|
|
option.textContent = field.displayName;
|
|
fieldSelector.appendChild(option);
|
|
});
|
|
|
|
fieldLabel.style.display = 'flex';
|
|
|
|
// Populate colormap selector
|
|
const colormapOptions = fieldVisualization.getColormapOptions(true);
|
|
colormapSelector.innerHTML = '';
|
|
colormapOptions.forEach(opt => {
|
|
const option = document.createElement('option');
|
|
option.value = opt.value;
|
|
option.textContent = opt.label;
|
|
if (opt.disabled) option.disabled = true;
|
|
if (opt.value === 'erdc_rainbow_bright') option.selected = true;
|
|
colormapSelector.appendChild(option);
|
|
});
|
|
|
|
colormapLabel.style.display = 'flex';
|
|
|
|
// Default to "None" - don't auto-select any field
|
|
// fieldSelector.value stays as "" (None option)
|
|
} else {
|
|
fieldLabel.style.display = 'none';
|
|
colormapLabel.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Update colormap visualization
|
|
function updateColormap() {
|
|
const fieldName = document.getElementById('fieldSelector').value;
|
|
if (!fieldName || !currentPolyData) {
|
|
mapper.setScalarVisibility(false);
|
|
property.setColor(0.6, 0.8, 1.0); // Light blue
|
|
renderWindow.render();
|
|
return;
|
|
}
|
|
|
|
const colormapName = document.getElementById('colormapSelector').value || 'erdc_rainbow_bright';
|
|
const autoRange = document.getElementById('autoColorRange').checked;
|
|
|
|
let range = null;
|
|
if (!autoRange) {
|
|
const minVal = parseFloat(document.getElementById('colormapMin').value);
|
|
const maxVal = parseFloat(document.getElementById('colormapMax').value);
|
|
if (!isNaN(minVal) && !isNaN(maxVal)) {
|
|
range = [minVal, maxVal];
|
|
}
|
|
}
|
|
|
|
fieldVisualization.applyField(mapper, currentPolyData, fieldName, {
|
|
colormap: colormapName,
|
|
range: range,
|
|
render: false
|
|
});
|
|
|
|
// Update range inputs if auto
|
|
if (autoRange) {
|
|
const isCellData = fieldName.startsWith('cell:');
|
|
const actualName = isCellData ? fieldName.substring(5) : fieldName.substring(6);
|
|
const data = isCellData ? currentPolyData.getCellData() : currentPolyData.getPointData();
|
|
const array = data.getArrayByName(actualName);
|
|
if (array) {
|
|
const dataRange = array.getRange();
|
|
document.getElementById('colormapMin').value = dataRange[0].toFixed(3);
|
|
document.getElementById('colormapMax').value = dataRange[1].toFixed(3);
|
|
}
|
|
}
|
|
|
|
renderWindow.render();
|
|
}
|
|
|
|
// Camera view
|
|
function setCameraView(direction) {
|
|
if (!meshBounds) return;
|
|
cameraController.setCameraView(direction, meshBounds);
|
|
renderWindow.render();
|
|
}
|
|
|
|
// Event listeners - Camera views
|
|
document.querySelectorAll('.view-button').forEach(button => {
|
|
button.addEventListener('click', () => setCameraView(button.dataset.view));
|
|
});
|
|
|
|
// Edges toggle
|
|
document.getElementById('showEdges').addEventListener('change', (e) => {
|
|
property.setEdgeVisibility(e.target.checked);
|
|
renderWindow.render();
|
|
});
|
|
|
|
// Field selector
|
|
document.getElementById('fieldSelector').addEventListener('change', updateColormap);
|
|
|
|
// Colormap selector
|
|
document.getElementById('colormapSelector').addEventListener('change', updateColormap);
|
|
|
|
// Auto range toggle
|
|
document.getElementById('autoColorRange').addEventListener('change', (e) => {
|
|
document.getElementById('colormapMin').disabled = e.target.checked;
|
|
document.getElementById('colormapMax').disabled = e.target.checked;
|
|
updateColormap();
|
|
});
|
|
|
|
// Manual range inputs
|
|
document.getElementById('colormapMin').addEventListener('change', () => {
|
|
if (!document.getElementById('autoColorRange').checked) updateColormap();
|
|
});
|
|
document.getElementById('colormapMax').addEventListener('change', () => {
|
|
if (!document.getElementById('autoColorRange').checked) updateColormap();
|
|
});
|
|
|
|
// Screenshot
|
|
document.getElementById('screenshot').addEventListener('click', async () => {
|
|
renderWindow.render();
|
|
const image = await screenshotCapture.capture();
|
|
window.parent.postMessage({
|
|
type: 'SCREENSHOT',
|
|
image: image,
|
|
timestamp: Date.now()
|
|
}, '*');
|
|
});
|
|
|
|
// Save mesh
|
|
document.getElementById('saveMesh').addEventListener('click', async () => {
|
|
if (!currentFilename) {
|
|
alert('No mesh loaded.');
|
|
return;
|
|
}
|
|
|
|
const saveButton = document.getElementById('saveMesh');
|
|
const originalText = saveButton.textContent;
|
|
|
|
try {
|
|
saveButton.disabled = true;
|
|
saveButton.textContent = 'Saving...';
|
|
|
|
const response = await fetch('/geometrypack/save_preview', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ temp_filename: currentFilename })
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
saveButton.textContent = '✓ Saved!';
|
|
alert(`Mesh saved!\n\nFilename: ${result.saved_filename}\nLocation: ComfyUI/output/`);
|
|
} else {
|
|
throw new Error(result.error);
|
|
}
|
|
} catch (error) {
|
|
saveButton.textContent = '✗ Error';
|
|
alert(`Error: ${error.message}`);
|
|
} finally {
|
|
setTimeout(() => {
|
|
saveButton.textContent = originalText;
|
|
saveButton.disabled = false;
|
|
}, 2000);
|
|
}
|
|
});
|
|
|
|
// Settings panel
|
|
const settingsButton = document.getElementById('settingsButton');
|
|
const settingsPanel = document.getElementById('settingsPanel');
|
|
const settingsOverlay = document.getElementById('settingsOverlay');
|
|
|
|
settingsButton.addEventListener('click', () => {
|
|
document.getElementById('pointSize').value = currentSettings.pointSize;
|
|
document.getElementById('meshColor').value = currentSettings.meshColor;
|
|
document.getElementById('edgeWidth').value = currentSettings.edgeWidth;
|
|
document.getElementById('representationSettings').value = currentSettings.representation;
|
|
document.getElementById('parallelProjection').checked = currentSettings.parallelProjection;
|
|
document.getElementById('showAxisIndicator').checked = currentSettings.showAxisIndicator;
|
|
|
|
// Lock representation for point clouds
|
|
const representationSelect = document.getElementById('representationSettings');
|
|
if (isCurrentDataPointCloud) {
|
|
representationSelect.value = '0';
|
|
representationSelect.disabled = true;
|
|
representationSelect.title = 'Point clouds can only be rendered in Points mode';
|
|
} else {
|
|
representationSelect.disabled = false;
|
|
representationSelect.title = '';
|
|
}
|
|
|
|
settingsPanel.classList.add('visible');
|
|
settingsOverlay.classList.add('visible');
|
|
});
|
|
|
|
function closeSettings() {
|
|
settingsPanel.classList.remove('visible');
|
|
settingsOverlay.classList.remove('visible');
|
|
}
|
|
|
|
settingsOverlay.addEventListener('click', closeSettings);
|
|
document.getElementById('cancelSettings').addEventListener('click', closeSettings);
|
|
|
|
document.getElementById('applySettings').addEventListener('click', () => {
|
|
const newPointSize = parseFloat(document.getElementById('pointSize').value);
|
|
const newColor = document.getElementById('meshColor').value;
|
|
const newEdgeWidth = parseFloat(document.getElementById('edgeWidth').value);
|
|
const newRepresentation = parseInt(document.getElementById('representationSettings').value);
|
|
const newParallel = document.getElementById('parallelProjection').checked;
|
|
const newAxisIndicator = document.getElementById('showAxisIndicator').checked;
|
|
|
|
// Convert hex to RGB
|
|
const rgb = hexToRgb(newColor);
|
|
|
|
// Disable scalar viz to show custom color
|
|
mapper.setScalarVisibility(false);
|
|
|
|
property.setColor(rgb[0], rgb[1], rgb[2]);
|
|
property.setLineWidth(newEdgeWidth);
|
|
property.setPointSize(newPointSize);
|
|
|
|
// Only apply representation change for non-point-cloud data
|
|
if (!isCurrentDataPointCloud) {
|
|
property.setRepresentation(newRepresentation);
|
|
}
|
|
|
|
cameraController.setParallelProjection(newParallel);
|
|
axisIndicator.setEnabled(newAxisIndicator);
|
|
|
|
// Update stored settings
|
|
currentSettings = {
|
|
pointSize: newPointSize,
|
|
meshColor: newColor,
|
|
edgeWidth: newEdgeWidth,
|
|
representation: newRepresentation,
|
|
parallelProjection: newParallel,
|
|
showAxisIndicator: newAxisIndicator
|
|
};
|
|
|
|
renderWindow.render();
|
|
closeSettings();
|
|
});
|
|
|
|
// Resize handling
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
const { width, height } = container.getBoundingClientRect();
|
|
if (width > 0 && height > 0) {
|
|
openGLRenderWindow.setSize(width, height);
|
|
renderWindow.render();
|
|
}
|
|
});
|
|
resizeObserver.observe(container);
|
|
|
|
// Focus camera on a specific 3D point
|
|
function focusOnPoint(point, viewRadius = null) {
|
|
const camera = renderer.getActiveCamera();
|
|
|
|
// Get current camera direction (from focal point to camera position)
|
|
const currentFocalPoint = camera.getFocalPoint();
|
|
const currentPosition = camera.getPosition();
|
|
const direction = [
|
|
currentPosition[0] - currentFocalPoint[0],
|
|
currentPosition[1] - currentFocalPoint[1],
|
|
currentPosition[2] - currentFocalPoint[2]
|
|
];
|
|
|
|
// Normalize direction
|
|
const length = Math.sqrt(direction[0]**2 + direction[1]**2 + direction[2]**2);
|
|
if (length > 0) {
|
|
direction[0] /= length;
|
|
direction[1] /= length;
|
|
direction[2] /= length;
|
|
} else {
|
|
// Default to looking from +Z if no current direction
|
|
direction[0] = 0;
|
|
direction[1] = 0;
|
|
direction[2] = 1;
|
|
}
|
|
|
|
// Calculate view distance based on mesh bounds
|
|
let distance;
|
|
if (viewRadius !== null) {
|
|
distance = viewRadius * 2.5;
|
|
} else if (meshBounds) {
|
|
const maxDim = Math.max(
|
|
meshBounds[1] - meshBounds[0],
|
|
meshBounds[3] - meshBounds[2],
|
|
meshBounds[5] - meshBounds[4]
|
|
);
|
|
distance = maxDim * 0.3; // Closer view for focused point
|
|
} else {
|
|
distance = 10; // Fallback
|
|
}
|
|
|
|
// Position camera along the direction from the point
|
|
const newPosition = [
|
|
point[0] + direction[0] * distance,
|
|
point[1] + direction[1] * distance,
|
|
point[2] + direction[2] * distance
|
|
];
|
|
|
|
camera.setFocalPoint(...point);
|
|
camera.setPosition(...newPosition);
|
|
|
|
renderer.resetCameraClippingRange();
|
|
renderWindow.render();
|
|
}
|
|
|
|
// Listen for postMessage from parent
|
|
window.addEventListener('message', (event) => {
|
|
if (event.data.type === 'LOAD_MESH') {
|
|
const lineWidth = event.data.lineWidth || 1.0;
|
|
loadMeshFile(event.data.filepath, lineWidth);
|
|
} else if (event.data.type === 'FOCUS_ON_POINT') {
|
|
const point = event.data.point;
|
|
const radius = event.data.radius || null;
|
|
if (point && Array.isArray(point) && point.length === 3) {
|
|
console.log('[GeomPack VTK Viewer] Focusing on point:', point);
|
|
focusOnPoint(point, radius);
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log('[GeomPack VTK Viewer] Ready (unified)');
|
|
</script>
|
|
</body>
|
|
</html>
|