Skip to content

Commit aa0f742

Browse files
fix(Merged): restores recursive implementation (#2366)
1 parent 71b1128 commit aa0f742

File tree

2 files changed

+87
-28
lines changed

2 files changed

+87
-28
lines changed

.storybook/stories/Merged.stories.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from 'react'
2+
import { Meta, StoryObj } from '@storybook/react'
3+
4+
import { Setup } from '../Setup'
5+
6+
import { useGLTF, Merged, Instance } from '../../src'
7+
8+
export default {
9+
title: 'Performance/Merged',
10+
component: Merged,
11+
decorators: [
12+
(Story) => (
13+
<Setup>
14+
<Story />
15+
</Setup>
16+
),
17+
],
18+
} satisfies Meta<typeof Merged>
19+
20+
type Story = StoryObj<typeof Merged>
21+
22+
function Scene() {
23+
const { nodes } = useGLTF('suzanne.glb', true)
24+
return <Merged meshes={nodes}>{({ Suzanne }) => <Suzanne />}</Merged>
25+
}
26+
27+
export const DefaultStory = {
28+
render: (args) => <Scene {...args} />,
29+
name: 'Default',
30+
} satisfies Story

src/core/Instances.tsx

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -218,39 +218,68 @@ export const Instances: ForwardRefComponent<InstancesProps, THREE.InstancedMesh>
218218
)
219219
})
220220

221-
export interface MergedProps extends InstancesProps {
222-
meshes: THREE.Mesh[]
223-
children: React.ReactNode
221+
export interface MergedProps extends Omit<InstancesProps, 'children'> {
222+
meshes: THREE.Mesh[] | Record<string, THREE.Object3D>
223+
children: (
224+
...instances: [React.FC<InstanceProps> & Record<string, React.FC<InstanceProps>>, ...React.FC<InstanceProps>[]]
225+
) => React.ReactNode
224226
}
225227

226-
export const Merged: ForwardRefComponent<any, THREE.Group> = React.forwardRef<THREE.Group, any>(function Merged(
227-
{ meshes, children, ...rest },
228-
ref
229-
) {
230-
const instances: React.FC[] = []
231-
232-
if (Array.isArray(meshes)) {
233-
for (const mesh of meshes) {
234-
if (mesh?.isMesh) {
235-
instances.push((props) => (
236-
<Instances key={mesh.geometry.uuid} geometry={mesh.geometry} material={mesh.material} {...rest} {...props} />
237-
))
238-
}
239-
}
240-
} else if (meshes != null && typeof meshes === 'object') {
241-
for (const key in meshes) {
242-
const mesh = meshes[key]
243-
if (mesh?.isMesh) {
244-
instances.push((props) => (
245-
<Instances key={mesh.geometry.uuid} geometry={mesh.geometry} material={mesh.material} {...rest} {...props} />
246-
))
247-
}
248-
}
249-
}
228+
// TODO: make this non-recursive and type-safe
229+
export const Merged: ForwardRefComponent<MergedProps, THREE.Group> = /* @__PURE__ */ React.forwardRef<
230+
THREE.Group,
231+
MergedProps
232+
>(function Merged({ meshes, children, ...props }, ref) {
233+
const isArray = Array.isArray(meshes)
234+
// Filter out meshes from collections, which may contain non-meshes
235+
// @ts-expect-error
236+
if (!isArray) for (const key of Object.keys(meshes)) if (!meshes[key].isMesh) delete meshes[key]
237+
238+
const render = (args) =>
239+
isArray
240+
? // @ts-expect-error
241+
children(...args)
242+
: children(
243+
// @ts-expect-error
244+
Object.keys(meshes)
245+
// @ts-expect-error
246+
.filter((key) => meshes[key].isMesh)
247+
.reduce((acc, key, i) => ({ ...acc, [key]: args[i] }), {})
248+
)
249+
250+
// @ts-expect-error
251+
const components = (isArray ? meshes : Object.values(meshes)).map(({ geometry, material }) => (
252+
<Instances key={geometry.uuid} geometry={geometry} material={material} {...props} />
253+
))
250254

251-
return <group ref={ref}>{children(instances)}</group>
255+
return <group ref={ref}>{renderRecursive(render, components)}</group>
252256
})
253257

258+
// https://github.com/jamesplease/react-composer
259+
function renderRecursive(
260+
render: Function,
261+
components: Array<React.ReactElement<{ children: any }> | Function>,
262+
results: unknown[] = []
263+
): React.ReactElement {
264+
// Once components is exhausted, we can render out the results array.
265+
if (!components[0]) {
266+
return render(results)
267+
}
268+
269+
// Continue recursion for remaining items.
270+
// results.concat([value]) ensures [...results, value] instead of [...results, ...value]
271+
function nextRender(value) {
272+
return renderRecursive(render, components.slice(1), results.concat([value]))
273+
}
274+
275+
// Each props.components entry is either an element or function [element factory]
276+
return typeof components[0] === 'function'
277+
? // When it is a function, produce an element by invoking it with "render component values".
278+
components[0]({ results, render: nextRender })
279+
: // When it is an element, enhance the element's props with the render prop.
280+
React.cloneElement(components[0], { children: nextRender })
281+
}
282+
254283
/** Idea and implementation for global instances and instanced attributes by
255284
/* Matias Gonzalez Fernandez https://x.com/matiNotFound
256285
/* and Paul Henschel https://x.com/0xca0a

0 commit comments

Comments
 (0)