Skip to content

Commit 9da2345

Browse files
authored
Merge pull request #4 from shivanshkc/dev-webgpu
Touch Controls for Mobiles
2 parents 9e9baaa + a972a5e commit 9da2345

File tree

5 files changed

+213
-89
lines changed

5 files changed

+213
-89
lines changed

CHANGELOG.md

Lines changed: 0 additions & 88 deletions
This file was deleted.

src/__tests__/CameraController.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,41 @@ describe('CameraController', () => {
8383

8484
expect(callback).toHaveBeenCalled();
8585
});
86+
87+
it('handles one-finger touch orbit', () => {
88+
const initialAzimuth = useCameraStore.getState().azimuth;
89+
90+
const touchStart = new Event('touchstart', { bubbles: true, cancelable: true }) as any;
91+
touchStart.touches = [{ clientX: 100, clientY: 100, identifier: 1 }];
92+
canvas.dispatchEvent(touchStart);
93+
94+
const touchMove = new Event('touchmove', { bubbles: true, cancelable: true }) as any;
95+
touchMove.touches = [{ clientX: 120, clientY: 100, identifier: 1 }];
96+
canvas.dispatchEvent(touchMove);
97+
98+
expect(useCameraStore.getState().azimuth).not.toBe(initialAzimuth);
99+
});
100+
101+
it('handles two-finger pinch zoom', () => {
102+
const initialDistance = useCameraStore.getState().distance;
103+
104+
const touchStart = new Event('touchstart', { bubbles: true, cancelable: true }) as any;
105+
touchStart.touches = [
106+
{ clientX: 100, clientY: 100, identifier: 1 },
107+
{ clientX: 200, clientY: 100, identifier: 2 },
108+
];
109+
canvas.dispatchEvent(touchStart);
110+
111+
const touchMove = new Event('touchmove', { bubbles: true, cancelable: true }) as any;
112+
// Increase distance between touches (pinch out => zoom in => camera distance should decrease)
113+
touchMove.touches = [
114+
{ clientX: 90, clientY: 100, identifier: 1 },
115+
{ clientX: 210, clientY: 100, identifier: 2 },
116+
];
117+
canvas.dispatchEvent(touchMove);
118+
119+
expect(useCameraStore.getState().distance).toBeLessThan(initialDistance);
120+
});
86121
});
87122

88123
describe('keyboard shortcuts', () => {

src/__tests__/Canvas.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ describe('Canvas Component', () => {
6464
expect(canvas).not.toBeNull();
6565
});
6666

67+
it('disables default touch gestures on canvas', () => {
68+
const { container } = render(<Canvas />);
69+
const canvas = container.querySelector('canvas') as HTMLCanvasElement;
70+
expect(canvas.className).toContain('touch-none');
71+
});
72+
6773
it('hides loading overlay when WebGPU initializes', async () => {
6874
render(<Canvas />);
6975

src/components/Canvas.tsx

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,14 @@ export function Canvas({ className, onRendererReady }: CanvasProps) {
148148

149149
// Click-to-select and gizmo interaction handling
150150
const mouseDownPos = useRef<{ x: number; y: number } | null>(null);
151+
const touchStartPos = useRef<{ x: number; y: number; t: number } | null>(
152+
null
153+
);
151154
const dragStartRay = useRef<Ray | null>(null);
152155
const dragStartRotation = useRef<[number, number, number] | null>(null);
153156
const dragStartScale = useRef<Vec3 | null>(null);
154157
const DRAG_THRESHOLD = 5;
158+
const TAP_MAX_MS = 250;
155159

156160
// Helper to get camera vectors
157161
const getCameraVectors = useCallback(() => {
@@ -477,6 +481,61 @@ export function Canvas({ className, onRendererReady }: CanvasProps) {
477481
[]
478482
);
479483

484+
const handleTouchStart = useCallback((e: React.TouchEvent<HTMLCanvasElement>) => {
485+
if (e.touches.length !== 1) {
486+
touchStartPos.current = null;
487+
return;
488+
}
489+
const t = e.touches[0]!;
490+
touchStartPos.current = { x: t.clientX, y: t.clientY, t: Date.now() };
491+
}, []);
492+
493+
const handleTouchEnd = useCallback((e: React.TouchEvent<HTMLCanvasElement>) => {
494+
const start = touchStartPos.current;
495+
touchStartPos.current = null;
496+
if (!start) return;
497+
498+
// Only treat as tap if it was short and didn't move much.
499+
const dt = Date.now() - start.t;
500+
if (dt > TAP_MAX_MS) return;
501+
502+
const canvas = canvasRef.current;
503+
if (!canvas) return;
504+
const rect = canvas.getBoundingClientRect();
505+
506+
// Use the last known end position from the changed touch
507+
const t = e.changedTouches[0];
508+
if (!t) return;
509+
const dx = t.clientX - start.x;
510+
const dy = t.clientY - start.y;
511+
if (Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) return;
512+
513+
const x = t.clientX - rect.left;
514+
const y = t.clientY - rect.top;
515+
516+
const cameraState = useCameraStore.getState();
517+
const viewMatrix = cameraState.getViewMatrix();
518+
const aspect = rect.width / rect.height;
519+
const projMatrix = mat4Perspective(cameraState.fovY, aspect, 0.1, 1000);
520+
const inverseView = mat4Inverse(viewMatrix);
521+
const inverseProjection = mat4Inverse(projMatrix);
522+
523+
const objects = useSceneStore.getState().objects;
524+
const result = raycaster.pick(
525+
x,
526+
y,
527+
rect.width,
528+
rect.height,
529+
cameraState.position,
530+
inverseProjection,
531+
inverseView,
532+
objects
533+
);
534+
535+
useSceneStore.getState().selectObject(result.objectId);
536+
rendererRef.current?.resetAccumulation();
537+
}, []);
538+
480539
// Handle mouse leave to clear hover
481540
const handleMouseLeave = useCallback(() => {
482541
useGizmoStore.getState().setHoveredAxis(null);
@@ -521,12 +580,14 @@ export function Canvas({ className, onRendererReady }: CanvasProps) {
521580
<div className={`relative w-full h-full ${className || ''}`}>
522581
<canvas
523582
ref={canvasRef}
524-
className="absolute inset-0 w-full h-full"
583+
className="absolute inset-0 w-full h-full touch-none"
525584
tabIndex={0}
526585
onMouseDown={handleMouseDown}
527586
onMouseUp={handleMouseUp}
528587
onMouseMove={handleMouseMove}
529588
onMouseLeave={handleMouseLeave}
589+
onTouchStart={handleTouchStart}
590+
onTouchEnd={handleTouchEnd}
530591
/>
531592
{status === 'loading' && (
532593
<div className="absolute inset-0 flex items-center justify-center bg-base z-10">

0 commit comments

Comments
 (0)