fanLocalDual.threejs.base.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. import * as THREE from 'three';
  2. // import { setModalCenter } from '/@/utils/threejs/util';
  3. import Smoke from '../../comment/threejs/Smoke';
  4. import { CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer';
  5. import { get } from 'lodash-es';
  6. import { getTextCanvas } from '/@/utils/threejs/util';
  7. // import * as dat from 'dat.gui';
  8. // const gui = new dat.GUI();
  9. // gui.domElement.style = 'position:absolute;top:100px;left:10px;z-index:99999999999999';
  10. class ModelContext {
  11. model;
  12. // modelName = 'jbfj-hd';
  13. modelName = 'jbfj-dual';
  14. /** 本模型的根3D对象 */
  15. group?: THREE.Object3D;
  16. /** 本模型所包含的所有元素合集 */
  17. private elements: unknown[] = [];
  18. /** 本模型支持的 Object3DGroup 模块 */
  19. private modules: {
  20. /** 模块名称 */
  21. name: string;
  22. /** 控制该模块所用的上下文 */
  23. context: THREE.Object3D;
  24. /** 控制时行为声明 */
  25. behavior: (context: THREE.Object3D) => void;
  26. }[] = [];
  27. constructor(model) {
  28. this.model = model;
  29. }
  30. addLight() {
  31. // optional implementation
  32. }
  33. /** 设置模型类型并切换,不同的类型通常对应不同的具体模型,在模型总控制器下的具体模型会根据传入的参数彼此交互、切换 */
  34. setModelType(modelType: 'inner' | 'outer' | string, data: any[]) {
  35. const fanOuter1Run = get<string>(data[0], 'Fan1StartStatus', '0') == '1';
  36. const fanInner1Run = get<string>(data[1], 'Fan1StartStatus', '0') == '1';
  37. if (modelType === 'inner') {
  38. this.execute('fanLeftStrong');
  39. }
  40. if (modelType === 'outer') {
  41. this.execute('fanRightStrong');
  42. }
  43. if (fanOuter1Run && fanInner1Run) {
  44. this.execute('fan1RightOpen&fan1LeftOpen');
  45. }
  46. if (fanOuter1Run && !fanInner1Run) {
  47. this.execute('fan1RightOpen&fan2LeftOpen');
  48. }
  49. if (!fanOuter1Run && fanInner1Run) {
  50. this.execute('fan2RightOpen&fan1LeftOpen');
  51. }
  52. if (!fanOuter1Run && !fanInner1Run) {
  53. this.execute('fan2RightOpen&fan2LeftOpen');
  54. }
  55. }
  56. execute(cmdname: string) {
  57. this.modules.forEach(({ name, context, behavior }) => {
  58. if (name === cmdname) {
  59. behavior(context);
  60. }
  61. });
  62. }
  63. mountedThree() {
  64. return new Promise((resolve) => {
  65. this.model.setGLTFModel([this.modelName]).then(async (gltf) => {
  66. this.group = gltf[0];
  67. if (this.group) {
  68. // const material = new THREE.MeshBasicMaterial({
  69. // color: '#000',
  70. // transparent: true,
  71. // opacity: 0.3,
  72. // side: THREE.DoubleSide, // 这里是双面渲染的意思
  73. // });
  74. // [
  75. // this.group.getObjectByName('Cylinder1054'),
  76. // this.group.getObjectByName('BuErTaiJuBuFengJi_shupailie_baiseziti'),
  77. // this.group.getObjectByName('pCylinder1'),
  78. // ].forEach((e: THREE.Mesh) => {
  79. // e.material = material;
  80. // // e.renderOrder = 300;
  81. // });
  82. // this.group.scale.set(2, 2, 2);
  83. // setModalCenter(this.group);
  84. this.addLight();
  85. this.setModelPosition();
  86. this.initModules().then(resolve);
  87. }
  88. });
  89. });
  90. }
  91. destroy() {
  92. if (!this.model) return;
  93. this.elements.forEach((element) => {
  94. this.model.clearGroup(element);
  95. });
  96. }
  97. // 设置模型位置
  98. setModelPosition() {
  99. if (!this.group) return;
  100. this.group.scale.set(0.6, 0.6, 0.6);
  101. // const ff = gui.addFolder(`位置调整`);
  102. // ff.add(this.group.position, 'x', -100, 100);
  103. // ff.add(this.group.position, 'y', -100, 100);
  104. // ff.add(this.group.position, 'z', -100, 100);
  105. this.group.position.set(0, 0, -60);
  106. this.group.rotation.y = Math.PI / 2;
  107. }
  108. // hideElements(eles: THREE.Object3D[]) {
  109. // eles.forEach((g) => {
  110. // g.visible = false;
  111. // });
  112. // }
  113. // showElements(eles: THREE.Object3D[]) {
  114. // eles.forEach((g) => {
  115. // g.visible = true;
  116. // });
  117. // }
  118. weakElements(eles: unknown[]) {
  119. eles.forEach((g) => {
  120. if (g instanceof Smoke) {
  121. g.oldOpacityFactor = 0.4;
  122. }
  123. if (g instanceof CSS3DObject) {
  124. g.element.style.setProperty('opacity', '0.3');
  125. }
  126. });
  127. }
  128. strongElements(eles: unknown[]) {
  129. eles.forEach((g) => {
  130. if (g instanceof Smoke) {
  131. g.oldOpacityFactor = 0.75;
  132. }
  133. if (g instanceof CSS3DObject) {
  134. g.element.style.setProperty('opacity', '1');
  135. }
  136. });
  137. }
  138. startAnimation(eles: unknown[]) {
  139. eles.forEach((g) => {
  140. if (g instanceof Smoke) {
  141. g.startSmoke();
  142. }
  143. });
  144. }
  145. stopAnimation(eles: unknown[]) {
  146. const smokes = eles.filter((g) => {
  147. return g instanceof Smoke;
  148. });
  149. return Promise.all(smokes.map((e) => e.stopSmoke()));
  150. }
  151. /** 核心方法,初始化本模型的各个模块,这些模块可以实现特定场景的展示、控制等功能 */
  152. async initModules() {
  153. if (this.elements.length > 0) return;
  154. // 右侧风机-主风机进风
  155. const curveFan1Right = this.generateSmokePath(['dian', 'dian1', 'dian2', 'dian3', 'dian4', 'dian5', 'dian6', 'dian7', 'dian8'], true);
  156. // 右侧风机-备风机进风
  157. const curveFan2Right = this.generateSmokePath(['dian9', 'dian10', 'dian2', 'dian3', 'dian4', 'dian5', 'dian6', 'dian7', 'dian8'], true);
  158. // 左侧风机-主风机进风
  159. const curveFan1Left = this.generateSmokePath(['dian11', 'dian12', 'dian15', 'dian16', 'dian17'], true);
  160. // 左侧风机-备风机进风
  161. const curveFan2Left = this.generateSmokePath(['dian14', 'dian13', 'dian15', 'dian16', 'dian17'], true);
  162. // 右侧巷道-回风前段
  163. const curveTunnelRight = this.generateSmokePath(['dian18', 'dian19', 'dian20']);
  164. // 左侧巷道-回风前段
  165. const curveTunnelLeft = this.generateSmokePath(['dian20', 'dian22']);
  166. // 左侧巷道-回风全长
  167. const curveTunnelMajor = this.generateSmokePath(['dian21', 'dian22']);
  168. const group1 = new THREE.Group();
  169. const smokeFan1Right = new Smoke('/model/img/texture-smoke.png', '#ffffff', 10, 0.75, 0.5, 400);
  170. smokeFan1Right.setPath(curveFan1Right);
  171. this.elements.push(smokeFan1Right);
  172. const smokeFan2Right = new Smoke('/model/img/texture-smoke.png', '#ffffff', 10, 0.75, 0.5, 400);
  173. smokeFan2Right.setPath(curveFan2Right);
  174. this.elements.push(smokeFan2Right);
  175. const smokeFan1Left = new Smoke('/model/img/texture-smoke.png', '#ffffff', 10, 0.75, 0.5, 400);
  176. smokeFan1Left.setPath(curveFan1Left);
  177. this.elements.push(smokeFan1Left);
  178. const smokeFan2Left = new Smoke('/model/img/texture-smoke.png', '#ffffff', 10, 0.75, 0.5, 400);
  179. smokeFan2Left.setPath(curveFan2Left);
  180. this.elements.push(smokeFan2Left);
  181. const smokeTunnelRight = new Smoke('/model/img/texture-smoke.png', '#ffffff', 0, 0.35, 1.5, 200);
  182. smokeTunnelRight.setPath(curveTunnelRight);
  183. this.elements.push(smokeTunnelRight);
  184. const smokeTunnelLeft = new Smoke('/model/img/texture-smoke.png', '#ffffff', 0, 0.35, 1.5, 200);
  185. smokeTunnelLeft.setPath(curveTunnelLeft);
  186. this.elements.push(smokeTunnelLeft);
  187. const smokeTunnelMajor = new Smoke('/model/img/texture-smoke.png', '#ffffff', 0, 0.35, 1.5, 200);
  188. smokeTunnelMajor.setPath(curveTunnelMajor);
  189. this.elements.push(smokeTunnelMajor);
  190. await smokeFan1Right.setPoints();
  191. this.group?.add(smokeFan1Right.points);
  192. await smokeFan2Right.setPoints();
  193. this.group?.add(smokeFan2Right.points);
  194. await smokeFan1Left.setPoints();
  195. this.group?.add(smokeFan1Left.points);
  196. await smokeFan2Left.setPoints();
  197. this.group?.add(smokeFan2Left.points);
  198. await smokeTunnelRight.setPoints();
  199. this.group?.add(smokeTunnelRight.points);
  200. await smokeTunnelLeft.setPoints();
  201. this.group?.add(smokeTunnelLeft.points);
  202. await smokeTunnelMajor.setPoints();
  203. this.group?.add(smokeTunnelMajor.points);
  204. const fanLeftSelectors = [
  205. {
  206. query: '#inputBox2',
  207. position: [-85, 8, -16],
  208. scale: 0.1,
  209. },
  210. {
  211. query: '#T1_1',
  212. position: [93, 18, -65],
  213. scale: 0.2,
  214. },
  215. {
  216. query: '#T1_2',
  217. position: [35, 16, -59],
  218. scale: 0.175,
  219. },
  220. ];
  221. const fanRightSelectors = [
  222. {
  223. query: '#inputBox3',
  224. position: [-85, 8, 24],
  225. scale: 0.1,
  226. },
  227. {
  228. query: '#T2_1',
  229. position: [93, 18, -98],
  230. scale: 0.2,
  231. },
  232. {
  233. query: '#T2_2',
  234. position: [35, 16, -92],
  235. scale: 0.175,
  236. },
  237. ];
  238. const commonSelectors = [
  239. {
  240. query: '#T3',
  241. position: [-26, 14, -86],
  242. scale: 0.15,
  243. },
  244. ];
  245. const fanLeftSprites = this.initCssElement(fanLeftSelectors);
  246. const fanRightSprites = this.initCssElement(fanRightSelectors);
  247. const commonSprites = this.initCssElement(commonSelectors);
  248. // 双巷道的通风机都开启有4种情况
  249. this.modules.push({
  250. name: 'fan1RightOpen&fan1LeftOpen',
  251. context: group1,
  252. behavior: () => {
  253. // this.weakElements(this.elements);
  254. this.stopAnimation(this.elements).then(() => {
  255. // this.strongElements();
  256. this.startAnimation([smokeFan1Right, smokeFan1Left, smokeTunnelRight, smokeTunnelMajor]);
  257. });
  258. },
  259. });
  260. this.modules.push({
  261. name: 'fan2RightOpen&fan1LeftOpen',
  262. context: group1,
  263. behavior: () => {
  264. this.stopAnimation(this.elements).then(() => {
  265. this.startAnimation([smokeFan2Right, smokeFan1Left, smokeTunnelRight, smokeTunnelMajor]);
  266. });
  267. },
  268. });
  269. this.modules.push({
  270. name: 'fan1RightOpen&fan2LeftOpen',
  271. context: group1,
  272. behavior: () => {
  273. this.stopAnimation(this.elements).then(() => {
  274. this.startAnimation([smokeFan1Right, smokeFan2Left, smokeTunnelRight, smokeTunnelMajor]);
  275. });
  276. },
  277. });
  278. this.modules.push({
  279. name: 'fan2RightOpen&fan2LeftOpen',
  280. context: group1,
  281. behavior: () => {
  282. this.stopAnimation(this.elements).then(() => {
  283. this.startAnimation([smokeFan2Right, smokeFan2Left, smokeTunnelRight, smokeTunnelMajor]);
  284. });
  285. },
  286. });
  287. // 只有一个风机启动有4种情况
  288. this.modules.push({
  289. name: 'fan1RightOpen',
  290. context: group1,
  291. behavior: () => {
  292. this.stopAnimation(this.elements).then(() => {
  293. this.startAnimation([smokeFan1Right, smokeTunnelMajor]);
  294. });
  295. },
  296. });
  297. this.modules.push({
  298. name: 'fan2RightOpen',
  299. context: group1,
  300. behavior: () => {
  301. this.stopAnimation(this.elements).then(() => {
  302. this.startAnimation([smokeFan2Right, smokeTunnelMajor]);
  303. });
  304. },
  305. });
  306. this.modules.push({
  307. name: 'fan1LeftOpen',
  308. context: group1,
  309. behavior: () => {
  310. this.stopAnimation(this.elements).then(() => {
  311. this.startAnimation([smokeFan1Left, smokeTunnelRight, smokeTunnelLeft]);
  312. });
  313. },
  314. });
  315. this.modules.push({
  316. name: 'fan2LeftOpen',
  317. context: group1,
  318. behavior: () => {
  319. this.stopAnimation(this.elements).then(() => {
  320. this.startAnimation([smokeFan2Left, smokeTunnelRight, smokeTunnelLeft]);
  321. });
  322. },
  323. });
  324. // 只有一个风机启动有2种告示牌情况
  325. this.modules.push({
  326. name: 'fanLeftStrong',
  327. context: group1,
  328. behavior: () => {
  329. this.weakElements(this.elements);
  330. this.strongElements([...fanLeftSprites, ...commonSprites]);
  331. },
  332. });
  333. this.modules.push({
  334. name: 'fanRightStrong',
  335. context: group1,
  336. behavior: () => {
  337. this.weakElements(this.elements);
  338. this.strongElements([...fanRightSprites, ...commonSprites]);
  339. },
  340. });
  341. }
  342. /** 初始化css元素,将css元素选择器传入,该方法会将这些元素按顺序放入传入的锚点中 */
  343. initCssElement(selectors: { query: string; position: number[]; scale: number }[]) {
  344. const arr: CSS3DObject[] = [];
  345. selectors.forEach(({ query, position, scale }) => {
  346. const element = document.querySelector(query) as HTMLElement;
  347. if (element) {
  348. const css3D = new CSS3DObject(element);
  349. this.elements.push(css3D);
  350. arr.push(css3D);
  351. css3D.name = query;
  352. css3D.scale.set(scale, scale, scale);
  353. css3D.rotation.y = -Math.PI / 2;
  354. css3D.position.set(position[0], position[1], position[2]);
  355. this.group?.add(css3D);
  356. }
  357. });
  358. return arr;
  359. }
  360. /** 生成适用于 Smoke 的曲线数据,输入途径点,输出路径,如果是进风类型,首个线段将有扩散效果,出风则是末尾线段有扩散效果 */
  361. generateSmokePath(namelist: string[], airIn?: boolean, airOut?: boolean) {
  362. if (!this.group) return;
  363. const result: any[] = [];
  364. const points: THREE.Vector3[] = namelist.map((name) => {
  365. const obj = this.group?.getObjectByName(name);
  366. return obj?.position as THREE.Vector3;
  367. });
  368. for (let index = 1; index < points.length; index++) {
  369. const path0 = points[index - 1];
  370. const path1 = points[index];
  371. const path = {
  372. path0,
  373. path1,
  374. isSpread: false,
  375. spreadDirection: 0,
  376. spreadRang: -10,
  377. };
  378. if (airIn) {
  379. // 首个线段需要扩散,由大变小
  380. path.isSpread = index === 1;
  381. path.spreadDirection = -1;
  382. }
  383. if (airOut) {
  384. // 首个线段需要扩散,由小变大
  385. path.isSpread = index === points.length - 1;
  386. path.spreadDirection = 1;
  387. }
  388. if (!airIn && !airOut) {
  389. path.isSpread = false;
  390. path.spreadDirection = 1;
  391. }
  392. result.push(path);
  393. }
  394. return result;
  395. }
  396. /** 添加风机描述,右侧风机是arr的第一项,左侧风机是第二项 */
  397. addText(arr) {
  398. const positions = [
  399. [-84.79, 0.82, 20.3],
  400. [-84.79, 0.82, 7.6],
  401. ];
  402. arr.forEach((e, i) => {
  403. this.addTextByData(e, positions[i], `text${i}`);
  404. });
  405. }
  406. // 从 .fanLocal.threejs.base 复制
  407. addTextByData(selectData, position, name) {
  408. if (!this.group) {
  409. return;
  410. }
  411. // @ts-ignore
  412. const screenDownText = get(VENT_PARAM, 'modalText', History_Type['type'] == 'remote' ? '国能神东煤炭集团监制' : '煤炭科学技术研究院有限公司研制');
  413. const screenDownTextX = 80 - (screenDownText.length - 10) * 6;
  414. const textArr = [
  415. {
  416. text: `智能局部通风机监测与控制系统`,
  417. font: 'normal 30px Arial',
  418. color: '#009900',
  419. strokeStyle: '#002200',
  420. x: 20,
  421. y: 108,
  422. },
  423. {
  424. text: `供风距离(m):`,
  425. font: 'normal 30px Arial',
  426. color: '#009900',
  427. strokeStyle: '#002200',
  428. x: 0,
  429. y: 152,
  430. },
  431. {
  432. text: `${
  433. selectData.airSupplyDistence_merge
  434. ? selectData.airSupplyDistence_merge
  435. : selectData.fchimenylength
  436. ? selectData.fchimenylength
  437. : selectData.airSupplyDistence_merge
  438. ? selectData.airSupplyDistence_merge
  439. : '-'
  440. }`,
  441. font: 'normal 30px Arial',
  442. color: '#009900',
  443. strokeStyle: '#002200',
  444. x: 228,
  445. y: 152,
  446. },
  447. {
  448. text: `风筒直径(mm): `,
  449. font: 'normal 30px Arial',
  450. color: '#009900',
  451. strokeStyle: '#002200',
  452. x: 0,
  453. y: 200,
  454. },
  455. {
  456. text: ` ${selectData.fchimenydiamlimit ? selectData.fchimenydiamlimit : selectData.ductDiameter_merge ? selectData.ductDiameter_merge : '-'}`,
  457. font: 'normal 30px Arial',
  458. color: '#009900',
  459. strokeStyle: '#002200',
  460. x: 220,
  461. y: 200,
  462. },
  463. {
  464. text: `故障诊断:`,
  465. font: 'normal 30px Arial',
  466. color: '#009900',
  467. strokeStyle: '#002200',
  468. x: 0,
  469. y: 245,
  470. },
  471. {
  472. text: `${selectData.warnLevel_str ? selectData.warnLevel_str : '-'}`,
  473. font: 'normal 30px Arial',
  474. color: '#009900',
  475. strokeStyle: '#002200',
  476. x: 220,
  477. y: 245,
  478. },
  479. {
  480. text: `型号功率:`,
  481. font: 'normal 30px Arial',
  482. color: '#009900',
  483. strokeStyle: '#002200',
  484. x: 0,
  485. y: 285,
  486. },
  487. {
  488. text: `${selectData.model_Power_merge ? selectData.model_Power_merge : '-'}`,
  489. font: 'normal 26px Arial',
  490. color: '#009900',
  491. strokeStyle: '#002200',
  492. x: 220,
  493. y: 285,
  494. },
  495. {
  496. text: screenDownText,
  497. font: 'normal 28px Arial',
  498. color: '#009900',
  499. strokeStyle: '#002200',
  500. x: screenDownTextX,
  501. y: 325,
  502. },
  503. ];
  504. getTextCanvas(526, 346, textArr, '').then((canvas: HTMLCanvasElement) => {
  505. const textMap = new THREE.CanvasTexture(canvas); // 关键一步
  506. const textMaterial = new THREE.MeshBasicMaterial({
  507. // 关于材质并未讲解 实操即可熟悉 这里是漫反射类似纸张的材质,对应的就有高光类似金属的材质.
  508. map: textMap, // 设置纹理贴图
  509. transparent: true,
  510. side: THREE.FrontSide, // 这里是双面渲染的意思
  511. });
  512. textMaterial.blending = THREE.CustomBlending;
  513. const monitorPlane = this.group?.getObjectByName(name);
  514. if (monitorPlane) {
  515. // @ts-ignore-next-line
  516. monitorPlane.material = textMaterial;
  517. } else {
  518. const planeGeometry = new THREE.PlaneGeometry(526, 346); // 平面3维几何体PlaneGeometry
  519. const planeMesh = new THREE.Mesh(planeGeometry, textMaterial);
  520. planeMesh.name = name;
  521. planeMesh.scale.set(0.0135, 0.0135, 0.0135);
  522. planeMesh.rotation.y = -Math.PI / 2;
  523. planeMesh.position.set(position[0], position[1], position[2]);
  524. this.group?.add(planeMesh);
  525. }
  526. });
  527. }
  528. }
  529. export default ModelContext;