diff --git a/B05_Profile/B05_Profile_UI_Profile_Balance.ts b/B05_Profile/B05_Profile_UI_Profile_Balance.ts index 5f7f7c68..f6166abe 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Balance.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Balance.ts @@ -4,9 +4,9 @@ * * 표시 항목(별표2 근거): * 최대 기울기 x.x% (상한 y%) — Ⅰ.2.라: 설계속도·지형별 상한 준수 판정 - * 불균형 z% / 허용 w% — Ⅰ.1.나.(4)(다): 시공계획고 절·성토 균형 * 곡선 필요 n곳 — Ⅰ.2.마: 대수차 5% 초과 변화점의 종단곡선 삽입 - * 절·성토 면적은 하단 유토곡선이 부피로 더 정확히 보여 주고, 변화점·종단곡선 개수와 + * 절·성토 균형은 **하단 유토곡선**이 횡단 기준 부피(㎥)로 보여 준다 — 여기 있던 종단 기준 + * 불균형(㎡ 비율)은 실제 토량이 아니라 의미가 없어 뺐다(2026-09-03). 변화점·종단곡선 개수와 * 기본 곡선길이 L은 판단 기준이 없어 뺐다(2026-08-19 사용자 지적 + 지식DB 대조). * [저장선 복원]은 상단에서 내려 그래프 조작부로 옮겼다. * @@ -53,7 +53,7 @@ export function renderBalanceBar(params: BalanceBarParams): void { return; } const { alignment } = params; - const { balance, policy, violations } = alignment; + const { policy, violations } = alignment; // 최대 종단기울기 — 법정 상한 대비가 이 화면의 첫 판정 항목이다(별표2 Ⅰ.2.라). const steepest = alignment.segments.reduce( (worst, segment) => Math.max(worst, Math.abs(segment.grade_percent)), @@ -70,12 +70,10 @@ export function renderBalanceBar(params: BalanceBarParams): void { `${steepest.toFixed(1)} % / 상한 ${policy.max_grade_pct.toFixed(1)} %`, violations.length ? "over" : undefined, ], - [ - "절·성토 불균형", - `${balance.imbalance_percent.toFixed(1)} % / 허용 ${balance.tolerance_percent.toFixed(0)} %`, - balance.within_tolerance ? undefined : "over", - ], ]; + // 절·성토 불균형은 2026-09-03 사용자 지시로 뺐다. 이 값은 **종단 기준**이었다 — + // 계획선과 지반선 사이 세로 면적(㎡)의 절·성토 비이지 실제 토량이 아니다. 의미 있는 + // 균형은 횡단 단면적을 쌓아 부피로 내는 하단 유토곡선 요약(㎥)이고, 그쪽이 이미 있다. // 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다. if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded} 곳`, "over"]); // 횡단배수 최소고 표시는 2026-09-02 사용자 지시로 삭제했다. 편집 차단(강제)은 diff --git a/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts b/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts index 840737d7..42c0cdfc 100644 --- a/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts +++ b/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts @@ -293,7 +293,11 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa mirror(scroll, main); } + /** 곡선을 한 번이라도 그렸는지 — 재계산 대기 중 화면을 비우지 않기 위한 표시. */ + let drawnOnce = false; + function note(message: string): void { + drawnOnce = false; bar.replaceChildren(); scroll.replaceChildren(); legendLayer.replaceChildren(); @@ -312,11 +316,25 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa note("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다."); return; } - // 저장된 횡단이 지금 계획선과 어긋나 있으면 그 면적은 **옛 계획고로 만든 값**이다. - // 그걸 잠깐 그렸다가 재계산 결과로 갈아 끼우면 사용자가 옛 그림을 본다(2026-09-03 - // 사용자 확정: 「새 값만 보여주기」). Panel이 이미 재계산을 예약해 두므로 기다린다. + // 저장된 횡단이 지금 계획선과 어긋나 있으면 그 면적은 옛 계획고로 만든 값이다 — + // 새 값이 올 때까지 기다린다(2026-09-03 사용자 확정: 「새 값만 보여주기」). + // + // 다만 **곡선을 지우지는 않는다**. 계획고를 연속으로 누르면(초당 10회) 매번 곡선이 + // 사라졌다 다시 서서 화면이 깜빡였다(2026-09-03 사용자 보고). 재계산은 브라우저 + // 안에서 끝나 수십 ms면 끝나므로, 그동안 **직전 곡선을 그대로 두고** 상태줄로만 + // 알린다. 아직 한 번도 못 그렸을 때만 문구로 자리를 채운다. if (params.pendingRecalc) { - note("횡단을 지금 계획선에 맞춰 다시 계산하는 중입니다."); + if (drawnOnce) { + const pending = bar.querySelector(".b05-profile__masshaul-pending"); + if (!pending) { + const chip = document.createElement("span"); + chip.className = "b05-profile__masshaul-pending"; + chip.textContent = "다시 계산 중…"; + bar.append(chip); + } + } else { + note("횡단을 지금 계획선에 맞춰 다시 계산하는 중입니다."); + } return; } // B06과 같은 계산 — 횡단 기준(정식)과 종단 기준(개략 비교)을 함께 낸다. @@ -386,6 +404,7 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa onChanged(); }), ); + drawnOnce = true; } return { diff --git a/B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts b/B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts index a22a7aaa..06ce78de 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts @@ -19,6 +19,7 @@ import { createProfileHistory, type ProfileHistory } from "./B05_Profile_UI_Prof import { createProfileTools, type ProfileTools } from "./B05_Profile_UI_Profile_Tools"; import { detectStraightRun, + hasStraightRuns, shiftStraightRuns, straightenBetween, tiltStraightRun, @@ -118,6 +119,10 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools { onRedo: () => inner.redo(), canUndo: () => inner.canUndo(), canRedo: () => inner.canRedo(), + hasStraightRuns: () => { + const alignment = ctx.alignment(); + return alignment ? hasStraightRuns(alignment) : false; + }, onChanged: ctx.refresh, }); diff --git a/B05_Profile/B05_Profile_UI_Profile_Straighten.ts b/B05_Profile/B05_Profile_UI_Profile_Straighten.ts index 1bcc587e..5b9279de 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Straighten.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Straighten.ts @@ -114,6 +114,22 @@ export function detectStraightRun( }; } +/** + * 지금 계획선에 **직선화된 구간이 하나라도 있는가** — [쉬프트] 안내 문구용. + * + * 초기 계획선은 모든 측점이 변화점이라 마디마다 기울기가 달라 직선 구간이 없다. 그때 + * [쉬프트]로 아무 데나 눌러도 잡히는 것이 없어 「선택이 안 된다」로 보였다 + * (2026-09-03 사용자 보고). 안내를 나누려면 이 판정이 필요하다. + */ +export function hasStraightRuns(alignment: ProfileAlignment): boolean { + for (let index = 1; index < alignment.segments.length; index += 1) { + if (Math.abs(gradeAt(alignment, index) - gradeAt(alignment, index - 1)) < COLLINEAR_EPSILON) { + return true; + } + } + return false; +} + /** 두 측점 사이(끝 제외)의 변화점 chainage. */ function interiorNodes(alignment: ProfileAlignment, fromM: number, toM: number): number[] { return pviChainages(alignment).filter( diff --git a/B05_Profile/B05_Profile_UI_Profile_Tools.ts b/B05_Profile/B05_Profile_UI_Profile_Tools.ts index e42c66a6..758e2e55 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Tools.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Tools.ts @@ -28,6 +28,8 @@ export interface ProfileToolsCallbacks { onRedo: () => void; canUndo: () => boolean; canRedo: () => boolean; + /** 지금 계획선에 직선화된 구간이 있는가 — [쉬프트] 안내 문구를 가른다. */ + hasStraightRuns: () => boolean; /** 선택 표시를 갱신해야 할 때(모드·선택 변화) 호출된다. */ onChanged: () => void; } @@ -166,12 +168,16 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo if (mode === "straighten" && pending !== null) { const hint = document.createElement("em"); hint.className = "b05-route-profile__tool-hint"; - hint.textContent = `${pending.toFixed(1)}m 선택 — 두 번째 측점을 고르세요`; + hint.textContent = `${pending.toFixed(1)}m 선택 — 두 번째 측점을 고르세요 (취소는 [직선화] 다시 누르기)`; wrap.append(hint); } else if (mode === "shift" && !runs.length) { const hint = document.createElement("em"); hint.className = "b05-route-profile__tool-hint"; - hint.textContent = "직선화된 라인을 고르세요"; + // 고를 것이 아예 없는 경우와 있는데 안 고른 경우를 갈라 적는다 — 초기 계획선은 + // 마디마다 기울기가 달라 직선 구간이 없어, 눌러도 안 잡히는 것이 정상이다. + hint.textContent = callbacks.hasStraightRuns() + ? "직선화된 라인을 고르세요 (여러 개 가능) — 끝내려면 [쉬프트]를 다시 누르세요" + : "직선화된 구간이 없습니다 — [직선화]로 먼저 만드세요"; wrap.append(hint); } return wrap; diff --git a/B05_Profile/B05_Profile_UI_Style_MassHaul.css b/B05_Profile/B05_Profile_UI_Style_MassHaul.css index 5ee9930a..c482c4ca 100644 --- a/B05_Profile/B05_Profile_UI_Style_MassHaul.css +++ b/B05_Profile/B05_Profile_UI_Style_MassHaul.css @@ -112,6 +112,14 @@ padding-top: 0; } +/* 재계산 대기 표시 — 곡선을 지우지 않고 요약줄 끝에만 붙인다(2026-09-03 깜빡임 제거). */ +.b05-profile__masshaul-pending { + margin-left: auto; + color: var(--color-text-secondary); + font-size: var(--text-caption); + opacity: 0.8; +} + .b05-profile__masshaul-empty { padding: var(--spacing-8) var(--spacing-16); color: var(--color-text-secondary);