This commit is contained in:
parent
16ed8e8b61
commit
f5812a995c
|
|
@ -0,0 +1,388 @@
|
|||
<template>
|
||||
<div class="dxf-viewer-root">
|
||||
<div class="dxf-sidebar">
|
||||
<div class="sidebar-title">图层列表</div>
|
||||
<ul class="layer-list">
|
||||
<li
|
||||
v-for="(layer, idx) in layerList"
|
||||
:key="layer.layerName"
|
||||
:class="{ active: idx === selectedLayerIndex }"
|
||||
@click="selectLayer(idx)"
|
||||
>
|
||||
{{ layer.layerName }} <span class="entity-count">({{ layer.entities.length }})</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="dxf-main">
|
||||
<div class="entity-list">
|
||||
<div class="entity-title">图元列表</div>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(entity, i) in selectedLayerEntities"
|
||||
:key="entity.id"
|
||||
:class="{ selected: selectedEntityId === entity.id }"
|
||||
@click="selectEntity(entity.id)"
|
||||
>
|
||||
<span class="entity-type">{{ entity.entityType }}</span>
|
||||
<span class="entity-id">#{{ entity.id }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="threejs-container" ref="threeContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
export default {
|
||||
name: 'DxfLayerEntityViewer',
|
||||
props: {
|
||||
entities: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
layerList: [],
|
||||
selectedLayerIndex: 0,
|
||||
selectedEntityId: null,
|
||||
scene: null,
|
||||
camera: null,
|
||||
renderer: null,
|
||||
controls: null,
|
||||
entityMeshMap: {}, // id: mesh
|
||||
scaleFactor: 1, // 动态计算
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 0,
|
||||
maxY: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedLayerEntities() {
|
||||
if (!this.layerList.length) return []
|
||||
return this.layerList[this.selectedLayerIndex].entities
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
entities: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.prepareLayers(val)
|
||||
this.$nextTick(() => {
|
||||
this.initThree()
|
||||
})
|
||||
}
|
||||
},
|
||||
selectedLayerIndex() {
|
||||
this.highlightEntities()
|
||||
},
|
||||
selectedEntityId() {
|
||||
this.highlightEntities()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.onWindowResize)
|
||||
if (this.renderer) {
|
||||
this.renderer.dispose()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prepareLayers(entities) {
|
||||
// 分组
|
||||
const map = {}
|
||||
entities.forEach(e => {
|
||||
if (!map[e.layerName]) map[e.layerName] = []
|
||||
map[e.layerName].push(e)
|
||||
})
|
||||
this.layerList = Object.keys(map).map(layerName => ({
|
||||
layerName,
|
||||
entities: map[layerName]
|
||||
}))
|
||||
this.selectedLayerIndex = 0
|
||||
this.selectedEntityId = null
|
||||
},
|
||||
selectLayer(idx) {
|
||||
this.selectedLayerIndex = idx
|
||||
this.selectedEntityId = null
|
||||
},
|
||||
selectEntity(id) {
|
||||
this.selectedEntityId = id
|
||||
},
|
||||
initThree() {
|
||||
// 清理
|
||||
if (this.renderer && this.$refs.threeContainer) {
|
||||
this.$refs.threeContainer.innerHTML = ''
|
||||
}
|
||||
this.scene = new THREE.Scene()
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
60,
|
||||
this.$refs.threeContainer.clientWidth / this.$refs.threeContainer.clientHeight,
|
||||
0.1,
|
||||
100000
|
||||
)
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
||||
this.renderer.setSize(
|
||||
this.$refs.threeContainer.clientWidth,
|
||||
this.$refs.threeContainer.clientHeight
|
||||
)
|
||||
this.renderer.setClearColor(0xf0f2f5, 1)
|
||||
this.$refs.threeContainer.appendChild(this.renderer.domElement)
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
|
||||
this.controls.enableDamping = true
|
||||
this.controls.dampingFactor = 0.05
|
||||
this.controls.screenSpacePanning = true
|
||||
// 灯光
|
||||
this.scene.add(new THREE.AmbientLight(0xffffff, 0.6))
|
||||
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8)
|
||||
dirLight.position.set(100, 100, 100)
|
||||
this.scene.add(dirLight)
|
||||
// 网格
|
||||
const gridHelper = new THREE.GridHelper(100, 10, 0x444444, 0x888888)
|
||||
this.scene.add(gridHelper)
|
||||
// 坐标轴
|
||||
const axesHelper = new THREE.AxesHelper(50)
|
||||
this.scene.add(axesHelper)
|
||||
// 渲染实体
|
||||
this.renderAllEntities()
|
||||
this.animate()
|
||||
},
|
||||
renderAllEntities() {
|
||||
// 计算边界
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
||||
this.entityMeshMap = {}
|
||||
this.entities.forEach(entity => {
|
||||
const geometry = entity.geometry ? JSON.parse(entity.geometry) : null
|
||||
if (!geometry) return
|
||||
if (entity.entityType === 'LINE') {
|
||||
if (Array.isArray(geometry.start)) {
|
||||
minX = Math.min(minX, geometry.start[0], geometry.end[0])
|
||||
minY = Math.min(minY, geometry.start[1], geometry.end[1])
|
||||
maxX = Math.max(maxX, geometry.start[0], geometry.end[0])
|
||||
maxY = Math.max(maxY, geometry.start[1], geometry.end[1])
|
||||
}
|
||||
} else if (entity.entityType === 'CIRCLE') {
|
||||
if (Array.isArray(geometry.center)) {
|
||||
minX = Math.min(minX, geometry.center[0] - geometry.radius)
|
||||
minY = Math.min(minY, geometry.center[1] - geometry.radius)
|
||||
maxX = Math.max(maxX, geometry.center[0] + geometry.radius)
|
||||
maxY = Math.max(maxY, geometry.center[1] + geometry.radius)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.minX = minX
|
||||
this.minY = minY
|
||||
this.maxX = maxX
|
||||
this.maxY = maxY
|
||||
this.offsetX = (minX + maxX) / 2
|
||||
this.offsetY = (minY + maxY) / 2
|
||||
|
||||
// 缩放适配,保持宽高比
|
||||
const containerWidth = this.$refs.threeContainer.clientWidth
|
||||
const containerHeight = this.$refs.threeContainer.clientHeight
|
||||
const rangeX = maxX - minX
|
||||
const rangeY = maxY - minY
|
||||
const scaleX = (containerWidth * 0.8) / rangeX
|
||||
const scaleY = (containerHeight * 0.8) / rangeY
|
||||
this.scaleFactor = Math.min(scaleX, scaleY)
|
||||
|
||||
// 渲染
|
||||
this.entities.forEach(entity => {
|
||||
const geometry = entity.geometry ? JSON.parse(entity.geometry) : null
|
||||
if (!geometry) return
|
||||
const color = this.getColor(entity.color)
|
||||
const material = new THREE.LineBasicMaterial({ color, linewidth: 1 })
|
||||
let mesh = null
|
||||
if (entity.entityType === 'LINE') {
|
||||
const start = this.transformPoint(geometry.start)
|
||||
const end = this.transformPoint(geometry.end)
|
||||
const points = [start, end]
|
||||
const lineGeometry = new THREE.BufferGeometry().setFromPoints(points)
|
||||
mesh = new THREE.Line(lineGeometry, material)
|
||||
} else if (entity.entityType === 'CIRCLE') {
|
||||
const center = this.transformPoint(geometry.center)
|
||||
const radius = geometry.radius * this.scaleFactor
|
||||
const circleGeometry = new THREE.CircleGeometry(radius, 64)
|
||||
mesh = new THREE.LineLoop(circleGeometry, material)
|
||||
mesh.position.set(center.x, center.y, center.z)
|
||||
}
|
||||
if (mesh) {
|
||||
mesh.userData = { entityId: entity.id, layerName: entity.layerName }
|
||||
this.scene.add(mesh)
|
||||
this.entityMeshMap[entity.id] = mesh
|
||||
}
|
||||
})
|
||||
// 居中相机
|
||||
this.camera.position.set(0, 0, Math.max(containerWidth, containerHeight))
|
||||
this.camera.lookAt(0, 0, 0)
|
||||
if (this.controls) this.controls.update()
|
||||
this.highlightEntities()
|
||||
},
|
||||
transformPoint(point) {
|
||||
return new THREE.Vector3(
|
||||
(point[0] - this.offsetX) * this.scaleFactor,
|
||||
(point[1] - this.offsetY) * this.scaleFactor,
|
||||
0
|
||||
)
|
||||
},
|
||||
highlightEntities() {
|
||||
// 只高亮当前层和选中实体
|
||||
Object.values(this.entityMeshMap).forEach(mesh => {
|
||||
mesh.material.color.set(0xcccccc)
|
||||
mesh.material.linewidth = 2
|
||||
})
|
||||
// 高亮当前层
|
||||
const currentLayer = this.layerList[this.selectedLayerIndex]
|
||||
if (currentLayer) {
|
||||
currentLayer.entities.forEach(entity => {
|
||||
const mesh = this.entityMeshMap[entity.id]
|
||||
if (mesh) mesh.material.color.set(0x409EFF)
|
||||
})
|
||||
}
|
||||
// 高亮选中实体
|
||||
if (this.selectedEntityId) {
|
||||
const mesh = this.entityMeshMap[this.selectedEntityId]
|
||||
if (mesh) mesh.material.color.set(0xff0000)
|
||||
}
|
||||
},
|
||||
transformPoint(point) {
|
||||
return new THREE.Vector3(
|
||||
(point[0] - this.offsetX) * this.scaleFactor,
|
||||
(point[1] - this.offsetY) * this.scaleFactor,
|
||||
0
|
||||
)
|
||||
},
|
||||
getColor(colorIndex) {
|
||||
const colors = {
|
||||
1: 0xff0000, 2: 0xffff00, 3: 0x00ff00, 4: 0x00ffff,
|
||||
5: 0x0000ff, 6: 0xff00ff, 7: 0xffffff, 256: 0x000000
|
||||
}
|
||||
return colors[colorIndex] || 0x00aaff
|
||||
},
|
||||
animate() {
|
||||
requestAnimationFrame(this.animate)
|
||||
if (this.controls) this.controls.update()
|
||||
if (this.renderer && this.scene && this.camera) {
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
}
|
||||
},
|
||||
onWindowResize() {
|
||||
if (this.camera && this.renderer && this.$refs.threeContainer) {
|
||||
this.camera.aspect = this.$refs.threeContainer.clientWidth / this.$refs.threeContainer.clientHeight
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.renderer.setSize(
|
||||
this.$refs.threeContainer.clientWidth,
|
||||
this.$refs.threeContainer.clientHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dxf-viewer-root {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
|
||||
}
|
||||
.dxf-sidebar {
|
||||
width: 200px;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e4e7ed;
|
||||
padding: 0 0 0 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.sidebar-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
padding: 16px 0 8px 20px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
.layer-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.layer-list li {
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
border-left: 4px solid transparent;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
.layer-list li.active {
|
||||
background: #f0f7ff;
|
||||
border-left: 4px solid #409EFF;
|
||||
color: #409EFF;
|
||||
}
|
||||
.entity-count {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.dxf-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
}
|
||||
.entity-list {
|
||||
width: 220px;
|
||||
background: #f7f9fc;
|
||||
border-right: 1px solid #e4e7ed;
|
||||
padding: 0 0 0 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.entity-title {
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
padding: 16px 0 8px 20px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
.entity-list ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.entity-list li {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-left: 4px solid transparent;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.entity-list li.selected {
|
||||
background: #fff7f7;
|
||||
border-left: 4px solid #ff4d4f;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.entity-type {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
}
|
||||
.entity-id {
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
}
|
||||
.threejs-container {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: #f0f2f5;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue