3D 구조물과 성토면의 관계를 "잘라내기"에서 "투영 표기"로 바꾸고, 세월교·BOX암거 조정창을 기슭막이 템플릿으로 통일했다. B05 코리도 - 구조물 탑뷰 투영 산출(_Corridor_Carve.ts 신규): 구조물 솔리드 링 사이 실좌표 사각형 + 변형 성토면 영역. 노선 파라미터가 아니라 행 횡단선과 실좌표로 교차시켜 곡선 구간·날개벽 벌어짐이 그대로 잡힌다. - 사각형 합집합 경계(_Corridor_Union.ts 신규): 변을 교점에서 토막 내고 내부·공유 토막을 버려 최외곽 윤곽만 남긴다(측정 133 -> 13 루프). - 표기 모드(CARVE_APPLY=false): 서피스를 자르지 않고 ① 성토면 위 절단 투영선 ② 원지반 최고 표고 위 Z평면에 구조물 윤곽(노랑)과 유입·유출 성토부 윤곽 (하늘·주황)을 따로 그린다. 구조물 커브는 성토면 없다고 보고, 성토부 커브는 구조물 없다고 보고 만든다. - 구조물 실루엣 공용화(_Corridor_Station_Structure.ts 신규): 기슭막이·BOX암거· 세월교 실루엣을 한 자료형으로. 성토면 강제 변형은 스위치로 끄고 (STRUCTURE_SILHOUETTE_OVERRIDE), 노견 확장만 상시 적용(shoulderSpans). - 평면 영역 분리(_Corridor_Region.ts), 절단기 일반화(clipGeometry가 영역 배열· 전체 정점 속성 보간 — 리본 UV 유지). 지형은 코리도 스트립만 도려낸다. - BOX암거 3D를 측점 프레임 직선 압출로(노선 추종 금지), 날개벽 계산 공용화 (_Corridor_Structures_Wing.ts 신규) 후 세월교에도 날개벽 4매 추가. - 세월교 보어를 관 련수만큼 뚫고, 보어 밴드를 단면 표고로 클램프해 구멍 찌그러짐 제거. 날개벽 뿌리를 벽 외측면 모서리로 교정. - 직교 카메라(_Viewer_Camera.ts 신규) — 원근 왜곡 없이 탑뷰 판독. 커서 피벗 줌이 직교에서 camera.zoom을 함께 조인다. B06 횡단 - 조정창 공용 부품(_Cross_Panel_Base.ts 신규): 머리줄·2행 항목·D-pad·키보드 방향키. 세월교·BOX 조정창을 같은 템플릿으로 옮기고 십자 세트를 창 맨 아래로. - 세월교 조정창에 날개벽 항목 추가(설치·짧은쪽 높이·길이·각도) — 정본 pipe_points 되쓰기, 바닥판 연장은 길이×cos각으로 즉시 재계산. - 세월교 좌우 이동을 집수정 규칙으로: 노견 수평 확장 + 상하 이동분만 성토선. BUILD_VERSION 19 -> 31(저장 코리도 캐시 무효화). typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
99 lines
3.9 KiB
TypeScript
99 lines
3.9 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_Api_Culvert_Options.ts
|
|
* B06 조정창에서 만진 **배수관 세트 구간값**(기슭막이·집수정 길이·전/후)을
|
|
* 원천인 `pipe_points.json`에 되돌려 쓴다(2026-08-24 사용자).
|
|
*
|
|
* 값을 B06 정본(design)에 따로 담지 않는 이유: 같은 숫자가 B05 배수관 카드에도
|
|
* 있어서 두 곳이 갈리면 어느 쪽이 참인지 알 수 없다. 원천은 하나여야 한다.
|
|
*
|
|
* 저장은 **묶어서 늦게** 한다 — +/- 한 번마다 PUT하면 세부유역 재계산이 매번
|
|
* 돌아간다. 화면은 캐시를 고쳐 즉시 반영하고, 저장은 마지막 조작 뒤 한 번만 간다.
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
fetchDetailPipePoints,
|
|
saveDetailPipePoints,
|
|
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
|
|
/** 마지막 조작 뒤 이만큼 조용하면 저장한다(ms). */
|
|
const SAVE_DELAY_MS = 800;
|
|
|
|
/** 측점 하나에 얹을 옵션 조각 — 키는 `pipe_points` 옵션 키 그대로.
|
|
* 값은 수치가 대부분이나 날개벽 설치("있음"/"없음")처럼 문자열도 있다. */
|
|
export type CulvertOptionPatch = Record<string, number | string>;
|
|
|
|
export interface CulvertOptionWriter {
|
|
/** 측점 옵션을 예약한다. 같은 측점의 앞선 예약과는 합쳐진다. */
|
|
queue: (chainageM: number, patch: CulvertOptionPatch) => void;
|
|
/** 예약분을 즉시 내보낸다(확정·임시저장 직전에 쓴다). */
|
|
flush: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* 저장기 하나를 만든다. `projectId`가 없으면 아무것도 하지 않는다(조회 전용 상태).
|
|
* `onError`는 저장 실패를 화면에 알리는 쪽이 구현한다.
|
|
*/
|
|
export function createCulvertOptionWriter(
|
|
projectId: () => string | null,
|
|
onError?: (message: string) => void,
|
|
): CulvertOptionWriter {
|
|
/** 측점(누가거리 문자열) → 아직 안 보낸 옵션 조각. */
|
|
const pending = new Map<string, CulvertOptionPatch>();
|
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
let inFlight: Promise<void> = Promise.resolve();
|
|
|
|
const keyOf = (chainageM: number): string => chainageM.toFixed(2);
|
|
|
|
async function send(): Promise<void> {
|
|
const id = projectId();
|
|
if (!id || pending.size === 0) {
|
|
pending.clear();
|
|
return;
|
|
}
|
|
const batch = new Map(pending);
|
|
pending.clear();
|
|
try {
|
|
const current = await fetchDetailPipePoints(id);
|
|
let touched = false;
|
|
const points: DetailPipeInput[] = current.pipe_points.map((point) => {
|
|
// 좌표는 서버가 다시 계산하므로 싣지 않는다(DetailPipeInput 규약).
|
|
const { lonlat: _lonlat, ...input } = point;
|
|
const patch = batch.get(keyOf(point.chainage_m));
|
|
if (!patch) return input;
|
|
touched = true;
|
|
return { ...input, options: { ...(input.options ?? {}), ...patch } };
|
|
});
|
|
if (!touched) return;
|
|
await saveDetailPipePoints(id, points);
|
|
} catch (error) {
|
|
// 실패한 조각은 되살려 다음 기회에 다시 시도한다 — 조용히 잃으면 화면과
|
|
// 저장분이 갈린다.
|
|
batch.forEach((patch, key) => {
|
|
pending.set(key, { ...patch, ...(pending.get(key) ?? {}) });
|
|
});
|
|
onError?.(error instanceof Error ? error.message : "배수관 구간값 저장 실패");
|
|
}
|
|
}
|
|
|
|
return {
|
|
queue(chainageM, patch) {
|
|
const key = keyOf(chainageM);
|
|
pending.set(key, { ...(pending.get(key) ?? {}), ...patch });
|
|
if (timer) clearTimeout(timer);
|
|
timer = setTimeout(() => {
|
|
timer = null;
|
|
inFlight = inFlight.then(send);
|
|
}, SAVE_DELAY_MS);
|
|
},
|
|
async flush() {
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
inFlight = inFlight.then(send);
|
|
await inFlight;
|
|
},
|
|
};
|
|
}
|