diff --git a/pcd_viewer.html b/pcd_viewer.html index 8b78e02..4117ec9 100644 --- a/pcd_viewer.html +++ b/pcd_viewer.html @@ -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;