Преглед изворни кода

[Feat 0000] 布尔台开发按设备灵活加减注氮机的模型

houzekong пре 1 недеља
родитељ
комит
a5a01b94ac

+ 1 - 1
src/views/vent/monitorManager/compressor/components/nitrogenHome_bet.vue

@@ -488,7 +488,7 @@
   watch([monitorDataGroupNum, loading], ([newMonitorDataGroupNum, newLoading]) => {
   watch([monitorDataGroupNum, loading], ([newMonitorDataGroupNum, newLoading]) => {
     nextTick(() => {
     nextTick(() => {
       if (newMonitorDataGroupNum && !newLoading) {
       if (newMonitorDataGroupNum && !newLoading) {
-        setModelType(props.modalType, newMonitorDataGroupNum);
+        setModelType('nitrogenFlex', newMonitorDataGroupNum);
       }
       }
     });
     });
   });
   });

+ 329 - 0
src/views/vent/monitorManager/compressor/nitrogen.flex.threejs.ts

@@ -0,0 +1,329 @@
+import * as THREE from 'three';
+import { CSS3DSprite } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
+import { setModalCenter } from '/@/utils/threejs/util';
+
+/**
+ * 注氮机地下管路模型(nitrogenFlex)
+ *
+ * 模型构成:
+ *   - 基底:nitrogen_base(管路 / 底座)
+ *   - 注氮机:nitrogen_ele_1 / nitrogen_ele_2 / nitrogen_ele_3
+ *
+ * ── 重要:三个注氮机模型是「预制好机位」的,必须原位(identity)摆放 ──
+ */
+class NitrogenFlex {
+  model;
+  /** 基底模型名 */
+  baseName = 'nitrogen_base';
+  /** 注氮机模型名,按机数取前 N 个(对应 右 / 中 / 左 机位) */
+  unitNames = ['nitrogen_ele_1', 'nitrogen_ele_2', 'nitrogen_ele_3'];
+  /** 总根节点 */
+  group: THREE.Object3D = new THREE.Object3D();
+  /** 已加载的注氮机模板 */
+  private unitTemplates: (THREE.Object3D | null)[] = [];
+  /** 当前场景中实际显示的注氮机节点 */
+  private unitGroups: THREE.Object3D[] = [];
+  /** 本模型显示的 css 详情元素数组 */
+  cssSprites: CSS3DSprite[] = [];
+  /** 当前机数 */
+  nitrogenNum = 0;
+  /** css 精灵世界缩放(模型为原位摆放,无需再补偿父级缩放) */
+  private spriteScale = 0.008;
+  /** 模型是否加载完成 */
+  private loaded = false;
+
+  /** 所有机体整体的垂直微调(默认 0 = 模型设计高度) */
+  private unitLiftY = 0;
+  /** 所有机体整体的深度微调(默认 0) */
+  private unitOffsetZ = 0;
+  /** 下风侧监测挂载位置:相对基底包围盒归一化(0=最小边,1=最大边) */
+  private downWindAnchor = { nx: 0.04, ny: 0.75, nz: 0.5 };
+
+  /**
+   * 相机取景:模型宽度占画面的比例(0.92 = 横向占满 92%)。
+   * 调大 = 相机更近 = 模型更大。
+   */
+  private cameraWidthFill = 0.92;
+  /** 相机距离下限(包围球拟合 × 该系数),防止超宽屏时贴太近被裁 */
+  private cameraMinFactor = 0.62;
+  /** 相机观察方向(从前上方俯视),会归一化后乘以上面的距离 */
+  private cameraDir = { x: 0.18, y: 0.85, z: 1.35 };
+  /** 机顶气泡再往上的高度 */
+  private spriteTopOffset = 0.34;
+
+  constructor(model) {
+    this.model = model;
+    this.group.name = 'nitrogenFlex';
+  }
+
+  addLight() {
+    // 灯光按场景尺度推导,避免小场景过曝、大场景欠曝
+    const box = new THREE.Box3().setFromObject(this.group);
+    const size = box.getSize(new THREE.Vector3());
+    const center = box.getCenter(new THREE.Vector3());
+    const maxDim = Math.max(size.x, size.y, size.z) || 1;
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
+    directionalLight.position.set(center.x + maxDim * 0.6, center.y + maxDim * 1.2, center.z + maxDim * 0.8);
+    directionalLight.target = this.group;
+    this.group.add(directionalLight);
+
+    const pointLight = new THREE.PointLight(0xffffff, 1, maxDim * 3);
+    pointLight.position.set(center.x, center.y + maxDim * 0.9, center.z + maxDim * 0.5);
+    this.group.add(pointLight);
+  }
+
+  /** 加载基底与三台注氮机模型 */
+  mountedThree() {
+    return new Promise((resolve) => {
+      const names = [this.baseName, ...this.unitNames];
+      this.model
+        .setGLTFModel(names)
+        .then((gltf) => {
+          const base = gltf[0];
+          if (base) {
+            base.name = this.baseName;
+            this.group.add(base);
+          } else {
+            console.warn(`[nitrogen-flex] 基底模型 ${this.baseName} 加载失败`);
+          }
+
+          this.unitTemplates = [];
+          for (let i = 0; i < this.unitNames.length; i++) {
+            const unit = gltf[i + 1];
+            if (!unit) {
+              console.warn(`[nitrogen-flex] 注氮机模型 ${this.unitNames[i]} 加载失败`);
+              this.unitTemplates.push(null);
+              continue;
+            }
+            unit.name = 'nitrogenUnitTpl' + i;
+            this.unitTemplates.push(unit);
+          }
+
+          this.loaded = true;
+          resolve(null);
+        })
+        .catch((e) => {
+          console.error('[nitrogen-flex] 模型加载异常', e);
+          this.loaded = true;
+          resolve(null);
+        });
+    });
+  }
+
+  /**
+   * 探测「机体」在模型内的位置。
+   *
+   * 每个 ele 模型除了机体,还含一条从机体延伸到管汇的长管路,
+   * 整体包围盒的中心会被管路拉偏,因此不能直接用整体包围盒做气泡锚点。
+   * 这里按尺寸特征找机体主箱体:X 宽 1.0~2.0 且 Y 高 > 0.4 的网格。
+   */
+  private computeMachineAnchor(unit: THREE.Object3D) {
+    unit.updateMatrixWorld(true);
+
+    const meshes: THREE.Mesh[] = [];
+    unit.traverse((o) => {
+      if ((o as THREE.Mesh).isMesh) meshes.push(o as THREE.Mesh);
+    });
+
+    let bestBox: THREE.Box3 | undefined;
+    let bestWidth = -1;
+    for (const mesh of meshes) {
+      const box = new THREE.Box3().setFromObject(mesh);
+      if (box.isEmpty()) continue;
+      const s = box.getSize(new THREE.Vector3());
+      if (s.x > 1.0 && s.x < 2.0 && s.y > 0.4 && s.x > bestWidth) {
+        bestBox = box;
+        bestWidth = s.x;
+      }
+    }
+
+    if (bestBox) {
+      const center = bestBox.getCenter(new THREE.Vector3());
+      return { x: center.x, y: bestBox.max.y, z: center.z };
+    }
+
+    // 回退:整体包围盒中心(仅在识别失败时使用)
+    const box = new THREE.Box3().setFromObject(unit);
+    const center = box.getCenter(new THREE.Vector3());
+    return { x: center.x, y: box.max.y, z: center.z };
+  }
+
+  /**
+   * 相机参数:按「包围球 + 相机 FOV + 画面宽高比」推导取景距离。
+   *
+   * 本场景宽而扁(约 8.58 × 1.12 × 1.78),横向才是真正的约束,
+   * 因此以「宽度刚好占满画面 WIDTH_FILL 比例」定距离;
+   * 再设一个包围球拟合的下限,避免超宽屏时相机贴得过近导致上下被裁。
+   *
+   * 相比写死坐标,这样在不同窗口尺寸下取景一致,不会屏幕一宽模型就变小。
+   */
+  getCameraConfig() {
+    const box = new THREE.Box3().setFromObject(this.group);
+    const sphere = box.getBoundingSphere(new THREE.Sphere());
+    const center = sphere.center;
+    const radius = sphere.radius || 1;
+    const size = box.getSize(new THREE.Vector3());
+
+    const cam = this.model?.camera;
+    const vFov = (((cam?.fov as number) ?? 50) * Math.PI) / 180;
+    const aspect = (cam?.aspect as number) || 1.8;
+    const hFov = 2 * Math.atan(Math.tan(vFov / 2) * aspect);
+
+    // 按宽度装下
+    const dByWidth = size.x / 2 / Math.tan(hFov / 2) / this.cameraWidthFill;
+    // 下限:包围球拟合再乘系数,保证纵向也不会被裁
+    const dFloor = (radius / Math.sin(Math.min(vFov, hFov) / 2)) * this.cameraMinFactor;
+    const distance = Math.max(dByWidth, dFloor);
+
+    const dir = new THREE.Vector3(this.cameraDir.x, this.cameraDir.y, this.cameraDir.z).normalize();
+    const pos = center.clone().add(dir.multiplyScalar(distance));
+
+    return {
+      camera: { x: pos.x, y: pos.y, z: pos.z },
+      target: { x: center.x, y: center.y, z: center.z },
+    };
+  }
+
+  /**
+   * 设置机数并重建机体。
+   * 模型为原位摆放:克隆后不做任何缩放/位移,机位与管路对接由模型自身保证。
+   */
+  setUnitCount(num: number) {
+    if (!this.loaded) return;
+    const count = Math.max(0, Math.min(this.unitNames.length, Number(num) || 0));
+    this.nitrogenNum = count;
+
+    this.clearCssText();
+    this.unitGroups.forEach((u) => this.group.remove(u));
+    this.unitGroups = [];
+
+    for (let i = 0; i < count; i++) {
+      const tpl = this.unitTemplates[i];
+      if (!tpl) {
+        console.warn(`[nitrogen-flex] 第 ${i + 1} 台注氮机模型缺失,跳过`);
+        continue;
+      }
+      const unit = tpl.clone(true);
+      unit.name = 'nitrogenUnit' + i;
+      // 原位摆放:保持模型内置机位,杜绝错位与管路脱开
+      unit.position.set(0, 0, 0);
+      unit.scale.set(1, 1, 1);
+      this.group.add(unit);
+      this.unitGroups.push(unit);
+    }
+
+    // 整体垂直/深度微调(默认 0,即模型设计位置)
+    if (this.unitLiftY || this.unitOffsetZ) {
+      this.unitGroups.forEach((u) => {
+        u.position.set(0, this.unitLiftY, this.unitOffsetZ);
+      });
+    }
+
+    setModalCenter(this.group);
+    this.addCssText();
+  }
+
+  /** 挂载数据气泡:三台机的监测框 + 下风侧监测框 */
+  addCssText = () => {
+    this.unitGroups.forEach((unit, i) => {
+      if (!unit) return;
+      if (unit.getObjectByName('monitorNitrogenText')) return;
+      const element = document.getElementById('nitrogenMonitor' + (i + 1)) as HTMLElement;
+      if (!element) return;
+
+      const anchor = this.computeMachineAnchor(unit);
+      const sprite = new CSS3DSprite(element);
+      sprite.name = 'monitorNitrogenText';
+      // CSS3DObject 构造会把元素设为 pointerEvents:'auto',会遮挡轨道控制器拖拽
+      element.style.pointerEvents = 'none';
+      sprite.scale.set(this.spriteScale, this.spriteScale, this.spriteScale);
+      // 气泡挂在机顶上方(机体位置由模型内部布局决定,故按探测结果定位)
+      sprite.position.set(anchor.x, anchor.y + this.spriteTopOffset, anchor.z);
+      unit.add(sprite);
+      this.cssSprites.push(sprite);
+    });
+
+    this.attachDownWindMonitor();
+  };
+
+  /**
+   * 挂载「下风侧监测」框。
+   *
+   * 若不显式挂载,该元素会以未定位状态留在 DOM 中,表现为跑到页面左上角。
+   * 这里按基底包围盒归一化锚点定位,默认放在管路的下风侧一端。
+   */
+  private attachDownWindMonitor() {
+    if (this.group.getObjectByName('downWindMonitor')) return;
+    const element = document.getElementById('downWindMonitor') as HTMLElement;
+    if (!element) return;
+
+    const base = this.group.getObjectByName(this.baseName);
+    if (!base) return;
+    const box = new THREE.Box3().setFromObject(base);
+    const { nx, ny, nz } = this.downWindAnchor;
+    const x = box.min.x + (box.max.x - box.min.x) * nx;
+    const y = box.min.y + (box.max.y - box.min.y) * ny;
+    const z = box.min.z + (box.max.z - box.min.z) * nz;
+
+    const sprite = new CSS3DSprite(element);
+    sprite.name = 'downWindMonitor';
+    element.style.pointerEvents = 'none';
+    sprite.scale.set(this.spriteScale, this.spriteScale, this.spriteScale);
+    sprite.position.set(x, y, z);
+    // 挂在 group 上而非某台机体,避免机体变换影响该框
+    this.group.add(sprite);
+    this.cssSprites.push(sprite);
+  }
+
+  clearCssText = () => {
+    this.cssSprites.forEach((s) => {
+      s.parent?.remove(s);
+    });
+    this.cssSprites = [];
+    // 兜底清理可能残留的精灵节点
+    this.unitGroups.forEach((u) => {
+      const old = u.getObjectByName('monitorNitrogenText');
+      if (old) u.remove(old);
+    });
+    const dw = this.group.getObjectByName('downWindMonitor');
+    if (dw) this.group.remove(dw);
+  };
+
+  /** 与既有类保持一致的接口(本模型无需额外处理) */
+  setModalPosition() {}
+
+  resetModel() {}
+
+  /**
+   * 布局/取景微调入口(默认值即当前效果,通常无需调整)。
+   * 仅在实物效果需要微调时使用,避免写死进结构里。
+   */
+  setLayoutTuning(opts: {
+    unitLiftY?: number;
+    unitOffsetZ?: number;
+    downWindAnchor?: { nx: number; ny: number; nz: number };
+    cameraWidthFill?: number;
+    spriteTopOffset?: number;
+  }) {
+    if (opts.unitLiftY !== undefined) this.unitLiftY = opts.unitLiftY;
+    if (opts.unitOffsetZ !== undefined) this.unitOffsetZ = opts.unitOffsetZ;
+    if (opts.downWindAnchor !== undefined) this.downWindAnchor = opts.downWindAnchor;
+    // 取景与气泡高度是纯渲染参数,改完直接重排即可生效
+    if (opts.cameraWidthFill !== undefined) this.cameraWidthFill = opts.cameraWidthFill;
+    if (opts.spriteTopOffset !== undefined) this.spriteTopOffset = opts.spriteTopOffset;
+    this.setUnitCount(this.nitrogenNum);
+  }
+
+  destroy() {
+    if (this.model) {
+      this.clearCssText();
+      this.model.clearGroup(this.group);
+      this.unitTemplates = [];
+      this.unitGroups = [];
+      this.model = null;
+    }
+  }
+}
+
+export default NitrogenFlex;

