You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
maotu-webtopo/src/components/vue-xq-test/vue-three-groundFloor.vue

907 lines
24 KiB
Vue

1 month ago
<template>
<div class="scene-wrapper">
<div ref="containerRef" class="container" />
<!-- 固定位置的控制按钮 -->
<div class="control-buttons">
<el-button type="primary" @click="displayModel('area_oneFloor')" :icon="Bell" />
<el-button type="warning" @click="displayModel('smokeDetector')" :icon="Compass" />
<el-button type="warning" @click="displayModel('acoustoOptic')" :icon="MagicStick" />
<el-button type="warning" @click="displayModel('manual')" :icon="Star" />
<!-- 1:左右 2:上下 3:前后 -->
<el-button type="warning" @click="displayModel('refresh')" :icon="Refresh" />
<el-button
type="warning"
@click="meshControllerArr[0].controller.startAlarm()"
:icon="Refresh"
/>
<el-button
type="warning"
@click="meshControllerArr[0].controller.restore()"
:icon="Refresh"
/>
</div>
<!-- <div class="control-panel">
<el-card class="card" :body-style="{ padding: '10px' }">
<h2 class="cardHead">单个装置基本信息</h2>
<div class="cardBody">
<el-row :gutter="0" class="info-row">
<el-col :span="8">厂家</el-col>
<el-col :span="16">
<el-tag type="primary">海湾公司</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">投运日期</el-col>
<el-col :span="16">
<el-tag type="primary">2014-08-08</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">电源电压</el-col>
<el-col :span="16">
<el-tag type="primary">220V AC</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">布设位置</el-col>
<el-col :span="16">
<el-tag type="primary">35kV高压室</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">供电电压</el-col>
<el-col :span="16">
<el-tag type="primary">18-30V DC</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">输出电流</el-col>
<el-col :span="16">
<el-tag type="primary">5A</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">IP等级</el-col>
<el-col :span="16">
<el-tag type="primary">IP30</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">相对湿度</el-col>
<el-col :span="16">
<el-tag type="primary">5% ~ 95%</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">接线方式</el-col>
<el-col :span="16">
<el-tag type="primary">两线制(L+L-)</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">尺寸</el-col>
<el-col :span="16">
<el-tag type="primary">350mm x 225mm x 125mm</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">报警灵敏度</el-col>
<el-col :span="16">
<el-tag type="primary">0.005% 20%obs/m</el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="info-row">
<el-col :span="8">运行条件</el-col>
<el-col :span="16">
<el-tag ref="conditionTagRef" type="primary"> 探测器环境(-5°C至45°C) </el-tag>
</el-col>
</el-row>
<el-row :gutter="0" class="equipment-row">
<el-col :span="24">
<div class="equipment-wrapper">
<vueThreeEquipment class="equipment-component" />
</div>
</el-col>
</el-row>
</div>
</el-card>
</div> -->
</div>
</template>
<script lang="ts" setup>
import {
Check,
Delete,
Edit,
Message,
Search,
Star,
Bell,
MagicStick,
Refresh,
Compass
} from '@element-plus/icons-vue';
import { onMounted, onUnmounted, ref, nextTick } from 'vue';
import {
AxesHelper,
Color,
PerspectiveCamera,
Scene,
WebGLRenderer,
AmbientLight,
DirectionalLight,
PointLight,
MeshStandardMaterial,
Vector2,
Raycaster,
CanvasTexture,
SpriteMaterial,
Sprite,
Box3,
Vector3,
HemisphereLight,
Mesh,
Group,
BoxHelper,
BoxGeometry,
Plane,
ConeGeometry,
SphereGeometry,
MeshBasicMaterial,
TextureLoader
} from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import gsap from 'gsap';
import vueThreeEquipment from '@/components/three-components/vue-three-equipment.vue';
const containerRef = ref<HTMLDivElement>();
const conditionTagRef = ref<any>();
const showTooltip = ref(false);
let drawer = true;
// 场景
const scene = new Scene();
// 创建摄像机
const camera = new PerspectiveCamera(
45,
containerRef.value?.clientWidth! / containerRef.value?.clientHeight!,
0.1,
1000
);
camera.position.set(0, 14, 13.5);
camera.lookAt(0, 0, 0);
const axesHelper = new AxesHelper(100);
scene.add(axesHelper);
// 渲染器
const renderer = new WebGLRenderer({
antialias: true, // 启用抗锯齿
alpha: true, // 启用透明背景
preserveDrawingBuffer: true // 保持绘图缓冲区
});
renderer.setClearColor(new Color('#131519'));
renderer.setSize(containerRef.value?.clientWidth!, containerRef.value?.clientHeight!);
renderer.shadowMap.enabled = true;
renderer.setPixelRatio(window.devicePixelRatio || 1); // 设置像素比率
// 轨道摄像机
const orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.autoRotate = false;
orbitControls.autoRotateSpeed = 5;
orbitControls.dampingFactor = 0.1;
orbitControls.enableDamping = true;
orbitControls.enableRotate = true;
orbitControls.minDistance = 10;
orbitControls.maxPolarAngle = Math.PI * 0.5;
// 物体
scene.add(new AxesHelper(20));
// 环境光:增强整体亮度
const ambientLight = new AmbientLight('#ffffff', 1.8); // 强度从 0.6 提高到 1.0
scene.add(ambientLight);
// 平行光
const directionalLight = new DirectionalLight('#ffffff', 1.8); // 强度从 0.2 提高到 0.8
scene.add(directionalLight);
directionalLight.position.set(20, 20, 10);
// // 点光源
const pointLight = new PointLight('#ffffff', 1.8, 1000); // 强度从 0.5 提高到 1.0
scene.add(pointLight);
pointLight.position.set(0, 40, 0);
// 创建一个悬浮的框
// const ctx = canvas.getContext('2d');
// if (ctx) {
// ctx.fillStyle = 'rgba(0,0,0,.7)';
// ctx.fillRect(0, 0, 200, 100);
// ctx.fillStyle = 'white';
// ctx.font = '21px 黑体';
// ctx.textAlign = 'center';
// ctx.textBaseline = 'middle';
// }
// const scaleFactor = 5;
// const texture = new CanvasTexture(canvas);
// const material = new SpriteMaterial({ map: texture });
// const sprite1 = new Sprite(material);
// sprite1.scale.set(scaleFactor, scaleFactor, scaleFactor);
// sprite1.position.set(-9.644502679789724, 5.815449539931501 + 0.5, 21.66345220492572);
//区域集合
let areaOneFloorArr: Mesh[] = [];
//光感报警器
let acoustoOpticArr: Mesh[] = [];
//手动报警器
let manualArr: Mesh[] = [];
// 烟雾探测器
let smokeDetectorArr: Mesh[] = [];
// 移动目标的集合
let moveArr: Mesh[] = [];
interface meshController {
meshName: string;
targetMesh: Group;
geometry: Mesh;
controller: any;
}
let meshControllerArr: meshController[] = [];
const loader = new GLTFLoader();
const textureLoader = new TextureLoader();
//alpha贴图
// F:\vue\workspace\maotu-webtopo\public\models\texture\alpha.png
const material = new MeshBasicMaterial({ color: 0x409eff, transparent: true, opacity: 0.3 });
const alphaTexture = textureLoader.load('/public/models/texture/alpha.png');
const alphaMaterial = new MeshBasicMaterial({
color: 0x409eff,
alphaMap: alphaTexture, //alpha贴图
transparent: true // 允许透明
});
// const dracoLoader = new DRACOLoader();
// loader.setDRACOLoader(dracoLoader);
//public\models\groundFloor\ground_floor.gltf
loader.load('/models/groundFloor/ground_floor06.gltf', (gltf: GLTF) => {
gltf.scene.children.forEach((child: Mesh) => {
// 区域
if (child.name.includes('area_oneFloor')) {
const duckGeometry = child.geometry;
duckGeometry.computeBoundingBox();
duckGeometry.center();
const duckBox = duckGeometry.boundingBox.clone();
child.updateWorldMatrix(true, true);
duckBox.applyMatrix4(child.matrixWorld);
const center = duckBox.getCenter(new Vector3());
const duckBoxHelper = new BoxHelper(child, 0xf0f0f0);
// 设置包围盒材质的透明度
(duckBoxHelper.material as any).transparent = true;
(duckBoxHelper.material as any).opacity = 0.38; // 50%透明度
scene.add(duckBoxHelper);
// 获取child对象的世界坐标
// 添加精灵
const worldPosition = new Vector3();
child.getWorldPosition(worldPosition);
// 创建文字精灵
const canvas = document.createElement('canvas');
// 提高Canvas分辨率以获得更清晰的文字
const scale = window.devicePixelRatio || 1;
canvas.width = 512 * scale;
canvas.height = 256 * scale;
canvas.style.width = '512px';
canvas.style.height = '256px';
const ctx = canvas.getContext('2d');
if (ctx) {
// 缩放上下文以匹配设备像素比率
ctx.scale(scale, scale);
// 设置文字样式
ctx.fillStyle = 'rgba(0, 0, 0, 0)';
ctx.fillRect(0, 0, 512, 256);
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 使用更大的字体和更清晰的字体族
ctx.font = '24px 微软雅黑, Arial, sans-serif';
ctx.fillText(getAreaOneFloorGetName(child.name), 256, 128);
}
// 创建精灵材质和对象
const texture = new CanvasTexture(canvas);
const spriteMaterial = new SpriteMaterial({ map: texture });
const sprite = new Sprite(spriteMaterial);
// 设置精灵大小和位置
sprite.scale.set(4, 2, 1); // 调整大小以适配新的Canvas尺寸
sprite.position.copy(worldPosition);
sprite.position.y += 1.5; // 稍微抬高一点
scene.add(sprite);
sprite.visible = false;
duckBoxHelper.visible = false;
child.visible = false;
areaOneFloorArr.push(sprite); // 将精灵也加入数组
areaOneFloorArr.push(duckBoxHelper);
areaOneFloorArr.push(child);
moveArr.push(child);
} else if (child.name.includes('acoustoOptic')) {
child.visible = false;
let acoustoPosition = child.position;
const geometry = new SphereGeometry(1, 13, 8);
const sphere = new Mesh(geometry, material);
sphere.position.set(acoustoPosition.x, acoustoPosition.y, acoustoPosition.z);
scene.add(sphere);
sphere.visible = false;
acoustoOpticArr.push(sphere);
acoustoOpticArr.push(child);
meshControllerArr.push({
meshName: child.name,
targetMesh: child,
geometry: sphere,
controller: setupAlarmEffect(sphere)
});
moveArr.push(child);
} else if (child.name.includes('manual')) {
let manualPosition = child.position;
const geometry = new SphereGeometry(1, 28, 14);
const sphere = new Mesh(geometry, material);
sphere.position.set(manualPosition.x, manualPosition.y, manualPosition.z);
scene.add(sphere);
child.visible = false;
sphere.visible = false;
manualArr.push(sphere);
manualArr.push(child);
meshControllerArr.push({
meshName: child.name,
targetMesh: child,
geometry: sphere,
controller: setupAlarmEffect(sphere)
});
moveArr.push(child);
} else if (child.name.includes('smokeDetecto')) {
// 烟感报警器
child.visible = false;
let smokePosition = child.position;
const geometry = new ConeGeometry(1.8, 1.8, 16, 1, true);
const cone = new Mesh(geometry, alphaMaterial);
cone.position.set(smokePosition.x, smokePosition.y - 0.8, smokePosition.z + 0.028);
scene.add(cone);
cone.visible = false;
smokeDetectorArr.push(cone);
smokeDetectorArr.push(child);
meshControllerArr.push({
meshName: child.name,
targetMesh: child,
geometry: cone,
controller: setupAlarmEffect(cone)
});
moveArr.push(child);
}
});
scene.add(gltf.scene);
});
console.log('xxx:', meshControllerArr);
// 安全的报警效果设置函数
function setupAlarmEffect(object: Mesh) {
// 安全检查 material 是否存在
if (!object.material) {
console.warn('对象没有材质属性:', object);
return;
}
// 保存原始材质
const originalMaterial = object.material.clone();
const alarmMaterial = object.material.clone();
alarmMaterial.color.set(0xe6a23c);
const faultMaterial = object.material.clone();
alarmMaterial.color.set(0xf56c6c);
let isAlarming = false;
let isMalfunctioning = false;
let alarmInterval = null;
// 报警控制函数 - 渐变闪烁版本
function startAlarm() {
if (isAlarming) return;
isAlarming = true;
object.material = alarmMaterial;
// 保存原始颜色
// const originalColor = object.material.color.clone();
const tl = gsap.timeline({ repeat: -1, yoyo: true });
// 只在黄色和透明之间切换,不显示原始颜色
tl.to(object.material, {
opacity: 0.7, // 完全不透明的黄色
duration: 0.5,
ease: 'sine.inOut'
}).to(object.material, {
opacity: 0.2, // 半透明的黄色
duration: 0.5,
ease: 'sine.inOut'
});
// object._alarmTimeline = tl;
// object._originalColor = originalColor; // 保存原始颜色用于恢复
}
//故障
function startMalfunction() {
if (isMalfunctioning) return;
isMalfunctioning = true;
object.material = faultMaterial;
// 保存原始颜色
// const originalColor = object.material.color.clone();
const tl = gsap.timeline({ repeat: -1, yoyo: true });
// 只在黄色和透明之间切换,不显示原始颜色
tl.to(object.material, {
opacity: 0.7, // 完全不透明的黄色
duration: 0.5,
ease: 'sine.inOut'
}).to(object.material, {
opacity: 0.2, // 半透明的黄色
duration: 0.5,
ease: 'sine.inOut'
});
// object._alarmTimeline = tl;
// object._originalColor = originalColor; // 保存原始颜色用于恢复
}
function restore() {
if (!isAlarming) return;
isAlarming = false;
if (alarmInterval) {
clearInterval(alarmInterval);
alarmInterval = null;
}
// 恢复原始状态
object.visible = true;
object.material = originalMaterial;
}
// 将控制函数附加到对象上,方便外部调用
object.startAlarm = startAlarm;
object.restore = restore;
object.startMalfunction = startMalfunction;
// 返回对象本身,支持链式调用
return object;
}
function getAreaOneFloorGetName(areaOneFloorName: string): string {
switch (areaOneFloorName) {
case 'area_oneFloor_35kV':
return '35kV高压室';
case 'area_oneFloor_220kV':
return '220kV维电器室';
case 'area_oneFloor_14':
return '备品备件间';
case 'area_oneFloor_3':
return '站用电柜室';
case 'area_oneFloor_13':
return '110kV电镜间(一)';
case 'area_oneFloor_4':
return '1站用变室';
case 'area_oneFloor_6':
return '消弧线圈1室';
case 'area_oneFloor_7':
return '消弧线圈2室';
case 'area_oneFloor_8':
return '消弧线圈3室';
case 'area_oneFloor_9':
return '2站用变室';
case 'area_oneFloor_10':
return '110kV电镜间(二)';
case 'area_oneFloor_12':
return '工具间';
case 'area_oneFloor_11':
return '消弧线圈4室';
default:
return '未知';
}
}
function animate() {
requestAnimationFrame(animate);
orbitControls.update();
renderer.render(scene, camera);
}
// ResizeObserver 监听容器尺寸变化
let resizeObserver: ResizeObserver | null = null;
//补间动画
const timeline1 = gsap.timeline();
const timeline2 = gsap.timeline();
//相机移动
function tranlate(position: Vector3, target: Vector3) {
timeline1.to(camera.position, {
x: position.x,
y: position.y,
z: position.z,
duration: 1,
ease: 'power2.inOut'
});
timeline2.to(orbitControls.target, {
x: target.x,
y: target.y,
z: target.z,
duration: 1,
ease: 'power2.inOut'
});
}
function displayModel(modelName: string) {
tranlate(new Vector3(0, 14, 13.5), new Vector3(0, 0, 0));
if (modelName === 'area_oneFloor') {
areaOneFloorArr.forEach((obj) => {
obj.visible = true;
});
acoustoOpticArr.forEach((obj) => {
obj.visible = false;
});
manualArr.forEach((obj) => {
obj.visible = false;
});
smokeDetectorArr.forEach((obj) => {
obj.visible = false;
});
} else if (modelName === 'acoustoOptic') {
areaOneFloorArr.forEach((obj) => {
obj.visible = false;
});
acoustoOpticArr.forEach((obj) => {
obj.visible = true;
});
manualArr.forEach((obj) => {
obj.visible = false;
});
smokeDetectorArr.forEach((obj) => {
obj.visible = false;
});
} else if (modelName === 'manual') {
areaOneFloorArr.forEach((obj) => {
obj.visible = false;
});
acoustoOpticArr.forEach((obj) => {
obj.visible = false;
});
manualArr.forEach((obj) => {
obj.visible = true;
});
smokeDetectorArr.forEach((obj) => {
obj.visible = false;
});
} else if (modelName === 'smokeDetector') {
areaOneFloorArr.forEach((obj) => {
obj.visible = false;
});
acoustoOpticArr.forEach((obj) => {
obj.visible = false;
});
manualArr.forEach((obj) => {
obj.visible = false;
});
smokeDetectorArr.forEach((obj) => {
obj.visible = true;
});
} else if (modelName === 'refresh') {
areaOneFloorArr.forEach((obj) => {
obj.visible = false;
});
acoustoOpticArr.forEach((obj) => {
obj.visible = false;
});
manualArr.forEach((obj) => {
obj.visible = false;
});
smokeDetectorArr.forEach((obj) => {
obj.visible = false;
});
}
}
onMounted(() => {
const container = containerRef.value!;
container.appendChild(renderer.domElement);
// 初始化摄像机和渲染器尺寸
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
// 监听容器尺寸变化
resizeObserver = new ResizeObserver(() => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
resizeObserver.observe(container);
// 创建射线检测器
const raycaster = new Raycaster();
const mouse = new Vector2();
// 添加点击事件监听
renderer.domElement.addEventListener('click', (event: MouseEvent) => {
const rect = container.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
mouse.x = (x / container.clientWidth) * 2 - 1;
mouse.y = -(y / container.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 合并所有需要检测的数组并过滤出Mesh类型
const targetObjects = [...moveArr].filter((obj) => obj.visible);
const intersects = raycaster.intersectObjects(targetObjects, true);
// const targetScenes = scene.children.filter((obj) => obj.name == 'Scene');
// console.log('targetScenes[0].children:', targetScenes[0].children);
// const intersects = raycaster.intersectObjects(targetScenes[0].children, true);
if (intersects.length > 0) {
let position: Vector3 | undefined;
let x = 0;
let y = 0;
let z = 0;
let targetObj: any;
if (
intersects[0].object.name.includes('acoustoSon') ||
intersects[0].object.name.includes('manualSon') ||
intersects[0].object.name.includes('smokeSon')
) {
targetObj = intersects[0].object.parent;
position = intersects[0].object.parent.position;
x = -0.3;
y = 0.5;
z = 0.5;
} else {
targetObj = intersects[0].object;
position = intersects[0].object.position;
x = -1;
y = 3;
z = 3;
}
console.log('点击了物体', targetObj);
tranlate(
new Vector3(position.x + x, position.y + y, position.z + z),
new Vector3(position.x, position.y, position.z)
);
// const geometry = new BoxGeometry(0.1, 0.1, 0.1);
// const material = new MeshBasicMaterial({ color: 0x00ff00 });
// const cube = new Mesh(geometry, material);
// cube.position.set(position.x + x, position.y + y, position.z + z);
// scene.add(cube);
}
});
animate();
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
</script>
<style scoped>
.scene-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.container {
width: 100%;
height: 100%;
position: relative;
}
.control-panel {
position: absolute;
top: 50%;
right: 2%;
transform: translateY(-50%); /* 垂直居中 */
display: flex;
flex-direction: column;
gap: 20px;
z-index: 101;
align-items: flex-end; /* 改为右对齐 */
background: rgba(0, 0, 0, 0.7);
width: 25%;
height: 100%;
}
.control-buttons {
position: absolute;
top: 50%;
left: 2%;
transform: translateY(-50%); /* 垂直居中 */
display: flex;
flex-direction: column;
gap: 20px;
z-index: 100;
align-items: flex-start; /* 确保所有按钮左对齐 */
}
/* Element Plus圆形按钮样式调整 */
.control-buttons :deep(.el-button) {
width: 40px !important;
height: 40px !important;
min-width: 40px !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
margin: 0 !important;
box-sizing: border-box !important;
}
.control-btn {
padding: 8px 16px;
background: rgba(0, 0, 0, 0.7);
color: white;
border: 1px solid #4fc3f7;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
min-width: 80px;
}
.card {
/* height: 100%; */
width: 100%;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
background-color: rgba(0, 0, 0, 0.3) !important;
backdrop-filter: blur(5px);
border: none;
box-shadow: 0 2px 12px rgba(255, 0, 0, 0.1);
}
.card :deep(.el-card__body) {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.cardHead {
margin: 0 0 15px 0;
padding: 0;
font-size: 16px;
font-weight: bold;
text-align: center;
color: #ffffff;
}
.cardBody {
font-size: 12px;
width: 100%;
}
.info-row {
margin-bottom: 5px;
display: flex;
align-items: center;
width: 100%;
}
/* 让组件继承父容器宽度 */
.full-width-component {
width: 100% !important;
max-width: 100% !important;
}
.equipment-container {
width: 100%;
height: auto;
}
/* 确保设备组件行与信息行宽度一致 */
.equipment-row {
width: 100%;
margin-top: 10px; /* 与其他行保持间距 */
}
.equipment-wrapper {
width: 100%;
padding: 0 10px; /* 与 card body padding 保持一致 */
}
.equipment-component {
width: 100% !important;
display: block !important;
box-sizing: border-box;
}
/* 确保所有行都有相同的宽度基准 */
.info-row,
.equipment-row {
max-width: 100%;
box-sizing: border-box;
}
/* 统一内边距 */
.cardBody {
padding: 10px;
}
.cardBody .el-row {
width: 100%;
margin-bottom: 8px;
}
/* .info-row :deep(.el-col) {
display: flex;
align-items: center;
}
.info-row :deep(.el-tag) {
justify-content: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
display: flex;
align-items: center;
height: 100%;
} */
</style>