feat(B05): 절성토 완전 분리(행 지반 트림) · 그랩 팬 · 측점 그룹 라이브 갱신
2026-08-23 사용자 지시 3건: - 절성토 완전 분리: 전환 구간을 상대 측점의 폭 0 축퇴점으로 모핑하던 방식 제거. 비탈을 측구와 같은 도로부 앵커 상대 좌표로 보간하고, 행마다 두 측점 지반선을 섞은 행 지반선과 직접 교차시켜 종결(trimSlopeToGround). 지반이 반대편인 행은 조각 드롭 - 절토가 지반으로 내려가거나 서피스끼리 침범하던 문제 해소. 클리핑 외곽도 행 실점유(비탈-측구-노견)로 일치. 700줄 제한: 측점 분류·트림 유틸을 Corridor_Station.ts로 분리(Build 494줄). - 그랩 팬: 가운데 드래그를 커서 아래 지형점이 1:1로 따라오는 팬으로 직접 구현(B04_PreProcess_UI_Camera). OrbitControls 기본 PAN은 카메라-target 거리 비례라 커서 피벗 회전·줌 후 극단적으로 느려졌다. B04 뷰어도 공용. - 측점 그룹 갱신: Profile_Panel이 라이브 alignment.samples 노출, Page의 측점 바 계획고·코리도 designSamples가 그걸 사용, 종단 편집 디바운스에서 renderStationLines 동반 호출. BUILD_VERSION 4-5로 저장본 만료. 검증: tsc 통과 · pytest 175 passed(신규 separation 8건) · 사용자 화면 확인. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -138,8 +138,11 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void
|
||||
// 회전·줌 모두 여기서 직접 처리한다(OrbitControls에는 휠 방향을 뒤집는 설정이 없다).
|
||||
controls.enableRotate = false;
|
||||
controls.enableZoom = false;
|
||||
// 가운데 버튼 드래그 = 화면 이동(전역 공통). 기본값(DOLLY)은 휠 줌과 겹쳐 쓸모가 없다.
|
||||
controls.mouseButtons.MIDDLE = THREE.MOUSE.PAN;
|
||||
// 가운데 버튼 드래그 = 그랩 팬(커서 아래 지형점이 커서를 1:1로 따라온다) — 아래에서
|
||||
// 직접 처리한다. OrbitControls 기본 PAN은 카메라↔target 거리에 비례하는데, 커서 피벗
|
||||
// 회전·줌이 target을 지형과 무관한 곳으로 옮겨 두면 실제 지형 거리보다 훨씬 느려진다
|
||||
// (2026-08-23 사용자 보고). 겹치지 않게 내장 팬은 끈다.
|
||||
controls.enablePan = false;
|
||||
|
||||
// 회전 중심 조준점 — 돌리는 동안에만 보인다. 항상 카메라를 향하는 스프라이트라
|
||||
// 어느 각도에서도 또렷하고, 화면상 크기는 거리와 무관하게 일정하다.
|
||||
@@ -176,6 +179,11 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void
|
||||
let pointerId: number | null = null;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
// 그랩 팬 상태 — 잡은 지형점(월드)과 그 점을 지나는 시선 수직 평면.
|
||||
let panPointerId: number | null = null;
|
||||
const panAnchor = new THREE.Vector3();
|
||||
const panPlane = new THREE.Plane();
|
||||
const panPoint = new THREE.Vector3();
|
||||
|
||||
/** 커서가 가리키는 지형 위 지점을 회전축으로 잡는다.
|
||||
*
|
||||
@@ -204,10 +212,20 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void
|
||||
}
|
||||
|
||||
function onPointerDown(event: PointerEvent): void {
|
||||
// 가운데 버튼은 브라우저 기본 동작(페이지 자동 스크롤)이 화면 이동과 겹치므로 막는다.
|
||||
// 가운데 버튼은 브라우저 기본 동작(페이지 자동 스크롤)이 팬과 겹치므로 막는다.
|
||||
if (event.button === 1) event.preventDefault();
|
||||
if (event.button !== 0 || pointerId !== null) return;
|
||||
if (options.blocked?.() || !controls.enabled) return;
|
||||
if (event.button === 1 && panPointerId === null) {
|
||||
// 그랩 팬 시작 — 커서 아래 지형점을 잡고, 시선 수직 평면 위에서 따라오게 한다.
|
||||
pickPivot(event);
|
||||
panAnchor.copy(pivot);
|
||||
camera.getWorldDirection(viewDirection);
|
||||
panPlane.setFromNormalAndCoplanarPoint(viewDirection, panAnchor);
|
||||
panPointerId = event.pointerId;
|
||||
element.setPointerCapture?.(event.pointerId);
|
||||
return;
|
||||
}
|
||||
if (event.button !== 0 || pointerId !== null) return;
|
||||
pickPivot(event);
|
||||
pointerId = event.pointerId;
|
||||
// 드래그가 캔버스 밖(브라우저 위·아래 끝)으로 나가도 회전이 끊기지 않게 포인터를 잡아 둔다.
|
||||
@@ -241,6 +259,22 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void
|
||||
}
|
||||
|
||||
function onPointerMove(event: PointerEvent): void {
|
||||
if (panPointerId === event.pointerId) {
|
||||
// 그랩 팬 — 잡은 점이 커서 아래에 계속 오도록 카메라·target을 평행 이동한다.
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) return;
|
||||
pointer.set(
|
||||
((event.clientX - rect.left) / rect.width) * 2 - 1,
|
||||
-((event.clientY - rect.top) / rect.height) * 2 + 1,
|
||||
);
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
if (!raycaster.ray.intersectPlane(panPlane, panPoint)) return;
|
||||
const delta = panAnchor.clone().sub(panPoint);
|
||||
camera.position.add(delta);
|
||||
controls.target.add(delta);
|
||||
controls.update();
|
||||
return;
|
||||
}
|
||||
if (pointerId !== event.pointerId) return;
|
||||
if (options.blocked?.()) {
|
||||
stop();
|
||||
@@ -311,6 +345,12 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void
|
||||
|
||||
function onPointerEnd(event: PointerEvent): void {
|
||||
if (pointerId === event.pointerId) stop();
|
||||
if (panPointerId === event.pointerId) {
|
||||
if (element.hasPointerCapture?.(event.pointerId)) {
|
||||
element.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
panPointerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 캔버스를 벗어나도 잡고 있는 동안에는 회전을 이어 간다 — 놓을 때만 끝낸다. */
|
||||
|
||||
Reference in New Issue
Block a user