+ 33 - 0
src/views/vent/monitorManager/compressor/nitrogen.threejs.ts

@@ -3,6 +3,7 @@ import { animateCamera, setModalCenter, updateAxisCenter } from '/@/utils/threej
 import UseThree from '../../../../utils/threejs/useThree';
 import UseThree from '../../../../utils/threejs/useThree';
 import NitrogenOverground from './nitrogen.dishang.threejs';
 import NitrogenOverground from './nitrogen.dishang.threejs';
 import NitrogenUnderground from './nitrogen.dixia.threejs';
 import NitrogenUnderground from './nitrogen.dixia.threejs';
+import NitrogenFlex from './nitrogen.flex.threejs';
 import useEvent from '../../../../utils/threejs/useEvent';
 import useEvent from '../../../../utils/threejs/useEvent';
 // import * as dat from 'dat.gui';
 // import * as dat from 'dat.gui';
 // const gui = new dat.GUI();
 // const gui = new dat.GUI();
@@ -13,6 +14,7 @@ let model: UseThree | undefined, //
   group: THREE.Object3D | undefined,
   group: THREE.Object3D | undefined,
   nitrogenObj: NitrogenOverground | undefined,
   nitrogenObj: NitrogenOverground | undefined,
   nitrogenUndergroundObj: NitrogenUnderground | undefined,
   nitrogenUndergroundObj: NitrogenUnderground | undefined,
