/* ============================================================================= * B06_Section_UI_Cross_Underdrain.ts * 맹암거 횡단 형상(288-1 · 요구 183-3). * * · 노면 = 설계 노면 — 서버 · TS 짝이 암거 경사로 지대 낮은 쪽 한 경사로 기울여 냄 · 측구 없음. * · 옵셋선 = 노면을 암거 높이만큼 아래로 같은 각도. * · 성토 쪽 = 옵셋선을 성토 사면과 만날 때까지 · 절토 쪽 = 절토 사면을 아래로 늘여 옵셋선과 만남. * · 노면 · 양쪽 선 · 옵셋선이 둘러싼 안 = 돌 무늬(흙막이 돌 그리기 `drawStones`). * 높이가 비면 그리지 않음(수치를 지어내지 않음). * ========================================================================== */ import type { CrossDesign } from "./B06_Section_Api_Fetch"; import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types"; import { drawStones } from "./B06_Section_UI_Cross_Wall_Hatch"; import type { UnderdrainSpec } from "@util/common_util_structure_underdrain"; const SVG_NS = "http://www.w3.org/2000/svg"; let clipSeq = 0; type Edge = { offset_m: number; elevation_m: number }; /** 한쪽 둘레(노면 끝 → 옵셋선과 만나는 자리) — `outward` = 바깥 부호(+1 좌 · −1 우). */ function sidePath( edge: Edge, outward: 1 | -1, line: readonly Edge[], roadGrade: number, heightM: number, ): OffsetPoint[] { const start = { offset: edge.offset_m, elevation: edge.elevation_m }; const offZ = (o: number) => edge.elevation_m + roadGrade * (o - edge.offset_m) - heightM; const outside = line .filter((p) => (p.offset_m - edge.offset_m) * outward > 1e-6) .sort((a, b) => (a.offset_m - b.offset_m) * outward); const first = outside[0]; if (!first) return [start, { offset: start.offset, elevation: offZ(start.offset) }]; if (first.elevation_m >= edge.elevation_m) { // 절토 — 사면을 안쪽 아래로 늘여 옵셋선과 만남 const k = (first.elevation_m - edge.elevation_m) / (first.offset_m - edge.offset_m); const d = Math.abs(k - roadGrade) < 1e-9 ? 0 : -heightM / (k - roadGrade); const offset = edge.offset_m + d; return [start, { offset, elevation: offZ(offset) }]; } // 성토 — 사면을 따라 내려가다 옵셋선을 만나는 자리 const path: OffsetPoint[] = [start]; let prev = edge; for (const point of outside) { const fa = prev.elevation_m - offZ(prev.offset_m); const fb = point.elevation_m - offZ(point.offset_m); if (fb <= 0) { const t = fa / (fa - fb); const offset = prev.offset_m + (point.offset_m - prev.offset_m) * t; path.push({ offset, elevation: offZ(offset) }); return path; } path.push({ offset: point.offset_m, elevation: point.elevation_m }); prev = point; } path.push({ offset: prev.offset_m, elevation: offZ(prev.offset_m) }); return path; } /** 맹암거 형상 둘레 — 좌 노면 끝 → 우 노면 끝 → 우 둘레 → 옵셋선 → 좌 둘레(위로). 높이 없으면 null. */ export function underdrainOutline( design: CrossDesign, heightM: number | null, ): OffsetPoint[] | null { const edges = design.road_edges; if (!edges || !(heightM && heightM > 0)) return null; const { left, right } = edges; const span = left.offset_m - right.offset_m; if (!(Math.abs(span) > 1e-9)) return null; const grade = (left.elevation_m - right.elevation_m) / span; const line = design.design_line ?? []; const leftPath = sidePath(left, 1, line, grade, heightM); const rightPath = sidePath(right, -1, line, grade, heightM); return [ { offset: left.offset_m, elevation: left.elevation_m }, ...rightPath, ...leftPath.slice(1).reverse(), ]; } /** 맹암거 형상을 그린다(돌 무늬 · 둘레 클립). 그렸으면 그 무리 — 둘레에 고름 표식(`data-structure-id` · 288-2). */ export function appendUnderdrainOverlay( svg: SVGElement, spec: UnderdrainSpec | undefined, design: CrossDesign, x: (offset: number) => number, y: (elevation: number) => number, ): SVGGElement | null { const outline = spec ? underdrainOutline(design, spec.height_m) : null; if (!spec || !outline) return null; const px = (p: OffsetPoint): [number, number] => [x(p.offset), y(p.elevation)]; const points = outline.map((p) => px(p).join(",")).join(" "); const group = document.createElementNS(SVG_NS, "g") as SVGGElement; group.setAttribute("class", "b06-chart__underdrain"); const body = document.createElementNS(SVG_NS, "polygon"); body.setAttribute("points", points); body.setAttribute("class", "b06-chart__underdrain-body"); if (spec.structure_id) body.setAttribute("data-structure-id", spec.structure_id); clipSeq += 1; const clipId = `b06-underdrain-clip-${clipSeq}`; const clip = document.createElementNS(SVG_NS, "clipPath"); clip.setAttribute("id", clipId); const clipShape = document.createElementNS(SVG_NS, "polygon"); clipShape.setAttribute("points", points); clip.append(clipShape); const stones = document.createElementNS(SVG_NS, "g"); stones.setAttribute("clip-path", `url(#${clipId})`); const height = spec.height_m!; const offsets = outline.map((p) => p.offset); const [lo, hi] = [Math.min(...offsets), Math.max(...offsets)]; const { left, right } = design.road_edges; const grade = (left.elevation_m - right.elevation_m) / (left.offset_m - right.offset_m); const roadZ = (o: number) => left.elevation_m + grade * (o - left.offset_m); const at = (u: number, v: number): OffsetPoint => { const offset = lo + (hi - lo) * u; return { offset, elevation: roadZ(offset) - height * (v + 0.5) }; }; const [ax, ay] = px(at(0, 0)); const [bx, by] = px(at(1, 0)); drawStones( stones, { spanM: Math.hypot(hi - lo, grade * (hi - lo)), pixelsPerMeter: Math.abs(x(1) - x(0)) || 1, point: (u, v) => px(at(u, v)), leanDegrees: (Math.atan2(bx - ax, -(by - ay)) * 180) / Math.PI, }, false, height, ); group.append(body, clip, stones); svg.append(group); return group; }