Z-up orbit default and three-finger touch pan

camera.up defaulted to three.js's Y-up while the actual data (ROS/FAST-
LIVO2) and WALK mode are Z-up. Orbiting with the pole misaligned from the
cloud's real vertical made yaw look like a diagonal tumble instead of a
clean spin around what's visually "up" in the rendered cloud. Set Z-up at
camera creation and on WALK exit (previously reverted to Y-up).

OrbitControls only tracks 1-2 simultaneous touches — a 3rd pointer makes
it go idle rather than doing anything. Added a three-finger pan on top,
replicating OrbitControls' own (private, unexported) screen-space pan math
so it feels identical to the existing right-drag/two-finger-pan.
This commit is contained in:
Dongubak
2026-08-23 15:40:21 +09:00
parent 0acddf3f5e
commit daa2257865
+54 -3
View File
@@ -541,6 +541,10 @@ scene.background = new THREE.Color(THEME_BG[currentTheme]);
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 10000);
camera.position.set(0, 0, 10);
// Z-up everywhere, matching the actual data (ROS/FAST-LIVO2) and WALK mode —
// orbiting with camera.up misaligned from the cloud's real vertical makes yaw
// look like a diagonal tumble instead of a clean spin. See also exitFP().
camera.up.set(0, 0, 1);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
@@ -553,10 +557,57 @@ controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.zoomToCursor = true; // zoom toward the cursor, not the orbit target — matches CAD/CloudCompare
// two-finger touch: pinch still zooms, but the drag also free-rotates (any direction the
// fingers move, not locked to one axis) instead of panning — trades away touch-panning,
// which double-click-to-recenter (below) mostly covers instead.
// fingers move, not locked to one axis) — OrbitControls only handles 1-2 simultaneous
// touches (a 3rd pointer makes it go idle), so three-finger pan below covers panning.
controls.touches.TWO = THREE.TOUCH.DOLLY_ROTATE;
// ─── three-finger pan — replicates OrbitControls' own (private, unexported) screen-
// space pan math so it feels identical to right-drag/two-finger-pan elsewhere ──
const panTouches = new Map(); // pointerId -> {x,y}
function touchCentroid(map) {
let x = 0, y = 0;
for (const p of map.values()) { x += p.x; y += p.y; }
return { x: x / map.size, y: y / map.size };
}
function panByScreenDelta(deltaX, deltaY) {
const offset = new THREE.Vector3().copy(camera.position).sub(controls.target);
const targetDistance = offset.length() * Math.tan((camera.fov / 2) * Math.PI / 180);
const panLeftDist = 2 * deltaX * targetDistance / renderer.domElement.clientHeight * controls.panSpeed;
const panUpDist = 2 * deltaY * targetDistance / renderer.domElement.clientHeight * controls.panSpeed;
const panOffset = new THREE.Vector3().setFromMatrixColumn(camera.matrix, 0).multiplyScalar(-panLeftDist);
const yCol = new THREE.Vector3();
if (controls.screenSpacePanning) yCol.setFromMatrixColumn(camera.matrix, 1);
else { yCol.setFromMatrixColumn(camera.matrix, 0); yCol.crossVectors(camera.up, yCol); }
panOffset.add(yCol.multiplyScalar(panUpDist));
camera.position.add(panOffset);
controls.target.add(panOffset);
controls.update();
invalidate();
}
let panCentroid = null;
renderer.domElement.addEventListener('pointerdown', e => {
if (e.pointerType !== 'touch') return;
panTouches.set(e.pointerId, { x: e.clientX, y: e.clientY });
panCentroid = panTouches.size === 3 ? touchCentroid(panTouches) : null;
});
renderer.domElement.addEventListener('pointermove', e => {
if (e.pointerType !== 'touch' || !panTouches.has(e.pointerId)) return;
panTouches.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (panTouches.size !== 3) return;
const c = touchCentroid(panTouches);
if (panCentroid) panByScreenDelta(c.x - panCentroid.x, c.y - panCentroid.y);
panCentroid = c;
});
function releaseTouchPan(e) {
if (e.pointerType !== 'touch' || !panTouches.has(e.pointerId)) return;
panTouches.delete(e.pointerId);
panCentroid = panTouches.size === 3 ? touchCentroid(panTouches) : null;
}
renderer.domElement.addEventListener('pointerup', releaseTouchPan);
renderer.domElement.addEventListener('pointercancel', releaseTouchPan);
// ─── on-demand rendering (perf: idle frames cost nothing) ────────────────
let needsRender = true;
const invalidate = () => { needsRender = true; };
@@ -1023,7 +1074,7 @@ function exitFP() {
const d = new THREE.Vector3(); camera.getWorldDirection(d);
const r = points?.geometry.boundingSphere?.radius || 5;
controls.target.copy(camera.position).addScaledVector(d, Math.max(r, 5));
camera.up.set(0, 1, 0);
camera.up.set(0, 0, 1); // back to the app-wide Z-up default, not three.js's Y-up
controls.enabled = true;
controls.update();
fpStartBtn.disabled = false; fpExitBtn.disabled = true;