+  nitrogenFlexObj: NitrogenFlex | undefined,
   modalType = 'nitrogen';
   modalType = 'nitrogen';
 
 
 const { mouseDownFn } = useEvent();
 const { mouseDownFn } = useEvent();
@@ -127,6 +129,30 @@ export const setModelType = (type, nitrogenNum?) => {
         resolve(null);
         resolve(null);
         await animateCamera(oldCameraPosition, oldControlsPosition, newCameraPosition, newControlsPosition, model, 0.8);
         await animateCamera(oldCameraPosition, oldControlsPosition, newCameraPosition, newControlsPosition, model, 0.8);
       }, 300);
       }, 300);
+    } else if (modalType === 'nitrogenFlex' && nitrogenFlexObj && nitrogenFlexObj.group) {
+      // 地下管路模型(基底 + 注氮机),与地上模型、地下管网模型三者共存,互不影响
+      if (model?.scene?.getObjectByName('nitrogen') && nitrogenObj && nitrogenObj.group) {
+        model.scene.remove(nitrogenObj.group);
+        nitrogenObj.clearCssText();
+      }
+      if (model?.scene?.getObjectByName('nitrogenUnderground') && nitrogenUndergroundObj && nitrogenUndergroundObj.group) {
+        model.scene.remove(nitrogenUndergroundObj.group);
+        nitrogenUndergroundObj.clearCssText();
+      }
+
+      // 机数由后端决定(1~3 台),动态增减
+      nitrogenFlexObj.setUnitCount(nitrogenNum ?? 3);
+
+      group = nitrogenFlexObj.group;
+      const oldCameraPosition = { x: -1000, y: 100, z: 500 };
+      const oldControlsPosition = { x: -10, y: 10, z: 10 };
+      const { camera, target } = nitrogenFlexObj.getCameraConfig();
+
+      setTimeout(async () => {
+        model?.scene?.add(group as THREE.Object3D);
+        resolve(null);
+        await animateCamera(oldCameraPosition, oldControlsPosition, camera, target, model, 0.8);
+      }, 300);
     }
     }
   });
   });
 };
 };
@@ -143,6 +169,11 @@ export const mountedThree = () => {
 
 
     nitrogenUndergroundObj = new NitrogenUnderground(model);
     nitrogenUndergroundObj = new NitrogenUnderground(model);
     await nitrogenUndergroundObj.mountedThree();
     await nitrogenUndergroundObj.mountedThree();
+
+    // 地下管路模型(基底 + 注氮机),作为新增视图与上面两个共存
+    nitrogenFlexObj = new NitrogenFlex(model);
+    await nitrogenFlexObj.mountedThree();
+
     setModelType('nitrogenUnderground');
     setModelType('nitrogenUnderground');
     resolve(null);
     resolve(null);
     model.animate();
     model.animate();
@@ -157,6 +188,8 @@ export const destroy = () => {
     nitrogenObj = undefined;
     nitrogenObj = undefined;
     nitrogenUndergroundObj?.destroy();
     nitrogenUndergroundObj?.destroy();
     nitrogenUndergroundObj = undefined;
     nitrogenUndergroundObj = undefined;
+    nitrogenFlexObj?.destroy();
+    nitrogenFlexObj = undefined;
     group = undefined;
     group = undefined;
     model.destroy();
     model.destroy();
     model = undefined;
     model = undefined;