fix(b06): 사면안정군 구간 시작 · 기준 · 종료 측점 심음 · 관 근처 독립 벽 가드 한 벌(카드 · 서버 면적 · B07 · 3D)(178-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -146,13 +146,15 @@ def is_station_planting_type(item: StructureType) -> bool:
|
||||
- A군(횡단배수·계곡 통과 시설): 그 자리에 횡단도가 필요하다.
|
||||
- D군 구간형(기슭막이 등): 구간 시작·기준·종료에 횡단도가 필요하다 — 길이가 길면
|
||||
그만큼 여러 장이 나온다(사용자 확정).
|
||||
- C군 구간형(사면안정 — 옹벽·흙막이·소단 등): D군 구간형과 같이 시작·기준·종료.
|
||||
안 심으면 측점 격자 사이에 놓인 벽이 횡단도에 안 얹힌다.
|
||||
|
||||
측점 생성(`resolve_extra_stations`)과 구조물 재이관 차단
|
||||
(`B05_Profile_Structures_Migration`)이 같은 규칙을 봐야 "기타" 고스트가 안 생긴다.
|
||||
"""
|
||||
if item.group == "A":
|
||||
return True
|
||||
return item.group == "D" and item.placement == "interval"
|
||||
return item.group in ("C", "D") and item.placement == "interval"
|
||||
|
||||
|
||||
def station_planting_labels() -> set[str]:
|
||||
|
||||
@@ -23,7 +23,12 @@
|
||||
import { structureHashParts } from "./B05_Profile_UI_Corridor_Structures_Hash";
|
||||
import type { CrossSection, CulvertSideSpec } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { computeCulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
|
||||
import { restrictToSide, tierSpanOf } from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
|
||||
import {
|
||||
culvertOwnerFor,
|
||||
ownWallsDrawAt,
|
||||
restrictToSide,
|
||||
tierSpanOf,
|
||||
} from "../B06_Section/B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { pipeWallThicknessM } from "../B06_Section/B06_Section_UI_Cross_Culvert_Const";
|
||||
import type { CulvertLayout } from "../B06_Section/B06_Section_UI_Cross_Culvert";
|
||||
import { computeFordLayout, fordAdjustOf } from "../B06_Section/B06_Section_UI_Cross_Ford";
|
||||
@@ -277,9 +282,9 @@ export function buildCorridorStructures(
|
||||
// 독립 기슭막이(옛 D군 경로) — 측점에 배관/숨김 기슭막이 세트가 붙으면 그쪽(위
|
||||
// culvertLayoutOf)이 그린다. 아직 이관 안 된 정본만 이 옛 경로로 스윕한다
|
||||
// (2026-08-28 이관 이중그리기 방지 — 횡단도 가드와 같은 규칙).
|
||||
const revetLayout = section.culvert
|
||||
? null
|
||||
: computeRevetmentLayout(section, section.design?.revet_adjust?.own);
|
||||
const revetLayout = ownWallsDrawAt(section, culvertOwnerFor(section, crossSections))
|
||||
? computeRevetmentLayout(section, section.design?.revet_adjust?.own)
|
||||
: null;
|
||||
const paveChute = fordPavementChute(section, crossSections);
|
||||
if (!layout && !fordLayout && !section.box && !revetLayout && !paveChute) continue;
|
||||
const chainage = section.chainage_m;
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { BasinShape, WallAdjust, WallLayout } from "./B06_Section_UI_Cross_
|
||||
import {
|
||||
computeCardCulvert,
|
||||
culvertLinkFor,
|
||||
ownWallsDrawAt,
|
||||
tierSpanOf,
|
||||
} from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import type {
|
||||
@@ -123,8 +124,9 @@ export function computeStoredLayouts(section: CrossSection, sections: readonly C
|
||||
culvert,
|
||||
);
|
||||
// 독립 기슭막이(옛 D군 경로) — 배관 세트가 붙었거나 옆에서 이어져 오면 그쪽이 그린다.
|
||||
const own =
|
||||
!section.culvert && !link ? computeRevetmentLayout(section, design.revet_adjust?.own) : null;
|
||||
const own = ownWallsDrawAt(section, link?.source)
|
||||
? computeRevetmentLayout(section, design.revet_adjust?.own)
|
||||
: null;
|
||||
return { design, link, culvert, box, ford, own };
|
||||
}
|
||||
|
||||
|
||||
@@ -262,6 +262,15 @@ export function culvertWallsStandAt(owner: CrossSection, section: CrossSection):
|
||||
return keys.some((key) => wallStandsAt(owner, section, key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 독립 벽(사면안정 C군 · 옛 D경로)을 이 측점에 그리나 — 카드 · 서버 면적 · B07 · 3D 가
|
||||
* 함께 쓰는 가드 한 벌(178-1). 관 세트가 붙은 측점이거나 연동 관(`owner`)의 벽이 이
|
||||
* 측점에 실제로 서면 배관 경로가 그린다(겹침 방지) — 링크만 있고 벽이 안 서면 그린다.
|
||||
*/
|
||||
export function ownWallsDrawAt(section: CrossSection, owner: CrossSection | undefined): boolean {
|
||||
return !section.culvert && !(owner && culvertWallsStandAt(owner, section));
|
||||
}
|
||||
|
||||
export function culvertLinkFor(
|
||||
section: CrossSection,
|
||||
sections: readonly CrossSection[],
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
hideFordPavedRoad,
|
||||
appendFordSurfaceDropPlan,
|
||||
} from "./B06_Section_UI_Cross_Ford_Pavement";
|
||||
import { culvertWallsStandAt } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { ownWallsDrawAt } from "./B06_Section_UI_Cross_Culvert_Wire";
|
||||
import { raiseWalls } from "./B06_Section_UI_Cross_Wall";
|
||||
import { attachChuteBandPick } from "./B06_Section_UI_Cross_Wall_Hatch";
|
||||
import { appendSpoilFillOverlay } from "./B06_Section_UI_Cross_SpoilFill";
|
||||
@@ -465,8 +465,7 @@ export function createCrossSectionCard(
|
||||
// 옆에서 이어져 온**(culvertLink) 경우엔 배관 경로가 그린다 — 옛 D경로는 건너뛴다
|
||||
// (둘 다 그리면 이웃 카드에 벽이 겹친다). 가드는 **그 링크의 벽이 이 카드에 실제로
|
||||
// 설 때만** 막는다(2026-09-09 — 링크만 있어도 막으면 구조물이 안 보였다).
|
||||
const linkedWallsHere = !!culvertLink && culvertWallsStandAt(culvertLink.source, section);
|
||||
if (!section.culvert && !linkedWallsHere) {
|
||||
if (ownWallsDrawAt(section, culvertLink?.source)) {
|
||||
const ownAdjust = revetOffset?.adjustFor(section, "own");
|
||||
const ownLayout = computeRevetmentLayout(section, ownAdjust);
|
||||
ownDesignTrim = ownLayout?.designTrim;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""178-1 사면안정 벽(흙막이 · 옹벽) 가드 한 벌 — 카드 · 서버 면적 · B07 · 3D.
|
||||
|
||||
흠: 카드는 「연동 관 벽이 이 측점에 실제로 설 때만」 독립 벽을 건너뛰는데, 서버 면적
|
||||
(`computeStoredLayouts` → Node) · B07 은 링크만 있어도 건너뛰고 3D 는 관 측점만 봤다.
|
||||
⇒ 관 링크는 있으나 관 벽이 안 서는 측점에서 카드엔 흙막이가 서고 서버 면적 · B07 엔 없음.
|
||||
|
||||
고침: `ownWallsDrawAt` 한 벌을 세 곳이 함께 씀. 서버 면적은 같은 TS 를 Node 로 돌리므로
|
||||
번들을 새로 빌드해 그 결과로 판정한다.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
VITE = ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"
|
||||
FIXTURE = ROOT / "resources" / "tester" / "fixtures" / "b06_73_4_body_guard_sections.json"
|
||||
|
||||
SOIL_GUARD = {
|
||||
"structure_id": "w1",
|
||||
"type_id": "soil_guard",
|
||||
"name": "흙막이",
|
||||
"start_m": 150,
|
||||
"end_m": 370,
|
||||
"anchor_m": 260,
|
||||
"form": "돌쌓기(찰)",
|
||||
"height_m": 1.5,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def bundle(tmp_path_factory) -> Path:
|
||||
if not VITE.is_file():
|
||||
pytest.skip("프론트엔드 의존성 미설치")
|
||||
out = tmp_path_factory.mktemp("server_calc")
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(VITE),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../B06_Section/B06_Section_Server_Calc_Node.ts",
|
||||
"--outDir",
|
||||
str(out),
|
||||
"--logLevel",
|
||||
"error",
|
||||
],
|
||||
cwd=str(ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return out / "B06_Section_Server_Calc_Node.js"
|
||||
|
||||
|
||||
def _areas(bundle: Path, sections: list, tmp_path: Path) -> dict[float, tuple]:
|
||||
src, dst = tmp_path / "in.json", tmp_path / "out.json"
|
||||
src.write_text(json.dumps({"detail": {"cross_sections": sections}}), encoding="utf-8")
|
||||
subprocess.run(["node", str(bundle), str(src), str(dst)], check=True, cwd=str(ROOT)) # noqa: S603
|
||||
rows = json.loads(dst.read_text(encoding="utf-8"))["areas"]
|
||||
return {row["chainage_m"]: (row["cut_area_m2"], row["fill_area_m2"]) for row in rows}
|
||||
|
||||
|
||||
def _sections() -> list:
|
||||
return copy.deepcopy(json.loads(FIXTURE.read_text(encoding="utf-8"))["cross_sections"])
|
||||
|
||||
|
||||
def test_링크만_있고_관_벽이_안_서면_흙막이_면적_행이_나온다(bundle, tmp_path):
|
||||
"""173.087 관의 다단 구간값(20 m)이 160 · 180 을 덮어 링크는 서지만 관 벽(전 · 후 5 m)은 안 섬."""
|
||||
sections = [s for s in _sections() if s["chainage_m"] < 300]
|
||||
sections[1]["design"]["extra_spans"] = {"extra0": {"before_m": 20, "after_m": 20}}
|
||||
for section in sections:
|
||||
if not section.get("culvert"):
|
||||
section["revetment"] = SOIL_GUARD
|
||||
areas = _areas(bundle, sections, tmp_path)
|
||||
assert 160 in areas and 180 in areas, f"흙막이가 선 측점 면적 행 없음: {sorted(areas)}"
|
||||
# 표준 성토(160 = 1.45 · 180 = 5.32 ㎡)보다 벽이 사면을 끊어 줄어듦
|
||||
assert areas[160][1] < 1.45 and areas[180][1] < 5.32
|
||||
|
||||
|
||||
def test_관_벽이_서는_측점은_독립_벽을_안_그린다(bundle, tmp_path):
|
||||
"""340 · 360 은 352.14 관 벽이 섬 — 흙막이를 얹어도 면적이 관 벽 그대로(겹쳐 그리지 않음)."""
|
||||
plain = _areas(bundle, _sections(), tmp_path)
|
||||
walled = _sections()
|
||||
for section in walled:
|
||||
if not section.get("culvert"):
|
||||
section["revetment"] = SOIL_GUARD
|
||||
areas = _areas(bundle, walled, tmp_path)
|
||||
assert areas[340] == plain[340] and areas[360] == plain[360]
|
||||
|
||||
|
||||
def test_카드_서버_3D_가_가드_한_벌을_쓴다():
|
||||
files = {
|
||||
"카드": ROOT / "B06_Section" / "B06_Section_UI_Cross_View.ts",
|
||||
"서버 면적 · B07": ROOT / "B06_Section" / "B06_Section_Structure_Layouts.ts",
|
||||
"3D": ROOT / "B05_Profile" / "B05_Profile_UI_Corridor_Structures.ts",
|
||||
}
|
||||
for where, path in files.items():
|
||||
source = path.read_text(encoding="utf-8")
|
||||
assert "ownWallsDrawAt(" in source, f"{where} 가 가드 한 벌을 안 씀"
|
||||
assert "culvertWallsStandAt(" not in source, f"{where} 에 따로 가드가 남음"
|
||||
@@ -133,17 +133,10 @@ def test_group_a_point_and_revetment_facility_join(tmp_path):
|
||||
|
||||
|
||||
def test_other_groups_do_not_plant_stations(tmp_path):
|
||||
"""C·E·G군은 측점을 만들지 않는다 — 지금 켠 것은 A군과 D군 구간형뿐이다."""
|
||||
"""E·G군은 측점을 만들지 않는다 — 켠 것은 A군과 C·D군 구간형뿐이다(178-1)."""
|
||||
_write_structures(
|
||||
tmp_path,
|
||||
[
|
||||
{
|
||||
"type_id": "retaining_wall",
|
||||
"chainage_m": 40.0,
|
||||
"start_m": 40.0,
|
||||
"end_m": 60.0,
|
||||
"placement": "interval",
|
||||
},
|
||||
{
|
||||
"type_id": "pavement_concrete",
|
||||
"chainage_m": 300.0,
|
||||
@@ -157,6 +150,40 @@ def test_other_groups_do_not_plant_stations(tmp_path):
|
||||
assert [chainage for chainage, _label in extras] == [84.3, 149.73, 200.92, 275.71]
|
||||
|
||||
|
||||
def test_slope_group_interval_plants_start_anchor_end(tmp_path):
|
||||
"""C군 구간형(옹벽·흙막이·소단)도 시작·기준·종료 셋을 심는다 — 격자 사이 벽도 횡단에 얹힘(178-1)."""
|
||||
_write_structures(
|
||||
tmp_path,
|
||||
[
|
||||
{
|
||||
"type_id": "soil_guard",
|
||||
"chainage_m": 45.0,
|
||||
"start_m": 40.0,
|
||||
"end_m": 50.0,
|
||||
"placement": "interval",
|
||||
},
|
||||
{
|
||||
"type_id": "retaining_wall",
|
||||
"chainage_m": 125.0,
|
||||
"start_m": 120.0,
|
||||
"end_m": 130.0,
|
||||
"placement": "interval",
|
||||
},
|
||||
{
|
||||
"type_id": "berm",
|
||||
"chainage_m": 165.0,
|
||||
"start_m": 160.0,
|
||||
"end_m": 170.0,
|
||||
"placement": "interval",
|
||||
},
|
||||
],
|
||||
)
|
||||
extras = resolve_extra_stations(tmp_path, parse_pipe_points(LIVE_POINTS))
|
||||
assert [c for c, label in extras if label == "흙막이"] == [40.0, 45.0, 50.0]
|
||||
assert [c for c, label in extras if label == "옹벽"] == [120.0, 125.0, 130.0]
|
||||
assert [c for c, label in extras if label == "소단"] == [160.0, 165.0, 170.0]
|
||||
|
||||
|
||||
def test_broken_structures_file_keeps_pipe_derived_stations(tmp_path):
|
||||
route_dir = tmp_path / "B05_Profile" / "route"
|
||||
route_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user