The viewer JS/HTML/bundles are now committed in javascript/ (declared via pyproject [tool.comfy] web = "javascript") instead of being copied from the shared comfy-3d-viewers pip package at prestartup. Consequences: - requirements.txt is now just comfy-env (host env installs nothing else, per the never-touch-the-host-env principle) - prestartup_script.py no longer copies viewers or web_src; it keeps only setup_env() and the input-asset copies - WEB_DIRECTORY attribute removed; the pyproject declaration is the single source (served at /extensions/comfyui-geometrypack/, keyed by project name) - web_src/ deleted; its 6 widgets now live in javascript/js/ as the tracked source, edited directly - viewer_pbr.html hardcoded-fallback URL updated to the new mount name Iframe URLs auto-detect the mount from import.meta.url, so the folder rename and mount-name change are transparent at runtime.
272 lines
9.1 KiB
JavaScript
272 lines
9.1 KiB
JavaScript
/**
|
|
* CameraController - Unified camera management for VTK viewers
|
|
*
|
|
* Handles camera positioning, view presets, and projection modes.
|
|
* Configurable distance multiplier allows different viewers to use different defaults.
|
|
*/
|
|
|
|
import { getCenter, getMaxDimension } from '../utils/BoundsUtils.js';
|
|
|
|
/**
|
|
* View direction configurations
|
|
* Maps direction strings to camera position offsets and viewUp vectors
|
|
*/
|
|
const VIEW_CONFIGS = {
|
|
'+X': { offset: [1, 0, 0], viewUp: [0, 0, 1] },
|
|
'-X': { offset: [-1, 0, 0], viewUp: [0, 0, 1] },
|
|
'+Y': { offset: [0, 1, 0], viewUp: [0, 0, 1] },
|
|
'-Y': { offset: [0, -1, 0], viewUp: [0, 0, 1] },
|
|
'+Z': { offset: [0, 0, 1], viewUp: [0, 1, 0] },
|
|
'-Z': { offset: [0, 0, -1], viewUp: [0, 1, 0] }
|
|
};
|
|
|
|
/**
|
|
* Default camera configuration
|
|
*/
|
|
const DEFAULT_CONFIG = {
|
|
distanceMultiplier: 2.5, // Standard viewers use 2.5, textured uses 2.0
|
|
initialViewOffset: [1, 1, 1], // Diagonal view for initial positioning
|
|
initialViewUp: [0, 0, 1]
|
|
};
|
|
|
|
export class CameraController {
|
|
/**
|
|
* Create a CameraController
|
|
* @param {Object} renderer - VTK renderer instance
|
|
* @param {Object} renderWindow - VTK render window instance
|
|
* @param {Object} config - Configuration options
|
|
* @param {number} config.distanceMultiplier - Camera distance = maxDim * multiplier (default: 2.5)
|
|
*/
|
|
constructor(renderer, renderWindow, config = {}) {
|
|
this.renderer = renderer;
|
|
this.renderWindow = renderWindow;
|
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
this.currentBounds = null;
|
|
}
|
|
|
|
/**
|
|
* Get the active camera from the renderer
|
|
* @returns {Object} VTK camera instance
|
|
*/
|
|
getCamera() {
|
|
return this.renderer.getActiveCamera();
|
|
}
|
|
|
|
/**
|
|
* Store bounds for later use (e.g., view presets)
|
|
* @param {number[]} bounds - VTK bounds [xmin, xmax, ymin, ymax, zmin, zmax]
|
|
*/
|
|
setBounds(bounds) {
|
|
this.currentBounds = bounds;
|
|
}
|
|
|
|
/**
|
|
* Position camera for initial viewing of mesh
|
|
* @param {number[]} bounds - VTK bounds [xmin, xmax, ymin, ymax, zmin, zmax]
|
|
*/
|
|
positionInitialCamera(bounds) {
|
|
this.currentBounds = bounds;
|
|
|
|
const camera = this.getCamera();
|
|
const center = getCenter(bounds);
|
|
const maxDim = getMaxDimension(bounds);
|
|
const distance = maxDim * this.config.distanceMultiplier;
|
|
|
|
const offset = this.config.initialViewOffset;
|
|
const position = [
|
|
center[0] + offset[0] * distance,
|
|
center[1] + offset[1] * distance,
|
|
center[2] + offset[2] * distance
|
|
];
|
|
|
|
camera.setPosition(...position);
|
|
camera.setFocalPoint(...center);
|
|
camera.setViewUp(...this.config.initialViewUp);
|
|
|
|
this.renderer.resetCamera();
|
|
this.renderWindow.render();
|
|
}
|
|
|
|
/**
|
|
* Set camera to a specific axis-aligned view
|
|
* @param {string} direction - One of '+X', '-X', '+Y', '-Y', '+Z', '-Z'
|
|
* @param {number[]} bounds - Optional bounds, uses stored bounds if not provided
|
|
*/
|
|
setCameraView(direction, bounds = null) {
|
|
const activeBounds = bounds || this.currentBounds;
|
|
if (!activeBounds) {
|
|
console.warn('[CameraController] No bounds available for setCameraView');
|
|
return;
|
|
}
|
|
|
|
const viewConfig = VIEW_CONFIGS[direction];
|
|
if (!viewConfig) {
|
|
console.warn(`[CameraController] Unknown view direction: ${direction}`);
|
|
return;
|
|
}
|
|
|
|
const camera = this.getCamera();
|
|
const center = getCenter(activeBounds);
|
|
const maxDim = getMaxDimension(activeBounds);
|
|
const distance = maxDim * this.config.distanceMultiplier;
|
|
|
|
const position = [
|
|
center[0] + viewConfig.offset[0] * distance,
|
|
center[1] + viewConfig.offset[1] * distance,
|
|
center[2] + viewConfig.offset[2] * distance
|
|
];
|
|
|
|
camera.setPosition(...position);
|
|
camera.setFocalPoint(...center);
|
|
camera.setViewUp(...viewConfig.viewUp);
|
|
|
|
this.renderer.resetCamera();
|
|
this.renderWindow.render();
|
|
}
|
|
|
|
/**
|
|
* Reset camera to fit the current bounds
|
|
*/
|
|
resetCamera() {
|
|
if (this.currentBounds) {
|
|
this.renderer.resetCamera(this.currentBounds);
|
|
} else {
|
|
this.renderer.resetCamera();
|
|
}
|
|
this.renderWindow.render();
|
|
}
|
|
|
|
/**
|
|
* Reset camera clipping range (useful after mesh updates)
|
|
*/
|
|
resetClippingRange() {
|
|
this.renderer.resetCameraClippingRange();
|
|
this.renderWindow.render();
|
|
}
|
|
|
|
/**
|
|
* Set parallel (orthographic) vs perspective projection
|
|
* @param {boolean} enabled - True for parallel, false for perspective
|
|
*/
|
|
setParallelProjection(enabled) {
|
|
const camera = this.getCamera();
|
|
camera.setParallelProjection(enabled);
|
|
this.renderWindow.render();
|
|
}
|
|
|
|
/**
|
|
* Get current parallel projection state
|
|
* @returns {boolean} True if parallel projection is enabled
|
|
*/
|
|
getParallelProjection() {
|
|
return this.getCamera().getParallelProjection();
|
|
}
|
|
|
|
/**
|
|
* Synchronize this camera with another camera
|
|
* Useful for side-by-side viewers
|
|
* @param {Object} sourceCamera - Camera to copy state from
|
|
*/
|
|
synchronizeFrom(sourceCamera) {
|
|
const camera = this.getCamera();
|
|
camera.setPosition(...sourceCamera.getPosition());
|
|
camera.setFocalPoint(...sourceCamera.getFocalPoint());
|
|
camera.setViewUp(...sourceCamera.getViewUp());
|
|
camera.setParallelProjection(sourceCamera.getParallelProjection());
|
|
camera.setClippingRange(...sourceCamera.getClippingRange());
|
|
camera.setParallelScale(sourceCamera.getParallelScale());
|
|
camera.setViewAngle(sourceCamera.getViewAngle());
|
|
}
|
|
|
|
/**
|
|
* Get camera state for serialization
|
|
* @returns {Object} Camera state object
|
|
*/
|
|
getState() {
|
|
const camera = this.getCamera();
|
|
return {
|
|
position: camera.getPosition(),
|
|
focalPoint: camera.getFocalPoint(),
|
|
viewUp: camera.getViewUp(),
|
|
parallelProjection: camera.getParallelProjection(),
|
|
clippingRange: camera.getClippingRange(),
|
|
parallelScale: camera.getParallelScale(),
|
|
viewAngle: camera.getViewAngle()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Restore camera state from serialized object
|
|
* @param {Object} state - Camera state object from getState()
|
|
*/
|
|
setState(state) {
|
|
const camera = this.getCamera();
|
|
if (state.position) camera.setPosition(...state.position);
|
|
if (state.focalPoint) camera.setFocalPoint(...state.focalPoint);
|
|
if (state.viewUp) camera.setViewUp(...state.viewUp);
|
|
if (state.parallelProjection !== undefined) camera.setParallelProjection(state.parallelProjection);
|
|
if (state.clippingRange) camera.setClippingRange(...state.clippingRange);
|
|
if (state.parallelScale !== undefined) camera.setParallelScale(state.parallelScale);
|
|
if (state.viewAngle !== undefined) camera.setViewAngle(state.viewAngle);
|
|
this.renderWindow.render();
|
|
}
|
|
|
|
/**
|
|
* Focus camera on a specific 3D point
|
|
* Moves the camera to look at the point while maintaining relative view direction
|
|
* @param {number[]} point - [x, y, z] coordinates to focus on
|
|
* @param {number} viewRadius - Optional radius around point to view (defaults to 1/10 of mesh size)
|
|
*/
|
|
focusOnPoint(point, viewRadius = null) {
|
|
const camera = this.getCamera();
|
|
|
|
// 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
|
|
let distance;
|
|
if (viewRadius !== null) {
|
|
distance = viewRadius * this.config.distanceMultiplier;
|
|
} else if (this.currentBounds) {
|
|
const maxDim = getMaxDimension(this.currentBounds);
|
|
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);
|
|
|
|
this.renderer.resetCameraClippingRange();
|
|
this.renderWindow.render();
|
|
}
|
|
}
|
|
|
|
export default CameraController;
|