/**
* BaseViewer - Foundation class for VTK viewers
*
* Provides core lifecycle management, VTK setup, and feature composition.
* Subclasses configure features and behavior through options.
*/
import { CameraController } from './CameraController.js';
import { ActorManager, Representation } from './ActorManager.js';
import { SettingsPanel } from '../ui/SettingsPanel.js';
import { ControlsBar, StandardControls } from '../ui/ControlsBar.js';
import { loadMesh } from '../loaders/LoaderFactory.js';
import { ScreenshotCapture } from '../features/ScreenshotCapture.js';
import { AxisIndicator } from '../features/AxisIndicator.js';
import { FieldVisualization } from '../features/FieldVisualization.js';
import { TextureManager } from '../features/TextureManager.js';
import { sendMeshLoaded, sendError, createMessageListener, extractFilename } from '../utils/MessageHandler.js';
import { getCenter, getMaxDimension, isValidBounds } from '../utils/BoundsUtils.js';
/**
* Default viewer configuration
*/
const DEFAULT_CONFIG = {
// Appearance
backgroundColor: [0.15, 0.15, 0.15],
defaultMeshColor: [0.6, 0.8, 1.0],
edgeColor: [0.2, 0.2, 0.2],
edgeWidth: 1.0,
showEdges: false,
// Camera
cameraDistanceMultiplier: 2.5,
parallelProjection: false,
// Lighting
twoSidedLighting: false,
// Features (enabled/disabled)
enableFields: true,
enableTextures: false,
enableScreenshot: true,
enableAxisIndicator: false,
enableSettings: true,
enableControls: true,
// UI
showLoadingOverlay: true,
showInfoOverlay: true
};
export class BaseViewer {
/**
* Create a BaseViewer
* @param {HTMLElement} container - Container element for the viewer
* @param {Object} config - Configuration options
*/
constructor(container, config = {}) {
this.container = container;
this.config = { ...DEFAULT_CONFIG, ...config };
// VTK objects (initialized in initialize())
this.vtk = null;
this.renderWindow = null;
this.renderer = null;
this.interactor = null;
this.openGLRenderWindow = null;
// Feature modules
this.cameraController = null;
this.actorManager = null;
this.settingsPanel = null;
this.controlsBar = null;
this.screenshotCapture = null;
this.axisIndicator = null;
this.fieldVisualization = null;
this.textureManager = null;
// State
this.actors = [];
this.polyDatas = [];
this.currentFilename = null;
this.isInitialized = false;
// UI elements
this.loadingOverlay = null;
this.infoOverlay = null;
// Message listener cleanup
this._messageCleanup = null;
}
/**
* Initialize the viewer
* Must be called before loading meshes
*/
async initialize() {
if (this.isInitialized) {
console.warn('[BaseViewer] Already initialized');
return;
}
// Get VTK from global
this.vtk = window.vtk;
if (!this.vtk) {
throw new Error('VTK.js not loaded. Include vtk-gltf.js before initializing viewer.');
}
// Create VTK rendering pipeline
this._createRenderingPipeline();
// Initialize feature modules
this._initializeFeatures();
// Create UI elements
this._createUI();
// Setup resize handling
this._setupResizeHandler();
// Setup message listener for loading meshes
this._setupMessageListener();
this.isInitialized = true;
console.log('[BaseViewer] Initialized');
}
/**
* Create VTK rendering pipeline
* @private
*/
_createRenderingPipeline() {
// Create render window
this.renderWindow = this.vtk.Rendering.Core.vtkRenderWindow.newInstance();
// Create renderer
this.renderer = this.vtk.Rendering.Core.vtkRenderer.newInstance();
this.renderer.setBackground(...this.config.backgroundColor);
this.renderWindow.addRenderer(this.renderer);
// Two-sided lighting
if (this.config.twoSidedLighting) {
this.renderer.setTwoSidedLighting(true);
}
// Create OpenGL render window
this.openGLRenderWindow = this.vtk.Rendering.OpenGL.vtkRenderWindow.newInstance();
this.openGLRenderWindow.setContainer(this.container);
this.renderWindow.addView(this.openGLRenderWindow);
// Create interactor
this.interactor = this.vtk.Rendering.Core.vtkRenderWindowInteractor.newInstance();
this.interactor.setView(this.openGLRenderWindow);
this.interactor.initialize();
this.interactor.bindEvents(this.container);
// Set interactor style
const interactorStyle = this.vtk.Interaction.Style.vtkInteractorStyleTrackballCamera.newInstance();
this.interactor.setInteractorStyle(interactorStyle);
// Size to container
const { width, height } = this.container.getBoundingClientRect();
this.openGLRenderWindow.setSize(width, height);
}
/**
* Initialize feature modules
* @private
*/
_initializeFeatures() {
// Camera controller
this.cameraController = new CameraController(this.renderer, {
distanceMultiplier: this.config.cameraDistanceMultiplier,
parallelProjection: this.config.parallelProjection
});
// Actor manager
this.actorManager = new ActorManager(this.vtk, {
defaultColor: this.config.defaultMeshColor,
edgeColor: this.config.edgeColor,
edgeWidth: this.config.edgeWidth,
showEdges: this.config.showEdges
});
// Screenshot capture
if (this.config.enableScreenshot) {
this.screenshotCapture = new ScreenshotCapture(this.renderWindow);
}
// Axis indicator
if (this.config.enableAxisIndicator) {
this.axisIndicator = new AxisIndicator(this.vtk, this.interactor, {
enabled: true
});
}
// Field visualization
if (this.config.enableFields) {
this.fieldVisualization = new FieldVisualization(this.vtk, this.renderWindow);
}
// Texture manager
if (this.config.enableTextures) {
this.textureManager = new TextureManager(this.vtk, this.renderWindow);
}
}
/**
* Create UI elements
* @private
*/
_createUI() {
// Loading overlay
if (this.config.showLoadingOverlay) {
this.loadingOverlay = this._createLoadingOverlay();
}
// Info overlay
if (this.config.showInfoOverlay) {
this.infoOverlay = this._createInfoOverlay();
}
// Settings panel
if (this.config.enableSettings && this.config.settingsConfig) {
this.settingsPanel = new SettingsPanel(this.config.settingsConfig);
this.settingsPanel.onApply((settings) => this._onSettingsApply(settings));
}
// Controls bar
if (this.config.enableControls && this.config.controlsConfig) {
this.controlsBar = new ControlsBar(this.config.controlsConfig);
this._setupControlsHandlers();
}
}
/**
* Create loading overlay element
* @private
* @returns {HTMLElement}
*/
_createLoadingOverlay() {
const overlay = document.createElement('div');
overlay.id = 'loading';
overlay.className = 'loading';
overlay.innerHTML = `
Loading mesh...
`;
this.container.parentElement.appendChild(overlay);
return overlay;
}
/**
* Update the download progress bar.
* @param {number|null} fraction - 0..1, or null for indeterminate (hides the bar)
*/
setProgress(fraction) {
if (!this.loadingOverlay) return;
const wrap = this.loadingOverlay.querySelector('.loading-progress');
const bar = this.loadingOverlay.querySelector('.loading-progress-bar');
const textEl = this.loadingOverlay.querySelector('.loading-text');
if (!wrap || !bar) return;
if (fraction == null || isNaN(fraction)) {
wrap.style.display = 'none'; // indeterminate -> spinner only
return;
}
const pct = Math.max(0, Math.min(100, Math.round(fraction * 100)));
wrap.style.display = 'block';
bar.style.width = pct + '%';
if (textEl) textEl.textContent = `Downloading mesh… ${pct}%`;
}
/**
* Create info overlay element
* @private
* @returns {HTMLElement}
*/
_createInfoOverlay() {
const overlay = document.createElement('div');
overlay.id = 'meshInfo';
overlay.className = 'mesh-info';
this.container.parentElement.appendChild(overlay);
return overlay;
}
/**
* Setup resize handler
* @private
*/
_setupResizeHandler() {
const resizeObserver = new ResizeObserver(() => {
const { width, height } = this.container.getBoundingClientRect();
if (width > 0 && height > 0) {
this.openGLRenderWindow.setSize(width, height);
this.renderWindow.render();
}
});
resizeObserver.observe(this.container);
this._resizeObserver = resizeObserver;
}
/**
* Setup message listener for postMessage API
* @private
*/
_setupMessageListener() {
this._messageCleanup = createMessageListener((data) => {
if (data.type === 'loadMesh' && data.url) {
this.loadMesh(data.url);
} else if (data.type === 'screenshot') {
this.captureScreenshot();
}
});
}
/**
* Setup controls bar handlers
* @private
*/
_setupControlsHandlers() {
if (!this.controlsBar) return;
// Camera view buttons
this.controlsBar.onAction('camera-view', (direction) => {
this.setCameraView(direction);
});
// Screenshot button
this.controlsBar.onAction('screenshot', () => {
this.captureScreenshot();
});
// Settings button
this.controlsBar.onAction('settings', () => {
if (this.settingsPanel) {
this.settingsPanel.open();
}
});
// Edges toggle
this.controlsBar.onAction('edges-toggle', (show) => {
this.setEdgeVisibility(show);
});
}
/**
* Handle settings panel apply
* @private
* @param {Object} settings - Applied settings
*/
_onSettingsApply(settings) {
// Apply to actor manager
if (this.actorManager) {
this.actorManager.applySettings(this.actors, settings);
}
// Apply background color
if (settings.backgroundColor) {
this.renderer.setBackground(...settings.backgroundColor);
}
this.render();
}
/**
* Load a mesh from URL
* @param {string} url - Mesh file URL
* @returns {Promise