/* ============================================================================= * B06_Section_UI_Cross_Box.ts * BOX암거 측점 횡단 카드의 구체(에이프런·저판·상판·내공·성토선) **오버레이 그리기**. * * 기하 계산은 `B06_Section_UI_Cross_Box_Geom.ts`가 맡는다(700줄 제한 분리). * 여기서는 계산 결과(`BoxLayout`)를 SVG 도형으로 옮기기만 한다 — 치수 결정 금지. * * 그리는 순서 = 시공 순서: ① 에이프런 → ② 저판 → ③ 상판 → ④ 내공 윤곽 → ⑤ 성토선. * 콘크리트 색은 세월교 바닥판 클래스를 그대로 쓴다. * ========================================================================== */ import type { BoxLayout, BoxSideRole } from "./B06_Section_UI_Cross_Box_Geom"; import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types"; export { computeBoxLayout, DEFAULT_BOX_ADJUST, DEFAULT_BOX_SIDE_ADJUST, } from "./B06_Section_UI_Cross_Box_Geom"; export type { BoxAdjust, BoxLayout, BoxSideAdjust, BoxSideLayout, BoxSideRole, } from "./B06_Section_UI_Cross_Box_Geom"; const SVG_NS = "http://www.w3.org/2000/svg"; /** 선택 강조를 카드 재생성 없이 갈아 끼우는 setter. */ export type BoxHighlightSetter = (role: BoxSideRole | null) => void; /** * BOX암거 구체 오버레이 — computeBoxLayout 결과를 그린다. * `onSelectSide`가 오면 좌·우 절반이 선택 대상이 된다(조정창이 그 측을 잡는다). */ export function appendBoxOverlay( layer: SVGElement, layout: BoxLayout, x: (offset: number) => number, toDisplayY: (elevation: number) => number, onSelectSide?: (role: BoxSideRole) => void, ): BoxHighlightSetter { const { box } = 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 wingText = (wing: { installed: boolean; length_m: number | null; angle_deg: number | null; }): string => (wing.installed ? `${wing.length_m ?? 0}m·${wing.angle_deg ?? 0}°` : "없음"); const slopeText = layout.slopeRatio ? ` · 구체 물매 1:${layout.slopeRatio.toFixed(1)}` : " · 구체 수평"; // ① 에이프런 — 날개벽 구간 바닥. 각 끝 표고에서 수평이다(2026-08-25 사용자). for (const side of layout.sides) { if (!side.apron.length) continue; const mark = side.role === "left" ? "좌" : "우"; layer.append( polygon( side.apron, "b06-chart__ford-slab", `${mark}측 날개벽 구간 바닥 — 수평 · 두께 ${box.slab_thickness_m.toFixed(2)}m` + ` (날개벽 ${wingText(side.role === "left" ? box.wing_in : box.wing_out)} 투영 연장)`, ), ); } // ② 저판 · ③ 상판 — 좌·우 표고가 다르면 함께 기운다. layer.append( polygon( layout.bottomSlab, "b06-chart__ford-slab", `BOX암거 저판 — 두께 ${box.slab_thickness_m.toFixed(2)}m${slopeText}` + (layout.bedGapM < -0.05 ? ` · 계류 하상보다 ${Math.abs(layout.bedGapM).toFixed(2)}m 낮음(터파기)` : layout.bedGapM > 0.05 ? ` · 계류 하상보다 ${layout.bedGapM.toFixed(2)}m 높음` : ""), ), polygon( layout.topSlab, "b06-chart__ford-slab", `BOX암거 상판 — 두께 ${box.top_thickness_m.toFixed(2)}m · 윗면이 노면 −${box.cover_m.toFixed(2)}m(복토)${slopeText}`, ), ); // ④ 내공(유로) — 면은 비우고 윤곽만 점선으로 두른다. layer.append( polygon( layout.barrel, "b06-chart__box-barrel", `BOX암거 내공(유로) ${box.inner_width_m.toFixed(1)}×${box.inner_height_m.toFixed(1)}m` + ` · 측벽 두께 ${box.wall_thickness_m.toFixed(2)}m · 구체 길이 ${layout.bodyLengthM.toFixed(2)}m`, ), ); // ⑤ 성토선 — 노견에서 (연장 후) 구체 최상단 모서리까지, 물매 1:1.2. for (const side of layout.sides) { if (side.fillLine.length < 2) continue; const polyline = document.createElementNS(SVG_NS, "polyline"); polyline.setAttribute("points", side.fillLine.map((point) => toXY(point).join(",")).join(" ")); polyline.setAttribute("class", "b06-chart__design-cross"); const title = document.createElementNS(SVG_NS, "title"); const drop = side.fillLine[0].elevation - side.fillLine[side.fillLine.length - 1].elevation; title.textContent = `BOX암거 성토선 — 물매 1:1.2 · 성토고 ${drop.toFixed(2)}m (구체 최상단에서 끝)`; polyline.append(title); layer.append(polyline); } 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); // 좌·우 절반을 덮는 투명 겹면 — 어느 끝을 조정할지 클릭으로 고른다. const hits = new Map(); if (onSelectSide) { const midOffset = (layout.sides[0].offset + layout.sides[1].offset) / 2; const top = Math.max(...layout.topSlab.map((point) => point.elevation)); const bottom = Math.min(...layout.bottomSlab.map((point) => point.elevation)); for (const side of layout.sides) { const outer = side.apron.length ? side.apron[1].offset : side.offset; const hit = polygon( [ { offset: midOffset, elevation: top }, { offset: outer, elevation: top }, { offset: outer, elevation: bottom }, { offset: midOffset, elevation: bottom }, ], // 강조용 클래스를 하나 더 단다 — 고른 끝을 옅게 칠한다(2026-08-30 사용자: // BOX암거는 선택해도 아무 표시가 없었다). 구체 부재는 좌·우가 한 몸이라 // 그쪽을 칠하면 어느 끝을 고른 것인지 되레 흐려진다. "b06-chart__culvert-revet-hit b06-chart__box-hit", "", ); hit.addEventListener("click", (event) => { event.stopPropagation(); onSelectSide(side.role); }); hits.set(side.role, hit); layer.append(hit); } } return (role: BoxSideRole | null): void => { for (const [side, hit] of hits) hit.classList.toggle("is-active", side === role); }; }