/** Capture-only silhouette trimming. Samples real alpha; never fills missing survey data. */ export function smoothLandingOutline(source: HTMLCanvasElement, terrain: HTMLCanvasElement, preserveExposedStructures = false) { const width=source.width,height=source.height,scale=width/2172; const alpha=terrain.getContext('2d')!.getImageData(0,0,width,height).data; const tops:number[]=[],bottoms:number[]=[]; for(let x=0;x240){top=Math.min(top,y);bottom=y;} tops.push(top);bottoms.push(bottom); } const support=tops.map((top,x)=>bottoms[x]-top>60*scale?x:-1).filter(x=>x>=0); if(!support.length)throw new Error('No continuous terrain silhouette'); const left=support[0]+Math.ceil(12*scale),right=support.at(-1)!-Math.ceil(12*scale); const inset=Math.ceil(4*scale),radius=Math.ceil(18*scale),average=Math.ceil(8*scale); const eroded=(values:number[],upper:boolean)=>values.map((_,x)=>{ const local=values.slice(Math.max(0,x-radius-average),Math.min(width,x+radius+average+1)); return (upper?Math.max(...local):Math.min(...local))+(upper?inset:-inset); }); const upper=eroded(tops,true),lower=eroded(bottoms,false); const mean=(values:number[],x:number)=>{ const samples=values.slice(Math.max(0,x-average),Math.min(width,x+average+1)); return samples.reduce((a,b)=>a+b,0)/samples.length; }; const points:[number,number][]=[]; const cap=Math.ceil(24*scale),step=Math.ceil(12*scale); for(let x=left+cap;x<=right-cap;x+=step)points.push([x,mean(upper,x)]); points.push([right-cap,mean(upper,right-cap)],[right,mean(upper,right-cap)+cap],[right,mean(lower,right-cap)-cap],[right-cap,mean(lower,right-cap)]); for(let x=right-cap-step;x>=left+cap;x-=step)points.push([x,mean(lower,x)]); points.push([left+cap,mean(lower,left+cap)],[left,mean(lower,left+cap)-cap],[left,mean(upper,left+cap)+cap],[left+cap,mean(upper,left+cap)]); const mid=(a:[number,number],b:[number,number])=>[(a[0]+b[0])/2,(a[1]+b[1])/2]; const first=mid(points.at(-1)!,points[0]); let path=`M${first[0]} ${first[1]}`; points.forEach((p,i)=>{const next=mid(p,points[(i+1)%points.length]);path+=`Q${p[0]} ${p[1]} ${next[0]} ${next[1]}`;});path+='Z'; const canvas=document.createElement('canvas');canvas.width=width;canvas.height=height; const ctx=canvas.getContext('2d')!;ctx.drawImage(source,0,0); const original=ctx.getImageData(0,0,width,height).data; ctx.globalCompositeOperation='destination-in';ctx.fill(new Path2D(path)); const outputImage=ctx.getImageData(0,0,width,height),output=outputImage.data; // A cutaway may expose piles beyond the terrain silhouette. Preserve their rendered pixels. if(preserveExposedStructures){ for(let i=0;i0)output.set(original.subarray(i,i+4),i); ctx.putImageData(outputImage,0,0); } // Compare to terrain alpha only: smoothing should remove its rough border, not invent pixels. let before=0,after=0,addedPixels=0; for(let i=3;i240){before++;if(output[i]>240)after++;}if(original[i]===0&&output[i]>0)addedPixels++;} return {canvas,metadata:{method:'inward-alpha-envelope-quadratic',path,retainedTerrainRatio:after/before,addedOutsideSourcePixels:addedPixels}}; }