/* ============================================================================= * B06_Section_UI_Cross_Ford.ts * 세월교 측점 횡단 카드의 구체(측벽·바닥판·관·성토 채움) **오버레이 그리기**. * * 기하 계산은 `B06_Section_UI_Cross_Ford_Geom.ts`가 맡는다(700줄 제한 분리). * 여기서는 계산 결과(`FordLayout`)를 SVG 도형으로 옮기기만 한다 — 치수 결정 금지. * * 그리는 순서 = 시공 순서: ① 바닥판 → ② 측벽(ㄴ형 집수정 재사용) → ③ 관 → * ④ 계류측 성토부선·절토선. 관 위 성토 채움은 그리지 않는다(2026-08-25 사용자). * ========================================================================== */ import type { FordLayout, FordWallRole } from "./B06_Section_UI_Cross_Ford_Geom"; import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types"; export { computeFordLayout, DEFAULT_FORD_ADJUST, DEFAULT_FORD_WALL_ADJUST, } from "./B06_Section_UI_Cross_Ford_Geom"; export type { FordAdjust, FordLayout, FordSideLayout, FordWallAdjust, FordWallRole, } from "./B06_Section_UI_Cross_Ford_Geom"; const SVG_NS = "http://www.w3.org/2000/svg"; /** 벽 강조를 카드 재생성 없이 갈아 끼우는 setter. */ export type FordHighlightSetter = (key: FordWallRole | null) => void; /** * 세월교 구체 오버레이 — computeFordLayout 결과를 그린다. * * `onSelectWall`이 오면 측벽이 **선택 대상**이 된다(집수정과 같은 규칙). * 클릭은 여기서 멈춘다 — 밑에 깔린 면적 밴드로 번지면 강조가 딴 것으로 바뀐다. */ export function appendFordOverlay( layer: SVGElement, layout: FordLayout, x: (offset: number) => number, toDisplayY: (elevation: number) => number, onSelectWall?: (key: FordWallRole) => void, ): FordHighlightSetter { const { ford } = layout; const toXY = (point: OffsetPoint): [number, number] => [ x(point.offset), toDisplayY(point.elevation), ]; const polygon = ( points: OffsetPoint[], className: string, tooltip: string, ): SVGPolygonElement => { const shape = document.createElementNS(SVG_NS, "polygon"); shape.setAttribute("points", points.map((point) => toXY(point).join(",")).join(" ")); shape.setAttribute("class", className); const title = document.createElementNS(SVG_NS, "title"); title.textContent = tooltip; shape.append(title); return shape; }; const planLine = (points: OffsetPoint[], tooltip: string): void => { if (points.length < 2) return; const line = document.createElementNS(SVG_NS, "polyline"); line.setAttribute("points", points.map((point) => toXY(point).join(",")).join(" ")); // 성토·절토 접속선은 공사 계획선 — 설계선과 같은 보라 실선. line.setAttribute("class", "b06-chart__design-cross"); const title = document.createElementNS(SVG_NS, "title"); title.textContent = tooltip; line.append(title); layer.append(line); }; const wingText = (wing: { installed: boolean; length_m: number | null; angle_deg: number | null; }): string => (wing.installed ? `${wing.length_m ?? 0}m·${wing.angle_deg ?? 0}°` : "없음"); // ① 바닥판 — 두 측벽 사이를 잇는 판. 각 측 바깥 연장은 측벽 부재로 함께 그린다. layer.append( polygon( layout.slabBridge, "b06-chart__ford-slab", `세월교 바닥판 — 길이 ${layout.slabLengthM.toFixed(2)}m · 두께 ${ford.slab_thickness_m.toFixed(2)}m` + ` (날개벽 유입 ${wingText(ford.wing_in)} / 유출 ${wingText(ford.wing_out)} 투영 연장 반영)`, ), ); // ② 측벽 — ㄴ형 집수정 부재(기운 벽 + 바닥판)를 좌우에 하나씩. const walls = new Map(); for (const side of layout.sides) { const role = side.role === "inlet" ? "유입" : "유출"; const shapes: SVGPolygonElement[] = []; for (const part of side.parts) { const shape = polygon( part.points, `b06-chart__culvert-basin${onSelectWall ? " is-selectable" : ""}`, `${role} 측벽(ㄴ형) ${part.kind === "wall" ? "벽" : "바닥판"} — 높이 ${side.heightM.toFixed(2)}m` + ` · 두께 ${ford.wall_thickness_m.toFixed(2)}m` + (onSelectWall ? " (클릭 후 조정창에서 높이·좌우·상하 조정)" : ""), ); if (onSelectWall) { shape.addEventListener("click", (event) => { event.stopPropagation(); onSelectWall(side.role); }); } shapes.push(shape); layer.append(shape); } walls.set(side.role, shapes); } // ③ 관 — 바닥판 상면에 안힌다. 단면엔 1개만, 련수는 라벨로 알린다. layer.append( polygon( layout.pipe.outer, "b06-chart__culvert-pipe b06-chart__culvert-pipe--outer", `${layout.label.text} — 관 밑이 바닥판 상면`, ), polygon(layout.pipe.inner, "b06-chart__culvert-pipe", layout.label.text), ); // ④ 계류측 마감선 — 구조물이 원지반 위면 성토부선, 안이면 절토선(집수정과 같은 체계). for (const side of layout.sides) { const role = side.role === "inlet" ? "유입" : "유출"; for (const segment of side.fillSegments) { if (segment.kind === "cut") continue; planLine( segment.points, `${role} 측벽 계류측 성토부선 — 사면길이 ${segment.lengthM.toFixed(2)}m`, ); } if (side.cutLine) { planLine([side.cutLine.from, side.cutLine.to], `${role} 측벽 절토선(계획선)`); } } // 관·채움이 벽 위에 겹쳐 클릭을 가로챈다 — 투명 겹면을 맨 위에 얹어 벽을 고른다 // (배수관 기슭막이 `culvert-revet-hit`와 같은 수법). if (onSelectWall) { for (const side of layout.sides) { const wall = side.parts.find((part) => part.kind === "wall"); if (!wall) continue; const hit = polygon(wall.points, "b06-chart__culvert-revet-hit", ""); hit.addEventListener("click", (event) => { event.stopPropagation(); onSelectWall(side.role); }); layer.append(hit); } } const label = document.createElementNS(SVG_NS, "text"); const [labelX, labelY] = toXY(layout.label.at); label.setAttribute("x", String(labelX)); label.setAttribute("y", String(labelY)); label.setAttribute("text-anchor", "middle"); label.setAttribute("class", "b06-chart__culvert-label"); label.textContent = layout.label.text; layer.append(label); return (key: FordWallRole | null): void => { for (const [role, shapes] of walls) { for (const shape of shapes) shape.classList.toggle("is-active", role === key); } }; }