Files
Aislo/B06_Section/B06_Section_UI_Cross_SpoilFill.ts
T
eomsangdonandClaude Opus 5 f569cb8498 fix(B06): 화면 실측으로 잡은 사토장 흠 둘 — 지운 뒤 남던 값 · 가짜 경고
⚠ **시험만으로는 안 잡히던 자리임.** 실제 프로젝트에 사토장을 놓고 눈으로 본 뒤 잡음.

① **사토장을 지워도 값이 남음** — `enforce_spoil_fills` 가 **얹기만** 해서, 구조물을
   지운 뒤에도 저장분의 `spoil_fill_*` 가 그대로 남아 횡단도에 계속 그려지고 면적표에도
   섰음. ⇒ 사토장이 덮지 않는 측점에 **값이 남아 있으면** 사토장 없이 다시 계산해 지움.
   노선 성토도 그때 원래 값으로 돌아옴(사토장 몫을 빼 두었던 것이 복구됨).
   ⚠ **칸이 있는 것과 값이 있는 것은 다름** — 설계 결과는 사토장이 없어도 `0.0` 을 늘
     실으므로 「값이 남은」 측점만 되돌림(`_has_spoil_value`)
② **말풍선에 늘 뜨던 가짜 경고** — 「⚠ 0.0㎥ 는 못 담음」. 폭을 이분법으로 찾아
   400 − 399.9937 = 0.0063 이 남는 것이라, 표시 자릿수에서 안 보이는 몫은 경고 안 함

실측(프로젝트 fa76c162) — 지운 뒤 사토장 0 · 용량 null · 노선 성토가 정확히
「지우기 전 + 옮겨 갔던 몫」으로 복구됨(100m 1.7563+4.9217=6.678 …).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 12:57:34 +09:00

96 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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;
}