案例效果截图如下:

具体案例场景和功能,详见b站视频:

https://www.bilibili.com/video/BV1Bb421E7WL/?vd_source=7d4ec9c9275b9c7d16afe9b4625f636c

案例场景逻辑代码:

<template>
<div id="whole">
<!-- threejs容器 -->
<div id="three" ref="container"></div> <!-- 搜索框 -->
<div id="search" v-if="props.itemType === '房屋数据'">
<a-input v-model:value="searchValue" placeholder="楼栋搜索" id="searchFrame" style="width: 100%; height: 4vh" @input="searchChange" />
<div id="searchContent" v-show="searchData.length > 0">
<div v-for="(val, index) in searchData" :key="index" id="searchItem" @click="viewAngleZoomIn(val)">{{ val }}</div>
</div>
</div> <!-- 建筑标记元素 -->
<div id="buildMarker" ref="buildMarker" style="display: none">
<div id="content">1幢</div>
</div> <!-- 楼栋点击弹出框 -->
<div id="popup" ref="popup" style="display: none">
<div id="head">
<div id="title">{{ popupTitle }}</div>
<div id="close" @click="popupClose"></div>
</div>
<div id="content">
<div class="common" @click="popupClick('1单元')">1单元</div>
<div class="common" @click="popupClick('2单元')">2单元</div>
<div class="common" @click="popupClick('3单元')">3单元</div>
<div class="common" @click="popupClick('4单元')">4单元</div>
</div>
</div>
</div> <!-- 楼栋单元信息弹框 -->
<infoPopFrame
:building="popupTitle"
:buildingUnit="buildingUnit"
:visible="infoPopFrameVisible"
:baseInfo="buildingBaseInfo"
:floorData="floorData"
@closePopFrame="infoPopFrameVisible = false"
></infoPopFrame>
</template>
<script lang="ts" setup>
import * as THREE from 'three';
import TWEEN from '@tweenjs/tween.js';
import { onMounted, ref, onUnmounted } from 'vue';
import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import { initBaseConfig } from './components/threeBaseConfig';
import { loadingModel } from './components/modelImport.js';
import { disposeObject } from './components/disposeObject.js';
import infoPopFrame from './components/infoPopFrame.vue'; // 父组件传值
const props = defineProps({
// 项目类型:总览/房屋数据
itemType: {
type: String,
},
}); // threejs基础配置
let scene, camera, renderer, controls, css2DRenderer;
// threejs画布容器
const container = ref();
// 弹框标题
const popupTitle = ref('');
// 弹框元素
const popup = ref();
// 2D弹框
let cSS2DPopup;
// 建筑标记元素
const buildMarker = ref();
// 搜索框输入值
const searchValue = ref('');
// 小区建筑模型
let model = null;
// 搜索框检索到的数据
const searchData = ref([]);
// 建筑名称数据,用于匹配搜索框的值searchValue
const buildNameData = [];
// 建筑单元
const buildingUnit = ref('');
// 建筑单元基本信息
const buildingBaseInfo = ref([
{ name: '产权人', value: '于晓敏' },
{ name: '商铺', value: 2 },
{ name: '自住房间', value: 3 },
{ name: '租住房间', value: 12 },
{ name: '常驻人口', value: 4 },
{ name: '流动人口', value: 8 },
]);
// 建筑信息弹框显示
const infoPopFrameVisible = ref(false);
// 建筑楼层数据
const floorData = ref([
{
name: '一楼',
houseNumArr: [
{ num: '1-101', type: '商铺' },
{ num: '1-102', type: '商铺' },
{ num: '1-103', type: '商铺' },
{ num: '1-104', type: '商铺' },
],
},
{
name: '二楼',
houseNumArr: [
{ num: '2-101', type: '租住' },
{ num: '2-102', type: '租住' },
{ num: '2-103', type: '租住' },
{ num: '2-104', type: '租住' },
],
},
{
name: '三楼',
houseNumArr: [
{ num: '3-101', type: '租住' },
{ num: '3-102', type: '租住' },
{ num: '3-103', type: '租住' },
{ num: '3-104', type: '租住' },
],
},
{
name: '四楼',
houseNumArr: [
{ num: '4-101', type: '自住' },
{ num: '4-102', type: '自住' },
{ num: '4-103', type: '自住' },
{ num: '4-104', type: '自住' },
],
},
{
name: '五楼',
houseNumArr: [
{ num: '5-101', type: '自住' },
{ num: '5-102', type: '自住' },
{ num: '5-103', type: '自住' },
{ num: '5-104', type: '自住' },
],
},
]);
// 组件卸载时清除场景scene中的所有内容,释放资源
onUnmounted(() => {
disposeObject(scene);
}); // 组件挂载完成,进行初始化
onMounted(async () => {
// 初始化基础配置:场景、相机、渲染器等
const baseConfig = initBaseConfig();
scene = baseConfig.scene;
camera = baseConfig.camera;
renderer = baseConfig.renderer;
controls = baseConfig.controls;
css2DRenderer = baseConfig.css2DRenderer; // 渲染器dom挂在threejs容器中
container.value.appendChild(renderer.domElement);
container.value.appendChild(css2DRenderer.domElement); // 加载3D模型
model = await loadingModel();
scene.add(model); // 初始化css2D弹框
initPopup();
// 添加鼠标移动事件
addMouseMoveEvent();
// 添加鼠标点击事件
addMouseClickEvent();
// 添加建筑的标记
addBuildMarker(); if (props.itemType === '房屋数据') {
// 获取建筑名称数据,用以搜索框检索
model.getObjectByName('建筑').traverse((item) => {
if (item.isMesh && item.name && item.name.includes('幢')) {
if (buildNameData.includes(item.name)) return;
buildNameData.push(item.name);
}
});
} // 开始循环渲染
render();
}); // 循环渲染
const render = () => {
requestAnimationFrame(render);
TWEEN.update();
controls.update();
css2DRenderer.render(scene, camera);
renderer.render(scene, camera);
}; // 射线检测
const rayTest = (e) => {
const px = e.offsetX;
const py = e.offsetY;
// 屏幕坐标转为标准设备坐标
const x = (px / window.innerWidth) * 2 - 1;
const y = -(py / (window.innerHeight - 36 - 56)) * 2 + 1;
// 创建射线
const raycaster = new THREE.Raycaster();
// 设置射线参数
raycaster.setFromCamera(new THREE.Vector2(x, y), camera);
// 射线交叉计算拾取模型
let intersects = raycaster.intersectObjects(model.getObjectByName('建筑').children);
return intersects;
}; // 鼠标移动事件,释放射线进行检测建筑模型,改变检测到的建筑模型的颜色
const moveEvent = (e) => {
const intersects = rayTest(e);
// 所有建筑模型发射光emissive重置黑色
model.getObjectByName('建筑').traverse((item) => {
if (item.isMesh) {
item.material.emissive = new THREE.Color('#000');
}
});
// 检测结果存在时
if (intersects[0]) {
// 改变鼠标样式为手指
document.body.style.cursor = 'pointer';
// 当前检测建筑模型
const currentBuildModel = intersects[0].object; // 定义材质颜色
currentBuildModel.material.emissive = new THREE.Color('#00BFFF');
} else {
// 恢复默认鼠标样式
document.body.style.cursor = 'default';
}
}; // 鼠标点击事件,释放射线进行检测建筑模型
const clickEvent = (e) => {
const intersects = rayTest(e);
// 检测结果存在时
if (intersects[0]) {
// 过滤掉其他建筑
if (intersects[0].object.name.includes('其他')) return; if (intersects[0].object.name.includes('配电')) {
popupTitle.value = '配电';
} else {
popupTitle.value = intersects[0].object.name;
} model.getObjectByName('建筑').traverse((item) => {
if (item.isMesh) {
item.material.color = item.color;
}
});
intersects[0].object.material.color = new THREE.Color('#00C5CD'); cSS2DPopup.visible = true;
controls.update();
cSS2DPopup.position.copy(controls.target);
}
}; // 初始css2D弹框,将弹框元素转换成threejs中的css2D对象
function initPopup() {
popup.value.style.display = 'block';
cSS2DPopup = new CSS2DObject(popup.value);
cSS2DPopup.renderOrder = 99;
cSS2DPopup.visible = false;
cSS2DPopup.position.set(0, 0, 0);
scene.add(cSS2DPopup);
} // 添加鼠标移动事件
function addMouseMoveEvent() {
// 节流函数
const throttleChange = throttle(moveEvent, 10);
// 监听鼠标移动事件
container.value.addEventListener('mousemove', (e) => {
throttleChange(e);
});
} // 节流函数,鼠标移动事件触发太过频繁需要节制触发次数
function throttle(func, limit) {
let inThrottle;
return function () {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
} // 添加鼠标点击事件
function addMouseClickEvent() {
// 监听鼠标点击事件
container.value.addEventListener('click', (e) => {
clickEvent(e);
});
} // 视角拉近
function viewAngleZoomIn(val) {
cSS2DPopup.visible = false;
// 当前目标建筑模型
const target = model.getObjectByName(val);
// 重置所有建筑模型颜色
model.getObjectByName('建筑').traverse((item) => {
if (item.isMesh) {
item.material.color = item.color;
}
});
// 设置建筑模型颜色
target.material.color = new THREE.Color('#00C5CD'); // 目标位置
const targetPos = target.getWorldPosition(new THREE.Vector3());
// 移动位置
const movePos = targetPos.clone();
movePos.y += 80;
movePos.z += 55;
// 开始位置
const startPos = camera.position.clone();
// 初始的控件目标
const initialTarget = controls.target.clone(); new TWEEN.Tween({ t: 0 })
.to({ t: 1 }, 1500)
.easing(TWEEN.Easing.Sinusoidal.InOut)
.onUpdate(function (e) {
const t = e.t;
camera.position.lerpVectors(startPos, movePos, t);
controls.target.lerpVectors(initialTarget, targetPos, t);
camera.updateProjectionMatrix();
controls.update();
})
.onComplete(function () {
cSS2DPopup.visible = true;
popupTitle.value = val;
controls.update();
cSS2DPopup.position.copy(controls.target);
})
.start();
} // 添加建筑标记
function addBuildMarker() {
model.getObjectByName('建筑').traverse((item) => {
if (item.name.includes('其他')) return;
if (item.isMesh) {
let closeDom; if (item.name.includes('配电')) {
closeDom = buildMarker.value.cloneNode(true);
closeDom.style.width = '2vw';
closeDom.children[0].innerHTML = '配电';
} else {
closeDom = buildMarker.value.cloneNode(true);
closeDom.style.width = `${item.name.length * 0.7}vw`;
closeDom.children[0].innerHTML = item.name;
} const cSS2DObject = new CSS2DObject(closeDom);
const pos = item.getWorldPosition(new THREE.Vector3());
cSS2DObject.position.copy(pos);
cSS2DObject.position.y += 5;
cSS2DObject.name = item.name + '标记';
scene.add(cSS2DObject);
}
});
} // 搜索框内容变化事件,模糊匹配建筑名称数据
function searchChange(e) {
if (!e) {
searchData.value = [];
return;
}
// 匹配结果
const rel = buildNameData.filter((item) => item.includes(e));
// 对匹配结果进行排序
rel.sort((a, b) => {
// 提取数值
const getNumber = (str) => parseInt(str.match(/\d+/)[0]);
// 检测名称中是否带有别墅
const isVilla = (str) => str.includes('别墅'); if (isVilla(a) && isVilla(b)) {
return getNumber(a) - getNumber(b);
} else if (isVilla(a)) {
return 1;
} else if (isVilla(b)) {
return -1;
} else {
return getNumber(a) - getNumber(b);
}
}); searchData.value = rel;
} // 弹框关闭事件
function popupClose() {
cSS2DPopup.visible = false;
model.getObjectByName('建筑').traverse((item) => {
if (item.isMesh) {
item.material.color = item.color;
}
});
} // 弹框点击事件
function popupClick(e) {
buildingUnit.value = e;
infoPopFrameVisible.value = true;
}
</script> <style lang="less" scoped>
body {
font-size: 0.7vw;
}
::v-deep .arco-card-body {
padding: 0px !important;
width: 100%;
height: 100%;
} ::v-deep .arco-input-wrapper {
border-radius: 10px;
background: #000;
border: 1px solid #009acd;
color: #fafafa;
} ::v-deep .arco-input-wrapper .arco-input.arco-input-size-medium {
font-size: 0.7vw !important;
} /* 当视口宽度小于 1400 像素时,设置最小字体大小 */
@media (max-width: 1400px) {
#buildMarker {
font-size: 12px !important;
width: 55px !important;
height: 20px !important;
}
}
#whole {
width: 100%;
height: calc(100% - 36px - 56px); #three {
width: 100%;
height: 100%;
} #search {
z-index: 999;
position: absolute;
width: 22vw;
right: 1%;
top: 3%; #searchContent {
border-radius: 4px;
margin-top: 4px;
width: 100%;
max-height: 300px;
border: 1px solid #0e2346;
background: rgba(0, 0, 0, 0.7); #searchItem {
text-indent: 1em;
line-height: 2.5vh;
font-size: 0.7vw;
width: 100%;
height: 2.5vh;
color: #eee;
border-bottom: 1px solid #12485a;
} #searchItem:hover {
cursor: pointer;
background: #0e2346;
}
} #searchContent {
overflow-y: auto;
}
#searchContent::-webkit-scrollbar {
width: 4px;
}
#searchContent::-webkit-scrollbar-thumb {
border-radius: 10px;
background: rgba(30, 150, 200, 0.7);
}
#searchContent::-webkit-scrollbar-track {
border-radius: 0;
background: rgba(0, 0, 0, 0.1);
}
} #buildMarker {
z-index: 997;
position: absolute;
top: 0;
left: 0;
font-size: 0.6vw;
height: 2.5vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center; #content {
width: 100%;
height: 100%;
background: #0e2346;
border: 1px solid #6298a9;
display: flex;
align-items: center;
justify-content: center;
color: #fafafa; #mapTag_value {
color: #ffd700;
}
}
} #popup {
z-index: 999;
position: absolute;
top: 0;
left: 0;
width: 20vw;
height: 15vh;
background: rgba(15, 41, 77, 0.85);
border-radius: 0.3vw;
border: 1px solid rgba(10, 109, 155, 0.95); #head {
width: 95%;
margin-left: 2.5%;
height: 30%;
border-bottom: 1px solid #009acd;
display: flex;
align-items: center;
justify-content: space-between; #title {
font-size: 0.85vw;
color: #bbffff;
margin-left: 2.5%;
} #close {
pointer-events: all;
width: 1vw;
height: 1vw;
background: url('../../assets/close.png') no-repeat;
background-size: 100% 100%;
} #close:hover {
cursor: pointer;
}
} #content {
width: 95%;
margin-left: 2.5%;
height: 70%;
display: flex;
justify-content: space-evenly;
align-items: center; .common {
font-size: 0.7vw;
display: flex;
justify-content: center;
align-items: center;
pointer-events: all;
width: 18%;
height: 3vh;
border: 1px solid #1f81a1;
color: #fafafa;
} .common:hover {
cursor: pointer;
color: #bbffff;
border: 1px solid #03c0ff;
}
}
}
}
</style>

