@react-three/postprocessing 3.0.4 → 3.1.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 +80 -81
- package/dist/EffectComposer.d.ts +14 -6
- package/dist/EffectGroup.d.ts +9 -0
- package/dist/Selection.d.ts +8 -8
- package/dist/createEffectComponent.d.ts +30 -0
- package/dist/effects/ASCII.d.ts +29 -7
- package/dist/effects/Autofocus.d.ts +8 -7
- package/dist/effects/Bloom.d.ts +10 -5
- package/dist/effects/BrightnessContrast.d.ts +15 -5
- package/dist/effects/ChromaticAberration.d.ts +30 -7
- package/dist/effects/ColorAverage.d.ts +7 -8
- package/dist/effects/ColorDepth.d.ts +8 -5
- package/dist/effects/Depth.d.ts +13 -5
- package/dist/effects/DepthOfField.d.ts +7 -18
- package/dist/effects/DotScreen.d.ts +15 -5
- package/dist/effects/FXAA.d.ts +11 -5
- package/dist/effects/Glitch.d.ts +17 -29
- package/dist/effects/GodRays.d.ts +7 -19
- package/dist/effects/Grid.d.ts +8 -8
- package/dist/effects/HueSaturation.d.ts +15 -5
- package/dist/effects/LUT.d.ts +4 -3
- package/dist/effects/LensFlare.d.ts +52 -11
- package/dist/effects/Noise.d.ts +8 -5
- package/dist/effects/Outline.d.ts +9 -25
- package/dist/effects/Pixelation.d.ts +3 -2
- package/dist/effects/Ramp.d.ts +58 -10
- package/dist/effects/SMAA.d.ts +10 -5
- package/dist/effects/SSAO.d.ts +7 -30
- package/dist/effects/ScanlineEffect.d.ts +13 -5
- package/dist/effects/SelectiveBloom.d.ts +5 -10
- package/dist/effects/Sepia.d.ts +13 -5
- package/dist/effects/ShockWave.d.ts +12 -5
- package/dist/effects/Texture.d.ts +7 -8
- package/dist/effects/TiltShift.d.ts +10 -5
- package/dist/effects/TiltShift2.d.ts +44 -8
- package/dist/effects/ToneMapping.d.ts +9 -7
- package/dist/effects/Vignette.d.ts +19 -5
- package/dist/effects/Water.d.ts +17 -5
- package/dist/index.d.ts +15 -11
- package/dist/index.js +1841 -45
- package/dist/index.js.map +1 -1
- package/dist/passes/DepthPicking.d.ts +11 -0
- package/dist/{effects → passes}/N8AO.d.ts +5 -2
- package/dist/util.d.ts +19 -17
- package/dist/wrapEffect.d.ts +12 -0
- package/package.json +74 -67
- package/src/EffectComposer.tsx +379 -200
- package/src/EffectGroup.tsx +74 -0
- package/src/Selection.tsx +86 -46
- package/src/createEffectComponent.tsx +71 -0
- package/src/effects/ASCII.tsx +183 -135
- package/src/effects/Autofocus.tsx +122 -153
- package/src/effects/Bloom.tsx +34 -6
- package/src/effects/BrightnessContrast.tsx +7 -4
- package/src/effects/ChromaticAberration.tsx +22 -5
- package/src/effects/ColorAverage.tsx +4 -15
- package/src/effects/ColorDepth.tsx +25 -4
- package/src/effects/Depth.tsx +6 -4
- package/src/effects/DepthOfField.tsx +109 -89
- package/src/effects/DotScreen.tsx +7 -4
- package/src/effects/FXAA.tsx +6 -4
- package/src/effects/Glitch.tsx +32 -40
- package/src/effects/GodRays.tsx +104 -16
- package/src/effects/Grid.tsx +27 -21
- package/src/effects/HueSaturation.tsx +7 -4
- package/src/effects/LUT.tsx +25 -26
- package/src/effects/LensFlare.tsx +765 -611
- package/src/effects/Noise.tsx +16 -4
- package/src/effects/Outline.tsx +84 -117
- package/src/effects/Pixelation.tsx +20 -15
- package/src/effects/Ramp.tsx +239 -150
- package/src/effects/SMAA.tsx +20 -4
- package/src/effects/SSAO.tsx +152 -41
- package/src/effects/ScanlineEffect.tsx +7 -7
- package/src/effects/SelectiveBloom.tsx +98 -126
- package/src/effects/Sepia.tsx +6 -4
- package/src/effects/ShockWave.tsx +32 -4
- package/src/effects/Texture.tsx +28 -23
- package/src/effects/TiltShift.tsx +32 -4
- package/src/effects/TiltShift2.tsx +156 -90
- package/src/effects/ToneMapping.tsx +19 -6
- package/src/effects/Vignette.tsx +7 -4
- package/src/effects/Water.tsx +50 -35
- package/src/index.ts +43 -40
- package/src/passes/DepthPicking.tsx +63 -0
- package/src/passes/N8AO.tsx +105 -0
- package/src/tests/Autofocus.test.tsx +38 -0
- package/src/tests/Bloom.test.tsx +76 -0
- package/src/tests/ChromaticAberration.test.tsx +81 -0
- package/src/tests/ColorDepth.test.tsx +71 -0
- package/src/tests/DepthOfField.test.tsx +130 -0
- package/src/tests/DepthPicking.test.tsx +250 -0
- package/src/tests/EffectComposer.test.tsx +1404 -0
- package/src/tests/EffectGroup.test.tsx +337 -0
- package/src/tests/Glitch.test.tsx +56 -0
- package/src/tests/GodRays.test.tsx +101 -0
- package/src/tests/Grid.test.tsx +34 -0
- package/src/tests/LUT.test.tsx +64 -0
- package/src/tests/N8AO.test.tsx +92 -0
- package/src/tests/Outline.test.tsx +211 -0
- package/src/tests/SSAO.test.tsx +140 -0
- package/src/tests/Selection.test.tsx +324 -0
- package/src/tests/SelectiveBloom.test.tsx +166 -0
- package/src/tests/ShockWave.test.tsx +95 -0
- package/src/tests/TiltShift.test.tsx +76 -0
- package/src/tests/createEffectComponent.test.tsx +286 -0
- package/src/tests/effects.smoke.test.tsx +238 -0
- package/src/tests/test-utils.tsx +96 -0
- package/src/tests/wrapEffect.test.tsx +209 -0
- package/src/util.tsx +221 -55
- package/src/wrapEffect.tsx +123 -0
- package/src/EffectComposer.test.tsx +0 -126
- package/src/effects/N8AO.tsx +0 -81
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { EffectComposer as EffectComposerImpl, OutlineEffect, Selection as PPSelection } from 'postprocessing'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Mesh, Object3D } from 'three'
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { EffectComposer } from '../EffectComposer'
|
|
6
|
+
import { Outline } from '../effects/Outline'
|
|
7
|
+
import { Select, Selection } from '../Selection'
|
|
8
|
+
import { flush, root, waitForComposer } from './test-utils'
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await React.act(async () => {
|
|
12
|
+
root.render(null)
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('Outline', () => {
|
|
17
|
+
it('does not re-set its (empty, declarative-mode) selection on unrelated re-renders', async () => {
|
|
18
|
+
const setSpy = vi.spyOn(PPSelection.prototype, 'set')
|
|
19
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
20
|
+
|
|
21
|
+
const render = (tick: number) =>
|
|
22
|
+
root.render(
|
|
23
|
+
<EffectComposer ref={composerRef}>
|
|
24
|
+
<Outline />
|
|
25
|
+
<group name={`tick-${tick}`} />
|
|
26
|
+
</EffectComposer>
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
await React.act(async () => render(0))
|
|
30
|
+
await waitForComposer(composerRef)
|
|
31
|
+
await flush()
|
|
32
|
+
setSpy.mockClear()
|
|
33
|
+
|
|
34
|
+
for (let t = 1; t <= 5; t++) {
|
|
35
|
+
await React.act(async () => render(t))
|
|
36
|
+
await flush()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
expect(setSpy).not.toHaveBeenCalled()
|
|
40
|
+
setSpy.mockRestore()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('sets its selection from the Selection/Select API and clears it when the object deselects', async () => {
|
|
44
|
+
const effectRef = React.createRef<OutlineEffect>()
|
|
45
|
+
const meshRef = React.createRef<Mesh>()
|
|
46
|
+
|
|
47
|
+
const render = (enabled: boolean) =>
|
|
48
|
+
root.render(
|
|
49
|
+
<EffectComposer>
|
|
50
|
+
<Selection>
|
|
51
|
+
<Select enabled={enabled}>
|
|
52
|
+
<mesh ref={meshRef}>
|
|
53
|
+
<boxGeometry />
|
|
54
|
+
<meshBasicMaterial />
|
|
55
|
+
</mesh>
|
|
56
|
+
</Select>
|
|
57
|
+
<Outline ref={effectRef} />
|
|
58
|
+
</Selection>
|
|
59
|
+
</EffectComposer>
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
await React.act(async () => render(true))
|
|
63
|
+
await flush()
|
|
64
|
+
await flush()
|
|
65
|
+
|
|
66
|
+
expect(Array.from(effectRef.current!.selection)).toContain(meshRef.current)
|
|
67
|
+
|
|
68
|
+
await React.act(async () => render(false))
|
|
69
|
+
await flush()
|
|
70
|
+
await flush()
|
|
71
|
+
|
|
72
|
+
expect(Array.from(effectRef.current!.selection)).not.toContain(meshRef.current)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('does not throw when a selection ref has not attached yet', async () => {
|
|
76
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
77
|
+
const unattachedRef = React.createRef<Object3D>()
|
|
78
|
+
|
|
79
|
+
await React.act(async () =>
|
|
80
|
+
root.render(
|
|
81
|
+
<EffectComposer ref={composerRef}>
|
|
82
|
+
<Outline selection={[unattachedRef]} />
|
|
83
|
+
</EffectComposer>
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
await waitForComposer(composerRef)
|
|
87
|
+
await expect(flush()).resolves.not.toThrow()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('applies visibleEdgeColor live, without reconstructing the effect (#143)', async () => {
|
|
91
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
92
|
+
const effectRef = React.createRef<OutlineEffect>()
|
|
93
|
+
|
|
94
|
+
const render = (color: number) =>
|
|
95
|
+
root.render(
|
|
96
|
+
<EffectComposer ref={composerRef}>
|
|
97
|
+
<Outline ref={effectRef} visibleEdgeColor={color} />
|
|
98
|
+
</EffectComposer>
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
await React.act(async () => render(0xff0000))
|
|
102
|
+
await waitForComposer(composerRef)
|
|
103
|
+
await flush()
|
|
104
|
+
|
|
105
|
+
const first = effectRef.current
|
|
106
|
+
expect(first!.visibleEdgeColor.getHex()).toBe(0xff0000)
|
|
107
|
+
|
|
108
|
+
await React.act(async () => render(0x00ff00))
|
|
109
|
+
await flush()
|
|
110
|
+
|
|
111
|
+
expect(effectRef.current).toBe(first)
|
|
112
|
+
expect(effectRef.current!.visibleEdgeColor.getHex()).toBe(0x00ff00)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('accepts a CSS color string for visibleEdgeColor/hiddenEdgeColor, not just a numeric hex (#187, #182)', async () => {
|
|
116
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
117
|
+
const effectRef = React.createRef<OutlineEffect>()
|
|
118
|
+
|
|
119
|
+
await React.act(async () =>
|
|
120
|
+
root.render(
|
|
121
|
+
<EffectComposer ref={composerRef}>
|
|
122
|
+
<Outline ref={effectRef} visibleEdgeColor="red" hiddenEdgeColor="blue" />
|
|
123
|
+
</EffectComposer>
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
await waitForComposer(composerRef)
|
|
127
|
+
await flush()
|
|
128
|
+
|
|
129
|
+
expect(effectRef.current!.visibleEdgeColor.getHex()).toBe(0xff0000)
|
|
130
|
+
expect(effectRef.current!.hiddenEdgeColor.getHex()).toBe(0x0000ff)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('resets edgeStrength to its constructor default when the prop is removed', async () => {
|
|
134
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
135
|
+
const effectRef = React.createRef<OutlineEffect>()
|
|
136
|
+
|
|
137
|
+
await React.act(async () =>
|
|
138
|
+
root.render(
|
|
139
|
+
<EffectComposer ref={composerRef}>
|
|
140
|
+
<Outline ref={effectRef} edgeStrength={100} />
|
|
141
|
+
</EffectComposer>
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
await waitForComposer(composerRef)
|
|
145
|
+
await flush()
|
|
146
|
+
|
|
147
|
+
expect(effectRef.current!.edgeStrength).toBe(100)
|
|
148
|
+
|
|
149
|
+
await React.act(async () =>
|
|
150
|
+
root.render(
|
|
151
|
+
<EffectComposer ref={composerRef}>
|
|
152
|
+
<Outline ref={effectRef} />
|
|
153
|
+
</EffectComposer>
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
await flush()
|
|
157
|
+
|
|
158
|
+
expect(effectRef.current!.edgeStrength).toBe(1)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('still reconstructs when a construction-only prop (resolutionScale) changes', async () => {
|
|
162
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
163
|
+
const effectRef = React.createRef<OutlineEffect>()
|
|
164
|
+
|
|
165
|
+
const render = (resolutionScale: number) =>
|
|
166
|
+
root.render(
|
|
167
|
+
<EffectComposer ref={composerRef}>
|
|
168
|
+
<Outline ref={effectRef} resolutionScale={resolutionScale} />
|
|
169
|
+
</EffectComposer>
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
await React.act(async () => render(0.5))
|
|
173
|
+
await waitForComposer(composerRef)
|
|
174
|
+
await flush()
|
|
175
|
+
const first = effectRef.current
|
|
176
|
+
|
|
177
|
+
await React.act(async () => render(1))
|
|
178
|
+
await flush()
|
|
179
|
+
|
|
180
|
+
expect(effectRef.current).not.toBe(first)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('does not dispose its render target on unrelated re-renders (multisampling has an unconditional dispose side effect)', async () => {
|
|
184
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
185
|
+
const effectRef = React.createRef<OutlineEffect>()
|
|
186
|
+
|
|
187
|
+
const render = (tick: number) =>
|
|
188
|
+
root.render(
|
|
189
|
+
<EffectComposer ref={composerRef}>
|
|
190
|
+
<Outline ref={effectRef} />
|
|
191
|
+
<group name={`tick-${tick}`} />
|
|
192
|
+
</EffectComposer>
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
await React.act(async () => render(0))
|
|
196
|
+
await waitForComposer(composerRef)
|
|
197
|
+
await flush()
|
|
198
|
+
|
|
199
|
+
// @ts-expect-error - `renderTargetMask` isn't part of the public OutlineEffect typing
|
|
200
|
+
const disposeSpy = vi.spyOn(effectRef.current!.renderTargetMask, 'dispose')
|
|
201
|
+
|
|
202
|
+
for (let t = 1; t <= 5; t++) {
|
|
203
|
+
await React.act(async () => render(t))
|
|
204
|
+
await flush()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
expect(disposeSpy).not.toHaveBeenCalled()
|
|
208
|
+
disposeSpy.mockRestore()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { EffectComposer as EffectComposerImpl, SSAOEffect } from 'postprocessing'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Color } from 'three'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { EffectComposer } from '../EffectComposer'
|
|
6
|
+
import { SSAO } from '../effects/SSAO'
|
|
7
|
+
import { flush, root, waitForComposer } from './test-utils'
|
|
8
|
+
|
|
9
|
+
describe('SSAO', () => {
|
|
10
|
+
it('resets color/fade/minRadiusScale to their constructor defaults when removed, not the first-mounted value', async () => {
|
|
11
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
12
|
+
const ref = React.createRef<SSAOEffect>()
|
|
13
|
+
|
|
14
|
+
const render = (withOverrides: boolean) =>
|
|
15
|
+
root.render(
|
|
16
|
+
<EffectComposer ref={composerRef} enableNormalPass>
|
|
17
|
+
<SSAO ref={ref} {...(withOverrides ? { color: new Color('red'), fade: 0.5, minRadiusScale: 0.9 } : {})} />
|
|
18
|
+
</EffectComposer>
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
await React.act(async () => render(true))
|
|
22
|
+
await waitForComposer(composerRef)
|
|
23
|
+
await flush()
|
|
24
|
+
|
|
25
|
+
expect(ref.current!.color!.getHexString()).toBe('ff0000')
|
|
26
|
+
expect(ref.current!.ssaoMaterial.fade).toBeCloseTo(0.5)
|
|
27
|
+
expect(ref.current!.ssaoMaterial.minRadiusScale).toBeCloseTo(0.9)
|
|
28
|
+
|
|
29
|
+
await React.act(async () => render(false))
|
|
30
|
+
await flush()
|
|
31
|
+
|
|
32
|
+
// SSAOEffect's own constructor defaults (null / 0.01 / 0.1), not the
|
|
33
|
+
// values from the first render this instance ever saw.
|
|
34
|
+
expect(ref.current!.color).toBeNull()
|
|
35
|
+
expect(ref.current!.ssaoMaterial.fade).toBeCloseTo(0.01)
|
|
36
|
+
expect(ref.current!.ssaoMaterial.minRadiusScale).toBeCloseTo(0.1)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('applies intensity live, without reconstructing the effect', async () => {
|
|
40
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
41
|
+
const ref = React.createRef<SSAOEffect>()
|
|
42
|
+
|
|
43
|
+
const render = (intensity: number) =>
|
|
44
|
+
root.render(
|
|
45
|
+
<EffectComposer ref={composerRef} enableNormalPass>
|
|
46
|
+
<SSAO ref={ref} intensity={intensity} />
|
|
47
|
+
</EffectComposer>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
await React.act(async () => render(1))
|
|
51
|
+
await waitForComposer(composerRef)
|
|
52
|
+
await flush()
|
|
53
|
+
const first = ref.current
|
|
54
|
+
expect(first!.intensity).toBe(1)
|
|
55
|
+
|
|
56
|
+
await React.act(async () => render(2))
|
|
57
|
+
await flush()
|
|
58
|
+
|
|
59
|
+
expect(ref.current).toBe(first)
|
|
60
|
+
expect(ref.current!.intensity).toBe(2)
|
|
61
|
+
|
|
62
|
+
await React.act(async () => root.render(null))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('applies bias live via the nested ssaoMaterial, without reconstructing the effect', async () => {
|
|
66
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
67
|
+
const ref = React.createRef<SSAOEffect>()
|
|
68
|
+
|
|
69
|
+
const render = (bias: number) =>
|
|
70
|
+
root.render(
|
|
71
|
+
<EffectComposer ref={composerRef} enableNormalPass>
|
|
72
|
+
<SSAO ref={ref} bias={bias} />
|
|
73
|
+
</EffectComposer>
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
await React.act(async () => render(0.5))
|
|
77
|
+
await waitForComposer(composerRef)
|
|
78
|
+
await flush()
|
|
79
|
+
const first = ref.current
|
|
80
|
+
expect(first!.ssaoMaterial.bias).toBeCloseTo(0.5)
|
|
81
|
+
|
|
82
|
+
await React.act(async () => render(0.8))
|
|
83
|
+
await flush()
|
|
84
|
+
|
|
85
|
+
expect(ref.current).toBe(first)
|
|
86
|
+
expect(ref.current!.ssaoMaterial.bias).toBeCloseTo(0.8)
|
|
87
|
+
|
|
88
|
+
await React.act(async () => root.render(null))
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('applies worldProximityThreshold live via the nested ssaoMaterial, without reconstructing the effect', async () => {
|
|
92
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
93
|
+
const ref = React.createRef<SSAOEffect>()
|
|
94
|
+
|
|
95
|
+
const render = (worldProximityThreshold: number) =>
|
|
96
|
+
root.render(
|
|
97
|
+
<EffectComposer ref={composerRef} enableNormalPass>
|
|
98
|
+
<SSAO ref={ref} worldProximityThreshold={worldProximityThreshold} />
|
|
99
|
+
</EffectComposer>
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
await React.act(async () => render(0.5))
|
|
103
|
+
await waitForComposer(composerRef)
|
|
104
|
+
await flush()
|
|
105
|
+
const first = ref.current
|
|
106
|
+
expect(first!.ssaoMaterial.worldProximityThreshold).toBeCloseTo(0.5)
|
|
107
|
+
|
|
108
|
+
await React.act(async () => render(0.8))
|
|
109
|
+
await flush()
|
|
110
|
+
|
|
111
|
+
expect(ref.current).toBe(first)
|
|
112
|
+
expect(ref.current!.ssaoMaterial.worldProximityThreshold).toBeCloseTo(0.8)
|
|
113
|
+
|
|
114
|
+
await React.act(async () => root.render(null))
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('still reconstructs when resolutionScale (construction-only) changes', async () => {
|
|
118
|
+
const composerRef = React.createRef<EffectComposerImpl>()
|
|
119
|
+
const ref = React.createRef<SSAOEffect>()
|
|
120
|
+
|
|
121
|
+
const render = (resolutionScale: number) =>
|
|
122
|
+
root.render(
|
|
123
|
+
<EffectComposer ref={composerRef} enableNormalPass>
|
|
124
|
+
<SSAO ref={ref} resolutionScale={resolutionScale} />
|
|
125
|
+
</EffectComposer>
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
await React.act(async () => render(0.5))
|
|
129
|
+
await waitForComposer(composerRef)
|
|
130
|
+
await flush()
|
|
131
|
+
const first = ref.current
|
|
132
|
+
|
|
133
|
+
await React.act(async () => render(1))
|
|
134
|
+
await flush()
|
|
135
|
+
|
|
136
|
+
expect(ref.current).not.toBe(first)
|
|
137
|
+
|
|
138
|
+
await React.act(async () => root.render(null))
|
|
139
|
+
})
|
|
140
|
+
})
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
4
|
+
import { Select, Selection, selectionContext } from '../Selection'
|
|
5
|
+
import { flush, root } from './test-utils'
|
|
6
|
+
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
await React.act(async () => {
|
|
9
|
+
root.render(null)
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
function Capture({ onSnapshot }: { onSnapshot: (selected: THREE.Object3D[]) => void }) {
|
|
14
|
+
const api = React.useContext(selectionContext)
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
onSnapshot(api?.selected ?? [])
|
|
17
|
+
})
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('Selection/Select', () => {
|
|
22
|
+
it('settles to a stable selected array without looping', async () => {
|
|
23
|
+
const snapshots: THREE.Object3D[][] = []
|
|
24
|
+
const meshRef = React.createRef<THREE.Mesh>()
|
|
25
|
+
|
|
26
|
+
await React.act(async () =>
|
|
27
|
+
root.render(
|
|
28
|
+
<Selection>
|
|
29
|
+
<Capture onSnapshot={(s) => snapshots.push(s)} />
|
|
30
|
+
<Select enabled>
|
|
31
|
+
<mesh ref={meshRef}>
|
|
32
|
+
<boxGeometry />
|
|
33
|
+
<meshBasicMaterial />
|
|
34
|
+
</mesh>
|
|
35
|
+
</Select>
|
|
36
|
+
</Selection>
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
await flush()
|
|
40
|
+
await flush()
|
|
41
|
+
|
|
42
|
+
// Exactly one render for the initial (empty) commit, one for the
|
|
43
|
+
// single necessary addition - the original bug kept re-triggering
|
|
44
|
+
// itself, so this would grow unbounded (and eventually throw) instead
|
|
45
|
+
// of stopping at 2.
|
|
46
|
+
expect(snapshots).toEqual([[], [meshRef.current]])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('selects a Line and a Points object, not just Mesh', async () => {
|
|
50
|
+
const snapshots: THREE.Object3D[][] = []
|
|
51
|
+
const lineRef = React.createRef<THREE.Line>()
|
|
52
|
+
const pointsRef = React.createRef<THREE.Points>()
|
|
53
|
+
|
|
54
|
+
await React.act(async () =>
|
|
55
|
+
root.render(
|
|
56
|
+
<Selection>
|
|
57
|
+
<Capture onSnapshot={(s) => snapshots.push(s)} />
|
|
58
|
+
<Select enabled>
|
|
59
|
+
<threeLine ref={lineRef}>
|
|
60
|
+
<bufferGeometry />
|
|
61
|
+
<lineBasicMaterial />
|
|
62
|
+
</threeLine>
|
|
63
|
+
</Select>
|
|
64
|
+
<Select enabled>
|
|
65
|
+
<points ref={pointsRef}>
|
|
66
|
+
<bufferGeometry />
|
|
67
|
+
<pointsMaterial />
|
|
68
|
+
</points>
|
|
69
|
+
</Select>
|
|
70
|
+
</Selection>
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
await flush()
|
|
74
|
+
await flush()
|
|
75
|
+
|
|
76
|
+
const last = snapshots[snapshots.length - 1]
|
|
77
|
+
expect(last).toContain(lineRef.current)
|
|
78
|
+
expect(last).toContain(pointsRef.current)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('removes its objects once unmounted, leaving a sibling Select untouched', async () => {
|
|
82
|
+
const snapshots: THREE.Object3D[][] = []
|
|
83
|
+
const meshARef = React.createRef<THREE.Mesh>()
|
|
84
|
+
const meshBRef = React.createRef<THREE.Mesh>()
|
|
85
|
+
|
|
86
|
+
const render = (mountA: boolean) =>
|
|
87
|
+
root.render(
|
|
88
|
+
<Selection>
|
|
89
|
+
<Capture onSnapshot={(s) => snapshots.push(s)} />
|
|
90
|
+
{mountA && (
|
|
91
|
+
<Select key="a" enabled>
|
|
92
|
+
<mesh ref={meshARef}>
|
|
93
|
+
<boxGeometry />
|
|
94
|
+
<meshBasicMaterial />
|
|
95
|
+
</mesh>
|
|
96
|
+
</Select>
|
|
97
|
+
)}
|
|
98
|
+
<Select key="b" enabled>
|
|
99
|
+
<mesh ref={meshBRef}>
|
|
100
|
+
<boxGeometry />
|
|
101
|
+
<meshBasicMaterial />
|
|
102
|
+
</mesh>
|
|
103
|
+
</Select>
|
|
104
|
+
</Selection>
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
await React.act(async () => render(true))
|
|
108
|
+
await flush()
|
|
109
|
+
await flush()
|
|
110
|
+
expect(snapshots[snapshots.length - 1]).toEqual(expect.arrayContaining([meshARef.current, meshBRef.current]))
|
|
111
|
+
|
|
112
|
+
await React.act(async () => render(false))
|
|
113
|
+
await flush()
|
|
114
|
+
await flush()
|
|
115
|
+
|
|
116
|
+
const last = snapshots[snapshots.length - 1]
|
|
117
|
+
expect(last).not.toContain(meshARef.current)
|
|
118
|
+
expect(last).toContain(meshBRef.current)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('removes its objects when enabled toggles to false', async () => {
|
|
122
|
+
const snapshots: THREE.Object3D[][] = []
|
|
123
|
+
const meshRef = React.createRef<THREE.Mesh>()
|
|
124
|
+
|
|
125
|
+
const render = (enabled: boolean) =>
|
|
126
|
+
root.render(
|
|
127
|
+
<Selection>
|
|
128
|
+
<Capture onSnapshot={(s) => snapshots.push(s)} />
|
|
129
|
+
<Select enabled={enabled}>
|
|
130
|
+
<mesh ref={meshRef}>
|
|
131
|
+
<boxGeometry />
|
|
132
|
+
<meshBasicMaterial />
|
|
133
|
+
</mesh>
|
|
134
|
+
</Select>
|
|
135
|
+
</Selection>
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
await React.act(async () => render(true))
|
|
139
|
+
await flush()
|
|
140
|
+
await flush()
|
|
141
|
+
expect(snapshots[snapshots.length - 1]).toContain(meshRef.current)
|
|
142
|
+
|
|
143
|
+
await React.act(async () => render(false))
|
|
144
|
+
await flush()
|
|
145
|
+
await flush()
|
|
146
|
+
expect(snapshots[snapshots.length - 1]).not.toContain(meshRef.current)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('selects a SkinnedMesh - isMesh is inherited, not just the exact type string', async () => {
|
|
150
|
+
const snapshots: THREE.Object3D[][] = []
|
|
151
|
+
const meshRef = React.createRef<THREE.SkinnedMesh>()
|
|
152
|
+
|
|
153
|
+
await React.act(async () =>
|
|
154
|
+
root.render(
|
|
155
|
+
<Selection>
|
|
156
|
+
<Capture onSnapshot={(s) => snapshots.push(s)} />
|
|
157
|
+
<Select enabled>
|
|
158
|
+
<skinnedMesh ref={meshRef} />
|
|
159
|
+
</Select>
|
|
160
|
+
</Selection>
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
await flush()
|
|
164
|
+
await flush()
|
|
165
|
+
|
|
166
|
+
expect(snapshots[snapshots.length - 1]).toContain(meshRef.current)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('does not duplicate an object claimed by nested Selects', async () => {
|
|
170
|
+
const snapshots: THREE.Object3D[][] = []
|
|
171
|
+
const meshRef = React.createRef<THREE.Mesh>()
|
|
172
|
+
|
|
173
|
+
await React.act(async () =>
|
|
174
|
+
root.render(
|
|
175
|
+
<Selection>
|
|
176
|
+
<Capture onSnapshot={(s) => snapshots.push(s)} />
|
|
177
|
+
<Select enabled>
|
|
178
|
+
<Select enabled>
|
|
179
|
+
<mesh ref={meshRef}>
|
|
180
|
+
<boxGeometry />
|
|
181
|
+
<meshBasicMaterial />
|
|
182
|
+
</mesh>
|
|
183
|
+
</Select>
|
|
184
|
+
</Select>
|
|
185
|
+
</Selection>
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
await flush()
|
|
189
|
+
await flush()
|
|
190
|
+
await flush()
|
|
191
|
+
|
|
192
|
+
const last = snapshots[snapshots.length - 1]
|
|
193
|
+
expect(last.filter((o) => o === meshRef.current)).toHaveLength(1)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('keeps an object selected via the outer Select after the inner one disables', async () => {
|
|
197
|
+
const snapshots: THREE.Object3D[][] = []
|
|
198
|
+
const meshRef = React.createRef<THREE.Mesh>()
|
|
199
|
+
|
|
200
|
+
const render = (innerEnabled: boolean) =>
|
|
201
|
+
root.render(
|
|
202
|
+
<Selection>
|
|
203
|
+
<Capture onSnapshot={(s) => snapshots.push(s)} />
|
|
204
|
+
<Select enabled>
|
|
205
|
+
<Select enabled={innerEnabled}>
|
|
206
|
+
<mesh ref={meshRef}>
|
|
207
|
+
<boxGeometry />
|
|
208
|
+
<meshBasicMaterial />
|
|
209
|
+
</mesh>
|
|
210
|
+
</Select>
|
|
211
|
+
</Select>
|
|
212
|
+
</Selection>
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
await React.act(async () => render(true))
|
|
216
|
+
await flush()
|
|
217
|
+
await flush()
|
|
218
|
+
await flush()
|
|
219
|
+
expect(snapshots[snapshots.length - 1]).toContain(meshRef.current)
|
|
220
|
+
|
|
221
|
+
await React.act(async () => render(false))
|
|
222
|
+
await flush()
|
|
223
|
+
await flush()
|
|
224
|
+
await flush()
|
|
225
|
+
|
|
226
|
+
// The inner Select's own cleanup drops its claim, but the mesh never
|
|
227
|
+
// left the outer Select's subtree - its own traversal still finds it.
|
|
228
|
+
expect(snapshots[snapshots.length - 1]).toContain(meshRef.current)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('does not change the selected array reference on a re-render where children only changes identity', async () => {
|
|
232
|
+
const refs: THREE.Object3D[][] = []
|
|
233
|
+
const meshRef = React.createRef<THREE.Mesh>()
|
|
234
|
+
|
|
235
|
+
function CaptureRef() {
|
|
236
|
+
const api = React.useContext(selectionContext)
|
|
237
|
+
React.useEffect(() => {
|
|
238
|
+
if (api) refs.push(api.selected)
|
|
239
|
+
})
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// A fresh JSX tree every call, like a parent re-rendering for an
|
|
244
|
+
// unrelated reason - Select's own `children` prop is a new reference
|
|
245
|
+
// each time even though the underlying mesh never changes.
|
|
246
|
+
const render = () =>
|
|
247
|
+
root.render(
|
|
248
|
+
<Selection>
|
|
249
|
+
<CaptureRef />
|
|
250
|
+
<Select enabled>
|
|
251
|
+
<mesh ref={meshRef}>
|
|
252
|
+
<boxGeometry />
|
|
253
|
+
<meshBasicMaterial />
|
|
254
|
+
</mesh>
|
|
255
|
+
</Select>
|
|
256
|
+
</Selection>
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
await React.act(async () => render())
|
|
260
|
+
await flush()
|
|
261
|
+
await flush()
|
|
262
|
+
|
|
263
|
+
refs.length = 0
|
|
264
|
+
for (let i = 0; i < 5; i++) {
|
|
265
|
+
await React.act(async () => render())
|
|
266
|
+
await flush()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
expect(new Set(refs).size).toBe(1)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('a cleanup updater returns the same reference when nothing is left for it to remove', async () => {
|
|
273
|
+
const meshRef = React.createRef<THREE.Mesh>()
|
|
274
|
+
const updaters: Array<(prev: THREE.Object3D[]) => THREE.Object3D[]> = []
|
|
275
|
+
|
|
276
|
+
// React batches both nested Selects' cleanup calls into a single
|
|
277
|
+
// re-render regardless of this bail-out (the net change from mount to
|
|
278
|
+
// unmount is real), so the optimization isn't observable through
|
|
279
|
+
// rendering alone. Capture the raw updater functions instead and
|
|
280
|
+
// replay them directly to verify the second one bails.
|
|
281
|
+
function CapturingSelection({ children }: { children: React.ReactNode }) {
|
|
282
|
+
const [selected, setSelected] = React.useState<THREE.Object3D[]>([])
|
|
283
|
+
const select = React.useCallback((updater: React.SetStateAction<THREE.Object3D[]>) => {
|
|
284
|
+
if (typeof updater === 'function') updaters.push(updater as (prev: THREE.Object3D[]) => THREE.Object3D[])
|
|
285
|
+
setSelected(updater)
|
|
286
|
+
}, [])
|
|
287
|
+
const value = React.useMemo(() => ({ selected, select, enabled: true }), [selected, select])
|
|
288
|
+
return <selectionContext.Provider value={value}>{children}</selectionContext.Provider>
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const render = (mounted: boolean) =>
|
|
292
|
+
root.render(
|
|
293
|
+
<CapturingSelection>
|
|
294
|
+
{mounted && (
|
|
295
|
+
<Select enabled>
|
|
296
|
+
<Select enabled>
|
|
297
|
+
<mesh ref={meshRef}>
|
|
298
|
+
<boxGeometry />
|
|
299
|
+
<meshBasicMaterial />
|
|
300
|
+
</mesh>
|
|
301
|
+
</Select>
|
|
302
|
+
</Select>
|
|
303
|
+
)}
|
|
304
|
+
</CapturingSelection>
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
await React.act(async () => render(true))
|
|
308
|
+
await flush()
|
|
309
|
+
await flush()
|
|
310
|
+
await flush()
|
|
311
|
+
|
|
312
|
+
const mesh = meshRef.current!
|
|
313
|
+
updaters.length = 0
|
|
314
|
+
await React.act(async () => render(false))
|
|
315
|
+
await flush()
|
|
316
|
+
await flush()
|
|
317
|
+
await flush()
|
|
318
|
+
|
|
319
|
+
expect(updaters).toHaveLength(2)
|
|
320
|
+
const afterFirst = updaters[0]([mesh])
|
|
321
|
+
const afterSecond = updaters[1](afterFirst)
|
|
322
|
+
expect(afterSecond).toBe(afterFirst)
|
|
323
|
+
})
|
|
324
|
+
})
|