浏览代码

fix(types): `useLoader` generics (#781)

* fix(types): improve useLoader generics

* docs: jsdocs
Alvaro Saburido 10 月之前
父节点
当前提交
b51d679237

+ 14 - 0
playground/src/composables/useFBX.ts

@@ -0,0 +1,14 @@
+import { FBXLoader } from 'three-stdlib'
+import { useLoader } from '@tresjs/core'
+import type { Object3D } from 'three'
+
+/**
+ * Loads an FBX file and returns a THREE.Object3D.
+ *
+ * @export
+ * @param {(string | string[])} path
+ * @return {*}  {Promise<Object3D>}
+ */
+export async function useFBX(path: string | string[]): Promise<Object3D> {
+  return (await useLoader(FBXLoader, path)) as unknown as Object3D
+}

+ 82 - 0
playground/src/composables/useGLTF.ts

@@ -0,0 +1,82 @@
+import { type TresLoader, type TresObject3D, useLoader } from '@tresjs/core'
+import type { AnimationClip, Material, Scene } from 'three'
+import { DRACOLoader, GLTFLoader } from 'three-stdlib'
+import type { GLTF } from 'three-stdlib'
+
+export interface GLTFLoaderOptions {
+  /**
+   * Whether to use Draco compression.
+   *
+   * @type {boolean}
+   * @memberof GLTFLoaderOptions
+   */
+  draco?: boolean
+  /**
+   * The path to the Draco decoder.
+   *
+   * @type {string}
+   * @memberof GLTFLoaderOptions
+   */
+  decoderPath?: string
+}
+
+export interface GLTFResult {
+  animations: Array<AnimationClip>
+  nodes: Record<string, TresObject3D>
+  materials: Record<string, Material>
+  scene: Scene
+}
+
+let dracoLoader: DRACOLoader | null = null
+
+export interface TresGLTFLoader extends TresLoader<GLTF> {
+  setDRACOLoader?: (dracoLoader: DRACOLoader) => void
+}
+
+/**
+ * Sets the extensions for the GLTFLoader.
+ *
+ * @param {GLTFLoaderOptions} options
+ * @param {(loader: TresGLTFLoader) => void} [extendLoader]
+ * @return {*}
+ */
+function setExtensions(options: GLTFLoaderOptions, extendLoader?: (loader: TresGLTFLoader) => void) {
+  return (loader: TresGLTFLoader) => {
+    if (extendLoader) {
+      extendLoader(loader)
+    }
+    if (options.draco) {
+      if (!dracoLoader) {
+        dracoLoader = new DRACOLoader()
+      }
+      dracoLoader.setDecoderPath(options.decoderPath || 'https://www.gstatic.com/draco/versioned/decoders/1.4.3/')
+      if (loader.setDRACOLoader) {
+        loader.setDRACOLoader(dracoLoader)
+      }
+    }
+  }
+}
+
+/**
+ * Loads a GLTF file and returns a THREE.Object3D.
+ *
+ * @export
+ * @param {(string | string[])} path
+ * @param {GLTFLoaderOptions} [options]
+ *
+ *
+ * @param {(loader: GLTFLoader) => void} [extendLoader]
+ * @return {*}  {Promise<GLTFResult>}
+ */
+export async function useGLTF<T extends string | string[]>(
+  path: T,
+  options: GLTFLoaderOptions = {
+    draco: false,
+  },
+  extendLoader?: (loader: TresGLTFLoader) => void,
+): Promise<T extends string[] ? GLTFResult[] : GLTFResult> {
+  const gltfModel = (await useLoader<GLTF>(GLTFLoader, path, setExtensions(options, extendLoader))) as unknown as GLTFResult
+  dracoLoader?.dispose()
+  dracoLoader = null
+  return gltfModel as T extends string[] ? GLTFResult[] : GLTFResult
+}

+ 2 - 0
playground/src/pages/index.vue

@@ -5,6 +5,7 @@ import {
   cameraRoutes,
   eventsRoutes,
   issuesRoutes,
+  loaderRoutes,
   miscRoutes,
   modelsRoutes,
 } from '../router/routes'
@@ -14,6 +15,7 @@ const sections = [
   { icon: '🤓', title: 'Advanced', routes: advancedRoutes },
   { icon: '📣', title: 'Events', routes: eventsRoutes },
   { icon: '📷', title: 'Camera', routes: cameraRoutes },
+  { icon: '🛜', title: 'Loaders', routes: loaderRoutes },
   { icon: '🐇', title: 'Models', routes: modelsRoutes },
   { icon: '🤪', title: 'Misc', routes: miscRoutes },
   { icon: '🔬', title: 'Issues', routes: issuesRoutes },

+ 14 - 0
playground/src/pages/loaders/fbx-loader/TheExperience.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import { OrbitControls } from '@tresjs/cientos'
+import TheModel from './TheModel.vue'
+</script>
+
+<template>
+  <TresPerspectiveCamera :position="[3, 3, 3]" />
+  <OrbitControls />
+  <TresGridHelper />
+  <TresAmbientLight :intensity="1" />
+  <Suspense>
+    <TheModel />
+  </Suspense>
+</template>

+ 10 - 0
playground/src/pages/loaders/fbx-loader/TheModel.vue

@@ -0,0 +1,10 @@
+<script setup lang="ts">
+import { useFBX } from '../../../composables/useFBX'
+
+const scene = await useFBX('https://raw.githubusercontent.com/Tresjs/assets/main/models/fbx/low-poly-truck/Jeep_done.fbx')
+scene.scale.set(0.01, 0.01, 0.01)
+</script>
+
+<template>
+  <primitive :object="scene" />
+</template>

+ 11 - 0
playground/src/pages/loaders/fbx-loader/index.vue

@@ -0,0 +1,11 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+
+import TheExperience from './TheExperience.vue'
+</script>
+
+<template>
+  <TresCanvas clear-color="#C0ffee">
+    <TheExperience />
+  </TresCanvas>
+</template>

+ 14 - 0
playground/src/pages/loaders/gltf-loader/TheExperience.vue

@@ -0,0 +1,14 @@
+<script setup lang="ts">
+import { OrbitControls } from '@tresjs/cientos'
+import TheModel from './TheModel.vue'
+</script>
+
+<template>
+  <TresPerspectiveCamera :position="[3, 3, 3]" />
+  <OrbitControls />
+  <TresGridHelper />
+  <TresAmbientLight :intensity="1" />
+  <Suspense>
+    <TheModel />
+  </Suspense>
+</template>

+ 10 - 0
playground/src/pages/loaders/gltf-loader/TheModel.vue

@@ -0,0 +1,10 @@
+<script setup lang="ts">
+import { useGLTF } from '../../../composables/useGLTF'
+
+const { nodes } = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
+const model = nodes.Cube
+</script>
+
+<template>
+  <primitive :object="model" />
+</template>

+ 11 - 0
playground/src/pages/loaders/gltf-loader/index.vue

@@ -0,0 +1,11 @@
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+
+import TheExperience from './TheExperience.vue'
+</script>
+
+<template>
+  <TresCanvas clear-color="#C0ffee">
+    <TheExperience />
+  </TresCanvas>
+</template>

+ 3 - 0
playground/src/router/routes/index.ts

@@ -5,6 +5,7 @@ import { basicRoutes } from './basic'
 import { advancedRoutes } from './advanced'
 import { miscRoutes } from './misc'
 import { issuesRoutes } from './issues'
+import { loaderRoutes } from './loaders'
 
 const allRoutes = [
   ...basicRoutes,
@@ -14,6 +15,7 @@ const allRoutes = [
   ...modelsRoutes,
   ...miscRoutes,
   ...issuesRoutes,
+  ...loaderRoutes,
 ]
 
 export {
@@ -25,4 +27,5 @@ export {
   miscRoutes,
   issuesRoutes,
   allRoutes,
+  loaderRoutes,
 }

+ 12 - 0
playground/src/router/routes/loaders.ts

@@ -0,0 +1,12 @@
+export const loaderRoutes = [
+  {
+    path: '/loaders/gltf',
+    name: 'GLTF Loader',
+    component: () => import('../../pages/loaders/gltf-loader/index.vue'),
+  },
+  {
+    path: '/loaders/fbx',
+    name: 'FBX Loader',
+    component: () => import('../../pages/loaders/fbx-loader/index.vue'),
+  },
+]

+ 21 - 21
src/composables/useLoader/index.ts

@@ -1,18 +1,20 @@
+import type { Loader, LoadingManager, Object3D } from 'three'
 import { isArray } from '@alvarosabu/utils'
-import type { Loader, Object3D } from 'three'
-import { useLogger } from '../useLogger'
+import type { TresObject } from '../../types'
+import { useLogger } from '..'
 
 export interface TresLoader<T> extends Loader {
   load: (
     url: string,
-    onLoad?: (result: T) => void,
+    onLoad: (result: T) => void,
     onProgress?: (event: ProgressEvent) => void,
-    onError?: (event: ErrorEvent) => void,
-  ) => unknown
+    onError?: (event: ErrorEvent) => void
+  ) => void
   loadAsync: (url: string, onProgress?: (event: ProgressEvent) => void) => Promise<T>
 }
 
-export type LoaderProto<T> = new (...args: any) => TresLoader<T extends unknown ? any : T>
+export type LoaderProto<T> = new (manager?: LoadingManager) => TresLoader<T>
+
 export type LoaderReturnType<T, L extends LoaderProto<T>> = T extends unknown
   ? Awaited<ReturnType<InstanceType<L>['loadAsync']>>
   : T
@@ -55,28 +57,27 @@ export type Extensions<T extends { prototype: LoaderProto<any> }> = (loader: T['
  * ```
  *
  * @export
- * @template T
- * @template U
- * @param {T} Loader
- * @param {U} url
- * @param {Extensions<T>} [extensions]
+ * @template LoaderProto<T>
+ * @template string | string[],
+ * @param {LoaderProto<T>} Loader
+ * @param {string | string[],} url
+ * @param {Extensions<TresLoader<T>>} [extensions]
  * @param {(event: ProgressEvent<EventTarget>) => void} [onProgress]
  * @param {(proto: TresLoader<T>) => void} [cb]
  * @return {*}
  */
-export async function useLoader<T extends LoaderProto<T>, U extends string | string[]>(
-  Loader: T,
-  url: U,
-  extensions?: Extensions<T>,
+export async function useLoader<T>(
+  Loader: LoaderProto<T>,
+  url: string | string[],
+  extensions?: (loader: TresLoader<T>) => void,
   onProgress?: (event: ProgressEvent<EventTarget>) => void,
   cb?: (proto: TresLoader<T>) => void,
-) {
+): Promise<T | T[]> {
   const { logError } = useLogger()
   const proto = new Loader()
   if (cb) {
     cb(proto)
   }
-
   if (extensions) {
     extensions(proto)
   }
@@ -88,7 +89,8 @@ export async function useLoader<T extends LoaderProto<T>, U extends string | str
       new Promise((resolve, reject) => {
         proto.load(
           path,
-          (data) => {
+          (result: T) => {
+            const data = result as unknown as TresObject
             if (data.scene) {
               Object.assign(data, trasverseObjects(data.scene))
             }
@@ -100,7 +102,5 @@ export async function useLoader<T extends LoaderProto<T>, U extends string | str
       }),
   )
 
-  return (isArray(url) ? await Promise.all(results) : await results[0]) as U extends any[]
-    ? LoaderReturnType<T, T>[]
-    : LoaderReturnType<T, T>
+  return (isArray(url) ? await Promise.all(results) : await results[0]) as T | T[]
 }