three.js+vue智慧社区web3d数字孪生三维地图的更多相关文章

  1. 使用three.js(webgl)搭建智慧楼宇、设备检测、数字孪生——第十三课

    老子云:有道无术,术尚可求,有术无道,止于术. 咱开篇引用老子的话术,也没其它意思,只是最近学习中忽有感悟,索性就写了上来. 这句话用现代辩证思维理解,这里的"道" 大抵是指方法论 ...

  2. 如何用three.js实现数字孪生、3D工厂、3D工业园区、智慧制造、智慧工业、智慧工厂-第十课

    文章前,先聊点啥吧. 最近元宇宙炒的挺火热,在所有人都争相定义元宇宙的时候,资本就开始着手入场了.当定义明确,全民皆懂之后,风口也就过去了. 前两天看到新闻,新世界CEO宣布购入最大的数字地块,这块虚 ...

  3. 如何使用webgl(three.js)实现3D消防、3D建筑消防大楼、消防数字孪生、消防可视化解决方案——第十八课(一)

    序: 又是很久没出随笔文章了,一篇文章有时候整理一天,实在是抽不出来时间. 最近在回顾几年前的项目时,发现这个智慧三维消防可视化项目很有回顾价值,索性就拿出来讲讲. 首先,我们要知道消防里的知识,不是 ...

  4. JS框架_(Vue.js)带有星期日期的数字时钟

    百度云盘 传送门 密码:tv1v 数字时钟效果: <!doctype html> <html> <head> <meta charset="utf- ...

  5. Web GIS 航拍实现的智慧园区数字孪生应用

    前言 随着智慧城市建设的不断发展,智慧园区作为智慧城市的先行区,其覆盖区域越来越大,产值越来越集中,对于园区数字化建设和智能化管理的诉求也愈加强烈.园区数字化管理是以实现园区多维度业务数据汇聚.融合. ...

  6. 【开源项目】合肥~超经典智慧城市CIM/BIM数字孪生可视化项目—开源工程及源码

    最新消息,数字孪生智慧宁波开源了其数据工程源码和工程,免费送出供大家学习.使用.分享. ​ 智慧宁波实现了一系列全面的功能,如实现长三角经济圈特效.智慧地铁特效.智慧灯杆特性等.这些项目利用数字孪生技 ...

  7. 图扑软件 3D 组态编辑器,低代码零代码构建数字孪生工厂

    行业背景 随着中国制造 2025 计划的提出,新一轮的工业改革拉开序幕.大数据积累的指数级增长为智能商业爆发奠定了良好的基础,传统制造业高污染.高能耗.低效率的生产模式已不符合现代工业要求. 图扑拖拽 ...

  8. PAA房产智慧社区:解决社区管理服务的痛点难点

    社区,是社交与生活的舞台,更是家的延伸.社区之所有能够有所创新发展,得益于借助数字化和智能化.智能化给社区带来的便利体现在社区门禁可以人脸识别:AI的摄像头可以自动捕获异常的现象,便于社区管理员第一时 ...

  9. Azure Digital Twins(2)- 在本地使用ADT Explorer 管理数字孪生

    本文介绍: 在本地运行ADT Explorer 并连接Azure Digital Twins 实例: 使用 VS CODE DTDL插件开发第一个 模型文件: ADT Explorer的几个基本功能: ...

  10. 数字孪生 3D 科技馆的科学传播新模式

    前言 科技馆是一种参与型体验型的博物馆,以传播科学知识.培养公众的科学创新技术为宗旨,并以其生动的展现方式得到公众的广泛欢迎.一直以来,我国科技馆的发展受到各种因素的制约和影响,发展缓慢.如今在我国经 ...

