@react-three/postprocessing 3.0.3 → 3.0.5
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.
- package/LICENSE +21 -21
- package/README.md +81 -81
- package/dist/EffectComposer.d.ts +28 -0
- package/dist/Selection.d.ts +17 -0
- package/dist/effects/ASCII.d.ts +19 -0
- package/dist/effects/Autofocus.d.ts +23 -0
- package/dist/effects/Bloom.d.ts +2 -0
- package/dist/effects/BrightnessContrast.d.ts +2 -0
- package/dist/effects/ChromaticAberration.d.ts +8 -0
- package/dist/effects/ColorAverage.d.ts +7 -0
- package/dist/effects/ColorDepth.d.ts +2 -0
- package/dist/effects/Depth.d.ts +2 -0
- package/dist/effects/DepthOfField.d.ts +14 -0
- package/dist/effects/DotScreen.d.ts +2 -0
- package/dist/effects/FXAA.d.ts +2 -0
- package/dist/effects/Glitch.d.ts +13 -0
- package/dist/effects/GodRays.d.ts +9 -0
- package/dist/effects/Grid.d.ts +11 -0
- package/dist/effects/HueSaturation.d.ts +2 -0
- package/dist/effects/LUT.d.ts +10 -0
- package/dist/effects/LensFlare.d.ts +54 -0
- package/dist/effects/N8AO.d.ts +19 -0
- package/dist/effects/Noise.d.ts +2 -0
- package/dist/effects/Outline.d.ts +11 -0
- package/dist/effects/Pixelation.d.ts +7 -0
- package/dist/effects/Ramp.d.ts +74 -0
- package/dist/effects/SMAA.d.ts +2 -0
- package/dist/effects/SSAO.d.ts +7 -0
- package/dist/effects/ScanlineEffect.d.ts +2 -0
- package/dist/effects/SelectiveBloom.d.ts +15 -0
- package/dist/effects/Sepia.d.ts +2 -0
- package/dist/effects/ShockWave.d.ts +2 -0
- package/dist/effects/Texture.d.ts +10 -0
- package/dist/effects/TiltShift.d.ts +2 -0
- package/dist/effects/TiltShift2.d.ts +18 -0
- package/dist/effects/ToneMapping.d.ts +4 -0
- package/dist/effects/Vignette.d.ts +2 -0
- package/dist/effects/Water.d.ts +8 -0
- package/dist/index.d.ts +38 -1
- package/dist/index.js +855 -551
- package/dist/index.js.map +1 -1
- package/dist/tests/test-utils.d.ts +12 -0
- package/dist/util.d.ts +25 -0
- package/dist/wrapEffect.d.ts +12 -0
- package/package.json +73 -66
- package/src/EffectComposer.tsx +278 -200
- package/src/Selection.tsx +86 -46
- package/src/effects/ASCII.tsx +136 -135
- package/src/effects/Autofocus.tsx +175 -153
- package/src/effects/Bloom.tsx +6 -6
- package/src/effects/BrightnessContrast.tsx +4 -4
- package/src/effects/ChromaticAberration.tsx +30 -5
- package/src/effects/ColorAverage.tsx +17 -15
- package/src/effects/ColorDepth.tsx +4 -4
- package/src/effects/Depth.tsx +4 -4
- package/src/effects/DepthOfField.tsx +87 -89
- package/src/effects/DotScreen.tsx +4 -4
- package/src/effects/FXAA.tsx +4 -4
- package/src/effects/Glitch.tsx +37 -40
- package/src/effects/GodRays.tsx +20 -16
- package/src/effects/Grid.tsx +28 -21
- package/src/effects/HueSaturation.tsx +4 -4
- package/src/effects/LUT.tsx +27 -26
- package/src/effects/LensFlare.tsx +611 -611
- package/src/effects/N8AO.tsx +81 -81
- package/src/effects/Noise.tsx +4 -4
- package/src/effects/Outline.tsx +85 -117
- package/src/effects/Pixelation.tsx +17 -15
- package/src/effects/Ramp.tsx +150 -150
- package/src/effects/SMAA.tsx +4 -4
- package/src/effects/SSAO.tsx +44 -41
- package/src/effects/ScanlineEffect.tsx +7 -7
- package/src/effects/SelectiveBloom.tsx +116 -126
- package/src/effects/Sepia.tsx +4 -4
- package/src/effects/ShockWave.tsx +4 -4
- package/src/effects/Texture.tsx +27 -23
- package/src/effects/TiltShift.tsx +4 -4
- package/src/effects/TiltShift2.tsx +90 -90
- package/src/effects/ToneMapping.tsx +6 -6
- package/src/effects/Vignette.tsx +4 -4
- package/src/effects/Water.tsx +35 -35
- package/src/index.ts +41 -40
- package/src/tests/ChromaticAberration.test.tsx +31 -0
- package/src/tests/EffectComposer.test.tsx +911 -0
- package/src/tests/Outline.test.tsx +89 -0
- package/src/tests/Selection.test.tsx +324 -0
- package/src/tests/SelectiveBloom.test.tsx +118 -0
- package/src/tests/effects.smoke.test.tsx +234 -0
- package/src/tests/test-utils.tsx +96 -0
- package/src/tests/wrapEffect.test.tsx +209 -0
- package/src/util.tsx +96 -55
- package/src/wrapEffect.tsx +122 -0
- package/src/EffectComposer.test.tsx +0 -126
package/src/EffectComposer.tsx
CHANGED
|
@@ -1,200 +1,278 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from '
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
)
|
|
1
|
+
import { useFrame, useThree, type Instance } from '@react-three/fiber'
|
|
2
|
+
import {
|
|
3
|
+
DepthDownsamplingPass,
|
|
4
|
+
Effect,
|
|
5
|
+
EffectAttribute,
|
|
6
|
+
EffectComposer as EffectComposerImpl,
|
|
7
|
+
EffectPass,
|
|
8
|
+
NormalPass,
|
|
9
|
+
Pass,
|
|
10
|
+
RenderPass,
|
|
11
|
+
} from 'postprocessing'
|
|
12
|
+
import {
|
|
13
|
+
createContext,
|
|
14
|
+
memo,
|
|
15
|
+
useEffect,
|
|
16
|
+
useImperativeHandle,
|
|
17
|
+
useLayoutEffect,
|
|
18
|
+
useMemo,
|
|
19
|
+
useRef,
|
|
20
|
+
useState,
|
|
21
|
+
type ReactNode,
|
|
22
|
+
type Ref,
|
|
23
|
+
} from 'react'
|
|
24
|
+
import type { Camera, Group, Scene, TextureDataType, WebGLRenderer } from 'three'
|
|
25
|
+
import { HalfFloatType, NoToneMapping } from 'three'
|
|
26
|
+
|
|
27
|
+
export const EffectComposerContext = /* @__PURE__ */ createContext<{
|
|
28
|
+
composer: EffectComposerImpl
|
|
29
|
+
normalPass: NormalPass | null
|
|
30
|
+
downSamplingPass: DepthDownsamplingPass | null
|
|
31
|
+
camera: Camera
|
|
32
|
+
scene: Scene
|
|
33
|
+
resolutionScale?: number
|
|
34
|
+
}>(null!)
|
|
35
|
+
|
|
36
|
+
export type EffectComposerProps = {
|
|
37
|
+
enabled?: boolean
|
|
38
|
+
children: ReactNode
|
|
39
|
+
depthBuffer?: boolean
|
|
40
|
+
/** Only used for SSGI currently, leave it disabled for everything else unless it's needed */
|
|
41
|
+
enableNormalPass?: boolean
|
|
42
|
+
stencilBuffer?: boolean
|
|
43
|
+
autoClear?: boolean
|
|
44
|
+
resolutionScale?: number
|
|
45
|
+
multisampling?: number
|
|
46
|
+
frameBufferType?: TextureDataType
|
|
47
|
+
renderPriority?: number
|
|
48
|
+
camera?: Camera
|
|
49
|
+
scene?: Scene
|
|
50
|
+
ref?: Ref<EffectComposerImpl>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type ComposerState = {
|
|
54
|
+
composer: EffectComposerImpl
|
|
55
|
+
normalPass: NormalPass | null
|
|
56
|
+
downSamplingPass: DepthDownsamplingPass | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isConvolution = (effect: Effect): boolean =>
|
|
60
|
+
(effect.getAttributes() & EffectAttribute.CONVOLUTION) === EffectAttribute.CONVOLUTION
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* autoClear/toneMapping get force-set and never restored by whoever sets
|
|
64
|
+
* them. Ref-counted per (renderer, property) since composers can share a
|
|
65
|
+
* renderer; skips restoring if the value already changed since acquire.
|
|
66
|
+
*/
|
|
67
|
+
function createRendererPropertyGuard<K extends 'autoClear' | 'toneMapping'>(property: K) {
|
|
68
|
+
const refs = new WeakMap<
|
|
69
|
+
WebGLRenderer,
|
|
70
|
+
{ count: number; original: WebGLRenderer[K]; forcedValue: WebGLRenderer[K] }
|
|
71
|
+
>()
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
acquire(gl: WebGLRenderer, forcedValue: WebGLRenderer[K]): void {
|
|
75
|
+
const existing = refs.get(gl)
|
|
76
|
+
if (existing) {
|
|
77
|
+
existing.count++
|
|
78
|
+
existing.forcedValue = forcedValue
|
|
79
|
+
} else {
|
|
80
|
+
refs.set(gl, { count: 1, original: gl[property], forcedValue })
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
release(gl: WebGLRenderer): void {
|
|
84
|
+
const entry = refs.get(gl)
|
|
85
|
+
if (!entry) return
|
|
86
|
+
|
|
87
|
+
if (--entry.count <= 0) {
|
|
88
|
+
if (gl[property] === entry.forcedValue) {
|
|
89
|
+
gl[property] = entry.original
|
|
90
|
+
}
|
|
91
|
+
refs.delete(gl)
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const autoClearGuard = /* @__PURE__ */ createRendererPropertyGuard('autoClear')
|
|
98
|
+
const toneMappingGuard = /* @__PURE__ */ createRendererPropertyGuard('toneMapping')
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Groups a flat, ordered list of Effect/Pass instances into actual composer
|
|
102
|
+
* passes, merging consecutive non-convolution Effects into a single
|
|
103
|
+
* EffectPass.
|
|
104
|
+
*/
|
|
105
|
+
function buildPasses(nodes: Array<Effect | Pass>, camera: Camera): Pass[] {
|
|
106
|
+
const passes: Pass[] = []
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
109
|
+
const node = nodes[i]
|
|
110
|
+
|
|
111
|
+
if (node instanceof Effect) {
|
|
112
|
+
const effects: Effect[] = [node]
|
|
113
|
+
|
|
114
|
+
if (!isConvolution(node)) {
|
|
115
|
+
let next: Effect | Pass | undefined
|
|
116
|
+
while ((next = nodes[i + 1]) instanceof Effect) {
|
|
117
|
+
if (isConvolution(next)) break
|
|
118
|
+
effects.push(next)
|
|
119
|
+
i++
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
passes.push(new EffectPass(camera, ...effects))
|
|
124
|
+
} else if (node instanceof Pass) {
|
|
125
|
+
passes.push(node)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return passes
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const EffectComposer = /* @__PURE__ */ memo(function EffectComposer({
|
|
133
|
+
children,
|
|
134
|
+
camera: _camera,
|
|
135
|
+
scene: _scene,
|
|
136
|
+
resolutionScale,
|
|
137
|
+
enabled = true,
|
|
138
|
+
renderPriority = 1,
|
|
139
|
+
autoClear = true,
|
|
140
|
+
depthBuffer,
|
|
141
|
+
enableNormalPass,
|
|
142
|
+
stencilBuffer,
|
|
143
|
+
multisampling = 8,
|
|
144
|
+
frameBufferType = HalfFloatType,
|
|
145
|
+
ref,
|
|
146
|
+
}: EffectComposerProps) {
|
|
147
|
+
const { gl, scene: defaultScene, camera: defaultCamera, size } = useThree()
|
|
148
|
+
const scene = _scene || defaultScene
|
|
149
|
+
const camera = _camera || defaultCamera
|
|
150
|
+
|
|
151
|
+
// EffectComposer owns WebGL resources, so it must be created and
|
|
152
|
+
// disposed inside an effect lifecycle. useMemo is not suitable here
|
|
153
|
+
// because React may discard memoized values without running cleanup.
|
|
154
|
+
const [composerState, setComposerState] = useState<ComposerState | null>(null)
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
autoClearGuard.acquire(gl, false)
|
|
158
|
+
|
|
159
|
+
const effectComposer = new EffectComposerImpl(gl, { depthBuffer, stencilBuffer, multisampling, frameBufferType })
|
|
160
|
+
effectComposer.addPass(new RenderPass(scene, camera))
|
|
161
|
+
|
|
162
|
+
let normalPass: NormalPass | null = null
|
|
163
|
+
let downSamplingPass: DepthDownsamplingPass | null = null
|
|
164
|
+
|
|
165
|
+
if (enableNormalPass) {
|
|
166
|
+
normalPass = new NormalPass(scene, camera)
|
|
167
|
+
normalPass.enabled = false
|
|
168
|
+
effectComposer.addPass(normalPass)
|
|
169
|
+
|
|
170
|
+
if (resolutionScale !== undefined) {
|
|
171
|
+
downSamplingPass = new DepthDownsamplingPass({ normalBuffer: normalPass.texture, resolutionScale })
|
|
172
|
+
downSamplingPass.enabled = false
|
|
173
|
+
effectComposer.addPass(downSamplingPass)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
effectComposer.setSize(size.width, size.height)
|
|
178
|
+
|
|
179
|
+
setComposerState({ composer: effectComposer, normalPass, downSamplingPass })
|
|
180
|
+
|
|
181
|
+
return () => {
|
|
182
|
+
effectComposer.dispose()
|
|
183
|
+
autoClearGuard.release(gl)
|
|
184
|
+
}
|
|
185
|
+
// `size` intentionally excluded: it's applied via the composer.setSize
|
|
186
|
+
// effect below, and shouldn't tear down/recreate the whole composer.
|
|
187
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
188
|
+
}, [camera, gl, depthBuffer, stencilBuffer, multisampling, frameBufferType, scene, enableNormalPass, resolutionScale])
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
composerState?.composer.setSize(size.width, size.height)
|
|
192
|
+
}, [composerState, size])
|
|
193
|
+
|
|
194
|
+
useFrame(
|
|
195
|
+
(_, delta) => {
|
|
196
|
+
if (!enabled || !composerState) return
|
|
197
|
+
const { composer } = composerState
|
|
198
|
+
const currentAutoClear = gl.autoClear
|
|
199
|
+
gl.autoClear = autoClear
|
|
200
|
+
if (stencilBuffer && !autoClear) gl.clearStencil()
|
|
201
|
+
composer.render(delta)
|
|
202
|
+
gl.autoClear = currentAutoClear
|
|
203
|
+
},
|
|
204
|
+
enabled ? renderPriority : 0
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
// Passes are derived from the actual r3f scene graph rather than tracked
|
|
208
|
+
// incrementally, so the list always matches current JSX order — including
|
|
209
|
+
// through wrapper components — even after a reorder or a remount.
|
|
210
|
+
const group = useRef<Group>(null!)
|
|
211
|
+
|
|
212
|
+
useLayoutEffect(() => {
|
|
213
|
+
if (!composerState) return
|
|
214
|
+
const { composer, normalPass, downSamplingPass } = composerState
|
|
215
|
+
|
|
216
|
+
const passes: Pass[] = []
|
|
217
|
+
const groupInstance = (group.current as Group & { __r3f: Instance<Group> }).__r3f
|
|
218
|
+
|
|
219
|
+
if (groupInstance) {
|
|
220
|
+
const nodes = groupInstance.children.map((child) => child.object).filter(
|
|
221
|
+
(object): object is Effect | Pass => object instanceof Effect || object instanceof Pass
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
passes.push(...buildPasses(nodes, camera))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const pass of passes) composer.addPass(pass)
|
|
228
|
+
|
|
229
|
+
if (passes.length) {
|
|
230
|
+
if (normalPass) normalPass.enabled = true
|
|
231
|
+
if (downSamplingPass) downSamplingPass.enabled = true
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return () => {
|
|
235
|
+
for (const pass of passes) composer.removePass(pass)
|
|
236
|
+
if (normalPass) normalPass.enabled = false
|
|
237
|
+
if (downSamplingPass) downSamplingPass.enabled = false
|
|
238
|
+
}
|
|
239
|
+
}, [composerState, children, camera])
|
|
240
|
+
|
|
241
|
+
// Disable tone mapping because threejs disallows tonemapping on render targets
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
toneMappingGuard.acquire(gl, NoToneMapping)
|
|
244
|
+
gl.toneMapping = NoToneMapping
|
|
245
|
+
return () => {
|
|
246
|
+
toneMappingGuard.release(gl)
|
|
247
|
+
}
|
|
248
|
+
}, [gl])
|
|
249
|
+
|
|
250
|
+
// Memoize state, otherwise it would trigger all consumers on every render
|
|
251
|
+
const state = useMemo(
|
|
252
|
+
() =>
|
|
253
|
+
composerState
|
|
254
|
+
? {
|
|
255
|
+
composer: composerState.composer,
|
|
256
|
+
normalPass: composerState.normalPass,
|
|
257
|
+
downSamplingPass: composerState.downSamplingPass,
|
|
258
|
+
resolutionScale,
|
|
259
|
+
camera,
|
|
260
|
+
scene,
|
|
261
|
+
}
|
|
262
|
+
: null,
|
|
263
|
+
[composerState, resolutionScale, camera, scene]
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
// Expose the composer
|
|
267
|
+
useImperativeHandle(ref, () => composerState?.composer as EffectComposerImpl, [composerState])
|
|
268
|
+
|
|
269
|
+
// Wait until the composer exists before mounting children so they always
|
|
270
|
+
// see a valid composer instance via context.
|
|
271
|
+
if (!state) return null
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<EffectComposerContext.Provider value={state}>
|
|
275
|
+
<group ref={group}>{children}</group>
|
|
276
|
+
</EffectComposerContext.Provider>
|
|
277
|
+
)
|
|
278
|
+
})
|
package/src/Selection.tsx
CHANGED
|
@@ -1,46 +1,86 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
1
|
+
import { type ThreeElements } from '@react-three/fiber'
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
use,
|
|
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 Line, type Mesh, type Object3D, type Points } from 'three'
|
|
14
|
+
|
|
15
|
+
export type Api = {
|
|
16
|
+
selected: Object3D[]
|
|
17
|
+
select: Dispatch<SetStateAction<Object3D[]>>
|
|
18
|
+
enabled: boolean
|
|
19
|
+
}
|
|
20
|
+
export type SelectApi = Omit<ThreeElements['group'], 'ref'> & {
|
|
21
|
+
enabled?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const selectionContext = /* @__PURE__ */ createContext<Api | null>(null)
|
|
25
|
+
|
|
26
|
+
export function Selection({ children, enabled = true }: { enabled?: boolean; children: ReactNode }) {
|
|
27
|
+
const [selected, select] = useState<Object3D[]>([])
|
|
28
|
+
const value = useMemo(() => ({ selected, select, enabled }), [selected, select, enabled])
|
|
29
|
+
return <selectionContext.Provider value={value}>{children}</selectionContext.Provider>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Covers Mesh/Line/Points subclasses too, unlike `.type`.
|
|
33
|
+
function isSelectable(object: Object3D): boolean {
|
|
34
|
+
const o = object as Partial<Mesh & Line & Points>
|
|
35
|
+
return !!(o.isMesh || o.isLine || o.isPoints)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function Select({ enabled = false, children, ...props }: SelectApi) {
|
|
39
|
+
const group = useRef<Group>(null!)
|
|
40
|
+
// Stable, unlike the context value - avoids retriggering off our own write.
|
|
41
|
+
const select = use(selectionContext)?.select
|
|
42
|
+
const claimed = useRef<Object3D[]>([])
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!select) return
|
|
46
|
+
|
|
47
|
+
const current: Object3D[] = []
|
|
48
|
+
if (enabled) {
|
|
49
|
+
group.current.traverse((o) => {
|
|
50
|
+
if (isSelectable(o)) current.push(o)
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const previouslyClaimed = claimed.current
|
|
55
|
+
claimed.current = current
|
|
56
|
+
|
|
57
|
+
select((prev) => {
|
|
58
|
+
const prevSet = new Set(prev)
|
|
59
|
+
const currentSet = new Set(current)
|
|
60
|
+
const toAdd = current.filter((o) => !prevSet.has(o))
|
|
61
|
+
const toRemove = previouslyClaimed.filter((o) => !currentSet.has(o) && prevSet.has(o))
|
|
62
|
+
if (!toAdd.length && !toRemove.length) return prev
|
|
63
|
+
const toRemoveSet = toRemove.length ? new Set(toRemove) : null
|
|
64
|
+
const kept = toRemoveSet ? prev.filter((o) => !toRemoveSet.has(o)) : prev
|
|
65
|
+
return toAdd.length ? [...kept, ...toAdd] : kept
|
|
66
|
+
})
|
|
67
|
+
}, [enabled, children, select])
|
|
68
|
+
|
|
69
|
+
// Separate from the effect above so unmount cleanup doesn't fire on every enabled/children change.
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
return () => {
|
|
72
|
+
if (!select || !claimed.current.length) return
|
|
73
|
+
const stillClaimed = new Set(claimed.current)
|
|
74
|
+
select((prev) => {
|
|
75
|
+
const next = prev.filter((o) => !stillClaimed.has(o))
|
|
76
|
+
return next.length !== prev.length ? next : prev
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}, [select])
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<group ref={group} {...props}>
|
|
83
|
+
{children}
|
|
84
|
+
</group>
|
|
85
|
+
)
|
|
86
|
+
}
|