⚠ **시험만으로는 안 잡히던 자리임.** 실제 프로젝트에 사토장을 놓고 눈으로 본 뒤 잡음.
① **사토장을 지워도 값이 남음** — `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>
96 lines
4.8 KiB
TypeScript
96 lines
4.8 KiB
TypeScript
/* =============================================================================
|
||
* 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;
|
||
}
|