@@ -40,6 +40,8 @@ export function attachNetworkStyle(group,model){
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 ( '当前模型支持整数倍率 1– 12。' ) ;
@@ -57,16 +59,25 @@ export function attachNetworkStyle(group,model){
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 arrowCount = 0 ;
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 , arrows : arrowCount , directionMode : style . direction , hasResults : ! ! results , resultTime : results ? . timestamp ? ? null , scale : style . scale } ; return summary ;
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 ( '倍率需为 1– 12。' ) ; 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 ( '样式数值须在 0– 1。' ) ; 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 ( ) ; owned . forEach ( m => m . dispose ( ) ) ; } , pipes , nodes , arrows } ;
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 ( '倍率需为 1– 12。' ) ; 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 ( '样式数值须在 0– 1。' ) ; 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 ;
}