Files
Aislo/B05_Profile/B05_Profile_UI_Structures_Balloon.ts
T
eomsangdonandClaude Opus 5.5 ce7d0125d2 feat(structure): 54-39 · 54-40 · 54-41 선택 안 함 = 값 없음 · 기본값은 칸에 실제 값 · 연동/경사는 흙막이만 · 내공 칸 숨김 · 상자 안 단추 높이 템플릿
- 54-39 고르기 칸 빈 보기 「선택 안 함」 · 「(기본)」 꼬리 · 숫자 칸 「(기본)」 자리표시 · 알약 라벨 「(기본)」 걷음
- 54-39 엔진 짝(파이썬 · TS): 배관 구조 · 형태 · 관종 · 기초 · 길이 · 전 · 후 · 집수정 높이 빈 값 = 값 없음(정의 기본값으로 안 읽음) · 관경 빈 값 = 관 없음 · 집수정 모양(basin_form) 실음
- 54-39 횡단 기하: 구조 · 집수정 모양 안 고름 = 집수정 없음 · 구조 · 형태 안 고른 배관 벽 = 그리지 않음 · patch 형식이 없으면 폼 모양
- 54-39 새로 놓기 · 종류 바꾸기 · 자동 배치 = 집수정 모양 L 도 실제 값
- 54-40 [연동] [경사] = 유입 · 유출 구조 흙막이일 때만 · 집수정 「내공」 줄 화면에서 숨김(값 · 그림 그대로)
- 54-41 상자 안 단추(연동 · 경사 · 선택 · 해제) 높이 = 숫자 칸 묶음 26 · 상자 밖 단추 그대로
- 시험: 새 규칙 기대값(culvert_set · culvert_sets_mirror · structure_span · new_structure_defaults)

화면 확인(sub5_검증): 새 배관 = 1000 · 집수정 L형 1.5 · 흙막이 찰 10 · 1.5 · 5 · 5 실제 값 · (기본) 0 · 유입 집수정 0 ↔ 흙막이 1 단추 줄 · 횡단 eval 형태 빈 값 = 집수정 · 벽 0 · 상자 안 단추 26 · 상자 밖 35.3 · 내공 줄 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
2026-10-06 14:37:10 +09:00

140 lines
5.8 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Structures_Balloon.ts
* 종단 구조물 알약 정보 라벨 줄(54-14) — 화면 요소 없는 순수 규칙. B05 · B06 알약 레인이 같이 씀.
*
* · 줄 = 종류 이름(기타 = 손 이름) · 기준 측점(구간 = 시작 ~ 끝) · 컨테이너마다 첫 옵션 값 한 줄
* (「관: 관경 1000」) · 컨테이너가 없으면 앞 두 줄만 · 메모 · 그 밖 옵션은 안 씀
* · 컨테이너 = 좌측 패널 폼과 같은 상자 — 명세 종류(`STRUCTURE_SPECS`)는 명세 컨테이너(선택 컨테이너는
* 인스턴스마다) · 그 밖 종류는 정의 옵션 그룹(`StructureType.groups`)
* · 첫 옵션 = 지금 보이는 첫 입력 칸(자동 칸 · 갈래가 없어 가린 칸 · 끈 칸은 건너뜀)
* · 값 `auto` = 「(자동)」 · 빈 값 = 「—」(54-39 · 기본값으로 안 바꿈 · 기본값은 새 구조물 칸에 실제 값)
* ========================================================================== */
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 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 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}`;
return item
? `${head}: ${itemLabel(c, item, names)} ${specValue(c, item, values, names)}`
: `${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>,
): 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;
return raw ? shown(raw) : 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}` : NONE;
return `${group.name}: ${option.label} ${value}`;
});
}