132 lines
7.8 KiB
JavaScript
132 lines
7.8 KiB
JavaScript
import * as THREE from 'three';
|
||
|
||
export function createAssetInspector({model,networkRoot,scene,style,onLocate,onSelectionChange=()=>{}}){
|
||
const links=new Map(model.links.map(link=>[link.id,link]));
|
||
const nodes=new Map(model.nodes.map(node=>[node.id,node]));
|
||
const meters=new Map(model.cadEquipmentBindings.map(meter=>[meter.assetId,meter]));
|
||
const halo=new THREE.Group();halo.name='Selection presentation';scene.add(halo);
|
||
const material=new THREE.MeshBasicMaterial({color:0xffbd45,transparent:true,opacity:.35,depthWrite:false,polygonOffset:true,polygonOffsetFactor:-2});
|
||
let selected=null;
|
||
|
||
const status=input=>input==null?'暂无数据':({open:'开启',closed:'关闭',active:'调节中'}[String(input).toLowerCase()]??String(input));
|
||
const value=(input,unit)=>typeof input==='number'?input.toFixed(3)+' '+unit:'暂无数据';
|
||
const fields=rows=>rows.map(([label,input])=>({label,value:input==null?'暂无数据':String(input)}));
|
||
|
||
function resolve(input){
|
||
let id=String(input);
|
||
if(id.startsWith('zjb:physical_'))id='inp:link:'+id.slice(13);
|
||
const cad=model.cadAttachments?.find(record=>record.assetId===id);
|
||
if(cad)return {assetId:id,id:cad.hostNodeId,node:nodes.get(cad.hostNodeId),cad,kind:'CAD 立管'};
|
||
const meter=meters.get(id);
|
||
if(meter)return {assetId:id,id:meter.inpLinkId,meter,link:links.get(meter.inpLinkId),kind:'流量计'};
|
||
if(id.startsWith('inp:link:'))id=id.slice(9);
|
||
else if(id.startsWith('inp:node:')){
|
||
id=id.slice(9);const node=nodes.get(id);
|
||
if(node)return {assetId:'inp:node:'+id,id,node,kind:'节点'};
|
||
}
|
||
const link=links.get(id);
|
||
if(link)return {assetId:'inp:link:'+id,id,link,kind:{PIPES:'管段',PUMPS:'水泵',VALVES:'阀门'}[link.kind]};
|
||
const node=nodes.get(id);
|
||
if(node)return {assetId:'inp:node:'+id,id,node,kind:'节点'};
|
||
throw Error('未找到构件:'+input);
|
||
}
|
||
|
||
function bounds(item=selected){
|
||
const box=new THREE.Box3();if(!item)return box;networkRoot.updateMatrixWorld(true);
|
||
if(item.cad||item.meter||item.link?.kind==='PUMPS'){
|
||
networkRoot.traverse(object=>{if(!object.isMesh&&object.userData.assetId===item.assetId)box.union(new THREE.Box3().setFromObject(object));});
|
||
}
|
||
if(box.isEmpty()){
|
||
const records=item.node?model.nodeInstances:model.pipeInstances;
|
||
const mesh=item.node?style.nodes:style.pipes;
|
||
for(let index=0;index<records.length;index++)if(records[index].inpId===item.id){
|
||
const matrix=new THREE.Matrix4();mesh.getMatrixAt(index,matrix);
|
||
if(!mesh.geometry.boundingBox)mesh.geometry.computeBoundingBox();
|
||
box.union(mesh.geometry.boundingBox.clone().applyMatrix4(matrix).applyMatrix4(mesh.matrixWorld));
|
||
}
|
||
}
|
||
return box;
|
||
}
|
||
|
||
function highlight(){
|
||
for(const object of halo.children)if(object.userData.owned){object.geometry.dispose();object.material.dispose();}
|
||
halo.clear();if(!selected||!networkRoot.visible)return;
|
||
if(selected.cad||selected.meter||selected.link?.kind==='PUMPS'){
|
||
const box=bounds();if(!box.isEmpty()){const helper=new THREE.Box3Helper(box,0xffbd45);helper.userData.owned=true;halo.add(helper);}
|
||
}else{
|
||
const records=selected.node?model.nodeInstances:model.pipeInstances;
|
||
const source=selected.node?style.nodes:style.pipes;
|
||
records.forEach((record,index)=>{
|
||
if(record.inpId!==selected.id)return;
|
||
const matrix=new THREE.Matrix4();source.getMatrixAt(index,matrix);
|
||
const object=new THREE.Mesh(source.geometry,material);object.matrixAutoUpdate=false;
|
||
object.matrix.copy(source.matrixWorld).multiply(matrix);object.renderOrder=3;halo.add(object);
|
||
});
|
||
}
|
||
}
|
||
|
||
function neighbors(item=selected){
|
||
if(!item)return [];
|
||
const nodeIds=item.node?[item.id]:[item.link.from,item.link.to];
|
||
return model.links.filter(link=>link.id!==item.id&&[link.from,link.to].some(nodeId=>nodeIds.includes(nodeId)));
|
||
}
|
||
|
||
function details(item=selected){
|
||
if(!item)return null;
|
||
const link=item.link;
|
||
const result=style.getResult(item.id,item.node?'nodes':'links');
|
||
const sections=[];
|
||
sections.push({title:'基本信息',fields:fields([
|
||
['编号',item.id],['类型',item.kind],
|
||
...(link?[['名义管径',link.kind==='PUMPS'?'连接模型参数 '+link.diameterMm+' mm':link.diameterMm+' mm'],['初始状态',status(link.initialStatus)]]:[]),
|
||
['展示比例',style.displayMode==='coordinated'?'泵房协调比例,范围外 '+style.style.scale+'×':'全局 '+style.style.scale+'×'],
|
||
])});
|
||
if(item.cad)sections.push({title:'CAD 竖向依据',fields:fields([
|
||
['接入节点',item.cad.hostNodeId],['管径',item.cad.diameter_mm+' mm'],['图示高差',item.cad.vertical_height_m+' m'],
|
||
['原图标注',item.cad.source_annotation],['CAD 句柄',item.cad.evidence.map(evidence=>evidence.handle).join('、')],
|
||
['上端','上层配水走向未确认'],['几何基准','全局显示抬高 0.35 m;高差按图纸'],['水力关系','附着于现有节点,不新增 INP 链接'],
|
||
])});
|
||
if(link&&model.verticalCoordination&&model.coordination.scopeLinkIds.includes(link.id))sections.push({title:'泵房竖向依据',fields:fields([
|
||
['管中心','CAD 换算为地面下 2.90 m,协调模式采用'],['接入走向','靠泵房端增加竖向过渡,折点位置估算'],['平面配准','INP 示意位置;与机械图接口尚未完成配准'],
|
||
])});
|
||
if(link)sections.push({title:'连接关系',fields:fields([
|
||
['起点',link.from],['终点',link.to],...(item.meter?[['宿主管段',item.meter.inpLinkId]]:[]),
|
||
])});
|
||
|
||
let pressure=result?.pressure,pressureBasis='直接结果';
|
||
if(link&&pressure==null){
|
||
const fromPressure=style.getResult(link.from,'nodes')?.pressure;
|
||
const toPressure=style.getResult(link.to,'nodes')?.pressure;
|
||
if(Number.isFinite(fromPressure)&&Number.isFinite(toPressure)){pressure=(fromPressure+toPressure)/2;pressureBasis='两端节点均值';}
|
||
}
|
||
if(item.cad)pressureBasis='接入节点压力,非立管上端压力';
|
||
const pressureSource=result?.source==='scada'?'SCADA '+(result.deviceId??'监测'):pressure!=null?'在线模拟':'暂无数据';
|
||
sections.push({title:'运行结果',fields:fields([
|
||
['运行状态',status(result?.status)],['压力',value(pressure,'mH₂O')],
|
||
...(pressure!=null?[['压力依据',pressureBasis],['压力来源',pressureSource]]:[]),
|
||
...(result?.source==='scada'&&Number.isFinite(result.simulationPressure)?[['同期模拟压力',value(result.simulationPressure,'mH₂O')]]:[]),
|
||
...(link?[['流速',value(result?.velocity==null?null:Math.abs(result.velocity),'m/s')],['流量',value(result?.flow,'L/s')],['流向',result?.status?.toLowerCase()==='closed'?'停流':result?.direction!=null||result?.flow!=null?((result.direction??Math.sign(result.flow))>0?'起点 → 终点':(result.direction??Math.sign(result.flow))<0?'终点 → 起点':'停流'):'暂无数据']]:[]),
|
||
])});
|
||
sections.push({title:'来源说明',fields:fields([
|
||
['拓扑来源','已核对 INP;本页不求解水力'],['位置与高度','展示调整,不代表实测埋深'],
|
||
...(link?.kind==='PUMPS'?[['设备对应','CAD 槽位顺序;现场铭牌未确认']]:[]),
|
||
...(item.meter?[['CAD 句柄',item.meter.cadHandles.join('、')]]:[]),
|
||
])});
|
||
return {
|
||
assetId:item.assetId,elementId:item.id,kind:item.kind,
|
||
title:item.kind+' · '+(item.cad?.label??item.meter?.label??item.id),
|
||
sections,
|
||
neighbors:neighbors(item).map(record=>({id:record.id,label:record.id})),
|
||
};
|
||
}
|
||
|
||
function refresh(){if(selected)onSelectionChange(details());highlight();}
|
||
function select(id){selected=resolve(id);highlight();const selection=details();onSelectionChange(selection);return selection;}
|
||
function clear(){
|
||
for(const object of halo.children)if(object.userData.owned){object.geometry.dispose();object.material.dispose();}
|
||
halo.clear();selected=null;onSelectionChange(null);
|
||
}
|
||
function locate(){if(selected)return onLocate(selected,bounds());}
|
||
return {select,clear,resolve,bounds,refresh,highlight,details,locate,get selected(){return selected;},neighbors};
|
||
}
|