Skip to content

Commit 4abfa19

Browse files
committed
refactor(wrapEffect): split out of util.tsx, support ref as a prop
Pulls wrapEffect out of the general util grab-bag into its own module, and lets generated effect components accept ref as a regular prop (React 19) instead of having no ref support at all - previously none of the wrapEffect-based effects (Bloom, Vignette, HueSaturation, etc.) could be given a ref. Autofocus gets the same ref-as-prop treatment directly (it doesn't go through wrapEffect). Also fixes two related wrapEffect bugs: - The args memo fingerprinted props with JSON.stringify, which threw on circular references (textures, refs, and other three.js objects commonly hold one) - two independent reports (#333, #334) hit this on ordinary usage. Swapped in a cycle-safe stringify. - That fingerprint also called JSON.stringify's implicit toJSON, so THREE.Texture.toJSON() was re-encoding the whole image to a base64 data URL on every render just to compute a value that got discarded - measured ~39ms for a 512x512 texture. Fingerprinting now walks own properties directly and collapses typed arrays/buffers to an identity token instead. - ref itself wasn't destructured out of props, so it leaked into that same fingerprint; once mounted, ref.current pointed at the effect instance, so every render after the first fingerprinted a different value and silently rebuilt the instance in a loop. Adds a shared test harness (test-utils.tsx) used by this and later commits' test suites.
1 parent 6e145ce commit 4abfa19

27 files changed

Lines changed: 655 additions & 245 deletions

src/Selection.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
import * as THREE from 'three'
2-
import React, { createContext, useState, useContext, useEffect, useRef, useMemo } from 'react'
31
import { type ThreeElements } from '@react-three/fiber'
2+
import {
3+
createContext,
4+
useContext,
5+
useEffect,
6+
useMemo,
7+
useRef,
8+
useState,
9+
type Dispatch,
10+
type ReactNode,
11+
type SetStateAction,
12+
} from 'react'
13+
import { type Group, type Object3D } from 'three'
414

515
export type Api = {
6-
selected: THREE.Object3D[]
7-
select: React.Dispatch<React.SetStateAction<THREE.Object3D[]>>
16+
selected: Object3D[]
17+
select: Dispatch<SetStateAction<Object3D[]>>
818
enabled: boolean
919
}
1020
export type SelectApi = Omit<ThreeElements['group'], 'ref'> & {
@@ -13,19 +23,19 @@ export type SelectApi = Omit<ThreeElements['group'], 'ref'> & {
1323

1424
export const selectionContext = /* @__PURE__ */ createContext<Api | null>(null)
1525

16-
export function Selection({ children, enabled = true }: { enabled?: boolean; children: React.ReactNode }) {
17-
const [selected, select] = useState<THREE.Object3D[]>([])
26+
export function Selection({ children, enabled = true }: { enabled?: boolean; children: ReactNode }) {
27+
const [selected, select] = useState<Object3D[]>([])
1828
const value = useMemo(() => ({ selected, select, enabled }), [selected, select, enabled])
1929
return <selectionContext.Provider value={value}>{children}</selectionContext.Provider>
2030
}
2131

2232
export function Select({ enabled = false, children, ...props }: SelectApi) {
23-
const group = useRef<THREE.Group>(null!)
33+
const group = useRef<Group>(null!)
2434
const api = useContext(selectionContext)
2535
useEffect(() => {
2636
if (api && enabled) {
2737
let changed = false
28-
const current: THREE.Object3D[] = []
38+
const current: Object3D[] = []
2939
group.current.traverse((o) => {
3040
o.type === 'Mesh' && current.push(o)
3141
if (api.selected.indexOf(o) === -1) changed = true

src/effects/Autofocus.tsx

Lines changed: 123 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
1-
import * as THREE from 'three'
2-
import React, {
3-
useRef,
1+
import { createPortal, useFrame, useThree, type Vector3 as R3FVector3 } from '@react-three/fiber'
2+
import { easing } from 'maath'
3+
import { CopyPass, DepthOfFieldEffect, DepthPickingPass } from 'postprocessing'
4+
import {
5+
Ref,
6+
useCallback,
47
useContext,
5-
useState,
68
useEffect,
7-
useCallback,
8-
forwardRef,
99
useImperativeHandle,
10-
RefObject,
1110
useMemo,
11+
useRef,
12+
useState,
13+
type ComponentProps,
14+
type RefObject,
1215
} from 'react'
13-
import { useThree, useFrame, createPortal, type Vector3 } from '@react-three/fiber'
14-
import { CopyPass, DepthPickingPass, DepthOfFieldEffect } from 'postprocessing'
15-
import { easing } from 'maath'
16+
import { Mesh, Vector3 } from 'three'
1617

17-
import { DepthOfField } from './DepthOfField'
1818
import { EffectComposerContext } from '../EffectComposer'
19+
import { DepthOfField } from './DepthOfField'
1920

20-
export type AutofocusProps = React.ComponentProps<typeof DepthOfField> & {
21-
target?: Vector3
21+
export type AutofocusProps = ComponentProps<typeof DepthOfField> & {
22+
target?: R3FVector3
2223
/** should the target follow the pointer */
2324
mouse?: boolean
2425
/** size of the debug green point */
@@ -27,127 +28,131 @@ export type AutofocusProps = React.ComponentProps<typeof DepthOfField> & {
2728
manual?: boolean
2829
/** approximate time to reach the target */
2930
smoothTime?: number
31+
ref?: Ref<AutofocusApi>
3032
}
3133

3234
export type AutofocusApi = {
3335
dofRef: RefObject<DepthOfFieldEffect | null>
34-
hitpoint: THREE.Vector3
36+
hitpoint: Vector3
3537
update: (delta: number, updateTarget: boolean) => void
3638
}
3739

38-
export const Autofocus = /* @__PURE__ */ forwardRef<AutofocusApi, AutofocusProps>(
39-
(
40-
{ target = undefined, mouse: followMouse = false, debug = undefined, manual = false, smoothTime = 0.25, ...props },
41-
fref
42-
) => {
43-
const dofRef = useRef<DepthOfFieldEffect>(null)
44-
const hitpointRef = useRef<THREE.Mesh>(null)
45-
const targetRef = useRef<THREE.Mesh>(null)
40+
export function Autofocus({
41+
target = undefined,
42+
mouse: followMouse = false,
43+
debug = undefined,
44+
manual = false,
45+
smoothTime = 0.25,
46+
ref,
47+
...props
48+
}: AutofocusProps) {
49+
const dofRef = useRef<DepthOfFieldEffect>(null)
50+
const hitpointRef = useRef<Mesh>(null)
51+
const targetRef = useRef<Mesh>(null)
4652

47-
const scene = useThree(({ scene }) => scene)
48-
const pointer = useThree(({ pointer }) => pointer)
49-
const { composer, camera } = useContext(EffectComposerContext)
53+
const scene = useThree(({ scene }) => scene)
54+
const pointer = useThree(({ pointer }) => pointer)
55+
const { composer, camera } = useContext(EffectComposerContext)
5056

51-
// see: https://codesandbox.io/s/depthpickingpass-x130hg
52-
const [depthPickingPass] = useState(() => new DepthPickingPass())
53-
const [copyPass] = useState(() => new CopyPass())
54-
useEffect(() => {
55-
composer.addPass(depthPickingPass)
56-
composer.addPass(copyPass)
57-
return () => {
58-
composer.removePass(depthPickingPass)
59-
composer.removePass(copyPass)
60-
}
61-
}, [composer, depthPickingPass, copyPass])
57+
// see: https://codesandbox.io/s/depthpickingpass-x130hg
58+
const [depthPickingPass] = useState(() => new DepthPickingPass())
59+
const [copyPass] = useState(() => new CopyPass())
60+
useEffect(() => {
61+
composer.addPass(depthPickingPass)
62+
composer.addPass(copyPass)
63+
return () => {
64+
composer.removePass(depthPickingPass)
65+
composer.removePass(copyPass)
66+
}
67+
}, [composer, depthPickingPass, copyPass])
6268

63-
useEffect(() => {
64-
return () => {
65-
depthPickingPass.dispose()
66-
copyPass.dispose()
67-
}
68-
}, [depthPickingPass, copyPass])
69+
useEffect(() => {
70+
return () => {
71+
depthPickingPass.dispose()
72+
copyPass.dispose()
73+
}
74+
}, [depthPickingPass, copyPass])
6975

70-
const [hitpoint] = useState(() => new THREE.Vector3(0, 0, 0))
76+
const [hitpoint] = useState(() => new Vector3(0, 0, 0))
7177

72-
const [ndc] = useState(() => new THREE.Vector3(0, 0, 0))
73-
const getHit = useCallback(
74-
async (x: number, y: number) => {
75-
ndc.x = x
76-
ndc.y = y
77-
ndc.z = await depthPickingPass.readDepth(ndc)
78-
ndc.z = ndc.z * 2.0 - 1.0
79-
const hit = 1 - ndc.z > 0.0000001 // it is missed if ndc.z is close to 1
80-
return hit ? ndc.unproject(camera) : false
81-
},
82-
[ndc, depthPickingPass, camera]
83-
)
78+
const [ndc] = useState(() => new Vector3(0, 0, 0))
79+
const getHit = useCallback(
80+
async (x: number, y: number) => {
81+
ndc.x = x
82+
ndc.y = y
83+
ndc.z = await depthPickingPass.readDepth(ndc)
84+
ndc.z = ndc.z * 2.0 - 1.0
85+
const hit = 1 - ndc.z > 0.0000001 // it is missed if ndc.z is close to 1
86+
return hit ? ndc.unproject(camera) : false
87+
},
88+
[ndc, depthPickingPass, camera]
89+
)
8490

85-
const update = useCallback(
86-
async (delta: number, updateTarget = true) => {
87-
// Update hitpoint
88-
if (target) {
89-
hitpoint.set(...(target as [number, number, number]))
90-
} else {
91-
const { x, y } = followMouse ? pointer : { x: 0, y: 0 }
92-
const hit = await getHit(x, y)
93-
if (hit) hitpoint.copy(hit)
94-
}
91+
const update = useCallback(
92+
async (delta: number, updateTarget = true) => {
93+
// Update hitpoint
94+
if (target) {
95+
hitpoint.set(...(target as unknown as [number, number, number]))
96+
} else {
97+
const { x, y } = followMouse ? pointer : { x: 0, y: 0 }
98+
const hit = await getHit(x, y)
99+
if (hit) hitpoint.copy(hit)
100+
}
95101

96-
// Update target
97-
if (updateTarget && dofRef.current?.target) {
98-
if (smoothTime > 0 && delta > 0) {
99-
easing.damp3(dofRef.current.target, hitpoint, smoothTime, delta)
100-
} else {
101-
dofRef.current.target.copy(hitpoint)
102-
}
102+
// Update target
103+
if (updateTarget && dofRef.current?.target) {
104+
if (smoothTime > 0 && delta > 0) {
105+
easing.damp3(dofRef.current.target, hitpoint, smoothTime, delta)
106+
} else {
107+
dofRef.current.target.copy(hitpoint)
103108
}
104-
},
105-
[target, hitpoint, followMouse, getHit, smoothTime, pointer]
106-
)
107-
108-
useFrame(async (_, delta) => {
109-
if (!manual) {
110-
update(delta)
111-
}
112-
if (hitpointRef.current) {
113-
hitpointRef.current.position.copy(hitpoint)
114-
}
115-
if (targetRef.current && dofRef.current?.target) {
116-
targetRef.current.position.copy(dofRef.current.target)
117109
}
118-
})
110+
},
111+
[target, hitpoint, followMouse, getHit, smoothTime, pointer]
112+
)
119113

120-
// Ref API
121-
const api = useMemo<AutofocusApi>(
122-
() => ({
123-
dofRef,
124-
hitpoint,
125-
update,
126-
}),
127-
[hitpoint, update]
128-
)
129-
useImperativeHandle(fref, () => api, [api])
114+
useFrame(async (_, delta) => {
115+
if (!manual) {
116+
update(delta)
117+
}
118+
if (hitpointRef.current) {
119+
hitpointRef.current.position.copy(hitpoint)
120+
}
121+
if (targetRef.current && dofRef.current?.target) {
122+
targetRef.current.position.copy(dofRef.current.target)
123+
}
124+
})
130125

131-
return (
132-
<>
133-
{debug
134-
? createPortal(
135-
<>
136-
<mesh ref={hitpointRef}>
137-
<sphereGeometry args={[debug, 16, 16]} />
138-
<meshBasicMaterial color="#00ff00" opacity={1} transparent depthWrite={false} />
139-
</mesh>
140-
<mesh ref={targetRef}>
141-
<sphereGeometry args={[debug / 2, 16, 16]} />
142-
<meshBasicMaterial color="#00ff00" opacity={0.5} transparent depthWrite={false} />
143-
</mesh>
144-
</>,
145-
scene
146-
)
147-
: null}
126+
// Ref API
127+
const api = useMemo<AutofocusApi>(
128+
() => ({
129+
dofRef,
130+
hitpoint,
131+
update,
132+
}),
133+
[hitpoint, update]
134+
)
135+
useImperativeHandle(ref, () => api, [api])
148136

149-
<DepthOfField ref={dofRef} {...props} target={hitpoint} />
150-
</>
151-
)
152-
}
153-
)
137+
return (
138+
<>
139+
{debug
140+
? createPortal(
141+
<>
142+
<mesh ref={hitpointRef}>
143+
<sphereGeometry args={[debug, 16, 16]} />
144+
<meshBasicMaterial color="#00ff00" opacity={1} transparent depthWrite={false} />
145+
</mesh>
146+
<mesh ref={targetRef}>
147+
<sphereGeometry args={[debug / 2, 16, 16]} />
148+
<meshBasicMaterial color="#00ff00" opacity={0.5} transparent depthWrite={false} />
149+
</mesh>
150+
</>,
151+
scene
152+
)
153+
: null}
154+
155+
<DepthOfField ref={dofRef} {...props} target={hitpoint} />
156+
</>
157+
)
158+
}

src/effects/Bloom.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { BloomEffect, BlendFunction } from 'postprocessing'
2-
import { wrapEffect } from '../util'
1+
import { BlendFunction, BloomEffect } from 'postprocessing'
2+
import { wrapEffect } from '../wrapEffect'
33

44
export const Bloom = /* @__PURE__ */ wrapEffect(BloomEffect, {
55
blendFunction: BlendFunction.ADD,

src/effects/BrightnessContrast.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import { BrightnessContrastEffect } from 'postprocessing'
2-
import { wrapEffect } from '../util'
2+
import { wrapEffect } from '../wrapEffect'
33

44
export const BrightnessContrast = /* @__PURE__ */ wrapEffect(BrightnessContrastEffect)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ChromaticAberrationEffect } from 'postprocessing'
2-
import { type EffectProps, wrapEffect } from '../util'
2+
import { type EffectProps, wrapEffect } from '../wrapEffect'
33

44
export type ChromaticAberrationProps = EffectProps<typeof ChromaticAberrationEffect>
55
export const ChromaticAberration = /* @__PURE__ */ wrapEffect(ChromaticAberrationEffect)

src/effects/ColorDepth.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import { ColorDepthEffect } from 'postprocessing'
2-
import { wrapEffect } from '../util'
2+
import { wrapEffect } from '../wrapEffect'
33

44
export const ColorDepth = /* @__PURE__ */ wrapEffect(ColorDepthEffect)

src/effects/Depth.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import { DepthEffect } from 'postprocessing'
2-
import { wrapEffect } from '../util'
2+
import { wrapEffect } from '../wrapEffect'
33

44
export const Depth = /* @__PURE__ */ wrapEffect(DepthEffect)

src/effects/DotScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import { DotScreenEffect } from 'postprocessing'
2-
import { wrapEffect } from '../util'
2+
import { wrapEffect } from '../wrapEffect'
33

44
export const DotScreen = /* @__PURE__ */ wrapEffect(DotScreenEffect)

src/effects/FXAA.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import { FXAAEffect } from 'postprocessing'
2-
import { wrapEffect } from '../util'
2+
import { wrapEffect } from '../wrapEffect'
33

44
export const FXAA = /* @__PURE__ */ wrapEffect(FXAAEffect)

src/effects/HueSaturation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import { HueSaturationEffect } from 'postprocessing'
2-
import { wrapEffect } from '../util'
2+
import { wrapEffect } from '../wrapEffect'
33

44
export const HueSaturation = /* @__PURE__ */ wrapEffect(HueSaturationEffect)

0 commit comments

Comments
 (0)