// Bounded satellite atlas for existing coarse roof primitives; geometry preserved bitwise. import assert from 'node:assert/strict' import {readFileSync,existsSync} from 'node:fs' import {createHash} from 'node:crypto' import {PNG} from 'pngjs' import jpeg from 'jpeg-js' import {decodePrimitive} from './instance-safe-primitives.mjs' import {parseGlb,packGlb} from './instance-repeated-meshes.mjs' import {roofRasterSize,sharpenRoofRgba} from './roof-raster-quality.mjs' import {enuToGeodetic} from './roof-geodesy.mjs' const images=new Map(),sources=new Map(),origin=JSON.parse(readFileSync('tools/city-massing/city/cad-bim/instanced-active-input.json')).transform const mercator=(lon,lat,z)=>[(lon+180)/360*2**z,(1-Math.asinh(Math.tan(lat*Math.PI/180))/Math.PI)/2*2**z] function getTile(x,y,z){const path=`D:/GIS/GIS/yuanlingPic3/${z}/${x}/${2**z-1-y}.png`;if(images.has(path))return images.get(path) assert.ok(existsSync(path),'missing roof imagery '+path);const bytes=readFileSync(path),image=bytes[0]===0xff?jpeg.decode(bytes):PNG.sync.read(bytes) assert.equal(image.width,256);assert.equal(image.height,256) images.set(path,image);sources.set(path,{path,sha256:createHash('sha256').update(bytes).digest('hex')});if(images.size>128)images.delete(images.keys().next().value);return image} export function textureCoarseRoofs(bytes,{maxDimension=256,zoom=19,metresPerPixel=null,sharpenAmount=0,jpegQuality=85}={}){ assert.ok(Number.isInteger(jpegQuality)&&jpegQuality>=1&&jpegQuality<=100) assert.ok(Number.isFinite(sharpenAmount)&&sharpenAmount>=0&&sharpenAmount<=0.5) const {json,bin}=parseGlb(bytes),t=json.nodes[0].translation||[0,0,0] assert.equal(json.nodes.length,1);assert.ok(!json.nodes[0].rotation&&!json.nodes[0].scale&&!json.nodes[0].matrix) const roofs=json.meshes[0].primitives.filter(p=>/^(Far_Roof|Roof_|SatelliteRoof_)/.test(json.materials[p.material].name)) if(!roofs.length){ const ids=[...new Set(json.meshes.flatMap(m=>m.primitives.map(p=>p.material)))],map=new Map(ids.map((v,i)=>[v,i])) json.materials=ids.map(i=>json.materials[i]);for(const mesh of json.meshes)for(const p of mesh.primitives)p.material=map.get(p.material) return{bytes:packGlb(json,bin),report:{textured:false}} } const geometry=roofs.map(p=>({p,g:decodePrimitive(json,bin,p)})),low=[Infinity,Infinity],high=[-Infinity,-Infinity];let z=0,n=0 for(const {g}of geometry)for(let i=0;i{low[a]=Math.min(low[a],v);high[a]=Math.max(high[a],v)});z+=g.positions[i+1]+t[1];n++} z/=n;const span=high.map((v,a)=>v-low[a]),[width,height]=roofRasterSize(span,{maxDimension,metresPerPixel}) assert.ok(span.every(v=>v>0));const atlas=Buffer.alloc(width*height*4),used=new Set() for(let y=0;yv*256),tx=Math.floor(pixel[0]/256),ty=Math.floor(pixel[1]/256),image=getTile(tx,ty,zoom) used.add(`D:/GIS/GIS/yuanlingPic3/${zoom}/${tx}/${2**zoom-1-ty}.png`) const from=(Math.floor(pixel[1]-ty*256)*256+Math.floor(pixel[0]-tx*256))*4,to=(y*width+x)*4;image.data.copy(atlas,to,from,from+4) } const processed=sharpenRoofRgba(atlas,width,height,sharpenAmount) const imageBytes=jpeg.encode({data:processed,width,height},jpegQuality).data,chunks=[bin];let offset=bin.length function append(data){const padding=(-offset)&3;if(padding){chunks.push(Buffer.alloc(padding));offset+=padding}const i=json.bufferViews.length;json.bufferViews.push({buffer:0,byteOffset:offset,byteLength:data.length});chunks.push(data);offset+=data.length;return i} const image=json.images?.length||0;json.images||=[];json.images.push({mimeType:'image/jpeg',bufferView:append(imageBytes)}) const sampler=json.samplers?.length||0;json.samplers||=[];json.samplers.push({wrapS:33071,wrapT:33071,minFilter:9987,magFilter:9729}) const texture=json.textures?.length||0;json.textures||=[];json.textures.push({source:image,sampler}) const material=json.materials.length;json.materials.push({name:'SatelliteRoofAtlas',doubleSided:false,pbrMetallicRoughness:{baseColorFactor:[1,1,1,1],baseColorTexture:{index:texture},metallicFactor:0,roughnessFactor:.9}}) let uvBytes=0 for(const {p,g} of geometry){const values=Buffer.alloc(g.positions.length/3*8);uvBytes+=values.length for(let i=0;im.primitives.map(p=>p.material)))],map=new Map(ids.map((v,i)=>[v,i]));json.materials=ids.map(i=>json.materials[i]);for(const mesh of json.meshes)for(const p of mesh.primitives)p.material=map.get(p.material) json.buffers=[{byteLength:offset}] let decoded=0,w=width,h=height;while(true){decoded+=w*h*4;if(w===1&&h===1)break;w=Math.max(1,w>>1);h=Math.max(1,h>>1)} return{bytes:packGlb(json,Buffer.concat(chunks)),report:{textured:true,width,height,boundsENU:[low,high],planeHeight:z,zoom,rasterBytes:imageBytes.length,uvBytes,decodedTextureEstimate:decoded, processing:{requestedMetresPerPixel:metresPerPixel,actualMetresPerPixel:[span[0]/width,span[1]/height],maxDimension,sharpenAmount,sharpenDeltaLimit:8,jpegQuality,rawRgbaSha256:createHash('sha256').update(atlas).digest('hex'),processedRgbaSha256:createHash('sha256').update(processed).digest('hex')}, sourceTiles:[...used].map(path=>sources.get(path)),rasterSha256:createHash('sha256').update(imageBytes).digest('hex')}} }