Files
Pier_v1/scripts/landing-ground-treatment.ts
T
ypshimandClaude Haiku 5.5 fc520ec03d feat: 프로젝트 공통 지반모델·교대 지반 통합·재료표/수량 진입·옥성교 랜딩 히어로 (11-06~11-09 작업분)
공용 파일(main.ts·index.html·style.css)에서 서로 얽혀 있어 한 커밋으로 묶음.
- 지반: ① 형상 프로젝트 지반모델 관리창(생성·JSON 불러오기·교각별 비교·선택 적용),
  프로젝트 JSON 내장 groundModel 추출, 교대 8행 통합 적용·공통지형 중복격자 제외
- 재료표·수량: 상단 ⑤ 메뉴·배근 바로가기, 결과 교각/탭 기억, 수량 워크북·정합 보정
- 랜딩: 옥성교 6교각 히어로 이미지·투영 강조점·좌→우 광선·지형 라운드/도로 연결
- 검증 스크립트(verify-project-ground/ground-abutments/okseong-user-files/quantity-*/landing-hero 등)
정본: DOC/04_실행/실행_*_2026-11-09_v0.1.00.md

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-09 18:01:02 +09:00

58 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<offsets.length-1;j++)indices.push(previous+j,first+j,previous+j+1,previous+j+1,first+j,first+j+1);
previous=first;validSections++;
}
const geometry=new THREE.BufferGeometry();geometry.setAttribute('position',new THREE.Float32BufferAttribute(positions,3));geometry.setAttribute('color',new THREE.Float32BufferAttribute(colors,4));geometry.setIndex(indices);geometry.computeVertexNormals();
const mesh=new THREE.Mesh(geometry,new THREE.MeshBasicMaterial({vertexColors:true,side:THREE.DoubleSide,transparent:true,depthWrite:false,polygonOffset:true,polygonOffsetFactor:-2,polygonOffsetUnits:-2}));
mesh.name=end.name;mesh.renderOrder=2;group.add(mesh);
descriptions.push({name:end.name,widthM,validSections,missingSections,triangles:indices.length/3,visualizationOnly:true});
}
return {group,descriptions};
}