firedoor.threejs.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import * as THREE from 'three';
  2. import gsap from 'gsap';
  3. import { ref, watch, Ref, nextTick } from 'vue';
  4. // 用于存储当前的门状态数据,方便外部调试或扩展
  5. const gateList = ref<any[]>([]);
  6. // 模块级变量:存储当前的 handleGateAnimate,供外部组件直接调用 3D 动画
  7. let currentHandleGateAnimate: ((doors: any[]) => void) | null = null;
  8. /** 供外部(如 2D 组件)调用的 3D 动画触发函数 */
  9. export function trigger3DAnimation(doors: any[]) {
  10. if (currentHandleGateAnimate) {
  11. currentHandleGateAnimate(doors);
  12. } else {
  13. console.warn('[3D动画] currentHandleGateAnimate 未初始化,请确保 modalAnimate 已调用');
  14. }
  15. }
  16. /**
  17. * 初始化防火门 3D 动画
  18. * @param modal Three.js 模态框实例
  19. * @param modalMonitorData 响应式数据对象,包含 devMonitorMap.door
  20. */
  21. export async function modalAnimate(modal: any, modalMonitorData: Ref<any, any>) {
  22. await nextTick();
  23. if (!modal || !modal.scene) {
  24. return;
  25. }
  26. // 尝试多种方式定位防火门模型的容器对象
  27. let fireDoorModal: THREE.Object3D | undefined;
  28. // 方式1:通过已知的层级路径 pidai → PiDaiHang_1
  29. const pidaiObj = modal.scene.getObjectByName('pidai');
  30. if (pidaiObj) {
  31. fireDoorModal = pidaiObj.getObjectByName('PiDaiHang_1');
  32. }
  33. // 方式2:直接在场景中搜索 PiDaiHang_1
  34. if (!fireDoorModal) {
  35. fireDoorModal = modal.scene.getObjectByName('PiDaiHang_1');
  36. }
  37. // 方式3:搜索名字中包含 PiDaiHang 的对象
  38. if (!fireDoorModal) {
  39. modal.scene.traverse((obj: THREE.Object3D) => {
  40. if (!fireDoorModal && obj.name.includes('PiDaiHang')) {
  41. fireDoorModal = obj;
  42. }
  43. });
  44. }
  45. // 方式4:通过模型根节点 (modelName = 'pidai-fire-door') 搜索
  46. if (!fireDoorModal) {
  47. const modelRoot = modal.scene.getObjectByName('pidai-fire-door');
  48. if (modelRoot) {
  49. modelRoot.traverse((obj: THREE.Object3D) => {
  50. if (!fireDoorModal && (obj.name.includes('PiDaiHang') || obj.name.includes('pidaihang') || obj.name.includes('PiDai'))) {
  51. fireDoorModal = obj;
  52. }
  53. });
  54. }
  55. }
  56. if (!fireDoorModal) {
  57. return;
  58. }
  59. // 类型收窄:确保后续代码中 fireDoorModal 为非 undefined
  60. const doorContainer: THREE.Object3D = fireDoorModal;
  61. resetModalParam(modal);
  62. /**
  63. * 在容器内查找与给定模式匹配的网格对象
  64. * 优先精确匹配 men_{suffix} / gaiban_{suffix},找不到则回退到包含关键词的对象
  65. */
  66. const findMesh = (container: THREE.Object3D, patterns: string[]): THREE.Object3D | undefined => {
  67. for (const pattern of patterns) {
  68. const exact = container.getObjectByName(pattern);
  69. if (exact) return exact;
  70. }
  71. // 模糊匹配
  72. let found: THREE.Object3D | undefined;
  73. container.traverse((child: THREE.Object3D) => {
  74. if (found) return;
  75. const lower = child.name.toLowerCase();
  76. if (patterns.some((p) => lower.includes(p.toLowerCase()))) {
  77. found = child;
  78. }
  79. });
  80. return found;
  81. };
  82. /**
  83. * 核心动画处理函数
  84. * @param doors 门设备数据数组
  85. */
  86. const handleGateAnimate = (doors: any[]) => {
  87. if (!doors || doors.length === 0) return;
  88. doors.forEach((gateData: any, index: number) => {
  89. // 获取状态值
  90. const frontGateOpenVal = gateData['readData']?.['frontGateOpen']?.value;
  91. const mbOpenVal = gateData['readData']?.['MBOpen']?.value;
  92. const isFireDoorOpen = frontGateOpenVal == '1' || frontGateOpenVal == 1;
  93. const isBeltCoverOpen = mbOpenVal == '1' || mbOpenVal == 1;
  94. const hasBelt = mbOpenVal !== undefined && mbOpenVal !== null;
  95. if (hasBelt) {
  96. // 有皮带:控制 men_1(防火门)+ gaiban(皮带盖)
  97. const fireDoorMesh = findMesh(doorContainer, [`men_${index + 1}`, 'men_1', 'men']);
  98. const beltCoverMesh = findMesh(doorContainer, [`gaiban_${index + 1}`, 'gaiban', 'gaiban_1']);
  99. if (fireDoorMesh) {
  100. animateFireDoorWithBelt(fireDoorMesh, isFireDoorOpen);
  101. }
  102. if (beltCoverMesh) {
  103. animateBeltCover(beltCoverMesh, isBeltCoverOpen);
  104. }
  105. } else {
  106. // 无皮带:控制 men_2 + men3(两扇防火门向两边开)
  107. const leftDoorMesh = findMesh(doorContainer, ['men_2']);
  108. const rightDoorMesh = findMesh(doorContainer, ['men3']);
  109. if (leftDoorMesh) {
  110. animateFireDoorLeft(leftDoorMesh, isFireDoorOpen);
  111. }
  112. if (rightDoorMesh) {
  113. animateFireDoorRight(rightDoorMesh, isFireDoorOpen);
  114. }
  115. }
  116. });
  117. };
  118. /**
  119. * 有皮带时的防火门动画
  120. * 关闭: (0, 0, 0)
  121. * 开启: (0, 0, 85度) -> 绕 Z 轴旋转
  122. */
  123. const animateFireDoorWithBelt = (mesh: THREE.Object3D, isOpen: boolean) => {
  124. const targetRotationZ = isOpen ? THREE.MathUtils.degToRad(85) : 0;
  125. gsap.to(mesh.rotation, {
  126. x: 0,
  127. y: 0,
  128. z: targetRotationZ,
  129. duration: 1.5,
  130. ease: 'power2.inOut',
  131. overwrite: true,
  132. });
  133. };
  134. /**
  135. * 无皮带时左侧防火门动画 (men_2)
  136. * 关闭: (0, 0, 0)
  137. * 开启: (0, 0, -90度) -> 绕 Z 轴旋转
  138. */
  139. const animateFireDoorLeft = (mesh: THREE.Object3D, isOpen: boolean) => {
  140. const targetRotationZ = isOpen ? THREE.MathUtils.degToRad(-90) : 0;
  141. gsap.to(mesh.rotation, {
  142. x: 0,
  143. y: 0,
  144. z: targetRotationZ,
  145. duration: 1.5,
  146. ease: 'power2.inOut',
  147. overwrite: true,
  148. });
  149. };
  150. /**
  151. * 无皮带时右侧防火门动画 (men_3)
  152. * 关闭: (0, 0, 0)
  153. * 开启: (0, 0, 90度) -> 绕 Z 轴旋转
  154. */
  155. const animateFireDoorRight = (mesh: THREE.Object3D, isOpen: boolean) => {
  156. const targetRotationZ = isOpen ? THREE.MathUtils.degToRad(90) : 0;
  157. gsap.to(mesh.rotation, {
  158. x: 0,
  159. y: 0,
  160. z: targetRotationZ,
  161. duration: 1.5,
  162. ease: 'power2.inOut',
  163. overwrite: true,
  164. });
  165. };
  166. /**
  167. * 皮带密闭动画
  168. * 关闭: (0, 0, 0)
  169. * 开启: (0, -70度, 0) -> 绕 Y 轴旋转
  170. */
  171. const animateBeltCover = (mesh: THREE.Object3D, isOpen: boolean) => {
  172. const targetRotationY = isOpen ? THREE.MathUtils.degToRad(-70) : 0;
  173. gsap.to(mesh.rotation, {
  174. x: 0,
  175. y: targetRotationY,
  176. z: 0,
  177. duration: 1.5,
  178. ease: 'power2.inOut',
  179. overwrite: true,
  180. });
  181. };
  182. // 将处理函数挂载到模块级变量,供外部组件(如 2D 面板)调用
  183. currentHandleGateAnimate = handleGateAnimate;
  184. // 监听数据变化,驱动动画
  185. watch(
  186. modalMonitorData,
  187. (newData) => {
  188. if (!newData) return;
  189. // 获取门数据,优先使用 devMonitorMap.door (与 2D 视图一致)
  190. const doors = newData.devMonitorMap?.door || [];
  191. gateList.value = doors;
  192. // 执行动画
  193. handleGateAnimate(doors);
  194. },
  195. { immediate: true, deep: true }
  196. );
  197. // 初始化 CSS3D 容器 (如果需要显示 HTML 标签,否则可移除)
  198. if (modal.initCSS3Renderer) {
  199. modal.initCSS3Renderer('#css3dContainer');
  200. }
  201. // 暴露方法供外部调用
  202. return {
  203. gateList,
  204. handleGateAnimate,
  205. };
  206. }
  207. function resetModalParam(modal: any) {
  208. if (!modal.orbitControls) return;
  209. modal.orbitControls.rotateSpeed = 0.5;
  210. modal.orbitControls.zoomSpeed = 0.6;
  211. modal.orbitControls.panSpeed = 0.5;
  212. modal.orbitControls.enableDamping = true;
  213. modal.orbitControls.dampingFactor = 0.05;
  214. }
  215. // 清理函数
  216. export function destroy(modal: any) {
  217. gateList.value = [];
  218. currentHandleGateAnimate = null;
  219. if (modal.canvasContainer) {
  220. // 移除事件监听器等
  221. }
  222. }