feat(B05,B06): 계획선 편집 중 횡단 재계산을 브라우저에서 수행 — 서버 왕복 제거

사용자 조작 중의 계산은 브라우저 안에서 끝나야 함(2026-09-03 사용자 확정). 계획고가
바뀔 때마다 전 측점 횡단 계산이 `POST …/cross-design/preview` 로 나가 왕복이 조작
속도를 지배했음.

- `refreshCrossDesigns` 를 로컬 계산으로 교체. 계획고는 저장 선형(`base_pvi`)에 편집
  델타를 얹어 TS `buildAlignment` 로 여기서 풂 — 호출부(B05·B06) 시그니처는 그대로라
  단일 창구가 유지됨. 서버 프리뷰는 선형 저장분이 없는 옛 데이터 폴백으로만 남음.
- 표준단면 config 기본값·암 경계 기본 오프셋을 `sections/context` 응답에서 세션에 기억
  (`rememberStandardDefaults`·`rememberRockBoundaryDefault`). 프론트에 config 수치를
  복제하지 않으면서 패널 없는 B05 도 같은 값으로 계산함.
- 디바운스 250 → 60ms — 왕복이 사라져 손을 떼는 즉시 유토곡선이 따라옴.
- 낡음 판정을 `hasStaleDesigns` 로 바꿔 첫 건에서 중단하고, 계획선 유효 샘플을
  샘플 배열당 한 번만 걸러 이진탐색(그리기 프레임당 2.33ms → 0.07ms 실측).
- 파이썬 엔진 2파일·프리뷰 라우터에 「TS 짝 파일과 한 벌」 경고 블록 추가.

검증: `tmp/tests/test_b06_cross_design_mirror.py` 10건 — 토사/암 2단계/L형 측구/포장/
세월교 하강/사면 미닫힘 등에서 파이썬과 TS 결과가 면적·설계선까지 일치. 전체 183건 통과.
`tsc --noEmit` 통과. 실측: route 127(측점 128) 전 측점 로컬 재계산 7.5ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 19:58:09 +09:00
co-authored by Claude Opus 5
parent de41958446
commit a35c8ff356
13 changed files with 353 additions and 94 deletions
+22 -11
View File
@@ -193,20 +193,30 @@ function resolveDitchSide(sectionMode: string, ditchSide: string | null | undefi
return "left";
}
/** 짝: `_ground_interpolator`. 정렬된 (offset, 지반고) 선형 보간(범위 밖 끝값 클램프). */
/**
* 짝: `_ground_interpolator`. 정렬된 (offset, 지반고) 선형 보간(범위 밖 끝값 클램프).
*
* 짝 파일은 앞에서부터 훑지만 여기서는 이진탐색으로 같은 구간을 고른다 — 사면·지반
* 교차 행진이 이 함수를 수만 번 부르는데 브라우저에서는 그 비용이 조작 속도에 바로
* 드러나기 때문이다. **결과는 완전히 같다**(같은 구간, 같은 선형보간).
*/
function groundInterpolator(valid: Array<[number, number]>): (offsetM: number) => number {
const last = valid.length - 1;
return (offsetM: number): number => {
if (offsetM <= valid[0][0]) return valid[0][1];
if (offsetM >= valid[valid.length - 1][0]) return valid[valid.length - 1][1];
for (let index = 1; index < valid.length; index += 1) {
const [x1, z1] = valid[index];
if (offsetM > x1) continue;
const [x0, z0] = valid[index - 1];
const span = x1 - x0;
if (span <= 0) return z1;
return z0 + (z1 - z0) * ((offsetM - x0) / span);
if (offsetM >= valid[last][0]) return valid[last][1];
let low = 1;
let high = last;
while (low < high) {
const mid = (low + high) >> 1;
if (valid[mid][0] < offsetM) low = mid + 1;
else high = mid;
}
return valid[valid.length - 1][1];
const [x1, z1] = valid[low];
const [x0, z0] = valid[low - 1];
const span = x1 - x0;
if (span <= 0) return z1;
return z0 + (z1 - z0) * ((offsetM - x0) / span);
};
}
@@ -652,7 +662,8 @@ export function computeCrossDesign(
// 자동 판정된 절/성토 역할에서 실제 단면 유형을 도출해 echo 한다(D-2).
let resolvedMode: string;
if (geometry.leftRole === "cut" && geometry.rightRole === "cut") resolvedMode = "both_cut";
else if (geometry.leftRole === "fill" && geometry.rightRole === "fill") resolvedMode = "both_fill";
else if (geometry.leftRole === "fill" && geometry.rightRole === "fill")
resolvedMode = "both_fill";
else if (geometry.leftRole === "cut") resolvedMode = "left_cut";
else resolvedMode = "right_cut";