Persist versioned models and validated network metadata in IndexedDB. Show scene download progress and recoverable errors with the existing frosted material. Connect existing skeletons to pending navigation and route boundaries while preserving the map. Previously only dynamic imports showed fallbacks, and rejected model promises prevented retries. Verified: 32 focused unit tests; 4 navigation and scene browser checks; 7 cache browser checks including invalid-metadata recovery; isolated production build and type checks.
48 lines
4.0 KiB
JavaScript
48 lines
4.0 KiB
JavaScript
// Framework-independent data adapter. Does not parse or modify the project's INP.
|
|
export const CAD_ORIGIN=Object.freeze([513800,2344450]);
|
|
// This decoder runs before IDB writes, including when checking an existing entry.
|
|
// A parseable HTTP 200 error response must not become a permanent model cache.
|
|
export function parseNetworkMetadata(data,modelId){
|
|
const model=JSON.parse(new TextDecoder().decode(data));
|
|
const arrays=['nodes','links','pipeInstances','nodeInstances','bendInstances','cadEquipmentBindings','pumpEntityBindings','cadAttachments'];
|
|
const objects=['layouts','connectivity','coordination','coordinatedLayouts','verticalCoordination'];
|
|
if(!model||model.modelId!==modelId||!arrays.every(key=>Array.isArray(model[key]))||!objects.every(key=>model[key]&&typeof model[key]==='object'&&!Array.isArray(model[key]))||!model.nodes.length||!model.links.length)throw Error('管网元数据格式无效,请重新载入场景。');
|
|
return model;
|
|
}
|
|
export function cadToGltf(point,displayZ=0){
|
|
if(!Array.isArray(point)||point.length<2||!point.slice(0,2).every(Number.isFinite)||!Number.isFinite(displayZ))throw Error('Expected finite CAD XY and an explicit display height.');
|
|
return [point[0]-CAD_ORIGIN[0],displayZ,-(point[1]-CAD_ORIGIN[1])];
|
|
}
|
|
// Supply longitude/latitude OF CAD_ORIGIN, not the old OSM anchor. This function
|
|
// deliberately cannot guess CRS, accept the disabled legacy candidate, or add 23.9deg.
|
|
export function mapModelMatrixElements(mercatorOrigin,meterScale,{rotationDeg=0,horizontalScale=1}={}){
|
|
if(![mercatorOrigin?.x,mercatorOrigin?.y,mercatorOrigin?.z,meterScale,rotationDeg,horizontalScale].every(Number.isFinite)||meterScale<=0||horizontalScale<=0)throw Error('Confirmed Mercator origin and positive scales required.');
|
|
const a=rotationDeg*Math.PI/180,c=Math.cos(a)*meterScale*horizontalScale,s=Math.sin(a)*meterScale*horizontalScale;
|
|
// Column-major THREE.Matrix4; glTF +Y up, -Z CAD north; Mercator +Y south.
|
|
return [c,-s,0,0, 0,0,meterScale,0, s,c,0,0, mercatorOrigin.x,mercatorOrigin.y,mercatorOrigin.z,1];
|
|
}
|
|
export function resolveMeterPlacements(bindingFile,{inpSha256,links,displayHeight=0.35}){
|
|
if(inpSha256!==bindingFile.compatibleInpSha256)throw Error('INP version mismatch: reconcile the equipment mapping before placement.');
|
|
const byId=new Map(links.map(link=>[String(link.id),link]));if(byId.size!==links.length)throw Error('Duplicate INP link IDs.');
|
|
const placements=[],issues=[];
|
|
for(const meter of bindingFile.meters){
|
|
const link=byId.get(meter.inpLinkId);
|
|
if(!link){issues.push({assetId:meter.assetId,reason:'missing_host_link',id:meter.inpLinkId});continue;}
|
|
const pts=link.coordinates;
|
|
if(!Array.isArray(pts)||pts.length<2||pts.some(p=>!Array.isArray(p)||p.length<2||!p.slice(0,2).every(Number.isFinite))||!Number.isFinite(link.diameterMm)||link.diameterMm<=0){issues.push({assetId:meter.assetId,reason:'invalid_host_geometry_or_diameter'});continue;}
|
|
let best;
|
|
for(let i=1;i<pts.length;i++){
|
|
const a=pts[i-1],b=pts[i],dx=b[0]-a[0],dy=b[1]-a[1],length=Math.hypot(dx,dy);if(length<1e-8)continue;
|
|
const t=Math.max(0,Math.min(1,((meter.cadXY[0]-a[0])*dx+(meter.cadXY[1]-a[1])*dy)/(length*length)));
|
|
const distance=Math.hypot(a[0]+t*dx-meter.cadXY[0],a[1]+t*dy-meter.cadXY[1]);
|
|
if(!best||distance<best.distance)best={a,b,dx,dy,length,t,distance};
|
|
}
|
|
if(!best){issues.push({assetId:meter.assetId,reason:'zero_length_host'});continue;}
|
|
if(best.distance>.25){issues.push({assetId:meter.assetId,reason:'CAD_host_registration_exceeds_0.25m',distanceM:best.distance});continue;}
|
|
const half=Math.min(.33,best.length*.2),t=Math.max(half/best.length,Math.min(1-half/best.length,best.t));
|
|
const center=[best.a[0]+best.dx*t,best.a[1]+best.dy*t];
|
|
placements.push({assetId:meter.assetId,prototype:meter.prototype,hostLinkId:meter.inpLinkId,position:cadToGltf(center,displayHeight),direction:[best.dx/best.length,0,-best.dy/best.length],scale:[half/.33,link.diameterMm/150,link.diameterMm/150],portDistanceM:2*half,createsHydraulicLink:false,scadaId:null});
|
|
}
|
|
return {placements,issues};
|
|
}
|