/* ============================================================================= * B06_Section_UI_Cross_Box_Geom.ts * BOX암거 구체 **기하 계산** — 그리기(`_Cross_Box.ts`)와 분리(700줄 제한). * 백엔드 `section.box` 제원을 좌표로 옮긴다(치수 결정 금지). * * 구성(2026-08-25 사용자 확정): * · 박스 축 = 계류 방향이라 횡단도에는 **종단 절단면**(상판·내공·저판)이 보인다. * · 상판 윗면 = **노면 − 복토 0.5m**(별표2 교차 참조), 구체는 수평 설치(교본 3장). * · 저판은 **날개벽 투영**(길이×cos각)만큼 계류 상·하류로 길어진다 — 세월교와 같은 산식. * · 내공(유로)은 비워 둔다 — 관 위 성토 채움처럼 면을 칠하지 않는다. * · 구체가 노면 아래 묻히므로 **계획선은 자르지 않는다** — 노견 밖 성토 비탈이 실제로 * 존재한다(세월교는 측벽이 비탈을 대신해 잘랐다). * 부재 두께는 세월교 승계(측벽 0.2·상판 0.3·저판 0.3)이며 백엔드가 실어 보낸다. * ========================================================================== */ import type { BoxSet, CrossSection, SectionSample } from "./B06_Section_Api_Fetch"; import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types"; import { designInterpolator, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve"; export interface BoxLayout { box: BoxSet; /** 상판 폴리곤(노견~노견). */ topSlab: OffsetPoint[]; /** 저판 폴리곤 — 날개벽 투영만큼 각 측 연장. */ bottomSlab: OffsetPoint[]; /** 내공(유로) 사각 — 비워 두고 윤곽선·라벨만 쓴다. */ barrel: OffsetPoint[]; /** 상판 윗면·저판 밑면 표고. */ topElevation: number; bottomElevation: number; /** 저판 전체 길이(m) — 노폭 + 유입·유출 날개벽 투영. */ slabLengthM: number; /** 구체 바닥과 계류 하상(원지반 최저)의 고저차(m). +면 구체가 하상 위로 떠 있다. */ bedGapM: number; label: { at: OffsetPoint; text: string }; } /** 노면 표고 — 설계선이 있으면 그 곡선, 없으면 노견 직선(세월교와 같은 규칙). */ function roadTopFactory( section: CrossSection, left: { offset_m: number; elevation_m: number }, right: { offset_m: number; elevation_m: number }, ): (offset: number) => number { const designAt = designInterpolator(section.design?.design_line); if (designAt) return designAt; const span = left.offset_m - right.offset_m; return (offset: number): number => { if (Math.abs(span) < 1e-6) return left.elevation_m; const t = (offset - right.offset_m) / span; return right.elevation_m + (left.elevation_m - right.elevation_m) * t; }; } /** 사각 폴리곤(좌상 → 우상 → 우하 → 좌하). */ function rect(left: number, right: number, top: number, bottom: number): OffsetPoint[] { return [ { offset: left, elevation: top }, { offset: right, elevation: top }, { offset: right, elevation: bottom }, { offset: left, elevation: bottom }, ]; } /** BOX암거 구체 기하. 제원·설계선·지반이 부족하면 null. */ export function computeBoxLayout( section: CrossSection, groundSamples: SectionSample[], ): BoxLayout | null { const box = section.box; if (!box) return null; const edges = section.design?.road_edges; if (!edges) return null; const groundAt = groundInterpolator(groundSamples); if (!groundAt) return null; // 좌표 규약: +offset = 좌측. 유입 = 상단측(미상이면 좌측 폴백). const inletOnLeft = (section.uphill_side ?? "left") === "left"; const roadTopAt = roadTopFactory(section, edges.left, edges.right); const leftEdge = edges.left.offset_m; const rightEdge = edges.right.offset_m; if (!(leftEdge > rightEdge)) return null; // 구체는 수평 설치(교본 3장) — 노면이 가장 낮은 자리에서도 복토를 확보한다. const roadLow = Math.min(roadTopAt(leftEdge), roadTopAt(rightEdge), roadTopAt(0)); const topElevation = roadLow - box.cover_m; const barrelTop = topElevation - box.top_thickness_m; const barrelBottom = barrelTop - box.inner_height_m; const bottomElevation = barrelBottom - box.slab_thickness_m; // 저판 연장 — 유입·유출 날개벽 각도가 만든 투영량(백엔드 계산값). const leftExtend = Math.max((inletOnLeft ? box.wing_in : box.wing_out).slab_extend_m, 0); const rightExtend = Math.max((inletOnLeft ? box.wing_out : box.wing_in).slab_extend_m, 0); const slabLeft = leftEdge + leftExtend; const slabRight = rightEdge - rightExtend; // 계류 하상과의 고저차 — 터파기·성토 판정은 후속이라 값만 넘긴다. let bed = Math.min(groundAt(leftEdge), groundAt(rightEdge)); for (let offset = rightEdge; offset <= leftEdge; offset += 0.25) { bed = Math.min(bed, groundAt(offset)); } return { box, topSlab: rect(leftEdge, rightEdge, topElevation, barrelTop), bottomSlab: rect(slabLeft, slabRight, barrelBottom, bottomElevation), barrel: rect(leftEdge, rightEdge, barrelTop, barrelBottom), topElevation, bottomElevation, slabLengthM: Math.abs(slabLeft - slabRight), bedGapM: Number((bottomElevation - bed).toFixed(3)), label: { at: { offset: (leftEdge + rightEdge) / 2, elevation: (barrelTop + barrelBottom) / 2 }, text: `BOX ${box.inner_width_m.toFixed(1)}×${box.inner_height_m.toFixed(1)}`, }, }; }