思路
可以将属性信息绑定到模型中,添加点击事件,使用Raycaster射线求交获取到最近的模型对象,结合CSS2DObject,就可将信息展示出来了,我封装了一个Popup弹框显示类,具体代码如下,后面会附上完整代码
代码
初始化场景
modelScene() {
let self=this
let containerElement = document.querySelector(".mapModel");
this.updateContainerElement(containerElement); // 计算 容器元素 高宽,左偏移值,顶偏移值
scene = new THREE.Scene();
sceneManager.switchMapping(scene); // 场景 贴图更换
stats = new Stats();
document.body.appendChild(stats.dom);
camera = new THREE.PerspectiveCamera(45, clientWidth / clientHeight, 1, 1000000);
camera.position.set(-41.84, 8.44, 114.57); //设置相机位置
camera.lookAt(new THREE.Vector3(0, 0, 0)); //设置相机方向(指向的场景对象)
global.camera=camera
// 创建一个CSS2渲染器CSS2DRenderer
labelRenderer=sceneManager.createCSS2Renderer(window.innerWidth, window.innerHeight)
const labelControls = new OrbitControls( camera, labelRenderer.domElement );
let renderer = new THREE.WebGLRenderer();
// 设置与容器元素相同大小
renderer.setSize(clientWidth, clientHeight);
containerElement.appendChild(renderer.domElement);
global.renderer=renderer
const controls = new OrbitControls(camera, renderer.domElement);
global.webglControl=controls
const light = new THREE.HemisphereLight(0xffffff, 0xcccccc, 1);
scene.add(light);
// 添加光源
const light_a = new THREE.AmbientLight();
//scene.add(light_a);
// 效果组合器
composer = new EffectComposer(renderer);
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 选择模型外边框
outlinePass = sceneManager.createOutlinePass(clientWidth,clientHeight,scene,camera)
composer.addPass(outlinePass);
const effectFXAA = new ShaderPass(FXAAShader);
effectFXAA.uniforms["resolution"].value.set(1 / window.innerWidth, 1 / window.innerHeight);
composer.addPass(effectFXAA);
// 选中高亮
raycaster = new THREE.Raycaster();
mouse = new THREE.Vector2(1, 1);
const mousePosition = {
x: 0,
y: 0
};
function onMouseMove(event) {
event.preventDefault();
mousePosition.x = event.clientX;
mousePosition.y = event.clientY;
mouse.x = ((event.clientX - offsetLeft) / clientWidth) * 2 - 1;
mouse.y = -((event.clientY - offsetTop) / clientHeight) * 2 + 1;
}
containerElement.addEventListener("mousemove", onMouseMove, false);
function onClick (event) {
event.preventDefault();
debugger
var windowX = event.clientX;//鼠标单击位置横坐标
var windowY = event.clientY;//鼠标单击位置纵坐标
const mousePoint = new THREE.Vector2();
mousePoint.x = (windowX / window.innerWidth) * 2 - 1;//标准设备横坐标
mousePoint.y = -(windowY / window.innerHeight) * 2 + 1;//标准设备纵坐标
//const rayCaster = new THREE.Raycaster();
raycaster.setFromCamera(mousePoint, camera);
//返回射线选中的对象
clickSelects= raycaster.intersectObjects(scene.children,true);
//console.log(clickSelects);
if (clickSelects.length > 0) {
let selectObj=clickSelects[0]; //射线在模型表面拾取的点坐标
let point=selectObj.point
let position=[point.x,point.y,point.z]
if(selectObj.object.popupContent||selectObj.object.parent.popupContent){
let popupContent=selectObj.object.popupContent||selectObj.object.parent.popupContent
let title=popupContent.title?popupContent.title:'提示'
let content=popupContent.content?popupContent.content:'无内容'
let popupClass=popupContent.cssClass?popupContent.cssClass:''
//popupWidget.containerNode.innerHTML='哈哈我被点击拉'
popupWidget.setLabelValue(content,title,popupClass)
popupWidget.setPosition(position)
popupWidget.show()
}
//触发选择模型 调整位置
/*if(global.selectMode){
let event = document.createEvent('Event');
event.initEvent("clickObject3D", true, true);
event.object3D = selectObj.object
document.dispatchEvent(event);
}*/
}
}
containerElement.addEventListener("click", onClick, false);
}
添加弹窗
//添加弹窗
popupWidget=new PopupWidget('暂无信息','信息',labelRenderer)
scene.add(popupWidget.CSS2Label)
// 添加动画
this.animate();
添加动画
animate() {
// TWEEN.update();
let intersection
if(clickSelects.length===0){
raycaster.setFromCamera(mouse, camera);
intersection = raycaster.intersectObjects(itemList);
}else{
intersection=clickSelects
}
if (intersection.length > 0) {
// console.log(intersection[0].object.name);
outlinePass.selectedObjects = [intersection[0].object];
} else {
outlinePass.selectedObjects = [];
}
if (this.moveType) {
scene.rotateY(0.001); //每次绕y轴旋转0.01弧度
}
composer.render();
labelRenderer.render(scene, camera);
requestAnimationFrame(this.animate);
stats.update();
// 获取相机方向
let d1=new THREE.Vector3()
camera.getWorldDirection(d1);
if(wanderControl.personModel){
if(d1){
wanderControl.personModel.lookAt(d1.x,d1.y,d1.z);
}
}
}
Popup类
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import Utils from './Utils'
/**
* @Author :TanShiJun 1826356125@qq.com
* @Date :2022/5/26
* @Describe :弹框
* Last Modified by : TSJ
* Last Modified time :2022/5/26
**/
export default class PopupWidget {
constructor (content,title,labelRenderer) {
this.domNode=null
this.labelRenderer=labelRenderer
this.containerNode=null
this.CSS2Label=this._init(content,title)
this.hide()
}
/**
* 初始化内容 默认隐藏
* @param content 内容
* @param title 标题
* @returns {CSS2DObject}
* @private
*/
_init(content,title) {
let self=this
let tempStr='<span class="closeButton" title="关闭">×</span>\n' +
' <div class="popup-content-wrapper">\n' +
' <div class="titlePane">'+title+'</div>\n' +
' <div class="contentPanel">\n'
if(Array.isArray(content)){
tempStr+=this._creatContentStr(content)
}
else if(typeof(content) === "string"||Utils.checkHtml(content)){
tempStr+=content
}
tempStr+=' </div>\n' +
' </div>\n' +
' <div class="three-popup-tip-container">\n' +
' <div class="three-popup-tip"></div>\n' +
' </div>'
this.containerNode=document.createElement('div')
this.containerNode.setAttribute('class','three-Popup');
this.containerNode.innerHTML=tempStr
const closeDom=Utils.getClass('closeButton',this.containerNode)
if(closeDom.length>0){
closeDom[0].addEventListener('click', function () {
self.hide()
})
}
let css2Obj=new CSS2DObject(this.containerNode);
css2Obj.position.set(-12.895390080777755,8.051334095702684,-3.9104457197725537)
return css2Obj;
}
show(){
this.CSS2Label.visible = true
this.labelRenderer.domElement.style.display='block'
}
hide(){
this.CSS2Label.visible = false
this.labelRenderer.domElement.style.display='none'
}
// 设置位置
setPosition(position){
this.CSS2Label.position.set(position[0],position[1]+4,position[2]);
//this.CSS2Label.position.set(-12.895390080777755,8.051334095702684,-3.9104457197725537); //标签标注在obj世界坐标
}
/**
* 设置名称 和 值 ; 设置标题
* @param object [] {
* key:'',
* value:''
* }
*/
setLabelValue(objects,title,cssClass){
let classList=this.containerNode.classList
if(cssClass){
// 获取类名,返回数组
classList.add(cssClass)
}else{
if(classList.length===2){
classList.remove(classList[1])
}
}
const contentDom=Utils.getClass('contentPanel',this.containerNode)
if(contentDom.length>0){
contentDom[0].innerHTML=this._creatContentStr(objects)
}
if(title){
const titleDom=Utils.getClass('titlePane',this.containerNode)
if(titleDom.length>0){
titleDom[0].innerHTML=title
}
}
}
/**
* 创建内容
* @param content
* @returns {string}
* @private
*/
_creatContentStr(content){
let contentStr=''
if(Array.isArray(content)){
for(let i=0;i<content.length;i++){
contentStr+='<div class="three-Popup-li">'
if(content[i].label){
contentStr+='<div class="Popup-label">' +content[i].label+'</div>'
}
if(content[i].value){
contentStr+='<div class="Popup-value">' +content[i].value+'</div>'
}
contentStr+='</div>'
}
}else{
contentStr=content
}
return contentStr
}
}
工具类
/**
* @Author :TanShiJun 1826356125@qq.com
* @Date :2022/5/26
* @Describe :工具类
* Last Modified by : TSJ
* Last Modified time :2022/5/26
**/
export default class Utils {
// 判断是否是dom节点
static isNode(o) {
return (
typeof Node === "object" ? o instanceof Node :
o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string"
)
}
static isElement(o) {
return (
typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string"
);
}
/**
* 字符串是否含有html标签的检测
* @param htmlStr
*/
static checkHtml(htmlStr) {
var reg = /<[^>]+>/g;
return reg.test(htmlStr);
}
/**
*
* 作用:根据指定的类名查找元素
*
* 参数:
* @param classname:需要查找的类名(字符串)
* @param oParent(可有可无):需要查找的元素的父级(对象),如果没
* 传入,默认为document;如果需要缩小范围,提高查找速度,可以
* 给值(建议给)
*
* 函数内局部变量:
* oChild 根据父级oParent获取到的该标签下的所有标签
* arr 存储拥有需要查找的classname的元素
*
* 步骤:
* 1.判断是否有传入oParent,如果没有传入,则赋予初始值document
* 2.获取父级oParent下的所有标签并存储到oChild中
* 3.定义空数组arr
* 4.对oChild进行循环,利用字符串函数indexOf对每个元素的类名进行查找(
* 类名可能不止一个),如果在类名中找到了传入进来的类名,便将拥有该类名
* 的元素添加到arr中
* 5.循环完毕,将arr返回
*/
static getClass(classname, oParent){
if(!oParent){
oParent = document;
}
var oChild = oParent.getElementsByTagName('*');
var arr = [];
for(var i = 0, len = oChild.length; i < len; i ++){
// indexOf,在字符串中查找指定字符,如果查找到了,返回该字符
// 在字符串中的索引;如果没有找到,返回-1
// 索引有可能为0,所以大于等于0就意味着找到
if(oChild[i].className.indexOf(classname) >= 0){
arr.push(oChild[i]);
}
}
return arr;
}
}
弹框样式
/*弹出气泡样式 开始*/
.three-Popup {
/*background-color: rgba(0, 66, 66, 0.4);*/
color: #ffffff;
font-size:18px;
/* padding:8px 12px;*/
width: 300px;
height: auto;
}
/*视频弹框*/
.three-Popup.popup-video {
width: 600px;
height: 450px;
}
.three-Popup .popup-content-wrapper {
background: linear-gradient(#00ffff, #00ffff) left top,
linear-gradient(#00ffff, #00ffff) left top,
linear-gradient(#00ffff, #00ffff) right bottom,
linear-gradient(#00ffff, #00ffff) right bottom;
background-repeat: no-repeat;
background-size: 1px 6px, 6px 1px;
background-color: rgba(0, 66, 66, 0.7);
/*text-align: center;*/
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
padding: 1px;
text-align: left;
border-radius: 3px;
}
.closeButton {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
text-align: center;
width: 20px;
height: 20px;
font: 16px/14px Tahoma, Verdana, sans-serif;
text-decoration: none;
font-weight: bold;
background: transparent;
z-index: 20170825;
cursor: pointer;
}
.three-Popup .titlePane {
height: 30px;
line-height: 30px;
font-size: 14px;
background-color: rgba(0, 0,0, 0.4);
padding-left:5px ;
}
.three-Popup .contentPanel {
margin-top: 5px;
}
.three-Popup .three-Popup-li {
height: auto;
line-height: 1;
font-size: 0;
border-top: 1px dotted #dadada;
white-space: nowrap;
padding-left: 5px;
}
.three-Popup .three-Popup-li:first-child {
border: none;
}
.three-Popup .three-Popup-li:hover {
/*cursor: pointer;
background-color: #F5F7FA;*/
}
.three-Popup .contentPanel .Popup-label {
display: inline-block;
width: 80px;
line-height: 30px;
font-size: 14px;
font-weight: 700;
vertical-align: middle;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.three-Popup .contentPanel .Popup-value {
display: inline-block;
padding-left: 5px;
font-size: 14px;
line-height: 22px;
vertical-align: middle;
width: 200px;
white-space: normal;
}
.three-popup-tip-container {
margin: 0 auto;
width: 40px;
height: 20px;
position: relative;
overflow: hidden;
}
.three-popup-tip-container .three-popup-tip {
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
/* transform: rotate(45deg); */
}
绑定模型属性格式
绑定属性可以使html片段,可以是属性信息,属性信息类似于如下格式:
{
content:[{
label:'报警信息',
value:'无异常'
},
{
label:'温度',
value:'37℃'
}
],
title:'监控点'
}
绑定属性
/**
* 加载标记点
* @param markerPointCfgs {
* name,
* floorName, 所属楼层名称
* size:0.5, 大小
* hasWave:false, 是否开启波纹
* content:[{label,value}]
* }
*/
createMarkerPoint(markerPointCfgs){
for(let i=0;i<markerPointCfgs.length;i++){
const pointConfig=markerPointCfgs[i]
// 创建的锥形标记模型,绑定显示的属性
const pyramidPoint=createPyramidPoint(pointConfig.name,pointConfig.size,pointConfig.hasWave)
// 绑定属性
if(pointConfig.popupContent){
pyramidPoint.popupContent=pointConfig.popupContent
}
if(pointConfig.position){
pyramidPoint.position.set(pointConfig.position[0],pointConfig.position[1],pointConfig.position[2])
}
const markerGroup=this.markerPoints.find((pt)=>{
return pt.name===pointConfig.floorName
})
if(markerGroup){
markerGroup.add(pyramidPoint)
//this.scene.add(markerGroup)
}
}
}
最终效果
如有不足之处,欢迎来指正,QQ 1826356125