diff --git a/B05_Profile/B05_Profile_UI_Panel_View.ts b/B05_Profile/B05_Profile_UI_Panel_View.ts index 6b340ab0e..b09d2e0e0 100644 --- a/B05_Profile/B05_Profile_UI_Panel_View.ts +++ b/B05_Profile/B05_Profile_UI_Panel_View.ts @@ -33,19 +33,29 @@ export function createViewControls(callbacks: ViewControlCallbacks): { (["iso", "top", "front", "side"] as const).forEach((preset) => viewButtons.append(button(preset.toUpperCase(), () => callbacks.onView(preset), "glass")), ); - // 직교/원근 전환(2026-09-04 사용자 지시) — 기본은 원근이고, 단추 글자는 **바뀔 쪽**을 - // 가리킨다(누르면 그쪽으로 간다). - let projection: "perspective" | "ortho" = "perspective"; - const projectionButton = button( - "직교로", - () => { - projection = projection === "perspective" ? "ortho" : "perspective"; - projectionButton.textContent = projection === "perspective" ? "직교로" : "원근으로"; - callbacks.onProjection(projection); - }, - "glass", - ); - viewButtons.append(projectionButton); + // 원근·직교 두 단추 — 지금 켜진 쪽을 강조한다. 기본은 직교(카메라 기본과 같음). + const projectionButtons = ( + [ + ["perspective", "원근"], + ["ortho", "직교"], + ] as const + ).map(([kind, label]) => { + const element = button( + label, + () => { + projectionButtons.forEach(({ kind: other, element: el }) => { + el.classList.toggle("is-active", other === kind); + el.setAttribute("aria-pressed", String(other === kind)); + }); + callbacks.onProjection(kind); + }, + "glass", + ); + element.classList.toggle("is-active", kind === "ortho"); + element.setAttribute("aria-pressed", String(kind === "ortho")); + return { kind, element }; + }); + viewButtons.append(...projectionButtons.map(({ element }) => element)); const visibilityButtons = document.createElement("div"); visibilityButtons.className = "b05-route__view-group"; visibilityButtons.append( diff --git a/B05_Profile/B05_Profile_UI_Viewer.ts b/B05_Profile/B05_Profile_UI_Viewer.ts index 165456dad..1d0d2e415 100644 --- a/B05_Profile/B05_Profile_UI_Viewer.ts +++ b/B05_Profile/B05_Profile_UI_Viewer.ts @@ -289,8 +289,9 @@ export function createRouteViewer(): RouteViewer { iso: [distance, distance, distance], // 정확히 수직이면 lookAt이 화면 방향을 못 정해 첫 드래그에 화면이 뒤집힌다. top: [0, distance, distance * TOP_VIEW_TILT], - front: [0, distance * 0.25, distance], - side: [distance, distance * 0.25, 0], + // 정면 = 남에서 북을, 측면 = 동에서 서를 수평으로 본다(장면 +z 가 남, +x 가 동). + front: [0, 0, distance], + side: [distance, 0, 0], } as const; const [x, y, z] = positions[view]; const camera = cameraRig.camera(); @@ -309,7 +310,9 @@ export function createRouteViewer(): RouteViewer { if (!bounds) return; const width = bounds.x[1] - bounds.x[0]; const depth = bounds.y[1] - bounds.y[0]; - placeCamera(new THREE.Vector3(0, 0, 0), Math.max(width, depth, 20) * 1.35, view); + // 수평 보기는 지형 앞 가장자리가 카메라에 가까워 원근에서 잘린다 — 그 반폭만큼 물린다. + const nearHalf = view === "front" ? depth / 2 : view === "side" ? width / 2 : 0; + placeCamera(new THREE.Vector3(0, 0, 0), Math.max(width, depth, 20) * 1.35 + nearHalf, view); } async function reloadContours(interval: number): Promise { diff --git a/B05_Profile/B05_Profile_UI_Viewer_Camera.ts b/B05_Profile/B05_Profile_UI_Viewer_Camera.ts index 32f94f3fd..7dbe1cdd7 100644 --- a/B05_Profile/B05_Profile_UI_Viewer_Camera.ts +++ b/B05_Profile/B05_Profile_UI_Viewer_Camera.ts @@ -2,8 +2,7 @@ * B05_Profile_UI_Viewer_Camera.ts * B05 뷰어의 **원근/직교 두 카메라**와 그 사이 갈아 끼우기. * - * 기본은 원근(시야각 45°) — B04 지표면 화면과 같은 조작감이다(2026-09-04 사용자 확정). - * 탑뷰에서 크기를 정밀하게 대조할 때만 직교로 바꾼다. 갈아 끼울 때 위치·시선·근평면· + * 기본은 직교 — 원근(시야각 45°)은 단추로 고른다. 갈아 끼울 때 위치·시선·근평면· * 먼평면과 **보이는 크기**를 그대로 옮기므로 화면이 튀지 않는다. * * 카메라 객체가 바뀌므로 쓰는 쪽은 붙잡아 두지 말고 `camera()`로 그때그때 읽을 것. @@ -22,7 +21,7 @@ export function createCameraRig() { const perspective = new THREE.PerspectiveCamera(FOV, 1, 0.1, 100000); perspective.position.set(100, 120, 100); const ortho = new THREE.OrthographicCamera(-100, 100, 100, -100, 0.1, 100000); - let kind: ProjectionKind = "perspective"; + let kind: ProjectionKind = "ortho"; let aspect = 1; let halfHeight = 100;