/* ============================================================================= * common_util/common_util_structure_attach.ts * 형상 얹기 한 벌(323-4) — 측점(횡단) + 현황(관 · 구조물) + 구조물 표 → 그 측점 그림 자료. * * 구조물 열쇠(`SHAPE_KEYS` · 빈칸 경고 `blank`)를 걷고 서버와 같은 차례로 다시 얹음 — 관 세트 → 벽 → * 포장 → 맹암거 → 빈칸 경고. 고르기 = 표 형상(322-1). 화면은 초안 ?? 저장분 현황으로 이것 하나를 씀 * (`B06_Section_Section_Drafts.withDraftShapes`) — 저장 전 그림 = 저장 뒤 그림. * 파이썬 짝 = `B06_Section/B06_Section_Engine_Attach.py` `attach_shapes`(서버 상세 읽기 · 재생성 응답) · * 거울 시험 = `resources/tester/test_323_4_attach_mirror.py`. * ========================================================================== */ import { attachCulvertSets, buildCulvertSets, registryDefaults, type CulvertPipePoint, } from "./common_util_culvert_sets"; import { attachWallSpecs, wallSpecsFrom, type WallStructureInput, } from "./common_util_structure_walls"; import { attachPavementSpecs, pavementSpecsFrom, type PavementStructureInput, } from "./common_util_structure_pavement"; import { attachUnderdrainSpecs, underdrainSpecsFrom, type UnderdrainInput, } from "./common_util_structure_underdrain"; import { catalogRow, fieldName } from "./common_util_structure_catalog"; import { blankKeys, planStations, SAME_STATION_M, type PlanPipe, type PlanStructure, type PlanType, } from "./common_util_structure_station"; import { SHAPE_KEYS } from "./common_util_structure_shape_keys"; export { SHAPE_KEYS }; export interface AttachType extends PlanType { name: string; options: ReadonlyArray<{ key: string; label?: string; default: unknown }>; } /** 빈 필수 칸 → 「종류 이름 칸 이름」(칸 이름 = 그 종류 정의 → 다른 종류 정의 → 부품 상자 → 키). */ function blankLabels( typeId: string, options: Record | null | undefined, types: ReadonlyArray, ): string[] { const name = catalogRow(typeId)?.name ?? typeId; const ordered = [...types.filter((t) => t.type_id === typeId), ...types]; return blankKeys(typeId, options).map((key) => { const option = ordered.flatMap((t) => t.options).find((o) => o.key === key); return `${name} ${option?.label ?? fieldName(key) ?? key}`; }); } /** 빈칸 경고 — 측점 계획 자리마다 덮는 관 · 구조물의 빈 필수 칸(파이썬 `_attach_blanks` 짝). */ function attachBlanks( sections: Array>, pipes: ReadonlyArray, structures: ReadonlyArray, types: ReadonlyArray, ): void { const owners = new Map | null | undefined]>(); for (const pipe of pipes) owners.set(pipe.id ?? `pipe-${pipe.chainage_m.toFixed(2)}`, [ pipe.facility ?? "pipe", pipe.options, ]); for (const item of structures) if (item.structure_id) owners.set(item.structure_id, [item.type_id, item.options]); for (const station of planStations(pipes, structures, types)) { if (!station.blank.length) continue; const labels = [ ...new Set( station.owners.flatMap((owner) => { const hit = owners.get(owner); return hit ? blankLabels(hit[0], hit[1], types) : []; }), ), ]; for (const section of sections) if (Math.abs(Number(section.chainage_m) - station.chainage_m) < SAME_STATION_M) section.blank = labels; } } /** 안 얹은 관 · 시설 누가거리(458) — 시종점 밖으로 밀림 · 주인 측점이 0.51 m 넘게 멂. */ export interface AttachMisses { outside: number[]; far: number[]; } /** 주인 측점 거리 상한(m) — `design_selection.PIPE_MATCH_M` · 파이썬 `PIPE_MATCH_M` 짝. */ const PIPE_MATCH_M = 0.51; /** 시종점 밖으로 밀린 관인가(458) — 시종점 측점 자리인데 좌표가 노선 진행 반대쪽으로 0.51 m 넘게 바깥 * (따라감이 누가거리를 0 · 끝에 붙여 둠 · 옆 거리는 안 봄 · 좌표 없으면 아님 · 파이썬 `_outside_route` 짝). */ function outsideRoute( pipe: { chainage_m: number; x?: number | null; y?: number | null }, sections: ReadonlyArray>, ): boolean { const placed = sections .filter((s) => [s.chainage_m, s.center_x, s.center_y].every(Number.isFinite)) .sort((a, b) => Number(a.chainage_m) - Number(b.chainage_m)); if (placed.length < 2 || !Number.isFinite(pipe.x) || !Number.isFinite(pipe.y)) return false; const ends: Array<[Record, Record]> = [ [placed[0], placed[1]], [placed[placed.length - 1], placed[placed.length - 2]], ]; return ends.some(([end, next]) => { if (Math.abs(Number(end.chainage_m) - pipe.chainage_m) >= PIPE_MATCH_M) return false; const [ex, ey] = [Number(end.center_x), Number(end.center_y)]; const [dx, dy] = [Number(next.center_x) - ex, Number(next.center_y) - ey]; const span = Math.hypot(dx, dy); return span > 0 && ((pipe.x! - ex) * dx + (pipe.y! - ey) * dy) / span <= -PIPE_MATCH_M; }); } /** 측점마다 그림 자료를 걷고 현황으로 다시 얹음(파이썬 `attach_shapes` 짝 · 제자리 고침) · * 시종점 밖 관 · 주인 측점 먼 관은 안 얹고 돌려줌(458 · 경고는 부르는 쪽). */ export function attachShapes( sections: Array>, allPipes: ReadonlyArray, structures: ReadonlyArray, types: ReadonlyArray, ): AttachMisses { for (const section of sections) for (const key of [...SHAPE_KEYS, "blank"]) delete section[key]; const misses: AttachMisses = { outside: [], far: [] }; const pipes = allPipes.filter((pipe) => { const out = outsideRoute(pipe as Parameters[0], sections); if (out) misses.outside.push(pipe.chainage_m); return !out; }); const registry = registryDefaults(types); attachCulvertSets( sections, buildCulvertSets(pipes as readonly CulvertPipePoint[], registry), misses.far, ); const shaped = (shape: string) => types.filter((t) => catalogRow(t.type_id)?.shape === shape); // 관 세트가 선 측점은 벽을 안 얹음(`attachWallSpecs`) — 차례 = 서버와 같음 attachWallSpecs( sections, wallSpecsFrom( structures as unknown as WallStructureInput[], new Map(shaped("wall").map((t) => [t.type_id, t.name] as const)), ), ); attachPavementSpecs( sections, pavementSpecsFrom( structures as unknown as PavementStructureInput[], new Set(shaped("pavement").map((t) => t.type_id)), ), ); attachUnderdrainSpecs(sections, underdrainSpecsFrom(structures as UnderdrainInput[])); attachBlanks(sections, pipes, structures, types); return misses; }