import * as THREE from 'three'; import type { ProjectFile } from '../src/core/project'; import { buildPier } from '../src/core/geometry'; import { buildAllPiers } from '../src/view/all-piers'; import { buildTerrainMapGroup, disposeTerrainMapGroup } from '../src/view/terrain-map'; import { buildAbut } from '../src/core/abut/geometry'; import { toAbutInput } from '../src/core/abut/parse'; import { buildMembers } from '../src/core/abut/members'; import { buildWings } from '../src/core/abut/wings'; import { buildAbutGroup } from '../src/view/abut-scene'; import { abutSoles, resolveBearingRule } from '../src/core/abut/bearing-table'; import { bridgePairs } from '../src/core/bridge-pairs'; import { buildRoadExtensions, roundedTerrainPlanes } from './landing-ground-treatment'; import { smoothLandingOutline } from './landing-smooth-outline'; /** Actual structure/terrain capture with optional landing-only road extensions. */ export async function renderLandingHero(project: ProjectFile, elevation: number, along = 0, endMarginM?: number, refined = false, smoothOutline = false, cutawayFrontM?: number) { const width = 2172, height = refined ? 870 : 724, aspect=width/height; const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, preserveDrawingBuffer: true }); renderer.setPixelRatio(2); renderer.setSize(width, height); renderer.setClearColor(0xffffff, 0); renderer.localClippingEnabled = true; const scene = new THREE.Scene(); scene.add(new THREE.HemisphereLight(0xffffff, 0x7c8a9a, 2.1)); const sun = new THREE.DirectionalLight(0xffffff, 2.4); sun.position.set(-100, 250, 150); scene.add(sun); const pairs=bridgePairs(project.piers.map(p=>p.input),project.abutments??[]); const entries = project.piers.map(p => ({ name: p.input.name, model: buildPier(p.input,{pairs}) })); const pivot = entries[0].model.pivot, grid = project.groundModel?.terrainGrid; const all = buildAllPiers(entries, grid); scene.add(all.piers); if (all.terrain) scene.add(all.terrain); const anchors = entries.map(e => ({ name: e.name, kind: 'pier', point: new THREE.Vector3(e.model.pivot.e - pivot.e, e.model.copingRefTop + 1, -(e.model.pivot.n - pivot.n)) })); for (const rec of project.abutments ?? []) { const model = buildAbut(toAbutInput(rec)); const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape); const soles = abutSoles(model, resolveBearingRule(rec.bearingRule), rec.bearingHeightMm, { pairs,slopeI:rec.slopeI, inputAzimuthDeg: Object.fromEntries(rec.bearings.filter(b => b.azimuthDeg !== undefined).map(b => [String(b.no), b.azimuthDeg!])), }); scene.add(buildAbutGroup(model, members, buildWings(model, members, rec.wings, rec.wingDims), pivot, rec.groundEl, grid, rec.slab, soles, undefined, !!all.terrain)); anchors.push({ name: rec.name, kind: 'abutment', point: new THREE.Vector3(rec.ip2.e-pivot.e, Math.max(members.parapetTopStartEl, members.parapetTopEndEl)+1, -(rec.ip2.n-pivot.n)) }); } const layers = project.groundModel?.mapLayers ?? []; await Promise.all(layers.map(l => new Promise((resolve, reject) => { const i = new Image(); i.onload = () => resolve(); i.onerror = reject; i.src = l.image; }))); let mapError = false; const maps = buildTerrainMapGroup(layers, pivot, 0, 'satellite', () => { mapError = true; }, grid); scene.add(maps); const deadline=performance.now()+15000; while (!maps.children.every(m => { const image=((m as THREE.Mesh).material as THREE.MeshBasicMaterial).map!.image as HTMLImageElement|null; return image?.complete && image.naturalWidth>0; })) { if(mapError || performance.now()>deadline) throw new Error('Map texture loading failed or timed out'); await new Promise(resolve=>setTimeout(resolve,20)); } if (mapError) throw new Error('Map texture loading failed'); scene.updateMatrixWorld(true); const box = new THREE.Box3().setFromObject(scene), target = box.getCenter(new THREE.Vector3()); const axis = new THREE.Vector3(entries.at(-1)!.model.pivot.e-pivot.e, 0, -(entries.at(-1)!.model.pivot.n-pivot.n)).normalize(); if (endMarginM !== undefined) { const ends=anchors.filter(a=>a.kind==='abutment').map(a=>a.point.dot(axis)); if(ends.length!==2)throw new Error('End margin requires both abutments'); const planes=refined ? roundedTerrainPlanes(axis,Math.min(...ends)-endMarginM,Math.max(...ends)+endMarginM,56,18) : [new THREE.Plane(axis.clone(),-Math.min(...ends)+endMarginM),new THREE.Plane(axis.clone().negate(),Math.max(...ends)+endMarginM)]; if(all.terrain)(all.terrain.material as THREE.Material).clippingPlanes=planes; maps.children.forEach(m=>{((m as THREE.Mesh).material as THREE.Material).clippingPlanes=planes;}); } const road=refined&&all.terrain ? buildRoadExtensions(all.terrain,anchors,axis,box.max.y) : null; if(road)scene.add(road.group); if(cutawayFrontM!==undefined){ const front=new THREE.Vector3(-axis.z,0,axis.x); const plane=new THREE.Plane(front.negate(),cutawayFrontM); for(const mesh of [all.terrain,...maps.children])if(mesh){ const material=(mesh as THREE.Mesh).material as THREE.Material; material.clippingPlanes=[...(material.clippingPlanes??[]),plane]; } } scene.updateMatrixWorld(true); const direction = new THREE.Vector3(-axis.z, elevation, axis.x).addScaledVector(axis, along).normalize(); const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, .1, 10000); camera.position.copy(target).addScaledVector(direction, 2000); camera.lookAt(target); camera.updateMatrixWorld(true); const bounds = new THREE.Box3(); scene.traverse(obj => { if (!(obj instanceof THREE.Mesh)) return; for (let p: THREE.Object3D | null = obj; p; p=p.parent) if (!p.visible) return; const pos = obj.geometry.getAttribute('position'); for (let i=0;ip.distanceToPoint(point)<0))continue; bounds.expandByPoint(point.applyMatrix4(camera.matrixWorldInverse)); } }); const w = Math.max(bounds.max.x-bounds.min.x, (bounds.max.y-bounds.min.y)*aspect)*1.12, h=w/aspect; const cx=(bounds.max.x+bounds.min.x)/2, cy=(bounds.max.y+bounds.min.y)/2; camera.left=cx-w/2; camera.right=cx+w/2; camera.top=cy+h/2; camera.bottom=cy-h/2; camera.updateProjectionMatrix(); let terrainCanvas:HTMLCanvasElement|undefined; if(smoothOutline){ const hidden=scene.children.filter(o=>o!==all.terrain&&o!==maps&&o!==road?.group&&!(o instanceof THREE.Light)&&o.visible); hidden.forEach(o=>{o.visible=false;});renderer.render(scene,camera); terrainCanvas=document.createElement('canvas');terrainCanvas.width=renderer.domElement.width;terrainCanvas.height=renderer.domElement.height; terrainCanvas.getContext('2d')!.drawImage(renderer.domElement,0,0); hidden.forEach(o=>{o.visible=true;}); } renderer.render(scene, camera); const points=anchors.map(a => { const p=a.point.clone().project(camera); return { name:a.name,kind:a.kind,x:+((p.x+1)*width/2).toFixed(2),y:+((1-p.y)*height/2).toFixed(2) }; }).sort((a,b)=>a.x-b.x); const ordered=anchors.toSorted((a,b)=>a.point.clone().project(camera).x-b.point.clone().project(camera).x); const ray=new THREE.Raycaster(), down=new THREE.Vector3(0,-1,0), spacing=(grid?.step??5)/2; const terrainFlow: ({x:number;y:number;el:number;world:[number,number,number]}|null)[]=[]; if(!all.terrain)throw new Error('Terrain mesh is required for surface-following light'); for(let i=0;i{if(o instanceof THREE.Mesh){o.geometry.dispose();for(const m of Array.isArray(o.material)?o.material:[o.material])m.dispose();}}); renderer.dispose();renderer.forceContextLoss();return result; }