/* ============================================================================= * B06_Section_Cross_Ford_Pavement.ts * 물넘이포장 기하 — 유입 쪽 · 노면 양 끝 · 월류 높이선 · 유출측 성토사면 선(287-2 · 294 · 315). * 그리기는 `UI_Cross_Ford_Pavement.ts` · 3D 띠(B05)도 같은 선을 씀. * ========================================================================== */ import type { CrossDesign } from "../B06_Section_Api_Fetch"; import type { OffsetPoint } from "./B06_Section_Cross_Culvert_Types"; /** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */ export interface FordPavementSpec { /** 관 지점 누가거리 — 폭 절반 걸친 옆 카드도 같은 값. */ chainage_m?: number; span_m: number; /** 월류 높이(m) — 노면 위 점선. 없으면 점선을 안 그림(수치를 지어내지 않는다). */ depth_m: number | null; /** 노면 경사(%) — 경사각도 환산 · 서버 설계 노면이 이 값으로 기욺. */ slope_pct: number | null; /** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */ defaulted?: string[]; /** 도수로 · 돌붙임(`ford_chute_{n}_*` · 136-2) — 유출측 성토사면 띠 · 길이 = 사면 따라(286-3 · 비면 원지반까지). */ chute?: { structure: string; thickness_m: number; length_m?: number }; } /** 유입(상류)이 좌측(+offset)인가 — 기울인 노면이면 높은 노면 끝 · 아니면 설계선 양 끝 표고가 높은 * 쪽(286-2) · 같으면 단면유형. */ export function inflowLeft(design: CrossDesign): boolean { const edges = design.road_edges; if (design.tilted && edges && Math.abs(edges.left.elevation_m - edges.right.elevation_m) > 1e-6) return edges.left.elevation_m > edges.right.elevation_m; const line = [...(design.design_line ?? [])].sort((a, b) => a.offset_m - b.offset_m); const gap = line.length >= 2 ? line[line.length - 1].elevation_m - line[0].elevation_m : 0; return Math.abs(gap) > 1e-3 ? gap > 0 : design.section_mode !== "right_cut"; } /** 콘크리트 · 노면 양 끝 — 낮은 쪽을 늘였으면(294-3 `tilt_reach`) 그 쪽은 원지반과 만난 자리까지. */ export function fordEdges(design: CrossDesign): CrossDesign["road_edges"] | undefined { const edges = design.road_edges; const reach = design.tilt_reach; if (!edges || !reach) return edges; const end = { offset_m: reach.offset_m, elevation_m: reach.elevation_m }; return reach.side === "left" ? { ...edges, left: end } : { ...edges, right: end }; } /** * 월류 높이선(315-2) — 노면 양 끝 위 월류 높이 · 노면과 같은 각도. 성토 쪽(노면 끝 밖 설계선이 내려감)은 * 성토 사면선을 노면 끝 너머 위 · 안쪽으로 늘여 월류 높이선과 만나는 자리에서 끝 → 노면 끝까지 그 연장선. * 절토 쪽 · 늘인 노면 끝(294-3)은 노면 끝 위 그대로. 좌(+offset) → 우 차례. */ export function overflowLine(design: CrossDesign, depth: number): OffsetPoint[] { const edges = fordEdges(design); if (!edges) return []; const { left, right } = edges; const span = Math.abs(left.offset_m - right.offset_m); const side = (edge: typeof left, other: typeof left): OffsetPoint[] => { const top = { offset: edge.offset_m, elevation: edge.elevation_m + depth }; const outward = Math.sign(edge.offset_m - other.offset_m) || 1; const next = (design.design_line ?? []) .filter((p) => (p.offset_m - edge.offset_m) * outward > 1e-6) .sort((a, b) => (a.offset_m - b.offset_m) * outward)[0]; const cut = edge.offset_m === design.tilt_reach?.offset_m; // 늘인 끝 = 원지반(294-3 · 절토) if (cut || !next || next.elevation_m >= edge.elevation_m - 1e-6 || span < 1e-9) return [top]; // 안쪽 d m 에서 성토선 = edge + fill·d · 월류선 = edge + depth + road·d → d = depth / (fill − road) const fill = (edge.elevation_m - next.elevation_m) / Math.abs(next.offset_m - edge.offset_m); const road = (other.elevation_m - edge.elevation_m) / span; if (fill <= road + 1e-9) return [top]; const d = Math.min(depth / (fill - road), span); const meet = { offset: edge.offset_m - outward * d, elevation: edge.elevation_m + fill * d }; return [{ offset: edge.offset_m, elevation: edge.elevation_m }, meet]; }; return [...side(left, right), ...side(right, left).reverse()]; } /** 원지반 꺾은선의 그 오프셋 표고(밖은 끝값). */ function groundZ(ground: readonly OffsetPoint[], offset: number): number | null { if (!ground.length) return null; if (offset <= ground[0].offset) return ground[0].elevation; for (let i = 1; i < ground.length; i += 1) { const [a, b] = [ground[i - 1], ground[i]]; if (offset > b.offset) continue; const span = b.offset - a.offset; return span > 1e-9 ? a.elevation + ((b.elevation - a.elevation) * (offset - a.offset)) / span : b.elevation; } return ground[ground.length - 1].elevation; } /** * 유출측(낮은 쪽) 성토사면 선(136-2 · 294-4) — 낮은 쪽 끝단(늘인 노면 끝 · 아니면 유출 쪽 노견 끝)에서 * 설계선을 바깥으로 따라 내려가는 동안만(오르면 절토라 멈춤) · 원지반(`ground`)에 닿으면 끝. * 성토가 없으면(끝단이 이미 원지반 · 양측 절토 물넘이) 점 하나 = 띠 없음. */ export function outletFillLine( design: CrossDesign, ground?: readonly OffsetPoint[], ): OffsetPoint[] { const edges = fordEdges(design); if (!edges || !design.design_line?.length) return []; const reach = design.tilt_reach; const outLeft = reach ? reach.side === "left" : !inflowLeft(design); // +offset = 좌측 const outward = outLeft ? 1 : -1; const edge = outLeft ? Math.max(edges.left.offset_m, edges.right.offset_m) : Math.min(edges.left.offset_m, edges.right.offset_m); const points = design.design_line .filter((p) => (p.offset_m - edge) * outward >= -1e-6) .sort((a, b) => (a.offset_m - b.offset_m) * outward); const line: OffsetPoint[] = []; for (const p of points) { const last = line[line.length - 1]; if (last && p.elevation_m > last.elevation + 1e-6) break; line.push({ offset: p.offset_m, elevation: p.elevation_m }); const z = ground ? groundZ(ground, p.offset_m) : null; if (z !== null && Math.abs(z - p.elevation_m) < 1e-3) break; // 원지반에 닿음 = 끝 } return line; }