fix(B06): 조정 단위를 관 길이 1m로·벽 형상 불변·사면 역전 차단

지적 3건:
- 우측(유출) 기슭막이만 좌우로 밀 때 단면이 변했다. 0.1m씩 미니 관 길이 정수의
  끝수를 벽 폭이 흡수해 0.45→0.90까지 뚱뚱해졌다. 조정 단위를 **관 길이 1m**로
  바꾸고, 조정창으로 옮긴 벽은 폭을 절대 건드리지 않게 했다. 자리만 옮기면 정수
  길이가 성립하므로 형상이 그대로다(검증: 두께 0.450 고정, L 1m씩 증감).
- 성토 사면이 역전되는 자리가 있었다. 벽 상단이 노견보다 높아지면 사면선이 위로
  올라간다 — 최소 성토고 0.3m(FILL_MIN_RISE_M)를 두고 그 앞에서 멈춘다.
- 이동이 노견을 넘어 도로 반대편까지 갔다. 벽은 제 노견 바깥에만 서게 막았다.

조정창은 "관 길이 8m · 바깥 2.0m"처럼 관 길이를 같이 적는다.
700줄 유지를 위해 clampWallOffset·cutLength·outletSlopeFactory를 _Solve로 옮겼다.

검증: 이동 ±5m 20케이스 역전 0건·두께 고정, 275.71 유출 이동 11↔7m 정상,
계획고 스윕 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 16:50:45 +09:00
co-authored by Claude Opus 5
parent 4eed191d2a
commit 8564e118d6
7 changed files with 148 additions and 61 deletions
@@ -21,6 +21,8 @@ export interface StructurePanelDeps {
nudge: (key: RevetKey, screenDeltaM: number) => void;
/** 자동 자리로 되돌린다. */
reset: (key: RevetKey) => void;
/** 지금 관 길이(m) — 조정 단위가 관 길이 1m이라 창에 같이 적는다. */
pipeLengthM: () => number | null;
/** 창을 닫는다 = 구조물 선택 해제. */
close: () => void;
}
@@ -31,8 +33,12 @@ export interface StructurePanelHandle {
show: (key: RevetKey | null) => void;
}
/** 한 걸음 이동량(m) — 벽 높이 눈금(0.1m)과 같은 단위로 맞춘다. */
const STEP_M = 0.1;
/**
* 한 걸음 이동량(m) — **관 길이 1m**에 맞춘다(2026-08-21 사용자 확정).
* 관은 m 단위로 설치하므로 0.1m씩 밀면 그 끝수를 벽 폭이 흡수하게 되어 미는 동안
* 벽 단면이 변한다. 한 걸음을 1m로 두면 벽은 자리만 옮기고 형상은 그대로다.
*/
const STEP_M = 1.0;
function makeButton(label: string, title: string, onClick: () => void): HTMLButtonElement {
const button = document.createElement("button");
@@ -105,10 +111,13 @@ export function buildStructurePanel(deps: StructurePanelDeps): StructurePanelHan
// 자동 자리 기준 이동량 — 0이면 "자동"으로 적어 손을 안 댔음을 바로 알린다.
// 부호(+/−)만 적으면 좌·우 벽에서 어느 쪽인지 읽히지 않아 **안/바깥**으로 적는다.
const direction = L(shift > 0 ? "B06_Cross_Revet_Outward" : "B06_Cross_Revet_Inward");
value.textContent =
const moved =
Math.abs(shift) < 1e-9
? L("B06_Cross_Revet_Auto")
: `${direction} ${Math.abs(shift).toFixed(1)}m`;
const pipeLength = deps.pipeLengthM();
value.textContent =
pipeLength === null ? moved : `${L("B06_Cross_Revet_Pipe")} ${pipeLength}m · ${moved}`;
},
};
}