随机推荐

  1. Leetcode: 586. Customer Placing the Largest Number of Orders

    题目要求如下: 给出的例子如下: 简单地说就是要找出表中订单最多客户的ID. 使用如下的代码进行实现: import pandas as pd def largest_orders(orders: p ...

  2. 高级工程师面试大全- spring篇

    1.spring是什么 Spring是一个轻量级的IoC和AOP容器框架.是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求.主要包括以下七 ...

  3. SMU 2024 spring 天梯赛2

    SMU 2024 spring 天梯赛2 7-1 计算指数 - SMU 2024 spring 天梯赛2 (pintia.cn) #include <bits/stdc++.h> usin ...

  4. 【粉丝问答19】Linux内核中为啥变量没初始化就用了?你确定了解宏定义?

    @ 目录 一.问题 二.分析 三.宏定义的注意点 1. 只占用编译时间 2. 宏替换发生时机 3. 预处理包括哪些工作 四.如何快速展开复杂的宏定义? 第一步 第二步 五.练习 六.15个经典宏定义小 ...

  5. 联想小新Air14使用傲梅分区助手进行硬盘克隆出现的问题,克隆完显示RAW格式解决方案,win10家庭版硬盘BitLocker上锁解锁方法

    联想小新Air14使用傲梅分区助手进行硬盘克隆出现的问题,克隆完显示RAW格式解决方案 买电脑时没考虑到512会不够用,也没注意到小新Air14是单插槽的,所以有了今天的故事. 本文会就自己的经历,提 ...

  6. 将微信小程序的代码上传到github

    在微信小程序端上传的时候会报错,昨天整了一晚上,没有解决.今天偶然发现了解决方案,下面分为两种情况来说. 一.未生成git仓库 将一号区域的代码粘贴到微信小程序的终端即可 二.已生成了git仓库 将二 ...

  7. AutoMaper使用

    使用 AutoMapper 进行赋值 一. 什么是 AutoMapper AutoMapper是对象到对象的映射工具.在完成映射规则之后,AutoMapper可以将源对象转换为目标对象. 二. Aut ...

  8. 自动调用关闭释放资源try-with-resources

    try-with-resources自动执行释放资源 看到了try这个关键字立马就应该能想到异常处理机制try-catch-finally语句块.这里要说的东西和异常处理背后的机制其实几乎是一样的,只 ...

  9. SpringBoot 引入第三方 jar

    SpringBoot 引入第三方 jar 项目结构 -BCJS |--lib |--hsm-talos-1.0.1.jar |--src |--pom.xml step1 : 配置第三方 jar 为依 ...

  10. Java实现英语作文单词扫盲程序

    来自背英语四级单词的突发奇想: 是否可以通过Java语言实现一个随机抽取作文中单词进行复习的程序. 首先,展示下成果: 点击查看代码 package Demo; import java.util.Ar ...