@react-three/postprocessing 3.0.0-rc.1 → 3.0.0
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/package.json +68 -68
- package/src/EffectComposer.test.tsx +126 -126
- package/src/EffectComposer.tsx +200 -200
- package/src/Selection.tsx +46 -46
- package/src/effects/ASCII.tsx +135 -135
- package/src/effects/Autofocus.tsx +153 -153
- package/src/effects/Bloom.tsx +6 -6
- package/src/effects/BrightnessContrast.tsx +4 -4
- package/src/effects/ChromaticAberration.tsx +5 -5
- package/src/effects/ColorAverage.tsx +15 -15
- package/src/effects/ColorDepth.tsx +4 -4
- package/src/effects/Depth.tsx +4 -4
- package/src/effects/DepthOfField.tsx +89 -89
- package/src/effects/DotScreen.tsx +4 -4
- package/src/effects/FXAA.tsx +4 -4
- package/src/effects/Glitch.tsx +40 -40
- package/src/effects/GodRays.tsx +16 -16
- package/src/effects/Grid.tsx +21 -21
- package/src/effects/HueSaturation.tsx +4 -4
- package/src/effects/LUT.tsx +26 -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 +117 -117
- package/src/effects/Pixelation.tsx +15 -15
- package/src/effects/Ramp.tsx +150 -150
- package/src/effects/SMAA.tsx +4 -4
- package/src/effects/SSAO.tsx +41 -41
- package/src/effects/ScanlineEffect.tsx +7 -7
- package/src/effects/SelectiveBloom.tsx +126 -126
- package/src/effects/Sepia.tsx +4 -4
- package/src/effects/ShockWave.tsx +4 -4
- package/src/effects/Texture.tsx +23 -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 +40 -40
- package/src/util.tsx +55 -55
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -555
- package/dist/index.js.map +0 -1
package/src/EffectComposer.tsx
CHANGED
|
@@ -1,200 +1,200 @@
|
|
|
1
|
-
import type { TextureDataType, Group, Camera, Scene } from 'three'
|
|
2
|
-
import { HalfFloatType, NoToneMapping } from 'three'
|
|
3
|
-
import {
|
|
4
|
-
type JSX,
|
|
5
|
-
memo,
|
|
6
|
-
forwardRef,
|
|
7
|
-
useMemo,
|
|
8
|
-
useEffect,
|
|
9
|
-
useLayoutEffect,
|
|
10
|
-
createContext,
|
|
11
|
-
useRef,
|
|
12
|
-
useImperativeHandle,
|
|
13
|
-
} from 'react'
|
|
14
|
-
import { useThree, useFrame, type Instance } from '@react-three/fiber'
|
|
15
|
-
import {
|
|
16
|
-
EffectComposer as EffectComposerImpl,
|
|
17
|
-
RenderPass,
|
|
18
|
-
EffectPass,
|
|
19
|
-
NormalPass,
|
|
20
|
-
DepthDownsamplingPass,
|
|
21
|
-
Effect,
|
|
22
|
-
Pass,
|
|
23
|
-
EffectAttribute,
|
|
24
|
-
} from 'postprocessing'
|
|
25
|
-
|
|
26
|
-
export const EffectComposerContext = /* @__PURE__ */ createContext<{
|
|
27
|
-
composer: EffectComposerImpl
|
|
28
|
-
normalPass: NormalPass | null
|
|
29
|
-
downSamplingPass: DepthDownsamplingPass | null
|
|
30
|
-
camera: Camera
|
|
31
|
-
scene: Scene
|
|
32
|
-
resolutionScale?: number
|
|
33
|
-
}>(null!)
|
|
34
|
-
|
|
35
|
-
export type EffectComposerProps = {
|
|
36
|
-
enabled?: boolean
|
|
37
|
-
children: JSX.Element | JSX.Element[]
|
|
38
|
-
depthBuffer?: boolean
|
|
39
|
-
/** Only used for SSGI currently, leave it disabled for everything else unless it's needed */
|
|
40
|
-
enableNormalPass?: boolean
|
|
41
|
-
stencilBuffer?: boolean
|
|
42
|
-
autoClear?: boolean
|
|
43
|
-
resolutionScale?: number
|
|
44
|
-
multisampling?: number
|
|
45
|
-
frameBufferType?: TextureDataType
|
|
46
|
-
renderPriority?: number
|
|
47
|
-
camera?: Camera
|
|
48
|
-
scene?: Scene
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const isConvolution = (effect: Effect): boolean =>
|
|
52
|
-
(effect.getAttributes() & EffectAttribute.CONVOLUTION) === EffectAttribute.CONVOLUTION
|
|
53
|
-
|
|
54
|
-
export const EffectComposer = /* @__PURE__ */ memo(
|
|
55
|
-
/* @__PURE__ */ forwardRef<EffectComposerImpl, EffectComposerProps>(
|
|
56
|
-
(
|
|
57
|
-
{
|
|
58
|
-
children,
|
|
59
|
-
camera: _camera,
|
|
60
|
-
scene: _scene,
|
|
61
|
-
resolutionScale,
|
|
62
|
-
enabled = true,
|
|
63
|
-
renderPriority = 1,
|
|
64
|
-
autoClear = true,
|
|
65
|
-
depthBuffer,
|
|
66
|
-
enableNormalPass,
|
|
67
|
-
stencilBuffer,
|
|
68
|
-
multisampling = 8,
|
|
69
|
-
frameBufferType = HalfFloatType,
|
|
70
|
-
},
|
|
71
|
-
ref
|
|
72
|
-
) => {
|
|
73
|
-
const { gl, scene: defaultScene, camera: defaultCamera, size } = useThree()
|
|
74
|
-
const scene = _scene || defaultScene
|
|
75
|
-
const camera = _camera || defaultCamera
|
|
76
|
-
|
|
77
|
-
const [composer, normalPass, downSamplingPass] = useMemo(() => {
|
|
78
|
-
// Initialize composer
|
|
79
|
-
const effectComposer = new EffectComposerImpl(gl, {
|
|
80
|
-
depthBuffer,
|
|
81
|
-
stencilBuffer,
|
|
82
|
-
multisampling,
|
|
83
|
-
frameBufferType,
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
// Add render pass
|
|
87
|
-
effectComposer.addPass(new RenderPass(scene, camera))
|
|
88
|
-
|
|
89
|
-
// Create normal pass
|
|
90
|
-
let downSamplingPass = null
|
|
91
|
-
let normalPass = null
|
|
92
|
-
if (enableNormalPass) {
|
|
93
|
-
normalPass = new NormalPass(scene, camera)
|
|
94
|
-
normalPass.enabled = false
|
|
95
|
-
effectComposer.addPass(normalPass)
|
|
96
|
-
if (resolutionScale !== undefined) {
|
|
97
|
-
downSamplingPass = new DepthDownsamplingPass({ normalBuffer: normalPass.texture, resolutionScale })
|
|
98
|
-
downSamplingPass.enabled = false
|
|
99
|
-
effectComposer.addPass(downSamplingPass)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return [effectComposer, normalPass, downSamplingPass]
|
|
104
|
-
}, [
|
|
105
|
-
camera,
|
|
106
|
-
gl,
|
|
107
|
-
depthBuffer,
|
|
108
|
-
stencilBuffer,
|
|
109
|
-
multisampling,
|
|
110
|
-
frameBufferType,
|
|
111
|
-
scene,
|
|
112
|
-
enableNormalPass,
|
|
113
|
-
resolutionScale,
|
|
114
|
-
])
|
|
115
|
-
|
|
116
|
-
useEffect(() => composer?.setSize(size.width, size.height), [composer, size])
|
|
117
|
-
useFrame(
|
|
118
|
-
(_, delta) => {
|
|
119
|
-
if (enabled) {
|
|
120
|
-
const currentAutoClear = gl.autoClear
|
|
121
|
-
gl.autoClear = autoClear
|
|
122
|
-
if (stencilBuffer && !autoClear) gl.clearStencil()
|
|
123
|
-
composer.render(delta)
|
|
124
|
-
gl.autoClear = currentAutoClear
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
enabled ? renderPriority : 0
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
const group = useRef<Group>(null!)
|
|
131
|
-
useLayoutEffect(() => {
|
|
132
|
-
const passes: Pass[] = []
|
|
133
|
-
|
|
134
|
-
// TODO: rewrite all of this with R3F v9
|
|
135
|
-
const groupInstance = (group.current as Group & { __r3f: Instance<Group> }).__r3f
|
|
136
|
-
|
|
137
|
-
if (groupInstance && composer) {
|
|
138
|
-
const children = groupInstance.children
|
|
139
|
-
|
|
140
|
-
for (let i = 0; i < children.length; i++) {
|
|
141
|
-
const child = children[i].object
|
|
142
|
-
|
|
143
|
-
if (child instanceof Effect) {
|
|
144
|
-
const effects: Effect[] = [child]
|
|
145
|
-
|
|
146
|
-
if (!isConvolution(child)) {
|
|
147
|
-
let next: unknown = null
|
|
148
|
-
while ((next = children[i + 1]?.object) instanceof Effect) {
|
|
149
|
-
if (isConvolution(next)) break
|
|
150
|
-
effects.push(next)
|
|
151
|
-
i++
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const pass = new EffectPass(camera, ...effects)
|
|
156
|
-
passes.push(pass)
|
|
157
|
-
} else if (child instanceof Pass) {
|
|
158
|
-
passes.push(child)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
for (const pass of passes) composer?.addPass(pass)
|
|
163
|
-
|
|
164
|
-
if (normalPass) normalPass.enabled = true
|
|
165
|
-
if (downSamplingPass) downSamplingPass.enabled = true
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return () => {
|
|
169
|
-
for (const pass of passes) composer?.removePass(pass)
|
|
170
|
-
if (normalPass) normalPass.enabled = false
|
|
171
|
-
if (downSamplingPass) downSamplingPass.enabled = false
|
|
172
|
-
}
|
|
173
|
-
}, [composer, children, camera, normalPass, downSamplingPass])
|
|
174
|
-
|
|
175
|
-
// Disable tone mapping because threejs disallows tonemapping on render targets
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
const currentTonemapping = gl.toneMapping
|
|
178
|
-
gl.toneMapping = NoToneMapping
|
|
179
|
-
return () => {
|
|
180
|
-
gl.toneMapping = currentTonemapping
|
|
181
|
-
}
|
|
182
|
-
}, [gl])
|
|
183
|
-
|
|
184
|
-
// Memoize state, otherwise it would trigger all consumers on every render
|
|
185
|
-
const state = useMemo(
|
|
186
|
-
() => ({ composer, normalPass, downSamplingPass, resolutionScale, camera, scene }),
|
|
187
|
-
[composer, normalPass, downSamplingPass, resolutionScale, camera, scene]
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
// Expose the composer
|
|
191
|
-
useImperativeHandle(ref, () => composer, [composer])
|
|
192
|
-
|
|
193
|
-
return (
|
|
194
|
-
<EffectComposerContext.Provider value={state}>
|
|
195
|
-
<group ref={group}>{children}</group>
|
|
196
|
-
</EffectComposerContext.Provider>
|
|
197
|
-
)
|
|
198
|
-
}
|
|
199
|
-
)
|
|
200
|
-
)
|
|
1
|
+
import type { TextureDataType, Group, Camera, Scene } from 'three'
|
|
2
|
+
import { HalfFloatType, NoToneMapping } from 'three'
|
|
3
|
+
import {
|
|
4
|
+
type JSX,
|
|
5
|
+
memo,
|
|
6
|
+
forwardRef,
|
|
7
|
+
useMemo,
|
|
8
|
+
useEffect,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
createContext,
|
|
11
|
+
useRef,
|
|
12
|
+
useImperativeHandle,
|
|
13
|
+
} from 'react'
|
|
14
|
+
import { useThree, useFrame, type Instance } from '@react-three/fiber'
|
|
15
|
+
import {
|
|
16
|
+
EffectComposer as EffectComposerImpl,
|
|
17
|
+
RenderPass,
|
|
18
|
+
EffectPass,
|
|
19
|
+
NormalPass,
|
|
20
|
+
DepthDownsamplingPass,
|
|
21
|
+
Effect,
|
|
22
|
+
Pass,
|
|
23
|
+
EffectAttribute,
|
|
24
|
+
} from 'postprocessing'
|
|
25
|
+
|
|
26
|
+
export const EffectComposerContext = /* @__PURE__ */ createContext<{
|
|
27
|
+
composer: EffectComposerImpl
|
|
28
|
+
normalPass: NormalPass | null
|
|
29
|
+
downSamplingPass: DepthDownsamplingPass | null
|
|
30
|
+
camera: Camera
|
|
31
|
+
scene: Scene
|
|
32
|
+
resolutionScale?: number
|
|
33
|
+
}>(null!)
|
|
34
|
+
|
|
35
|
+
export type EffectComposerProps = {
|
|
36
|
+
enabled?: boolean
|
|
37
|
+
children: JSX.Element | JSX.Element[]
|
|
38
|
+
depthBuffer?: boolean
|
|
39
|
+
/** Only used for SSGI currently, leave it disabled for everything else unless it's needed */
|
|
40
|
+
enableNormalPass?: boolean
|
|
41
|
+
stencilBuffer?: boolean
|
|
42
|
+
autoClear?: boolean
|
|
43
|
+
resolutionScale?: number
|
|
44
|
+
multisampling?: number
|
|
45
|
+
frameBufferType?: TextureDataType
|
|
46
|
+
renderPriority?: number
|
|
47
|
+
camera?: Camera
|
|
48
|
+
scene?: Scene
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isConvolution = (effect: Effect): boolean =>
|
|
52
|
+
(effect.getAttributes() & EffectAttribute.CONVOLUTION) === EffectAttribute.CONVOLUTION
|
|
53
|
+
|
|
54
|
+
export const EffectComposer = /* @__PURE__ */ memo(
|
|
55
|
+
/* @__PURE__ */ forwardRef<EffectComposerImpl, EffectComposerProps>(
|
|
56
|
+
(
|
|
57
|
+
{
|
|
58
|
+
children,
|
|
59
|
+
camera: _camera,
|
|
60
|
+
scene: _scene,
|
|
61
|
+
resolutionScale,
|
|
62
|
+
enabled = true,
|
|
63
|
+
renderPriority = 1,
|
|
64
|
+
autoClear = true,
|
|
65
|
+
depthBuffer,
|
|
66
|
+
enableNormalPass,
|
|
67
|
+
stencilBuffer,
|
|
68
|
+
multisampling = 8,
|
|
69
|
+
frameBufferType = HalfFloatType,
|
|
70
|
+
},
|
|
71
|
+
ref
|
|
72
|
+
) => {
|
|
73
|
+
const { gl, scene: defaultScene, camera: defaultCamera, size } = useThree()
|
|
74
|
+
const scene = _scene || defaultScene
|
|
75
|
+
const camera = _camera || defaultCamera
|
|
76
|
+
|
|
77
|
+
const [composer, normalPass, downSamplingPass] = useMemo(() => {
|
|
78
|
+
// Initialize composer
|
|
79
|
+
const effectComposer = new EffectComposerImpl(gl, {
|
|
80
|
+
depthBuffer,
|
|
81
|
+
stencilBuffer,
|
|
82
|
+
multisampling,
|
|
83
|
+
frameBufferType,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Add render pass
|
|
87
|
+
effectComposer.addPass(new RenderPass(scene, camera))
|
|
88
|
+
|
|
89
|
+
// Create normal pass
|
|
90
|
+
let downSamplingPass = null
|
|
91
|
+
let normalPass = null
|
|
92
|
+
if (enableNormalPass) {
|
|
93
|
+
normalPass = new NormalPass(scene, camera)
|
|
94
|
+
normalPass.enabled = false
|
|
95
|
+
effectComposer.addPass(normalPass)
|
|
96
|
+
if (resolutionScale !== undefined) {
|
|
97
|
+
downSamplingPass = new DepthDownsamplingPass({ normalBuffer: normalPass.texture, resolutionScale })
|
|
98
|
+
downSamplingPass.enabled = false
|
|
99
|
+
effectComposer.addPass(downSamplingPass)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [effectComposer, normalPass, downSamplingPass]
|
|
104
|
+
}, [
|
|
105
|
+
camera,
|
|
106
|
+
gl,
|
|
107
|
+
depthBuffer,
|
|
108
|
+
stencilBuffer,
|
|
109
|
+
multisampling,
|
|
110
|
+
frameBufferType,
|
|
111
|
+
scene,
|
|
112
|
+
enableNormalPass,
|
|
113
|
+
resolutionScale,
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
useEffect(() => composer?.setSize(size.width, size.height), [composer, size])
|
|
117
|
+
useFrame(
|
|
118
|
+
(_, delta) => {
|
|
119
|
+
if (enabled) {
|
|
120
|
+
const currentAutoClear = gl.autoClear
|
|
121
|
+
gl.autoClear = autoClear
|
|
122
|
+
if (stencilBuffer && !autoClear) gl.clearStencil()
|
|
123
|
+
composer.render(delta)
|
|
124
|
+
gl.autoClear = currentAutoClear
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
enabled ? renderPriority : 0
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const group = useRef<Group>(null!)
|
|
131
|
+
useLayoutEffect(() => {
|
|
132
|
+
const passes: Pass[] = []
|
|
133
|
+
|
|
134
|
+
// TODO: rewrite all of this with R3F v9
|
|
135
|
+
const groupInstance = (group.current as Group & { __r3f: Instance<Group> }).__r3f
|
|
136
|
+
|
|
137
|
+
if (groupInstance && composer) {
|
|
138
|
+
const children = groupInstance.children
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < children.length; i++) {
|
|
141
|
+
const child = children[i].object
|
|
142
|
+
|
|
143
|
+
if (child instanceof Effect) {
|
|
144
|
+
const effects: Effect[] = [child]
|
|
145
|
+
|
|
146
|
+
if (!isConvolution(child)) {
|
|
147
|
+
let next: unknown = null
|
|
148
|
+
while ((next = children[i + 1]?.object) instanceof Effect) {
|
|
149
|
+
if (isConvolution(next)) break
|
|
150
|
+
effects.push(next)
|
|
151
|
+
i++
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const pass = new EffectPass(camera, ...effects)
|
|
156
|
+
passes.push(pass)
|
|
157
|
+
} else if (child instanceof Pass) {
|
|
158
|
+
passes.push(child)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const pass of passes) composer?.addPass(pass)
|
|
163
|
+
|
|
164
|
+
if (normalPass) normalPass.enabled = true
|
|
165
|
+
if (downSamplingPass) downSamplingPass.enabled = true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return () => {
|
|
169
|
+
for (const pass of passes) composer?.removePass(pass)
|
|
170
|
+
if (normalPass) normalPass.enabled = false
|
|
171
|
+
if (downSamplingPass) downSamplingPass.enabled = false
|
|
172
|
+
}
|
|
173
|
+
}, [composer, children, camera, normalPass, downSamplingPass])
|
|
174
|
+
|
|
175
|
+
// Disable tone mapping because threejs disallows tonemapping on render targets
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
const currentTonemapping = gl.toneMapping
|
|
178
|
+
gl.toneMapping = NoToneMapping
|
|
179
|
+
return () => {
|
|
180
|
+
gl.toneMapping = currentTonemapping
|
|
181
|
+
}
|
|
182
|
+
}, [gl])
|
|
183
|
+
|
|
184
|
+
// Memoize state, otherwise it would trigger all consumers on every render
|
|
185
|
+
const state = useMemo(
|
|
186
|
+
() => ({ composer, normalPass, downSamplingPass, resolutionScale, camera, scene }),
|
|
187
|
+
[composer, normalPass, downSamplingPass, resolutionScale, camera, scene]
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
// Expose the composer
|
|
191
|
+
useImperativeHandle(ref, () => composer, [composer])
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<EffectComposerContext.Provider value={state}>
|
|
195
|
+
<group ref={group}>{children}</group>
|
|
196
|
+
</EffectComposerContext.Provider>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
)
|
package/src/Selection.tsx
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import * as THREE from 'three'
|
|
2
|
-
import React, { createContext, useState, useContext, useEffect, useRef, useMemo } from 'react'
|
|
3
|
-
import { type ThreeElements } from '@react-three/fiber'
|
|
4
|
-
|
|
5
|
-
export type Api = {
|
|
6
|
-
selected: THREE.Object3D[]
|
|
7
|
-
select: React.Dispatch<React.SetStateAction<THREE.Object3D[]>>
|
|
8
|
-
enabled: boolean
|
|
9
|
-
}
|
|
10
|
-
export type SelectApi = Omit<ThreeElements['group'], 'ref'> & {
|
|
11
|
-
enabled?: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const selectionContext = /* @__PURE__ */ createContext<Api | null>(null)
|
|
15
|
-
|
|
16
|
-
export function Selection({ children, enabled = true }: { enabled?: boolean; children: React.ReactNode }) {
|
|
17
|
-
const [selected, select] = useState<THREE.Object3D[]>([])
|
|
18
|
-
const value = useMemo(() => ({ selected, select, enabled }), [selected, select, enabled])
|
|
19
|
-
return <selectionContext.Provider value={value}>{children}</selectionContext.Provider>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function Select({ enabled = false, children, ...props }: SelectApi) {
|
|
23
|
-
const group = useRef<THREE.Group>(null!)
|
|
24
|
-
const api = useContext(selectionContext)
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (api && enabled) {
|
|
27
|
-
let changed = false
|
|
28
|
-
const current: THREE.Object3D[] = []
|
|
29
|
-
group.current.traverse((o) => {
|
|
30
|
-
o.type === 'Mesh' && current.push(o)
|
|
31
|
-
if (api.selected.indexOf(o) === -1) changed = true
|
|
32
|
-
})
|
|
33
|
-
if (changed) {
|
|
34
|
-
api.select((state) => [...state, ...current])
|
|
35
|
-
return () => {
|
|
36
|
-
api.select((state) => state.filter((selected) => !current.includes(selected)))
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}, [enabled, children, api])
|
|
41
|
-
return (
|
|
42
|
-
<group ref={group} {...props}>
|
|
43
|
-
{children}
|
|
44
|
-
</group>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
1
|
+
import * as THREE from 'three'
|
|
2
|
+
import React, { createContext, useState, useContext, useEffect, useRef, useMemo } from 'react'
|
|
3
|
+
import { type ThreeElements } from '@react-three/fiber'
|
|
4
|
+
|
|
5
|
+
export type Api = {
|
|
6
|
+
selected: THREE.Object3D[]
|
|
7
|
+
select: React.Dispatch<React.SetStateAction<THREE.Object3D[]>>
|
|
8
|
+
enabled: boolean
|
|
9
|
+
}
|
|
10
|
+
export type SelectApi = Omit<ThreeElements['group'], 'ref'> & {
|
|
11
|
+
enabled?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const selectionContext = /* @__PURE__ */ createContext<Api | null>(null)
|
|
15
|
+
|
|
16
|
+
export function Selection({ children, enabled = true }: { enabled?: boolean; children: React.ReactNode }) {
|
|
17
|
+
const [selected, select] = useState<THREE.Object3D[]>([])
|
|
18
|
+
const value = useMemo(() => ({ selected, select, enabled }), [selected, select, enabled])
|
|
19
|
+
return <selectionContext.Provider value={value}>{children}</selectionContext.Provider>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Select({ enabled = false, children, ...props }: SelectApi) {
|
|
23
|
+
const group = useRef<THREE.Group>(null!)
|
|
24
|
+
const api = useContext(selectionContext)
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (api && enabled) {
|
|
27
|
+
let changed = false
|
|
28
|
+
const current: THREE.Object3D[] = []
|
|
29
|
+
group.current.traverse((o) => {
|
|
30
|
+
o.type === 'Mesh' && current.push(o)
|
|
31
|
+
if (api.selected.indexOf(o) === -1) changed = true
|
|
32
|
+
})
|
|
33
|
+
if (changed) {
|
|
34
|
+
api.select((state) => [...state, ...current])
|
|
35
|
+
return () => {
|
|
36
|
+
api.select((state) => state.filter((selected) => !current.includes(selected)))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, [enabled, children, api])
|
|
41
|
+
return (
|
|
42
|
+
<group ref={group} {...props}>
|
|
43
|
+
{children}
|
|
44
|
+
</group>
|
|
45
|
+
)
|
|
46
|
+
}
|