|
|
@ -2,333 +2,48 @@ |
|
|
<div class="canvas-container" ref="canvasContainer" tabindex="1" /> |
|
|
<div class="canvas-container" ref="canvasContainer" tabindex="1" /> |
|
|
</template> |
|
|
</template> |
|
|
<script setup lang="ts"> |
|
|
<script setup lang="ts"> |
|
|
import * as THREE from 'three' |
|
|
import { getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref } from 'vue' |
|
|
import $ from 'jquery' |
|
|
import Viewport from '@/designer/Viewport.ts' |
|
|
import Stats from 'three/examples/jsm/libs/stats.module' |
|
|
|
|
|
import { |
|
|
|
|
|
ref, |
|
|
|
|
|
onMounted, |
|
|
|
|
|
nextTick, |
|
|
|
|
|
reactive, |
|
|
|
|
|
watch, |
|
|
|
|
|
getCurrentInstance, |
|
|
|
|
|
onUnmounted, |
|
|
|
|
|
onBeforeUnmount, |
|
|
|
|
|
defineExpose |
|
|
|
|
|
} from 'vue' |
|
|
|
|
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' |
|
|
|
|
|
|
|
|
|
|
|
// DOM refs |
|
|
// DOM refs |
|
|
const canvasContainer = ref(null) |
|
|
const canvasContainer = ref<HTMLElement>(null) |
|
|
|
|
|
|
|
|
// 窗口大小关联 |
|
|
|
|
|
let resizeObserver |
|
|
|
|
|
|
|
|
|
|
|
// Three.js 场景相关 |
|
|
// Three.js 场景相关 |
|
|
let scene, camera, renderer, controls |
|
|
let viewport: Viewport |
|
|
let statsControls, axesHelper, gridHelper |
|
|
|
|
|
|
|
|
|
|
|
const state = designer.editorState |
|
|
|
|
|
|
|
|
|
|
|
function initMode2DCamera() { |
|
|
|
|
|
if (camera) { |
|
|
|
|
|
scene.remove(camera) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ============================ 创建正交相机 |
|
|
|
|
|
const viewerDom = canvasContainer.value |
|
|
|
|
|
const cameraNew = new THREE.OrthographicCamera( |
|
|
|
|
|
viewerDom.clientWidth / -2, |
|
|
|
|
|
viewerDom.clientWidth / 2, |
|
|
|
|
|
viewerDom.clientHeight / 2, |
|
|
|
|
|
viewerDom.clientHeight / -2, |
|
|
|
|
|
1, |
|
|
|
|
|
500 |
|
|
|
|
|
) |
|
|
|
|
|
cameraNew.position.set(0, 100, 0) |
|
|
|
|
|
cameraNew.lookAt(0, 0, 0) |
|
|
|
|
|
cameraNew.zoom = 30 |
|
|
|
|
|
camera = cameraNew |
|
|
|
|
|
scene.add(camera) |
|
|
|
|
|
|
|
|
|
|
|
// ============================ 创建控制器 |
|
|
|
|
|
const controlsNew = new OrbitControls( |
|
|
|
|
|
camera, |
|
|
|
|
|
renderer.domElement |
|
|
|
|
|
) |
|
|
|
|
|
controlsNew.enableDamping = false |
|
|
|
|
|
controlsNew.enableZoom = true |
|
|
|
|
|
controlsNew.enableRotate = false |
|
|
|
|
|
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN } // 鼠标中键平移 |
|
|
|
|
|
controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动 |
|
|
|
|
|
controlsNew.listenToKeyEvents(viewerDom) // 监听键盘事件 |
|
|
|
|
|
controlsNew.keys = { LEFT: 'KeyA', UP: 'KeyW', RIGHT: 'KeyD', BOTTOM: 'KeyS' } |
|
|
|
|
|
controlsNew.panSpeed = 1 |
|
|
|
|
|
controlsNew.keyPanSpeed = 20 // normal 7 |
|
|
|
|
|
controlsNew.minDistance = 0.1 |
|
|
|
|
|
controlsNew.maxDistance = 1000 |
|
|
|
|
|
controls = controlsNew |
|
|
|
|
|
controlsNew.addEventListener('change', syncCameraState) |
|
|
|
|
|
|
|
|
|
|
|
camera.updateProjectionMatrix() |
|
|
|
|
|
|
|
|
|
|
|
syncCameraState() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function initThree() { |
|
|
|
|
|
const viewerDom = canvasContainer.value |
|
|
|
|
|
|
|
|
|
|
|
// 场景 |
|
|
|
|
|
scene = new THREE.Scene() |
|
|
|
|
|
scene.background = new THREE.Color(0xeeeeee) |
|
|
|
|
|
|
|
|
|
|
|
// 渲染器 |
|
|
|
|
|
renderer = new THREE.WebGLRenderer({ |
|
|
|
|
|
logarithmicDepthBuffer: true, |
|
|
|
|
|
antialias: true, |
|
|
|
|
|
alpha: true, |
|
|
|
|
|
precision: 'mediump', |
|
|
|
|
|
premultipliedAlpha: true, |
|
|
|
|
|
preserveDrawingBuffer: false, |
|
|
|
|
|
powerPreference: 'high-performance' |
|
|
|
|
|
}) |
|
|
|
|
|
renderer.outputEncoding = THREE.SRGBColorSpace |
|
|
|
|
|
renderer.clearDepth() |
|
|
|
|
|
renderer.shadowMap.enabled = true |
|
|
|
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping |
|
|
|
|
|
renderer.setPixelRatio(window.devicePixelRatio) |
|
|
|
|
|
renderer.setSize(viewerDom.getBoundingClientRect().width, viewerDom.getBoundingClientRect().height) |
|
|
|
|
|
viewerDom.appendChild(renderer.domElement) |
|
|
|
|
|
|
|
|
|
|
|
// 创建正交摄像机 |
|
|
|
|
|
initMode2DCamera() |
|
|
|
|
|
|
|
|
|
|
|
// 辅助线 |
|
|
|
|
|
axesHelper = new THREE.AxesHelper(3) |
|
|
|
|
|
scene.add(axesHelper) |
|
|
|
|
|
|
|
|
|
|
|
gridHelper = new THREE.GridHelper(500, 500) |
|
|
|
|
|
gridHelper.material = new THREE.LineBasicMaterial({ |
|
|
|
|
|
color: 0x888888, |
|
|
|
|
|
opacity: 0.8, |
|
|
|
|
|
transparent: true |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
scene.add(gridHelper) |
|
|
|
|
|
|
|
|
|
|
|
// 光照 |
|
|
|
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5) |
|
|
|
|
|
scene.add(ambientLight) |
|
|
|
|
|
|
|
|
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5) |
|
|
|
|
|
directionalLight.position.set(5, 5, 5).multiplyScalar(3) |
|
|
|
|
|
directionalLight.castShadow = true |
|
|
|
|
|
scene.add(directionalLight) |
|
|
|
|
|
|
|
|
|
|
|
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1) |
|
|
|
|
|
scene.add(hemisphereLight) |
|
|
|
|
|
|
|
|
|
|
|
// 性能监控 |
|
|
|
|
|
statsControls = new Stats() |
|
|
|
|
|
statsControls.showPanel(0) |
|
|
|
|
|
statsControls.dom.style.position = 'absolute' |
|
|
|
|
|
statsControls.dom.style.top = '2px' |
|
|
|
|
|
statsControls.dom.style.left = '0' |
|
|
|
|
|
viewerDom.appendChild(statsControls.dom) |
|
|
|
|
|
$(statsControls.dom).children().css('height', '28px') |
|
|
|
|
|
|
|
|
|
|
|
// 创建几何体和材质 |
|
|
|
|
|
const geometry = new THREE.BoxGeometry(1, 1, 1) |
|
|
|
|
|
const material = new THREE.MeshStandardMaterial({ |
|
|
|
|
|
color: 0xcccccc, |
|
|
|
|
|
metalness: 0.9, |
|
|
|
|
|
roughness: 0.1 |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
const cube = new THREE.Mesh(geometry, material) |
|
|
|
|
|
scene.add(cube) |
|
|
|
|
|
camera.position.z = 5 |
|
|
|
|
|
|
|
|
|
|
|
animate() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 动画循环 |
|
|
|
|
|
function animate() { |
|
|
|
|
|
requestAnimationFrame(animate) |
|
|
|
|
|
renderView() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function renderView() { |
|
|
|
|
|
statsControls?.update() |
|
|
|
|
|
renderer?.render(scene, camera) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => { |
|
|
onMounted(() => { |
|
|
const editor = getCurrentInstance() |
|
|
const instance = getCurrentInstance() |
|
|
nextTick(() => { |
|
|
nextTick(() => { |
|
|
initThree() |
|
|
|
|
|
|
|
|
|
|
|
const viewerDom = canvasContainer.value |
|
|
const viewerDom = canvasContainer.value |
|
|
if (resizeObserver) { |
|
|
if (!viewerDom) { |
|
|
resizeObserver.unobserve(viewerDom) |
|
|
system.showErrorDialog('viewerDom is null') |
|
|
|
|
|
return |
|
|
} |
|
|
} |
|
|
resizeObserver = new ResizeObserver(handleResize) |
|
|
|
|
|
resizeObserver.observe(viewerDom) |
|
|
|
|
|
|
|
|
|
|
|
state.ready = true |
|
|
viewport = new Viewport(worldModel) |
|
|
|
|
|
viewport.initThree(viewerDom) |
|
|
|
|
|
|
|
|
window['editor'] = editor |
|
|
window['editor'] = instance |
|
|
window['renderer'] = renderer |
|
|
window['viewport'] = viewport |
|
|
window['scene'] = scene |
|
|
window['scene'] = viewport.scene |
|
|
window['camera'] = camera |
|
|
window['renderer'] = viewport.renderer |
|
|
window['renderer'] = renderer |
|
|
window['camera'] = viewport.camera |
|
|
window['controls'] = controls |
|
|
window['renderer'] = viewport.renderer |
|
|
|
|
|
window['controls'] = viewport.controls |
|
|
|
|
|
|
|
|
viewerDom?.focus() |
|
|
viewerDom?.focus() |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
onBeforeUnmount(() => { |
|
|
onBeforeUnmount(() => { |
|
|
state.ready = false |
|
|
viewport.destroy() |
|
|
|
|
|
|
|
|
cleanupThree() |
|
|
|
|
|
|
|
|
|
|
|
const viewerDom = canvasContainer.value |
|
|
|
|
|
if (resizeObserver) { |
|
|
|
|
|
resizeObserver.unobserve(viewerDom) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
delete window['editor'] |
|
|
delete window['editor'] |
|
|
delete window['renderer'] |
|
|
delete window['viewport'] |
|
|
delete window['scene'] |
|
|
delete window['scene'] |
|
|
|
|
|
delete window['renderer'] |
|
|
delete window['camera'] |
|
|
delete window['camera'] |
|
|
delete window['renderer'] |
|
|
delete window['renderer'] |
|
|
delete window['controls'] |
|
|
delete window['controls'] |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleResize(entries) { |
|
|
|
|
|
for (let entry of entries) { |
|
|
|
|
|
// entry.contentRect包含了元素的尺寸信息 |
|
|
|
|
|
console.log('Element size changed:', entry.contentRect) |
|
|
|
|
|
|
|
|
|
|
|
const width = entry.contentRect.width |
|
|
|
|
|
const height = entry.contentRect.height |
|
|
|
|
|
|
|
|
|
|
|
if (camera instanceof THREE.PerspectiveCamera) { |
|
|
|
|
|
camera.aspect = width / height |
|
|
|
|
|
camera.updateProjectionMatrix() |
|
|
|
|
|
|
|
|
|
|
|
} else if (camera instanceof THREE.OrthographicCamera) { |
|
|
|
|
|
camera.left = width / -2 |
|
|
|
|
|
camera.right = width / 2 |
|
|
|
|
|
camera.top = height / 2 |
|
|
|
|
|
camera.bottom = height / -2 |
|
|
|
|
|
camera.updateProjectionMatrix() |
|
|
|
|
|
} |
|
|
|
|
|
renderer.setSize(width, height) |
|
|
|
|
|
break |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
watch(() => state.camera.position.y, (newVal) => { |
|
|
|
|
|
if (state.ready) { |
|
|
|
|
|
updateGridVisibility() |
|
|
|
|
|
} |
|
|
|
|
|
}, { deep: true }) |
|
|
|
|
|
|
|
|
|
|
|
function cleanupThree() { |
|
|
|
|
|
// 移除旧模型 |
|
|
|
|
|
if (scene) { |
|
|
|
|
|
scene.traverse((obj) => { |
|
|
|
|
|
// 释放几何体 |
|
|
|
|
|
if (obj.geometry) { |
|
|
|
|
|
obj.geometry.dispose() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 释放材质 |
|
|
|
|
|
if (obj.material) { |
|
|
|
|
|
if (Array.isArray(obj.material)) { |
|
|
|
|
|
obj.material.forEach(m => m.dispose()) |
|
|
|
|
|
} else { |
|
|
|
|
|
obj.material.dispose() |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 释放纹理 |
|
|
|
|
|
if (obj.texture) { |
|
|
|
|
|
obj.texture.dispose() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 释放渲染目标 |
|
|
|
|
|
if (obj.renderTarget) { |
|
|
|
|
|
obj.renderTarget.dispose() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 移除事件监听(如 OrbitControls) |
|
|
|
|
|
if (obj.dispose) { |
|
|
|
|
|
obj.dispose() |
|
|
|
|
|
} |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
// 清空场景 |
|
|
|
|
|
scene.children = [] |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (statsControls) { |
|
|
|
|
|
statsControls.dom.remove() |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (renderer) { |
|
|
|
|
|
renderer.dispose() |
|
|
|
|
|
renderer.forceContextLoss() |
|
|
|
|
|
console.log('WebGL disposed, memory:', renderer.info.memory) |
|
|
|
|
|
renderer.domElement = null |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 根据可视化范围更新网格的透明度 |
|
|
|
|
|
*/ |
|
|
|
|
|
function updateGridVisibility() { |
|
|
|
|
|
const cameraDistance = state.camera.position.y |
|
|
|
|
|
const maxVisibleDistance = 60 // 网格完全可见的最大距离 |
|
|
|
|
|
const fadeStartDistance = 15 // 开始淡出的距离 |
|
|
|
|
|
|
|
|
|
|
|
// 计算透明度(0~1) |
|
|
|
|
|
let opacity = 1 |
|
|
|
|
|
if (cameraDistance > fadeStartDistance) { |
|
|
|
|
|
opacity = 1 - Math.min((cameraDistance - fadeStartDistance) / (maxVisibleDistance - fadeStartDistance), 1) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 修改网格材质透明度 |
|
|
|
|
|
gridHelper.material.opacity = opacity |
|
|
|
|
|
gridHelper.visible = opacity > 0 |
|
|
|
|
|
console.log('opacity', opacity) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 计算相机到目标的有效视距 |
|
|
|
|
|
*/ |
|
|
|
|
|
function getEffectiveViewDistance() { |
|
|
|
|
|
if (!camera) { |
|
|
|
|
|
return 10 |
|
|
|
|
|
} |
|
|
|
|
|
const viewHeight = (camera.top - camera.bottom) / camera.zoom |
|
|
|
|
|
// 假设我们希望匹配一个虚拟的透视相机(通常使用45度fov作为参考) |
|
|
|
|
|
const referenceFOV = 45 // 参考视场角 |
|
|
|
|
|
return viewHeight / (2 * Math.tan(THREE.MathUtils.degToRad(referenceFOV) / 2)) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 同步相机状态到全局状态 |
|
|
|
|
|
*/ |
|
|
|
|
|
function syncCameraState() { |
|
|
|
|
|
if (camera) { |
|
|
|
|
|
state.camera.position.x = camera.position.x |
|
|
|
|
|
state.camera.position.y = getEffectiveViewDistance() |
|
|
|
|
|
state.camera.position.z = camera.position.z |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
</script> |
|
|
</script> |