Files
PozzettiAndrea-ComfyUI-Geom…/javascript/js/viewer/core/CameraController.js
T
PozzettiAndrea 414f759791 Vendor frontend JS into javascript/; drop the prestartup copy dance
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.
2026-08-12 17:37:09 +02:00

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;