import * as THREE from 'three'; // import { setModalCenter } from '/@/utils/threejs/util'; import Smoke from '../../comment/threejs/Smoke'; import { CSS3DObject } from 'three/examples/jsm/renderers/CSS3DRenderer'; import { get } from 'lodash-es'; import { getTextCanvas } from '/@/utils/threejs/util'; // import * as dat from 'dat.gui'; // const gui = new dat.GUI(); // gui.domElement.style = 'position:absolute;top:100px;left:10px;z-index:99999999999999'; class ModelContext { model; // modelName = 'jbfj-hd'; modelName = 'jbfj-dual'; /** 本模型的根3D对象 */ group?: THREE.Object3D; /** 本模型所包含的所有元素合集 */ private elements: unknown[] = []; /** 本模型支持的 Object3DGroup 模块 */ private modules: { /** 模块名称 */ name: string; /** 控制该模块所用的上下文 */ context: THREE.Object3D; /** 控制时行为声明 */ behavior: (context: THREE.Object3D) => void; }[] = []; constructor(model) { this.model = model; } addLight() { // optional implementation } /** * 设置模型类型并切换,不同的类型通常对应不同的具体模型,在模型总控制器下的具体模型会根据传入的参数彼此交互、切换 * * 本模型分为外侧(右侧)风机、内侧(左侧)风机,用户选择一个风机后,详情参数的框需要高亮,风机之间需要联动形成风流 * * @param data 风机数据,第一项应为外侧(右侧)风机,第二项为内侧(左侧) */ setModelType(modelType: 'inner' | 'outer' | string, data: any[]) { const fanOuter1Run = get(data[0], 'Fan1StartStatus', '0') == '1'; const fanInner1Run = get(data[1], 'Fan1StartStatus', '0') == '1'; if (modelType === 'inner') { this.execute('fanLeftStrong'); } if (modelType === 'outer') { this.execute('fanRightStrong'); } if (fanOuter1Run && fanInner1Run) { this.execute('fan1RightOpen&fan1LeftOpen'); } if (fanOuter1Run && !fanInner1Run) { this.execute('fan1RightOpen&fan2LeftOpen'); } if (!fanOuter1Run && fanInner1Run) { this.execute('fan2RightOpen&fan1LeftOpen'); } if (!fanOuter1Run && !fanInner1Run) { this.execute('fan2RightOpen&fan2LeftOpen'); } } private execute(cmdname: string) { this.modules.forEach(({ name, context, behavior }) => { if (name === cmdname) { behavior(context); } }); } mountedThree() { return new Promise((resolve) => { this.model.setGLTFModel([this.modelName]).then(async (gltf) => { this.group = gltf[0]; if (this.group) { // 将管道由黑色不透光的材质修改为半透明材质 const material = new THREE.MeshBasicMaterial({ color: '#000', transparent: true, opacity: 0.3, side: THREE.DoubleSide, // 这里是双面渲染的意思 }); [ this.group.getObjectByName('Cylinder1054'), this.group.getObjectByName('BuErTaiJuBuFengJi_shupailie_baisezitiCylinder1054'), this.group.getObjectByName('pCylinder1'), ].forEach((e: THREE.Mesh) => { e.material = material; // e.renderOrder = 300; }); // this.group.scale.set(2, 2, 2); // setModalCenter(this.group); this.addLight(); this.setModelPosition(); this.initModules().then(resolve); } }); }); } destroy() { if (!this.model) return; this.elements.forEach((element) => { this.model.clearGroup(element); }); } // 设置模型位置 setModelPosition() { if (!this.group) return; this.group.scale.set(0.6, 0.6, 0.6); // const ff = gui.addFolder(`位置调整`); // ff.add(this.group.position, 'x', -100, 100); // ff.add(this.group.position, 'y', -100, 100); // ff.add(this.group.position, 'z', -100, 100); this.group.position.set(0, 0, -60); this.group.rotation.y = Math.PI / 2; } // hideElements(eles: THREE.Object3D[]) { // eles.forEach((g) => { // g.visible = false; // }); // } // showElements(eles: THREE.Object3D[]) { // eles.forEach((g) => { // g.visible = true; // }); // } weakElements(eles: unknown[]) { eles.forEach((g) => { if (g instanceof Smoke) { g.oldOpacityFactor = 0.4; } if (g instanceof CSS3DObject) { g.element.style.setProperty('opacity', '0.3'); } }); } strongElements(eles: unknown[]) { eles.forEach((g) => { if (g instanceof Smoke) { g.oldOpacityFactor = 0.75; } if (g instanceof CSS3DObject) { g.element.style.setProperty('opacity', '1'); } }); } startAnimation(eles: unknown[]) { eles.forEach((g) => { if (g instanceof Smoke) { g.startSmoke(); } }); } stopAnimation(eles: unknown[]) { const smokes = eles.filter((g) => { return g instanceof Smoke; }); return Promise.all(smokes.map((e) => e.stopSmoke())); } /** 核心方法,初始化本模型的各个模块,这些模块可以实现特定场景的展示、控制等功能 */ async initModules() { if (this.elements.length > 0) return; // 右侧风机-主风机进风 const curveFan1Right = this.generateSmokePath( [ new THREE.Vector3(-85.69, 2.18, 51.89), new THREE.Vector3(-85.69, 2.18, 41.89), new THREE.Vector3(-85.69, 2.18, 35.32), new THREE.Vector3(-85.69, 0.78, 33.08), new THREE.Vector3(-85.69, 0.78, 27.84), new THREE.Vector3(-85.69, 4.72, 21.56), new THREE.Vector3(-85.69, 4.72, -13), new THREE.Vector3(-26.2, 4.72, -13.24), new THREE.Vector3(-25.61, 4.72, -47.03), new THREE.Vector3(80.03, 4.72, -47.03), ], true ); // 右侧风机-备风机进风 const curveFan2Right = this.generateSmokePath( [ new THREE.Vector3(-85.69, -0.53, 51.89), new THREE.Vector3(-85.69, -0.53, 41.89), new THREE.Vector3(-85.69, -0.51, 35.32), new THREE.Vector3(-85.69, 0.78, 33.08), new THREE.Vector3(-85.69, 0.78, 27.84), new THREE.Vector3(-85.69, 4.72, 21.56), new THREE.Vector3(-85.69, 4.72, -13), new THREE.Vector3(-26.2, 4.72, -13.24), new THREE.Vector3(-25.61, 4.72, -47.03), new THREE.Vector3(80.03, 4.72, -47.03), ], true ); // 左侧风机-主风机进风 const curveFan1Left = this.generateSmokePath( [ new THREE.Vector3(-85.69, 2.18, 12.72), new THREE.Vector3(-85.69, 2.18, 2.72), new THREE.Vector3(-85.69, 2.18, -3.85), new THREE.Vector3(-85.69, 0.78, -6.09), new THREE.Vector3(-85.69, 0.78, -12.92), new THREE.Vector3(80.25, 0.78, -12.92), ], true ); // 左侧风机-备风机进风 const curveFan2Left = this.generateSmokePath( [ new THREE.Vector3(-85.69, -0.53, 12.72), new THREE.Vector3(-85.69, -0.53, 2.72), new THREE.Vector3(-85.69, -0.51, -3.85), new THREE.Vector3(-85.69, 0.78, -6.09), new THREE.Vector3(-85.69, 0.78, -12.92), new THREE.Vector3(80.25, 0.78, -12.92), ], true ); // 右侧巷道-回风前段 const curveTunnelRight = this.generateSmokePath([ new THREE.Vector3(86.67, 0.78, -16.57), new THREE.Vector3(30.11, 0.78, -16.57), new THREE.Vector3(30.11, 0.78, -50.39), ]); // 左侧巷道-回风前段 const curveTunnelLeft = this.generateSmokePath([new THREE.Vector3(30.11, 0.78, -50.39), new THREE.Vector3(-72.58, 0.78, -50.17)]); // 左侧巷道-回风全长 const curveTunnelMajor = this.generateSmokePath([new THREE.Vector3(86.55, 0.78, -50.39), new THREE.Vector3(-72.58, 0.78, -50.17)]); const group1 = new THREE.Group(); const smokeFan1Right = new Smoke('/model/img/texture-smoke.png', '#ffffff', 1, 0.75, 0.5, 600); smokeFan1Right.setPath(curveFan1Right); this.elements.push(smokeFan1Right); const smokeFan2Right = new Smoke('/model/img/texture-smoke.png', '#ffffff', 10, 0.75, 0.5, 400); smokeFan2Right.setPath(curveFan2Right); this.elements.push(smokeFan2Right); const smokeFan1Left = new Smoke('/model/img/texture-smoke.png', '#ffffff', 10, 0.75, 0.5, 600); smokeFan1Left.setPath(curveFan1Left); this.elements.push(smokeFan1Left); const smokeFan2Left = new Smoke('/model/img/texture-smoke.png', '#ffffff', 10, 0.75, 0.5, 400); smokeFan2Left.setPath(curveFan2Left); this.elements.push(smokeFan2Left); const smokeTunnelRight = new Smoke('/model/img/texture-smoke.png', '#ffffff', 0, 0.35, 1.5, 150); smokeTunnelRight.setPath(curveTunnelRight); this.elements.push(smokeTunnelRight); const smokeTunnelLeft = new Smoke('/model/img/texture-smoke.png', '#ffffff', 0, 0.35, 1.5, 150); smokeTunnelLeft.setPath(curveTunnelLeft); this.elements.push(smokeTunnelLeft); const smokeTunnelMajor = new Smoke('/model/img/texture-smoke.png', '#ffffff', 0, 0.35, 1.5, 150); smokeTunnelMajor.setPath(curveTunnelMajor); this.elements.push(smokeTunnelMajor); await smokeFan1Right.setPoints(); this.group?.add(smokeFan1Right.points); await smokeFan2Right.setPoints(); this.group?.add(smokeFan2Right.points); await smokeFan1Left.setPoints(); this.group?.add(smokeFan1Left.points); await smokeFan2Left.setPoints(); this.group?.add(smokeFan2Left.points); await smokeTunnelRight.setPoints(); this.group?.add(smokeTunnelRight.points); await smokeTunnelLeft.setPoints(); this.group?.add(smokeTunnelLeft.points); await smokeTunnelMajor.setPoints(); this.group?.add(smokeTunnelMajor.points); const fanLeftSelectors = [ { query: '#inputBox2', position: [-85, 8, -16], scale: 0.1, }, { query: '#T1_1', position: [93, 18, -65], scale: 0.2, }, { query: '#T1_2', position: [35, 16, -59], scale: 0.175, }, ]; const fanRightSelectors = [ { query: '#inputBox3', position: [-85, 8, 24], scale: 0.1, }, { query: '#T2_1', position: [93, 18, -98], scale: 0.2, }, { query: '#T2_2', position: [35, 16, -92], scale: 0.175, }, ]; const commonSelectors = [ { query: '#T3', position: [-26, 14, -86], scale: 0.15, }, ]; const fanLeftSprites = this.initCssElement(fanLeftSelectors); const fanRightSprites = this.initCssElement(fanRightSelectors); const commonSprites = this.initCssElement(commonSelectors); // 双巷道的通风机都开启有4种情况 this.modules.push({ name: 'fan1RightOpen&fan1LeftOpen', context: group1, behavior: () => { // this.weakElements(this.elements); this.stopAnimation(this.elements).then(() => { // this.strongElements(); this.startAnimation([smokeFan1Right, smokeFan1Left, smokeTunnelRight, smokeTunnelMajor]); }); }, }); this.modules.push({ name: 'fan2RightOpen&fan1LeftOpen', context: group1, behavior: () => { this.stopAnimation(this.elements).then(() => { this.startAnimation([smokeFan2Right, smokeFan1Left, smokeTunnelRight, smokeTunnelMajor]); }); }, }); this.modules.push({ name: 'fan1RightOpen&fan2LeftOpen', context: group1, behavior: () => { this.stopAnimation(this.elements).then(() => { this.startAnimation([smokeFan1Right, smokeFan2Left, smokeTunnelRight, smokeTunnelMajor]); }); }, }); this.modules.push({ name: 'fan2RightOpen&fan2LeftOpen', context: group1, behavior: () => { this.stopAnimation(this.elements).then(() => { this.startAnimation([smokeFan2Right, smokeFan2Left, smokeTunnelRight, smokeTunnelMajor]); }); }, }); // 只有一个风机启动有4种情况 this.modules.push({ name: 'fan1RightOpen', context: group1, behavior: () => { this.stopAnimation(this.elements).then(() => { this.startAnimation([smokeFan1Right, smokeTunnelMajor]); }); }, }); this.modules.push({ name: 'fan2RightOpen', context: group1, behavior: () => { this.stopAnimation(this.elements).then(() => { this.startAnimation([smokeFan2Right, smokeTunnelMajor]); }); }, }); this.modules.push({ name: 'fan1LeftOpen', context: group1, behavior: () => { this.stopAnimation(this.elements).then(() => { this.startAnimation([smokeFan1Left, smokeTunnelRight, smokeTunnelLeft]); }); }, }); this.modules.push({ name: 'fan2LeftOpen', context: group1, behavior: () => { this.stopAnimation(this.elements).then(() => { this.startAnimation([smokeFan2Left, smokeTunnelRight, smokeTunnelLeft]); }); }, }); // 只有一个风机启动有2种告示牌情况 this.modules.push({ name: 'fanLeftStrong', context: group1, behavior: () => { this.weakElements(this.elements); this.strongElements([...fanLeftSprites, ...commonSprites]); }, }); this.modules.push({ name: 'fanRightStrong', context: group1, behavior: () => { this.weakElements(this.elements); this.strongElements([...fanRightSprites, ...commonSprites]); }, }); } /** 初始化css元素,将css元素选择器传入,该方法会将这些元素按顺序放入传入的锚点中 */ initCssElement(selectors: { query: string; position: number[]; scale: number }[]) { const arr: CSS3DObject[] = []; selectors.forEach(({ query, position, scale }) => { const element = document.querySelector(query) as HTMLElement; if (element) { const css3D = new CSS3DObject(element); this.elements.push(css3D); arr.push(css3D); css3D.name = query; css3D.scale.set(scale, scale, scale); css3D.rotation.y = -Math.PI / 2; css3D.position.set(position[0], position[1], position[2]); this.group?.add(css3D); } }); return arr; } /** 生成适用于 Smoke 的曲线数据,输入途径点,输出路径,如果是进风类型,首个线段将有扩散效果,出风则是末尾线段有扩散效果 */ generateSmokePath(points: THREE.Vector3[], airIn?: boolean, airOut?: boolean) { if (!this.group) return; const result: any[] = []; for (let index = 1; index < points.length; index++) { const path0 = points[index - 1]; const path1 = points[index]; const path = { path0, path1, isSpread: false, spreadDirection: 0, // spreadRang: -10, }; if (airIn && index === 1) { // 首个线段需要扩散,由大变小 path.isSpread = true; path.spreadDirection = -1; } if (airOut && index === points.length - 1) { // 末个线段需要扩散,由小变大 path.isSpread = false; path.spreadDirection = 1; } if (!airIn && !airOut) { path.isSpread = false; path.spreadDirection = 2; } result.push(path); } return result; } /** 添加风机描述,右侧风机是arr的第一项,左侧风机是第二项 */ addText(arr) { const positions = [ [-84.79, 0.82, 20.3], [-84.79, 0.82, 7.6], ]; arr.forEach((e, i) => { this.addTextByData(e, positions[i], `text${i}`); }); } // 从 .fanLocal.threejs.base 复制 addTextByData(selectData, position, name) { if (!this.group) { return; } // @ts-ignore const screenDownText = get(VENT_PARAM, 'modalText', History_Type['type'] == 'remote' ? '国能神东煤炭集团监制' : '煤炭科学技术研究院有限公司研制'); const screenDownTextX = 80 - (screenDownText.length - 10) * 6; const textArr = [ { text: `智能局部通风机监测与控制系统`, font: 'normal 30px Arial', color: '#009900', strokeStyle: '#002200', x: 20, y: 108, }, { text: `供风距离(m):`, font: 'normal 30px Arial', color: '#009900', strokeStyle: '#002200', x: 0, y: 152, }, { text: `${ selectData.airSupplyDistence_merge ? selectData.airSupplyDistence_merge : selectData.fchimenylength ? selectData.fchimenylength : selectData.airSupplyDistence_merge ? selectData.airSupplyDistence_merge : '-' }`, font: 'normal 30px Arial', color: '#009900', strokeStyle: '#002200', x: 228, y: 152, }, { text: `风筒直径(mm): `, font: 'normal 30px Arial', color: '#009900', strokeStyle: '#002200', x: 0, y: 200, }, { text: ` ${selectData.fchimenydiamlimit ? selectData.fchimenydiamlimit : selectData.ductDiameter_merge ? selectData.ductDiameter_merge : '-'}`, font: 'normal 30px Arial', color: '#009900', strokeStyle: '#002200', x: 220, y: 200, }, { text: `故障诊断:`, font: 'normal 30px Arial', color: '#009900', strokeStyle: '#002200', x: 0, y: 245, }, { text: `${selectData.warnLevel_str ? selectData.warnLevel_str : '-'}`, font: 'normal 30px Arial', color: '#009900', strokeStyle: '#002200', x: 220, y: 245, }, { text: `型号功率:`, font: 'normal 30px Arial', color: '#009900', strokeStyle: '#002200', x: 0, y: 285, }, { text: `${selectData.model_Power_merge ? selectData.model_Power_merge : '-'}`, font: 'normal 26px Arial', color: '#009900', strokeStyle: '#002200', x: 220, y: 285, }, { text: screenDownText, font: 'normal 28px Arial', color: '#009900', strokeStyle: '#002200', x: screenDownTextX, y: 325, }, ]; getTextCanvas(526, 346, textArr, '').then((canvas: HTMLCanvasElement) => { const textMap = new THREE.CanvasTexture(canvas); // 关键一步 const textMaterial = new THREE.MeshBasicMaterial({ // 关于材质并未讲解 实操即可熟悉 这里是漫反射类似纸张的材质,对应的就有高光类似金属的材质. map: textMap, // 设置纹理贴图 transparent: true, side: THREE.FrontSide, // 这里是双面渲染的意思 }); textMaterial.blending = THREE.CustomBlending; const monitorPlane = this.group?.getObjectByName(name); if (monitorPlane) { // @ts-ignore-next-line monitorPlane.material = textMaterial; } else { const planeGeometry = new THREE.PlaneGeometry(526, 346); // 平面3维几何体PlaneGeometry const planeMesh = new THREE.Mesh(planeGeometry, textMaterial); planeMesh.name = name; planeMesh.scale.set(0.0135, 0.0135, 0.0135); planeMesh.rotation.y = -Math.PI / 2; planeMesh.position.set(position[0], position[1], position[2]); this.group?.add(planeMesh); } }); } } export default ModelContext;