205 lines
4.6 KiB
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>
|