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.

313 lines
8.6 KiB
Vue

3 weeks ago
<template>
<div
id="plane"
:style="{
left: infoRef.style.left + 'px',
top: infoRef.style.top + 'px',
display: infoRef.style.display
}"
>
<p>机柜名称{{ infoRef.curCabinet.name }}</p>
<p>机柜温度{{ infoRef.curCabinet.temperature }}°</p>
<p>使用情况{{ infoRef.curCabinet.count }}/{{ infoRef.curCabinet.capacity }}</p>
</div>
<div ref="containerRef" class="container"></div>
</template>
<script setup lang="ts">
import {
WebGLRenderer,
PerspectiveCamera,
Scene,
Color,
Texture,
Mesh,
TextureLoader,
MeshBasicMaterial,
Raycaster,
Vector2,
MeshStandardMaterial,
Object3D,
RepeatWrapping
} from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { ref, reactive, onMounted, onUnmounted, watchEffect } from 'vue';
import TWEEN from '@tweenjs/tween.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
const containerRef = ref<HTMLDivElement>();
const rendererRef = ref<WebGLRenderer>();
const cameraRef = ref<PerspectiveCamera>();
const controlRef = ref<OrbitControls>();
const maps: Map<string, Texture> = new Map();
const cabinets: Mesh[] = [];
const scene = new Scene();
const raycaster = new Raycaster();
const pointer = new Vector2();
const curCabinetRef = ref<Mesh | null>();
const infoRef = reactive({
style: { left: 0, top: 0, display: 'none' },
curCabinet: {
name: 'Loading……',
temperature: 0,
capacity: 0,
count: 0
}
});
// 初始化相机
function initCamera() {
const container = containerRef.value!;
cameraRef.value = new PerspectiveCamera(
45,
container.clientWidth / container.clientHeight, // 使用容器宽高比
0.1,
1000
);
const start = { x: 0, y: 0, z: 0 };
new TWEEN.Tween(start)
.to({ x: 0, y: 10, z: 15 }, 800)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(() => {
const { x, y, z } = start;
cameraRef.value?.position.set(x, y, z);
})
.start();
controlRef.value = new OrbitControls(cameraRef.value, rendererRef.value?.domElement);
controlRef.value.autoRotate = false;
controlRef.value.autoRotateSpeed = 1;
controlRef.value.dampingFactor = 0.2;
controlRef.value.rotateSpeed = 0.3; // 降低旋转速度(默认为 1.0
controlRef.value.enableDamping = true;
controlRef.value.enableRotate = true;
controlRef.value.minDistance = 10;
controlRef.value.maxPolarAngle = Math.PI * 0.5;
}
// 初始化渲染器
function initRenderer() {
const container = containerRef.value!;
rendererRef.value = new WebGLRenderer({
antialias: true, // 启用抗锯齿
alpha: true // 启用透明背景
});
rendererRef.value.setClearColor(new Color('#212d36'));
rendererRef.value.setSize(container.clientWidth, container.clientHeight); // 使用容器尺寸
rendererRef.value.shadowMap.enabled = true;
}
// 渲染循环
function render() {
requestAnimationFrame(render);
if (cameraRef.value) {
rendererRef.value?.render(scene, cameraRef.value);
}
controlRef.value?.update();
TWEEN.update();
}
// 纹理加载
function crtTexture(imgName: string) {
let curTexture = maps.get(imgName);
if (!curTexture) {
curTexture = new TextureLoader().load('/models/' + imgName);
curTexture.flipY = false;
curTexture.wrapS = RepeatWrapping;
curTexture.wrapT = RepeatWrapping;
maps.set(imgName, curTexture);
}
return curTexture;
}
// 材质切换
function changeMat(obj: Mesh, map: Texture | null, color: Color) {
if (map) {
obj.material = new MeshBasicMaterial({
map: crtTexture(map.name)
});
} else {
obj.material = new MeshBasicMaterial({ color });
}
}
// 鼠标交互逻辑
function onMouseOverCabinet({ name }: { name: string }) {
infoRef.style.display = 'block';
infoRef.curCabinet.name = name;
infoRef.curCabinet.temperature = Math.round(Math.random() * 100);
const capacity = Math.ceil(200 * Math.random());
infoRef.curCabinet.capacity = capacity;
infoRef.curCabinet.count = capacity - 5;
}
function onMouseOutCabinet() {
infoRef.style.display = 'none';
}
//原逻辑
function onMouseMoveCabinet(left: number, top: number) {
infoRef.style.left = left;
infoRef.style.top = top;
}
//原逻辑
// function selectCabinet(x: number, y: number) {
// const container = rendererRef.value!.domElement;
// const { width, height } = container;
// pointer.set((x / width) * 2 - 1, -(y / height) * 2 + 1);
// if (cameraRef.value) {
// raycaster.setFromCamera(pointer, cameraRef.value);
// }
// const intersect = raycaster.intersectObjects(cabinets)[0];
// let intersectObj = intersect ? (intersect.object as Mesh) : null;
// if (curCabinetRef.value && curCabinetRef.value !== intersectObj) {
// const material = curCabinetRef.value.material as MeshBasicMaterial;
// material.setValues({
// map: maps.get('cabinet.jpg')
// });
// }
// if (intersectObj) {
// onMouseMoveCabinet(x, y);
// if (intersectObj !== curCabinetRef.value) {
// curCabinetRef.value = intersectObj;
// const material = intersectObj.material as MeshBasicMaterial;
// material.setValues({
// map: maps.get('cabinet-hover.jpg')
// });
// onMouseOverCabinet(intersectObj);
// }
// } else if (curCabinetRef.value) {
// curCabinetRef.value = null;
// onMouseOutCabinet();
// }
// }
function selectCabinet(x: number, y: number) {
const container = rendererRef.value!.domElement;
const { width, height } = container;
// 鼠标坐标转裁剪坐标注意Y 轴翻转)
pointer.set((x / width) * 2 - 1, -(y / height) * 2 + 1);
if (cameraRef.value) {
raycaster.setFromCamera(pointer, cameraRef.value);
}
const intersect = raycaster.intersectObjects(cabinets)[0];
let intersectObj = intersect ? (intersect.object as Mesh) : null;
if (curCabinetRef.value && curCabinetRef.value !== intersectObj) {
const material = curCabinetRef.value.material as MeshBasicMaterial;
material.setValues({
map: maps.get('cabinet.jpg')
});
}
if (intersectObj) {
onMouseMoveCabinet(x, y);
if (intersectObj !== curCabinetRef.value) {
curCabinetRef.value = intersectObj;
const material = intersectObj.material as MeshBasicMaterial;
material.setValues({
map: maps.get('cabinet-hover.jpg')
});
onMouseOverCabinet(intersectObj);
}
} else if (curCabinetRef.value) {
curCabinetRef.value = null;
onMouseOutCabinet();
}
}
//原逻辑
// function mouseMove({ clientX, clientY }: { clientX: number; clientY: number }) {
// console.log('mouseMove', clientX, clientY);
// selectCabinet(clientX, clientY);
// }
function mouseMove(event: MouseEvent) {
const container = containerRef.value!;
const rect = container.getBoundingClientRect(); // 获取容器相对于视口的位置
const x = event.clientX - rect.left; // 相对于容器的 X 坐标
const y = event.clientY - rect.top; // 相对于容器的 Y 坐标
selectCabinet(x, y);
}
// 尺寸变化监听
let resizeObserver: ResizeObserver | null = null;
onMounted(() => {
crtTexture('cabinet-hover.jpg');
containerRef.value!.onmousemove = mouseMove;
const loader = new GLTFLoader();
loader.load('/models/machineRoom.gltf', (gltf: any) => {
gltf.scene.children.forEach((obj: Object3D) => {
console.log('obj', obj);
if (obj instanceof Mesh) {
const { map, color } = obj.material as MeshStandardMaterial;
changeMat(obj, map, color);
if (obj.name.includes('cabinet')) {
cabinets.push(obj);
}
}
});
scene.add(...gltf.scene.children);
});
initRenderer();
initCamera();
render();
// 监听容器尺寸变化
resizeObserver = new ResizeObserver(() => {
const container = containerRef.value!;
cameraRef.value!.aspect = container.clientWidth / container.clientHeight;
cameraRef.value!.updateProjectionMatrix();
rendererRef.value!.setSize(container.clientWidth, container.clientHeight);
});
resizeObserver.observe(containerRef.value!);
});
onUnmounted(() => {
// 清理 ResizeObserver
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
watchEffect(() => {
if (rendererRef.value) {
containerRef.value?.append(rendererRef.value.domElement);
}
});
</script>
<style scoped>
.container {
position: relative; /* 确保容器定位正确 */
width: 100%;
height: 100%;
overflow: hidden; /* 防止滚动条干扰 */
}
#plane {
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
color: #b1acac;
padding: 0 18px;
transform: translate(12px, -100%);
display: block;
z-index: 9999; /* 提高层级,确保在最上层 */
}
</style>