diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 0b5b51c9..757e2876 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -43,6 +43,7 @@ import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul"; import { computeHaulPlan } from "@util/common_util_mass_haul_balance"; import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view"; import { staleDesignChainages } from "./B06_Section_UI_Section_Common"; +import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const"; import { createStandardPanel, type StandardPanelController } from "./B06_Section_UI_Standard_Panel"; import { createB06StructuresPanel, @@ -138,6 +139,18 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { const structuresPanel = createB06StructuresPanel({ projectId, detail: () => sectionDetail, + // 폼 기본 높이 = 지금 도면에 그려진 순수 높이(조정창이 보여주던 값과 같은 계산). + wallHeight: (chainageM, role) => { + const owner = sectionDetail?.cross_sections.find( + (section) => Math.abs(section.chainage_m - chainageM) < 0.51, + ); + const culvert = owner?.culvert; + if (!owner || !culvert) return null; + const spec = role === "outlet" ? culvert.outlet : culvert.inlet; + const adjust = stationControls.revetOffset.adjustFor(owner, role); + return revetWallSpec(spec, adjust, culvert.hidden_pipe === true, culvert.diameter_m) + .pureHeight; + }, stationInterval: () => stationInterval ?? 20, focusChainage: focusStationAt, queuePipeOptions: (chainageM, patch) => stationControls.queueCulvertOptions(chainageM, patch), @@ -183,19 +196,39 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { put(side, "revet_after_m", num(`${prefix}_revet_after_m`)); put(side, "structure", patch[`${prefix}_type`]); } - // 폼이 높이·형태를 정하면 그 벽의 **조정값**(revet_adjust.h·m)은 비운다 — - // 조정값이 남아 있으면 그림이 폼 값을 따라오지 않는다(2026-08-29 사용자 보고). + // 배관 벽의 **높이·형태는 스펙이 아니라 조정값**(revet_adjust.h·m)이 정한다 + // — `revetWallSpec`이 배관에서는 spec.revet_height_m을 쓰지 않기 때문이다. + // 폼에서 고친 값을 조정창과 같은 채널로 보내야 도면이 따라온다(2026-08-29 사용자). for (const [role, prefix] of [ ["inlet", "inlet"], ["outlet", "outlet"], ] as const) { - const patchHeight = patch[`${prefix}_revet_height_m`] !== undefined; - const patchForm = patch[`${prefix}_revet_form`] !== undefined; - if (!patchHeight && !patchForm) continue; + const height = num(`${prefix}_revet_height_m`); + const form = patch[`${prefix}_revet_form`]; + if (height === undefined && typeof form !== "string") continue; stationControls.revetOffset.update(owner.chainage_m, role, { - ...(patchHeight ? { h: null } : {}), - ...(patchForm ? { m: null } : {}), + ...(height !== undefined ? { h: height } : {}), + ...(typeof form === "string" ? { m: form } : {}), }); + // 형태마다 높이 한계가 있다(돌쌓기(메) 2.0m 등). 기하가 잘라 낸 실제 높이를 + // 폼에 되돌린다 — 숫자만 커지고 그림은 그대로인 상태를 남기지 않는다. + if (height === undefined) continue; + const applied = revetWallSpec( + role === "outlet" ? culvert.outlet : culvert.inlet, + stationControls.revetOffset.adjustFor(owner, role), + culvert.hidden_pipe === true, + culvert.diameter_m, + ).pureHeight; + if (Math.abs(applied - height) > 0.05) { + structuresPanel.overrideOptions(owner.chainage_m, { + [`${prefix}_revet_height_m`]: Number(applied.toFixed(1)), + }); + showToast( + `${prefix === "outlet" ? "유출" : "유입"} 기슭막이 높이는 형태 한계로 ` + + `${applied.toFixed(1)}m까지만 적용됩니다.`, + "error", + ); + } } put(culvert.inlet, "basin_length_m", num("inlet_basin_length_m")); put(culvert.inlet, "basin_before_m", num("inlet_basin_before_m")); diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 1d22679f..3380398f 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -38,6 +38,9 @@ export interface B06StructuresPanelDeps { /** 횡단 캐시 — 폼 기본값을 조정창과 **같은 원천**(culvert 스펙)에서 읽는다 * (2026-08-29 사용자: 예전 조정창이 보여주던 값이 기본값이다). */ detail?: () => SectionDetailResponse | null; + /** 지금 그려진 벽 높이(m) — 배관 벽은 스펙이 아니라 기하가 정하므로 폼 기본 + * 높이도 이 값을 쓴다(조정창 표시값과 같다). */ + wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null; /** 측점 간격(m) — 측점번호+잔여거리 환산용. */ stationInterval: () => number; /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */ @@ -62,6 +65,9 @@ export interface B06StructuresPanel { load: () => Promise; /** 횡단도에서 고른 벽·구체의 시설을 폼에 올린다(null = 해제). */ showPipeAt: (chainageM: number | null) => void; + /** 기하가 한계로 자른 실제 값을 폼에 되돌린다 — 입력 숫자만 커지고 그림은 그대로인 + * 상태를 막는다(2026-08-29 사용자: 재질 높이 한계에 걸린 값). */ + overrideOptions: (chainageM: number, patch: Record) => void; /** 고른 횡단도(측점)에 있는 구조물을 폼에 올린다 — 계곡 통과 시설이 먼저고, * 없으면 그 측점을 덮는 구조물 정본 항목. 둘 다 없으면 해제(2026-08-29). */ showAtChainage: (chainageM: number | null) => void; @@ -94,6 +100,7 @@ function withSpecDefaults( options: Record | undefined, detail: SectionDetailResponse | null, chainageM: number, + wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null, ): Record | undefined { const owner = detail?.cross_sections.find( (section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M, @@ -112,7 +119,8 @@ function withSpecDefaults( ] as const) { put(`${prefix}_type`, side.structure); put(`${prefix}_revet_form`, side.revet_form); - put(`${prefix}_revet_height_m`, side.revet_height_m); + // 높이는 기하가 정한 실제 값이 기준이다 — 못 구하면 스펙 값으로 물러난다. + put(`${prefix}_revet_height_m`, wallHeight?.(chainageM, prefix) ?? side.revet_height_m); put(`${prefix}_revet_length_m`, side.revet_length_m); put(`${prefix}_revet_before_m`, side.revet_before_m); put(`${prefix}_revet_after_m`, side.revet_after_m); @@ -218,7 +226,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc end_m: pipe.end_m, source: pipe.source, // 폼 기본값 = 조정창이 쓰던 값(횡단 스펙). 저장된 옵션이 있으면 그쪽이 이긴다. - options: withSpecDefaults(pipe.options, detail, pipe.chainage_m), + options: withSpecDefaults(pipe.options, detail, pipe.chainage_m, deps.wallHeight), design_flow_m3s: null, })); section.setPipeFacilities(pipeFacilities); @@ -236,7 +244,12 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc (entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M, ); if (!hit) return; - hit.options = withSpecDefaults(hit.options, deps.detail?.() ?? null, hit.chainage_m); + hit.options = withSpecDefaults( + hit.options, + deps.detail?.() ?? null, + hit.chainage_m, + deps.wallHeight, + ); section.setPipeFacilities(pipeFacilities); } @@ -249,6 +262,16 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc setInletStructure: (value) => section.facility.setInletStructure(value), onInletStructureChange: (handler) => section.facility.onInletStructureChange(handler), }, + overrideOptions: (chainageM, patch) => { + const hit = pipeFacilities.find( + (entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M, + ); + if (!hit) return; + hit.options = { ...(hit.options ?? {}), ...patch }; + section.setPipeFacilities(pipeFacilities); + // 폼을 그 값으로 다시 세운다 — 조정창이 한계를 알려 주던 것과 같은 자리다. + section.selectPipeByChainage(hit.chainage_m); + }, showPipeAt: (chainageM) => { currentChainageM = chainageM; if (chainageM !== null) refreshSpecDefaults(chainageM);