@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,877 @@
1
+ # React Three Fiber Fundamentals
2
+
3
+ ## Quick Start
4
+
5
+ ```tsx
6
+ import { Canvas } from '@react-three/fiber'
7
+ import { useRef } from 'react'
8
+ import { useFrame } from '@react-three/fiber'
9
+
10
+ function RotatingBox() {
11
+ const meshRef = useRef()
12
+
13
+ useFrame((state, delta) => {
14
+ meshRef.current.rotation.x += delta
15
+ meshRef.current.rotation.y += delta * 0.5
16
+ })
17
+
18
+ return (
19
+ <mesh ref={meshRef}>
20
+ <boxGeometry args={[1, 1, 1]} />
21
+ <meshStandardMaterial color="hotpink" />
22
+ </mesh>
23
+ )
24
+ }
25
+
26
+ export default function App() {
27
+ return (
28
+ <Canvas camera={{ position: [0, 0, 5], fov: 75 }}>
29
+ <ambientLight intensity={0.5} />
30
+ <directionalLight position={[5, 5, 5]} />
31
+ <RotatingBox />
32
+ </Canvas>
33
+ )
34
+ }
35
+ ```
36
+
37
+ ## Canvas Component
38
+
39
+ The root component that creates the WebGL context, scene, camera, and renderer.
40
+
41
+ ```tsx
42
+ import { Canvas } from '@react-three/fiber'
43
+
44
+ function App() {
45
+ return (
46
+ <Canvas
47
+ // Camera configuration
48
+ camera={{
49
+ position: [0, 5, 10],
50
+ fov: 75,
51
+ near: 0.1,
52
+ far: 1000,
53
+ }}
54
+ // Or use orthographic
55
+ orthographic
56
+ camera={{ zoom: 50, position: [0, 0, 100] }}
57
+
58
+ // Renderer settings
59
+ gl={{
60
+ antialias: true,
61
+ alpha: true,
62
+ powerPreference: 'high-performance',
63
+ preserveDrawingBuffer: true, // For screenshots
64
+ }}
65
+ dpr={[1, 2]} // Pixel ratio min/max
66
+
67
+ // Shadows
68
+ shadows // or shadows="soft" | "basic" | "percentage"
69
+
70
+ // Color management
71
+ flat // Disable automatic sRGB color management
72
+
73
+ // Frame loop control
74
+ frameloop="demand" // 'always' | 'demand' | 'never'
75
+
76
+ // Event handling
77
+ eventSource={document.getElementById('root')}
78
+ eventPrefix="client" // 'offset' | 'client' | 'page' | 'layer' | 'screen'
79
+
80
+ // Callbacks
81
+ onCreated={(state) => {
82
+ console.log('Canvas ready:', state.gl, state.scene, state.camera)
83
+ }}
84
+ onPointerMissed={() => console.log('Clicked background')}
85
+
86
+ // Styling
87
+ style={{ width: '100%', height: '100vh' }}
88
+ >
89
+ <Scene />
90
+ </Canvas>
91
+ )
92
+ }
93
+ ```
94
+
95
+ ### Canvas Defaults
96
+
97
+ R3F sets sensible defaults:
98
+
99
+ - Renderer: antialias, alpha, outputColorSpace = SRGBColorSpace
100
+ - Camera: PerspectiveCamera at [0, 0, 5]
101
+ - Scene: Automatic resize handling
102
+ - Events: Pointer events enabled
103
+
104
+ ## useFrame Hook
105
+
106
+ Subscribe to the render loop. Called every frame (typically 60fps).
107
+
108
+ ```tsx
109
+ import { useFrame } from '@react-three/fiber'
110
+ import { useRef } from 'react'
111
+
112
+ function AnimatedMesh() {
113
+ const meshRef = useRef()
114
+
115
+ useFrame((state, delta, xrFrame) => {
116
+ // state: Full R3F state (see useThree)
117
+ // delta: Time since last frame in seconds
118
+ // xrFrame: XR frame if in VR/AR mode
119
+
120
+ // Animate rotation
121
+ meshRef.current.rotation.y += delta
122
+
123
+ // Access clock
124
+ const elapsed = state.clock.elapsedTime
125
+ meshRef.current.position.y = Math.sin(elapsed) * 2
126
+
127
+ // Access pointer position (-1 to 1)
128
+ const { x, y } = state.pointer
129
+ meshRef.current.rotation.x = y * 0.5
130
+ meshRef.current.rotation.z = x * 0.5
131
+ })
132
+
133
+ return (
134
+ <mesh ref={meshRef}>
135
+ <boxGeometry />
136
+ <meshStandardMaterial color="orange" />
137
+ </mesh>
138
+ )
139
+ }
140
+ ```
141
+
142
+ ### useFrame with Priority
143
+
144
+ Control render order with priority (higher = later).
145
+
146
+ ```tsx
147
+ // Default priority is 0
148
+ useFrame((state, delta) => {
149
+ // Runs first
150
+ }, -1)
151
+
152
+ useFrame((state, delta) => {
153
+ // Runs after priority -1
154
+ }, 0)
155
+
156
+ // Manual rendering with positive priority
157
+ useFrame((state, delta) => {
158
+ // Take over rendering
159
+ state.gl.render(state.scene, state.camera)
160
+ }, 1)
161
+ ```
162
+
163
+ ### Conditional useFrame
164
+
165
+ ```tsx
166
+ function ConditionalAnimation({ active }) {
167
+ useFrame((state, delta) => {
168
+ if (!active) return // Skip when inactive
169
+ meshRef.current.rotation.y += delta
170
+ })
171
+ }
172
+ ```
173
+
174
+ ## useThree Hook
175
+
176
+ Access the R3F state store.
177
+
178
+ ```tsx
179
+ import { useThree } from '@react-three/fiber'
180
+
181
+ function CameraInfo() {
182
+ // Get full state (triggers re-render on any change)
183
+ const state = useThree()
184
+
185
+ // Selective subscription (recommended)
186
+ const camera = useThree((state) => state.camera)
187
+ const gl = useThree((state) => state.gl)
188
+ const scene = useThree((state) => state.scene)
189
+ const size = useThree((state) => state.size)
190
+
191
+ // Available state properties:
192
+ // gl: WebGLRenderer
193
+ // scene: Scene
194
+ // camera: Camera
195
+ // raycaster: Raycaster
196
+ // pointer: Vector2 (normalized -1 to 1)
197
+ // mouse: Vector2 (deprecated, use pointer)
198
+ // clock: Clock
199
+ // size: { width, height, top, left }
200
+ // viewport: { width, height, factor, distance, aspect }
201
+ // performance: { current, min, max, debounce, regress }
202
+ // events: Event handlers
203
+ // set: State setter
204
+ // get: State getter
205
+ // invalidate: Trigger re-render (for frameloop="demand")
206
+ // advance: Advance one frame (for frameloop="never")
207
+
208
+ return null
209
+ }
210
+ ```
211
+
212
+ ### Common useThree Patterns
213
+
214
+ ```tsx
215
+ // Responsive to viewport
216
+ function ResponsiveObject() {
217
+ const viewport = useThree((state) => state.viewport)
218
+ return (
219
+ <mesh scale={[viewport.width / 4, viewport.height / 4, 1]}>
220
+ <planeGeometry />
221
+ <meshBasicMaterial color="blue" />
222
+ </mesh>
223
+ )
224
+ }
225
+
226
+ // Manual render trigger
227
+ function TriggerRender() {
228
+ const invalidate = useThree((state) => state.invalidate)
229
+
230
+ const handleClick = () => {
231
+ // Trigger render when using frameloop="demand"
232
+ invalidate()
233
+ }
234
+ }
235
+
236
+ // Update camera
237
+ function CameraController() {
238
+ const camera = useThree((state) => state.camera)
239
+ const set = useThree((state) => state.set)
240
+
241
+ useEffect(() => {
242
+ camera.position.set(10, 10, 10)
243
+ camera.lookAt(0, 0, 0)
244
+ }, [camera])
245
+ }
246
+ ```
247
+
248
+ ## JSX Elements
249
+
250
+ All Three.js objects are available as JSX elements (camelCase).
251
+
252
+ ### Meshes
253
+
254
+ ```tsx
255
+ // Basic mesh structure
256
+ <mesh
257
+ position={[0, 0, 0]} // x, y, z
258
+ rotation={[0, Math.PI, 0]} // Euler angles in radians
259
+ scale={[1, 2, 1]} // x, y, z or single number
260
+ visible={true}
261
+ castShadow
262
+ receiveShadow
263
+ >
264
+ <boxGeometry args={[1, 1, 1]} />
265
+ <meshStandardMaterial color="red" />
266
+ </mesh>
267
+
268
+ // With ref
269
+ const meshRef = useRef()
270
+ <mesh ref={meshRef} />
271
+ // meshRef.current is the THREE.Mesh
272
+ ```
273
+
274
+ ### Geometry args
275
+
276
+ Constructor arguments via `args` prop:
277
+
278
+ ```tsx
279
+ // BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)
280
+ <boxGeometry args={[1, 1, 1, 1, 1, 1]} />
281
+
282
+ // SphereGeometry(radius, widthSegments, heightSegments)
283
+ <sphereGeometry args={[1, 32, 32]} />
284
+
285
+ // PlaneGeometry(width, height, widthSegments, heightSegments)
286
+ <planeGeometry args={[10, 10]} />
287
+
288
+ // CylinderGeometry(radiusTop, radiusBottom, height, radialSegments)
289
+ <cylinderGeometry args={[1, 1, 2, 32]} />
290
+ ```
291
+
292
+ ### Groups
293
+
294
+ ```tsx
295
+ <group position={[5, 0, 0]} rotation={[0, Math.PI / 4, 0]}>
296
+ <mesh position={[-1, 0, 0]}>
297
+ <boxGeometry />
298
+ <meshStandardMaterial color="red" />
299
+ </mesh>
300
+ <mesh position={[1, 0, 0]}>
301
+ <boxGeometry />
302
+ <meshStandardMaterial color="blue" />
303
+ </mesh>
304
+ </group>
305
+ ```
306
+
307
+ ### Nested Properties
308
+
309
+ Use dashes for nested properties:
310
+
311
+ ```tsx
312
+ <mesh
313
+ position-x={5}
314
+ rotation-y={Math.PI}
315
+ scale-z={2}
316
+ >
317
+ <meshStandardMaterial
318
+ color="red"
319
+ metalness={0.8}
320
+ roughness={0.2}
321
+ />
322
+ </mesh>
323
+
324
+ // Shadow camera properties
325
+ <directionalLight
326
+ castShadow
327
+ shadow-mapSize={[2048, 2048]}
328
+ shadow-camera-left={-10}
329
+ shadow-camera-right={10}
330
+ shadow-camera-top={10}
331
+ shadow-camera-bottom={-10}
332
+ />
333
+ ```
334
+
335
+ ### attach Prop
336
+
337
+ Control how children attach to parents:
338
+
339
+ ```tsx
340
+ <mesh>
341
+ <boxGeometry />
342
+ {/* Default: attaches as 'material' */}
343
+ <meshStandardMaterial />
344
+ </mesh>
345
+
346
+ {/* Explicit attach */}
347
+ <mesh>
348
+ <boxGeometry attach="geometry" />
349
+ <meshStandardMaterial attach="material" />
350
+ </mesh>
351
+
352
+ {/* Array attachment */}
353
+ <mesh>
354
+ <boxGeometry />
355
+ <meshStandardMaterial attach="material-0" color="red" />
356
+ <meshStandardMaterial attach="material-1" color="blue" />
357
+ </mesh>
358
+
359
+ {/* Custom attachment with function */}
360
+ <someObject>
361
+ <texture
362
+ attach={(parent, self) => {
363
+ parent.map = self
364
+ return () => { parent.map = null } // Cleanup
365
+ }}
366
+ />
367
+ </someObject>
368
+ ```
369
+
370
+ ## Event Handling
371
+
372
+ R3F provides React-style events on 3D objects.
373
+
374
+ ```tsx
375
+ function InteractiveBox() {
376
+ const [hovered, setHovered] = useState(false)
377
+ const [clicked, setClicked] = useState(false)
378
+
379
+ return (
380
+ <mesh
381
+ onClick={(e) => {
382
+ e.stopPropagation() // Prevent bubbling
383
+ setClicked(!clicked)
384
+
385
+ // Event properties:
386
+ console.log(e.object) // THREE.Mesh
387
+ console.log(e.point) // Vector3 - intersection point
388
+ console.log(e.distance) // Distance from camera
389
+ console.log(e.face) // Intersected face
390
+ console.log(e.faceIndex) // Face index
391
+ console.log(e.uv) // UV coordinates
392
+ console.log(e.normal) // Face normal
393
+ console.log(e.pointer) // Normalized pointer coords
394
+ console.log(e.ray) // Raycaster ray
395
+ console.log(e.camera) // Camera
396
+ console.log(e.delta) // Distance moved (drag events)
397
+ }}
398
+ onContextMenu={(e) => console.log('Right click')}
399
+ onDoubleClick={(e) => console.log('Double click')}
400
+ onPointerOver={(e) => {
401
+ e.stopPropagation()
402
+ setHovered(true)
403
+ document.body.style.cursor = 'pointer'
404
+ }}
405
+ onPointerOut={(e) => {
406
+ setHovered(false)
407
+ document.body.style.cursor = 'default'
408
+ }}
409
+ onPointerDown={(e) => console.log('Pointer down')}
410
+ onPointerUp={(e) => console.log('Pointer up')}
411
+ onPointerMove={(e) => console.log('Moving over mesh')}
412
+ onWheel={(e) => console.log('Wheel:', e.deltaY)}
413
+ scale={hovered ? 1.2 : 1}
414
+ >
415
+ <boxGeometry />
416
+ <meshStandardMaterial color={clicked ? 'hotpink' : 'orange'} />
417
+ </mesh>
418
+ )
419
+ }
420
+ ```
421
+
422
+ ### Event Propagation
423
+
424
+ Events bubble up through the scene graph:
425
+
426
+ ```tsx
427
+ <group onClick={(e) => console.log('Group clicked')}>
428
+ <mesh onClick={(e) => {
429
+ e.stopPropagation() // Stop bubbling to group
430
+ console.log('Mesh clicked')
431
+ }}>
432
+ <boxGeometry />
433
+ <meshStandardMaterial />
434
+ </mesh>
435
+ </group>
436
+ ```
437
+
438
+ ## primitive Element
439
+
440
+ Use existing Three.js objects directly:
441
+
442
+ ```tsx
443
+ import * as THREE from 'three'
444
+
445
+ // Existing object
446
+ const geometry = new THREE.BoxGeometry()
447
+ const material = new THREE.MeshStandardMaterial({ color: 'red' })
448
+ const mesh = new THREE.Mesh(geometry, material)
449
+
450
+ function Scene() {
451
+ return <primitive object={mesh} position={[0, 1, 0]} />
452
+ }
453
+
454
+ // Common with loaded models
455
+ function Model({ gltf }) {
456
+ return <primitive object={gltf.scene} />
457
+ }
458
+ ```
459
+
460
+ ## extend Function
461
+
462
+ Register custom Three.js classes for JSX use:
463
+
464
+ ```tsx
465
+ import { extend } from '@react-three/fiber'
466
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
467
+
468
+ // Extend once (usually at module level)
469
+ extend({ OrbitControls })
470
+
471
+ // Now use as JSX
472
+ function Scene() {
473
+ const { camera, gl } = useThree()
474
+ return <orbitControls args={[camera, gl.domElement]} />
475
+ }
476
+
477
+ // TypeScript declaration
478
+ declare global {
479
+ namespace JSX {
480
+ interface IntrinsicElements {
481
+ orbitControls: ReactThreeFiber.Object3DNode<OrbitControls, typeof OrbitControls>
482
+ }
483
+ }
484
+ }
485
+ ```
486
+
487
+ ## Refs and Imperative Access
488
+
489
+ ```tsx
490
+ import { useRef, useEffect } from 'react'
491
+ import { useFrame } from '@react-three/fiber'
492
+ import * as THREE from 'three'
493
+
494
+ function MeshWithRef() {
495
+ const meshRef = useRef<THREE.Mesh>(null)
496
+ const materialRef = useRef<THREE.MeshStandardMaterial>(null)
497
+
498
+ useEffect(() => {
499
+ if (meshRef.current) {
500
+ // Direct Three.js access
501
+ meshRef.current.geometry.computeBoundingBox()
502
+ console.log(meshRef.current.geometry.boundingBox)
503
+ }
504
+ }, [])
505
+
506
+ useFrame(() => {
507
+ if (materialRef.current) {
508
+ materialRef.current.color.setHSL(Math.random(), 1, 0.5)
509
+ }
510
+ })
511
+
512
+ return (
513
+ <mesh ref={meshRef}>
514
+ <boxGeometry />
515
+ <meshStandardMaterial ref={materialRef} />
516
+ </mesh>
517
+ )
518
+ }
519
+ ```
520
+
521
+ ## Performance Patterns
522
+
523
+ ### Avoiding Re-renders
524
+
525
+ ```tsx
526
+ // BAD: Creates new object every render
527
+ <mesh position={[x, y, z]} />
528
+
529
+ // GOOD: Mutate existing position
530
+ const meshRef = useRef()
531
+ useFrame(() => {
532
+ meshRef.current.position.x = x
533
+ })
534
+ <mesh ref={meshRef} />
535
+
536
+ // GOOD: Use useMemo for static values
537
+ const position = useMemo(() => [x, y, z], [x, y, z])
538
+ <mesh position={position} />
539
+ ```
540
+
541
+ ### Component Isolation
542
+
543
+ ```tsx
544
+ // Isolate animated components to prevent parent re-renders
545
+ function Scene() {
546
+ return (
547
+ <>
548
+ <StaticEnvironment />
549
+ <AnimatedObject /> {/* Only this re-renders on animation */}
550
+ </>
551
+ )
552
+ }
553
+
554
+ function AnimatedObject() {
555
+ const ref = useRef()
556
+ useFrame((_, delta) => {
557
+ ref.current.rotation.y += delta
558
+ })
559
+ return <mesh ref={ref}><boxGeometry /></mesh>
560
+ }
561
+ ```
562
+
563
+ ### Dispose
564
+
565
+ R3F auto-disposes geometries, materials, and textures. Override with:
566
+
567
+ ```tsx
568
+ <mesh dispose={null}> {/* Prevent auto-dispose */}
569
+ <boxGeometry />
570
+ <meshStandardMaterial />
571
+ </mesh>
572
+ ```
573
+
574
+ ## Common Patterns
575
+
576
+ ### Fullscreen Canvas
577
+
578
+ ```tsx
579
+ // styles.css
580
+ html, body, #root {
581
+ margin: 0;
582
+ padding: 0;
583
+ width: 100%;
584
+ height: 100%;
585
+ }
586
+
587
+ // App.tsx
588
+ <Canvas style={{ width: '100%', height: '100%' }}>
589
+ ```
590
+
591
+ ### Responsive Canvas
592
+
593
+ ```tsx
594
+ function ResponsiveScene() {
595
+ const { viewport } = useThree()
596
+
597
+ return (
598
+ <mesh scale={Math.min(viewport.width, viewport.height) / 5}>
599
+ <boxGeometry />
600
+ <meshStandardMaterial />
601
+ </mesh>
602
+ )
603
+ }
604
+ ```
605
+
606
+ ### Forwarding Refs
607
+
608
+ ```tsx
609
+ import { forwardRef } from 'react'
610
+
611
+ const CustomMesh = forwardRef((props, ref) => {
612
+ return (
613
+ <mesh ref={ref} {...props}>
614
+ <boxGeometry />
615
+ <meshStandardMaterial color="orange" />
616
+ </mesh>
617
+ )
618
+ })
619
+
620
+ // Usage
621
+ const meshRef = useRef()
622
+ <CustomMesh ref={meshRef} position={[0, 1, 0]} />
623
+ ```
624
+
625
+ ## Debugging with Leva
626
+
627
+ Leva provides a GUI for tweaking parameters in real-time during development.
628
+
629
+ ### Installation
630
+
631
+ ```bash
632
+ npm install leva
633
+ ```
634
+
635
+ ### Basic Controls
636
+
637
+ ```tsx
638
+ import { useControls } from 'leva'
639
+
640
+ function DebugMesh() {
641
+ const { position, color, scale, visible } = useControls({
642
+ position: { value: [0, 0, 0], step: 0.1 },
643
+ color: '#ff0000',
644
+ scale: { value: 1, min: 0.1, max: 5, step: 0.1 },
645
+ visible: true,
646
+ })
647
+
648
+ return (
649
+ <mesh position={position} scale={scale} visible={visible}>
650
+ <boxGeometry />
651
+ <meshStandardMaterial color={color} />
652
+ </mesh>
653
+ )
654
+ }
655
+ ```
656
+
657
+ ### Organized Folders
658
+
659
+ ```tsx
660
+ import { useControls, folder } from 'leva'
661
+
662
+ function DebugScene() {
663
+ const { lightIntensity, lightColor, shadowMapSize } = useControls({
664
+ Lighting: folder({
665
+ lightIntensity: { value: 1, min: 0, max: 5 },
666
+ lightColor: '#ffffff',
667
+ shadowMapSize: { value: 1024, options: [512, 1024, 2048, 4096] },
668
+ }),
669
+ Camera: folder({
670
+ fov: { value: 75, min: 30, max: 120 },
671
+ near: { value: 0.1, min: 0.01, max: 1 },
672
+ }),
673
+ })
674
+
675
+ return (
676
+ <directionalLight
677
+ intensity={lightIntensity}
678
+ color={lightColor}
679
+ shadow-mapSize={[shadowMapSize, shadowMapSize]}
680
+ />
681
+ )
682
+ }
683
+ ```
684
+
685
+ ### Button Actions
686
+
687
+ ```tsx
688
+ import { useControls, button } from 'leva'
689
+
690
+ function DebugActions() {
691
+ const meshRef = useRef()
692
+
693
+ useControls({
694
+ 'Reset Position': button(() => {
695
+ meshRef.current.position.set(0, 0, 0)
696
+ }),
697
+ 'Random Color': button(() => {
698
+ meshRef.current.material.color.setHex(Math.random() * 0xffffff)
699
+ }),
700
+ 'Log State': button(() => {
701
+ console.log(meshRef.current.position)
702
+ }),
703
+ })
704
+
705
+ return <mesh ref={meshRef}>...</mesh>
706
+ }
707
+ ```
708
+
709
+ ### Hide in Production
710
+
711
+ ```tsx
712
+ import { Leva } from 'leva'
713
+
714
+ function App() {
715
+ return (
716
+ <>
717
+ {/* Hide Leva panel in production */}
718
+ <Leva hidden={process.env.NODE_ENV === 'production'} />
719
+
720
+ <Canvas>
721
+ <Scene />
722
+ </Canvas>
723
+ </>
724
+ )
725
+ }
726
+ ```
727
+
728
+ ### Monitor Values (Read-Only)
729
+
730
+ ```tsx
731
+ import { useControls, monitor } from 'leva'
732
+ import { useFrame } from '@react-three/fiber'
733
+
734
+ function PerformanceMonitor() {
735
+ const [fps, setFps] = useState(0)
736
+
737
+ useControls({
738
+ FPS: monitor(() => fps, { graph: true, interval: 100 }),
739
+ })
740
+
741
+ useFrame((state) => {
742
+ // Update FPS display
743
+ setFps(Math.round(1 / state.clock.getDelta()))
744
+ })
745
+
746
+ return null
747
+ }
748
+ ```
749
+
750
+ ### Integration with useFrame
751
+
752
+ ```tsx
753
+ function AnimatedDebugMesh() {
754
+ const meshRef = useRef()
755
+
756
+ const { speed, amplitude, enabled } = useControls('Animation', {
757
+ enabled: true,
758
+ speed: { value: 1, min: 0, max: 5 },
759
+ amplitude: { value: 1, min: 0, max: 3 },
760
+ })
761
+
762
+ useFrame(({ clock }) => {
763
+ if (!enabled) return
764
+ meshRef.current.position.y = Math.sin(clock.elapsedTime * speed) * amplitude
765
+ })
766
+
767
+ return (
768
+ <mesh ref={meshRef}>
769
+ <sphereGeometry />
770
+ <meshStandardMaterial color="cyan" />
771
+ </mesh>
772
+ )
773
+ }
774
+ ```
775
+
776
+ ## 妙搭沙箱适配铁律 (React 19 + 严格模式)
777
+
778
+ 妙搭沙箱跑 React 19 + React Compiler 严格 lint。下列写法 commit 时会报错,且**运行时部分组件会被 React 跳过编译/重复挂载抛错 → Canvas 内部黑屏**。失败 case `app_4k7v39352vh9q` 12 次循环改 lint 没修对就是踩了这些坑。
779
+
780
+ ### 1. `Math.random()` / `Date.now()` 严禁 render 期间直接调用
781
+
782
+ ```tsx
783
+ // ❌ render 期间生成位置, React 19 报 "impure function during render"
784
+ {enemies.map((_, i) => <group position={[Math.random() * 10, 0, 0]}>...</group>)}
785
+
786
+ // ✅ useMemo 一次性算好, deps=[]
787
+ const enemyPositions = useMemo(
788
+ () => Array.from({ length: 8 }, () => [Math.random() * 10, 0, Math.random() * 10]),
789
+ []
790
+ )
791
+ {enemyPositions.map((pos, i) => <group key={i} position={pos}>...</group>)}
792
+ ```
793
+
794
+ ### 2. 动画 / 位置更新优先用 drei 高阶组件, 严禁 render 里读写 ref.current
795
+
796
+ ```tsx
797
+ // ❌ render 期间访问 ref.current — React 19 报 "Cannot access refs during render"
798
+ <mesh rotation={[ref.current?.rotation.y + 0.01 || 0, 0, 0]} />
799
+
800
+ // ❌ 自己写 useFrame 逐帧推位置 — 容易踩坑且代码冗长
801
+ function Earth() {
802
+ const ref = useRef<THREE.Mesh>(null)
803
+ useFrame(() => { if (ref.current) ref.current.rotation.y += 0.005 })
804
+ return <mesh ref={ref}>...</mesh>
805
+ }
806
+
807
+ // ✅ 优先用 drei 高阶组件让库帮你驱动动画
808
+ <OrbitControls autoRotate autoRotateSpeed={0.4} /> // 整个场景自动旋转
809
+ <Float speed={1.5} rotationIntensity={1} floatIntensity={2}> // 自动悬浮
810
+ <mesh>...</mesh>
811
+ </Float>
812
+
813
+ // ✅ 必须自己写 useFrame 时, ref 操作放 useFrame 内, 不进 render JSX
814
+ function Earth() {
815
+ const ref = useRef<THREE.Mesh>(null)
816
+ useFrame(() => { if (ref.current) ref.current.rotation.y += 0.005 })
817
+ return <mesh ref={ref}><sphereGeometry /><meshStandardMaterial /></mesh>
818
+ }
819
+ ```
820
+
821
+ ### 3. useEffect 里 setState 加 if 条件 / 用 setter 函数, 防级联渲染
822
+
823
+ ```tsx
824
+ // ❌ React 19 报 "Calling setState synchronously within an effect can trigger cascading renders"
825
+ useEffect(() => { setHealth(100) }, [gameState])
826
+
827
+ // ✅ 加条件或 setter 函数
828
+ useEffect(() => {
829
+ if (gameState === 'playing') {
830
+ setHealth((h) => (h > 0 ? h : 100))
831
+ }
832
+ }, [gameState])
833
+ ```
834
+
835
+ ### 4. Canvas 外必须包 ErrorBoundary, Suspense 不吞 error
836
+
837
+ `<Canvas>` 内部 mesh / geometry / material 抛 runtime error 时, **Suspense fallback 只吞 Promise (异步加载), 不吞 Error**. 没 ErrorBoundary → 整个 Canvas 黑屏, 控制台报错被吞, 用户看到只剩 HUD.
838
+
839
+ ```tsx
840
+ // ❌ 只有 Suspense, Canvas 内部错误 → 整片黑
841
+ <Canvas>
842
+ <Suspense fallback={null}>
843
+ <Scene />
844
+ </Suspense>
845
+ </Canvas>
846
+
847
+ // ✅ ErrorBoundary 包 Canvas, fallback 显示降级 UI 而不是黑屏
848
+ import { ErrorBoundary } from 'react-error-boundary'
849
+
850
+ <ErrorBoundary fallback={<div className="p-8 text-center">3D 场景加载失败, 请刷新重试</div>}>
851
+ <Canvas camera={{ position: [0, 0, 5] }}>
852
+ <Suspense fallback={null}>
853
+ <Scene />
854
+ </Suspense>
855
+ </Canvas>
856
+ </ErrorBoundary>
857
+ ```
858
+
859
+ ### 5. 黑屏必查清单 (调试 3D 场景空白时按顺序排查)
860
+
861
+ | 项 | 要求 |
862
+ |----|------|
863
+ | Canvas 父容器有显式高度 | `h-screen` / `h-[800px]`, **不要** `h-full` 链断 |
864
+ | 至少 1 个 `<ambientLight intensity≥0.3>` | 没灯 = 全黑 |
865
+ | `<directionalLight position={[5,5,5]} intensity≥0.5>` | 主光源 |
866
+ | Camera position 跟 mesh 有距离 (5-20 单位) | `<Canvas camera={{ position: [0, 0, 5] }}>` |
867
+ | 至少 1 个 mesh 在 `[0, 0, 0]` 附近 | 调试时放在原点确认 |
868
+ | 浏览器 Console 看有无红字 Error | 看 R3F 抛了什么(没 ErrorBoundary 时只有 Console 看得到) |
869
+
870
+ ## See Also
871
+
872
+ - `r3f-geometry` - Geometry creation
873
+ - `r3f-materials` - Material configuration
874
+ - `r3f-lighting` - Lights and shadows
875
+ - `r3f-interaction` - Controls and user input
876
+ - `r3f-animation` - 动画(useFrame / drei autoRotate / Float)
877
+ - `r3f-loaders` - GLTF 加载 + Suspense 模式