Files
TJWaterFrontend_Refine/public/three-dimensional/zjb/v29/network-style.mjs
T
jiang 1622eaa2da
Generic Container CI/CD / test-build-publish (push) Successful in 1m5s
Frontend CI/CD v2 / build-test-publish-and-deploy (push) Successful in 1m5s
fix(3d): restore complete SCADA scene context
The scene previously returned early when realtime rows were absent and selected map-tier assets for network mode, hiding SCADA markers and station interiors and floors.

Use SCADA timestamps as valid frames, render node-aligned monitor markers, and pin the full network context in the manifest regression test. Keep the timeline close action inside its drag header.
2026-09-14 16:57:41 +08:00

86 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as THREE from 'three';
export const DEFAULT_STYLE=Object.freeze({scale:6,mode:'uniform',color:'#098ed0',missingColor:'#89949d',lowColor:'#2b83ba',highColor:'#e66c37',opacity:1,roughness:.3,metalness:.22,nodes:true,direction:'none',arrowColor:'#f2b447',autoRange:true,min:0,max:3});
const finite=v=>typeof v==='number'&&Number.isFinite(v);
export function validateResults(input,model){
if(input?.modelId!==model.modelId)throw Error('结果文件与当前管网模型编号不一致。');
if(input.units?.velocity!=='m/s'||input.units?.pressure!=='m'||input.units?.flow!=='m³/h')throw Error('结果单位须明确为 m/s、m、m³/h。');
const linkIds=new Set(model.links.map(l=>l.id)),nodeIds=new Set(model.nodes.map(n=>n.id));
for(const [table,ids,fields] of [[input.links??{},linkIds,['velocity','flow','pressure','direction']],[input.nodes??{},nodeIds,['pressure']]]){
if(typeof table!=='object'||table===null||Array.isArray(table))throw Error('结果必须按编号提供对象。');
for(const [id,r] of Object.entries(table)){
if(!ids.has(id))throw Error('模型中不存在编号:'+id);
if(typeof r!=='object'||r===null||Array.isArray(r))throw Error('无效结果:'+id);
for(const k of fields)if(r[k]!==undefined&&r[k]!==null&&!finite(r[k]))throw Error('非数值结果:'+id+'.'+k);
if(r.status!=null&&(typeof r.status!=='string'||!['open','closed','active'].includes(r.status.toLowerCase())))throw Error('无效运行状态:'+id);
if(r.direction!=null&&![-1,0,1].includes(r.direction))throw Error('流向只能为 -1、0、1。');
}
}
return structuredClone(input);
}
export function metricValue(mode,link,results){
const r=results?.links?.[link.id];
if(mode==='velocity')return finite(r?.velocity)?Math.abs(r.velocity):null;
if(mode==='pressure'){
if(finite(r?.pressure))return r.pressure;
const a=results?.nodes?.[link.from]?.pressure,b=results?.nodes?.[link.to]?.pressure;return finite(a)&&finite(b)?(a+b)/2:null;
}
if(mode==='direction'){
if(r?.status?.toLowerCase()==='closed')return 0;
if(finite(r?.direction))return r.direction;
return finite(r?.flow)?Math.sign(r.flow):null;
}
return null;
}
export function attachNetworkStyle(group,model){
let pipes,nodes,bends;group.traverse(o=>{if(o.isInstancedMesh&&o.userData.instances?.length){if(o.name==='WaterBendInstances')bends=o;else if(o.userData.instances[0].kind==='NODE')nodes=o;else pipes=o;}});
if(!pipes||!nodes)throw Error('管网实体缺少可编辑管段或节点。');
pipes.userData.instances=model.pipeInstances;nodes.userData.instances=model.nodeInstances;
const style={...DEFAULT_STYLE},byId=new Map(model.links.map(l=>[l.id,l])),partsById=new Map();let results=null,displayMode='global',focusScope=false;const scopeIds=new Set(model.coordination?.scopeLinkIds??[]),scopeNodes=new Set(model.links.filter(l=>scopeIds.has(l.id)).flatMap(l=>[l.from,l.to]));
model.pipeInstances.forEach((p,i)=>{if(!partsById.has(p.inpId))partsById.set(p.inpId,[]);partsById.get(p.inpId).push(i);});
const owned=[];for(const mesh of [pipes,nodes,bends].filter(Boolean)){const material=new THREE.MeshPhysicalMaterial({color:0xffffff,roughness:.3,metalness:.22,clearcoat:.22,clearcoatRoughness:.38});mesh.material=material;mesh.castShadow=true;mesh.receiveShadow=true;owned.push(material);}
const geo=new THREE.ConeGeometry(1,2,12),mat=new THREE.MeshBasicMaterial({color:style.arrowColor}),arrows=new THREE.InstancedMesh(geo,mat,model.links.length);arrows.name='WaterFlowArrows';arrows.userData.instances=model.links.map(l=>({assetId:'inp:link:'+l.id,inpId:l.id,kind:l.kind}));group.add(arrows);
const markerGeo=new THREE.OctahedronGeometry(.72,0),markerMat=new THREE.MeshBasicMaterial({color:0x1d83e6,transparent:true,opacity:.96,depthTest:false,depthWrite:false}),scadaMarkers=new THREE.InstancedMesh(markerGeo,markerMat,model.nodeInstances.length);scadaMarkers.name='ScadaSensorMarkers';scadaMarkers.userData.instances=model.nodeInstances.map(n=>({assetId:'inp:node:'+n.inpId,inpId:n.inpId,kind:'SCADA'}));scadaMarkers.renderOrder=6;group.add(scadaMarkers);
const ringGeo=new THREE.TorusGeometry(1.15,.09,8,28);ringGeo.rotateX(Math.PI/2);const ringMat=new THREE.MeshBasicMaterial({color:0x1d83e6,transparent:true,opacity:.76,depthTest:false,depthWrite:false}),scadaRings=new THREE.InstancedMesh(ringGeo,ringMat,model.nodeInstances.length);scadaRings.name='ScadaSensorRings';scadaRings.userData.instances=scadaMarkers.userData.instances;scadaRings.renderOrder=5;group.add(scadaRings);
const matrix=new THREE.Matrix4(),dummy=new THREE.Object3D(),pos=new THREE.Vector3(),quat=new THREE.Quaternion(),scale=new THREE.Vector3();let summary={};
function apply(){
const layout=(displayMode==='coordinated'?model.coordinatedLayouts:model.layouts)?.[style.scale];if(!layout)throw Error('当前模型支持整数倍率 112。');
if(layout.pipes.length!==pipes.instanceMatrix.array.length||layout.nodes.length!==nodes.instanceMatrix.array.length)throw Error('模型倍率数据与几何不匹配。');
if(bends){bends.instanceMatrix.array.set(layout.bends);bends.instanceMatrix.needsUpdate=true;}pipes.instanceMatrix.array.set(layout.pipes);nodes.instanceMatrix.array.set(layout.nodes);pipes.instanceMatrix.needsUpdate=true;nodes.instanceMatrix.needsUpdate=true;
for(const d of layout.devices){group.traverse(o=>{if(o.userData.assetId===d.id&&!o.isMesh){o.position.fromArray(d.position);o.quaternion.fromArray(d.quaternion);o.scale.fromArray(d.scale);}});}
const vals=model.links.map(l=>metricValue(style.mode,l,results)).filter(finite);let min=style.autoRange&&vals.length?Math.min(...vals):style.min,max=style.autoRange&&vals.length?Math.max(...vals):style.max;
const low=new THREE.Color(style.lowColor),high=new THREE.Color(style.highColor),missing=new THREE.Color(style.missingColor),plain=new THREE.Color(style.color);
function color(value){if(style.mode==='uniform')return plain;if(!finite(value))return missing;if(style.mode==='direction')return value===0?new THREE.Color('#a7aab0'):value>0?high:low;return low.clone().lerp(high,max===min?.5:Math.max(0,Math.min(1,(value-min)/(max-min))));}
model.pipeInstances.forEach((p,i)=>pipes.setColorAt(i,color(metricValue(style.mode,byId.get(p.inpId),results))));
model.nodeInstances.forEach((p,i)=>nodes.setColorAt(i,color(style.mode==='pressure'?results?.nodes?.[p.inpId]?.pressure:null)));
if(bends){model.bendInstances.forEach((p,i)=>bends.setColorAt(i,color(metricValue(style.mode,byId.get(p.inpId),results))));bends.instanceColor.needsUpdate=true;}pipes.instanceColor.needsUpdate=true;nodes.instanceColor.needsUpdate=true;
for(const mesh of [pipes,nodes,bends].filter(Boolean)){mesh.material.opacity=style.opacity;mesh.material.transparent=style.opacity<1;mesh.material.depthWrite=style.opacity===1;mesh.material.roughness=style.roughness;mesh.material.metalness=style.metalness;mesh.material.needsUpdate=true;mesh.computeBoundingBox();mesh.computeBoundingSphere();}
group.traverse(o=>{if(o.userData.layoutMode){o.visible=o.userData.layoutMode===displayMode&&o.userData.layoutScale===style.scale;if(o.isMesh){o.material.color.copy(color(metricValue(style.mode,byId.get(o.userData.inpId),results)));o.material.opacity=style.opacity;o.material.transparent=style.opacity<1;o.material.roughness=style.roughness;}}});
group.traverse(o=>{if(o.userData.cadAttachment){const r=model.cadAttachments.find(r=>r.assetId===o.userData.assetId);o.traverse(mesh=>{if(!mesh.isMesh)return;if(mesh.userData.cadRadialScale)mesh.scale.set(style.scale,1,style.scale);if(mesh.userData.cadTerminalScale)mesh.scale.setScalar(style.scale);mesh.material.color.copy(color(style.mode==='pressure'?results?.nodes?.[r.hostNodeId]?.pressure:null));mesh.material.opacity=style.opacity;mesh.material.transparent=style.opacity<1;mesh.material.depthWrite=style.opacity===1;mesh.material.roughness=style.roughness;mesh.material.metalness=style.metalness;});}});
if(focusScope){const zero=new THREE.Matrix4().makeScale(0,0,0);for(const [mesh,recs,isNode] of [[pipes,model.pipeInstances,false],[nodes,model.nodeInstances,true],[bends,model.bendInstances,false]]){if(!mesh)continue;recs.forEach((r,i)=>{if(!(isNode?scopeNodes:scopeIds).has(r.inpId))mesh.setMatrixAt(i,zero);else if(isNode&&displayMode==='coordinated'){mesh.getMatrixAt(i,matrix);matrix.decompose(pos,quat,scale);const radius=Math.max(.075,...model.links.filter(l=>scopeIds.has(l.id)&&[l.from,l.to].includes(r.inpId)).map(l=>l.diameterMm/2000));matrix.compose(pos,quat,new THREE.Vector3(radius,radius,radius));mesh.setMatrixAt(i,matrix);}});mesh.instanceMatrix.needsUpdate=true;mesh.computeBoundingBox();mesh.computeBoundingSphere();}}
group.traverse(o=>{if(o.userData.cadAttachment)o.visible=!focusScope;if(o.userData.layoutMode&&focusScope)o.visible=false;});
nodes.visible=style.nodes;let scadaCount=0;
model.nodeInstances.forEach((record,index)=>{
const visible=results?.nodes?.[record.inpId]?.source==='scada'&&(!focusScope||scopeNodes.has(record.inpId));
if(!visible){dummy.position.set(0,0,0);dummy.scale.setScalar(0);dummy.quaternion.identity();dummy.updateMatrix();scadaMarkers.setMatrixAt(index,dummy.matrix);scadaRings.setMatrixAt(index,dummy.matrix);return;}
nodes.getMatrixAt(index,matrix);matrix.decompose(pos,quat,scale);
dummy.position.copy(pos);dummy.position.y+=1.18;dummy.quaternion.identity();dummy.scale.setScalar(.72);dummy.updateMatrix();scadaMarkers.setMatrixAt(index,dummy.matrix);
dummy.position.copy(pos);dummy.position.y+=.2;dummy.quaternion.identity();dummy.scale.setScalar(.72);dummy.updateMatrix();scadaRings.setMatrixAt(index,dummy.matrix);scadaCount++;
});
for(const marker of [scadaMarkers,scadaRings]){marker.instanceMatrix.needsUpdate=true;marker.visible=scadaCount>0;marker.computeBoundingBox();marker.computeBoundingSphere();}
let arrowCount=0;
model.links.forEach((l,i)=>{
const ids=partsById.get(l.id)??[],idx=ids[Math.floor(ids.length/2)];let direction=style.direction==='topology'?1:style.direction==='results'?metricValue('direction',l,results):0;
if(!direction||idx===undefined||(focusScope&&!scopeIds.has(l.id))){dummy.position.set(0,0,0);dummy.scale.setScalar(0);dummy.quaternion.identity();}
else{pipes.getMatrixAt(idx,matrix);matrix.decompose(pos,quat,scale);dummy.position.copy(pos);dummy.quaternion.copy(quat);if(direction<0)dummy.quaternion.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1,0,0),Math.PI));const r=Math.max(.22,scale.x*1.7);dummy.scale.set(r,r*1.5,r);arrowCount++;}
dummy.updateMatrix();arrows.setMatrixAt(i,dummy.matrix);
});arrows.instanceMatrix.needsUpdate=true;arrows.visible=style.direction!=='none';arrows.material.color.set(style.arrowColor);arrows.computeBoundingBox();arrows.computeBoundingSphere();
summary={focusScope,displayMode,mode:style.mode,min,max,dataLinks:vals.length,totalLinks:model.links.length,scadaMarkers:scadaCount,arrows:arrowCount,directionMode:style.direction,hasResults:!!results,resultTime:results?.timestamp??null,scale:style.scale};return summary;
}
const api={setFocusScope(value){focusScope=!!value;return apply();},get displayMode(){return displayMode;},setDisplayMode(value){if(!['global','coordinated'].includes(value)||value==='coordinated'&&!model.coordinatedLayouts)throw Error('不支持的展示模式');displayMode=value;return apply();},getResult(id,kind='links'){return results?.[kind]?.[id]?structuredClone(results[kind][id]):null;},get style(){return {...style};},get summary(){return {...summary};},setStyle(patch){const next={...style,...patch};if(!Number.isInteger(next.scale)||next.scale<1||next.scale>12)throw Error('倍率需为 112。');if(!['uniform','velocity','pressure','direction'].includes(next.mode)||!['none','topology','results'].includes(next.direction))throw Error('未知样式。');for(const k of ['color','missingColor','lowColor','highColor','arrowColor'])if(!/^#[0-9a-f]{6}$/i.test(next[k]))throw Error('颜色需为六位十六进制。');for(const k of ['opacity','roughness','metalness'])if(!finite(next[k])||next[k]<0||next[k]>1)throw Error('样式数值须在 01。');if(!finite(next.min)||!finite(next.max)||next.min>=next.max)throw Error('色带上限必须大于下限。');Object.assign(style,next);return apply();},setResults(input){const parsed=validateResults(input,model);results=parsed;return apply();},clearResults(){results=null;return apply();},dispose(){geo.dispose();mat.dispose();markerGeo.dispose();markerMat.dispose();ringGeo.dispose();ringMat.dispose();owned.forEach(m=>m.dispose());},pipes,nodes,arrows,scadaMarkers,scadaRings};
apply();return api;
}