/* ============================================================================= * B06_Section_UI_Cross_View_Frame.ts * 횡단 카드의 **틀 그리기** — 십자선(계획 노선 자리) · 아래 · 왼쪽 축선 · 높이 축 이름, 그리고 * 구조물 폐회로 면적 반영. `_Cross_View` 가 700줄을 넘어 여기로 뗐다(계획서 24-3b). * ========================================================================== */ import type { CrossSection } from "./B06_Section_Api_Fetch"; import { computeStructureAreas, type DesignTrim } from "@util/common_util_cross_structure_areas"; import { CROSS_PAD, L, svgElement, svgText } from "./B06_Section_UI_Section_Common"; type Sample = CrossSection["samples"][number]; export interface CardFrameInput { /** 표고가 있는 샘플 — 계획고가 없으면 중심에 가장 가까운 지반고로 십자선을 세운다. */ valid: Sample[]; centerX: number; /** 계획고(없으면 null — 옛 자료는 지반고 폴백). */ designElevation: number | null; /** 표고 → 화면 y(높이 배율 반영). */ toY: (elevation: number) => number; widthPx: number; heightPx: number; } /** 십자선은 도형 레이어(줌·팬 따라감), 축선 · 축 이름은 틀 레이어(제자리)에 붙인다. */ export function appendCardFrame( svg: SVGElement, plotLayer: SVGElement, input: CardFrameInput, ): void { const { valid, centerX, designElevation, toY, widthPx, heightPx } = input; const hasDesign = designElevation !== null; const centerSample = valid.reduce((nearest, sample) => { if (!nearest || Math.abs(sample.offset_m ?? 0) < Math.abs(nearest.offset_m ?? 0)) return sample; return nearest; }, null); // 십자선 = 계획 노선 자리(구 데이터는 지반고 폴백). const centerElevation = hasDesign ? designElevation : (centerSample?.elevation_m ?? null); const centerY = centerElevation !== null ? toY(centerElevation) : heightPx / 2; const centerMarkerClass = `b06-chart__center-marker${hasDesign ? " b06-chart__center-marker--design" : ""}`; plotLayer.append( svgElement("line", { x1: centerX, y1: centerY - 18, x2: centerX, y2: centerY + 18, class: centerMarkerClass, }), svgElement("line", { x1: centerX - 18, y1: centerY, x2: centerX + 18, y2: centerY, class: centerMarkerClass, }), ); svg.append( svgElement("line", { x1: CROSS_PAD.left, y1: heightPx - CROSS_PAD.bottom, x2: widthPx - CROSS_PAD.right, y2: heightPx - CROSS_PAD.bottom, class: "b06-chart__axis", }), svgElement("line", { x1: CROSS_PAD.left, y1: CROSS_PAD.top, x2: CROSS_PAD.left, y2: heightPx - CROSS_PAD.bottom, class: "b06-chart__axis", }), svgText(L("B06_Profile_View_ElevationAxis"), { x: 13, y: heightPx / 2, "text-anchor": "middle", transform: `rotate(-90 13 ${heightPx / 2})`, class: "b06-chart__axis-label", }), ); } /** * 구조물이 만든 폐회로 면적을 측점 설계에 반영한다(2026-09-06 사용자 확정). * * 표준 설계선만 보는 기본 계산은 구조물이 있는지 모른다 — 기슭막이·세월교·BOX가 서면 * 성토 사면이 벽에서 끊기고 그 바깥은 벽·성토부선이 대신 그리므로 폐회로가 달라진다. * 트림이 없으면(구조물 없는 측점) 아무것도 하지 않는다. * * 조작 중 즉시 반영용이다. 정본은 [저장]·[확정] 때 서버가 같은 코드 * (`B06_Section_Structure_Areas_Node.ts`)로 다시 계산해 얹는다(2026-09-06). */ export function applyStructureAreas( section: CrossSection, trim: DesignTrim | undefined | null, ): void { const design = section.design; if (!trim || !design || !Array.isArray(design.design_line) || design.design_line.length < 2) { return; } const ground = section.samples .filter((sample) => sample.valid !== false && typeof sample.elevation_m === "number") .map((sample) => ({ offset: sample.offset_m ?? 0, elevation: sample.elevation_m as number })) .sort((a, b) => a.offset - b.offset); const areas = computeStructureAreas({ designLine: design.design_line as Array<{ offset_m: number; elevation_m: number }>, ground, trim, rockBoundaryOffsetM: typeof design.rock_boundary_offset_m === "number" ? design.rock_boundary_offset_m : null, }); if (!areas) return; design.cut_area_m2 = Number(areas.cutAreaM2.toFixed(4)); design.fill_area_m2 = Number(areas.fillAreaM2.toFixed(4)); if (typeof design.cut_soil_area_m2 === "number") { design.cut_soil_area_m2 = Number(areas.cutSoilAreaM2.toFixed(4)); } }