/* ============================================================================= * B06_Section_UI_Cross_SpoilFill.ts * 횡단도에 **사토장(유용토운반작업장) 성토선**을 얹는다. * * 왜 있나 — 임도기술교본 6장 3절이 **운반처리 위치를 평면도와 횡단도에 표시**하도록 * 요구한다(지식DB `01_임도/02_상세설계/유용토운반작업장.md` §2). 수량을 세기 전에 * 도면에 그것이 보여야 한다. * * ⚠ **터파기 파선과 선 종류를 가른다** — 터파기는 짧은 점선, 사토장은 긴 파선+점. * 같은 파선으로 두면 도면에서 둘을 구별할 수 없다(2026-09-09 네 창 확정). * ⚠ 값은 설계 결과(`spoil_fill_*`)에서 온다 — 여기서 **다시 세지 않는다**. * ========================================================================== */ const SVG_NS = "http://www.w3.org/2000/svg"; /** 설계 결과에서 사토장 그리기에 쓰는 값만 추려 받는다. */ export interface SpoilFillDrawing { spoil_fill_line?: Array<{ offset_m: number; elevation_m: number }> | null; spoil_fill_area_m2?: number | null; spoil_fill_width_m?: number | null; spoil_fill_unclosed?: boolean | null; spoil_fill_capacity_m3?: number | null; spoil_fill_placed_m3?: number | null; spoil_fill_unplaced_m3?: number | null; } /** 말풍선 문구 — 무엇이 얼마나 쌓였는지와 **근거**를 함께 적는다. */ export function spoilFillTooltip(design: SpoilFillDrawing): string { const area = Number(design.spoil_fill_area_m2 ?? 0); const width = Number(design.spoil_fill_width_m ?? 0); const lines = [ `유용토운반작업장(구 사토장) · 단면 ${area.toFixed(2)}㎡ · 폭 ${width.toFixed(2)}m`, ]; const capacity = design.spoil_fill_capacity_m3; if (typeof capacity === "number" && capacity > 0) { const placed = Number(design.spoil_fill_placed_m3 ?? 0); lines.push(`구간 용량 ${capacity.toFixed(1)}㎥ 중 ${placed.toFixed(1)}㎥ 담김`); const unplaced = Number(design.spoil_fill_unplaced_m3 ?? 0); // ⚠ **적어 보이는 0 을 경고로 띄우지 않는다**(2026-09-09 화면 실측). 폭을 이분법으로 // 찾으므로 용량과 담긴 양이 소수점 아래에서 조금 남는다(400 − 399.9937 = 0.0063). // 그것을 「못 담음」으로 띄우면 **늘 경고가 뜬 채**가 되어 진짜 경고가 안 보인다. // 표시 자릿수(0.1㎥)에서 보이지 않는 몫은 없는 것으로 본다. if (unplaced >= 0.05) { lines.push(`⚠ ${unplaced.toFixed(1)}㎥ 는 못 담음 — 지반 자료가 있는 데까지만 넓힘`); } } if (design.spoil_fill_unclosed) { lines.push("⚠ 비탈이 원지반을 못 만나 잘림 — 지반 자료 범위를 넘어감"); } lines.push("폭은 노면 끝(노견이 시작하는 자리)에서 잼 — 그 구간 노견도 이 성토 안에 듦"); lines.push("교본 6장 3절이 평면도·횡단도 표시를 요구함"); return lines.join("\n"); } /** * 사토장 성토선을 그린다. 선이 없으면 아무것도 하지 않는다. * 돌려주는 값은 그린 폴리라인(없으면 `null`) — 부르는 쪽이 강조에 쓸 수 있다. */ export function appendSpoilFillOverlay( layer: SVGElement, design: SpoilFillDrawing | null | undefined, x: (offset: number) => number, toDisplayY: (elevation: number) => number, ): SVGElement | null { const line = design?.spoil_fill_line; if (!design || !Array.isArray(line) || line.length < 2) return null; const polyline = document.createElementNS(SVG_NS, "polyline"); polyline.setAttribute( "points", line.map((point) => `${x(point.offset_m)},${toDisplayY(point.elevation_m)}`).join(" "), ); polyline.setAttribute( "class", design.spoil_fill_unclosed ? "b06-chart__spoil-fill is-unclosed" : "b06-chart__spoil-fill", ); const title = document.createElementNS(SVG_NS, "title"); title.textContent = spoilFillTooltip(design); polyline.append(title); layer.append(polyline); // 이름표 — 평상 한가운데 위에 얹는다. 선만 있으면 그것이 무엇인지 도면에서 모른다. const first = line[0]; const last = line[line.length - 1]; const label = document.createElementNS(SVG_NS, "text"); label.setAttribute("x", String((x(first.offset_m) + x(last.offset_m)) / 2)); label.setAttribute("y", String(toDisplayY(Math.max(first.elevation_m, last.elevation_m)) - 4)); label.setAttribute("text-anchor", "middle"); label.setAttribute("class", "b06-chart__spoil-fill-label"); label.textContent = `유용토운반작업장 ${Number(design.spoil_fill_area_m2 ?? 0).toFixed(2)}㎡`; const labelTitle = document.createElementNS(SVG_NS, "title"); labelTitle.textContent = spoilFillTooltip(design); label.append(labelTitle); layer.append(label); return polyline; }