feat(3d): 优化3D场景性能并添加纹理缓存
refactor(DeviceModel): 使用相机Z坐标阈值减少背板渲染状态更新 refactor(LODManager): 基于距离变化率优化LOD切换逻辑 feat(TextureCache): 新增纹理缓存类用于U位标签纹理管理 refactor(RackModel): 使用纹理缓存重构U位标签生成逻辑 feat(backend): 添加端口兼容性检查功能 feat(cables): 实现端口类型和速率兼容性验证API feat(frontend): 新增设备筛选组件FilterableDeviceSelect refactor(PortPanel): 优化端口占用状态显示和交互 feat(CableWizardModal): 集成端口兼容性检查并优化UI
This commit is contained in:
@@ -630,7 +630,7 @@ const DeviceModel = ({
|
||||
// 背板渲染控制状态
|
||||
const [backPanelLevel, setBackPanelLevel] = useState(0); // 0: 不渲染, 1: 简化, 2: 完整
|
||||
const { camera } = useThree();
|
||||
const frameCount = useRef(0);
|
||||
const prevCameraZRef = useRef(null);
|
||||
|
||||
// 使用传入的 uHeight (默认为 0.04445m)
|
||||
const uHeight = propUHeight || 0.04445;
|
||||
@@ -653,20 +653,22 @@ const DeviceModel = ({
|
||||
});
|
||||
|
||||
// 背板渲染控制 - 根据相机位置动态调整
|
||||
// 只在相机绕到背面时才渲染背板
|
||||
// 仅在相机 Z 坐标变化超过阈值时触发,减少不必要的状态更新
|
||||
useFrame(() => {
|
||||
// 每10帧检查一次,减少计算频率
|
||||
frameCount.current++;
|
||||
if (frameCount.current % 10 !== 0) return;
|
||||
|
||||
if (!groupRef.current) return;
|
||||
|
||||
const cameraZ = camera.position.z;
|
||||
|
||||
// 仅当相机 Z 坐标变化超过阈值(0.1)时才检查
|
||||
if (prevCameraZRef.current !== null && Math.abs(cameraZ - prevCameraZRef.current) < 0.1) {
|
||||
return;
|
||||
}
|
||||
prevCameraZRef.current = cameraZ;
|
||||
|
||||
// 获取相机相对设备的位置
|
||||
const deviceWorldPos = new THREE.Vector3();
|
||||
groupRef.current.getWorldPosition(deviceWorldPos);
|
||||
|
||||
const cameraZ = camera.position.z;
|
||||
|
||||
// 简单逻辑:相机在正面(z > threshold)时不渲染背板
|
||||
// 相机在背面(z <= threshold)时渲染完整背板
|
||||
const shouldShowBackPanel = cameraZ <= BACK_PANEL_CONFIG.visibilityThreshold;
|
||||
|
||||
@@ -109,28 +109,26 @@ const LODManager = ({
|
||||
const mediumDetailRef = useRef();
|
||||
const lowDetailRef = useRef();
|
||||
const { camera } = useThree();
|
||||
// 使用 ref 存储 LOD 级别,避免 React 状态更新
|
||||
const lodLevelRef = useRef(LOD_LEVELS.HIGH);
|
||||
const distanceRef = useRef(0);
|
||||
// 帧计数器,用于节流
|
||||
const frameCount = useRef(0);
|
||||
// 用于强制重新渲染的 state(仅在必要时更新)
|
||||
const prevDistanceRef = useRef(null);
|
||||
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
|
||||
|
||||
useFrame(() => {
|
||||
if (!groupRef.current) return;
|
||||
|
||||
// 节流:每5帧检查一次
|
||||
frameCount.current++;
|
||||
if (frameCount.current % 5 !== 0) return;
|
||||
|
||||
const distance = camera.position.distanceTo(groupRef.current.position);
|
||||
|
||||
// 仅当距离变化超过阈值(10%)时才检查
|
||||
if (prevDistanceRef.current !== null) {
|
||||
const changeRatio = Math.abs(distance - prevDistanceRef.current) / (prevDistanceRef.current || 1);
|
||||
if (changeRatio < 0.1) return;
|
||||
}
|
||||
prevDistanceRef.current = distance;
|
||||
distanceRef.current = distance;
|
||||
|
||||
// 添加缓冲避免频繁切换(10% 缓冲)
|
||||
const buffer = 0.1;
|
||||
const highThreshold = LOD_DISTANCES.HIGH * (1 + buffer);
|
||||
const mediumThreshold = LOD_DISTANCES.MEDIUM * (1 + buffer);
|
||||
const highThreshold = LOD_DISTANCES.HIGH;
|
||||
const mediumThreshold = LOD_DISTANCES.MEDIUM;
|
||||
|
||||
let newLevel = LOD_LEVELS.HIGH;
|
||||
if (distance > mediumThreshold) {
|
||||
@@ -139,10 +137,8 @@ const LODManager = ({
|
||||
newLevel = LOD_LEVELS.MEDIUM;
|
||||
}
|
||||
|
||||
// 只有当级别变化时才更新
|
||||
if (newLevel !== lodLevelRef.current) {
|
||||
lodLevelRef.current = newLevel;
|
||||
// 直接操作 ref 切换可见性,避免频繁的 React 重渲染
|
||||
if (highDetailRef.current) {
|
||||
highDetailRef.current.visible = newLevel === LOD_LEVELS.HIGH;
|
||||
}
|
||||
@@ -152,10 +148,7 @@ const LODManager = ({
|
||||
if (lowDetailRef.current) {
|
||||
lowDetailRef.current.visible = newLevel === LOD_LEVELS.LOW;
|
||||
}
|
||||
// 偶尔强制更新以确保同步(每30帧)
|
||||
if (frameCount.current % 30 === 0) {
|
||||
forceUpdate();
|
||||
}
|
||||
forceUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
|
||||
import * as THREE from 'three';
|
||||
import DeviceModel from './DeviceModel';
|
||||
import LODManager, { LOD_LEVELS } from './LODManager';
|
||||
import { getULabelTexture } from './materials/TextureCache';
|
||||
|
||||
const RackModel = ({
|
||||
rack,
|
||||
@@ -56,80 +57,50 @@ const RackModel = ({
|
||||
// 生成U位刻度标识 - 写在机柜左右两侧柱子上
|
||||
const uLabels = useMemo(() => {
|
||||
const labels = [];
|
||||
// 每一个U位都显示数字,从底部开始(U1在底部)
|
||||
for (let u = 1; u <= rackHeight; u += 1) {
|
||||
// 计算U位标识的Y坐标,需要加上设备组的偏移量
|
||||
const yPos = (u - 1) * uHeight + uHeight / 2 + deviceGroupOffset;
|
||||
|
||||
// 创建数字纹理 - 白色数字在深色背景上更清晰
|
||||
const createNumberTexture = num => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 128;
|
||||
canvas.height = 128;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const texture = getULabelTexture(u);
|
||||
|
||||
// 清除画布
|
||||
ctx.clearRect(0, 0, 128, 128);
|
||||
|
||||
// 绘制白色数字
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 80px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(num.toString(), 64, 64);
|
||||
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
};
|
||||
|
||||
const leftTexture = createNumberTexture(u);
|
||||
const rightTexture = createNumberTexture(u);
|
||||
|
||||
// 创建平面几何体显示数字 - 每5U使用稍大的尺寸
|
||||
const isMajorU = u % 5 === 0;
|
||||
const planeSize = isMajorU ? 0.035 : 0.025;
|
||||
const planeGeometry = new THREE.PlaneGeometry(planeSize, planeSize);
|
||||
|
||||
// 前侧柱子的位置: [-width/2 + postWidth/2, y, depth/2 - postWidth/2]
|
||||
const leftPostX = -width / 2 + postWidth / 2;
|
||||
const rightPostX = width / 2 - postWidth / 2;
|
||||
const frontPostZ = depth / 2 - postWidth / 2;
|
||||
|
||||
// 刻度线颜色:每5U使用醒目的黄色,其他使用灰色
|
||||
const tickColor = isMajorU ? '#fbbf24' : '#6b7280';
|
||||
const tickHeight = isMajorU ? 0.002 : 0.001;
|
||||
|
||||
labels.push(
|
||||
<group key={`u-label-${u}`}>
|
||||
{/* 左前侧柱子上的U位标识 - 贴在柱子正侧面(面向前方) */}
|
||||
<mesh
|
||||
position={[leftPostX, yPos, frontPostZ + postWidth / 2 + 0.001]}
|
||||
geometry={planeGeometry}
|
||||
>
|
||||
<meshBasicMaterial
|
||||
map={leftTexture}
|
||||
map={texture}
|
||||
transparent={true}
|
||||
opacity={1}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
{/* 右前侧柱子上的U位标识 - 贴在柱子正侧面(面向前方) */}
|
||||
<mesh
|
||||
position={[rightPostX, yPos, frontPostZ + postWidth / 2 + 0.001]}
|
||||
geometry={planeGeometry}
|
||||
>
|
||||
<meshBasicMaterial
|
||||
map={rightTexture}
|
||||
map={texture}
|
||||
transparent={true}
|
||||
opacity={1}
|
||||
side={THREE.DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
{/* 左前侧柱子上的刻度线 */}
|
||||
<mesh position={[leftPostX, yPos, frontPostZ + postWidth / 2 + 0.002]}>
|
||||
<boxGeometry args={[postWidth, tickHeight, 0.001]} />
|
||||
<meshBasicMaterial color={tickColor} />
|
||||
</mesh>
|
||||
{/* 右前侧柱子上的刻度线 */}
|
||||
<mesh position={[rightPostX, yPos, frontPostZ + postWidth / 2 + 0.002]}>
|
||||
<boxGeometry args={[postWidth, tickHeight, 0.001]} />
|
||||
<meshBasicMaterial color={tickColor} />
|
||||
@@ -138,7 +109,7 @@ const RackModel = ({
|
||||
);
|
||||
}
|
||||
return labels;
|
||||
}, [rackHeight, uHeight, width, depth, deviceGroupOffset]);
|
||||
}, [rackHeight]);
|
||||
|
||||
// 生成机柜框架
|
||||
const frame = useMemo(() => {
|
||||
|
||||
@@ -165,7 +165,6 @@ const Scene = forwardRef(
|
||||
subTitle="3D 场景在渲染过程中遇到错误,请检查浏览器是否支持 WebGL"
|
||||
>
|
||||
<Canvas
|
||||
shadows
|
||||
dpr={deviceDpr}
|
||||
performance={{ min: 0.5 }}
|
||||
gl={{
|
||||
@@ -178,12 +177,12 @@ const Scene = forwardRef(
|
||||
<PerspectiveCamera makeDefault position={cameraPosition} fov={45} />
|
||||
|
||||
<ambientLight intensity={0.5} color="#ffffff" />
|
||||
<pointLight position={[5, 8, 5]} intensity={2} color="#ffffff" castShadow />
|
||||
<pointLight position={[5, 8, 5]} intensity={2} color="#ffffff" />
|
||||
<directionalLight
|
||||
position={[10, 10, 5]}
|
||||
intensity={1}
|
||||
castShadow
|
||||
shadow-mapSize={[2048, 2048]}
|
||||
shadow-mapSize={[512, 512]}
|
||||
shadow-camera-far={20}
|
||||
shadow-camera-left={-10}
|
||||
shadow-camera-right={10}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
class TextureCache {
|
||||
constructor() {
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
getOrCreate(key, creator) {
|
||||
if (!this.cache.has(key)) {
|
||||
this.cache.set(key, creator());
|
||||
}
|
||||
return this.cache.get(key);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.cache.forEach(texture => {
|
||||
if (texture instanceof THREE.CanvasTexture) {
|
||||
texture.dispose();
|
||||
}
|
||||
});
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const uLabelTextureCache = new TextureCache();
|
||||
|
||||
export const getULabelTexture = num => {
|
||||
return uLabelTextureCache.getOrCreate(num, () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 32;
|
||||
canvas.height = 32;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.clearRect(0, 0, 32, 32);
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 20px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(num.toString(), 16, 16);
|
||||
|
||||
return new THREE.CanvasTexture(canvas);
|
||||
});
|
||||
};
|
||||
|
||||
export const disposeULabelTextures = () => {
|
||||
uLabelTextureCache.dispose();
|
||||
};
|
||||
|
||||
export default TextureCache;
|
||||
Reference in New Issue
Block a user