diff --git a/B06_Section/B06_Section_Engine_SpoilFill.py b/B06_Section/B06_Section_Engine_SpoilFill.py index 5437b3a9..f4e6bde5 100644 --- a/B06_Section/B06_Section_Engine_SpoilFill.py +++ b/B06_Section/B06_Section_Engine_SpoilFill.py @@ -65,6 +65,17 @@ def _spans(sections: list[dict[str, Any]], start_m: float, end_m: float) -> list return spans +def _has_spoil_value(design: dict[str, Any]) -> bool: + """그 측점에 **사토장 값이 실제로 남아 있나**. 칸이 0 으로 있는 것은 값이 아니다.""" + for key in ("spoil_fill_area_m2", "spoil_fill_width_m", "spoil_fill_capacity_m3"): + try: + if float(design.get(key) or 0.0) > 0: + return True + except (TypeError, ValueError): + continue + return bool(design.get("spoil_fill_structure_id")) + + def _side_of(design: dict[str, Any], option: Any) -> str | None: """쌓는 쪽 — 「좌·우」면 그대로, 「자동」이면 그 측점의 **성토 쪽**.""" word = str(option or "").strip() @@ -169,10 +180,8 @@ def enforce_spoil_fills( try: _revision, structures = load_structures(str(project_root)) except Exception: # noqa: BLE001 — 정본이 없으면 사토장도 없다 - return 0 + structures = [] sites = spoil_sites(structures) - if not sites: - return 0 def recompute(section, side, width_m, slope_ratio_n, longitudinal_data, standard_spec): design = section.get("design") @@ -206,6 +215,45 @@ def enforce_spoil_fills( return None changed = 0 + # ⚠⚠ **지운 사토장이 그림·수량에 남던 자리**(2026-09-09 화면 실측으로 잡음). + # `enforce_spoil_fills` 는 **얹기만** 했으므로 구조물을 지워도 저장분의 `spoil_fill_*` + # 가 그대로 남아 **횡단도에 계속 그려지고 면적표에도 섰다.** + # ⇒ 사토장이 덮지 않는 측점에 값이 남아 있으면 **사토장 없이 다시 계산**해 지운다. + # (노선 성토도 그때 원래 값으로 돌아온다 — 사토장 몫을 빼 두었기 때문이다.) + covered: set[float] = set() + for site in sites: + for section in _sections_in(cross_sections, site["start_m"], site["end_m"]): + covered.add(float(section["chainage_m"])) + for section in cross_sections: + design = section.get("design") + if not isinstance(design, dict): + continue + chainage = section.get("chainage_m") + if chainage is None or float(chainage) in covered: + continue + # ⚠ **칸이 있는 것과 값이 있는 것은 다르다** — 설계 결과는 사토장이 없어도 + # `spoil_fill_area_m2: 0.0` 을 늘 싣는다. 「값이 남아 있는」 측점만 되돌린다. + if not _has_spoil_value(design): + continue + side = _side_of(design, None) + plain = recompute(section, side, 0.0, None, longitudinal, standard) if side else None + if plain is None: + # 다시 계산할 수 없으면 **적어도 칸은 지운다** — 값이 남아 그려지는 것보다 낫다. + for key in [k for k in design if str(k).startswith("spoil_fill_")]: + design.pop(key, None) + # 노선 성토는 사토장 몫을 뺀 값이라, 다시 계산 못 하면 그 몫을 되돌려 준다. + moved = float(design.get("spoil_fill_replaced_fill_m2") or 0.0) + if moved > 0: + design["fill_area_m2"] = round(float(design.get("fill_area_m2") or 0.0) + moved, 4) + changed += 1 + continue + for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS): + if design.get(key) is not None: + plain[key] = design[key] + section["design"] = plain + changed += 1 + if not sites: + return changed for site in sites: sections = _sections_in(cross_sections, site["start_m"], site["end_m"]) if not sections: diff --git a/B06_Section/B06_Section_UI_Cross_SpoilFill.ts b/B06_Section/B06_Section_UI_Cross_SpoilFill.ts index ae88da48..c62db5d8 100644 --- a/B06_Section/B06_Section_UI_Cross_SpoilFill.ts +++ b/B06_Section/B06_Section_UI_Cross_SpoilFill.ts @@ -36,7 +36,11 @@ export function spoilFillTooltip(design: SpoilFillDrawing): string { 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); - if (unplaced > 0) { + // ⚠ **적어 보이는 0 을 경고로 띄우지 않는다**(2026-09-09 화면 실측). 폭을 이분법으로 + // 찾으므로 용량과 담긴 양이 소수점 아래에서 조금 남는다(400 − 399.9937 = 0.0063). + // 그것을 「못 담음」으로 띄우면 **늘 경고가 뜬 채**가 되어 진짜 경고가 안 보인다. + // 표시 자릿수(0.1㎥)에서 보이지 않는 몫은 없는 것으로 본다. + if (unplaced >= 0.05) { lines.push(`⚠ ${unplaced.toFixed(1)}㎥ 는 못 담음 — 지반 자료가 있는 데까지만 넓힘`); } }