2026-09-02 사용자 지시 9건 반영. 화면 실조작 검증은 다음 세션 몫 (사용자 지시로 코딩까지만 진행). 1. 초기 계획선 = 전체 측점 폴리라인 — `design_ground_following_profile()` 신설. 모든 측점을 변화점으로 잡고 계획고 = 원지반고, 라운드는 R을 지정한 자리에만 (`build_curves(only_explicit=)`·`build_alignment(only_explicit_curves=)`). basis `ground_polyline`. 관 정착 선형은 폴백으로 내림. 2. 구간 쉬프트(⬆⬇) 삭제. 3. [직선화] 신설 — `B05_Profile_UI_Profile_Straighten.ts`. 두 측점의 라운드에 탄젠트한 직선으로 대체하고 사이 라운드 삭제. 직선 틸팅 시 가운데 라운드 + 양측 탄젠트 재구성. 4. [쉬프트] 신설 — 직선 구간을 기하에서 되읽어(`detectStraightRun`) 복수 선택, 최외곽 라운드 중심 기준 상·하 평행이동. 5. 방향키 조작 — 상하 0.1m 계획고, 좌우 0.1m 누가거리(구조물·비정규 측점 한정). 6. undo/redo 신설 — B05·B06 조작 세션 키 묶음 스냅샷(`_Profile_History.ts`). 버튼은 요약줄 맨 앞(최대 기울기 좌측), 21x17px. Ctrl+Z / Ctrl+Shift+Z. 7. 종단 요약줄의 횡단배수 최소고 표시 삭제(산식·편집 차단은 유지). 8. [편집 되돌리기] 버튼 삭제 — undo/redo로 대체. 9. 지형 구분 기본값 특수지형 — 패널 셀렉트와 백엔드 기본값(스키마·체인 폴백) 일치. 700줄 제한 — 패널을 `_Profile_Panel_Tools` · `_Profile_Preview` 로 분리하고 측점↔구조물 짝짓기를 `_Profile_Structures` 로 이관(842줄 → 696줄). 검증: tsc --noEmit 오류 0, ruff format/check 통과, pytest tmp/tests/ -q → 366 passed / 14 skipped / 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
80 lines
3.5 KiB
TypeScript
80 lines
3.5 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Profile_Preview.ts
|
|
* 계획선 편집 → 횡단 설계 프리뷰 반영 (패널 본체에서 분리, 2026-09-02 · 700줄 한계).
|
|
*
|
|
* 계획고가 바뀌면 측점별 횡단 단면적도 바뀐다 — 서버에 한 번 물어 전 측점을 다시 계산해
|
|
* **공유 캐시가 들고 있는 같은 객체**를 제자리 갱신한다. 그래야 횡단 기준 유토곡선과
|
|
* 3D 예상형상이 같은 값으로 따라온다(2026-08-03·08-23 사용자 보고).
|
|
*
|
|
* 끌기 중에는 계속 호출되므로 디바운스하고, 늦게 온 응답은 seq 비교로 버린다.
|
|
* ========================================================================== */
|
|
|
|
import type { AlignmentEdits } from "./B05_Profile_UI_Profile_Alignment";
|
|
import { previewCrossDesigns } from "../B06_Section/B06_Section_Api_Fetch";
|
|
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
|
|
|
export interface CrossPreviewContext {
|
|
projectId: string;
|
|
detail: () => SectionDetailResponse | null;
|
|
routeId: () => number | null;
|
|
edits: () => AlignmentEdits;
|
|
debounceMs: number;
|
|
/** 반영이 끝난 뒤 다시 그린다. */
|
|
onApplied: () => void;
|
|
}
|
|
|
|
export interface CrossPreview {
|
|
/** 편집이 있을 때마다 부른다 — 마지막 값만 서버로 나간다. */
|
|
schedule: () => void;
|
|
/** 패널을 걷을 때 대기 중인 요청 타이머를 끈다. */
|
|
dispose: () => void;
|
|
}
|
|
|
|
export function createCrossPreview(ctx: CrossPreviewContext): CrossPreview {
|
|
let timer = 0;
|
|
let seq = 0;
|
|
|
|
return {
|
|
schedule() {
|
|
if (!ctx.detail() || ctx.routeId() === null) return;
|
|
window.clearTimeout(timer);
|
|
timer = window.setTimeout(() => {
|
|
const detail = ctx.detail();
|
|
const routeId = ctx.routeId();
|
|
if (!detail || routeId === null) return;
|
|
const current = (seq += 1);
|
|
// full_designs — 설계선 좌표까지 통째로 받아야 3D 코리도가 편집 즉시 정확한
|
|
// 형상으로 재빌드된다(2026-08-23 사용자 지시). 암 경계는 백엔드가 세션값이 없으면
|
|
// DB 저장 echo를 폴백으로 쓰므로 그대로 유지된다.
|
|
void previewCrossDesigns(ctx.projectId, routeId, ctx.edits(), undefined, {
|
|
fullDesigns: true,
|
|
})
|
|
.then((next) => {
|
|
const live = ctx.detail();
|
|
if (current !== seq || !live || ctx.routeId() !== routeId) return;
|
|
const designByChainage = new Map(
|
|
next.designs.map((entry) => [entry.chainage_m.toFixed(3), entry.design]),
|
|
);
|
|
for (const section of live.cross_sections) {
|
|
const full = designByChainage.get(section.chainage_m.toFixed(3));
|
|
if (!full || !section.design) continue;
|
|
// 전체 교체(설계선 포함) — B06 reconcile과 같은 패턴으로 사용자 부속값은 보존.
|
|
section.design = {
|
|
...(full as NonNullable<typeof section.design>),
|
|
inlet_structure: section.design.inlet_structure,
|
|
basin_adjust: section.design.basin_adjust,
|
|
};
|
|
}
|
|
ctx.onApplied();
|
|
})
|
|
.catch(() => {
|
|
/* 프리뷰 실패는 무시 — 화면의 계획선은 그대로 두고 다음 편집에서 다시 시도한다. */
|
|
});
|
|
}, ctx.debounceMs);
|
|
},
|
|
dispose() {
|
|
window.clearTimeout(timer);
|
|
},
|
|
};
|
|
}
|