공용 파일(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>
58 lines
3.9 KiB
TypeScript
58 lines
3.9 KiB
TypeScript
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};
|
||
}
|