feat(expert-mode): improve mobile touch controls for 3D scene

- Simplified OrbitControls touch gesture configuration:
  - ONE: THREE.TOUCH.ROTATE (one-finger orbit)
  - TWO: THREE.TOUCH.DOLLY (pinch zoom only, no accidental pan)
  - THREE: THREE.TOUCH.PAN (three-finger pan)

- Removed complex dynamic enablePan toggling that didn't work reliably
  with OrbitControls' internal touch processing

- Added iOS Safari-specific touch improvements:
  - Double-tap zoom prevention
  - Touch action and user select CSS properties
  - -webkit-tap-highlight-color: transparent

- Enhanced CSS for better mobile touch handling:
  - touch-action: none on scene container and canvas
  - -webkit-touch-callout: none
  - -webkit-user-select: none
  - user-select: none

All mobile tests (29 tests) and quick-actions tests (22 tests) pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-25 11:00:56 -04:00
parent 7aae1c2a46
commit ef9cd3fe15
13 changed files with 75 additions and 67 deletions

File diff suppressed because one or more lines are too long

View file

@ -11,6 +11,6 @@
"cost_usd": null, "cost_usd": null,
"captured_at": "2026-04-11T00:53:39.772264711Z", "captured_at": "2026-04-11T00:53:39.772264711Z",
"trace_format": "claude_json", "trace_format": "claude_json",
"pruned": false, "pruned": true,
"template_version": null "template_version": null
} }

View file

@ -1,3 +0,0 @@
```json
{"splittable": false}
```

View file

@ -11,6 +11,6 @@
"cost_usd": null, "cost_usd": null,
"captured_at": "2026-04-11T00:53:51.503031372Z", "captured_at": "2026-04-11T00:53:51.503031372Z",
"trace_format": "claude_json", "trace_format": "claude_json",
"pruned": false, "pruned": true,
"template_version": null "template_version": null
} }

View file

@ -1,3 +0,0 @@
```json
{"splittable": false}
```

View file

@ -1 +1 @@
03fd4e2752b6afb09385b74d87a7c1b7ff9061aa ab5e77352d2cf097851d9b0e8b9a8edf2d8e5605

View file

@ -261,6 +261,11 @@ body {
width: 100%; width: 100%;
height: 100%; height: 100%;
touch-action: none; touch-action: none;
/* Mobile-specific touch handling improvements */
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
} }
/* Live-view overlay panels (float over canvas) /* Live-view overlay panels (float over canvas)

View file

@ -227,6 +227,12 @@
height: 100%; height: 100%;
touch-action: none; touch-action: none;
z-index: 1; z-index: 1;
/* Mobile-specific touch handling improvements */
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
/* Prevent iOS Safari from delaying single taps */
-webkit-tap-highlight-color: transparent;
} }
/* iOS Safari-specific handling for visual viewport */ /* iOS Safari-specific handling for visual viewport */
@ -251,6 +257,11 @@
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
touch-action: none; touch-action: none;
/* Mobile-specific touch handling improvements */
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
} }
/* Handle visual viewport resize events (iOS Safari keyboard, address bar) */ /* Handle visual viewport resize events (iOS Safari keyboard, address bar) */

View file

@ -188,58 +188,52 @@
// Touch gesture configuration for OrbitControls // Touch gesture configuration for OrbitControls
// One finger: rotate (orbit) // One finger: rotate (orbit)
// Two fingers: pinch-to-zoom (dolly) only // Two fingers: pinch-to-zoom (dolly) only - NO pan on two fingers
// Three fingers: pan (native OrbitControls support) // Three fingers: pan (native OrbitControls support)
// //
// Using OrbitControls' built-in touch configuration: // Using OrbitControls' built-in touch configuration:
// - ONE: THREE.TOUCH.ROTATE (one-finger orbit) // - ONE: THREE.TOUCH.ROTATE (one-finger orbit)
// - TWO: THREE.TOUCH.DOLLY_PAN (pinch zoom + two-finger pan, with pan disabled below) // - TWO: THREE.TOUCH.DOLLY (pinch zoom ONLY, no pan)
// - THREE: THREE.TOUCH.PAN (three-finger pan) // - THREE: THREE.TOUCH.PAN (three-finger pan)
// //
// Note: We set enablePan=false initially to prevent two-finger pan, // Note: THREE.TOUCH.DOLLY (not DOLLY_PAN) ensures two-finger touch
// allowing only pinch zoom on two fingers. Three-finger pan works // only performs pinch zoom, eliminating accidental pan during zoom.
// through the THREE.TOUCH.PAN configuration.
controls.touches = { controls.touches = {
ONE: THREE.TOUCH.ROTATE, // One-finger touch rotates the camera ONE: THREE.TOUCH.ROTATE, // One-finger touch rotates the camera
TWO: THREE.TOUCH.DOLLY_PAN, // Two-finger touch zooms (pinch) and pans TWO: THREE.TOUCH.DOLLY, // Two-finger touch zooms (pinch) ONLY - no pan
THREE: THREE.TOUCH.PAN // Three-finger touch pans the camera THREE: THREE.TOUCH.PAN // Three-finger touch pans the camera
}; };
// Disable two-finger pan to prevent accidental pan during pinch zoom
// Only allow three-finger pan for deliberate camera panning
controls.listenToKeyEvents(window); // Enable keyboard controls for desktop controls.listenToKeyEvents(window); // Enable keyboard controls for desktop
// Custom touch event handling to prevent two-finger pan while allowing three-finger pan // Mobile-specific touch handling improvements
const element = renderer.domElement; // Prevent default browser behaviors that interfere with OrbitControls
let originalEnablePan = controls.enablePan; const canvasElement = renderer.domElement;
element.addEventListener('touchstart', function(event) { // Ensure touch events are properly handled on mobile devices
// Disable pan for two-finger touch to prevent accidental pan during pinch zoom canvasElement.style.touchAction = 'none';
if (event.touches.length === 2) { canvasElement.style.webkitTouchCallout = 'none';
controls.enablePan = false; canvasElement.style.webkitUserSelect = 'none';
} else if (event.touches.length === 3) { canvasElement.style.userSelect = 'none';
// Enable pan for three-finger touch
controls.enablePan = true; // Handle iOS Safari-specific touch issues
} else { // Prevent double-tap zoom on iOS
controls.enablePan = originalEnablePan; canvasElement.addEventListener('touchstart', function(event) {
if (event.touches.length === 1) {
// Store timestamp for double-tap detection
canvasElement._lastTouchStart = Date.now();
} }
}, { passive: true }); }, { passive: true });
element.addEventListener('touchend', function(event) { canvasElement.addEventListener('touchend', function(event) {
// Restore pan state when fingers are lifted if (event.touches.length === 0 && canvasElement._lastTouchStart) {
if (event.touches.length === 0) { const timeSinceLastTouch = Date.now() - canvasElement._lastTouchStart;
controls.enablePan = originalEnablePan; // If it's a quick tap (potential double-tap), prevent default
} else if (event.touches.length === 2) { if (timeSinceLastTouch < 300) {
controls.enablePan = false; event.preventDefault();
} else if (event.touches.length === 3) { }
controls.enablePan = true;
} }
}, { passive: true }); }, { passive: false });
element.addEventListener('touchcancel', function() {
// Restore pan state on touch cancel
controls.enablePan = originalEnablePan;
}, { passive: true });
// Grid helper (XZ plane, Y-up) // Grid helper (XZ plane, Y-up)
gridHelper = new THREE.GridHelper( gridHelper = new THREE.GridHelper(

Binary file not shown.

Binary file not shown.