fix(B04,B05): 탑뷰에서 살짝만 끌어도 화면이 뒤집히던 문제

기본 탑뷰는 시선이 up과 거의 나란하다(극각 1.9e-6 rad). 이때 회전축으로 쓰던
시선×up이 0에 수렴해 축 방향이 잡음으로 정해졌고, 살짝만 끌어도 엉뚱한 축으로 돌아
화면이 뒤집혔다. 앞 커밋의 극점 클램프도 각도를 여유각(0.02 rad) 경계까지 끌어올려,
작은 드래그가 여유각만큼 튀게 만들었다.

- 시선×up이 시선 길이 대비 무시할 만큼 짧으면 카메라 자신의 가로축으로 대체한다.
  두 축은 모든 각도에서 방향이 같아(내적 1.000000) 전환 순간에도 조작감이 안 바뀐다.
- 클램프 경계를 현재 극각과 여유각 중 극점에서 먼 쪽으로 잡는다. 이미 여유각 안쪽인
  탑뷰에서는 경계로 끌어올리지 않고, 극점에서 멀어지는 쪽만 열어 준다.

검증(탑뷰 재현): 0.005 요청 -> 0.005 적용, 0.05 -> 0.05, 극점 쪽 -0.005 -> 0(정지).
typecheck·prettier 통과, 정적 번들 재빌드.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-08 18:31:47 +09:00
co-authored by Claude Opus 5
parent c3f643b8b0
commit 7d50ded7b7
+12 -3
View File
@@ -251,8 +251,15 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void
// 수직 회전 — 시선의 오른쪽 축 기준. 마우스를 내리면 위에서 내려다보는 방향(2026-08-01
// 사용자 지시로 상하 반전). 극점을 넘으면 상하 성분만 버린다.
const eyeDirection = cameraOffset.clone().sub(targetOffset);
// 회전축은 화면의 가로축이다. 보통은 시선×up으로 얻지만, 탑뷰처럼 시선이 up과 거의
// 나란하면 이 외적이 0에 수렴해 축 방향이 잡음으로 정해진다 — 살짝만 끌어도 엉뚱한
// 축으로 돌아 화면이 뒤집혔다(2026-08-08 사용자 보고). 그럴 때는 카메라 자신의
// 가로축을 쓴다(시선×up과 같은 방향이라 조작감이 달라지지 않는다).
const right = eyeDirection.clone().cross(up);
if (right.lengthSq() > 1e-8) {
if (right.lengthSq() < 1e-6 * Math.max(eyeDirection.lengthSq(), 1e-12)) {
right.setFromMatrixColumn(camera.matrix, 0).negate();
}
if (right.lengthSq() > 1e-12) {
right.normalize();
// 극점을 넘길 회전은 **버리지 않고 극점 직전까지만 잘라서** 적용한다. 통째로 버리면
// 끄는 동안 화면이 멈춰 있다가 각도가 범위 안으로 돌아오는 순간 한꺼번에 움직여
@@ -262,10 +269,12 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void
// 실제로 재 본다 — 카메라가 극점 어느 쪽에 있느냐에 따라 뒤집힌다.
const probe = eyeDirection.clone().applyAxisAngle(right, 1e-3).angleTo(up);
const direction = probe >= polar ? 1 : -1;
// 기본 탑뷰는 극각이 이미 여유각 안쪽(거의 0)이다. 경계로 끌어올리면 살짝만 끌어도
// 여유각만큼 튄다 — 현재 각도를 경계로 삼아, 극점에서 **멀어지는 쪽만** 열어 준다.
const clamped = THREE.MathUtils.clamp(
polar + direction * pitch,
POLAR_EPSILON,
Math.PI - POLAR_EPSILON,
Math.min(polar, POLAR_EPSILON),
Math.max(polar, Math.PI - POLAR_EPSILON),
);
const applied = direction * (clamped - polar);
if (Math.abs(applied) > 1e-9) {