/* ============================================================================= * common_util/common_util_structure_catalog.ts * 구조물 표(catalog · PLAN 322-1) — 종류마다 「무엇을 할지」 한 줄 = 정의 `반영` ⊕ 부품 상자 처음값 ⊕ 부품 끔. * * 종류 키 · 군 글자 · 자리꼴 · managed_by 로 가리던 자리(목록 · 측점 · 카드 이름 · 형상 · 3D · 알약)가 이 한 줄을 * 읽음. 그리는 함수는 그대로(그림 자료 열쇠만 봄). 부품 상자 = `common_util_structure_parts.json`(파이썬과 같은 파일). * 정의를 받기 전 = 처음값만으로 선 표(이름 · 군 · 정본 · 색 없음) — 받으면 `useCatalogDefinition` 이 갈아 끼움. * 파이썬 짝 = `common_util_structure_catalog.py` · 거울 시험 = `resources/tester/test_322_1_catalog_mirror.py`. * ========================================================================== */ import PARTS from "./common_util_structure_parts.json" with { type: "json" }; /** 고르기 목록 자리 — both = B05 · B06 · b06 = B06 만 · hidden = 안 보임(옛 자료는 그대로 그림). */ export type ListPlace = "both" | "b06" | "hidden"; export type PipeOwner = "pipe_points" | "structures"; export interface CatalogRow { typeId: string; name: string; group: string; groupName: string; groupOrder: number; typeOrder: number; placement: string; owner: PipeOwner; list: ListPlace; /** 횡단도 부품 — none · anchor · span3. */ station: string; /** 카드 이름 부품 — name · pipe. */ title: string; /** 형상 부품 — none · pipe_set · box · ford · ford_pavement · hidden_revet · wall · berm · underdrain · pavement · spoil. */ shape: string; shapeOpts: Record; /** 3D 부품 — none · pipe · box · ford · chute · wall · surface. */ model3d: string; color: string | null; } type Branch = "횡단도" | "카드이름" | "형상" | "3D"; type Reflect = Partial> & { 형상칸?: Record; 자리꼴?: string; }; /** 정의 한 벌 중 표가 읽는 칸 — `반영` · `부품` 은 M02 [저장] 때 생김(없으면 처음값). */ export interface CatalogDefinition { 구조물군?: Record; types?: ReadonlyArray<{ type_id: string; name?: string; group?: string; placement?: string; managed_by?: string | null; enabled?: boolean; style?: { color?: string }; options?: ReadonlyArray<{ key: string; choices?: readonly string[] }>; }>; 반영?: Record; 부품?: Record; } interface Parts { parts: Record>; 처음값: Record; } /** 갈래 → 부품을 끈 값(`부품.{갈래}.{id} === false`). */ const OFF: Record = { 횡단도: "none", 카드이름: "name", 형상: "none", "3D": "none", }; /** 정의 + 부품 상자 → 종류마다 한 줄(정의 types 차례 · 정의가 없으면 처음값 차례). */ export function resolveCatalog( definition: CatalogDefinition | null, parts: Parts = PARTS as unknown as Parts, ): Map { const start = parts.처음값; const groups = definition?.구조물군 ?? {}; const groupKeys = Object.keys(groups); const off = new Set( Object.entries(definition?.부품 ?? {}) .filter(([, on]) => on === false) .map(([id]) => id), ); const types = definition?.types?.length ? definition.types : Object.keys(start).map((type_id) => ({ type_id }) as const); const rows = new Map(); types.forEach((item, typeOrder) => { const id = item.type_id; const base = start[id] ?? {}; const mine = definition?.반영?.[id] ?? {}; const part = (branch: Branch): string => { const value = mine[branch] ?? base[branch] ?? OFF[branch]; return off.has(`${branch}.${value}`) ? OFF[branch] : value; }; const group = "group" in item ? (item.group ?? "") : ""; const index = groupKeys.indexOf(group); rows.set(id, { typeId: id, name: ("name" in item && item.name) || id, group, groupName: groups[group] ?? group, groupOrder: index < 0 ? groupKeys.length : index, typeOrder, placement: base.자리꼴 ?? ("placement" in item ? item.placement : undefined) ?? "point", owner: "managed_by" in item && item.managed_by === "pipe_points" ? "pipe_points" : "structures", // 반영이 없으면 옛 짝 — 처음값 hidden 은 숨김 · 그 밖은 정의 enabled 가 가름(꺼짐 = B06 만) list: (mine.목록 ?? (base.목록 === "hidden" ? "hidden" : "enabled" in item && item.enabled === false ? "b06" : "both")) as ListPlace, station: part("횡단도"), title: part("카드이름"), shape: part("형상"), shapeOpts: { ...(base.형상칸 ?? {}), ...(mine.형상칸 ?? {}) }, model3d: part("3D"), color: ("style" in item && item.style?.color) || null, }); }); return rows; } let current = resolveCatalog(null); let pipeKinds: string[] = []; /** 정의를 받으면(종류 레지스트리 · `fetchStructureTypes`) 표를 갈아 끼움 — 못 받았으면 처음값 표 그대로. */ export function useCatalogDefinition(definition: CatalogDefinition | null): void { current = resolveCatalog(definition); pipeKinds = [ ...new Set( (definition?.types ?? []) .filter((type) => current.get(type.type_id)?.title === "pipe") .flatMap( (type) => type.options?.find((option) => option.key === "pipe_kind")?.choices ?? [], ), ), ]; } /** 종류 한 줄 — 표에 없는 종류면 undefined. */ export const catalogRow = (typeId: string): CatalogRow | undefined => current.get(typeId); /** 고르기 목록에 보일 종류인가 — B05 = both · B06 = both + b06(표에 없는 종류는 보임). */ export const listPick = (page: "b05" | "b06") => (type: { type_id: string }): boolean => { const list = catalogRow(type.type_id)?.list ?? "both"; return list === "both" || (page === "b06" && list === "b06"); }; /** 카드 · 측점 이름 — 카드이름 pipe = 「관종 D관경」(빈칸 = 그 종류 정의 기본값) · 그 밖 = 종류 이름. */ export function cardTitle( typeId: string, options: Record | null | undefined, defaults: Record = {}, ): string { const row = catalogRow(typeId); if (row?.title !== "pipe") return row?.name ?? typeId; const value = (key: string): unknown => { const own = options?.[key]; return own === undefined || own === null || own === "" ? defaults[key] : own; }; return `${value("pipe_kind") ?? ""} D${Math.trunc(Number(value("pipe_diameter_mm"))) || 0}`; } /** 종류가 지금 쓰는 형상 부품의 필수 칸(비면 그 그림이 안 섬 · 빈칸 경고 · 322-3) — 없으면 빈 목록. */ /** 어느 종류 정의에도 없는 읽는 칸의 화면 글(부품 상자 `칸 이름` · 빈칸 경고 323-4 · 파이썬 `field_name` 짝). */ export const fieldName = (key: string): string | undefined => (PARTS as unknown as { "칸 이름"?: Record })["칸 이름"]?.[key]; export function shapeNeeds(typeId: string): readonly string[] { const shape = catalogRow(typeId)?.shape ?? "none"; const box = (PARTS as unknown as Parts).parts.형상 as Record; return Object.hasOwn(box, shape) ? (box[shape]["필수 칸"] ?? []) : []; } /** 관 공식 이름의 관종들(정의 선택지) — 표시 이름이 「배수관」으로 줄일 이름을 알아봄. */ export const pipeTitleKinds = (): readonly string[] => pipeKinds;