@@ -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