mattercontrol/src/components/Viewport.vue

205 lines
4.6 KiB
Vue

<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
const props = withDefaults(
defineProps<{
showGrid?: boolean
showAxis?: boolean
}>(),
{
showGrid: true,
showAxis: true,
}
)
const containerRef = ref<HTMLDivElement | null>(null)
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let animationFrameId: number
let resizeObserver: ResizeObserver
let gridHelper: THREE.GridHelper
let axisHelper: THREE.AxesHelper
let controls: OrbitControls
function initScene() {
if (!containerRef.value) return
// Create scene
scene = new THREE.Scene()
scene.background = new THREE.Color(0x1a1a2e)
// Create camera
const { clientWidth: width, clientHeight: height } = containerRef.value
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
camera.position.set(5, 5, 5)
camera.lookAt(0, 0, 0)
// Create renderer
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.setPixelRatio(window.devicePixelRatio)
containerRef.value.appendChild(renderer.domElement)
// Setup orbit controls
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
// Add lighting
addLighting()
// Add grid helper
addGridHelper()
// Add axis helper
addAxisHelper()
// Add a test cube to verify rendering
addTestCube()
// Setup resize observer
setupResizeObserver()
// Start render loop
animate()
}
function addGridHelper() {
gridHelper = new THREE.GridHelper(10, 10, 0x444444, 0x333333)
gridHelper.visible = props.showGrid
scene.add(gridHelper)
}
watch(
() => props.showGrid,
(visible) => {
if (gridHelper) gridHelper.visible = visible
}
)
function addAxisHelper() {
axisHelper = new THREE.AxesHelper(5)
axisHelper.visible = props.showAxis
scene.add(axisHelper)
}
watch(
() => props.showAxis,
(visible) => {
if (axisHelper) axisHelper.visible = visible
}
)
function addLighting() {
// Ambient light for base illumination
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4)
scene.add(ambientLight)
// Primary directional light (top-front-right)
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight1.position.set(5, 10, 7)
scene.add(directionalLight1)
// Secondary directional light (back-left, softer)
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.3)
directionalLight2.position.set(-5, 5, -5)
scene.add(directionalLight2)
}
function addTestCube() {
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshStandardMaterial({ color: 0x4a90d9 })
const cube = new THREE.Mesh(geometry, material)
scene.add(cube)
}
function setupResizeObserver() {
if (!containerRef.value) return
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect
if (width > 0 && height > 0) {
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height)
}
}
})
resizeObserver.observe(containerRef.value)
}
function animate() {
animationFrameId = requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
}
function fitToSelection(objects: THREE.Object3D[]) {
if (objects.length === 0) return
const box = new THREE.Box3()
for (const obj of objects) {
box.expandByObject(obj)
}
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))
controls.target.copy(center)
controls.update()
}
function cleanup() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
if (resizeObserver) {
resizeObserver.disconnect()
}
if (controls) {
controls.dispose()
}
if (renderer) {
renderer.dispose()
renderer.domElement.remove()
}
}
onMounted(() => {
initScene()
})
onBeforeUnmount(() => {
cleanup()
})
defineExpose({
fitToSelection,
})
</script>
<template>
<div ref="containerRef" class="viewport"></div>
</template>
<style scoped>
.viewport {
width: 100%;
height: 100%;
overflow: hidden;
}
</style>