From 1622eaa2da4914d92557bf3298337da3348f7aa2 Mon Sep 17 00:00:00 2001 From: Huarch Date: Mon, 14 Sep 2026 16:57:41 +0800 Subject: [PATCH] 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. --- .../three-dimensional/zjb/v29/manifest.json | 10 +++ .../zjb/v29/network-style.mjs | 17 ++++- public/three-dimensional/zjb/v29/preview.html | 2 +- public/three-dimensional/zjb/v29/preview.mjs | 4 +- .../ThreeDimensionalScene.tsx | 2 +- .../ThreeDimensionalTimeline.tsx | 6 +- .../threeDimensional/sceneAssets.test.ts | 18 +++++ .../threeDimensional/sceneData.test.ts | 71 +++++++++++++++++++ src/components/threeDimensional/sceneData.ts | 37 +++++++++- 9 files changed, 154 insertions(+), 13 deletions(-) diff --git a/public/three-dimensional/zjb/v29/manifest.json b/public/three-dimensional/zjb/v29/manifest.json index fb73dde..33046aa 100644 --- a/public/three-dimensional/zjb/v29/manifest.json +++ b/public/three-dimensional/zjb/v29/manifest.json @@ -17006,6 +17006,16 @@ "map_roads", "map_pump_shell" ], + "networkContext": [ + "detail_roof", + "detail_facade", + "detail_interior", + "detail_ceiling", + "map_platforms", + "map_railway", + "map_roads", + "map_pump_shell" + ], "detailBase": [ "map_roof", "map_facade", diff --git a/public/three-dimensional/zjb/v29/network-style.mjs b/public/three-dimensional/zjb/v29/network-style.mjs index 0457bb9..7264c64 100644 --- a/public/three-dimensional/zjb/v29/network-style.mjs +++ b/public/three-dimensional/zjb/v29/network-style.mjs @@ -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; } diff --git a/public/three-dimensional/zjb/v29/preview.html b/public/three-dimensional/zjb/v29/preview.html index b6dc731..ae7304f 100644 --- a/public/three-dimensional/zjb/v29/preview.html +++ b/public/three-dimensional/zjb/v29/preview.html @@ -20,6 +20,6 @@
正在载入三维模型
- + diff --git a/public/three-dimensional/zjb/v29/preview.mjs b/public/three-dimensional/zjb/v29/preview.mjs index 38e9e23..b56d45d 100644 --- a/public/three-dimensional/zjb/v29/preview.mjs +++ b/public/three-dimensional/zjb/v29/preview.mjs @@ -5,7 +5,7 @@ import * as THREE from 'three'; import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js'; import {OrbitControls} from 'three/addons/controls/OrbitControls.js'; import {MeshoptDecoder} from './vendor/meshopt_decoder.module.js'; -import {attachNetworkStyle,DEFAULT_STYLE} from './network-style.mjs?v=32-default-scale'; +import {attachNetworkStyle,DEFAULT_STYLE} from './network-style.mjs?v=33-scada-markers'; import {cadToGltf} from './integration.mjs'; import {createAppearance,enhanceMaterials,grain,applyBuildingContext} from './appearance.mjs?v=30-context-opacity'; @@ -96,7 +96,7 @@ async function load(id){ } async function show(next,{frame=true}={}){ if(!['network','hydraulic','map','detail','pump','meters'].includes(next))throw Error('不支持的场景模式:'+next); - const token=++generation;mode=next;let ids=manifest.mapDefault; + const token=++generation;mode=next;let ids=next==='network'?manifest.networkContext:manifest.mapDefault; if(next==='detail')ids=[...manifest.mapDefault.filter(id=>!['map_roof','map_facade'].includes(id)),'detail_roof','detail_facade','detail_interior','detail_ceiling']; if(next==='pump')ids=['detail_pump_walls','detail_pump_access','detail_pump_auxiliary','detail_pump_piping_reference',...manifest.assets.filter(asset=>asset.id.startsWith('physical_')).map(asset=>asset.id)]; if(next==='meters')ids=['meter_dn150','valve_dn250','pump_symbol']; diff --git a/src/components/threeDimensional/ThreeDimensionalScene.tsx b/src/components/threeDimensional/ThreeDimensionalScene.tsx index e5c63c6..89de5da 100644 --- a/src/components/threeDimensional/ThreeDimensionalScene.tsx +++ b/src/components/threeDimensional/ThreeDimensionalScene.tsx @@ -38,7 +38,7 @@ import { type SceneRuntimeState, } from "./sceneProtocol"; -const SCENE_URL = "/three-dimensional/zjb/v29/preview.html?host=platform2&v=32-context-opacity"; +const SCENE_URL = "/three-dimensional/zjb/v29/preview.html?host=platform2&v=34-detailed-network-context"; const SCENE_LOAD_TIMEOUT_MS = 30_000; const initialRuntimeState: SceneRuntimeState = { diff --git a/src/components/threeDimensional/ThreeDimensionalTimeline.tsx b/src/components/threeDimensional/ThreeDimensionalTimeline.tsx index aaed8b1..52e1128 100644 --- a/src/components/threeDimensional/ThreeDimensionalTimeline.tsx +++ b/src/components/threeDimensional/ThreeDimensionalTimeline.tsx @@ -225,14 +225,14 @@ export function ThreeDimensionalTimeline({ "pointer-events-auto relative z-10 w-full max-w-[950px] rounded-2xl opacity-95 transition-opacity duration-200 hover:opacity-100", )} > -
-