12 changed files with 1229 additions and 239 deletions
@ -0,0 +1,828 @@ |
|||||
|
<template> |
||||
|
<div class="model3d-view"> |
||||
|
<el-space :gutter="10" class="toolbar"> |
||||
|
<el-button type="primary" @click="test1">测试1</el-button> |
||||
|
<el-button type="primary" @click="test2">测试2</el-button> |
||||
|
<el-button type="primary" @click="test3">测试3</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> |
||||
|
<span class="demonstration"> 标签:<el-text type="danger">{{ restate.viewLabelCount }}</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' |
||||
|
import { Text } from 'troika-three-text' |
||||
|
import SimSunTTF from '@/assets/fonts/simsunb.ttf' |
||||
|
|
||||
|
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 |
||||
|
}) |
||||
|
|
||||
|
/** |
||||
|
* 测试1:创建平面标识 |
||||
|
*/ |
||||
|
function test1() { |
||||
|
// cleanupThree() |
||||
|
|
||||
|
const [x, y, z] = [5, 0, 5] |
||||
|
|
||||
|
// 宽度 |
||||
|
const width = 1.5 |
||||
|
// 深度 |
||||
|
const depth = 2.0 |
||||
|
// 线条宽度 |
||||
|
const strokeWidth = 0.1 |
||||
|
// 每一组货架有几节 |
||||
|
const sectionCount = 1 |
||||
|
|
||||
|
const material = new THREE.MeshBasicMaterial({ |
||||
|
color: 0xd8dad0, |
||||
|
transparent: true, |
||||
|
opacity: 1, |
||||
|
side: THREE.BackSide |
||||
|
}) |
||||
|
|
||||
|
const mesh = new THREE.Mesh( |
||||
|
createGroundMarking(strokeWidth, width, depth, sectionCount, x, y, z), |
||||
|
material) |
||||
|
scene.add(mesh) |
||||
|
|
||||
|
refreshCount() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建平面标识 |
||||
|
*/ |
||||
|
function createGroundMarking(weight: number, width: number, depth: number, sectionCount: number, x: number, y: number, z: number, anchor?: THREE.Vector3) { |
||||
|
const sectionDepth = (depth - (sectionCount + 1) * weight) / sectionCount |
||||
|
|
||||
|
const shape = buildRectangle(width, depth) |
||||
|
for (let i = 0; i < sectionCount; i++) { |
||||
|
shape.holes.push(buildRectangle(width - 2 * weight, sectionDepth, 0, weight, weight + i * (weight + sectionDepth))) |
||||
|
} |
||||
|
|
||||
|
return createExtrudeItem(shape, 0.1, BasePlane.BOTTOM, x, y, -z, null, anchor) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 根据自定义的 Shape,通过放样得到一个实体。默认实体的样式是在 front 面放样的。 |
||||
|
* |
||||
|
* @param {*} shape 自定义的型状 |
||||
|
* @param {*} depth 放样深度(放样后的物体厚度) |
||||
|
* @param {*} basePlane 放样的基准面。 |
||||
|
* @param {*} x 目标定位x |
||||
|
* @param {*} y 目标定位y |
||||
|
* @param {*} z 目标定位z |
||||
|
* @param {*} euler 旋转到目标位 |
||||
|
* @param {*} anchor 锚点 |
||||
|
* @returns THREE.ExtrudeGeometry |
||||
|
*/ |
||||
|
function createExtrudeItem(shape, depth, basePlane, x = 0, y = 0, z = 0, euler = null, anchor = new THREE.Vector3(0, 0, 0)) { |
||||
|
const geometry = new THREE.ExtrudeGeometry(shape, { |
||||
|
steps: 1, |
||||
|
depth: -depth, |
||||
|
bevelEnabled: false, |
||||
|
bevelThickness: 0, |
||||
|
bevelSize: 0, |
||||
|
bevelOffset: 0, |
||||
|
bevelSegments: 0 |
||||
|
}) |
||||
|
|
||||
|
geometry.center() |
||||
|
const size = geometry.boundingBox.getSize(new THREE.Vector3()) |
||||
|
|
||||
|
let dx, dy, dz |
||||
|
|
||||
|
switch (basePlane) { |
||||
|
case BasePlane.LEFT: |
||||
|
geometry.rotateY(Math.PI / 2) |
||||
|
dx = size.z / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.x / 2 |
||||
|
break |
||||
|
case BasePlane.RIGHT: |
||||
|
geometry.rotateY(-Math.PI / 2) |
||||
|
dx = size.z / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.x / 2 |
||||
|
break |
||||
|
case BasePlane.BEHIND: |
||||
|
geometry.translate(0, 0, size.z) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.z / 2 |
||||
|
break |
||||
|
case BasePlane.TOP: |
||||
|
geometry.rotateZ(Math.PI) |
||||
|
geometry.rotateX(Math.PI / 2) |
||||
|
geometry.translate(size.x, -size.z, 0) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.z / 2 |
||||
|
dz = size.y / 2 |
||||
|
break |
||||
|
case BasePlane.BOTTOM: |
||||
|
geometry.rotateX(-Math.PI / 2) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.z / 2 |
||||
|
dz = size.y / 2 |
||||
|
break |
||||
|
default: |
||||
|
//BasePlane.FRONT: |
||||
|
dx = size.x / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.z / 2 |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
if (euler != null && euler.isEuler) { |
||||
|
// 注意,需要先旋转,再平移。 |
||||
|
geometry.rotateX(euler.x) |
||||
|
geometry.rotateY(euler.y) |
||||
|
geometry.rotateZ(euler.z) |
||||
|
} |
||||
|
|
||||
|
geometry.translate(dx + x + anchor.x, dy + y + anchor.y, -dz + z + anchor.z) |
||||
|
|
||||
|
return geometry |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建一个矩形的 Shape |
||||
|
*/ |
||||
|
function buildRectangle(width: number, height: number, padding: number[] | number = 0, offsetX = 0, offsetY = 0) { |
||||
|
let pl = 0, |
||||
|
pt = 0, |
||||
|
pr = 0, |
||||
|
pb = 0 |
||||
|
|
||||
|
if (typeof padding == 'number') { |
||||
|
pl = padding |
||||
|
pt = padding |
||||
|
pr = padding |
||||
|
pb = padding |
||||
|
} else if (Array.isArray(padding)) { |
||||
|
if (padding.length == 1) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[0] |
||||
|
pr = padding[0] |
||||
|
pb = padding[0] |
||||
|
} else if (padding.length > 1 && padding.length < 4) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[1] |
||||
|
pr = padding[0] |
||||
|
pb = padding[1] |
||||
|
} else if (padding.length >= 4) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[1] |
||||
|
pr = padding[2] |
||||
|
pb = padding[3] |
||||
|
} |
||||
|
} |
||||
|
const shape = new THREE.Shape() |
||||
|
shape.moveTo(0 + pl + offsetX, 0 + pb + offsetY) |
||||
|
shape.lineTo(width - pr + offsetX, 0 + pb + offsetY) |
||||
|
shape.lineTo(width - pr + offsetX, height - pt + offsetY) |
||||
|
shape.lineTo(0 + pl + offsetX, height - pt + offsetY) |
||||
|
shape.closePath() |
||||
|
return shape |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 平面产量 |
||||
|
*/ |
||||
|
const BasePlane = { |
||||
|
LEFT: 0b000010, |
||||
|
RIGHT: 0b000001, |
||||
|
FRONT: 0b001000, |
||||
|
BEHIND: 0b000100, |
||||
|
TOP: 0b100000, |
||||
|
BOTTOM: 0b010000, |
||||
|
TRANSVERSE: 0b10000000, |
||||
|
LONGITUDINAL: 0b01000000, |
||||
|
THROW: 0b00100000, |
||||
|
toArray: () => { |
||||
|
return [BasePlane.LEFT, BasePlane.BEHIND, BasePlane.RIGHT, BasePlane.FRONT, BasePlane.BOTTOM, BasePlane.TOP, BasePlane.TRANSVERSE, BasePlane.LONGITUDINAL, BasePlane.THROW] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* LineSegments2 |
||||
|
*/ |
||||
|
function test2() { |
||||
|
const count = 10000 // 要创建的实例数量 |
||||
|
|
||||
|
const material = new THREE.MeshBasicMaterial({ |
||||
|
color: '#038217', |
||||
|
transparent: true, |
||||
|
opacity: 1, |
||||
|
side: THREE.BackSide |
||||
|
}) |
||||
|
|
||||
|
// 创建 InstancedMesh |
||||
|
const instancedMesh = new THREE.InstancedMesh(groundMarkingGeometry, material, count) |
||||
|
|
||||
|
// 设置所有实例的世界变换矩阵, 10列 列间距0.3, |
||||
|
for (let i = 0; i < count; i++) { |
||||
|
const x = (i % 10) * 3 |
||||
|
const y = 0 |
||||
|
const z = Math.floor(i / 10) * 3 |
||||
|
console.log(`Setting instance [${i}]=(${x}, ${z})`) |
||||
|
|
||||
|
const matrix = new THREE.Matrix4() |
||||
|
matrix.setPosition(x, y, z) // 可以加旋转/缩放 |
||||
|
instancedMesh.setMatrixAt(i, matrix) |
||||
|
} |
||||
|
|
||||
|
scene.add(instancedMesh) |
||||
|
|
||||
|
refreshCount() |
||||
|
} |
||||
|
|
||||
|
const groundMarkingGeometry = createGroundMarkingGeometry() // 共享几何体 |
||||
|
|
||||
|
function createGroundMarkingGeometry() { |
||||
|
const weight = 0.1 |
||||
|
const width = 1.5 |
||||
|
const depth = 2.0 |
||||
|
const sectionCount = 1 |
||||
|
|
||||
|
const sectionDepth = (depth - (sectionCount + 1) * weight) / sectionCount |
||||
|
|
||||
|
const shape = buildRectangle(width, depth) |
||||
|
for (let i = 0; i < sectionCount; i++) { |
||||
|
shape.holes.push( |
||||
|
buildRectangle( |
||||
|
width - 2 * weight, |
||||
|
sectionDepth, |
||||
|
0, |
||||
|
weight, |
||||
|
weight + i * (weight + sectionDepth) |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
return createExtrudeItem(shape, 0.1, BasePlane.BOTTOM, 0, 0, 0) |
||||
|
} |
||||
|
|
||||
|
function isLabelInView(label, frustum) { |
||||
|
const pos = new THREE.Vector3() |
||||
|
label.getWorldPosition(pos) |
||||
|
|
||||
|
// 正交相机的视锥体范围 |
||||
|
if (frustum.containsPoint(pos)) { |
||||
|
// 检查标签是否在相机位置的最大距离内 |
||||
|
if (shouldShowLabel(label)) { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
const labels: Text[] = [] |
||||
|
|
||||
|
function getLabelPixelSize(fontSize, cameraZoom) { |
||||
|
const pixelRatio = renderer.getPixelRatio() |
||||
|
const referenceZoom = 1 |
||||
|
const referenceFontSize = 0.2 |
||||
|
const referencePixelSize = 16 // fontSize=0.2, zoom=1 时显示为 16px |
||||
|
|
||||
|
const scale = (fontSize / referenceFontSize) * (cameraZoom / referenceZoom) |
||||
|
return referencePixelSize * scale * pixelRatio |
||||
|
} |
||||
|
|
||||
|
function shouldShowLabel(label, minPixelSize = 700) { |
||||
|
const pixelSize = getLabelPixelSize(label.fontSize, camera.zoom) |
||||
|
return pixelSize >= minPixelSize |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* InstanceMesh(Point) + BufferGeometry + Label |
||||
|
*/ |
||||
|
function test3() { |
||||
|
cleanupThree() // 清空画布 |
||||
|
const xcount = 300 |
||||
|
const zcount = 100 |
||||
|
const dist = 1.25 |
||||
|
const spacing = dist |
||||
|
const y = 0.1 |
||||
|
|
||||
|
const noShaderMaterial = new THREE.MeshBasicMaterial({ |
||||
|
color: 0xFFFF99, |
||||
|
transparent: true, |
||||
|
depthWrite: false, |
||||
|
side: THREE.DoubleSide |
||||
|
}) |
||||
|
const planeGeometry = new THREE.PlaneGeometry(0.25, 0.25) |
||||
|
|
||||
|
// 使用 InstancedMesh 网格优化 |
||||
|
const instancedMesh = new THREE.InstancedMesh(planeGeometry, noShaderMaterial, zcount * xcount) |
||||
|
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage) |
||||
|
const dummy = new THREE.Object3D() |
||||
|
|
||||
|
const points = [] |
||||
|
|
||||
|
// 创建所有点 |
||||
|
for (let z = 0; z < zcount; z++) { |
||||
|
for (let x = 0; x < xcount; x++) { |
||||
|
const px = x * dist |
||||
|
const pz = z * dist |
||||
|
|
||||
|
dummy.position.set(px, y, pz) |
||||
|
dummy.rotation.set(-Math.PI / 2, 0, 0) |
||||
|
dummy.updateMatrix() |
||||
|
instancedMesh.setMatrixAt(z * xcount + x, dummy.matrix) |
||||
|
|
||||
|
points.push(new THREE.Vector3(px, y, pz)) |
||||
|
} |
||||
|
} |
||||
|
scene.add(instancedMesh) |
||||
|
|
||||
|
const positions = [] |
||||
|
labels.length = 0 |
||||
|
|
||||
|
function createTextLabel(text, position): Text { |
||||
|
const label = new Text() |
||||
|
label.text = text |
||||
|
label.font = SimSunTTF |
||||
|
label.fontSize = 0.2 |
||||
|
label.color = '#333333' |
||||
|
label.opacity = 0.8 |
||||
|
label.padding = 0.2 |
||||
|
label.anchorX = 'center' |
||||
|
label.anchorY = 'middle' |
||||
|
label.depthOffset = 1 |
||||
|
label.backgroundColor = '#000000' |
||||
|
label.backgroundOpacity = 0.6 |
||||
|
label.material.depthTest = false |
||||
|
label.position.copy(position) |
||||
|
label.name = MeasureRenderer.LABEL_NAME |
||||
|
|
||||
|
label.position.set(position.x, position.y + 0.3, position.z - 0.2) |
||||
|
label.quaternion.copy(camera.quaternion) |
||||
|
label.visible = false |
||||
|
// label.sync() |
||||
|
|
||||
|
return label |
||||
|
} |
||||
|
|
||||
|
// 横向连接(右) |
||||
|
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) |
||||
|
|
||||
|
// 计算中点和长度 |
||||
|
const midPoint = new THREE.Vector3((x1 + x2) / 2, y + 0.5, (z1 + z2) / 2) |
||||
|
const length = Math.hypot(x2 - x1, y - y, z2 - z1) |
||||
|
const label = createTextLabel(length.toFixed(2) + 'm', midPoint) |
||||
|
labels.push(label) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 纵向连接(下) |
||||
|
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 midPoint = new THREE.Vector3((x1 + x2) / 2, y + 0.5, (z1 + z2) / 2) |
||||
|
const length = Math.hypot(x2 - x1, y - y, z2 - z1) |
||||
|
const label = createTextLabel(length.toFixed(2) + 'm', midPoint) |
||||
|
labels.push(label) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const positionNums = new Float32Array(positions) |
||||
|
const geometry = new THREE.BufferGeometry() |
||||
|
geometry.setAttribute('position', new THREE.BufferAttribute(positionNums, 3)) |
||||
|
const material = new THREE.LineBasicMaterial({ color: 0xFF8C00 }) |
||||
|
const lineSegments = new THREE.LineSegments(geometry, material) |
||||
|
lineSegments.name = 'grid-lines' |
||||
|
scene.add(lineSegments) |
||||
|
|
||||
|
labels.forEach(label => scene.add(label)) |
||||
|
|
||||
|
// 统计总数量 |
||||
|
refreshCount() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
const restate = reactive({ |
||||
|
targetColor: '#ff0000', |
||||
|
loadScale: 1, |
||||
|
viewLabelCount: 0, |
||||
|
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) |
||||
|
|
||||
|
renderer.setAnimationLoop(animate) |
||||
|
renderer.setClearColor(0x000000, 0) |
||||
|
|
||||
|
// animate() |
||||
|
} |
||||
|
|
||||
|
// 动画循环 |
||||
|
let frameCount = 0 |
||||
|
|
||||
|
function animate() { |
||||
|
// animationFrameId = requestAnimationFrame(animate) |
||||
|
renderView() |
||||
|
|
||||
|
if (frameCount++ % 60 === 0) { // 每 60 帧更新一次文本 |
||||
|
const frustum = new THREE.Frustum() |
||||
|
const cameraCopy = camera.clone() |
||||
|
|
||||
|
// 必须更新相机的世界矩阵 |
||||
|
cameraCopy.updateMatrixWorld() |
||||
|
|
||||
|
// 构造投影矩阵 |
||||
|
const projScreenMatrix = new THREE.Matrix4().multiplyMatrices( |
||||
|
cameraCopy.projectionMatrix, |
||||
|
cameraCopy.matrixWorldInverse |
||||
|
) |
||||
|
|
||||
|
// 设置视锥体 |
||||
|
frustum.setFromProjectionMatrix(projScreenMatrix) |
||||
|
|
||||
|
let viewLabelCount = 0 |
||||
|
labels.forEach((label: Text) => { |
||||
|
// label.quaternion.copy(camera.quaternion) // billboard 效果保持朝向相机 |
||||
|
const isvis = isLabelInView(label, frustum, camera.position) |
||||
|
if (isvis) { |
||||
|
viewLabelCount++ |
||||
|
} |
||||
|
if (isvis && label.visible === false) { |
||||
|
label.visible = true |
||||
|
label.sync() |
||||
|
} else if (!isvis && label.visible === true) { |
||||
|
label.visible = false |
||||
|
label.sync() |
||||
|
} |
||||
|
}) |
||||
|
restate.viewLabelCount = viewLabelCount |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleResize(entries) { |
||||
|
for (let entry of entries) { |
||||
|
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 refreshCount() { |
||||
|
// 遍历场景中的所有对象 |
||||
|
let totalObjects = 0 |
||||
|
let totalVertices = 0 |
||||
|
let totalFaces = 0 |
||||
|
scene.traverse(function(child) { |
||||
|
if (child.isMesh) { |
||||
|
totalObjects++ |
||||
|
|
||||
|
// 获取几何体 |
||||
|
const geometry = child.geometry |
||||
|
|
||||
|
// 如果几何体是 BufferGeometry 类型 |
||||
|
if (geometry.isBufferGeometry) { |
||||
|
// 计算顶点数 |
||||
|
if (geometry.attributes.position) { |
||||
|
totalVertices += geometry.attributes.position.count |
||||
|
} |
||||
|
|
||||
|
// 计算面数(假设每个面都是由三个顶点组成的三角形) |
||||
|
if (geometry.index) { |
||||
|
totalFaces += geometry.index.count / 3 |
||||
|
} else if (geometry.attributes.position) { |
||||
|
// 如果没有索引,计算非索引几何体的面数 |
||||
|
totalFaces += geometry.attributes.position.count / 3 |
||||
|
} |
||||
|
} |
||||
|
// 如果几何体是 Geometry 类型(较旧的版本使用) |
||||
|
else if (geometry.isGeometry) { |
||||
|
// 计算顶点数 |
||||
|
totalVertices += geometry.vertices.length |
||||
|
|
||||
|
// 计算面数 |
||||
|
totalFaces += geometry.faces.length |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
restate.objects = totalObjects |
||||
|
restate.vertices = totalVertices |
||||
|
restate.faces = totalFaces |
||||
|
} |
||||
|
|
||||
|
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() |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// 清空场景 |
||||
|
scene.children = [] |
||||
|
modelGroup = 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> |
||||
@ -0,0 +1,130 @@ |
|||||
|
import * as THREE from 'three' |
||||
|
import { BasePlane } from '@/types/ModelTypes.ts' |
||||
|
|
||||
|
/** |
||||
|
* 创建一个矩形的 Shape |
||||
|
*/ |
||||
|
export function buildRectangle(width: number, height: number, padding: number[] | number = 0, offsetX = 0, offsetY = 0): THREE.Shape { |
||||
|
let pl = 0, |
||||
|
pt = 0, |
||||
|
pr = 0, |
||||
|
pb = 0 |
||||
|
|
||||
|
if (typeof padding == 'number') { |
||||
|
pl = padding |
||||
|
pt = padding |
||||
|
pr = padding |
||||
|
pb = padding |
||||
|
} else if (Array.isArray(padding)) { |
||||
|
if (padding.length == 1) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[0] |
||||
|
pr = padding[0] |
||||
|
pb = padding[0] |
||||
|
} else if (padding.length > 1 && padding.length < 4) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[1] |
||||
|
pr = padding[0] |
||||
|
pb = padding[1] |
||||
|
} else if (padding.length >= 4) { |
||||
|
pl = padding[0] |
||||
|
pt = padding[1] |
||||
|
pr = padding[2] |
||||
|
pb = padding[3] |
||||
|
} |
||||
|
} |
||||
|
const shape = new THREE.Shape() |
||||
|
shape.moveTo(0 + pl + offsetX, 0 + pb + offsetY) |
||||
|
shape.lineTo(width - pr + offsetX, 0 + pb + offsetY) |
||||
|
shape.lineTo(width - pr + offsetX, height - pt + offsetY) |
||||
|
shape.lineTo(0 + pl + offsetX, height - pt + offsetY) |
||||
|
shape.closePath() |
||||
|
return shape |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 根据自定义的 Shape,通过放样得到一个实体。默认实体的样式是在 front 面放样的。 |
||||
|
* |
||||
|
* @param {*} shape 自定义的型状 |
||||
|
* @param {*} depth 放样深度(放样后的物体厚度) |
||||
|
* @param {*} basePlane 放样的基准面。 |
||||
|
* @param {*} x 目标定位x |
||||
|
* @param {*} y 目标定位y |
||||
|
* @param {*} z 目标定位z |
||||
|
* @param {*} euler 旋转到目标位 |
||||
|
* @param {*} anchor 锚点 |
||||
|
* @returns THREE.ExtrudeGeometry |
||||
|
*/ |
||||
|
export function createExtrudeItem(shape: THREE.Shape, depth: number, basePlane: number, |
||||
|
x = 0, y = 0, z = 0, euler = null, |
||||
|
anchor = new THREE.Vector3(0, 0, 0)) { |
||||
|
const geometry = new THREE.ExtrudeGeometry(shape, { |
||||
|
steps: 1, |
||||
|
depth: -depth, |
||||
|
bevelEnabled: false, |
||||
|
bevelThickness: 0, |
||||
|
bevelSize: 0, |
||||
|
bevelOffset: 0, |
||||
|
bevelSegments: 0 |
||||
|
}) |
||||
|
|
||||
|
geometry.center() |
||||
|
const size = geometry.boundingBox.getSize(new THREE.Vector3()) |
||||
|
|
||||
|
let dx, dy, dz |
||||
|
|
||||
|
switch (basePlane) { |
||||
|
case BasePlane.LEFT: |
||||
|
geometry.rotateY(Math.PI / 2) |
||||
|
dx = size.z / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.x / 2 |
||||
|
break |
||||
|
case BasePlane.RIGHT: |
||||
|
geometry.rotateY(-Math.PI / 2) |
||||
|
dx = size.z / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.x / 2 |
||||
|
break |
||||
|
case BasePlane.BEHIND: |
||||
|
geometry.translate(0, 0, size.z) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.z / 2 |
||||
|
break |
||||
|
case BasePlane.TOP: |
||||
|
geometry.rotateZ(Math.PI) |
||||
|
geometry.rotateX(Math.PI / 2) |
||||
|
geometry.translate(size.x, -size.z, 0) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.z / 2 |
||||
|
dz = size.y / 2 |
||||
|
break |
||||
|
case BasePlane.BOTTOM: |
||||
|
geometry.rotateX(-Math.PI / 2) |
||||
|
dx = size.x / 2 |
||||
|
dy = size.z / 2 |
||||
|
dz = size.y / 2 |
||||
|
break |
||||
|
default: |
||||
|
//BasePlane.FRONT:
|
||||
|
dx = size.x / 2 |
||||
|
dy = size.y / 2 |
||||
|
dz = size.z / 2 |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
if (euler != null && euler.isEuler) { |
||||
|
// 注意,需要先旋转,再平移。
|
||||
|
geometry.rotateX(euler.x) |
||||
|
geometry.rotateY(euler.y) |
||||
|
geometry.rotateZ(euler.z) |
||||
|
} |
||||
|
|
||||
|
geometry.translate(dx + x + anchor.x, dy + y + anchor.y, -dz + z + anchor.z) |
||||
|
geometry.center() |
||||
|
return geometry |
||||
|
} |
||||
|
|
||||
|
|
||||
@ -1,12 +1,20 @@ |
|||||
import type { PropertySetter } from "@/core/base/PropertyTypes.ts"; |
import type { PropertySetter } from '@/core/base/PropertyTypes.ts' |
||||
import { basicFieldsSetter } from "@/editor/widgets/property/PropertyPanelConstant.ts"; |
import { basicFieldsSetter } from '@/editor/widgets/property/PropertyPanelConstant.ts' |
||||
|
|
||||
const propertySetter: PropertySetter = { |
const propertySetter: PropertySetter = { |
||||
flatten: { |
flatten: { |
||||
fields: [ |
fields: [ |
||||
...basicFieldsSetter, |
...basicFieldsSetter, |
||||
], |
{ |
||||
|
dataPath: 'dt.strokeColor', label: '边线颜色', input: 'ColorPicker', |
||||
|
inputProps: {} |
||||
}, |
}, |
||||
}; |
{ |
||||
|
dataPath: 'dt.strokeWidth', label: '边线宽度', input: 'InputNumber', |
||||
|
inputProps: {} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
|
||||
export default propertySetter; |
export default propertySetter |
||||
|
|||||
Loading…
Reference in new issue