/* ============================================================================= * B06_Section_UI_Cross_View_Structure.ts * 횡단 카드 ↔ **구조물 조정창** 배선 — 카드 렌더러(`_UI_Cross_View.ts`)에서 700줄 * 제한으로 분리했다(2026-08-24). 조정창이 부르는 동작을 제어기(기슭막이 4축·유입 * 구조물·다단·구간값·연동)로 넘기는 얇은 층이고, 기하나 그리기는 하지 않는다. * ========================================================================== */ import type { CrossSection } from "./B06_Section_Api_Fetch"; import type { RevetKey } from "./B06_Section_UI_Cross_Culvert"; import { REVET_EMBED_DEPTH_M } from "./B06_Section_UI_Cross_Culvert_Const"; import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const"; import type { CulvertLayout } from "./B06_Section_UI_Cross_Culvert_Types"; import type { WallAdjust } from "./B06_Section_UI_Cross_Culvert_Types"; import type { StructurePanelDeps } from "./B06_Section_UI_Cross_Structure_Panel"; import { showToast } from "@ui/ui_template_elements"; import { L } from "./B06_Section_UI_Section_Common"; import type { ExtraWallControl, InletStructureControl, RevetLinkControl, RevetOffsetControl, SpanRole, StructureSpanControl, } from "./B06_Section_UI_Cross_Culvert_Wire"; /** * 선택된 벽이 구간값을 갖는 요소인지 — 유입·유출 기슭막이와 유입 집수정만 자기 * 길이·전/후를 갖는다. 다단(extra·bextra)은 소유 벽 연장을 그대로 따른다. */ export function spanRoleOf(key: RevetKey, inletIsBasin = false): SpanRole | null { if (key === "inlet") return inletIsBasin ? "basin" : "inlet"; if (key === "outlet") return "outlet"; return null; } /** * 조정창 배선에 필요한 카드 상태·제어기 묶음. 마지막 계산 결과(높이·재질·단 수 등)는 * 카드가 그릴 때마다 바뀌므로 **값이 아니라 읽는 함수**로 받는다. */ export interface StructurePanelContext { section: CrossSection; /** * 4축·다단 조작이 실제로 실릴 측점의 누가거리. 연동 중인 링크 카드에서는 **소유 * 측점**이다 — 연동은 "본 기슭막이를 조절하면 같이 움직인다"는 뜻이므로 조작값이 * 한 곳에만 있어야 한다(2026-08-24 사용자). 연동을 풀면 이 카드 자신이 된다. */ adjustChainage: () => number; /** 이 카드가 옆 측점에서 연장돼 온 링크 카드인가. */ isLinked: boolean; revetOffset?: RevetOffsetControl; inletStructure?: InletStructureControl; extraWalls?: ExtraWallControl; structureSpan?: StructureSpanControl; revetLink?: RevetLinkControl; adjustOf: (key: RevetKey) => WallAdjust; /** 화면 좌(◀) 방향을 벽 기준 부호로 환산한다. */ outwardOf: (key: RevetKey) => number; heightOfWall: (key: RevetKey) => number; materialOfWall: (key: RevetKey) => RevetMaterial; /** 마지막 계산의 실제 적용 d(상하) — d 미지정 벽의 ▲▼ 시작값. */ appliedD: () => Map; pipeLengthM: () => number | null; inletIsBasin: () => boolean; inletOptions: () => { revetAllowed: boolean; basinLUAllowed: boolean }; extraState: () => { canAdd: boolean; count: number }; basinExtraState: () => { canAdd: boolean; count: number }; activeRevet: () => RevetKey | null; setActiveRevet: (key: RevetKey | null) => void; toggleRevet: (key: RevetKey) => void; /** 창 스크롤 자리를 기억할 카드 키(측점 id). */ scrollKey: string; } export function structurePanelDeps(ctx: StructurePanelContext): StructurePanelDeps { return { scrollKey: ctx.scrollKey, adjustFor: ctx.adjustOf, nudge: (key, screenDeltaM) => ctx.revetOffset?.update(ctx.adjustChainage(), key, { x: ctx.adjustOf(key).x + screenDeltaM * ctx.outwardOf(key), }), // d 미지정(자동)이면 실제 적용 자리에서부터 움직인다(2026-08-23 — d는 0점 // 기준 절대값이라 시작값이 필요하다). nudgeSlope: (key, deltaM) => ctx.revetOffset?.update(ctx.adjustChainage(), key, { d: (ctx.adjustOf(key).d ?? ctx.appliedD().get(key) ?? 0) + deltaM, }), nudgeHeight: (key, deltaM) => { const current = ctx.adjustOf(key); const base = current.h ?? ctx.heightOfWall(key); ctx.revetOffset?.update(ctx.adjustChainage(), key, { h: Math.round((base + deltaM) * 10) / 10, }); }, materialFor: (key) => ctx.materialOfWall(key), setMaterial: (key, material) => ctx.revetOffset?.update(ctx.adjustChainage(), key, { m: material }), heightFor: (key) => ctx.heightOfWall(key), reset: (key) => ctx.revetOffset?.reset(ctx.adjustChainage(), key), pipeLengthM: () => ctx.pipeLengthM(), close: () => { const active = ctx.activeRevet(); if (active) ctx.toggleRevet(active); }, structureFor: () => ctx.inletStructure?.valueFor(ctx.section) ?? "auto", setStructure: (value) => ctx.inletStructure?.set(ctx.adjustChainage(), value), basinAdjustFor: () => ctx.inletStructure?.adjustFor(ctx.section) ?? { innerWidthM: 1, innerHeightM: 1.2, lateralM: 0, slopeM: 0, }, updateBasin: (patch) => ctx.inletStructure?.updateAdjust(ctx.adjustChainage(), { ...ctx.inletStructure.adjustFor(ctx.section), ...patch, }), // 집수정 좌우는 **화면 기준**으로 받는다(2026-08-22 사용자 확정) — 유입이 우측인 // 측점에서 ◀가 화면 오른쪽으로 가던 반전을 없앤다. 노견 안쪽(음수)은 금지라 // 0에서 멈추고, 멈춘 사실은 토스트로 알린다(기슭막이와 같은 규칙). nudgeBasinLateral: (screenDeltaM) => { const current = ctx.inletStructure?.adjustFor(ctx.section); if (!current) return; const next = current.lateralM + screenDeltaM * ctx.outwardOf("inlet"); if (next < -1e-9) { showToast(L("B06_Cross_Revet_Limit_Inward"), "info"); } ctx.inletStructure?.updateAdjust(ctx.adjustChainage(), { ...current, lateralM: Math.max(0, Math.round(next * 10) / 10), }); }, resetBasin: () => ctx.inletStructure?.resetAdjust(ctx.adjustChainage()), // 집수정은 자리 고정 — 유입이 집수정이면 ◀/▶/↺를 숨긴다(추가 벽은 항상 이동). canNudge: (key) => key !== "inlet" || !ctx.inletIsBasin(), optionsFor: () => ctx.inletOptions(), extraState: () => ctx.extraState(), basinExtraState: () => ctx.basinExtraState(), setExtraCount: (count) => { // 줄어들며 사라지는 벽이 선택 중이면 선택을 유출 벽으로 옮긴다(빈 대상 방지). const active = ctx.activeRevet() ?? ""; if ( active.startsWith("extra") && !active.startsWith("bextra") && Number(active.slice(5)) >= count ) { ctx.setActiveRevet("outlet"); ctx.revetOffset?.select(ctx.section.chainage_m, "outlet"); } ctx.extraWalls?.setCount(ctx.adjustChainage(), count, "outlet"); }, setBasinExtraCount: (count) => { const activeBasin = ctx.activeRevet() ?? ""; if (activeBasin.startsWith("bextra") && Number(activeBasin.slice(6)) >= count) { ctx.setActiveRevet("inlet"); ctx.revetOffset?.select(ctx.section.chainage_m, "inlet"); } ctx.extraWalls?.setCount(ctx.adjustChainage(), count, "basin"); }, equalizeExtras: () => ctx.extraWalls?.equalize(ctx.adjustChainage()), // 구간값(길이·전·후) — 다단은 소유 벽 연장을 따르는 파생물이라 대상이 아니다. // 링크 카드에서 만져도 제어기가 소유 측점 값을 고친다(원천은 하나). spanFor: (key) => { const role = spanRoleOf(key, ctx.inletIsBasin()); return role && ctx.structureSpan ? ctx.structureSpan.valuesFor(ctx.section, role) : null; }, setSpan: (key, patch) => { const role = spanRoleOf(key, ctx.inletIsBasin()); if (role) ctx.structureSpan?.update(ctx.section, role, patch); }, linkState: () => ctx.isLinked ? { linked: ctx.revetLink?.linkedFor(ctx.section) ?? true } : null, setLinked: (linked) => ctx.revetLink?.setLinked(ctx.section.chainage_m, linked), followGrade: () => ctx.revetLink?.followGradeFor(ctx.structureSpan?.ownerOf(ctx.section) ?? ctx.section) ?? true, setFollowGrade: (follow) => { const owner = ctx.structureSpan?.ownerOf(ctx.section) ?? ctx.section; ctx.revetLink?.setFollowGrade(owner, follow); }, }; } /** * 조정창이 쓸 **카드 상태**를 마지막 기하 결과에서 뽑는다(2026-08-25 분리 — 카드 * 렌더러 700줄). 값 계산은 하지 않고 레이아웃 결과를 조정창 키 체계로 옮기기만 한다. */ export function culvertCardState(layout: CulvertLayout): { inletOptions: CulvertLayout["inletOptions"]; extraState: { canAdd: boolean; count: number }; basinExtraState: { canAdd: boolean; count: number }; wallSpecs: Map; appliedD: Map; } { const wallSpecs = new Map(); const specOf = (wall: CulvertLayout["walls"][number], key: RevetKey): void => { wallSpecs.set(key, { // 배관 벽은 순수 높이(바닥~상단 = 계산용 + 근입 0.5 — 2026-08-23 사용자). height: wall.role === "extra" ? wall.height : wall.height + REVET_EMBED_DEPTH_M, material: wall.material, }); }; for (const wall of layout.walls) specOf(wall, wall.role as RevetKey); layout.extraWalls.forEach((wall, i) => specOf(wall, `extra${i}` as RevetKey)); layout.basinExtras.forEach((wall, i) => specOf(wall, `bextra${i}` as RevetKey)); const appliedD = new Map(); appliedD.set("inlet", layout.revetShift.inlet.d ?? 0); appliedD.set("outlet", layout.revetShift.outlet.d ?? 0); layout.revetShift.extras.forEach((applied, i) => appliedD.set(`extra${i}` as RevetKey, applied.d ?? 0), ); layout.revetShift.basinExtras.forEach((applied, i) => appliedD.set(`bextra${i}` as RevetKey, applied.d ?? 0), ); return { inletOptions: layout.inletOptions, extraState: { canAdd: layout.outletFill.addable, count: layout.extraWalls.length }, basinExtraState: { canAdd: layout.basinFill.addable, count: layout.basinExtras.length }, wallSpecs, appliedD, }; }