Files
klp-mono/apps/steelmill/src/hooks/useThree/index.ts
砂糖 74a066dd80 feat: 新增钢铁厂数字孪生平台基础架构和功能模块
- 添加Three.js基础场景配置和核心功能模块
- 实现模型加载器、动画循环和交互选择器
- 添加温度、压力等仪表盘组件
- 配置Vite构建工具和ESLint规范
- 添加基础UI组件和布局系统
- 实现数据可视化图表组件
- 配置Nginx部署文件
- 添加钢铁厂设备数据模型
2025-11-28 16:58:15 +08:00

232 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';