diff --git a/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts b/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts index f35dd73f..3ac50b85 100644 --- a/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts +++ b/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts @@ -41,6 +41,10 @@ import { normalizeVisibleBasis, } from "@util/common_util_mass_haul"; import { computeHaulPlan } from "@util/common_util_mass_haul_balance"; +import { + badgeValuesFrom, + type MassHaulBadgeValues, +} from "@util/common_util_mass_haul_badge"; import { resetBalloonOffsets } from "@util/common_util_mass_haul_balance_view"; import { createMassHaulChart, @@ -185,18 +189,7 @@ function readVisible(): Set { * 종단 상단줄이 **접힌 상태에서도** 보여 주는 유토곡선 총괄값(2026-09-06 사용자 지시). * 곡선 자체는 접으면 사라져도 이 값은 계획선을 만지며 계속 봐야 한다. */ -export interface MassHaulSummaryValues { - /** 노선 끝 누가토량(㎥) — 양수 잉여, 음수 부족. */ - finalM3: number; - /** 절토(자연상태) 합계(㎥). */ - cutNaturalM3: number; - /** 절토를 다짐으로 환산한 값(㎥). */ - cutCompactedM3: number; - /** 성토(다짐) 합계(㎥). */ - fillCompactedM3: number; - surplusM3: number; - shortageM3: number; -} +export type MassHaulSummaryValues = MassHaulBadgeValues; export function createRouteMassHaulPanel( onChanged: () => void, @@ -339,31 +332,31 @@ export function createRouteMassHaulPanel( /** 마지막으로 그린 입력 — 이동이 아직 안 끝났으면 다음 프레임에 이걸로 한 번 더 그린다. */ let lastParams: RouteMassHaulDrawParams | null = null; - /** 총괄값만 따로 낸다 — 패널을 접어도 상단줄이 값을 잃지 않게(2026-09-06). */ - function reportSummary(params: RouteMassHaulDrawParams): void { + /** 이미 낸 계열에서 총괄값만 꺼낸다 — 계산은 하지 않는다. */ + function emitSummary(series: MassHaulSeries[]): 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); // 총괄값 기준은 **횡단 고정**이다(2026-09-06 사용자: 종단 기준 곡선은 뒤에 없앨 예정). // 범례에서 무엇을 켜 두었든 배지·상단줄이 흔들리지 않는다. const picked = series.find((entry) => entry.basis === "cross") ?? 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, - }); + onSummary(badgeValuesFrom(picked.result)); + } + + /** 총괄값만 따로 낸다 — 패널을 접어도 상단줄이 값을 잃지 않게(2026-09-06). + * **횡단 기준만** 적분한다 — 접힌 상태에서 종단 기준까지 내던 것은 버려질 값이었다 + * (2026-09-06 조작 실측: 계획고 ▲ → 라벨 갱신 중앙 130ms 중 낭비분). */ + function reportSummary(params: RouteMassHaulDrawParams): void { + if (!onSummary) return; + if (!context || params.pendingRecalc) return; + emitSummary( + computeMassHaulSeries( + params.longitudinal, + params.crossSections, + context.conversion, + context.naturalSpoilMinSlope, + ["cross"], + ), + ); } function draw(params: RouteMassHaulDrawParams): void { @@ -371,9 +364,12 @@ export function createRouteMassHaulPanel( clearSelection = params.onClearSelection; overlay.hidden = !open; syncHandlePosition(); - // 곡선을 그리기 전에 총괄값부터 낸다 — 접혀 있어도 상단줄에는 값이 서야 한다. - reportSummary(params); - if (!open) return; + // 접혀 있어도 상단줄에는 값이 서야 한다 — 이때는 **횡단 기준만** 적분한다. + // 펼친 경우에는 아래에서 두 기준을 내므로 그 결과로 총괄값을 낸다(계산 한 번). + if (!open) { + reportSummary(params); + return; + } if (!context) { note("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다."); return; @@ -400,6 +396,8 @@ export function createRouteMassHaulPanel( context.conversion, context.naturalSpoilMinSlope, ); + // 총괄값은 방금 낸 계열에서 꺼낸다 — 접힘 경로처럼 따로 적분하지 않는다. + emitSummary(series); if (!series.length) { note("횡단 설계가 아직 없어 유토곡선을 그릴 수 없습니다."); return; diff --git a/B05_Profile/B05_Profile_UI_Profile_Panel.ts b/B05_Profile/B05_Profile_UI_Profile_Panel.ts index 3116f244..5da1bc5c 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Panel.ts @@ -85,8 +85,10 @@ const MAX_PANEL_HEIGHT_RATIO = 0.9; /** 계획고 편집 후 횡단을 다시 계산하기까지 기다리는 시간(ms). * ▲/▼ 길게 누르기(초당 10회)·끌기에서 매 프레임 다시 돌지 않게 마지막 값만 계산한다. * 계산이 브라우저 안에서 끝나면서(2026-09-03) 서버 왕복이 사라져 250→60ms 로 줄였다 — - * 실측 전 측점(128) 재계산 7.5ms 라 손을 떼는 즉시 유토곡선이 따라온다. */ -const CROSS_PREVIEW_DEBOUNCE_MS = 60; + * 실측 전 측점(128) 재계산 7.5ms 라 손을 떼는 즉시 유토곡선이 따라온다. + * 60→16ms(2026-09-06) — ▲ 한 번에 라벨이 서기까지 중앙 130ms 를 쟀는데 그중 60ms 가 + * 이 대기였다. 한 프레임(16ms)이면 연속 입력은 여전히 마지막 값만 계산한다. */ +const CROSS_PREVIEW_DEBOUNCE_MS = 16; /** 구조물 측점을 종단 정본 측점으로 맞춰 주는 허용 오차(m). 층마다 반올림이 달라 생기는 * mm~cm 어긋남만 흡수할 크기다 — 이보다 멀면 서로 다른 측점이다. */ const STATION_SNAP_TOLERANCE_M = 0.1; diff --git a/common_util/common_util_mass_haul.ts b/common_util/common_util_mass_haul.ts index 10272729..ad27a0d5 100644 --- a/common_util/common_util_mass_haul.ts +++ b/common_util/common_util_mass_haul.ts @@ -445,15 +445,18 @@ export function computeMassHaulSeries( crossSections: MassHaulSection[], conversion: EarthworkConversion, naturalSpoilMinSlope?: number, + /** 낼 기준. 총괄값(배지)만 필요하면 `["cross"]` 로 불러 종단 기준 적분을 건너뛴다. */ + bases: readonly MassHaulBasis[] = SERIES_BASES, ): MassHaulSeries[] { const sections = designedSections(crossSections); - const sampleSets: Record = { - cross: crossAreaSamples(sections, naturalSpoilMinSlope), - longitudinal: longitudinalAreaSamples(longitudinal, sections), - }; + const samplesFor = (basis: MassHaulBasis): AreaSample[] => + basis === "cross" + ? crossAreaSamples(sections, naturalSpoilMinSlope) + : longitudinalAreaSamples(longitudinal, sections); const series: MassHaulSeries[] = []; for (const basis of SERIES_BASES) { - const result = integrate(sampleSets[basis], conversion); + if (!bases.includes(basis)) continue; + const result = integrate(samplesFor(basis), conversion); if (result) series.push({ key: basis, basis, result }); } return series;