feat: 新增钢铁厂数字孪生平台基础架构和功能模块

- 添加Three.js基础场景配置和核心功能模块
- 实现模型加载器、动画循环和交互选择器
- 添加温度、压力等仪表盘组件
- 配置Vite构建工具和ESLint规范
- 添加基础UI组件和布局系统
- 实现数据可视化图表组件
- 配置Nginx部署文件
- 添加钢铁厂设备数据模型
This commit is contained in:
砂糖
2025-11-28 16:58:15 +08:00
parent bffd7a0666
commit 74a066dd80
115 changed files with 20907 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
import TWEEN, { Easing } from '@tweenjs/tween.js';
import * as THREE from 'three';
import { CSS2DRenderer } from 'three/examples/jsm/Addons.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import * as ThreeBase from './core';
import Loop, { type Updatable } from './loop';
import { throttle } from 'lodash';
const events = <const>['click', 'dblclick', 'pointermove'];
type EventMap = (typeof events)[number];
const useThree = () => {
const containerEl = ref<HTMLElement>();
const scene = shallowRef<THREE.Scene>();
const camera = shallowRef<THREE.Camera>();
const renderer = shallowRef<THREE.WebGLRenderer>();
const cssRenderer = shallowRef<CSS2DRenderer>();
const controls = shallowRef<OrbitControls>();
const mixers: THREE.AnimationMixer[] = [];
const resizeListener: Array<() => void> = [];
// let raycaster: THREE.Raycaster, pointer: THREE.Vector2;
let loop: Loop;
const resizeObserver = new ResizeObserver(() => {
setSize(containerEl.value!, camera.value!, renderer.value!, cssRenderer.value);
resizeListener.forEach((func) => {
func?.();
});
});
const onResize = (callback: () => void) => {
resizeListener.push(callback);
};
onMounted(() => {
init();
resizeObserver.observe(containerEl.value!);
});
const init = () => {
const el = containerEl.value as HTMLElement;
if (el.clientHeight === 0 || el.clientWidth === 0) {
throw new Error('element should had width and height before init.');
}
scene.value = ThreeBase.initScene();
camera.value = ThreeBase.initCamera(el);
renderer.value = ThreeBase.initRenderer(el);
cssRenderer.value = ThreeBase.initCSSRender(el);
controls.value = ThreeBase.initControls(camera.value, cssRenderer.value!.domElement);
loop = new Loop(camera.value, scene.value, renderer.value);
};
const setSize = (
container: HTMLElement,
camera: THREE.Camera,
renderer: THREE.WebGLRenderer,
cssRenderer?: CSS2DRenderer
) => {
if (!container || !camera || !renderer || !cssRenderer) {
return;
}
const { clientHeight, clientWidth } = container;
if (clientHeight === 0 || clientWidth === 0) {
throw new Error('element should had width and height before init.');
}
console.log('camera.type', camera.type);
if (camera.type === 'PerspectiveCamera') {
(camera as THREE.PerspectiveCamera).aspect = clientWidth / clientHeight;
(camera as THREE.PerspectiveCamera).updateProjectionMatrix();
}
renderer?.setSize(clientWidth, clientHeight);
renderer?.setPixelRatio(window.devicePixelRatio);
cssRenderer?.setSize(clientWidth, clientHeight);
};
const render = (onUpdate?: Updatable['update']) => {
loop.addUpdatables(controls.value!, {
key: 'cssRenderer loop',
update: () => {
// console.log(`The last frame rendered in ${delta * 1000} milliseconds,run 'cssRenderer loop'`);
cssRenderer.value!.render(scene.value!, camera.value!);
},
});
loop.start((deltaTime) => {
onUpdate?.(deltaTime);
TWEEN.update();
});
};
const focus = (target: THREE.Object3D) => {
const pos = new THREE.Vector3();
target.getWorldPosition(pos);
// 相机飞行到的位置和观察目标拉开一定的距离
const pos2 = pos.clone().addScalar(16);
// const box = new THREE.Box3();
// const center = new THREE.Vector3();
// const sphere = new THREE.Sphere();
// const delta = new THREE.Vector3();
// const quaternion = new THREE.Quaternion();
// box.setFromObject(target);
// box.getCenter(center);
// target.getWorldQuaternion(quaternion);
createCameraTween(pos2, pos);
};
/**
* 相机动画函数从A点飞行到B点A点表示相机当前所处状态
* @param endPos {THREE.Vector3} 表示动画结束相机位置
* @param endTarget {THREE.Vector3} 表示相机动画结束lookAt指向的目标观察点
*/
function createCameraTween(endPos: THREE.Vector3, endTarget: THREE.Vector3) {
const cameraStartPos = camera.value!.position;
const controlsPos = controls.value!.target;
console.log('createCameraTween', cameraStartPos, controlsPos, endPos, endTarget);
new TWEEN.Tween({
// 不管相机此刻处于什么状态,直接读取当前的位置和目标观察点
x: cameraStartPos.x,
y: cameraStartPos.y,
z: cameraStartPos.z,
tx: controlsPos.x,
ty: controlsPos.y,
tz: controlsPos.z,
})
.to(
{
// 动画结束相机位置坐标
x: endPos.x,
y: endPos.y,
z: endPos.z,
// 动画结束相机指向的目标观察点
tx: endTarget.x,
ty: endTarget.y,
tz: endTarget.z,
},
1000
)
.easing(Easing.Quadratic.Out)
.onUpdate(function (obj) {
// 动态改变相机位置
camera.value!.position.set(obj.x, obj.y, obj.z);
// 动态计算相机视线
// camera.lookAt(obj.tx, obj.ty, obj.tz);
controls.value!.target.set(obj.tx, obj.ty, obj.tz);
controls.value!.update(); //内部会执行.lookAt()
})
.start();
}
const addEventListener = (
eventName: EventMap,
handler: (inersectObjects: THREE.Intersection[]) => void,
objects?: THREE.Object3D<THREE.Object3DEventMap>[]
) => {
const throttleFunc = throttle((event: MouseEvent | PointerEvent) => {
const px = event.offsetX;
const py = event.offsetY;
const { offsetWidth: width, offsetHeight: height } = renderer.value!.domElement!;
const x = (px / width) * 2 - 1;
const y = -(py / height) * 2 + 1;
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
pointer.set(x, y);
raycaster.setFromCamera(pointer, camera.value!);
if (!objects) {
objects = [];
}
const intersects = raycaster.intersectObjects(objects, true);
handler(intersects);
}, 50);
cssRenderer.value!.domElement.addEventListener<EventMap>(eventName, throttleFunc);
};
const dispose = () => {
controls.value?.dispose();
loop.stop();
scene.value?.clear();
renderer.value?.forceContextLoss();
renderer.value?.dispose();
resizeObserver.disconnect();
};
onUnmounted(() => {
dispose();
});
const onclick = (
objects: THREE.Object3D<THREE.Object3DEventMap>[],
handler: (inersectObjects: THREE.Intersection[]) => void
) => {
addEventListener('click', handler, objects);
};
/**
* 加载模型动画
* @param mesh
* @param animations
* @param animationName
* @returns
*/
const loadAnimate = (
mesh: THREE.Mesh | THREE.AnimationObjectGroup | THREE.Group,
animations: Array<THREE.AnimationClip>,
animationName: string
) => {
const mixer = new THREE.AnimationMixer(mesh);
const clip = THREE.AnimationClip.findByName(animations, animationName);
if (!clip) return undefined;
const action = mixer.clipAction(clip);
action.play();
mixers.push(mixer);
};
// const addStats = () => {};
return {
render,
loadAnimate,
scene,
camera,
renderer,
controls,
containerEl,
cssRenderer,
onclick,
addEventListener,
onResize,
focus,
};
};
export default useThree;
export { loadGLTF } from './modelLoader';
export { initStats } from './core';
export * from './utils';
export * from './Object3dWrap';