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