- 레인 쌓임 층 풂 · 라벨 z 6 — ▲▼ 단추층 · 고정 Y축 · 테이블 · 유토곡선 위 - 라벨을 그래프 바닥 ▼ 단추 높이만큼 위로 · 위가 잘리면 덜 잘리는 쪽(알약 아래) - 줄 = 종류 이름 · 기준 측점 · 좌측 폼 컨테이너마다 첫 옵션 값 · 빈 값 (기본) · auto (자동) · 메모 뺌 - B05 · B06 공용 레인 한 곳 · 시험 test_54_14_balloon_lines · ORCA 시나리오 54-14_알약_라벨 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPcuokNm1ZR2kqgGGDfaJA
161 lines
6.7 KiB
TypeScript
161 lines
6.7 KiB
TypeScript
/* =============================================================================
|
|
* 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<string, string | number>,
|
|
): Record<string, Instance[]> {
|
|
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<string, string>,
|
|
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<string, string | number>,
|
|
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<string, string | number> {
|
|
return Object.fromEntries(
|
|
(type?.options ?? [])
|
|
.filter((option) => option.default !== null && option.default !== undefined)
|
|
.map((option) => [option.key, option.default as string | number]),
|
|
);
|
|
}
|