feat(B06): 유출부 보호공 삭제·성토부선 전환 + 추가 기슭막이(계단식)

- 보호공(돌붙임) 띠 삭제, 윗면 선만 성토부선(계획선)으로 유지 — 구간별
  사면길이·5m 초과 여부 툴팁 표기
- 성토부 5m 이상이면 조정창 +로 추가 기슭막이(배관 없는 동일 형상 벽)
  계단식 추가 — 자동 자리는 배관용과 같은 규칙(1:1.2 최소 자리)
- 추가 벽도 ◀/▶/↺ 이동(안쪽 한계 = 앞 구조물), 마지막 벽 - 삭제
- 개수·이동량 세션 보관(b06:extrawall / revetx extra{n} 키)
- 700줄 제한: Extra(성토부선·추가 벽)·Wire(카드 배선) 분리 신설

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 13:24:47 +09:00
co-authored by Claude Opus 5
parent 97053ab0d2
commit 9a58e3b1bd
13 changed files with 503 additions and 155 deletions
+44 -37
View File
@@ -23,8 +23,8 @@ import {
sectionModeLabel,
type RockBoundaryControl,
} from "./B06_Section_UI_Cross_Design";
import { showToast } from "@ui/ui_template_elements";
import { appendCulvertOverlay, computeCulvertLayout } from "./B06_Section_UI_Cross_Culvert";
import { appendCulvertOverlay } from "./B06_Section_UI_Cross_Culvert";
import { computeCardCulvert } from "./B06_Section_UI_Cross_Culvert_Wire";
import type { InletStructureChoice } from "./B06_Section_UI_Cross_Culvert";
import { buildStructurePanel } from "./B06_Section_UI_Cross_Structure_Panel";
import { attachZoomPan, buildZoomControls } from "./B06_Section_UI_Cross_View_Zoom";
@@ -181,6 +181,16 @@ export interface InletStructureControl {
set: (chainageM: number, value: InletStructureChoice) => void;
}
/** 유출측 추가 기슭막이 개수 제어(2026-08-22 사용자 — 성토부 5m 이상 계단식 추가). */
export interface ExtraWallControl {
countFor: (section: CrossSection) => number;
add: (chainageM: number) => void;
/** 마지막 추가 벽 삭제(그 벽의 이동량도 함께 지운다). */
removeLast: (chainageM: number) => void;
/** 기하가 실제로 세운 개수로 잘라 동기화(성토부가 없어져 못 세운 벽 정리). */
syncCount: (chainageM: number, built: number) => void;
}
export function createCrossSectionCard(
section: CrossSection,
selected: boolean,
@@ -203,6 +213,8 @@ export function createCrossSectionCard(
revetOffset?: RevetOffsetControl,
/** 유입측 구조물 형식 선택(드롭다운 — 2026-08-22). */
inletStructure?: InletStructureControl,
/** 유출측 추가 기슭막이 개수 제어(2026-08-22). */
extraWalls?: ExtraWallControl,
): CrossCardElement {
// 이 카드의 실효 표시 반폭 — 개별값이 전역 반폭보다 우선한다(2026-08-06).
const effectiveHalfWidth = stationWidth?.widthFor(section) ?? crossHalfWidth;
@@ -230,6 +242,8 @@ export function createCrossSectionCard(
let culvertInletIsBasin = false;
/** 드롭다운 선택지 가용성(2026-08-22) — 기하 판정을 조정창에 전달. */
let culvertInletOptions = { revetAllowed: true, basinLUAllowed: true };
/** 추가 기슭막이 상태(2026-08-22) — 끝 성토부 5m 이상(canAdd)·세워진 개수. */
let culvertExtraState = { canAdd: false, count: 0 };
/**
* 기슭막이를 고른다. 구조물 선택과 절·성토 면적 강조는 **같은 레벨**이라 하나를
* 고르면 다른 하나는 풀린다(2026-08-21 사용자). 또 구조물을 고르면 그 **측점 카드도
@@ -467,17 +481,14 @@ export function createCrossSectionCard(
toDisplayY,
toggleArea,
);
// 배수관 세트 기하를 먼저 계산한다 — 설계선이 기슭막이 밖 성토 경사를 끊는 데 쓴다.
const culvertLayout: ReturnType<typeof computeCulvertLayout> = computeCulvertLayout(
// 배수관 세트 기하 계산 + 이동량 되받기(토스트) — 분리 파일(Wire, 700줄 제한).
const culvertLayout = computeCardCulvert(
section,
sourceSamples,
revetOffset
? {
inlet: revetOffset.shiftFor(section, "inlet"),
outlet: revetOffset.shiftFor(section, "outlet"),
}
: undefined,
inletStructure?.valueFor(section),
activeRevet,
revetOffset,
inletStructure,
extraWalls,
);
// 포장층 → 설계선 → 암 경계선 순으로 겹쳐, 설계선이 포장 박스 위에 오게 한다.
appendPavementOverlay(plotLayer, section.design, x, toDisplayY);
@@ -499,32 +510,16 @@ export function createCrossSectionCard(
toDisplayY,
);
}
// 배수관 측점 세트(배관·기슭막이·보호공) — 기존 도형 위에 추가만 한다(2026-08-19).
// 배수관 측점 세트 — 기존 도형 위에 추가만 한다(2026-08-19). 조정창이 쓸
// 카드 상태(관 길이·집수정 여부·선택지·추가 벽 상태)를 여기서 받아 둔다.
culvertPipeLengthM = culvertLayout?.pipe.lengthM ?? null;
culvertInletIsBasin = !!culvertLayout?.basin;
if (culvertLayout) culvertInletOptions = culvertLayout.inletOptions;
// 요청한 이동량이 한계에 걸려 잘렸으면 그 값으로 되돌려 담는다 — 안 그러면 눌러도
// 안 움직이는데 창의 숫자만 계속 커진다(2026-08-21 사용자 지적).
// 잘린 순간에는 **왜 안 움직였는지 토스트로 알린다**(2026-08-22 사용자 요청 —
// 13+15.7처럼 자동 자리가 이미 안쪽 한계라 ◀가 그대로 먹히지 않는 경우).
// 조작 중(그 벽이 선택된 상태)일 때만 띄운다 — 리로드로 복원된 옛 세션 값에는 침묵.
if (culvertLayout && revetOffset) {
for (const role of ["inlet", "outlet"] as const) {
const requested = revetOffset.shiftFor(section, role);
const applied = culvertLayout.revetShift[role];
if (activeRevet === role && Math.abs(requested - applied) > 0.05) {
// 이동량 부호: + = 계류측 바깥. 요청이 적용보다 작으면 안쪽 요청이 잘린 것.
showToast(
L(
requested < applied
? "B06_Cross_Revet_Limit_Inward"
: "B06_Cross_Revet_Limit_Outward",
),
"info",
);
}
revetOffset.syncShift(section.chainage_m, role, applied);
}
if (culvertLayout) {
culvertInletOptions = culvertLayout.inletOptions;
culvertExtraState = {
canAdd: culvertLayout.outletFill.addable,
count: culvertLayout.extraWalls.length,
};
}
if (culvertLayout) {
setRevetActive = appendCulvertOverlay(
@@ -622,9 +617,21 @@ export function createCrossSectionCard(
},
structureFor: () => inletStructure?.valueFor(section) ?? "auto",
setStructure: (value) => inletStructure?.set(section.chainage_m, value),
// 집수정은 자리 고정 — 유입이 집수정이면 ◀/▶/↺를 숨긴다.
canNudge: (key) => key === "outlet" || !culvertInletIsBasin,
// 집수정은 자리 고정 — 유입이 집수정이면 ◀/▶/↺를 숨긴다(추가 벽은 항상 이동).
canNudge: (key) => key !== "inlet" || !culvertInletIsBasin,
optionsFor: () => culvertInletOptions,
extraState: () => culvertExtraState,
addExtra: () => extraWalls?.add(section.chainage_m),
removeExtra: () => {
// 삭제되는 벽이 선택 중이면 선택을 유출 벽으로 옮긴다 — 다시 그려질 때
// 없는 키를 강조하려다 조정창이 빈 대상으로 남는 것을 막는다.
const last = `extra${culvertExtraState.count - 1}` as RevetKey;
if (activeRevet === last) {
activeRevet = "outlet";
revetOffset?.select(section.chainage_m, "outlet");
}
extraWalls?.removeLast(section.chainage_m);
},
});
showRevetControl = (visible) => panel.show(visible ? activeRevet : null);
showRevetControl(activeRevet !== null);