Skip to content

Commit

Permalink
feat(useLoader): support loader instances
Browse files Browse the repository at this point in the history
  • Loading branch information
CodyJasonBennett committed Dec 28, 2023
1 parent 9a61979 commit 6d53ff2
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 14 deletions.
42 changes: 28 additions & 14 deletions packages/fiber/src/core/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as THREE from 'three'
import * as React from 'react'
import { suspend, preload, clear } from 'suspend-react'
import { context, RootState, RenderCallback, UpdateCallback, StageTypes, RootStore } from './store'
import { buildGraph, ObjectMap, is, useMutableCallback, useIsomorphicLayoutEffect } from './utils'
import { buildGraph, ObjectMap, is, useMutableCallback, useIsomorphicLayoutEffect, isObject3D } from './utils'
import { Stages } from './stages'
import type { Instance } from './reconciler'

Expand Down Expand Up @@ -91,24 +91,35 @@ export type Extensions<T> = (loader: Loader<T>) => void

const memoizedLoaders = new WeakMap<LoaderProto<any>, Loader<any>>()

const isConstructor = <T,>(value: unknown): value is LoaderProto<T> =>
typeof value === 'function' && value?.prototype?.constructor === value

function loadingFn<T>(extensions?: Extensions<T>, onProgress?: (event: ProgressEvent) => void) {
return function (Proto: LoaderProto<T>, ...input: string[]) {
// Construct new loader and run extensions
let loader = memoizedLoaders.get(Proto)!
if (!loader) {
loader = new Proto()
memoizedLoaders.set(Proto, loader)
return async function (Proto: Loader<T> | LoaderProto<T>, ...input: string[]) {
let loader: Loader<any>

// Construct and cache loader if constructor was passed
if (isConstructor(Proto)) {
loader = memoizedLoaders.get(Proto)!
if (!loader) {
loader = new Proto()
memoizedLoaders.set(Proto, loader)
}
} else {
loader = Proto
}

// Apply loader extensions
if (extensions) extensions(loader)

// Go through the urls and load them
return Promise.all(
input.map(
(input) =>
new Promise<LoaderResult<T>>((res, reject) =>
loader.load(
input,
(data) => res(data?.scene instanceof THREE.Object3D ? Object.assign(data, buildGraph(data.scene)) : data),
(data) => res(isObject3D(data?.scene) ? Object.assign(data, buildGraph(data.scene)) : data),
onProgress,
(error) => reject(new Error(`Could not load ${input}: ${(error as ErrorEvent)?.message}`)),
),
Expand All @@ -125,14 +136,14 @@ function loadingFn<T>(extensions?: Extensions<T>, onProgress?: (event: ProgressE
* @see https://docs.pmnd.rs/react-three-fiber/api/hooks#useloader
*/
export function useLoader<T, U extends string | string[] | string[][]>(
Proto: LoaderProto<T>,
loader: Loader<T> | LoaderProto<T>,
input: U,
extensions?: Extensions<T>,
onProgress?: (event: ProgressEvent) => void,
) {
// Use suspense to load async assets
const keys = (Array.isArray(input) ? input : [input]) as string[]
const results = suspend(loadingFn(extensions, onProgress), [Proto, ...keys], { equal: is.equ })
const results = suspend(loadingFn(extensions, onProgress), [loader, ...keys], { equal: is.equ })
// Return the object(s)
return (Array.isArray(input) ? results : results[0]) as unknown as U extends any[]
? LoaderResult<T>[]
Expand All @@ -143,18 +154,21 @@ export function useLoader<T, U extends string | string[] | string[][]>(
* Preloads an asset into cache as a side-effect.
*/
useLoader.preload = function <T, U extends string | string[] | string[][]>(
Proto: LoaderProto<T>,
loader: Loader<T> | LoaderProto<T>,
input: U,
extensions?: Extensions<T>,
): void {
const keys = (Array.isArray(input) ? input : [input]) as string[]
return preload(loadingFn(extensions), [Proto, ...keys])
return preload(loadingFn(extensions), [loader, ...keys])
}

/**
* Removes a loaded asset from cache.
*/
useLoader.clear = function <T, U extends string | string[] | string[][]>(Proto: LoaderProto<T>, input: U): void {
useLoader.clear = function <T, U extends string | string[] | string[][]>(
loader: Loader<T> | LoaderProto<T>,
input: U,
): void {
const keys = (Array.isArray(input) ? input : [input]) as string[]
return clear([Proto, ...keys])
return clear([loader, ...keys])
}
18 changes: 18 additions & 0 deletions packages/fiber/tests/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,24 @@ describe('hooks', () => {
expect(extensions).toBeCalledTimes(1)
})

it('can handle useLoader with an existing loader instance', async () => {
class Loader extends THREE.Loader {
load(_url: string, onLoad: (result: null) => void): void {
onLoad(null)
}
}

const loader = new Loader()
let proto!: Loader

function Test(): null {
return useLoader(loader, '', (loader) => (proto = loader))
}
await act(async () => root.render(<Test />))

expect(proto).toBe(loader)
})

it('can handle useLoader with a loader extension', async () => {
class Loader extends THREE.Loader {
load(_url: string, onLoad: (result: null) => void): void {
Expand Down

0 comments on commit 6d53ff2

Please sign in to comment.