@lark-apaas/coding-steering 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,880 @@
1
+ # React Three Fiber Interaction
2
+
3
+ ## Quick Start
4
+
5
+ ```tsx
6
+ import { Canvas } from '@react-three/fiber'
7
+ import { OrbitControls } from '@react-three/drei'
8
+
9
+ function InteractiveMesh() {
10
+ return (
11
+ <mesh
12
+ onClick={(e) => console.log('Clicked!', e.point)}
13
+ onPointerOver={(e) => console.log('Hover')}
14
+ onPointerOut={(e) => console.log('Unhover')}
15
+ >
16
+ <boxGeometry />
17
+ <meshStandardMaterial color="hotpink" />
18
+ </mesh>
19
+ )
20
+ }
21
+
22
+ export default function App() {
23
+ return (
24
+ <Canvas>
25
+ <ambientLight />
26
+ <InteractiveMesh />
27
+ <OrbitControls />
28
+ </Canvas>
29
+ )
30
+ }
31
+ ```
32
+
33
+ ## Pointer Events
34
+
35
+ R3F provides built-in pointer events on mesh elements.
36
+
37
+ ### Available Events
38
+
39
+ ```tsx
40
+ <mesh
41
+ // Click events
42
+ onClick={(e) => {}} // Click (pointerdown + pointerup on same object)
43
+ onDoubleClick={(e) => {}} // Double click
44
+ onContextMenu={(e) => {}} // Right click
45
+
46
+ // Pointer events
47
+ onPointerDown={(e) => {}} // Pointer pressed
48
+ onPointerUp={(e) => {}} // Pointer released
49
+ onPointerMove={(e) => {}} // Pointer moved while over object
50
+ onPointerOver={(e) => {}} // Pointer enters object
51
+ onPointerOut={(e) => {}} // Pointer leaves object
52
+ onPointerEnter={(e) => {}} // Pointer enters object (no bubbling)
53
+ onPointerLeave={(e) => {}} // Pointer leaves object (no bubbling)
54
+ onPointerMissed={(e) => {}} // Click that missed all objects
55
+
56
+ // Wheel
57
+ onWheel={(e) => {}} // Mouse wheel
58
+
59
+ // Touch
60
+ onPointerCancel={(e) => {}} // Touch cancelled
61
+ >
62
+ <boxGeometry />
63
+ <meshStandardMaterial />
64
+ </mesh>
65
+ ```
66
+
67
+ ### Event Object
68
+
69
+ ```tsx
70
+ function InteractiveMesh() {
71
+ const handleClick = (event) => {
72
+ // Stop propagation to parent objects
73
+ event.stopPropagation()
74
+
75
+ // Event properties
76
+ console.log({
77
+ object: event.object, // The mesh that was clicked
78
+ point: event.point, // World coordinates of intersection
79
+ distance: event.distance, // Distance from camera
80
+ face: event.face, // Intersected face
81
+ faceIndex: event.faceIndex, // Face index
82
+ uv: event.uv, // UV coordinates at intersection
83
+ normal: event.normal, // Face normal
84
+ camera: event.camera, // Current camera
85
+ ray: event.ray, // Ray used for intersection
86
+ intersections: event.intersections, // All intersections
87
+ nativeEvent: event.nativeEvent, // Original DOM event
88
+ delta: event.delta, // Click distance (useful for drag detection)
89
+ })
90
+ }
91
+
92
+ return (
93
+ <mesh onClick={handleClick}>
94
+ <boxGeometry />
95
+ <meshStandardMaterial />
96
+ </mesh>
97
+ )
98
+ }
99
+ ```
100
+
101
+ ### Hover Effects
102
+
103
+ ```tsx
104
+ import { useState } from 'react'
105
+
106
+ function HoverableMesh() {
107
+ const [hovered, setHovered] = useState(false)
108
+
109
+ return (
110
+ <mesh
111
+ onPointerOver={(e) => {
112
+ e.stopPropagation()
113
+ setHovered(true)
114
+ document.body.style.cursor = 'pointer'
115
+ }}
116
+ onPointerOut={(e) => {
117
+ setHovered(false)
118
+ document.body.style.cursor = 'default'
119
+ }}
120
+ scale={hovered ? 1.2 : 1}
121
+ >
122
+ <boxGeometry />
123
+ <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
124
+ </mesh>
125
+ )
126
+ }
127
+ ```
128
+
129
+ ### Selective Raycasting
130
+
131
+ ```tsx
132
+ // Disable raycasting for specific objects
133
+ <mesh raycast={() => null}>
134
+ <boxGeometry />
135
+ <meshStandardMaterial />
136
+ </mesh>
137
+
138
+ // Or use layers
139
+ <mesh
140
+ layers={1} // Only raycast against layer 1
141
+ onClick={() => console.log('clicked')}
142
+ >
143
+ <boxGeometry />
144
+ <meshStandardMaterial />
145
+ </mesh>
146
+ ```
147
+
148
+ ## Camera Controls
149
+
150
+ ### OrbitControls
151
+
152
+ ```tsx
153
+ import { OrbitControls } from '@react-three/drei'
154
+
155
+ function Scene() {
156
+ return (
157
+ <>
158
+ <mesh>
159
+ <boxGeometry />
160
+ <meshStandardMaterial />
161
+ </mesh>
162
+
163
+ <OrbitControls
164
+ makeDefault // Use as default controls
165
+ enableDamping // Smooth movement
166
+ dampingFactor={0.05}
167
+ enableZoom={true}
168
+ enablePan={true}
169
+ enableRotate={true}
170
+ autoRotate={false}
171
+ autoRotateSpeed={2}
172
+ minDistance={2}
173
+ maxDistance={50}
174
+ minPolarAngle={0} // Top limit
175
+ maxPolarAngle={Math.PI / 2} // Horizon limit
176
+ minAzimuthAngle={-Math.PI / 4} // Left limit
177
+ maxAzimuthAngle={Math.PI / 4} // Right limit
178
+ target={[0, 1, 0]} // Look-at point
179
+ />
180
+ </>
181
+ )
182
+ }
183
+ ```
184
+
185
+ ### OrbitControls with Ref
186
+
187
+ ```tsx
188
+ import { OrbitControls } from '@react-three/drei'
189
+ import { useRef, useEffect } from 'react'
190
+
191
+ function Scene() {
192
+ const controlsRef = useRef()
193
+
194
+ useEffect(() => {
195
+ // Access controls methods
196
+ if (controlsRef.current) {
197
+ controlsRef.current.reset()
198
+ controlsRef.current.target.set(0, 1, 0)
199
+ controlsRef.current.update()
200
+ }
201
+ }, [])
202
+
203
+ return <OrbitControls ref={controlsRef} />
204
+ }
205
+ ```
206
+
207
+ ### MapControls
208
+
209
+ Top-down map-style controls.
210
+
211
+ ```tsx
212
+ import { MapControls } from '@react-three/drei'
213
+
214
+ <MapControls
215
+ enableDamping
216
+ dampingFactor={0.05}
217
+ screenSpacePanning={false} // Pan in world space
218
+ maxPolarAngle={Math.PI / 2}
219
+ />
220
+ ```
221
+
222
+ ### FlyControls
223
+
224
+ Free-flying camera controls.
225
+
226
+ ```tsx
227
+ import { FlyControls } from '@react-three/drei'
228
+
229
+ <FlyControls
230
+ movementSpeed={10}
231
+ rollSpeed={Math.PI / 24}
232
+ dragToLook
233
+ />
234
+ ```
235
+
236
+ ### FirstPersonControls
237
+
238
+ FPS-style controls.
239
+
240
+ ```tsx
241
+ import { FirstPersonControls } from '@react-three/drei'
242
+
243
+ <FirstPersonControls
244
+ movementSpeed={10}
245
+ lookSpeed={0.1}
246
+ lookVertical
247
+ />
248
+ ```
249
+
250
+ ### PointerLockControls
251
+
252
+ Lock pointer for FPS games.
253
+
254
+ ```tsx
255
+ import { PointerLockControls } from '@react-three/drei'
256
+ import { useRef } from 'react'
257
+
258
+ function Scene() {
259
+ const controlsRef = useRef()
260
+
261
+ return (
262
+ <>
263
+ <PointerLockControls ref={controlsRef} />
264
+
265
+ {/* Click to lock pointer */}
266
+ <mesh onClick={() => controlsRef.current?.lock()}>
267
+ <planeGeometry args={[10, 10]} />
268
+ <meshBasicMaterial color="green" />
269
+ </mesh>
270
+ </>
271
+ )
272
+ }
273
+ ```
274
+
275
+ ### CameraControls
276
+
277
+ Advanced camera controls with smooth transitions.
278
+
279
+ ```tsx
280
+ import { CameraControls } from '@react-three/drei'
281
+ import { useRef } from 'react'
282
+
283
+ function Scene() {
284
+ const controlsRef = useRef()
285
+
286
+ const focusOnObject = async () => {
287
+ // Smooth transition to target
288
+ await controlsRef.current?.setLookAt(
289
+ 5, 3, 5, // Camera position
290
+ 0, 0, 0, // Look-at target
291
+ true // Enable transition
292
+ )
293
+ }
294
+
295
+ return (
296
+ <>
297
+ <CameraControls ref={controlsRef} />
298
+
299
+ <mesh onClick={focusOnObject}>
300
+ <boxGeometry />
301
+ <meshStandardMaterial color="red" />
302
+ </mesh>
303
+ </>
304
+ )
305
+ }
306
+ ```
307
+
308
+ ### TrackballControls
309
+
310
+ Unconstrained rotation controls.
311
+
312
+ ```tsx
313
+ import { TrackballControls } from '@react-three/drei'
314
+
315
+ <TrackballControls
316
+ rotateSpeed={2.0}
317
+ zoomSpeed={1.2}
318
+ panSpeed={0.8}
319
+ staticMoving={true}
320
+ />
321
+ ```
322
+
323
+ ### ArcballControls
324
+
325
+ Arc-based rotation controls.
326
+
327
+ ```tsx
328
+ import { ArcballControls } from '@react-three/drei'
329
+
330
+ <ArcballControls
331
+ enableAnimations
332
+ dampingFactor={25}
333
+ />
334
+ ```
335
+
336
+ ## Transform Controls
337
+
338
+ Gizmo for moving/rotating/scaling objects.
339
+
340
+ ```tsx
341
+ import { TransformControls, OrbitControls } from '@react-three/drei'
342
+ import { useRef, useState } from 'react'
343
+
344
+ function Scene() {
345
+ const meshRef = useRef()
346
+ const [mode, setMode] = useState('translate')
347
+ const orbitRef = useRef()
348
+
349
+ return (
350
+ <>
351
+ <OrbitControls ref={orbitRef} makeDefault />
352
+
353
+ <TransformControls
354
+ object={meshRef}
355
+ mode={mode} // 'translate' | 'rotate' | 'scale'
356
+ space="local" // 'local' | 'world'
357
+ onMouseDown={() => {
358
+ // Disable orbit while transforming
359
+ if (orbitRef.current) orbitRef.current.enabled = false
360
+ }}
361
+ onMouseUp={() => {
362
+ if (orbitRef.current) orbitRef.current.enabled = true
363
+ }}
364
+ />
365
+
366
+ <mesh ref={meshRef}>
367
+ <boxGeometry />
368
+ <meshStandardMaterial color="orange" />
369
+ </mesh>
370
+
371
+ {/* Mode switching buttons in HTML */}
372
+ <div className="controls">
373
+ <button onClick={() => setMode('translate')}>Move</button>
374
+ <button onClick={() => setMode('rotate')}>Rotate</button>
375
+ <button onClick={() => setMode('scale')}>Scale</button>
376
+ </div>
377
+ </>
378
+ )
379
+ }
380
+ ```
381
+
382
+ ### PivotControls
383
+
384
+ Alternative transform gizmo with pivot point.
385
+
386
+ ```tsx
387
+ import { PivotControls } from '@react-three/drei'
388
+
389
+ function Scene() {
390
+ return (
391
+ <PivotControls
392
+ anchor={[0, 0, 0]} // Anchor point
393
+ depthTest={false} // Always visible
394
+ lineWidth={2} // Axis line width
395
+ axisColors={['red', 'green', 'blue']}
396
+ scale={1} // Gizmo scale
397
+ fixed={false} // Fixed screen size
398
+ >
399
+ <mesh>
400
+ <boxGeometry />
401
+ <meshStandardMaterial color="orange" />
402
+ </mesh>
403
+ </PivotControls>
404
+ )
405
+ }
406
+ ```
407
+
408
+ ## Drag Controls
409
+
410
+ ### useDrag from @use-gesture/react
411
+
412
+ ```bash
413
+ npm install @use-gesture/react
414
+ ```
415
+
416
+ ```tsx
417
+ import { useDrag } from '@use-gesture/react'
418
+ import { useSpring, animated } from '@react-spring/three'
419
+ import { useThree } from '@react-three/fiber'
420
+
421
+ function DraggableMesh() {
422
+ const { size, viewport } = useThree()
423
+ const aspect = size.width / viewport.width
424
+
425
+ const [spring, api] = useSpring(() => ({
426
+ position: [0, 0, 0],
427
+ config: { mass: 1, tension: 280, friction: 60 }
428
+ }))
429
+
430
+ const bind = useDrag(({ movement: [mx, my], down }) => {
431
+ api.start({
432
+ position: down ? [mx / aspect, -my / aspect, 0] : [0, 0, 0]
433
+ })
434
+ })
435
+
436
+ return (
437
+ <animated.mesh {...bind()} position={spring.position}>
438
+ <boxGeometry />
439
+ <meshStandardMaterial color="hotpink" />
440
+ </animated.mesh>
441
+ )
442
+ }
443
+ ```
444
+
445
+ ### DragControls (Drei)
446
+
447
+ ```tsx
448
+ import { DragControls, OrbitControls } from '@react-three/drei'
449
+ import { useRef } from 'react'
450
+
451
+ function Scene() {
452
+ const meshRef = useRef()
453
+ const orbitRef = useRef()
454
+
455
+ return (
456
+ <>
457
+ <OrbitControls ref={orbitRef} makeDefault />
458
+
459
+ <DragControls
460
+ onDragStart={() => {
461
+ if (orbitRef.current) orbitRef.current.enabled = false
462
+ }}
463
+ onDragEnd={() => {
464
+ if (orbitRef.current) orbitRef.current.enabled = true
465
+ }}
466
+ >
467
+ <mesh ref={meshRef}>
468
+ <boxGeometry />
469
+ <meshStandardMaterial color="orange" />
470
+ </mesh>
471
+ </DragControls>
472
+ </>
473
+ )
474
+ }
475
+ ```
476
+
477
+ ## Keyboard Controls
478
+
479
+ ### KeyboardControls (Drei)
480
+
481
+ ```tsx
482
+ import { KeyboardControls, useKeyboardControls } from '@react-three/drei'
483
+ import { useFrame } from '@react-three/fiber'
484
+ import { useRef } from 'react'
485
+
486
+ // Define key mappings
487
+ const keyMap = [
488
+ { name: 'forward', keys: ['ArrowUp', 'KeyW'] },
489
+ { name: 'backward', keys: ['ArrowDown', 'KeyS'] },
490
+ { name: 'left', keys: ['ArrowLeft', 'KeyA'] },
491
+ { name: 'right', keys: ['ArrowRight', 'KeyD'] },
492
+ { name: 'jump', keys: ['Space'] },
493
+ { name: 'sprint', keys: ['ShiftLeft'] },
494
+ ]
495
+
496
+ function Player() {
497
+ const meshRef = useRef()
498
+ const [, getKeys] = useKeyboardControls()
499
+
500
+ useFrame((state, delta) => {
501
+ const { forward, backward, left, right, jump, sprint } = getKeys()
502
+
503
+ const speed = sprint ? 10 : 5
504
+
505
+ if (forward) meshRef.current.position.z -= speed * delta
506
+ if (backward) meshRef.current.position.z += speed * delta
507
+ if (left) meshRef.current.position.x -= speed * delta
508
+ if (right) meshRef.current.position.x += speed * delta
509
+ if (jump) meshRef.current.position.y += speed * delta
510
+ })
511
+
512
+ return (
513
+ <mesh ref={meshRef}>
514
+ <boxGeometry />
515
+ <meshStandardMaterial color="blue" />
516
+ </mesh>
517
+ )
518
+ }
519
+
520
+ export default function App() {
521
+ return (
522
+ <KeyboardControls map={keyMap}>
523
+ <Canvas>
524
+ <ambientLight />
525
+ <Player />
526
+ </Canvas>
527
+ </KeyboardControls>
528
+ )
529
+ }
530
+ ```
531
+
532
+ ### Subscribe to Key Changes
533
+
534
+ ```tsx
535
+ import { useKeyboardControls } from '@react-three/drei'
536
+ import { useEffect } from 'react'
537
+
538
+ function KeyListener() {
539
+ const jumpPressed = useKeyboardControls((state) => state.jump)
540
+
541
+ useEffect(() => {
542
+ if (jumpPressed) {
543
+ console.log('Jump!')
544
+ }
545
+ }, [jumpPressed])
546
+
547
+ return null
548
+ }
549
+ ```
550
+
551
+ ## Selection System
552
+
553
+ ### Click to Select
554
+
555
+ ```tsx
556
+ import { useState } from 'react'
557
+
558
+ function SelectableScene() {
559
+ const [selected, setSelected] = useState(null)
560
+
561
+ return (
562
+ <>
563
+ {[[-2, 0, 0], [0, 0, 0], [2, 0, 0]].map((position, i) => (
564
+ <mesh
565
+ key={i}
566
+ position={position}
567
+ onClick={(e) => {
568
+ e.stopPropagation()
569
+ setSelected(i)
570
+ }}
571
+ >
572
+ <boxGeometry />
573
+ <meshStandardMaterial
574
+ color={selected === i ? 'hotpink' : 'orange'}
575
+ emissive={selected === i ? 'hotpink' : 'black'}
576
+ emissiveIntensity={0.3}
577
+ />
578
+ </mesh>
579
+ ))}
580
+
581
+ {/* Click on empty space to deselect */}
582
+ <mesh
583
+ position={[0, -1, 0]}
584
+ rotation={[-Math.PI / 2, 0, 0]}
585
+ onClick={() => setSelected(null)}
586
+ >
587
+ <planeGeometry args={[20, 20]} />
588
+ <meshStandardMaterial color="gray" />
589
+ </mesh>
590
+ </>
591
+ )
592
+ }
593
+ ```
594
+
595
+ ### Multi-Select with Outline
596
+
597
+ ```tsx
598
+ import { useState } from 'react'
599
+ import { EffectComposer, Outline, Selection, Select } from '@react-three/postprocessing'
600
+
601
+ function MultiSelectScene() {
602
+ const [selected, setSelected] = useState(new Set())
603
+
604
+ const toggleSelect = (id, event) => {
605
+ event.stopPropagation()
606
+ setSelected((prev) => {
607
+ const next = new Set(prev)
608
+ if (event.shiftKey) {
609
+ // Multi-select with shift
610
+ if (next.has(id)) {
611
+ next.delete(id)
612
+ } else {
613
+ next.add(id)
614
+ }
615
+ } else {
616
+ // Single select
617
+ next.clear()
618
+ next.add(id)
619
+ }
620
+ return next
621
+ })
622
+ }
623
+
624
+ return (
625
+ <Selection>
626
+ <EffectComposer autoClear={false}>
627
+ <Outline
628
+ blur
629
+ visibleEdgeColor={0xffffff}
630
+ edgeStrength={10}
631
+ />
632
+ </EffectComposer>
633
+
634
+ {[0, 1, 2, 3, 4].map((id) => (
635
+ <Select key={id} enabled={selected.has(id)}>
636
+ <mesh
637
+ position={[(id - 2) * 2, 0, 0]}
638
+ onClick={(e) => toggleSelect(id, e)}
639
+ >
640
+ <boxGeometry />
641
+ <meshStandardMaterial color="orange" />
642
+ </mesh>
643
+ </Select>
644
+ ))}
645
+ </Selection>
646
+ )
647
+ }
648
+ ```
649
+
650
+ ## Screen-Space to World-Space
651
+
652
+ ### Get World Position from Click
653
+
654
+ ```tsx
655
+ import { useThree } from '@react-three/fiber'
656
+ import * as THREE from 'three'
657
+
658
+ function ClickToPlace() {
659
+ const { camera, raycaster, pointer } = useThree()
660
+ const planeRef = useRef()
661
+
662
+ const handleClick = (event) => {
663
+ // Create intersection plane
664
+ const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
665
+ const intersection = new THREE.Vector3()
666
+
667
+ // Cast ray from pointer
668
+ raycaster.setFromCamera(pointer, camera)
669
+ raycaster.ray.intersectPlane(plane, intersection)
670
+
671
+ console.log('World position:', intersection)
672
+ }
673
+
674
+ return (
675
+ <mesh
676
+ ref={planeRef}
677
+ rotation={[-Math.PI / 2, 0, 0]}
678
+ onClick={handleClick}
679
+ >
680
+ <planeGeometry args={[100, 100]} />
681
+ <meshBasicMaterial visible={false} />
682
+ </mesh>
683
+ )
684
+ }
685
+ ```
686
+
687
+ ### World Position to Screen Position
688
+
689
+ ```tsx
690
+ import { useThree, useFrame } from '@react-three/fiber'
691
+ import { Html } from '@react-three/drei'
692
+ import * as THREE from 'three'
693
+
694
+ function WorldToScreen({ target }) {
695
+ const { camera, size } = useThree()
696
+
697
+ const getScreenPosition = (worldPos) => {
698
+ const vector = worldPos.clone()
699
+ vector.project(camera)
700
+
701
+ return {
702
+ x: (vector.x * 0.5 + 0.5) * size.width,
703
+ y: (1 - (vector.y * 0.5 + 0.5)) * size.height
704
+ }
705
+ }
706
+
707
+ // Or use Html component which handles this automatically
708
+ return (
709
+ <Html position={target}>
710
+ <div className="label">Label</div>
711
+ </Html>
712
+ )
713
+ }
714
+ ```
715
+
716
+ ## Gesture Recognition
717
+
718
+ ### usePinch and useWheel
719
+
720
+ ```tsx
721
+ import { usePinch, useWheel } from '@use-gesture/react'
722
+ import { useSpring, animated } from '@react-spring/three'
723
+
724
+ function ZoomableMesh() {
725
+ const [spring, api] = useSpring(() => ({
726
+ scale: 1,
727
+ config: { mass: 1, tension: 200, friction: 30 }
728
+ }))
729
+
730
+ usePinch(
731
+ ({ offset: [s] }) => {
732
+ api.start({ scale: s })
733
+ },
734
+ { target: window }
735
+ )
736
+
737
+ useWheel(
738
+ ({ delta: [, dy] }) => {
739
+ api.start({ scale: spring.scale.get() - dy * 0.001 })
740
+ },
741
+ { target: window }
742
+ )
743
+
744
+ return (
745
+ <animated.mesh scale={spring.scale}>
746
+ <boxGeometry />
747
+ <meshStandardMaterial color="cyan" />
748
+ </animated.mesh>
749
+ )
750
+ }
751
+ ```
752
+
753
+ ## Scroll Controls
754
+
755
+ ```tsx
756
+ import { Canvas } from '@react-three/fiber'
757
+ import { ScrollControls, Scroll, useScroll } from '@react-three/drei'
758
+ import { useFrame } from '@react-three/fiber'
759
+ import { useRef } from 'react'
760
+
761
+ function AnimatedOnScroll() {
762
+ const meshRef = useRef()
763
+ const scroll = useScroll()
764
+
765
+ useFrame(() => {
766
+ const offset = scroll.offset // 0 to 1
767
+ meshRef.current.rotation.y = offset * Math.PI * 2
768
+ meshRef.current.position.y = offset * 5
769
+ })
770
+
771
+ return (
772
+ <mesh ref={meshRef}>
773
+ <boxGeometry />
774
+ <meshStandardMaterial color="orange" />
775
+ </mesh>
776
+ )
777
+ }
778
+
779
+ export default function App() {
780
+ return (
781
+ <Canvas>
782
+ <ScrollControls pages={3} damping={0.25}>
783
+ <Scroll>
784
+ <AnimatedOnScroll />
785
+ </Scroll>
786
+
787
+ {/* HTML content that scrolls */}
788
+ <Scroll html>
789
+ <h1 style={{ position: 'absolute', top: '10vh' }}>Page 1</h1>
790
+ <h1 style={{ position: 'absolute', top: '110vh' }}>Page 2</h1>
791
+ <h1 style={{ position: 'absolute', top: '210vh' }}>Page 3</h1>
792
+ </Scroll>
793
+ </ScrollControls>
794
+ </Canvas>
795
+ )
796
+ }
797
+ ```
798
+
799
+ ## Presentation Controls
800
+
801
+ For product showcases with limited rotation.
802
+
803
+ ```tsx
804
+ import { PresentationControls } from '@react-three/drei'
805
+
806
+ function ProductShowcase() {
807
+ return (
808
+ <PresentationControls
809
+ global // Apply to whole scene
810
+ snap // Snap back when released
811
+ speed={1} // Rotation speed
812
+ zoom={1} // Zoom speed
813
+ rotation={[0, 0, 0]} // Initial rotation
814
+ polar={[-Math.PI / 4, Math.PI / 4]} // Vertical limits
815
+ azimuth={[-Math.PI / 4, Math.PI / 4]} // Horizontal limits
816
+ config={{ mass: 1, tension: 170, friction: 26 }}
817
+ >
818
+ <mesh>
819
+ <boxGeometry />
820
+ <meshStandardMaterial color="gold" />
821
+ </mesh>
822
+ </PresentationControls>
823
+ )
824
+ }
825
+ ```
826
+
827
+ ## Performance Tips
828
+
829
+ 1. **Stop propagation**: Prevent unnecessary raycasts
830
+ 2. **Use layers**: Filter raycast targets
831
+ 3. **Simpler collision meshes**: Use invisible simple geometry
832
+ 4. **Throttle events**: Limit onPointerMove frequency
833
+ 5. **Disable controls when not needed**: `enabled={false}`
834
+
835
+ ```tsx
836
+ // Use simpler geometry for raycasting
837
+ function OptimizedInteraction() {
838
+ return (
839
+ <group>
840
+ {/* Complex visible mesh */}
841
+ <mesh raycast={() => null}>
842
+ <torusKnotGeometry args={[1, 0.4, 100, 16]} />
843
+ <meshStandardMaterial color="purple" />
844
+ </mesh>
845
+
846
+ {/* Simple invisible collision mesh */}
847
+ <mesh onClick={() => console.log('clicked')}>
848
+ <sphereGeometry args={[1.5]} />
849
+ <meshBasicMaterial visible={false} />
850
+ </mesh>
851
+ </group>
852
+ )
853
+ }
854
+
855
+ // Throttle pointer move events
856
+ import { useMemo, useCallback } from 'react'
857
+ import throttle from 'lodash/throttle'
858
+
859
+ function ThrottledHover() {
860
+ const handleMove = useMemo(
861
+ () => throttle((e) => {
862
+ console.log('Move', e.point)
863
+ }, 100),
864
+ []
865
+ )
866
+
867
+ return (
868
+ <mesh onPointerMove={handleMove}>
869
+ <boxGeometry />
870
+ <meshStandardMaterial />
871
+ </mesh>
872
+ )
873
+ }
874
+ ```
875
+
876
+ ## See Also
877
+
878
+ - `r3f-fundamentals` - Canvas and scene setup
879
+ - `r3f-animation` - Animating interactions
880
+ - `r3f-postprocessing` - Visual feedback effects (outline, selection)