/* ============================================================================= * B07_DesignDetail_UI_Cad_Structures.ts * 횡단도 **구조물 작도** — 배수관·기슭막이·세월교·BOX암거·물넘이포장. * * 산식은 새로 만들지 않는다. B06 화면이 쓰는 기하·그리기 함수를 **그대로** 불러 * 화면에 붙이지 않은 오프스크린 SVG에 그린 뒤, 그 도형을 CAD 엔티티로 옮긴다 * (2026-08-30 사용자 확정: 프론트에서 B06 산식 재사용 — 파이썬 포팅 금지). * 앞으로 B06에 구조물·치수·글자가 붙으면 여기 손대지 않아도 도면에 따라온다. * * 자리 맞추기는 서버가 도면에 실어 보내는 `cross_placements`가 정한다: * x_mm = offset*mm_per_m + ox, y_mm = (elev - dy)*mm_per_m + oy * SVG는 y가 아래로 자라므로 그릴 때 부호를 뒤집고 수확할 때 되돌린다. * ========================================================================== */ import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch"; import { loadSectionDetail } from "../B06_Section/B06_Section_Section_Store"; import { DESIGN_LAYER_ID, entityBox, overlayEntities, type Entity, } from "./B07_DesignDetail_Cross_Overlay"; /** 서버가 도면에 실어 보내는 측점별 실좌표(m) → 종이(mm) 변환값. */ export interface CrossPlacement { chainage_m: number; ox: number; oy: number; dy: number; mm_per_m: number; x0: number; x1: number; /** 블록 테두리(종이 mm) [x0, y0, x1, y1] — 그림을 이 안으로 자른다. */ frame?: number[]; } /** 측점과 도면 배치를 같은 자리로 볼 허용 오차(m). 정본이 누가거리를 0.01m로 끊어 쓴다. */ const CHAINAGE_TOLERANCE_M = 0.02; /** * 도면에 구조물 엔티티를 얹는다 (제자리 수정). 배치 메타가 없거나 자료를 못 읽으면 * 아무것도 하지 않는다 — 구조물이 빠져도 도면 자체는 열려야 한다. */ export async function appendStructureEntities( projectId: string, routeId: number, drawing: { entities: Record[]; cross_placements?: CrossPlacement[] }, ): Promise { const placements = drawing.cross_placements ?? []; if (!placements.length) return 0; let sections: CrossSection[]; try { // B05·B06과 같은 공유 캐시를 쓴다 — 도면마다 새로 받으면 장을 넘길 때마다 // 종횡단 상세를 다시 내려받아 0.2초씩 더 걸린다(2026-08-30 실측). sections = (await loadSectionDetail(projectId, routeId)).cross_sections; } catch { return 0; } let added = 0; const fresh: Entity[] = []; const replaced: number[][] = []; for (const placement of placements) { const section = sections.find( (item) => Math.abs(item.chainage_m - placement.chainage_m) <= CHAINAGE_TOLERANCE_M, ); const frame = placement.frame; if (!section || !frame) continue; const x = (offset: number): number => offset * placement.mm_per_m + placement.ox; const y = (elevation: number): number => -((elevation - placement.dy) * placement.mm_per_m + placement.oy); let entities: Entity[]; try { const drawn = overlayEntities(section, sections, x, y, frame); if (!drawn) continue; entities = drawn; } catch { continue; // 한 측점의 기하 실패가 도면 전체를 막으면 안 된다. } if (!entities.length) continue; // 이 블록 설계선은 우리가 다시 그렸다 — 서버가 낸 트림 전 설계선은 걷어낸다. replaced.push(frame); fresh.push(...entities); added += entities.length; } if (replaced.length) { // 걷어내기는 새 엔티티를 넣기 **전에** 한다 — 뒤에 하면 우리 것까지 같이 지운다. drawing.entities = drawing.entities.filter((entity) => { if (entity.layerId !== DESIGN_LAYER_ID) return true; const box = entityBox(entity); if (!box) return true; return !replaced.some( ([fx0, fy0, fx1, fy1]) => box[0] >= fx0 && box[2] <= fx1 && box[1] >= fy0 && box[3] <= fy1, ); }); } drawing.entities.push(...fresh); return added; }