fix(B06): 이동량이 한계에 걸려도 숫자만 커지던 문제·안쪽 이동 최소 물매

- 조정창 숫자가 실제와 갈렸다. 안쪽으로 계속 누르니 "안쪽 10.0m"까지 올라가는데
  벽은 노견 한계에서 멈춰 있었다. 기하가 **실제 적용한 이동량**(revetShift)을 돌려
  주고, 카드가 그 값을 제어기에 되담는다 — 이제 한계에 닿으면 숫자도 멈춘다.
- 안쪽으로 당기면 사면이 짧아져 물매가 급해진다. **1:1.2(가장 급한 한계)**를 넘지
  않는 자리까지만 들어가게 막았다(2026-08-21 사용자 지시). 바깥으로 밀 때는 완만해
  지므로 막지 않고 범위 밖이면 경고로만 알린다.

검증: 4+4.3은 자동 자리가 이미 1:1.21이라 안쪽 이동 0(차단), 13+15.7은 −1m까지
허용(1:1.61)하고 그 이상 차단. 조정창 숫자도 같은 자리에서 멈춘다. 두께 16케이스
0.450 고정, 계획고 스윕 24케이스 문제 0. tsc 통과, pytest 148 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 17:24:35 +09:00
co-authored by Claude Opus 5
parent ce03edc180
commit 564b289ee6
5 changed files with 39 additions and 1 deletions
@@ -118,6 +118,8 @@ export function computeCulvertLayout(
// 하므로 자리를 풀어서 정한다(집수정은 측구부 자리 그대로).
const wallHeightFor = (spec: CulvertSideSpec): number =>
Math.min(revetTargetHeight(diameter), revetHeightLimit(spec.revet_form));
/** 요청값이 한계에 걸려 잘린 뒤의 **실제** 이동량. 조정창이 이 값을 되받는다. */
const appliedShift = { inlet: 0, outlet: 0 };
let inletPlacement: FillWallPlacement | null = null;
if (!basinReason && designAt) {
// 훑는 범위는 **성토 사면 끝까지**다 — 그 너머는 받칠 성토가 없다. 성토가 얕아 벽
@@ -142,6 +144,8 @@ export function computeCulvertLayout(
inletBaseAt,
inletInfo.outward,
);
appliedShift.inlet =
Math.round((shifted - inletPlacement.offset) * inletInfo.outward * 10) / 10;
inlet.offset = shifted;
inlet.elevation = Math.min(groundAt(shifted), invertCap(inletInfo.edge));
}
@@ -440,6 +444,8 @@ export function computeCulvertLayout(
invertAt,
outletInfo.outward,
);
appliedShift.outlet =
Math.round((shifted - outletPlacement.offset) * outletInfo.outward * 10) / 10;
outletWallAnchor = { offset: shifted, elevation: invertAt(shifted) };
}
}
@@ -669,6 +675,7 @@ export function computeCulvertLayout(
minLengthM: minPitchingM,
},
basin,
revetShift: appliedShift,
designTrim:
walls.length || basin
? {
@@ -235,12 +235,21 @@ export function clampWallOffset(
outward: number,
): number {
const steps = 40;
// 안쪽으로 당기면 사면이 짧아져 물매가 급해진다 — **1:1.2(가장 급한 한계)**를 넘지
// 않게 막는다(2026-08-21 사용자 지시). 바깥으로 밀 때는 완만해지므로 여기서 막지
// 않고(관 길이 조정 여지를 남긴다) 범위 밖이면 경고로만 알린다.
const inward = (wantedOffset - autoOffset) * outward < 0;
let allowed = autoOffset;
for (let i = 1; i <= steps; i += 1) {
const candidate = autoOffset + ((wantedOffset - autoOffset) * i) / steps;
// 벽은 제 노견 바깥에 선다 — 안쪽으로 넘기면 도로 밑을 파고들어 반대편까지 간다.
if ((candidate - edge.offset_m) * outward < 0) break;
if (edge.elevation_m - (baseAt(candidate) + height) < FILL_MIN_RISE_M) break;
const rise = edge.elevation_m - (baseAt(candidate) + height);
if (rise < FILL_MIN_RISE_M) break;
if (inward) {
const run = Math.abs(candidate + outward * REVET_TRAP_TOP_M - edge.offset_m);
if (run / rise < FILL_SLOPE_RATIO_MIN) break;
}
allowed = candidate;
}
return allowed;
@@ -111,4 +111,10 @@ export interface CulvertLayout {
/** 유입 집수정 단면(있을 때만). */
basin: BasinLayout | null;
designTrim: CulvertDesignTrim | null;
/**
* **실제 적용된** 기슭막이 이동량(m, + = 계류측 바깥). 요청값이 노견·사면 역전·사면 끝
* 한계에 걸리면 잘려 들어온다 — 조정창은 이 값을 보여 주고 저장한다. 그러지 않으면
* 눌러도 안 움직이는데 숫자만 계속 커진다(2026-08-21 사용자 지적).
*/
revetShift: { inlet: number; outlet: number };
}
+8
View File
@@ -167,6 +167,8 @@ export interface RevetOffsetControl {
shiftFor: (section: CrossSection, role: RevetKey) => number;
selectedFor: (section: CrossSection) => RevetKey | null;
select: (chainageM: number, key: RevetKey | null) => void;
/** 기하가 실제로 적용한 이동량을 되받아 담는다(한계에 걸린 요청값을 잘라 낸다). */
syncShift: (chainageM: number, role: RevetKey, appliedM: number) => void;
adjust: (chainageM: number, role: RevetKey, deltaM: number) => void;
reset: (chainageM: number, role: RevetKey) => void;
}
@@ -484,6 +486,12 @@ export function createCrossSectionCard(
}
// 배수관 측점 세트(배관·기슭막이·보호공) — 기존 도형 위에 추가만 한다(2026-08-19).
culvertPipeLengthM = culvertLayout?.pipe.lengthM ?? null;
// 요청한 이동량이 한계에 걸려 잘렸으면 그 값으로 되돌려 담는다 — 안 그러면 눌러도
// 안 움직이는데 창의 숫자만 계속 커진다(2026-08-21 사용자 지적).
if (culvertLayout && revetOffset) {
revetOffset.syncShift(section.chainage_m, "inlet", culvertLayout.revetShift.inlet);
revetOffset.syncShift(section.chainage_m, "outlet", culvertLayout.revetShift.outlet);
}
if (culvertLayout) {
setRevetActive = appendCulvertOverlay(
plotLayer,
@@ -146,6 +146,14 @@ export function createStationControls(deps: StationControlDeps): StationControls
if (key) revetSelected.set(chainageM.toFixed(2), key);
else revetSelected.delete(chainageM.toFixed(2));
},
syncShift: (chainageM, role, appliedM) => {
const key = revetKey(chainageM, role);
const stored = revetShifts.get(key) ?? 0;
if (Math.abs(stored - appliedM) < 1e-9) return;
if (Math.abs(appliedM) < 1e-9) revetShifts.delete(key);
else revetShifts.set(key, appliedM);
persistRevetShifts();
},
adjust: (chainageM, role, deltaM) => {
const key = revetKey(chainageM, role);
revetShifts.set(key, clampRevetShift((revetShifts.get(key) ?? 0) + deltaM));