feat(3d): integrate ZJB scene with project context
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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};
|
||||
}
|
||||
Reference in New Issue
Block a user