/* ============================================================================= * B05_Profile_UI_Structures_Balloon.ts * 종단 구조물 알약 정보 라벨 줄(54-14) — 화면 요소 없는 순수 규칙. B05 · B06 알약 레인이 같이 씀. * * · 줄 = 종류 이름(기타 = 손 이름) · 기준 측점(구간 = 시작 ~ 끝) · 컨테이너마다 첫 옵션 값 한 줄 * (「관: 관경 1000」) · 컨테이너가 없으면 앞 두 줄만 · 메모 · 그 밖 옵션은 안 씀 * · 컨테이너 = 좌측 패널 폼과 같은 상자 — 명세 종류(`STRUCTURE_SPECS`)는 명세 컨테이너(선택 컨테이너는 * 인스턴스마다) · 그 밖 종류는 정의 옵션 그룹(`StructureType.groups`) * · 첫 옵션 = 지금 보이는 첫 입력 칸(자동 칸 · 갈래가 없어 가린 칸 · 끈 칸은 건너뜀) * · 빈 값 = 등록부 기본값 「(기본)」 · 값 `auto` = 「(자동)」(54-13 표시와 같음) · 그것도 없으면 「—」 * ========================================================================== */ import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures"; import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe"; import { bodyOf, boxOptionsIn } from "../A00_Common/design/design_structure_box"; import { withLegacyGuards } from "../A00_Common/design/design_structure_ford"; import { branchOf, choiceLabel, choicesOf, containerTitle, itemLabel, readState, type Instance, type SpecContainer, type SpecItem, type StructureSpec, } from "../A00_Common/design/design_structure_spec"; const DEFAULT_MARK = "(기본)"; const AUTO_MARK = "(자동)"; const NONE = "—"; /** 선택 컨테이너 개수를 밖에서 정함(배관 흙막이 = 유출 단 수 · 폼 `hooks.count` 와 같은 값). */ export type ContainerCount = (containerId: string) => number | null | undefined; /** 라벨 줄 — [이름, 기준 측점, ...컨테이너 줄]. */ export function balloonLines( structure: StructureInstance, type: StructureType | undefined, head: { name: string; position: string }, count?: ContainerCount, ): string[] { const spec = STRUCTURE_SPECS[structure.type_id]; const rest = spec ? specLines(spec, structure, type, count) : groupLines(structure.options ?? {}, type); return [head.name, head.position, ...rest]; } /** 명세 컨테이너 줄 — 폼(`Drainage_Facility.setFacility` · `paintStructureSpec`)과 같은 값 · 이름. */ function specLines( spec: StructureSpec, structure: StructureInstance, type: StructureType | undefined, count?: ContainerCount, ): string[] { let names = type?.spec_names ?? {}; if (structure.type_id === "pipe") { // 정의 파일의 옛 이름 「흙막이 추가」 — 명세 이름 「흙막이」가 앞섬(54-1) const { soil_add: _old, ...pipeNames } = names; names = pipeNames; } const state = formState(spec, structure.type_id, structure.options ?? {}); // 빈 칸이 그림에서 쓰는 값 — 등록부 기본값을 같은 길로 읽은 필수 컨테이너 값 const fallback = formState(spec, structure.type_id, registryDefaults(type)); const groups = Object.fromEntries((type?.groups ?? []).map((g) => [g.id, g.name])); return spec.containers.flatMap((c) => { const title = containerTitle(c, names, groups[c.link]); return instancesOf(c, state[c.id] ?? [], count).map((values, index) => { const item = c.items.find( (entry) => entry.kind !== "auto" && (!entry.by || branchOf(entry, values)), ); const head = c.required ? title : `${title} ${index + 1}`; const base = c.required ? (fallback[c.id]?.[0]?.[item?.id ?? ""] ?? "") : ""; return item ? `${head}: ${itemLabel(c, item, names)} ${specValue(c, item, values, names, base)}` : `${head}: ${NONE}`; }); }); } /** 저장 값 → 폼 칸 값 — 폼(`Drainage_Facility.setFacility`)과 같은 손질(BOX 옛 설치 · 본체 규격 · * 세월교 옛 날개벽). */ function formState( spec: StructureSpec, typeId: string, options: Record, ): Record { const input = typeId === "box_culvert" ? boxOptionsIn(options) : typeId === "ford_bridge" ? withLegacyGuards(options) : options; const state = readState(spec, input); // 본체 규격은 저장 안 함 — 폭 · 높이로 되찾음 if (typeId === "box_culvert") state.body = [{ ...state.body?.[0], ...bodyOf(options) }]; return state; } /** 보이는 인스턴스 — 필수 = 한 벌 · 선택 = 개수가 정해졌으면 그 수(모자라면 빈 인스턴스) · 아니면 저장된 것. */ function instancesOf(c: SpecContainer, list: Instance[], count?: ContainerCount): Instance[] { if (c.required) return [list[0] ?? {}]; const n = count?.(c.id); return n === undefined || n === null ? list : Array.from({ length: n }, (_, index) => list[index] ?? {}); } function specValue( c: SpecContainer, item: SpecItem, values: Instance, names: Record, fallback: string, ): string { const shown = (raw: string): string => { const choice = choicesOf(item, values).find((ch) => ch.value === raw); return choice ? choiceLabel(c, item, choice, names) : raw; }; const raw = (values[item.id] ?? "").trim(); if (raw === "auto") return AUTO_MARK; if (raw) return shown(raw); return fallback.trim() ? `${shown(fallback.trim())} ${DEFAULT_MARK}` : NONE; } /** 정의 옵션 그룹 줄 — 그룹마다 첫 옵션(정의에 있고 켜진 칸). */ function groupLines( options: Record, type: StructureType | undefined, ): string[] { const byKey = new Map((type?.options ?? []).map((option) => [option.key, option])); return (type?.groups ?? []).map((group) => { const option = group.options .map((key) => byKey.get(key)) .find((entry) => entry && entry.visible !== false && entry.enabled !== false); if (!option) return `${group.name}: ${NONE}`; const unit = option.unit ?? ""; const raw = options[option.key]; const value = raw === "auto" ? AUTO_MARK : raw !== undefined && raw !== "" ? `${raw}${unit}` : option.default !== null && option.default !== undefined && option.default !== "" ? `${option.default}${unit} ${DEFAULT_MARK}` : NONE; return `${group.name}: ${option.label} ${value}`; }); } /** 등록부 기본값(키 → 값) — 빈 칸이 그림에서 쓰는 값. */ function registryDefaults(type: StructureType | undefined): Record { return Object.fromEntries( (type?.options ?? []) .filter((option) => option.default !== null && option.default !== undefined) .map((option) => [option.key, option.default as string | number]), ); }