/* ============================================================================= * common_util_structure_walls.ts * 구조물 정본(C군 사면안정 벽)을 횡단 측점 제원으로 바꾸는 자리 — * 파이썬 `B06_Section_Engine_Structures_Wall.py` 의 **짝**이다(거울 테스트로 대조). * * 왜 두 벌인가(CLAUDE.md 5장) — 서버는 상세를 내려보낼 때 얹어야 하고(저장분 기준), * 브라우저는 사용자가 **아직 저장하지 않은** 목록으로 즉시 얹어야 한다. 값을 만드는 * 산식이 같아야 하므로 두 파일 머리에 짝임을 적고 거울 테스트를 둔다. * * 얹는 것은 제원뿐이다 — 도형·면적은 `B06_Section_UI_Cross_Revetment` 가 그린다. * ========================================================================== */ /** 구조물 정본 한 건(필요한 칸만). `structures.json` 과 같은 이름을 쓴다. */ export interface WallStructureInput { structure_id?: string | null; type_id: string; placement?: string | null; chainage_m: number; start_m?: number | null; end_m?: number | null; options?: Record | null; } /** 측점에 얹는 벽 제원 — `section.revetment` 와 같은 꼴. */ export interface WallSpec { structure_id: string | null; type_id: string; name: string; start_m: number; end_m: number; anchor_m: number; form: string | null; height_m: number | null; side: string | null; /** 기초 축 — "기초유" | "기초버림". 저장 칸과 같은 글자(터파기 그림이 이 값으로 갈린다). */ foundation: string | null; tiers: number | null; lift_m: number | null; shift_m: number | null; } /** 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 파이썬 `_EDGE_TOLERANCE_M`. */ const EDGE_TOLERANCE_M = 0.02; /** * 구조물 종류 → 횡단 기하가 아는 형태 이름 — 파이썬 `_FORM_BY_TYPE` 와 같은 표. * * ⚠⚠ **「형상 동일」이 「수량 동일」이 아니다**(2026-09-09 사용자 확정 4차 원문: * 「흙막이는 횡단도에서 표현방식들과 옵션들은 동일하게 반영 · **형상은 동일** · * 물론 **데이터는 분리하여 계산**되어야 함」). * ⇒ 흙막이는 기슭막이와 **같은 그리기 경로·같은 옵션 벌**을 쓰되, 원단위·공종코드·성분 * 줄은 **흙막이 자기 것**이다. 그림이 같다고 수량을 빌려 쓰면 안 된다. * ⚠ 반대 방향의 사례가 골막이다 — 그림도 수량도 다르고, 두께식마저 다른 식이었다. * **구조물 계열이 다르면 상수를 나눠 쓸 것.** */ export const FORM_BY_TYPE: Record = { masonry_wet: "돌쌓기(찰)", masonry_dry: "돌쌓기(메)", boulder_masonry: "돌쌓기(메)", retaining_wall: "콘크리트", soil_guard: "통나무·목재틀", }; const num = (value: unknown): number | null => typeof value === "number" && Number.isFinite(value) ? value : null; /** * C군 벽 구조물을 제원 목록으로 바꾼다. 이름표(`names`)는 타입 레지스트리에서 온다. * 구간(start·end)이 없는 항목은 건너뛴다 — 벽은 구간형이다. */ export function wallSpecsFrom( structures: readonly WallStructureInput[], names: ReadonlyMap, ): WallSpec[] { const specs: WallSpec[] = []; for (const structure of structures) { const name = names.get(structure.type_id); if (!name) continue; const start = num(structure.start_m); const end = num(structure.end_m); if (start === null || end === null) continue; const options = (structure.options ?? {}) as Record; specs.push({ structure_id: structure.structure_id ?? null, type_id: structure.type_id, name, start_m: Math.min(start, end), end_m: Math.max(start, end), anchor_m: num(structure.chainage_m) ?? Math.min(start, end), form: (options.form as string) || FORM_BY_TYPE[structure.type_id] || null, height_m: num(options.height_m), side: (options.side as string) ?? null, // 기초 축 — 저장 칸과 **같은 글자**. 초안 경로에서 빠지면 터파기가 안 그려진다. foundation: (options.foundation as string) ?? null, tiers: num(options.tiers), lift_m: num(options.lift_m), shift_m: num(options.shift_m), }); } return specs; } /** * 구간 안 측점에 제원을 얹는다(파이썬 `attach_wall_structures`). 얹은 개수를 돌려준다. * 관 세트가 이미 붙은 측점은 건드리지 않는다 — 한 자리에 두 벽이 겹치면 읽히지 않는다. */ export function attachWallSpecs( sections: Array>, specs: readonly WallSpec[], ): number { if (!specs.length) return 0; let attached = 0; for (const section of sections) { const chainage = num(section.chainage_m); if (chainage === null) continue; if (section.culvert || section.revetment) continue; for (const spec of specs) { if ( spec.start_m - EDGE_TOLERANCE_M <= chainage && chainage <= spec.end_m + EDGE_TOLERANCE_M ) { section.revetment = spec; attached += 1; break; } } } return attached; }