Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Merged): restores recursive implementation #2366

Merged
merged 1 commit into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .storybook/stories/Merged.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react'
import { Meta, StoryObj } from '@storybook/react'

import { Setup } from '../Setup'

import { useGLTF, Merged, Instance } from '../../src'

export default {
title: 'Performance/Merged',
component: Merged,
decorators: [
(Story) => (
<Setup>
<Story />
</Setup>
),
],
} satisfies Meta<typeof Merged>

type Story = StoryObj<typeof Merged>

function Scene() {
const { nodes } = useGLTF('suzanne.glb', true)
return <Merged meshes={nodes}>{({ Suzanne }) => <Suzanne />}</Merged>
}

export const DefaultStory = {
render: (args) => <Scene {...args} />,
name: 'Default',
} satisfies Story
85 changes: 57 additions & 28 deletions src/core/Instances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,39 +218,68 @@ export const Instances: ForwardRefComponent<InstancesProps, THREE.InstancedMesh>
)
})

export interface MergedProps extends InstancesProps {
meshes: THREE.Mesh[]
children: React.ReactNode
export interface MergedProps extends Omit<InstancesProps, 'children'> {
meshes: THREE.Mesh[] | Record<string, THREE.Object3D>
children: (
...instances: [React.FC<InstanceProps> & Record<string, React.FC<InstanceProps>>, ...React.FC<InstanceProps>[]]
) => React.ReactNode
}

export const Merged: ForwardRefComponent<any, THREE.Group> = React.forwardRef<THREE.Group, any>(function Merged(
{ meshes, children, ...rest },
ref
) {
const instances: React.FC[] = []

if (Array.isArray(meshes)) {
for (const mesh of meshes) {
if (mesh?.isMesh) {
instances.push((props) => (
<Instances key={mesh.geometry.uuid} geometry={mesh.geometry} material={mesh.material} {...rest} {...props} />
))
}
}
} else if (meshes != null && typeof meshes === 'object') {
for (const key in meshes) {
const mesh = meshes[key]
if (mesh?.isMesh) {
instances.push((props) => (
<Instances key={mesh.geometry.uuid} geometry={mesh.geometry} material={mesh.material} {...rest} {...props} />
))
}
}
}
// TODO: make this non-recursive and type-safe
export const Merged: ForwardRefComponent<MergedProps, THREE.Group> = /* @__PURE__ */ React.forwardRef<
THREE.Group,
MergedProps
>(function Merged({ meshes, children, ...props }, ref) {
const isArray = Array.isArray(meshes)
// Filter out meshes from collections, which may contain non-meshes
// @ts-expect-error
if (!isArray) for (const key of Object.keys(meshes)) if (!meshes[key].isMesh) delete meshes[key]

const render = (args) =>
isArray
? // @ts-expect-error
children(...args)
: children(
// @ts-expect-error
Object.keys(meshes)
// @ts-expect-error
.filter((key) => meshes[key].isMesh)
.reduce((acc, key, i) => ({ ...acc, [key]: args[i] }), {})
)

// @ts-expect-error
const components = (isArray ? meshes : Object.values(meshes)).map(({ geometry, material }) => (
<Instances key={geometry.uuid} geometry={geometry} material={material} {...props} />
))

return <group ref={ref}>{children(instances)}</group>
return <group ref={ref}>{renderRecursive(render, components)}</group>
})

// https://github.com/jamesplease/react-composer
function renderRecursive(
render: Function,
components: Array<React.ReactElement<{ children: any }> | Function>,
results: unknown[] = []
): React.ReactElement {
// Once components is exhausted, we can render out the results array.
if (!components[0]) {
return render(results)
}

// Continue recursion for remaining items.
// results.concat([value]) ensures [...results, value] instead of [...results, ...value]
function nextRender(value) {
return renderRecursive(render, components.slice(1), results.concat([value]))
}

// Each props.components entry is either an element or function [element factory]
return typeof components[0] === 'function'
? // When it is a function, produce an element by invoking it with "render component values".
components[0]({ results, render: nextRender })
: // When it is an element, enhance the element's props with the render prop.
React.cloneElement(components[0], { children: nextRender })
}

/** Idea and implementation for global instances and instanced attributes by
/* Matias Gonzalez Fernandez https://x.com/matiNotFound
/* and Paul Henschel https://x.com/0xca0a
Expand Down
Loading