diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts index 8d7214b8..ce7251c2 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts @@ -310,6 +310,7 @@ export async function renderB05Route(root: HTMLElement): Promise { onAxesVisible: viewer.setAxesVisible, onStationLinesVisible: viewer.setStationLinesVisible, onStationLabelsVisible: viewer.setStationLabelsVisible, + onSurfaceGrayscale: viewer.setSurfaceGrayscale, onView: viewer.setView, onResetView: () => viewer.setView("top"), onMovePoint: viewer.beginMoveSelected, diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts index 27e514d2..e4cf789d 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Panel.ts @@ -54,6 +54,8 @@ interface PanelCallbacks { onStationLinesVisible: (visible: boolean) => void; /** 측점 번호·이름 라벨 표시 토글(기본 켜짐 — BP·EP·5측점 배수·구조물). */ onStationLabelsVisible: (visible: boolean) => void; + /** 지표면 흑백 표시 토글(기본 꺼짐 — 무지개 고도색). */ + onSurfaceGrayscale: (grayscale: boolean) => void; onView: (view: "iso" | "top" | "front" | "side") => void; onResetView: () => void; onMovePoint: () => void; @@ -162,6 +164,8 @@ export function createRoutePanel(callbacks: PanelCallbacks) { toggleButton("축 표시", false, callbacks.onAxesVisible), toggleButton(L("B05_Route_Field_StationLines"), true, callbacks.onStationLinesVisible), toggleButton(L("B05_Route_Field_StationLabels"), true, callbacks.onStationLabelsVisible), + // 무지개 고도색이 헷갈릴 때 명도만 남기는 흑백 표시(2026-08-05 사용자 요청). + toggleButton("흑백 지형", false, callbacks.onSurfaceGrayscale), ); const separator1 = document.createElement("span"); separator1.className = "b05-route__view-separator"; diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts index c1f95047..455b35fa 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_MassHaul.ts @@ -128,8 +128,6 @@ export interface RouteMassHaulPanel { /** 오버레이 서브패널 — 호출한 쪽이 `position: relative`인 패널 본문 래퍼에 붙인다. */ overlay: HTMLElement; isOpen(): boolean; - /** 접힌 손잡이가 앉을 바닥 높이(px) — 열린 테이블 오버레이 위 경계에 일렬로 놓기 위함. */ - setHandleBase(px: number): void; setContext(next: RouteMassHaulContext | null): void; /** 종단 가로 스크롤러와 scrollLeft를 양방향 동기화한다(설치 1회). */ attachScrollSync(main: HTMLElement): void; @@ -228,13 +226,11 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa /** * 손잡이를 오버레이 **위 경계**에 태운다(2026-08-04 사용자 지시 — 펼치면 같이 올라와 - * 테이블 영역을 침범해도 된다). 접혀 있으면 열린 서브패널 더미(테이블 오버레이)의 - * 위 경계(handleBase)로 올라간다 — 접힌 버튼들이 열린 패널 수평선에 일렬로 놓이도록 - * (2026-08-05 사용자 지시). 아무것도 안 열려 있으면 패널 바닥(0)이다. + * 테이블 영역을 침범해도 된다). 접혀 있으면 패널 바닥으로 돌아온다 — 유토곡선은 서브패널 + * 순서상 맨 아래라 테이블이 열려 있어도 버튼은 바닥에 남는다(2026-08-05 사용자 확정). */ - let handleBase = 0; function syncHandlePosition(): void { - handle.style.bottom = open ? `${overlay.offsetHeight}px` : `${handleBase}px`; + handle.style.bottom = open ? `${overlay.offsetHeight}px` : "0px"; } function applyOpen(next: boolean): void { @@ -371,11 +367,6 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa handle, overlay, isOpen: () => open, - setHandleBase(px) { - if (handleBase === px) return; - handleBase = px; - syncHandlePosition(); - }, setContext(next) { context = next; }, diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts index c0977526..aab5d9f4 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts @@ -492,8 +492,9 @@ export function createRouteProfilePanel( const massHeight = massHaul.overlay.hidden ? 0 : massHaul.overlay.offsetHeight; const tableOverlayHeight = tableOverlay.isOpen() ? tableOverlay.overlay.offsetHeight : 0; tableOverlay.setBottomOffset(massHeight); - // 접힌 유토곡선 손잡이는 열린 테이블의 위 경계에 일렬로 앉힌다(2026-08-05 사용자 지시). - massHaul.setHandleBase(tableOverlayHeight); + // 접힌 유토곡선 손잡이는 **하단(바닥)** 그대로 둔다 — 순서가 종단>테이블>유토곡선이라 + // 유토곡선은 테이블 아래에서 나오기 때문(2026-08-05 사용자 확정). 반대로 접힌 테이블 + // 손잡이는 열린 유토곡선 위 경계에 나란히 앉는다(TableOverlay.syncHandlePosition). // 종단 그래프 최소 높이는 반드시 확보한다 — 서브패널이 갑자기 펼쳐져 자리가 모자라면 // 종단이 뒤로 넘어가는 대신 **메인 하단 패널 높이를 키운다**(2026-08-05 사용자 지시). const deficit = diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Viewer.ts b/B05_wf2_Route/B05_wf2_Route_UI_Viewer.ts index f8ecc969..3e19c285 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Viewer.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Viewer.ts @@ -44,6 +44,8 @@ export interface RouteViewer { ) => Promise; reloadContours: (interval: number) => Promise; setSurfaceVisible: (visible: boolean) => void; + /** 지표면 흑백 표시 — 무지개 고도색이 헷갈릴 때 명도만 남긴다(정점색 1회 변환, 성능 영향 없음). */ + setSurfaceGrayscale: (grayscale: boolean) => void; setContoursVisible: (visible: boolean) => void; setAxesVisible: (visible: boolean) => void; setStationLinesVisible: (visible: boolean) => void; @@ -331,6 +333,30 @@ export function createRouteViewer(): RouteViewer { } animate(); + // 지표면 흑백 표시(2026-08-05 사용자 요청). 정점색 배열을 한 번 바꿔치기하는 것뿐이라 + // (원본은 userData에 보관, 재질·셰이더 교체 없음) 로딩·회전 속도에 영향이 없다. + let surfaceGrayscale = false; + function applySurfaceGrayscale(): void { + terrain?.traverse((child) => { + const geometry = (child as THREE.Mesh).geometry as THREE.BufferGeometry | undefined; + const color = geometry?.getAttribute("color") as THREE.BufferAttribute | undefined; + if (!geometry || !color) return; + const target = color.array as Float32Array; + if (!child.userData.originalColors) child.userData.originalColors = target.slice(); + const original = child.userData.originalColors as Float32Array; + const stride = color.itemSize; + if (surfaceGrayscale) { + for (let i = 0; i + 2 < original.length; i += stride) { + const gray = 0.299 * original[i] + 0.587 * original[i + 1] + 0.114 * original[i + 2]; + target[i] = target[i + 1] = target[i + 2] = gray; + } + } else { + target.set(original); + } + color.needsUpdate = true; + }); + } + return { root, markers, @@ -356,6 +382,8 @@ export function createRouteViewer(): RouteViewer { if (child instanceof THREE.Mesh) child.material.side = THREE.DoubleSide; }); scene.add(terrain); + // 흑백 토글이 켜진 채 모델을 다시 불러와도 상태를 유지한다. + applySurfaceGrayscale(); fit("top"); markers.renderMarkers(); await reloadContours(interval); @@ -365,6 +393,11 @@ export function createRouteViewer(): RouteViewer { setSurfaceVisible(visible) { if (terrain) terrain.visible = visible; }, + setSurfaceGrayscale(grayscale) { + if (surfaceGrayscale === grayscale) return; + surfaceGrayscale = grayscale; + applySurfaceGrayscale(); + }, setContoursVisible(visible) { contours.visible = visible; }, diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_Common.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_Common.ts index 5e016648..a8fd1a91 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_Common.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_Common.ts @@ -44,7 +44,10 @@ export const CROSS_GRID_GAP = 16; // bottom은 측점 라벨 한 줄 몫이다. X축 제목을 이름표로 합치며 남던 여백을 걷어내, // 종단면도 아래 라벨이 그 밑 유토곡선과의 틈 **가운데**에 오도록 좁혔다. // left/right는 유토곡선과 X축을 맞추는 값이라 함부로 바꾸면 두 그래프 측점선이 어긋난다. -export const LONG_PAD = { left: 62, right: 24, top: 12, bottom: 26 }; +// left 62→78: B05 좌측 패널 접기 버튼이 Y축 라벨·행제목을 가려 여유를 더 줬다 +// (2026-08-05 사용자 보고). sticky Y축 마스크·테이블 행제목 폭이 모두 이 값에서 +// 파생되므로 함께 넓어져 가로 스크롤 시 값 누출이 없다. +export const LONG_PAD = { left: 78, right: 24, top: 12, bottom: 26 }; export const CROSS_PAD = { left: 58, right: 20, top: 10, bottom: 52 }; export interface YScaleOptions {