梁帽可视化大屏系统
东方电气成都研究院科研项目。
在生产车间,工人通过手持激光测距工具,对正在进行生产的风机叶片的边缘进行测距,同时将其测量的数据通过车间WIFI上报到内部服务器。
服务器通过MQTT将点位数据传递给前端web界面,前端基于基础数据,使用threejs进行模拟叶片和标准点位,得到一个参考的叶片形状。
同时用一条拟合的曲线和实际点位数据来表示正在进行生产的叶片的轮廓形状,并在超出参考值时给予错误的提示信息。
该项目不同于常规的OA项目,管理系统,或者常规的工控程序。本项目旨在数字化车间的同时,以更友好更方便的形式展示车间的生产过程和进度。让每一只叶片的点位数据都可以有迹可循,让每一次生产都更加直观。
提示
由于某些原因,仅提供简要文档。
若需要更加详细的流程,请联系相关负责人。
项目结构
结构如下:
📂梁帽可视化大屏系统
├─ 📂1、立项报告
│ ├─ 📄梁帽定位偏差数字化立项报告(第5版.docx
│ └─ 📄梁帽定位偏差数字化软件立项(第1版.pptx
├─ 📂2、UI设计稿
│ └─ 📄一些设计图-略过
├─ 📂3、设计方案
│ └─ 📄梁帽定位软件设计方案.docx
├─ 📂4、接口文档
│ ├─ 📄修改用户密码接口.png
│ ├─ 📄mqtt数据上报格式.png
│ ├─ 📄标准点位查询接口.png
│ ├─ 📄查询警告数量接口.png
│ ├─ 📄查询历史点位接口.png
│ ├─ 📄删除用户接口.png
│ └─ 📄修改用户密码接口.png
├─ 📂5、项目源代码
│ ├─ 📂安装包
│ ├─ 📂源码
│ └─ 📄git地址及说明.txt
├─ 📂6、软件著作
│ ├─ 📄操作说明.docx
│ ├─ 📂源码
│ └─ 📄软著申请表.docx
├─ 📂7、软件说明使用书
│ └─ 📄梁帽定位偏差数字化软件使用说明书.docx
├─ 📂8、相关资料图片
├─ 📂9、软件规格书
├─ 📂10、项目成果
└─ 📂11、结项报告
└─ 📄结项报告.pptx项目简介
项目的目标是设计一套在生产过程中,将叶片摆放的位置、以及具体点位角度的数据通过设备上报、后台进行记录、前端处理计算,将计算结果以及历史结果展示在大屏设备的包含配套设施的定制化软件。 基于上述,项目共分为软件和配套设施两大部分,其中软件包括前端程序,后台程序。而配套设施包含一台一体机。
基于以上需求完成了梁帽定位偏差数字化监控系统,该系统主要包括:
- 通过实时点位坐标动态绘制的实时数据。
- 查询历史点位坐标集绘制图形的历史数据。
- 用户管理
项目意义
项目结合工厂叶片实际生产流程,通过采集的点位数据,模拟计算出当前叶片的实际摆放位置以及大致轮廓,将叶片的部分生产流程实现了大屏化、数字化、可视化。
近年来,随着大屏化,可视化的项目日益火热,网络带宽的增长,硬件能力的提升也推动了技术的发展,如今在浏览器端也能很好的实现3D程序,显示大屏,图表等,而本项目的前端正是使用了3d的threejs库去构建了一个拟2d的程序,后台使用mqtt,两者技术的结合使得测量设备与图形可视化大屏的联通实现,并为同类型的大屏可视化项目积累了经验。
项目周期
设计时间为,2023.09 – 2024.11,实际上只用了3个月,后续的时间是为了避免后续有需求需要重新立项。
项目技术路线
✨后端使用go语言、go-zero微服务框架、Lua脚本开发的后台服务。
✨前端使用Vuejs框架,threejs库、electron库开发的桌面程序。
项目难点
难点:
- 手持设备的测量数据上报到数据库,后台需要
侦听数据库的变化并传递到前端。 - 前端利用
threeJS通过点位去绘制图形界面。
创新点:
- 为了实现UI效果,在网页上构建了一个3D程序,通过点线面在空间内创建集合体,通过坐标渲染图形。
解决方式:
- 后端通过lua脚本和监听数据库框架感知数据变化,通过mqtt订阅发布将数据传输给前端。
- 前端使用threejs库,完成了界面渲染。
项目说明
项目接口请求,入口文件,以及其它代码不予展示,这里仅展示核心threejs的构成文件,此文件为初始版本,核心功能包含图形的构建,点位的生成和渲染。
代码编写受限于当时开发者的薄弱功底,因此对该核心没有一个较好的封装。所以显得非常臃肿和难以阅读维护。
若你是维护者可以尝试使用AI进行重构。或者对实际正在使用项目核心进行重构,详细AI可以做的更好。😊
import * as THREE from 'three';
import { YEPIAN, BarPoints } from "./Yepian.js";
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { CSS3DRenderer, CSS3DObject } from "three/examples/jsm/renderers/CSS3DRenderer";
export const BorderYellow = new THREE.Color("rgba(255,187,0)"); // #ffbb00 黄色边框
export const FillYellow = new THREE.Color("rgba(255,187,0,0.4)"); // #ffbb00 - 0.4 黄色填充
export const BorderBlue = new THREE.Color("rgba(0,125,255)"); // #007dff 蓝色边框
export const FillBlue = new THREE.Color("rgba(0,159,255,0.2)"); // #008bff - 0.2 蓝色填充
export const CircleYellow = new THREE.Color("rgba(255,187,0)"); // #ffbb00 黄色圆
export const CircleBlue = new THREE.Color("rgba(0,125,255)"); // #007dff 蓝色圆
export const BackgroundColor = new THREE.Color("rgba(15,16,31)"); // #0f1011 叶片背景底色
export const LineRed = new THREE.Color("rgba(233,67,67)");
export const DirtyData = {};
export const initData = {};
class initThree {
/** 构造函数 */
constructor(name) {
this.name = name;
this.eleWidth = 100;
this.eleHeight = 100;
this.scene = null;
this.camera = null;
this.renderer = null;
this.controls = null;
this.labelRenderer = null;
}
/** 初始化之前需要先定义元素宽度和高度 */
BeforeInit(width, height) { this.eleWidth = width; this.eleHeight = height; }
/** 初始化threejs **/
async InitThree() {
await this.init()
await this.ShowYepian()
}
/** 初始化 **/
async init() {
await new Promise(res => {
// 场景-相机-渲染器
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(65,this.eleWidth/this.eleHeight,0.1,1000);
this.camera.position.set(55,-8,45);
this.camera.layers.enableAll();
this.renderer = new THREE.WebGLRenderer({antialias:true,alpha:true});
this.renderer.setSize(this.eleWidth,this.eleHeight);
this.renderer.setClearAlpha(0.2);
this.labelRenderer = new CSS3DRenderer();
this.controls = new OrbitControls(this.camera, this.labelRenderer.domElement);
this.controls.enableRotate = false;
this.controls.target.set(this.camera.position.x,this.camera.position.y,0);
this.controls.maxDistance = 200;
this.controls.minDistance = 3;
// 添加光源
const ambient = new THREE.AmbientLight(0xffffff, 10); // 光源
this.scene.add(ambient);
document.getElementById("threejs").appendChild(this.renderer.domElement);
// 文字相关
this.labelRenderer.setSize( this.eleWidth, this.eleHeight );
this.labelRenderer.domElement.style.position = 'absolute';
this.labelRenderer.domElement.style.top = '9%';
this.labelRenderer.domElement.style.left = '1%';
document.getElementById("renderjs").appendChild( this.labelRenderer.domElement );
// 初始化完成
res(true)
})
}
/** 叶片的边框加载,以及颜色的填充 */
async ShowYepian(){
await new Promise(re => {
// 接下来开始构建边框
const border_geometry = new THREE.BufferGeometry();
border_geometry.setAttribute('position',new THREE.BufferAttribute(YEPIAN,3));
const border_material = new THREE.LineBasicMaterial({ color: BorderBlue });
const border_yp = new THREE.Line(border_geometry, border_material);
border_yp.position.z = 0;
// 接下来开始构建多边形
const shape = new THREE.Shape();
for(let i = 0; i <= YEPIAN.length/3; i++){
if(i === 0) shape.moveTo(YEPIAN[i],YEPIAN[i+1])
else shape.lineTo(YEPIAN[i*3],YEPIAN[i*3+1])
}
const shape_geometry = new THREE.ShapeGeometry(shape);
const shape_material = new THREE.MeshBasicMaterial({ color: BackgroundColor, transparent:true, opacity: 0.57 });
const shape_mesh = new THREE.Mesh(shape_geometry, shape_material);
shape_mesh.position.z = -0.015;
// 将两个形状添加到场景中
this.scene.add(border_yp, shape_mesh);
re(true)
})
}
/** 新点位与边框图形,该方法主要是处理了标准点的点位数据 */
SDrawStrandardPointBorder(PointBorder) {
// 清楚之前的旧图形
let clearnBefore = (child)=>{
this.RemoveGeometry(child);
this.RemoveTxt('lm');
this.RemoveTxt('fl');
this.RemoveTxt('ud');
this.SRemoveTitle();
}
for(let $child of this.scene.children) {
if($child.name == '主梁帽') {
clearnBefore($child);
break;
}
}
for(let $child of this.scene.children) {
if($child.name == '副梁帽') {
clearnBefore($child);
}
}
for(let $child of this.scene.children) {
if($child.name == 'UD') {
clearnBefore($child);
}
}
// 处理新的标准点集,将数据构造成我需要的结构
let it = PointBorder;
if(PointBorder.main_beam) {
for(let mb of it.main_beam) { mb.standard_pos = { axisposi: mb.axisposi, angle: mb.angle } }
}
if(PointBorder.vice_beam) {
for(let vb of it.vice_beam) { vb.standard_pos = { axisposi: vb.axisposi, angle: vb.angle } }
}
if(PointBorder.ud) {
for(let ud of it.ud) { ud.standard_pos = { axisposi: ud.axisposi, angle: ud.angle } }
}
// 深度复制数据
let temp = JSON.stringify(PointBorder);
PointBorder = JSON.parse(temp);
// 将数据进行排序,通过下面代码之后,标准点位便在DirtyData上了
if(PointBorder.main_beam && PointBorder.main_beam.length > 2) {
DirtyData.lm = { Points: PointBorder.main_beam.sort((a,b) => a.pointnum - b.pointnum) };
} else {
DirtyData.lm = { Points: PointBorder.main_beam }
}
if(PointBorder.vice_beam && PointBorder.vice_beam.length > 2) {
DirtyData.fl = { Points: PointBorder.vice_beam.sort((a,b) => a.pointnum - b.pointnum) };
} else {
DirtyData.fl = { Points: PointBorder.vice_beam }
}
if(PointBorder.ud && PointBorder.ud.length > 2) {
DirtyData.ud = { Points: PointBorder.ud.sort((a,b) => a.pointnum - b.pointnum) };
} else {
DirtyData.ud = { Points: PointBorder.ud }
}
// 设置比例,105是程序长度,120是假定的实际长度
DirtyData.xScale = Number( (105/120).toFixed(5) );
// 预设置名称,图形角度(计算偏移量),标准高度,以及阈值
DirtyData.lm.name = "主梁帽";
DirtyData.fl.name = "副梁帽";
DirtyData.ud.name = "UD";
DirtyData.lm.rotate = 0;
DirtyData.fl.rotate = 0;
DirtyData.ud.rotate = 5;
DirtyData.lm.sh = 4;
DirtyData.fl.sh = 3.5;
DirtyData.ud.sh = 3;
DirtyData.yz = 5;
// 开始创建三个梁帽,在获取到标准点之后,就可以绘制出整个标准图形
const GroupZL = new THREE.Group();
GroupZL.name = "主梁帽";
GroupZL.position.y = -10;
const GroupFL = new THREE.Group();
GroupFL.name = "副梁帽";
GroupFL.position.y = -18;
const GroupUD = new THREE.Group();
GroupUD.name = "UD";
GroupUD.position.y = -30;
// 调用绘制方法,并将点位参数传入
this.SDrawPoint_Standard(DirtyData.lm, GroupZL);
this.SDrawPoint_Standard(DirtyData.fl, GroupFL);
this.SDrawPoint_Standard(DirtyData.ud, GroupUD);
}
/** 绘制标准点, 该方法主要是绘制标准点位色块儿 */
SDrawPoint_Standard(temp, Group) {
// 深度复制数据,数据给了prams
let prams = JSON.stringify(temp);
prams = JSON.parse(prams);
// 新增左端点
let left_point = prams.Points[0].standard_pos.axisposi;
left_point = parseFloat(left_point);
prams.Points.unshift({ pointnum:-1, standard_pos:{ axisposi:`${left_point-0.5}m`, angle:0 } });
// 新增右端点
let len = prams.Points.length;
let right_point = prams.Points[len-1].standard_pos.axisposi;
right_point = parseFloat(right_point);
prams.Points.push({ pointnum:999, standard_pos:{ axisposi:`${right_point+0.5}m`, angle:0 } })
// 提取数据,优化数据结构,并将点位结构添加到源数据上
let tempArray = prams.Points.map(item => {
return {
id: item.pointnum,
x: ( parseFloat(item.standard_pos.axisposi)*DirtyData.xScale ).toFixed(5)*1,
y: this.SComputedHeiByAngel(temp.rotate, parseFloat(item.standard_pos.axisposi)),
z: 0,
axisposi: item.axisposi,
angle: item.standard_pos.angle
}
})
temp.Points = tempArray;
// 开始画点
let $ps = this.SDrawStandardPonit(temp.Points);
Group.add($ps);
// 开始画线 与 边框
let $border_line_peanl = this.SDrawLineByStand(temp);
Group.add($border_line_peanl);
// 开始画阈值红线
let $redLine = this.SDrawTwoRedLine(temp);
Group.add($redLine);
// 添加省略
let $shenlue = this.SDrawWave(temp);
Group.add($shenlue);
// 添加文字
let $txt = this.SDrawText(temp);
Group.add($txt);
// 将内容添加到场景
this.scene.add(Group);
}
/** 根据角度值去计算高度 */
SComputedHeiByAngel(angel, width) {
if(angel == 0) return 0
else {
let result = Math.tan(Math.PI/180*angel)*width;
return Number(result.toFixed(6));
}
}
/** 绘制标准点 */
SDrawStandardPonit(arr) {
const StandardGroup = new THREE.Group(); // 绘制标准点位
StandardGroup.name = "标准点集合"; // 名字
for(let i in arr) {
if(i == 0 || i == arr.length-1) continue
else p(i, arr, StandardGroup, CircleBlue);
}
/** 下标, 数组,组,点颜色 */
function p(i, arr, group, color) {
let inSideCircle = new THREE.CircleGeometry(0.15,24); // 内圆
let inSideColor = new THREE.MeshPhongMaterial({ color: color }); // 内圆颜色
let outSideCircle = new THREE.CircleGeometry(0.20,24); // 外圆
let outSideColor = new THREE.MeshPhongMaterial({ color: 0xeeeeee }); // 外圆颜色(固定为白色)
let INCIRCLE = new THREE.Mesh(inSideCircle, inSideColor); // 生成一个内圆
let OUTCIRCLE = new THREE.Mesh(outSideCircle, outSideColor); // 生成一个外圆
OUTCIRCLE.position.z = 0.025; // 外圆的层级要比内圆要低,这样内圆才能显示在上面
INCIRCLE.position.z = 0.03; // 外圆的层级要比内圆要低,这样内圆才能显示在上面
let Spoint = new THREE.Group();
Spoint.name = `点位${arr[i].id}`;
Spoint.add(INCIRCLE, OUTCIRCLE);
Spoint.position.set( arr[i].x, arr[i].y, 0 );
group.add(Spoint);
}
return StandardGroup;
}
/** 将点位用线连接起来,并且构建一个长方形 */
SDrawLineByStand(tp) {
const Group = new THREE.Group();
Group.name = "标准边框与图形"
// 画线,顶点和末尾点
let linePoint = [
tp.Points[0].x, tp.Points[0].y, tp.Points[0].z,
tp.Points[tp.Points.length-1].x, tp.Points[tp.Points.length-1].y, tp.Points[tp.Points.length-1].z
];
// 确定第三个点和第四个点位,这个和高度 与 旋转角度有关
let p2 = tp.Points[tp.Points.length-1];
let p1 = tp.Points[0];
let h = tp.sh;
if(tp.rotate == 0) {
let point3 = [ p2.x, p2.y+h, p2.z ];
let point4 = [ p1.x, p1.y+h, p1.z ];
linePoint.push(...point3, ...point4);
} else {
let offsetX = ( Math.sin(tp.rotate * (Math.PI/180))*h ).toFixed(6)*1
let offsetY = ( Math.cos(tp.rotate * (Math.PI/180))*h ).toFixed(6)*1
let point3 = [ p2.x-offsetX, p2.y+offsetY, p2.z ];
let point4 = [ p1.x-offsetX, p1.y+offsetY, p1.z ];
linePoint.push(...point3, ...point4);
}
// 确定四个顶点,用线条连接起来
let LP = new Float32Array(linePoint);
const line_geometry = new THREE.BufferGeometry();
const line_material = new THREE.LineBasicMaterial({ color: BorderBlue });
line_geometry.setAttribute('position', new THREE.BufferAttribute(LP,3));
const LineMesh = new THREE.LineLoop(line_geometry, line_material);
LineMesh.name = "线框";
// 构建色块
const shape = new THREE.Shape();
for(let i_ = 0; i_ < linePoint.length/3; i_++){
if(i_ === 0) shape.moveTo(linePoint[0],linePoint[1])
else shape.lineTo(linePoint[i_*3],linePoint[i_*3+1])
}
shape.lineTo(linePoint[0],linePoint[1]);
const shape_geometry = new THREE.ShapeGeometry(shape);
const shape_material = new THREE.MeshBasicMaterial({ color: FillBlue, transparent:true, opacity: 0.4 });
const shape_mesh = new THREE.Mesh(shape_geometry, shape_material);
Group.add(LineMesh, shape_mesh);
return Group;
}
/** 绘制文字 */
SDrawText(tp) {
const TxtGroup = new THREE.Group();
TxtGroup.name = "文字集合";
const Tdiv = document.createElement('div');
if(tp.name == '主梁帽') {
Tdiv.id = "zhuliang";
Tdiv.innerHTML = "主梁帽";
Tdiv.style="writing-mode: vertical-rl;"
}
if(tp.name == '副梁帽') {
Tdiv.id = "fuliang";
Tdiv.innerText = "副梁帽";
Tdiv.style="writing-mode: vertical-rl;"
}
if(tp.name == 'UD') {
Tdiv.id = "ud";
Tdiv.innerText = "UD";
}
Tdiv.className = 'labelClass';
const TitleLabel = new CSS3DObject(Tdiv);
if(tp.rotate == 0) TitleLabel.position.set( tp.Points[0].x-1, tp.Points[0].y+1.5, 0 ); // 坐标
else {
TitleLabel.rotateZ(Math.PI/180*tp.rotate);
TitleLabel.position.set( tp.Points[0].x-1.2, tp.Points[0].y+0.1, 0 ); // 坐标
}
TitleLabel.scale.set(1,1,1)
TxtGroup.add(TitleLabel);
return TxtGroup;
}
/** 绘制波浪线 */
SDrawWave(tp) {
// 波浪分组
const WareGroup = new THREE.Group();
WareGroup.name = "省略波浪";
let points = [
new THREE.Vector2(0,0),
new THREE.Vector2(0.7,0),
new THREE.Vector2(1.2,-0.4),
new THREE.Vector2(1.7,0),
new THREE.Vector2(2.4,0)
]
const wereGeometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: BorderBlue });
const Ware1 = new THREE.Line(wereGeometry, material);
const Ware2 = new THREE.Line(wereGeometry, material);
const Ware3 = new THREE.Line(wereGeometry, material);
const Ware4 = new THREE.Line(wereGeometry, material);
// 定位波浪的高度
let p1 = tp.Points[0];
let p2 = tp.Points[tp.Points.length-1];
let h = tp.sh*0.88;
if(tp.rotate == 0) {
Ware1.position.set(p1.x-0.3, p1.y+h, p1.z)
Ware2.position.set(p1.x-0.3, p1.y-0.3+h, p1.z)
Ware3.position.set(p2.x-1.8, p2.y+h, p2.z)
Ware4.position.set(p2.x-1.8, p2.y-0.3+h, p2.z)
} else {
Ware1.rotateZ(Math.PI/180*tp.rotate)
Ware2.rotateZ(Math.PI/180*tp.rotate)
Ware3.rotateZ(Math.PI/180*tp.rotate)
Ware4.rotateZ(Math.PI/180*tp.rotate)
Ware1.position.set(p1.x-0.6, p1.y+h, p1.z)
Ware2.position.set(p1.x-0.6, p1.y-0.3+h, p1.z)
Ware3.position.set(p2.x-2.2, p2.y+h-0.2, p2.z)
Ware4.position.set(p2.x-2.2, p2.y-0.5+h, p2.z)
}
WareGroup.add(Ware1, Ware2, Ware3, Ware4);
WareGroup.position.z = 0.02;
return WareGroup;
}
/** 绘制两条阈值线 默认5mm, 但是图形实际距离为上下正负1 */
SDrawTwoRedLine(tp) {
// 创建分组
const Group = new THREE.Group();
Group.name = '阈值红线';
// 计算点位
let p1 = tp.Points[0];
let p2 = tp.Points[tp.Points.length-1];
let p3 = null;
let p4 = null;
let p5 = null;
let p6 = null;
let h = DirtyData.yz/5;
if(tp.rotate == 0) {
p3 = [ p2.x, p2.y+h, p2.z ];
p4 = [ p1.x, p1.y+h, p1.z ];
p5 = [ p2.x, p2.y-h, p2.z ];
p6 = [ p1.x, p1.y-h, p1.z ];
} else {
let offsetX = ( Math.sin(tp.rotate * (Math.PI/180))*h ).toFixed(5)*1
let offsetY = ( Math.cos(tp.rotate * (Math.PI/180))*h ).toFixed(5)*1
p3 = [ p2.x-offsetX, p2.y+offsetY, p2.z ];
p4 = [ p1.x-offsetX, p1.y+offsetY, p1.z ];
p5 = [ p2.x+offsetX, p2.y-offsetY, p2.z ];
p6 = [ p1.x+offsetX, p1.y-offsetY, p1.z ];
}
// 绘制线条
const redLine_up = new THREE.BufferGeometry();
const redPoints_up = new Float32Array([ ...p3, ...p4 ])
redLine_up.setAttribute('position', new THREE.BufferAttribute(redPoints_up,3));
const redLine_down = new THREE.BufferGeometry();
const redPoints_down = new Float32Array([ ...p5, ...p6 ])
redLine_down.setAttribute('position', new THREE.BufferAttribute(redPoints_down,3));
const redMaterial = new THREE.LineDashedMaterial({ color: LineRed, scale: 0.8, dashSize: 1, gapSize: 0.8 }); // 虚线材质
const line1 = new THREE.Line(redLine_up, redMaterial);
const line2 = new THREE.Line(redLine_down, redMaterial);
line1.computeLineDistances();
line2.computeLineDistances();
line1.name = 'up';
line2.name = 'down';
Group.add(line1, line2);
Group.position.z = 0.01;
return Group;
}
/** 开始绘制点位 */
SDrawPointFromMQTT(Msg, HistoryArray) {
// 如果原有点存在,清除掉原有的点位、创建点位并且显示
let targetScene = null;
let inx = null;
// 寻找对应的数据集合,寻找具体凉帽
for(let child of this.scene.children) {
if(child.name == Msg.circleposi) {
targetScene = child;
if(child.name == '主梁帽') inx = "lm";
if(child.name == '副梁帽') inx = "fl";
if(child.name == 'UD') inx = "ud";
break;
}
}
if(!targetScene) return 0;
// 寻找到目标,销毁
for(let child of targetScene.children) {
if(child.name == '实际点位集合') {
this.RemoveGeometry(child);
}
}
// 寻找到文字,销毁
for(let child of targetScene.children) {
if(child.name == '实际点位文字集合') {
this.RemoveGeometry(child);
this.RemoveTxt(inx);
}
}
// 有HistoryArray则为历史数据,将对应点的实测点放入标准结构的reality中
if(HistoryArray) {
for(let itemx of HistoryArray) {
for(let pi of DirtyData[inx].Points) {
if(pi.id == itemx.pointnum) {
pi.reality = itemx;
break;
}
}
}
} else {
for(let pi of DirtyData[inx].Points) {
if(pi.id == Msg.pointnum) {
pi.reality = Msg;
break;
}
}
}
// 修改对应的点数据
const Group = new THREE.Group();
Group.name = '实际点位集合';
const TextGroup = new THREE.Group();
TextGroup.name = "实际点位文字集合";
DirtyData[inx].Points.map((item, index)=>{
if(item.reality) {
// 判断是否超出界限,超出改变颜色
let _c = CircleYellow;
let jl = (item.reality.angle - item.angle)*2
if( jl > DirtyData.yz || jl < -DirtyData.yz) _c = LineRed;
let pot = Cp(_c);
pot.userData.ZhouJu = Number((jl/5).toFixed(2));
pot.userData.DianJu = Number(jl.toFixed(2)*1);
// 创建文字
const Tinfo = document.createElement('div'); // 创建元素
Tinfo.id = `${index}-${inx}`; // 文字
Tinfo.className = 'labelClass'; // 元素应用样式
Tinfo.innerText = `(${item.axisposi},${Number(jl.toFixed(2)) }mm)`; // 元素里面的文字
const Taxis = new CSS3DObject(Tinfo);
// 修改位置 - 根据是否有倾斜角度
if(DirtyData[inx].rotate == 0) {
pot.position.set(item.x, item.y+(jl/5), 0.026);
// 记录实际点位数据,方便以后使用
item.reality.$x = item.x;
item.reality.$y = item.y+(jl/5);
item.reality.$z = 0.026;
Taxis.position.set(item.x+1.8, item.y+3, 0.026)
Taxis.rotateZ(Math.PI/180*25)
} else {
let offsetX = ( Math.sin(DirtyData[inx].rotate * (Math.PI/180))*(jl/5) ).toFixed(5)*1
let offsetY = ( Math.cos(DirtyData[inx].rotate * (Math.PI/180))*(jl/5) ).toFixed(5)*1
pot.position.set(item.x-offsetX, item.y+offsetY, 0.026);
// 记录实际点位数据,方便以后使用
item.reality.$x = item.x-offsetX;
item.reality.$y = item.y+offsetY;
item.reality.$z = 0.026;
let pY = ( Math.cos(DirtyData[inx].rotate * (Math.PI/180))*2 ).toFixed(5)*1
Taxis.position.set(item.x+1.8, item.y+pY+0.8, 0.026)
Taxis.rotateZ(Math.PI/180*30)
}
// 对文字的设置
Taxis.scale.set(0.7, 0.7, 0.7)
TextGroup.add(Taxis)
// 添加到组
Group.add(pot);
}
})
// 创建目标,并将数据放入渲染
targetScene.add(Group, TextGroup);
// 开启点位闪烁
if(!HistoryArray) this.SOpenIterval();
this.SDrawBorderFromMQTT(Msg); // 画框
if(!HistoryArray) this.SDrawRelityPerson(Msg) // 画打点人员
// 点位创建方法
function Cp(color) {
let inSideCircle = new THREE.CircleGeometry(0.15,24); // 内圆
let inSideColor = new THREE.MeshPhongMaterial({ color: color }); // 内圆颜色
let outSideCircle = new THREE.CircleGeometry(0.20,24); // 外圆
let outSideColor = new THREE.MeshPhongMaterial({ color: 0xFFFFFF }); // 外圆颜色(固定为白色)
let INCIRCLE = new THREE.Mesh(inSideCircle, inSideColor); // 生成一个内圆
let OUTCIRCLE = new THREE.Mesh(outSideCircle, outSideColor); // 生成一个外圆
OUTCIRCLE.position.z = 0.024; // 外圆的层级要比内圆要低,这样内圆才能显示在上面
INCIRCLE.position.z = 0.025; // 外圆的层级要比内圆要低,这样内圆才能显示在上面
INCIRCLE.name = '内圆';
let Spoint = new THREE.Group();
Spoint.add(INCIRCLE, OUTCIRCLE);
return Spoint;
}
}
/** 开始绘制边框 */
SDrawBorderFromMQTT(Msg) {
//
let targetScene = null;
// 寻找对应的数据集合
for(let child of this.scene.children) {
if(child.name == Msg.circleposi) {
targetScene = child;
break;
}
}
if(!targetScene) return 0;
//
let inx = null;
if(Msg.circleposi == '主梁帽') inx = "lm";
if(Msg.circleposi == '副梁帽') inx = "fl";
if(Msg.circleposi == 'UD') inx = "ud";
if(!inx) return 0;
//
for(let child of targetScene.children) {
if(child.name == '实际边框与图形') {
this.RemoveGeometry(child);
}
}
//
const Group = new THREE.Group();
Group.name = '实际边框与图形';
// 绘制边框 框加线,底部线条一定是曲线 - step1 边框
let tp = DirtyData[inx]
let p2 = tp.Points[tp.Points.length-1];
let p1 = tp.Points[0];
let h = tp.sh+0.5;
let linePoint = [ p2.x, p2.y, p2.z ];
// 确定第三个点和第四个点位,这个和高度 与 旋转角度有关
if(tp.rotate == 0) {
let point3 = [ p2.x, p2.y+h, p2.z ];
let point4 = [ p1.x, p1.y+h, p1.z ];
linePoint.push(...point3, ...point4);
} else {
let offsetX = ( Math.sin(tp.rotate * (Math.PI/180))*h ).toFixed(6)*1
let offsetY = ( Math.cos(tp.rotate * (Math.PI/180))*h ).toFixed(6)*1
let point3 = [ p2.x-offsetX, p2.y+offsetY, p2.z ];
let point4 = [ p1.x-offsetX, p1.y+offsetY, p1.z ];
linePoint.push(...point3, ...point4);
}
linePoint.push(p1.x, p1.y, p1.z);
let po = new Float32Array(linePoint);
const bor_geometry = new THREE.BufferGeometry();
bor_geometry.setAttribute('position', new THREE.BufferAttribute(po,3));
const bor_material = new THREE.LineBasicMaterial({ color: BorderYellow });
const borders = new THREE.Line(bor_geometry, bor_material);
borders.name = `标准边框`;
// 绘制曲线
let tempCur = [ new THREE.Vector3(p1.x, p1.y, p1.z) ];
for(let i = 0; i < tp.Points.length; i++){
if(tp.Points[i].reality) {
tempCur.push(new THREE.Vector3(
tp.Points[i].reality.$x,
tp.Points[i].reality.$y,
tp.Points[i].reality.$z
));
}
}
tempCur.push(new THREE.Vector3(p2.x, p2.y, p2.z));
const curve = new THREE.CatmullRomCurve3(tempCur);
const cps = curve.getPoints(100);
const geometry_ = new THREE.BufferGeometry().setFromPoints(cps);
const material_ = new THREE.LineBasicMaterial({ color: BorderYellow });
const splineObject = new THREE.Line( geometry_, material_ );
// 绘制图形
const shape = new THREE.Shape();
shape.moveTo(linePoint[0].toFixed(2)*1,linePoint[1].toFixed(2)*1,0);
for(let v3 of cps) {
shape.lineTo(v3.x.toFixed(2)*1, v3.y.toFixed(2)*1, 0);
}
shape.lineTo(linePoint[3].toFixed(2)*1,linePoint[4].toFixed(2)*1,0);
shape.lineTo(linePoint[6].toFixed(2)*1,linePoint[7].toFixed(2)*1,0);
shape.lineTo(linePoint[9].toFixed(2)*1,linePoint[10].toFixed(2)*1,0);
shape.lineTo(linePoint[0].toFixed(2)*1,linePoint[1].toFixed(2)*1,0);
const shape_geometry = new THREE.ShapeGeometry(shape);
const shape_material = new THREE.MeshBasicMaterial({ color: FillYellow, transparent:true, opacity: 0.4 });
const shape_mesh = new THREE.Mesh(shape_geometry, shape_material);
shape_mesh.name = `实际图形`
shape_mesh.position.z = 0.02;
Group.add(borders, splineObject, shape_mesh);
//Group.add( shape_mesh);
targetScene.add(Group);
}
/** 销毁场景内容 */
RemoveGeometry(item) {
if(item) {
item.parent.remove(item);
item.traverse((child) => {
if (child.material) child.material.dispose()
if (child.geometry) child.geometry.dispose()
child = null;
});
}
}
/** 销毁文字(标题) */
SRemoveTitle() {
let zl = document.getElementById(`zhuliang`);
let fl = document.getElementById(`fuliang`);
let ud = document.getElementById(`ud`);
if(zl) zl.remove();
if(fl) fl.remove();
if(ud) ud.remove();
}
/** 销毁文字(点位) , 对象,名字,id部分 */
RemoveTxt(ids, arr) {
try{
if(DirtyData[ids] && DirtyData[ids]?.Points && Array.isArray(DirtyData[ids].Points)) {
arr = DirtyData[ids]?.Points;
if(arr) {
for(let i in arr) {
let x = document.getElementById(`${i}-${ids}`);
if(x) x.remove();
}
}
}
} catch(err) {
// // console.log("执行文字销毁时,发生错误mt.js-RemoveTxt方法。"+err)
}
}
/** 显示实时打点人员 */
SDrawRelityPerson(msg) {
let targetScene = null;
let index = null;
let sub = null;
for(let child of this.scene.children) {
if(child.name == msg.circleposi) {
targetScene = child;
if(child.name == '主梁帽') {
index = "lm_Timer";
sub = 'lm'
}
if(child.name == '副梁帽') {
index = "fl_Timer";
sub = 'fl'
}
if(child.name == 'UD') {
index = "ud_Timer";
sub = 'ud'
}
for(let item of child.children) {
if(item.name == '实时打点人员') {
if(DirtyData[index]) clearTimeout(DirtyData[index])
let x = document.getElementById(`${index}`);
if(x) x.remove();
this.RemoveGeometry(item)
}
}
break;
}
}
if(!targetScene) return 0;
//
const Tdiv = document.createElement('div');
Tdiv.innerText = `${msg.operator}`;
Tdiv.style = "color: #FFF;";
Tdiv.className = 'labelClass';
Tdiv.id = index;
const TitleLabel = new CSS3DObject(Tdiv);
for(let xs of DirtyData[sub].Points) {
if(xs.id == msg.pointnum) {
TitleLabel.position.set(xs.reality.$x, xs.y-2.4 , 0.2)
// // console.log(DirtyData[sub])
break;
}
}
// 对文字的设置
TitleLabel.scale.set(0.7, 0.7, -1)
targetScene.add(TitleLabel);
TitleLabel.name = '实时打点人员'
// // console.log(this.scene.children[3])
DirtyData[index] = setTimeout(()=>{
this.SDelOperator(targetScene)
},1000 * 60)
}
/** 删除界面上的文字显示 */
SDelOperator(targetScene) {
let index = null;
if(targetScene.name == '主梁帽') index = "lm_Timer";
if(targetScene.name == '副梁帽') index = "fl_Timer";
if(targetScene.name == 'UD') index = "ud_Timer";
for(let item of targetScene.children) {
if(item.name == '实时打点人员') {
this.RemoveGeometry(item);
let x = document.getElementById(`${index}`);
if(x) x.remove();
}
}
}
/** 闪烁点位 */
SOpenIterval() {
if(DirtyData.blind) clearInterval(DirtyData.blind);
DirtyData.blindArray = [];
this.NDfunction()
DirtyData.blind = setInterval(()=>{
if(DirtyData.blindArray.length > 0) {
for(let it of DirtyData.blindArray) {
it.visible = !it.visible
}
}
}, 500)
}
/** 取消点位闪烁 */
SStopIterval() {
if(DirtyData.blind) clearInterval(DirtyData.blind);
DirtyData.blind = 0;
DirtyData.blindArray = [];
this.NDfunction(true)
}
/** 独立方法, 配合点位闪烁使用 */
NDfunction(isStop) {
console.log(this.scene)
this.scene.children.map(item => {
if(item.name == '主梁帽' || item.name == '副梁帽' || item.name == 'UD') {
for(let child of item.children) {
if(child.name == '实际点位集合') {
child.children.map(it => {
if(it.userData.DianJu > DirtyData.yz || it.userData.DianJu < -DirtyData.yz) {
it.scale.x = 1.5
it.scale.y = 1.5
it.scale.z = 1.5
if(isStop) it.visible = true
for(let po of it.children) {
if(po.name == "内圆") po.material.color = LineRed
}
DirtyData.blindArray.push(it)
} else {
it.scale.x = 1
it.scale.y = 1
it.scale.z = 1
it.visible = true;
for(let po of it.children) {
if(po.name == "内圆") po.material.color = CircleYellow
}
}
})
break;
}
}
}
})
}
}
export const mt = new initThree('叶片');