<template>
<div class="wrap">
<div id="container"></div>
<div class="toolBar">
<el-button class="btn" type="primary" @click="resetPerspective()">{{$t('resetPerspective')}}</el-button>
<el-button class="btn" type="primary" @click="targetPerspective()">{{enableCamera ? $t('lockPerspective') : $t('unlockPerspective')}}</el-button>
<el-button class="btn" type="primary" @click="strabismus()">{{$t('strabismus')}}</el-button>
</div>
</div>
</template>
<script>
import * as THREE from 'three'
import * as OrbitControls from 'three-orbitcontrols'
import * as sceneConfig from './sceneConfig.js'
import Util3D from "./Util3D"
export default {
name: 'scene3D',
props: {
roads: {
type: Object,
default(){
return {}
}
},
laneStatus: {
type: Array,
default(){
return []
}
},
channelStatus: {
type: Array,
default(){
return []
}
},
roadInfo: {
type: Array,
default(){
return []
}
},
noCheck: { // 是否可交互,用于区分实时状态与底图配置
type: Boolean,
default(){
return false
}
},
channels: {
type: Array,
default(){
return []
}
}
},
methods: {
init() {
let container = document.getElementById('container');
this.container = container;
let height = container.offsetHeight;
let width = container.offsetWidth;
let scene = new THREE.Scene();
window.SceneApp.scene = scene;
//相机
let camera = new THREE.PerspectiveCamera(45,width/height,0.01,1000);
camera.position.set(0,60,0);
camera.lookAt(scene.position);
this.camera = camera;
// 射线
this.raycaster = new THREE.Raycaster();
//光照
let spotLight = new THREE.SpotLight( 0xffffff, 1, 800 );
spotLight.position.set(-80,100,-50);
spotLight.castShadow = true; //生成阴影
spotLight.shadowMapWidth = spotLight.shadowMapHeight = 512;
scene.add(spotLight);
let ambientLight = new THREE.AmbientLight(0xffffff,0.5);
scene.add(ambientLight);
//渲染器
this.renderer = new THREE.WebGLRenderer();
this.renderer.setClearColor(0xffffff);
this.renderer.setSize(width,height);
this.renderer.shadowMap.enabled = true; //渲染器渲染阴影
this.renderer.shadowMapSoft = true; //软阴影,即边缘虚化
this.renderer.gammaInput = true;
this.renderer.gammaOutput = true;
container.appendChild(this.renderer.domElement);
//镜头控制器
let controller = new OrbitControls(camera, this.renderer.domElement);
// 距离中心点最小距离和最大距离
controller.minDistance = 10
controller.maxDistance = 60
// 视角的最大仰角和俯角
controller.maxPolarAngle = Math.PI / 2.35;
controller.enableKeys = false //禁用键盘移动
controller.rotateSpeed = 0.7
sceneConfig.decoratorCtrl(controller)
window.SceneApp.controller = controller
// 创建方向标注
Util3D.createDirTitle(scene)
const animate = () => {
this.timer = requestAnimationFrame(animate);
this.renderer.render(scene, camera);
controller.update();
}
animate()
},
// 初始化路口
initRoad(roadType) {
roadType = roadType || this.roadType
let scene = window.SceneApp.scene;
// 去重
roadType = [...new Set(roadType)].sort((a,b) => {return a-b})
// 删除原有的路口
Util3D.findAllChildrenMesh().filter((v) => {
return v.name === 'crossroad' || v.name === 'centerRoad' || v.name === 'decorations'
}).forEach((v) => {
this.dispose(v) // 销毁掉部分网格体,减少内存增长
v.parent.remove(v)
})
// 根据路口类型,填补中间处的空白
let shape = Util3D.getCrossroadShape(roadType)
scene.add(Util3D.getCenterRoad(shape, roadType))
Util3D.drawDecoration(scene, shape, roadType)
roadType.forEach((dir) => {
// 此处考虑加个计算路口位置的算法
let crossroad = Util3D.getRoadMesh();
crossroad.userData.dir = dir
if([1,2,3,4,6,7,8,9].indexOf(dir) < 0) return;
Util3D.setCrossroadPos(crossroad, dir, roadType);
scene.add(crossroad);
})
// 画车道线
this.drawLaneLine()
// 更新路名
this.updateRoadInfo()
// 切换用户状态
this.userState = 'normal';
},
// 更新车道数量
updateRoad(mesh){
mesh = mesh || Util3D.choosedMesh;
if(!mesh) return;
Util3D.findAllChildrenMesh(mesh).filter((v) => {
return v.name === 'laneLine' || v.name === "arrow";
}).forEach((v) => {
this.dispose(v)
v.parent.remove(v)
})
let crossroad = mesh;
const dir = parseInt(crossroad.userData.dir);
// 遍历路口,查询属于该路口的车道
let laneList = this.laneList.filter((lane, i) => {
lane.dir = parseInt(lane.dir)
return lane.dir === dir
}).sort((p,c) => p.seq - c.seq)
Util3D.drawLane(laneList, crossroad)
},
// 更新车道类型
updateArrowType(type, mesh){
mesh = mesh || Util3D.choosedMesh;
if(mesh) mesh.updateType(type);
if(!mesh) return;
},
// 画车道线
drawLaneLine(){
// 清空原有车道线与箭头
Util3D.findAllChildrenMesh().filter((v) => {
return v.name === 'laneLine' || v.name === 'arrow';
}).forEach((v) => {
this.dispose(v)
v.parent.remove(v)
})
Util3D.findAllChildrenMesh().filter((v) => {
return v.name === 'crossroad';
}).forEach((m, n) => {
let crossroad = m;
const dir = parseInt(crossroad.userData.dir);
// 遍历路口,查询属于该路口的车道
let laneList = this.laneList.filter((lane, i) => {
lane.dir = parseInt(lane.dir)
return lane.dir === dir
}).sort((p,c) => p.seq - c.seq)
Util3D.drawLane(laneList, crossroad)
})
},
// 画信号灯
drawLight(){
// 清空原有信号灯
Util3D.findAllChildrenMesh().filter((v) => {
return v.name === 'trafficLight';
}).forEach((v) => {
this.dispose(v)
v.parent.remove(v)
})
Util3D.findAllChildrenMesh().filter((v) => {
return v.name === 'crossroad';
}).forEach((m, n) => {
let crossroad = m;
const dir = parseInt(crossroad.userData.dir);
// 遍历路口,查询属于该路口的通道
let channels = this.channels.filter((channel, i) => {
const channelDir = this.CONST.channelDir[channel.dir] || ""
return parseInt(channelDir.split("_")[0]) === dir
}).sort((p,c) => p.seq - c.seq)
Util3D.drawLight(channels, crossroad)
})
this.userState = 'normal';
},
// 更新通道类型
updateLightType(type, mesh){
mesh = mesh || Util3D.choosedMesh;
if(mesh) mesh.updateType(type);
},
// 更新信号灯数量
updateLights(crossroad){
crossroad = crossroad || Util3D.choosedMesh;
if(!crossroad) return;
Util3D.findAllChildrenMesh(crossroad).filter((v) => {
return v.name === 'trafficLight'; // 主杆删掉也就删掉了所含的信号灯
}).forEach((v) => {
this.dispose(v)
v.parent.remove(v)
})
const dir = parseInt(crossroad.userData.dir);
// 遍历路口,查询属于该路口的通道
let channels = this.channels.filter((channel, i) => {
const channelDir = this.CONST.channelDir[channel.dir] || ""
return parseInt(channelDir.split("_")[0]) === dir
})
Util3D.drawLight(channels, crossroad)
crossroad.onSelected() // 触发一下选中操作,恢复高亮
},
// 重置视角
resetPerspective() {
window.SceneApp.controller.reset()
},
// 锁定/解锁视角
targetPerspective() {
this.enableCamera = !this.enableCamera
window.SceneApp.controller.enabled = this.enableCamera
},
// 斜视视角
strabismus() {
window.SceneApp.controller.object.position.set(0, 16, 30)
},
// 实时更新车道状态
updateStatus(list, type) {
let meshs = Util3D.findAllChildrenMesh().filter((v) => {
if(type === 'lane') return v.name === "arrow" || v.name === "zebra";
else if(type === 'channel') return v.name === "light" || v.name === "passageChannel";
else return
})
meshs.forEach((mesh) => {
let id = mesh.userData && mesh.userData.id
mesh.unHighlight()
mesh.unFlashlight()
let item = list.find((v) => v.id === id );
if(item){
switch(item.status) {
case 1: // 绿灯
mesh.highlight(0x00ff00)
break;
case 2: // 红灯
mesh.highlight(0xff0000)
break;
case 3: // 黄灯
mesh.highlight(0xffff00)
break;
case 4: // 绿闪
mesh.flashlight(0x00ff00)
break;
case 5: // 黄闪
mesh.flashlight(0xffff00)
break;
case 6: // 全红
mesh.highlight(0xff0000)
break;
case 8: // 红闪
mesh.flashlight(0xff0000)
break;
default:
break;
}
if(type === 'channel' && (item.type == 32 || item.type == 33)) {
let laneMeshs = Util3D.findAllChildrenMesh().filter((v) => {
return v.name === "arrow" && item.linkLaneIds.find(laneId => v.userData.id == laneId);
})
if(item.type == 32) { // 可变
laneMeshs.forEach(mesh => mesh.followChannelStatus(item.status))
}else if(item.type == 33) { // 潮汐
}
}
}
})
},
// 更新道路参数
updateRoadInfo(info) {
info = info || this.roadInfo
let roads = Util3D.findAllChildrenMesh().filter((v) => {
return v.name === "crossroad"
})
roads.forEach(road => {
let _road = info.find(v => v.dir === road.userData.dir)
let name = (_road && _road.roadName) || ""
Util3D.drawRoadName(road, name)
})
},
// 释放内存
dispose(root) {
root = root || window.SceneApp.scene
if(root === window.SceneApp.scene) {
if(root) {
root.traverse((node) => {
if(node.geometry && node.geometry.dispose) node.geometry.dispose() //释放形状
if(node.material && node.material.dispose) node.material.dispose() //释放材质
if(node.material && node.material.map && node.material.map.dispose) node.material.map.dispose() // 释放贴图
node.dispatchEvent({ type: 'dispose' });
node = null
});
// 删除scene下所有的物体
while (root.children.length > 0) {
root.remove(root.children[0]);
}
this.renderer.render(root, this.camera);
this.camera.dispatchEvent({ type: 'dispose' });
this.camera = null
window.SceneApp.controller.dispatchEvent({ type: 'dispose' })
window.SceneApp.controller = null
this.raycaster = null
this.container.removeChild(this.renderer.domElement)
this.container = null
this.renderer.dispose()
// this.renderer.forceContextLoss();
// this.renderer.context = null;
// this.renderer.domElement = null;
this.renderer = null
if(this.timer) {
cancelAnimationFrame(this.timer)
}
root.dispatchEvent({ type: 'dispose' })
root = null;
}
}else {
if(!(root instanceof Array)) {
root = [root]
root.forEach((parent) => {
parent.traverse((node) => {
if(node.geometry && node.geometry.dispose) node.geometry.dispose() //释放形状
if(node.material && node.material.dispose) node.material.dispose() //释放材质
if(node.material && node.material.map && node.material.map.dispose) node.material.map.dispose() // 释放贴图
node.dispatchEvent({ type: 'dispose' });
node = null
})
while (parent.children.length > 0) {
parent.remove(parent.children[0]);
}
})
}
}
}
},
data() {
// 切勿将网格体绑定到Vue中,目前为放在window下
return {
renderer: null, // 渲染器
timer: null,
userState: null, // 用户状态
container: null, // 场景容器
raycaster: null, // 射线,用于物体拾取
laneList: [],
roadType: [],
enableCamera: true, //是否锁定视角
};
},
computed: {
},
watch: {
userState(val, old) {
if(val === old || this.noCheck) return;
let container = this.container;
let offsetDom = document.querySelector('.stepContent') || container; // 偏移元素,因布局原因,容器的偏移计算需以此元素为准
switch(val) {
case 'normal': //正常状态,可选路口和箭头
Util3D.NoHighLight();
// 滑过高亮
container.onmousemove = (e) => {
if(Util3D.choosedMesh) return // 因为 onmousedown之后会再次触发onmousemove,故在此拦截
_getMouse(e, this);
Util3D.NoHighLight();
let intersects = this.raycaster.intersectObjects(Util3D.findAllChildrenMesh().filter((v) => {
return v.bPicked
}).map((v) => { return [v] }).reduce((previous, current) => {
return previous.concat(current);
}, []));
if(Util3D.tempObject && Util3D.tempObject.onCancelPicked) Util3D.tempObject.onCancelPicked()
if(intersects.length) {
let choosedMesh = intersects[0].object
if(choosedMesh.choosedTarget) choosedMesh = choosedMesh.choosedTarget
if(choosedMesh.onPicked) {
choosedMesh.onPicked()
}
Util3D.HighLight(choosedMesh)
}
}
// 点击选中
container.onmousedown = (e) => {
_getMouse(e, this);
var intersects = this.raycaster.intersectObjects(Util3D.findAllChildrenMesh().filter((v) => {
return v.bSelected;
}).map((v) => { return [v] }).reduce((previous, current) => {
return previous.concat(current);
}, []));
if(intersects.length) {
if(Util3D.choosedMesh && Util3D.choosedMesh.onCancelSelected) Util3D.choosedMesh.onCancelSelected()
let choosedMesh = intersects[0].object
if(choosedMesh.choosedTarget) choosedMesh = choosedMesh.choosedTarget
if(choosedMesh.onSelected) choosedMesh.onSelected()
Util3D.HighLight(choosedMesh)
Util3D.choosedMesh = choosedMesh
if(choosedMesh.name === "crossroad") {
this.userState = 'selRoad'
this.$emit('sel-mesh', 'selRoad', choosedMesh.userData);
} else if(choosedMesh.name === "arrow" || choosedMesh.name === "zebra"){
this.userState = 'selArrow'
this.$emit('sel-mesh', 'selArrow', choosedMesh.userData);
} else if(choosedMesh.name === "light" || choosedMesh.name === "passageChannel"){
this.userState = 'selLight'
this.$emit('sel-mesh', 'selLight', choosedMesh.userData);
}
}
}
break;
default:
container.onmousemove = null;
break;
}
// 获取鼠标坐标对象,并创建射线
function _getMouse(e, self){
var mouse = new THREE.Vector2(); //创建二维向量,保存鼠标位置
e.preventDefault(); //取消事件的默认行为,以便重新定义
//mouse的值是(鼠标与画布中心点的距离)/(网页可见部分的大小),也就是说当鼠标在网页可见部分的中心点时,mouse.x和mouse.y都为0,他们的取值范围是(-1,1),下文中 *2-1 与 *2+1是为了判断鼠标在中心点的哪一侧
mouse.x = ((e.clientX - offsetDom.offsetLeft) / container.clientWidth) * 2 - 1; // 此处需考虑滚动条位置,待修改
mouse.y = -((e.clientY - offsetDom.offsetTop) / container.clientHeight) * 2 + 1;
self.raycaster.setFromCamera(mouse, self.camera); //将当前的鼠标位置信息作为射线的起点,相机的朝向作为射线方向
}
},
roads(val, old){
if(JSON.stringify(val) === JSON.stringify(old)) return;
let roadType = []
let laneList = []
for(let roadTypeNo in val){
if([1,2,3,4,6,7,8,9].find(v => v == roadTypeNo)){ // 去除不合法方向
roadType.push(parseInt(roadTypeNo))
val[roadTypeNo].forEach((lane) => {
laneList.push(lane)
})
}
}
this.roadType = roadType
this.laneList = laneList
if(JSON.stringify(Object.keys(val)) === JSON.stringify(Object.keys(old))){ // 车道数量改变
this.updateRoad(Util3D.choosedMesh)
}else{ // 路口类型改变 此处在选择相同路口类型时,无法重置
this.initRoad(roadType)
// this.drawLight()
}
},
laneStatus(laneList){
this.updateStatus(laneList, 'lane')
},
channelStatus(channelList){
this.updateStatus(channelList, 'channel')
},
roadInfo(val, old){
this.updateRoadInfo(val)
},
channels(val, old){
if(JSON.stringify(val) === JSON.stringify(old)) return;
let newDirs = [...new Set(val.map(channel => channel.dir))]
let oldDirs = [...new Set(old.map(channel => channel.dir))]
if(JSON.stringify(newDirs) === JSON.stringify(oldDirs) &&
JSON.stringify(newDirs.map(dir => val.filter(c => c.dir == dir).length)) === JSON.stringify(oldDirs.map(dir => old.filter(c => c.dir == dir).length))) return
// 只要所有方向不变,即视为修改通道数量
// if(JSON.stringify([...new Set(val.map(channel => channel.dir))]) === JSON.stringify([...new Set(old.map(channel => channel.dir))])) {
// this.updateLights()
// } else {
this.drawLight()
// }
}
},
/*** 生命周期 ***/
created(){
window.SceneApp = {
scene: null,
controller: null,
}
// console.log('created')
},// 此阶段编译DOM模板
beforeMount(){
// console.log('beforeMount')
},// 此阶段重渲染虚拟dom
mounted() { // 模板编译完成后执行
// console.log('mounted')
this.init();
},
beforeUpdate(){ // 仅在data中的数据变化并渲染到视图时才会触发,仅改变data不触发
// console.log('beforeUpdate')
},// 此阶段重渲染虚拟dom
updated() {
// console.log('updated')
},
beforeDestroy() {
// console.log('beforeDestroy')
},// 此阶段卸载 watcher、子组件、事件***
destroyed() {
// console.log('destroyed')
this.dispose()
}
};
</script>
<style scoped lang="less">
#container, .wrap{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.toolBar{
position: absolute;
top: 15px;
left: 15px;
}
</style>