import * as THREE from 'three'; /** Rounded capture footprint; the underlying survey grid remains untouched. */ export function roundedTerrainPlanes(axis: THREE.Vector3, start: number, end: number, halfWidth: number, radius: number) { const lateral = new THREE.Vector3(-axis.z, 0, axis.x); const corners = [[end-radius,halfWidth-radius,0],[start+radius,halfWidth-radius,90],[start+radius,-halfWidth+radius,180],[end-radius,-halfWidth+radius,270]]; const points: [number,number][]=[]; for(const [s,t,angle] of corners) for(let i=0;i<=8;i++) { const a=(angle+i*90/8)*Math.PI/180; points.push([s+radius*Math.cos(a),t+radius*Math.sin(a)]); } return points.map((p,i)=>{ const q=points[(i+1)%points.length],ds=q[0]-p[0],dt=q[1]-p[1]; const normal=axis.clone().multiplyScalar(-dt).addScaledVector(lateral,ds).normalize(); const origin=axis.clone().multiplyScalar(p[0]).addScaledVector(lateral,p[1]); return new THREE.Plane(normal,-normal.dot(origin)); }); } /** Landing-only road ribbons, joining the photographed road to ground at each abutment. */ export function buildRoadExtensions(terrain: THREE.Mesh, anchors: {name:string;kind:string;point:THREE.Vector3}[], axis: THREE.Vector3, top: number) { const piers=anchors.filter(a=>a.kind==='pier').toSorted((a,b)=>a.point.dot(axis)-b.point.dot(axis)); const abuts=anchors.filter(a=>a.kind==='abutment').toSorted((a,b)=>a.point.dot(axis)-b.point.dot(axis)); if(piers.length<2||abuts.length!==2)throw new Error('Road extensions require two abutments and at least two piers'); const group=new THREE.Group();group.name='landing-road-extensions'; const ray=new THREE.Raycaster(),down=new THREE.Vector3(0,-1,0),widthM=8; const sample=(x:number,z:number)=>{ ray.set(new THREE.Vector3(x,top+100,z),down); return ray.intersectObject(terrain,false)[0]?.point; }; const descriptions=[]; const ends=[{name:'A1–P1',points:[abuts[0].point,piers[0].point,piers[1].point],from:0,to:.58}, {name:'P6–A2',points:[piers.at(-2)!.point,piers.at(-1)!.point,abuts[1].point],from:.42,to:1}]; for(const end of ends){ const curve=new THREE.CatmullRomCurve3(end.points.map(p=>new THREE.Vector3(p.x,0,p.z)),false,'catmullrom',.2); const steps=80,positions:number[]=[],colors:number[]=[],indices:number[]=[]; let previous=-1,validSections=0,missingSections=0; for(let i=0;i<=steps;i++){ const t=i/steps,u=end.from+(end.to-end.from)*t,p=curve.getPoint(u),tangent=curve.getTangent(u).normalize(); const cross=new THREE.Vector3(-tangent.z,0,tangent.x); const offsets=Array.from({length:17},(_,j)=>-widthM/2+widthM*j/16); const hits=offsets.map(o=>sample(p.x+cross.x*o,p.z+cross.z*o)); if(hits.some(h=>!h)){previous=-1;missingSections++;continue;} const first=positions.length/3; const blend=end.from===0?1-THREE.MathUtils.smoothstep(t,.72,1):THREE.MathUtils.smoothstep(t,0,.28); const color=new THREE.Color('#dcd8c7').multiplyScalar(1+Math.sin(i*2.33)*.018); hits.forEach((hit,j)=>{positions.push(hit!.x,hit!.y+.15,hit!.z);colors.push(color.r,color.g,color.b,blend*(j===0||j===offsets.length-1?0:.96));}); if(previous>=0)for(let j=0;j