Add perspective toggles

This commit is contained in:
Nettika 2026-01-29 00:04:05 -08:00
parent 0c65c9848e
commit 128bfde8c0
No known key found for this signature in database
2 changed files with 73 additions and 19 deletions

View file

@ -46,7 +46,7 @@ A step-by-step checklist for porting MatterControl's design features to a Vue +
- [x] Add "fit to selection" camera function
- [x] Add "reset view" function (home position)
- [x] Add view presets (front, back, top, bottom, left, right)
- [ ] Add orthographic/perspective toggle
- [x] Add orthographic/perspective toggle
### Scene State Management
- [ ] Create `useScene` composable for scene state

View file

@ -17,7 +17,10 @@ const props = withDefaults(
const containerRef = ref<HTMLDivElement | null>(null)
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let perspectiveCamera: THREE.PerspectiveCamera
let orthographicCamera: THREE.OrthographicCamera
let activeCamera: THREE.PerspectiveCamera | THREE.OrthographicCamera
let isOrthographic = false
let renderer: THREE.WebGLRenderer
let animationFrameId: number
let resizeObserver: ResizeObserver
@ -47,11 +50,27 @@ function initScene() {
scene = new THREE.Scene()
scene.background = new THREE.Color(0x1a1a2e)
// Create camera
// Create cameras
const { clientWidth: width, clientHeight: height } = containerRef.value
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
camera.position.copy(HOME_POSITION)
camera.lookAt(HOME_TARGET)
const aspect = width / height
perspectiveCamera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000)
perspectiveCamera.position.copy(HOME_POSITION)
perspectiveCamera.lookAt(HOME_TARGET)
const frustumSize = 10
orthographicCamera = new THREE.OrthographicCamera(
(-frustumSize * aspect) / 2,
(frustumSize * aspect) / 2,
frustumSize / 2,
-frustumSize / 2,
0.1,
1000
)
orthographicCamera.position.copy(HOME_POSITION)
orthographicCamera.lookAt(HOME_TARGET)
activeCamera = perspectiveCamera
// Create renderer
renderer = new THREE.WebGLRenderer({ antialias: true })
@ -60,7 +79,7 @@ function initScene() {
containerRef.value.appendChild(renderer.domElement)
// Setup orbit controls
controls = new OrbitControls(camera, renderer.domElement)
controls = new OrbitControls(activeCamera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
@ -139,8 +158,18 @@ function setupResizeObserver() {
for (const entry of entries) {
const { width, height } = entry.contentRect
if (width > 0 && height > 0) {
camera.aspect = width / height
camera.updateProjectionMatrix()
const aspect = width / height
perspectiveCamera.aspect = aspect
perspectiveCamera.updateProjectionMatrix()
const frustumSize = 10
orthographicCamera.left = (-frustumSize * aspect) / 2
orthographicCamera.right = (frustumSize * aspect) / 2
orthographicCamera.top = frustumSize / 2
orthographicCamera.bottom = -frustumSize / 2
orthographicCamera.updateProjectionMatrix()
renderer.setSize(width, height)
}
}
@ -152,7 +181,7 @@ function setupResizeObserver() {
function animate() {
animationFrameId = requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
renderer.render(scene, activeCamera)
}
function fitToSelection(objects: THREE.Object3D[]) {
@ -166,31 +195,54 @@ function fitToSelection(objects: THREE.Object3D[]) {
const center = box.getCenter(new THREE.Vector3())
const size = box.getSize(new THREE.Vector3())
const maxDim = Math.max(size.x, size.y, size.z)
const fov = camera.fov * (Math.PI / 180)
const distance = maxDim / (2 * Math.tan(fov / 2)) * 1.5
const direction = camera.position.clone().sub(controls.target).normalize()
camera.position.copy(center).add(direction.multiplyScalar(distance))
const fov = perspectiveCamera.fov * (Math.PI / 180)
const distance = (maxDim / (2 * Math.tan(fov / 2))) * 1.5
const direction = activeCamera.position.clone().sub(controls.target).normalize()
activeCamera.position.copy(center).add(direction.multiplyScalar(distance))
controls.target.copy(center)
controls.update()
}
function resetView() {
camera.position.copy(HOME_POSITION)
activeCamera.position.copy(HOME_POSITION)
controls.target.copy(HOME_TARGET)
controls.update()
}
function setViewPreset(preset: ViewPreset) {
const position = VIEW_POSITIONS[preset]
camera.position.copy(position)
activeCamera.position.copy(position)
controls.target.set(0, 0, 0)
camera.up.set(0, 1, 0)
if (preset === 'top') camera.up.set(0, 0, -1)
if (preset === 'bottom') camera.up.set(0, 0, 1)
activeCamera.up.set(0, 1, 0)
if (preset === 'top') activeCamera.up.set(0, 0, -1)
if (preset === 'bottom') activeCamera.up.set(0, 0, 1)
controls.update()
}
function setOrthographic(ortho: boolean) {
if (isOrthographic === ortho) return
const newCamera = ortho ? orthographicCamera : perspectiveCamera
// Copy position and rotation from current camera
newCamera.position.copy(activeCamera.position)
newCamera.up.copy(activeCamera.up)
newCamera.lookAt(controls.target)
activeCamera = newCamera
isOrthographic = ortho
// Update controls to use new camera
controls.object = activeCamera
controls.update()
}
function toggleProjection() {
setOrthographic(!isOrthographic)
}
function cleanup() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
@ -222,6 +274,8 @@ defineExpose({
fitToSelection,
resetView,
setViewPreset,
setOrthographic,
toggleProjection,
})
</script>