diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts index da2ef841..059d186f 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts @@ -634,7 +634,8 @@ export function computeCulvertLayout( }); // 성토부선 + 다단 기슭막이(2026-08-22 — 보호공 삭제, 윗면 선만 성토부선으로). // 유출측과 **집수정 계류측**이 같은 체계를 쓴다: 5m 넘으면 다단을 둘 수 있다. - const extras = buildExtrasAt(outletWall ? pipeCorners.outlet.bottom : null, { + const outletStart = outletWall ? pipeCorners.outlet.bottom : null; + const outletInput = { startBottomElevation: outletWall?.bottomBack.elevation ?? 0, outward: outletInfo.outward, groundAt, @@ -643,24 +644,46 @@ export function computeCulvertLayout( ownerForm: outletWallSpec.form, equalize: equalizeExtras === true, leanFor: extraLean, - }); + }; + const extras = buildExtrasAt(outletStart, outletInput); // 독립 기슭막이(관 숨김)는 집수정이 없어 이 채널이 **유입측 벽의 성토부선·다단**이다 — // 유출측(`extras`)과 같은 시작점(관 하단 꼭짓점)·같은 `bextra` 키(2026-08-29 사용자). - const basinExtras = buildExtrasAt( - culvert.hidden_pipe ? (inletWall ? pipeCorners.inlet.bottom : null) : basinFillStart, - { - startBottomElevation: culvert.hidden_pipe - ? (inletWall?.bottomBack.elevation ?? 0) - : basinFillBottom, - outward: inletInfo.outward, - groundAt, - limitOffset: inletInfo.limit, - adjusts: (revetShift?.basinExtras ?? []).map(adjustOf), - ownerForm: inletWallSpec.form, + const basinStart = culvert.hidden_pipe + ? inletWall + ? pipeCorners.inlet.bottom + : null + : basinFillStart; + const basinInput = { + startBottomElevation: culvert.hidden_pipe + ? (inletWall?.bottomBack.elevation ?? 0) + : basinFillBottom, + outward: inletInfo.outward, + groundAt, + limitOffset: inletInfo.limit, + adjusts: (revetShift?.basinExtras ?? []).map(adjustOf), + ownerForm: inletWallSpec.form, + equalize: false, + leanFor: extraLean, + }; + const basinExtras = buildExtrasAt(basinStart, basinInput); + /** ⭐ 면적용 다단 — **미확정 단(높이 안 적음)부터 아래는 없는 것**으로 봄(2026-09-14 브레인 판정 + * 「미확정 벽은 성토도 안 깎음 · 벽 금액과 성토 감소가 함께」). 그림은 그대로 다 그림. + * B08 도 첫 미확정 단 아래는 미확정으로 셈(`facility_structures`) — 두 곳이 같은 선에서 끊김. */ + const confirmedOnly = ( + built: OutletExtrasResult, + start: OffsetPoint | null, + input: typeof outletInput, + ): OutletExtrasResult => { + const firstUnset = built.appliedAdjusts.findIndex((applied) => applied.h == null); + if (firstUnset < 0) return built; + return buildExtrasAt(start, { + ...input, + adjusts: input.adjusts.slice(0, firstUnset), equalize: false, - leanFor: extraLean, - }, - ); + }); + }; + const extrasForArea = confirmedOnly(extras, outletStart, outletInput); + const basinExtrasForArea = confirmedOnly(basinExtras, basinStart, basinInput); // ── 성토 사면 구간 확정. 벽 자리가 굳은 뒤에 물매를 역산해야 관 길이 맞춤(벽 이동)이 // 접점을 다시 깨뜨리지 않는다 — 종전 어긋남의 직접 원인이 이 순서였다. @@ -700,8 +723,8 @@ export function computeCulvertLayout( trimMinSlope = { points: [...(trimMinSlope?.points ?? []), ...points] }; } }; - extendTrimSlope(drawnFillPoints(extras), outletInfo.outward); - extendTrimSlope(drawnFillPoints(basinExtras), inletInfo.outward); + extendTrimSlope(drawnFillPoints(extrasForArea), outletInfo.outward); + extendTrimSlope(drawnFillPoints(basinExtrasForArea), inletInfo.outward); const outletWallFinal = walls.find((wall) => wall.role === "outlet") ?? null; const outletSlope = outletWallFinal ? slopeOf(outletWallFinal) : null; diff --git a/B08_Quantity/B08_Quantity_Engine_Pipe.py b/B08_Quantity/B08_Quantity_Engine_Pipe.py index c83f9c7c..79a9f060 100644 --- a/B08_Quantity/B08_Quantity_Engine_Pipe.py +++ b/B08_Quantity/B08_Quantity_Engine_Pipe.py @@ -326,6 +326,8 @@ UNCONFIRMED_EXTRA_HEIGHT = ( "다단 높이를 안 적음 — 기본 1.5m 를 지반에 맞춰 그린 {height}m 로 섰음 · 적으면 금액이 섬" ) UNCONFIRMED_EXTRA_FORM = "다단 형태를 안 정해 기준벽의 기본 형태 「{form}」로 섰음" +#: 첫 미확정 단 아래는 횡단도 면적도 없는 것으로 봄(B06 `confirmedOnly`) — 같은 선에서 끊음. +UNCONFIRMED_EXTRA_ABOVE = "윗단({above}단)이 미확정이라 이 단도 미확정 — 윗단 높이를 적으면 섬" def extra_walls_from_designs(designs: list[dict[str, Any]] | None) -> dict[float, list[dict]]: @@ -448,9 +450,12 @@ def facility_structures( # 다단 — 기준벽의 기초·(독립이면) 제원 칸을 물려받고, 형태·높이·구간은 선 벽 목록 값. parents = {role: (values, filled) for role, _label, values, _notes, _w, filled in walls} counts = {"outlet": 0, "basin": 0} + first_unset: dict[str, int] = {} for tier in _extra_walls_at(extra_walls, chainage): side = "basin" if str(tier.get("side")) == "basin" else "outlet" counts[side] += 1 + if not tier.get("height_set"): + first_unset.setdefault(side, counts[side]) parent_values, parent_filled = parents.get( EXTRA_SIDE_ROLES[side], next(iter(parents.values()), ({}, [])) ) @@ -460,6 +465,8 @@ def facility_structures( reasons = [] if not tier.get("height_set"): reasons.append(UNCONFIRMED_EXTRA_HEIGHT.format(height=f"{height:g}")) + elif first_unset.get(side, counts[side]) < counts[side]: + reasons.append(UNCONFIRMED_EXTRA_ABOVE.format(above=first_unset[side])) if not tier.get("form_set") and any(str(f).startswith("형태") for f in parent_filled): reasons.append(UNCONFIRMED_EXTRA_FORM.format(form=form)) label = f"{EXTRA_SIDE_LABELS[side]} 다단 기슭막이 {counts[side]}단" diff --git a/resources/tester/test_b08_extra_walls.py b/resources/tester/test_b08_extra_walls.py index fa4b5c76..791844d2 100644 --- a/resources/tester/test_b08_extra_walls.py +++ b/resources/tester/test_b08_extra_walls.py @@ -104,6 +104,27 @@ def test_다단_줄이_원단위와_금액_합에_든다() -> None: assert tiers[1]["unconfirmed"] +def test_윗단이_미확정이면_아랫단도_미확정() -> None: + """면적은 첫 미확정 단부터 아래를 없는 것으로 봄(B06) — B08 도 같은 선에서 끊어야 성토·벽이 함께 움직임.""" + walls = [ + dict(WALLS[1], key="extra0"), + dict(WALLS[0], key="extra1"), + ] # 1단 미확정 · 2단 높이 적음 + tiers = _tiers(facility_structures([POINT], {85.0: walls})) + assert "다단 높이를 안 적음" in tiers[0]["unconfirmed"] + assert "윗단(1단)이 미확정" in tiers[1]["unconfirmed"] + + +def test_미확정_단은_성토를_안_깎는다() -> None: + """2026-09-14 브레인 판정 — 미확정 벽이 성토만 깎으면 「벽도 성토도 없는 빈 자리」가 생김.""" + geom = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Culvert_Geom.ts").read_text( + encoding="utf-8" + ) + assert "extendTrimSlope(drawnFillPoints(extrasForArea)" in geom + assert "extendTrimSlope(drawnFillPoints(basinExtrasForArea)" in geom + assert "applied.h == null" in geom + + def test_목록이_없으면_종전과_같다() -> None: assert not _tiers(facility_structures([POINT])) assert not _tiers(facility_structures([POINT], {})) diff --git a/resources/tester/test_user_values_survive.py b/resources/tester/test_user_values_survive.py index 970b0c52..9a8392fe 100644 --- a/resources/tester/test_user_values_survive.py +++ b/resources/tester/test_user_values_survive.py @@ -225,8 +225,11 @@ def test_다단_성토부선이_면적_트림에_들어간다(): ) assert "const extendTrimSlope = (" in source, "트림 확장 자리가 사라짐" # 유출측·집수정 계류측 **둘 다** — 한쪽만 넣으면 그쪽 단만 물량에 잡힌다. - assert "extendTrimSlope(drawnFillPoints(extras)" in source, "유출측 다단이 빠짐" - assert "extendTrimSlope(drawnFillPoints(basinExtras)" in source, "집수정 계류측 다단이 빠짐" + # 면적용은 확정 단까지만(`extrasForArea` — 2026-09-14 ㉠ 미확정 단은 성토를 안 깎음). + assert "extendTrimSlope(drawnFillPoints(extrasForArea)" in source, "유출측 다단이 빠짐" + assert "extendTrimSlope(drawnFillPoints(basinExtrasForArea)" in source, ( + "집수정 계류측 다단이 빠짐" + ) # 그리지 않는 `cut` 갈래(벽이 묻힌 자리)까지 넣으면 면적이 거꾸로 부푼다. assert 'segment.kind !== "cut"' in source, "묻힌 구간을 안 걸러냄"