You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
476 lines
12 KiB
476 lines
12 KiB
<template>
|
|
<div class="model3d-view">
|
|
<el-space :gutter="10" class="toolbar">
|
|
<el-button type="primary" @click="test1">测试1</el-button>
|
|
<div class="demo-color-block">
|
|
<span class="demonstration">物体数:<el-text type="danger">{{ restate.objects }}</el-text></span>
|
|
<span class="demonstration"> 顶点数:<el-text type="danger">{{ restate.vertices }}</el-text></span>
|
|
<span class="demonstration"> 三角形数:<el-text type="danger">{{ restate.faces }}</el-text></span>
|
|
</div>
|
|
</el-space>
|
|
<div class="main-content">
|
|
<div class="canvas-container" ref="canvasContainer" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import * as THREE from 'three'
|
|
import { getCurrentInstance, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
|
import Stats from 'three/examples/jsm/libs/stats.module'
|
|
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry'
|
|
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
|
|
import { Line2 } from 'three/examples/jsm/lines/Line2'
|
|
import MeasureRenderer from '@/modules/measure/MeasureRenderer.ts'
|
|
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry'
|
|
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2'
|
|
|
|
const canvasContainer = ref(null)
|
|
let resizeObserver: ResizeObserver | null = null
|
|
let scene: THREE.Scene | null = null
|
|
let renderer: THREE.WebGLRenderer | null = null
|
|
let viewerDom: HTMLElement | null = null
|
|
let camera: THREE.PerspectiveCamera | THREE.OrthographicCamera | null = null
|
|
let controls: OrbitControls | null = null
|
|
let axesHelper: THREE.AxesHelper | null = null
|
|
let gridHelper: THREE.GridHelper | null = null
|
|
let statsControls: Stats | null = null
|
|
let animationFrameId: number | null = null
|
|
let modelGroup = new THREE.Group()
|
|
|
|
const pointMaterial = new THREE.SpriteMaterial({
|
|
color: 0xFFFF99, // 0x303133,
|
|
transparent: true,
|
|
side: THREE.DoubleSide,
|
|
opacity: 1,
|
|
sizeAttenuation: true
|
|
})
|
|
// const lineMaterial = new LineMaterial({
|
|
// color: 0xFF8C00,
|
|
// linewidth: 5,
|
|
// vertexColors: false,
|
|
// dashed: false
|
|
// })
|
|
const lineMaterial = new LineMaterial({
|
|
color: 0xFF8C00,
|
|
linewidth: 5
|
|
});
|
|
|
|
function test1() {
|
|
const xcount = 100
|
|
const zcount = 100
|
|
const dist = 1.25
|
|
const spacing = dist
|
|
const y = 0.1
|
|
|
|
const points = []
|
|
|
|
// 创建所有点
|
|
for (let z = 0; z < zcount; z++) {
|
|
for (let x = 0; x < xcount; x++) {
|
|
const px = x * dist
|
|
const pz = z * dist
|
|
const point = new THREE.Sprite(pointMaterial)
|
|
point.position.set(px, y, pz)
|
|
point.scale.set(0.25, 0.25, 1) // 设置大小
|
|
scene.add(point)
|
|
points.push(point.position.clone())
|
|
}
|
|
}
|
|
|
|
// // 绘制线段
|
|
// const lines = []
|
|
//
|
|
// // 绘制线段(每两个点一组)
|
|
// for (let z = 0; z < zcount; z++) {
|
|
// for (let x = 0; x < xcount - 1; x++) {
|
|
// const i = z * xcount + x
|
|
// drawLine(points[i], points[i + 1])
|
|
// }
|
|
// }
|
|
//
|
|
// for (let z = 0; z < zcount - 1; z++) {
|
|
// for (let x = 0; x < xcount; x++) {
|
|
// const i = z * xcount + x
|
|
// const belowIndex = (z + 1) * xcount + x
|
|
// drawLine(points[i], points[belowIndex])
|
|
// }
|
|
// }
|
|
const positions = [];
|
|
|
|
// 横向连接(右)
|
|
for (let z = 0; z < zcount; z++) {
|
|
for (let x = 0; x < xcount - 1; x++) {
|
|
const x1 = x * spacing;
|
|
const z1 = z * spacing;
|
|
const x2 = (x + 1) * spacing;
|
|
const z2 = z * spacing;
|
|
positions.push(x1, y, z1, x2, y, z2);
|
|
}
|
|
}
|
|
|
|
// 纵向连接(下)
|
|
for (let z = 0; z < zcount - 1; z++) {
|
|
for (let x = 0; x < xcount; x++) {
|
|
const x1 = x * spacing;
|
|
const z1 = z * spacing;
|
|
const x2 = x * spacing;
|
|
const z2 = (z + 1) * spacing;
|
|
positions.push(x1, y, z1, x2, y, z2);
|
|
}
|
|
}
|
|
|
|
const geometry = new LineSegmentsGeometry();
|
|
geometry.setPositions(positions); // [x1,y1,z1, x2,y2,z2, ...]
|
|
|
|
const material = new LineMaterial({
|
|
color: 0xFF8C00,
|
|
linewidth: 5,
|
|
vertexColors: false
|
|
});
|
|
|
|
const lineSegments = new LineSegments2(geometry, material);
|
|
lineSegments.computeLineDistances(); // 必须调用一次
|
|
lineSegments.name = 'grid-lines';
|
|
scene.add(lineSegments);
|
|
}
|
|
|
|
function drawLine(p1: THREE.Vector3, p2: THREE.Vector3) {
|
|
const geometry = new LineGeometry();
|
|
geometry.setPositions([p1.x, p1.y, p1.z, p2.x, p2.y, p2.z]);
|
|
|
|
// const material = new LineMaterial({
|
|
// color: 0xFF8C00,
|
|
// linewidth: 5
|
|
// });
|
|
|
|
const line = new Line2(geometry, lineMaterial);
|
|
line.computeLineDistances();
|
|
|
|
scene.add(line)
|
|
}
|
|
|
|
const restate = reactive({
|
|
targetColor: '#ff0000',
|
|
loadScale: 1,
|
|
mode: 'translate',
|
|
objects: 0,
|
|
vertices: 0,
|
|
faces: 0
|
|
})
|
|
|
|
// 状态变量
|
|
const state = {
|
|
showAxesHelper: true,
|
|
showGridHelper: true,
|
|
camera: {
|
|
position: { x: 0, y: 5, z: 10 },
|
|
rotation: { x: 0, y: 0, z: 0 }
|
|
}
|
|
}
|
|
onMounted(() => {
|
|
nextTick(() => {
|
|
initThree()
|
|
|
|
const viewerDom = canvasContainer.value
|
|
if (resizeObserver) {
|
|
resizeObserver.unobserve(viewerDom)
|
|
}
|
|
resizeObserver = new ResizeObserver(handleResize)
|
|
resizeObserver.observe(viewerDom)
|
|
|
|
window['cp'] = getCurrentInstance()
|
|
})
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (animationFrameId !== null) {
|
|
cancelAnimationFrame(animationFrameId)
|
|
animationFrameId = null
|
|
}
|
|
|
|
cleanupThree()
|
|
|
|
const viewerDom = canvasContainer.value
|
|
if (resizeObserver) {
|
|
resizeObserver.unobserve(viewerDom)
|
|
}
|
|
|
|
window['cp'] = null
|
|
})
|
|
|
|
function initThree() {
|
|
viewerDom = canvasContainer.value
|
|
if (!viewerDom) {
|
|
console.error('Viewer DOM element not found')
|
|
return
|
|
}
|
|
|
|
// 场景
|
|
scene = new THREE.Scene()
|
|
scene.background = new THREE.Color(0xeeeeee)
|
|
|
|
// 渲染器
|
|
renderer = new THREE.WebGLRenderer({
|
|
antialias: true,
|
|
alpha: true,
|
|
powerPreference: 'high-performance'
|
|
})
|
|
renderer.clearDepth()
|
|
renderer.setPixelRatio(window.devicePixelRatio)
|
|
renderer.setSize(viewerDom.clientWidth, viewerDom.clientHeight)
|
|
viewerDom.appendChild(renderer.domElement)
|
|
|
|
// 摄像机
|
|
// initMode3DCamera()
|
|
initMode2DCamera()
|
|
|
|
// 辅助线
|
|
axesHelper = new THREE.AxesHelper(5)
|
|
scene.add(axesHelper)
|
|
|
|
gridHelper = new THREE.GridHelper(1000, 1000)
|
|
scene.add(gridHelper)
|
|
gridHelper.visible = false
|
|
|
|
// 光照
|
|
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.right = '0'
|
|
statsControls.dom.style.left = 'auto'
|
|
viewerDom.appendChild(statsControls.dom)
|
|
|
|
animate()
|
|
}
|
|
|
|
// 动画循环
|
|
function animate() {
|
|
animationFrameId = requestAnimationFrame(animate)
|
|
renderView()
|
|
}
|
|
|
|
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.setPixelRatio(window.devicePixelRatio)
|
|
renderer.setSize(width, height)
|
|
break
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 初始化2D相机
|
|
*/
|
|
function initMode2DCamera() {
|
|
if (camera) {
|
|
scene.remove(camera)
|
|
}
|
|
|
|
// ============================ 创建正交相机
|
|
const cameraNew = new THREE.OrthographicCamera(
|
|
viewerDom.clientWidth / -2,
|
|
viewerDom.clientWidth / 2,
|
|
viewerDom.clientHeight / 2,
|
|
viewerDom.clientHeight / -2,
|
|
1,
|
|
500
|
|
)
|
|
cameraNew.position.set(0, 60, 0)
|
|
cameraNew.lookAt(0, 0, 0)
|
|
cameraNew.zoom = 60
|
|
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, RIGHT: 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
|
|
|
|
cameraNew.updateProjectionMatrix()
|
|
}
|
|
|
|
function initMode3DCamera() {
|
|
if (camera) {
|
|
scene.remove(camera)
|
|
}
|
|
|
|
const viewerDom = canvasContainer.value
|
|
|
|
// ============================ 创建透视相机
|
|
const cameraNew = new THREE.PerspectiveCamera(25, viewerDom.clientWidth / viewerDom.clientHeight, 0.1, 2000)
|
|
cameraNew.position.set(5, 5, 5)
|
|
cameraNew.lookAt(0, 0, 0)
|
|
|
|
camera = cameraNew
|
|
scene.add(camera)
|
|
|
|
const controlsNew = new OrbitControls(camera, viewerDom)
|
|
controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.ROTATE } // 鼠标中键平移
|
|
controlsNew.enableDamping = false
|
|
controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动
|
|
controlsNew.minDistance = 2
|
|
controlsNew.addEventListener('change', syncCameraState)
|
|
controls = controlsNew
|
|
controls.update()
|
|
|
|
camera.updateProjectionMatrix()
|
|
|
|
syncCameraState()
|
|
}
|
|
|
|
/**
|
|
* 重新加载相机状态到全局状态
|
|
*/
|
|
function syncCameraState() {
|
|
if (camera) {
|
|
state.camera.position.x = camera.position.x
|
|
state.camera.position.y = camera.position.y
|
|
state.camera.position.z = camera.position.z
|
|
|
|
state.camera.rotation.x = camera.rotation.x
|
|
state.camera.rotation.y = camera.rotation.y
|
|
state.camera.rotation.z = camera.rotation.z
|
|
}
|
|
}
|
|
|
|
function renderView() {
|
|
statsControls?.update()
|
|
renderer?.render(scene, camera)
|
|
}
|
|
|
|
|
|
function cleaupModel() {
|
|
if (modelGroup) {
|
|
scene.remove(modelGroup)
|
|
}
|
|
tcontrols.detach()
|
|
transformEditCtl.value.detach()
|
|
}
|
|
|
|
function cleanupThree() {
|
|
// 移除旧模型
|
|
if (scene) {
|
|
scene.traverse((obj: THREE.Mesh) => {
|
|
// 释放几何体
|
|
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()
|
|
}
|
|
})
|
|
|
|
if (modelGroup) {
|
|
scene.remove(modelGroup)
|
|
}
|
|
// 清空场景
|
|
scene.children = []
|
|
|
|
modelGroup = null
|
|
}
|
|
|
|
if (statsControls) {
|
|
statsControls.dom.remove()
|
|
}
|
|
|
|
if (renderer) {
|
|
renderer.dispose()
|
|
renderer.forceContextLoss()
|
|
console.log('WebGL disposed, memory:', renderer.info.memory)
|
|
renderer.domElement = null
|
|
}
|
|
}
|
|
|
|
|
|
</script>
|
|
<style scoped lang="less">
|
|
.model3d-view {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-grow: 1;
|
|
overflow: hidden;
|
|
|
|
.main-content {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
|
|
.model3d-content {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
}
|
|
|
|
.canvas-container {
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
|