feat(B05): 유토곡선 총괄값을 종단 상단줄로 — 곡선을 접어도 남게
2026-09-06 사용자 지시. 요약 막대가 유토곡선 오버레이 안에 있어 패널을 접으면 같이 사라졌는데, 계획선을 만지며 봐야 하는 값이라 상단줄로 올림. - 상단줄 「최대 기울기」 오른쪽에 「누가토량」 추가. 값은 유토곡선 계산 결과의 마지막 누계이며 토량분배 켜짐과 무관하다. 음수(부족)면 경고색. - 세부(절토 자연·다짐, 성토, 잉여/부족)는 그 항목 툴팁으로 내림 — 예전 요약 막대가 세부를 툴팁에 두던 방식 그대로. - 유토곡선 오버레이의 요약 막대 행 삭제. 비면 접히게 해 곡선이 그만큼 넓게 씀. - 총괄값은 패널이 접혀 있어도 계산해 넘김(draw 앞단에서 산출). - 상단줄 높이 24 → 28px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,12 @@
|
|||||||
|
|
||||||
import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment";
|
import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment";
|
||||||
import type { MinCoverViolation } from "./B05_Profile_UI_Profile_MinCover";
|
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 {
|
export interface BalanceBarParams {
|
||||||
/** 표시줄 컨테이너. 그릴 때마다 통째로 갈아 끼운다. */
|
/** 표시줄 컨테이너. 그릴 때마다 통째로 갈아 끼운다. */
|
||||||
@@ -36,6 +42,9 @@ export interface BalanceBarParams {
|
|||||||
minCoverViolations?: MinCoverViolation[];
|
minCoverViolations?: MinCoverViolation[];
|
||||||
/** 요약줄 **오른쪽 끝**에 붙이는 묶음(줌·Y레인지 조작구) — 2026-09-04. */
|
/** 요약줄 **오른쪽 끝**에 붙이는 묶음(줌·Y레인지 조작구) — 2026-09-04. */
|
||||||
trailing?: HTMLElement;
|
trailing?: HTMLElement;
|
||||||
|
/** 유토곡선 총괄값 — 「최대 기울기」 오른쪽에 적는다(2026-09-06 사용자 지시).
|
||||||
|
* 유토곡선 패널을 접어도 이 값은 남아야 계획선을 만지며 볼 수 있다. */
|
||||||
|
massHaul?: MassHaulSummaryValues | null;
|
||||||
/** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */
|
/** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */
|
||||||
onResetAll: () => void;
|
onResetAll: () => void;
|
||||||
}
|
}
|
||||||
@@ -77,6 +86,13 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
|||||||
// 절·성토 불균형은 2026-09-03 사용자 지시로 뺐다. 이 값은 **종단 기준**이었다 —
|
// 절·성토 불균형은 2026-09-03 사용자 지시로 뺐다. 이 값은 **종단 기준**이었다 —
|
||||||
// 계획선과 지반선 사이 세로 면적(㎡)의 절·성토 비이지 실제 토량이 아니다. 의미 있는
|
// 계획선과 지반선 사이 세로 면적(㎡)의 절·성토 비이지 실제 토량이 아니다. 의미 있는
|
||||||
// 균형은 횡단 단면적을 쌓아 부피로 내는 하단 유토곡선 요약(㎥)이고, 그쪽이 이미 있다.
|
// 균형은 횡단 단면적을 쌓아 부피로 내는 하단 유토곡선 요약(㎥)이고, 그쪽이 이미 있다.
|
||||||
|
// 유토곡선 총괄값 — 「최대 기울기」 **바로 오른쪽**(2026-09-06 사용자 지시). 곡선을 접어도
|
||||||
|
// 남는다. 세부 내역(절토·성토·잉여/부족)은 이 항목의 툴팁으로 내렸다 — 예전 요약 막대가
|
||||||
|
// 세부를 툴팁에 두던 방식 그대로다.
|
||||||
|
const mass = params.massHaul;
|
||||||
|
if (mass) {
|
||||||
|
entries.push(["누가토량", volume(mass.finalM3), mass.finalM3 < 0 ? "over" : undefined]);
|
||||||
|
}
|
||||||
// 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다.
|
// 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다.
|
||||||
if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded} 곳`, "over"]);
|
if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded} 곳`, "over"]);
|
||||||
// 횡단배수 최소고 표시는 2026-09-02 사용자 지시로 삭제했다. 편집 차단(강제)은
|
// 횡단배수 최소고 표시는 2026-09-02 사용자 지시로 삭제했다. 편집 차단(강제)은
|
||||||
@@ -91,6 +107,13 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
|||||||
item.append(caption, document.createTextNode(value));
|
item.append(caption, document.createTextNode(value));
|
||||||
// 상한 초과 구간의 내역은 별도 경고 칩 대신 이 항목의 툴팁으로 붙인다 —
|
// 상한 초과 구간의 내역은 별도 경고 칩 대신 이 항목의 툴팁으로 붙인다 —
|
||||||
// 같은 사실을 두 번 적지 않는다(2026-08-19 재편).
|
// 같은 사실을 두 번 적지 않는다(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) {
|
if (label === "최대 기울기" && violations.length) {
|
||||||
item.title = violations
|
item.title = violations
|
||||||
.map(
|
.map(
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ import { resetBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
|||||||
import {
|
import {
|
||||||
createMassHaulChart,
|
createMassHaulChart,
|
||||||
createMassHaulLegend,
|
createMassHaulLegend,
|
||||||
createMassHaulSummary,
|
|
||||||
createMassHaulWindowState,
|
createMassHaulWindowState,
|
||||||
MASS_HAUL_MIN_HEIGHT,
|
MASS_HAUL_MIN_HEIGHT,
|
||||||
scheduleMassHaulSettle,
|
scheduleMassHaulSettle,
|
||||||
@@ -180,7 +179,28 @@ function readVisible(): Set<string> {
|
|||||||
/**
|
/**
|
||||||
* @param onChanged 펼침·범례 토글·높이 조절로 다시 그려야 할 때 호출된다(패널 전체 redraw).
|
* @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 사용자
|
// 손잡이는 다른 패널과 **같은 양식**의 표준 삼각형 손잡이 하나만 쓴다(2026-08-04 사용자
|
||||||
// 지시 — 예전 풀폭 바 + "유토곡선" 캡션은 다른 패널들과 모양이 달랐다). 무엇의 손잡이인지는
|
// 지시 — 예전 풀폭 바 + "유토곡선" 캡션은 다른 패널들과 모양이 달랐다). 무엇의 손잡이인지는
|
||||||
// 툴팁으로 밝힌다.
|
// 툴팁으로 밝힌다.
|
||||||
@@ -190,7 +210,8 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
|
|||||||
handle.append(handleControl.root);
|
handle.append(handleControl.root);
|
||||||
handleControl.root.setAttribute("aria-label", "유토곡선 패널");
|
handleControl.root.setAttribute("aria-label", "유토곡선 패널");
|
||||||
|
|
||||||
// 오버레이 뼈대 — 위 경계 리사이저 + 요약 막대 + 가로 스크롤러 + 범례.
|
// 오버레이 뼈대 — 위 경계 리사이저 + 안내 문구 자리 + 가로 스크롤러 + 범례.
|
||||||
|
// (요약 막대는 2026-09-06 종단 상단줄로 올라갔다.)
|
||||||
const overlay = document.createElement("div");
|
const overlay = document.createElement("div");
|
||||||
overlay.className = "b05-profile__masshaul-overlay";
|
overlay.className = "b05-profile__masshaul-overlay";
|
||||||
const bar = document.createElement("div");
|
const bar = document.createElement("div");
|
||||||
@@ -316,11 +337,39 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
|
|||||||
/** 마지막으로 그린 입력 — 이동이 아직 안 끝났으면 다음 프레임에 이걸로 한 번 더 그린다. */
|
/** 마지막으로 그린 입력 — 이동이 아직 안 끝났으면 다음 프레임에 이걸로 한 번 더 그린다. */
|
||||||
let lastParams: RouteMassHaulDrawParams | null = null;
|
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 {
|
function draw(params: RouteMassHaulDrawParams): void {
|
||||||
lastParams = params;
|
lastParams = params;
|
||||||
clearSelection = params.onClearSelection;
|
clearSelection = params.onClearSelection;
|
||||||
overlay.hidden = !open;
|
overlay.hidden = !open;
|
||||||
syncHandlePosition();
|
syncHandlePosition();
|
||||||
|
// 곡선을 그리기 전에 총괄값부터 낸다 — 접혀 있어도 상단줄에는 값이 서야 한다.
|
||||||
|
reportSummary(params);
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
if (!context) {
|
if (!context) {
|
||||||
note("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다.");
|
note("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다.");
|
||||||
@@ -361,8 +410,8 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
|
|||||||
banded && visible.has(MASS_HAUL_BALANCE_KEY)
|
banded && visible.has(MASS_HAUL_BALANCE_KEY)
|
||||||
? computeHaulPlan(banded.result, context.haulLimits)
|
? computeHaulPlan(banded.result, context.haulLimits)
|
||||||
: null;
|
: null;
|
||||||
const summarySeries = banded ?? series[0];
|
// 요약 막대는 2026-09-06 사용자 지시로 뺐다 — 총괄값은 종단 상단줄이 늘 보여 주고,
|
||||||
bar.append(createMassHaulSummary(summarySeries, haulPlan));
|
// 이 자리는 곡선이 넓게 쓴다. `bar` 는 안내 문구 자리로만 남는다.
|
||||||
|
|
||||||
// 곡선 몫 = 스크롤러의 **안쪽 높이**(clientHeight = 가로 스크롤바 제외). 오버레이 전체
|
// 곡선 몫 = 스크롤러의 **안쪽 높이**(clientHeight = 가로 스크롤바 제외). 오버레이 전체
|
||||||
// 높이로 잡으면 스크롤바가 곡선 바닥(Y축 -200 라벨)을 덮는다(2026-08-04 사용자 보고).
|
// 높이로 잡으면 스크롤바가 곡선 바닥(Y축 -200 라벨)을 덮는다(2026-08-04 사용자 보고).
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { structureGroupMenuItems } from "./B05_Profile_UI_Profile_Structures";
|
|||||||
import { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
import { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
||||||
import {
|
import {
|
||||||
createRouteMassHaulPanel,
|
createRouteMassHaulPanel,
|
||||||
|
type MassHaulSummaryValues,
|
||||||
MASSHAUL_MIN_HEIGHT,
|
MASSHAUL_MIN_HEIGHT,
|
||||||
type RouteMassHaulContext,
|
type RouteMassHaulContext,
|
||||||
} from "./B05_Profile_UI_Profile_MassHaul";
|
} from "./B05_Profile_UI_Profile_MassHaul";
|
||||||
@@ -144,7 +145,14 @@ export function createRouteProfilePanel(
|
|||||||
if (subPanelDragging) scheduleLightSync();
|
if (subPanelDragging) scheduleLightSync();
|
||||||
else draw();
|
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);
|
bodyWrap.append(massHaul.overlay, massHaul.handle);
|
||||||
// 오버레이의 가로 스크롤을 종단 스크롤러와 양방향 동기화 — 측점 세로선 정렬 유지.
|
// 오버레이의 가로 스크롤을 종단 스크롤러와 양방향 동기화 — 측점 세로선 정렬 유지.
|
||||||
massHaul.attachScrollSync(body);
|
massHaul.attachScrollSync(body);
|
||||||
@@ -329,6 +337,7 @@ export function createRouteProfilePanel(
|
|||||||
tools: tools.render(),
|
tools: tools.render(),
|
||||||
trailing: profileZoom.bar,
|
trailing: profileZoom.bar,
|
||||||
alignment,
|
alignment,
|
||||||
|
massHaul: massHaulSummary,
|
||||||
legacyAlignment: !!detail && hasLegacyAlignment(detail.longitudinal),
|
legacyAlignment: !!detail && hasLegacyAlignment(detail.longitudinal),
|
||||||
edited: store.edited(),
|
edited: store.edited(),
|
||||||
hasIrregularStations: irregularStations.length > 0,
|
hasIrregularStations: irregularStations.length > 0,
|
||||||
|
|||||||
@@ -509,7 +509,9 @@
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: var(--spacing-16);
|
gap: var(--spacing-16);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 24px;
|
/* 유토곡선 총괄값이 이 줄로 올라와(2026-09-06) 항목이 늘었다 — 두 픽셀 높여 글자가
|
||||||
|
위아래로 눌리지 않게 한다. */
|
||||||
|
height: 28px;
|
||||||
padding: 0 var(--spacing-8);
|
padding: 0 var(--spacing-8);
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|||||||
@@ -70,6 +70,12 @@
|
|||||||
top: -3px;
|
top: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 요약 막대는 2026-09-06 종단 상단줄로 올라갔다 — 이 자리는 안내 문구가 있을 때만 쓴다.
|
||||||
|
비어 있으면 테두리 한 줄도 남기지 않고 접어 곡선이 그만큼 넓게 쓴다. */
|
||||||
|
.b05-profile__masshaul-bar:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.b05-profile__masshaul-bar {
|
.b05-profile__masshaul-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user