feat(3d): refine scene controls and camera views
Keep the control panel geometry stable when toggling building context, and use accessible switch controls to prevent focus-driven movement. Add browser coverage for camera framing, opacity, responsive layout, and the default 6x pipe scale.
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
import {createRenderEffects} from './render-effects.mjs';
|
||||
import {createCameraNavigation,framePose} from './camera-navigation.mjs?v=28';
|
||||
import {createCameraNavigation,framePose} from './camera-navigation.mjs?v=31-camera-presets';
|
||||
import {createAssetInspector} from './asset-inspector.mjs?v=29-platform2';
|
||||
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=28b';
|
||||
import {attachNetworkStyle,DEFAULT_STYLE} from './network-style.mjs?v=32-default-scale';
|
||||
import {cadToGltf} from './integration.mjs';
|
||||
import {createAppearance,enhanceMaterials,grain,applyBuildingContext} from './appearance.mjs?v=29';
|
||||
import {createAppearance,enhanceMaterials,grain,applyBuildingContext} from './appearance.mjs?v=30-context-opacity';
|
||||
|
||||
const HOST_CHANNEL='tjwater:zjb-scene',HOST_VERSION=2,PROJECT_CODE='zjb',MODEL_ID='zjb-water-network-v23';
|
||||
let net,networkRoot,networkStyle,navigation,inspector;
|
||||
let mode='network',generation=0,contextVisible=true,roofVisible=true;
|
||||
let sceneStatus='正在载入三维模型';
|
||||
let cameraState={active:null,note:'正在准备观察位置',views:[]};
|
||||
let appearanceState={preset:'day',exposure:.95,shadows:true,effects:true,quality:'standard'};
|
||||
let appearanceState={preset:'day',exposure:.95,contextOpacity:.4,shadows:true,effects:true,quality:'standard'};
|
||||
|
||||
function postHost(type,detail={}){
|
||||
if(window.parent===window)return;
|
||||
@@ -37,7 +37,7 @@ function runtimeState(){
|
||||
function postState(){if(net&&networkStyle)postHost('scene-state',{state:runtimeState()});}
|
||||
function setStatus(value){sceneStatus=value;postState();}
|
||||
|
||||
window.webQA={errors:[],loaded:[],mode:'network'};
|
||||
window.webQA={errors:[],loaded:[],mode:'network',contextVisible};
|
||||
window.addEventListener('error',event=>{const message=String(event.message||'三维场景运行错误');window.webQA.errors.push(message);postHost('error',{message});});
|
||||
window.addEventListener('unhandledrejection',event=>{const message=String(event.reason||'三维场景异步任务失败');window.webQA.errors.push(message);postHost('error',{message});});
|
||||
|
||||
@@ -78,6 +78,7 @@ function fit(){
|
||||
if(box.isEmpty())return;
|
||||
appearance.frame(box,mode);
|
||||
const sphere=box.getBoundingSphere(new THREE.Sphere());controls.target.copy(sphere.center);
|
||||
camera.up.copy(new THREE.Vector3(0,1,0));
|
||||
camera.position.copy(sphere.center).add(new THREE.Vector3(.65,.72,1).normalize().multiplyScalar(sphere.radius*3.1/Math.min(1,camera.aspect)));
|
||||
camera.near=Math.max(.005,sphere.radius/5000);camera.far=sphere.radius*30+100;camera.updateProjectionMatrix();controls.update();
|
||||
}
|
||||
@@ -103,7 +104,7 @@ async function show(next,{frame=true}={}){
|
||||
const objects=await Promise.all(ids.map(load));if(token!==generation)return;
|
||||
root.clear();objects.forEach((object,index)=>{
|
||||
object.position.set(next==='meters'?index*1.5:0,0,0);object.visible=true;root.add(object);
|
||||
object.traverse(mesh=>{if(mesh.isMesh)for(const material of Array.isArray(mesh.material)?mesh.material:[mesh.material])applyBuildingContext(material,['network','hydraulic'].includes(next));});
|
||||
object.traverse(mesh=>{if(mesh.isMesh)for(const material of Array.isArray(mesh.material)?mesh.material:[mesh.material])applyBuildingContext(material,['network','hydraulic'].includes(next),appearanceState.contextOpacity);});
|
||||
});
|
||||
if(networkRoot)networkRoot.visible=['network','hydraulic'].includes(next);
|
||||
networkStyle?.setFocusScope(next==='hydraulic');applyVisibility();inspector?.highlight();if(frame)fit();
|
||||
@@ -115,12 +116,14 @@ function setDisplayMode(value){networkStyle.setDisplayMode(value);navigation?.st
|
||||
function applyAppearance(patch){
|
||||
if(patch.preset!==undefined){if(!['day','studio','evening'].includes(patch.preset))throw Error('不支持的光照场景');appearance.preset(patch.preset);appearanceState.preset=patch.preset;appearanceState.exposure=appearance.state.exposure;}
|
||||
if(patch.exposure!==undefined){if(!Number.isFinite(patch.exposure)||patch.exposure<.55||patch.exposure>1.6)throw Error('亮度超出有效范围');appearance.exposure(patch.exposure);appearanceState.exposure=patch.exposure;}
|
||||
if(patch.contextOpacity!==undefined){if(!Number.isFinite(patch.contextOpacity)||patch.contextOpacity<0||patch.contextOpacity>1)throw Error('建筑背景透明度超出有效范围');appearanceState.contextOpacity=patch.contextOpacity;for(const object of root.children)object.traverse(mesh=>{if(mesh.isMesh)for(const material of Array.isArray(mesh.material)?mesh.material:[mesh.material])applyBuildingContext(material,['network','hydraulic'].includes(mode),appearanceState.contextOpacity);});appearance.refresh();}
|
||||
if(patch.shadows!==undefined){appearance.shadows(Boolean(patch.shadows));appearanceState.shadows=Boolean(patch.shadows);}
|
||||
if(patch.effects!==undefined){effects.setEnabled(Boolean(patch.effects));appearanceState.effects=Boolean(patch.effects);}
|
||||
if(patch.quality!==undefined){if(!['standard','high'].includes(patch.quality))throw Error('不支持的画质');effects.setQuality(patch.quality);appearanceState.quality=patch.quality;localStorage.setItem('zjb-render-quality-v27',patch.quality);}
|
||||
window.webQA.appearance={...appearanceState};
|
||||
postState();
|
||||
}
|
||||
function toggleContext(){contextVisible=!contextVisible;applyVisibility();appearance.refresh();fit();postState();}
|
||||
function toggleContext(){contextVisible=!contextVisible;window.webQA.contextVisible=contextVisible;applyVisibility();appearance.refresh();postState();}
|
||||
function toggleRoof(){roofVisible=!roofVisible;applyVisibility();appearance.refresh();postState();}
|
||||
function resizeView(){const current=viewport();camera.aspect=current.width/current.height;camera.updateProjectionMatrix();renderer.setSize(current.width,current.height);effects.resize(current.width,current.height);}
|
||||
addEventListener('resize',resizeView);
|
||||
@@ -146,7 +149,7 @@ async function locateAsset(item){
|
||||
if(mode!==targetMode)await show(targetMode,{frame:false});if(request!==locateRequest||generation!==expected)return;
|
||||
setDisplayMode(targetMode==='hydraulic'?'coordinated':'global');contextVisible=targetMode!=='hydraulic';applyVisibility();
|
||||
const box=inspector.bounds(item);if(box.isEmpty())return;box.expandByScalar(item.link?.kind==='PUMPS'?.6:1);
|
||||
const pose=framePose(box,camera.aspect,{padding:1.3});controls.target.fromArray(pose.target);camera.position.fromArray(pose.position);camera.far=20000;controls.update();appearance.frame(box,targetMode);inspector.highlight();postState();
|
||||
const pose=framePose(box,camera.aspect,{padding:1.3});camera.up.fromArray(pose.up??[0,1,0]).normalize();controls.target.fromArray(pose.target);camera.position.fromArray(pose.position);camera.far=20000;controls.update();appearance.frame(box,targetMode);inspector.highlight();postState();
|
||||
}
|
||||
|
||||
let lastFrame=performance.now(),frameTimes=[];
|
||||
@@ -155,15 +158,16 @@ renderer.setAnimationLoop(()=>{
|
||||
if(frameTimes.length>=60)window.webQA.performance={samples:frameTimes.length,meanFps:1000/(frameTimes.reduce((sum,value)=>sum+value,0)/frameTimes.length),viewport:viewport(),quality:effects.state.quality};
|
||||
renderer.info.reset();navigation?.tick(performance.now());controls.update();const near=Math.max(.02,camera.position.distanceTo(controls.target)/150);
|
||||
if(Math.abs(camera.near-near)>.001){camera.near=near;camera.updateProjectionMatrix();}
|
||||
effects.render(mode);window.webQA.drawCalls=renderer.info.render.calls;window.webQA.triangles=renderer.info.render.triangles;document.getElementById('qa').textContent=JSON.stringify(window.webQA);
|
||||
effects.render(mode);window.webQA.drawCalls=renderer.info.render.calls;window.webQA.triangles=renderer.info.render.triangles;window.webQA.camera={active:cameraState.active,position:camera.position.toArray(),target:controls.target.toArray(),up:camera.up.toArray()};document.getElementById('qa').textContent=JSON.stringify(window.webQA);
|
||||
});
|
||||
|
||||
net=await (await fetch(manifest.networkModel.metadata,{cache:'no-store'})).json();
|
||||
const built=await loader.loadAsync(manifest.networkModel.file+'?v='+manifest.networkModel.sha256);networkRoot=built.scene;enhanceMaterials(networkRoot);scene.add(networkRoot);
|
||||
networkStyle=attachNetworkStyle(networkRoot,net);grain(networkStyle.pipes.material,{frequency:140,amplitude:.00004,variation:.08});
|
||||
try{const saved=localStorage.getItem('zjb-network-style-v23');if(saved)networkStyle.setStyle(JSON.parse(saved));}catch{}
|
||||
try{const saved=localStorage.getItem('zjb-network-style-v24');if(saved)networkStyle.setStyle(JSON.parse(saved));}catch{}
|
||||
try{const quality=localStorage.getItem('zjb-render-quality-v27')??'standard';effects.setQuality(quality);appearanceState.quality=quality;}catch{effects.setQuality('standard');}
|
||||
window.webQA.network={...net.connectivity,meters:net.cadEquipmentBindings.length,modelId:net.modelId,runtimeInpRequired:false};
|
||||
window.webQA.networkStyle={...networkStyle.summary,style:networkStyle.style};window.webQA.appearance={...appearanceState};
|
||||
navigation=createCameraNavigation({camera,controls,model:net,networkRoot,root,show,getMode:()=>mode,appearance,getDisplayMode:()=>networkStyle.displayMode,setDisplayMode,onChange(state){
|
||||
cameraState={active:state.active,note:state.note,views:state.views.map(view=>({id:view.id,label:view.label,mode:view.mode,note:view.note,saved:view.id.startsWith('saved-')}))};postState();
|
||||
}});
|
||||
@@ -185,8 +189,8 @@ window.addEventListener('message',async event=>{
|
||||
else if(command.name==='visit-camera')await navigation.visit(command.viewId);
|
||||
else if(command.name==='save-camera')navigation.save(command.label);
|
||||
else if(command.name==='remove-camera')navigation.remove(command.viewId);
|
||||
else if(command.name==='set-style'){networkStyle.setStyle(command.patch);localStorage.setItem('zjb-network-style-v23',JSON.stringify(networkStyle.style));updateStyle();}
|
||||
else if(command.name==='reset-style'){networkStyle.setStyle(DEFAULT_STYLE);localStorage.removeItem('zjb-network-style-v23');updateStyle();}
|
||||
else if(command.name==='set-style'){networkStyle.setStyle(command.patch);localStorage.setItem('zjb-network-style-v24',JSON.stringify(networkStyle.style));updateStyle();}
|
||||
else if(command.name==='reset-style'){networkStyle.setStyle(DEFAULT_STYLE);localStorage.removeItem('zjb-network-style-v24');updateStyle();}
|
||||
else if(command.name==='set-display-mode')setDisplayMode(command.mode);
|
||||
else if(command.name==='set-appearance')applyAppearance(command.patch);
|
||||
else if(command.name==='toggle-context')toggleContext();
|
||||
|
||||
Reference in New Issue
Block a user