feat(B05): 3D 흑백 지형 토글·서브패널 손잡이 규칙·좌측 여백 확대
- 3D 뷰 토글에 '흑백 지형' 추가(측점 라벨 우측, 기본 무지개). 정점색 1회 바꿔치기(원본 userData 보관)라 로딩·회전 성능 영향 없음 - 유토곡선 접힘 버튼은 항상 바닥 — 순서(종단>테이블>유토곡선)상 테이블 아래에서 나오므로. 테이블 접힘 버튼만 열린 유토곡선 위 경계에 나란히 - LONG_PAD.left 62->78: 좌측 패널 접기 버튼이 Y축 라벨·행제목을 가리는 문제. sticky 마스크·행제목 폭이 같은 값에서 파생돼 가로 스크롤 값 누출 없음 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -310,6 +310,7 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
|
||||
onAxesVisible: viewer.setAxesVisible,
|
||||
onStationLinesVisible: viewer.setStationLinesVisible,
|
||||
onStationLabelsVisible: viewer.setStationLabelsVisible,
|
||||
onSurfaceGrayscale: viewer.setSurfaceGrayscale,
|
||||
onView: viewer.setView,
|
||||
onResetView: () => viewer.setView("top"),
|
||||
onMovePoint: viewer.beginMoveSelected,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -44,6 +44,8 @@ export interface RouteViewer {
|
||||
) => Promise<void>;
|
||||
reloadContours: (interval: number) => Promise<void>;
|
||||
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;
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user