diff --git a/B05_Profile/B05_Profile_UI_Profile_Balance.ts b/B05_Profile/B05_Profile_UI_Profile_Balance.ts index 3d5732bf..ac23412c 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Balance.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Balance.ts @@ -16,6 +16,12 @@ import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment"; import type { MinCoverViolation } from "./B05_Profile_UI_Profile_MinCover"; +import type { MassHaulSummaryValues } from "./B05_Profile_UI_Profile_MassHaul"; + +/** 누가토량 표기 — 천 단위 구분 + 소수점 1자리(유토곡선 요약과 같은 규칙). */ +function volume(value: number): string { + return `${value.toLocaleString("ko-KR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })}㎥`; +} export interface BalanceBarParams { /** 표시줄 컨테이너. 그릴 때마다 통째로 갈아 끼운다. */ @@ -36,6 +42,9 @@ export interface BalanceBarParams { minCoverViolations?: MinCoverViolation[]; /** 요약줄 **오른쪽 끝**에 붙이는 묶음(줌·Y레인지 조작구) — 2026-09-04. */ trailing?: HTMLElement; + /** 유토곡선 총괄값 — 「최대 기울기」 오른쪽에 적는다(2026-09-06 사용자 지시). + * 유토곡선 패널을 접어도 이 값은 남아야 계획선을 만지며 볼 수 있다. */ + massHaul?: MassHaulSummaryValues | null; /** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */ onResetAll: () => void; } @@ -77,6 +86,13 @@ export function renderBalanceBar(params: BalanceBarParams): void { // 절·성토 불균형은 2026-09-03 사용자 지시로 뺐다. 이 값은 **종단 기준**이었다 — // 계획선과 지반선 사이 세로 면적(㎡)의 절·성토 비이지 실제 토량이 아니다. 의미 있는 // 균형은 횡단 단면적을 쌓아 부피로 내는 하단 유토곡선 요약(㎥)이고, 그쪽이 이미 있다. + // 유토곡선 총괄값 — 「최대 기울기」 **바로 오른쪽**(2026-09-06 사용자 지시). 곡선을 접어도 + // 남는다. 세부 내역(절토·성토·잉여/부족)은 이 항목의 툴팁으로 내렸다 — 예전 요약 막대가 + // 세부를 툴팁에 두던 방식 그대로다. + const mass = params.massHaul; + if (mass) { + entries.push(["누가토량", volume(mass.finalM3), mass.finalM3 < 0 ? "over" : undefined]); + } // 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다. if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded} 곳`, "over"]); // 횡단배수 최소고 표시는 2026-09-02 사용자 지시로 삭제했다. 편집 차단(강제)은 @@ -91,6 +107,13 @@ export function renderBalanceBar(params: BalanceBarParams): void { item.append(caption, document.createTextNode(value)); // 상한 초과 구간의 내역은 별도 경고 칩 대신 이 항목의 툴팁으로 붙인다 — // 같은 사실을 두 번 적지 않는다(2026-08-19 재편). + if (label === "누가토량" && mass) { + item.title = [ + `절토(자연) ${volume(mass.cutNaturalM3)} · 절토(다짐) ${volume(mass.cutCompactedM3)}`, + `성토(다짐) ${volume(mass.fillCompactedM3)}`, + mass.shortageM3 > 0 ? `부족 ${volume(mass.shortageM3)}` : `잉여 ${volume(mass.surplusM3)}`, + ].join("\n"); + } if (label === "최대 기울기" && violations.length) { item.title = violations .map( diff --git a/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts b/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts index 88e36a1d..fcb2a6b6 100644 --- a/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts +++ b/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts @@ -44,7 +44,6 @@ import { resetBalloonOffsets } from "@util/common_util_mass_haul_balance_view"; import { createMassHaulChart, createMassHaulLegend, - createMassHaulSummary, createMassHaulWindowState, MASS_HAUL_MIN_HEIGHT, scheduleMassHaulSettle, @@ -180,7 +179,28 @@ function readVisible(): Set { /** * @param onChanged 펼침·범례 토글·높이 조절로 다시 그려야 할 때 호출된다(패널 전체 redraw). */ -export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPanel { +/** + * 종단 상단줄이 **접힌 상태에서도** 보여 주는 유토곡선 총괄값(2026-09-06 사용자 지시). + * 곡선 자체는 접으면 사라져도 이 값은 계획선을 만지며 계속 봐야 한다. + */ +export interface MassHaulSummaryValues { + /** 노선 끝 누가토량(㎥) — 양수 잉여, 음수 부족. */ + finalM3: number; + /** 절토(자연상태) 합계(㎥). */ + cutNaturalM3: number; + /** 절토를 다짐으로 환산한 값(㎥). */ + cutCompactedM3: number; + /** 성토(다짐) 합계(㎥). */ + fillCompactedM3: number; + surplusM3: number; + shortageM3: number; +} + +export function createRouteMassHaulPanel( + onChanged: () => void, + /** 총괄값이 새로 나올 때마다 부른다 — 종단 상단줄이 받아 적는다. */ + onSummary?: (summary: MassHaulSummaryValues | null) => void, +): RouteMassHaulPanel { // 손잡이는 다른 패널과 **같은 양식**의 표준 삼각형 손잡이 하나만 쓴다(2026-08-04 사용자 // 지시 — 예전 풀폭 바 + "유토곡선" 캡션은 다른 패널들과 모양이 달랐다). 무엇의 손잡이인지는 // 툴팁으로 밝힌다. @@ -190,7 +210,8 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa handle.append(handleControl.root); handleControl.root.setAttribute("aria-label", "유토곡선 패널"); - // 오버레이 뼈대 — 위 경계 리사이저 + 요약 막대 + 가로 스크롤러 + 범례. + // 오버레이 뼈대 — 위 경계 리사이저 + 안내 문구 자리 + 가로 스크롤러 + 범례. + // (요약 막대는 2026-09-06 종단 상단줄로 올라갔다.) const overlay = document.createElement("div"); overlay.className = "b05-profile__masshaul-overlay"; const bar = document.createElement("div"); @@ -316,11 +337,39 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa /** 마지막으로 그린 입력 — 이동이 아직 안 끝났으면 다음 프레임에 이걸로 한 번 더 그린다. */ let lastParams: RouteMassHaulDrawParams | null = null; + /** 총괄값만 따로 낸다 — 패널을 접어도 상단줄이 값을 잃지 않게(2026-09-06). */ + function reportSummary(params: RouteMassHaulDrawParams): void { + if (!onSummary) return; + if (!context || params.pendingRecalc) return; + const series = computeMassHaulSeries( + params.longitudinal, + params.crossSections, + context.conversion, + context.naturalSpoilMinSlope, + ); + if (!series.length) return onSummary(null); + const visible = readVisible(); + const picked = series.find((entry) => visible.has(entry.key)) ?? series[0]; + const result = picked.result; + const cut = result.cut_natural_m3; + const points = result.points; + onSummary({ + finalM3: points.length ? points[points.length - 1].cumulative_volume_m3 : 0, + cutNaturalM3: cut.soil + cut.ripping_rock + cut.blasting_rock, + cutCompactedM3: result.cut_compacted_m3, + fillCompactedM3: result.fill_compacted_m3, + surplusM3: result.surplus_m3, + shortageM3: result.shortage_m3, + }); + } + function draw(params: RouteMassHaulDrawParams): void { lastParams = params; clearSelection = params.onClearSelection; overlay.hidden = !open; syncHandlePosition(); + // 곡선을 그리기 전에 총괄값부터 낸다 — 접혀 있어도 상단줄에는 값이 서야 한다. + reportSummary(params); if (!open) return; if (!context) { note("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다."); @@ -361,8 +410,8 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa banded && visible.has(MASS_HAUL_BALANCE_KEY) ? computeHaulPlan(banded.result, context.haulLimits) : null; - const summarySeries = banded ?? series[0]; - bar.append(createMassHaulSummary(summarySeries, haulPlan)); + // 요약 막대는 2026-09-06 사용자 지시로 뺐다 — 총괄값은 종단 상단줄이 늘 보여 주고, + // 이 자리는 곡선이 넓게 쓴다. `bar` 는 안내 문구 자리로만 남는다. // 곡선 몫 = 스크롤러의 **안쪽 높이**(clientHeight = 가로 스크롤바 제외). 오버레이 전체 // 높이로 잡으면 스크롤바가 곡선 바닥(Y축 -200 라벨)을 덮는다(2026-08-04 사용자 보고). diff --git a/B05_Profile/B05_Profile_UI_Profile_Panel.ts b/B05_Profile/B05_Profile_UI_Profile_Panel.ts index 3ea62def..245738f7 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Panel.ts @@ -46,6 +46,7 @@ import { structureGroupMenuItems } from "./B05_Profile_UI_Profile_Structures"; import { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view"; import { createRouteMassHaulPanel, + type MassHaulSummaryValues, MASSHAUL_MIN_HEIGHT, type RouteMassHaulContext, } from "./B05_Profile_UI_Profile_MassHaul"; @@ -144,7 +145,14 @@ export function createRouteProfilePanel( if (subPanelDragging) scheduleLightSync(); else draw(); }; - const massHaul = createRouteMassHaulPanel(subPanelChanged); + /** 유토곡선 총괄값 — 상단줄이 쓴다. 패널을 접어도 값이 남게 여기에 들고 있는다. */ + let massHaulSummary: MassHaulSummaryValues | null = null; + const massHaul = createRouteMassHaulPanel(subPanelChanged, (summary) => { + const before = massHaulSummary; + massHaulSummary = summary; + // 값이 그대로면 상단줄을 다시 그리지 않는다 — 계획고를 연속으로 누를 때 헛일이 된다. + if (before?.finalM3 !== summary?.finalM3) renderBalance(); + }); bodyWrap.append(massHaul.overlay, massHaul.handle); // 오버레이의 가로 스크롤을 종단 스크롤러와 양방향 동기화 — 측점 세로선 정렬 유지. massHaul.attachScrollSync(body); @@ -329,6 +337,7 @@ export function createRouteProfilePanel( tools: tools.render(), trailing: profileZoom.bar, alignment, + massHaul: massHaulSummary, legacyAlignment: !!detail && hasLegacyAlignment(detail.longitudinal), edited: store.edited(), hasIrregularStations: irregularStations.length > 0, diff --git a/B05_Profile/B05_Profile_UI_Style.css b/B05_Profile/B05_Profile_UI_Style.css index a2f2b510..d0b76f53 100644 --- a/B05_Profile/B05_Profile_UI_Style.css +++ b/B05_Profile/B05_Profile_UI_Style.css @@ -509,7 +509,9 @@ flex-wrap: nowrap; gap: var(--spacing-16); align-items: center; - height: 24px; + /* 유토곡선 총괄값이 이 줄로 올라와(2026-09-06) 항목이 늘었다 — 두 픽셀 높여 글자가 + 위아래로 눌리지 않게 한다. */ + height: 28px; padding: 0 var(--spacing-8); overflow-x: auto; overflow-y: hidden; diff --git a/B05_Profile/B05_Profile_UI_Style_MassHaul.css b/B05_Profile/B05_Profile_UI_Style_MassHaul.css index 85c65516..5d9234cf 100644 --- a/B05_Profile/B05_Profile_UI_Style_MassHaul.css +++ b/B05_Profile/B05_Profile_UI_Style_MassHaul.css @@ -70,6 +70,12 @@ top: -3px; } +/* 요약 막대는 2026-09-06 종단 상단줄로 올라갔다 — 이 자리는 안내 문구가 있을 때만 쓴다. + 비어 있으면 테두리 한 줄도 남기지 않고 접어 곡선이 그만큼 넓게 쓴다. */ +.b05-profile__masshaul-bar:empty { + display: none; +} + .b05-profile__masshaul-bar { display: flex; flex: none;