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{ 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,'m')], ...(pressure!=null?[['压力依据',pressureBasis],['压力来源',pressureSource]]:[]), ...(result?.source==='scada'&&Number.isFinite(result.simulationPressure)?[['同期模拟压力',value(result.simulationPressure,'m')]]:[]), ...(link?[['流速',value(result?.velocity==null?null:Math.abs(result.velocity),'m/s')],['流量',value(result?.flow,'m³/h')],['流向',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}; }