Explorar o código

Merge pull request #204 from Tresjs/feature/primitives

feat: primitives
Alvaro Saburido %!s(int64=2) %!d(string=hai) anos
pai
achega
0701db8fc7

+ 44 - 23
docs/examples/load-models.md

@@ -23,16 +23,18 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
 const { scene } = await useLoader(GLTFLoader, '/models/AkuAku.gltf')
 const { scene } = await useLoader(GLTFLoader, '/models/AkuAku.gltf')
 ```
 ```
 
 
-Then you can pass the model scene to a `TresMesh` component:
+Then you can pass the model scene to a TresJS `primitive`:
 
 
 ```html{3}
 ```html{3}
-<Suspense>
-  <TresCanvas>
-      <TresMesh v-bind="scene" />
-  </TresCanvas>
-</Suspense>
+<TresCanvas>
+  <Suspense>
+    <primitive :object="scene" />
+  </Suspense>
+</TresCanvas>
 ```
 ```
 
 
+> The `<primitive />` component is not a standalone component in the Tres source code. Instead, it's a part of the Tres core functionality. When you use `<primitive>`, it is translated to a `createElement` call, which creates the appropriate Three.js object based on the provided "object" prop.
+
 Notice in the example above that we are using the `Suspense` component to wrap the `TresCanvas` component. This is because `useLoader` returns a `Promise` and we need to wait for it to resolve before rendering the scene.
 Notice in the example above that we are using the `Suspense` component to wrap the `TresCanvas` component. This is because `useLoader` returns a `Promise` and we need to wait for it to resolve before rendering the scene.
 
 
 ## Using `useGLTF`
 ## Using `useGLTF`
@@ -42,7 +44,7 @@ A more convenient way of loading models is using the `useGLTF` composable availa
 ```ts
 ```ts
 import { useGLTF } from '@tresjs/cientos'
 import { useGLTF } from '@tresjs/cientos'
 
 
-const { scene } = await useGLTF('/models/AkuAku.gltf')
+const { scene, nodes, animations, materials } = await useGLTF('/models/AkuAku.gltf')
 ```
 ```
 
 
 An advantage of using `useGLTF`is that you can pass a `draco` prop to enable [Draco compression](https://threejs.org/docs/index.html?q=drac#examples/en/loaders/DRACOLoader) for the model. This will reduce the size of the model and improve performance.
 An advantage of using `useGLTF`is that you can pass a `draco` prop to enable [Draco compression](https://threejs.org/docs/index.html?q=drac#examples/en/loaders/DRACOLoader) for the model. This will reduce the size of the model and improve performance.
@@ -50,7 +52,26 @@ An advantage of using `useGLTF`is that you can pass a `draco` prop to enable [Dr
 ```ts
 ```ts
 import { useGLTF } from '@tresjs/cientos'
 import { useGLTF } from '@tresjs/cientos'
 
 
-const { scene } = await useGLTF('/models/AkuAku.gltf', { draco: true })
+const { scene, nodes, animations, materials } = await useGLTF('/models/AkuAku.gltf', { draco: true })
+```
+
+Alternatively you can easily select Objects inside the model using `nodes` property
+
+```vue
+<script setup lang="ts">
+import { useGLTF } from '@tresjs/cientos'
+
+const { scene, nodes, animations, materials } = await useGLTF('/models/AkuAku.gltf', { draco: true })
+</script>
+<template>
+  <TresCanvas clear-color="#82DBC5" shadows alpha>
+    <TresPerspectiveCamera :position="[11, 11, 11]" />
+    <OrbitControls />
+    <Suspense>
+      <primitive :object="nodes.MyModel" />
+    </Suspense>
+  </TresCanvas>
+</template>
 ```
 ```
 
 
 ## Using `GLTFModel`
 ## Using `GLTFModel`
@@ -67,8 +88,8 @@ import { OrbitControls, GLTFModel } from '@tresjs/cientos'
     <OrbitControls />
     <OrbitControls />
     <Suspense>
     <Suspense>
       <GLTFModel path="/models/AkuAku.gltf" draco />
       <GLTFModel path="/models/AkuAku.gltf" draco />
-      <TresDirectionalLight :position="[-4, 8, 4]" :intensity="1.5" cast-shadow />
     </Suspense>
     </Suspense>
+    <TresDirectionalLight :position="[-4, 8, 4]" :intensity="1.5" cast-shadow />
   </TresCanvas>
   </TresCanvas>
 </template>
 </template>
 ```
 ```
@@ -88,14 +109,14 @@ const model = await useFBX('/models/AkuAku.fbx')
 Then is as straightforward as adding the scene to your scene:
 Then is as straightforward as adding the scene to your scene:
 
 
 ```html{3}
 ```html{3}
-<Suspense>
-  <TresCanvas shadows alpha>
-      <TresMesh v-bind="scene" />
-  </TresCanvas>
-</Suspense>
+<TresCanvas shadows alpha>
+  <Suspense>
+    <primitive :object="scene" />
+  </Suspense>
+</TresCanvas>
 ```
 ```
 
 
-## FBXModel
+## FBXModel
 
 
 The `FBXModel` component is a wrapper around `useFBX` that's available from [@tresjs/cientos](https://github.com/Tresjs/tres/tree/main/packages/cientos) package. It's similar usage to `GLTFModel`:
 The `FBXModel` component is a wrapper around `useFBX` that's available from [@tresjs/cientos](https://github.com/Tresjs/tres/tree/main/packages/cientos) package. It's similar usage to `GLTFModel`:
 
 
@@ -104,13 +125,13 @@ The `FBXModel` component is a wrapper around `useFBX` that's available from [@tr
 import { OrbitControls, FBXModel } from '@tresjs/cientos'
 import { OrbitControls, FBXModel } from '@tresjs/cientos'
 </script>
 </script>
 <template>
 <template>
-    <TresCanvas clear-color="#82DBC5" shadows alpha>
-      <TresPerspectiveCamera :position="[11, 11, 11]" />
-      <OrbitControls />
-        <Suspense>
-          <FBXModel path="/models/AkuAku.fbx" />
-        </Suspense>
-        <TresDirectionalLight :position="[-4, 8, 4]" :intensity="1.5" cast-shadow />
-    </TresCanvas>
+  <TresCanvas clear-color="#82DBC5" shadows alpha>
+    <TresPerspectiveCamera :position="[11, 11, 11]" />
+    <OrbitControls />
+      <Suspense>
+        <FBXModel path="/models/AkuAku.fbx" />
+      </Suspense>
+      <TresDirectionalLight :position="[-4, 8, 4]" :intensity="1.5" cast-shadow />
+  </TresCanvas>
 </template>
 </template>
 ```
 ```

+ 2 - 0
playground/components.d.ts

@@ -12,6 +12,7 @@ declare module '@vue/runtime-core' {
     AkuAku: typeof import('./src/components/gltf/AkuAku.vue')['default']
     AkuAku: typeof import('./src/components/gltf/AkuAku.vue')['default']
     AnimatedModel: typeof import('./src/components/AnimatedModel.vue')['default']
     AnimatedModel: typeof import('./src/components/AnimatedModel.vue')['default']
     FBXModels: typeof import('./src/components/FBXModels.vue')['default']
     FBXModels: typeof import('./src/components/FBXModels.vue')['default']
+    Gltf: typeof import('./src/components/gltf/index.vue')['default']
     MultipleCanvas: typeof import('./src/components/MultipleCanvas.vue')['default']
     MultipleCanvas: typeof import('./src/components/MultipleCanvas.vue')['default']
     PortalJourney: typeof import('./src/components/portal-journey/index.vue')['default']
     PortalJourney: typeof import('./src/components/portal-journey/index.vue')['default']
     Responsiveness: typeof import('./src/components/Responsiveness.vue')['default']
     Responsiveness: typeof import('./src/components/Responsiveness.vue')['default']
@@ -32,6 +33,7 @@ declare module '@vue/runtime-core' {
     TheFirstScene: typeof import('./src/components/TheFirstScene.vue')['default']
     TheFirstScene: typeof import('./src/components/TheFirstScene.vue')['default']
     TheGizmos: typeof import('./src/components/TheGizmos.vue')['default']
     TheGizmos: typeof import('./src/components/TheGizmos.vue')['default']
     TheGroups: typeof import('./src/components/TheGroups.vue')['default']
     TheGroups: typeof import('./src/components/TheGroups.vue')['default']
+    TheModel: typeof import('./src/components/gltf/TheModel.vue')['default']
     TheParticles: typeof import('./src/components/TheParticles.vue')['default']
     TheParticles: typeof import('./src/components/TheParticles.vue')['default']
     ThePortal: typeof import('./src/components/portal-journey/ThePortal.vue')['default']
     ThePortal: typeof import('./src/components/portal-journey/ThePortal.vue')['default']
     TheSmallExperience: typeof import('./src/components/TheSmallExperience.vue')['default']
     TheSmallExperience: typeof import('./src/components/TheSmallExperience.vue')['default']

+ 1 - 3
playground/src/components/gltf/AkuAku.vue → playground/src/components/gltf/TheModel.vue

@@ -9,8 +9,6 @@ const { scene: model } = await useGLTF(
     draco: true,
     draco: true,
   },
   },
 )
 )
-model.position.set(0, 4, 0)
-model.updateMatrixWorld()
 
 
 const akuAkuRef = ref(null)
 const akuAkuRef = ref(null)
 
 
@@ -20,5 +18,5 @@ watch(akuAkuRef, value => {
 </script>
 </script>
 
 
 <template>
 <template>
-  <TresMesh ref="akuAkuRef" v-bind="model" />
+  <primitive ref="akuAkuRef" :object="model" />
 </template>
 </template>

+ 6 - 4
playground/src/components/gltf/TheExperiment.vue → playground/src/components/gltf/index.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { sRGBEncoding, BasicShadowMap, NoToneMapping } from 'three'
 import { sRGBEncoding, BasicShadowMap, NoToneMapping } from 'three'
 import { TresCanvas } from '/@/'
 import { TresCanvas } from '/@/'
-import { GLTFModel, OrbitControls } from '@tresjs/cientos'
+import { OrbitControls } from '@tresjs/cientos'
 
 
 const state = reactive({
 const state = reactive({
   clearColor: '#82DBC5',
   clearColor: '#82DBC5',
@@ -29,12 +29,14 @@ watchEffect(() => {
     <TresAmbientLight :intensity="0.5" />
     <TresAmbientLight :intensity="0.5" />
 
 
     <Suspense>
     <Suspense>
-      <GLTFModel
+      <!--  <GLTFModel
         ref="akuAkuRef"
         ref="akuAkuRef"
         path="https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/aku-aku/AkuAku.gltf"
         path="https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/aku-aku/AkuAku.gltf"
         draco
         draco
-      />
-      <!--   <AkuAku /> -->
+      /> -->
+      <TresGroup :position="[0, 4, 0]">
+        <TheModel />
+      </TresGroup>
     </Suspense>
     </Suspense>
     <TresAxesHelper />
     <TresAxesHelper />
     <TresDirectionalLight :position="[0, 2, 4]" :intensity="1" cast-shadow />
     <TresDirectionalLight :position="[0, 2, 4]" :intensity="1" cast-shadow />

+ 1 - 1
playground/src/pages/index.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts"></script>
 <script setup lang="ts"></script>
 <template>
 <template>
   <Suspense>
   <Suspense>
-    <VectorSetProps />
+    <Gltf />
   </Suspense>
   </Suspense>
 </template>
 </template>

+ 1 - 1
playground/vite.config.ts

@@ -13,7 +13,7 @@ export default defineConfig({
     vue({
     vue({
       template: {
       template: {
         compilerOptions: {
         compilerOptions: {
-          isCustomElement: tag => tag.startsWith('Tres') && tag !== 'TresCanvas',
+          isCustomElement: tag => (tag.startsWith('Tres') && tag !== 'TresCanvas') || tag === 'primitive',
         },
         },
       },
       },
     }),
     }),

+ 37 - 24
src/core/nodeOps.ts

@@ -1,13 +1,11 @@
-import { BufferAttribute } from 'three'
-import { useCamera } from '/@/composables'
+import { BufferAttribute, BufferGeometry, Material, Object3D } from 'three'
+import { useCamera, useLogger } from '/@/composables'
 import { RendererOptions } from 'vue'
 import { RendererOptions } from 'vue'
 import { catalogue } from './catalogue'
 import { catalogue } from './catalogue'
 import { isFunction } from '@vueuse/core'
 import { isFunction } from '@vueuse/core'
-import { TresObject } from '../types'
+import { TresInstance, TresObject } from '../types'
 import { isHTMLTag, kebabToCamel } from '../utils'
 import { isHTMLTag, kebabToCamel } from '../utils'
 
 
-import type { Object3D, Material, BufferGeometry } from 'three'
-
 const onRE = /^on[^a-z]/
 const onRE = /^on[^a-z]/
 export const isOn = (key: string) => onRE.test(key)
 export const isOn = (key: string) => onRE.test(key)
 
 
@@ -15,25 +13,38 @@ function noop(fn: string): any {
   fn
   fn
 }
 }
 
 
-let scene: TresObject | null = null
+let fallback: TresObject | null = null
 
 
 const OBJECT_3D_USER_DATA_KEYS = {
 const OBJECT_3D_USER_DATA_KEYS = {
   GEOMETRY_VIA_PROP: 'tres__geometryViaProp',
   GEOMETRY_VIA_PROP: 'tres__geometryViaProp',
   MATERIAL_VIA_PROP: 'tres__materialViaProp',
   MATERIAL_VIA_PROP: 'tres__materialViaProp',
 }
 }
 
 
+const { logError } = useLogger()
+
 export const nodeOps: RendererOptions<TresObject, TresObject> = {
 export const nodeOps: RendererOptions<TresObject, TresObject> = {
   createElement(tag, _isSVG, _anchor, props) {
   createElement(tag, _isSVG, _anchor, props) {
+    if (!props) props = {}
+
+    if (!props.args) {
+      props.args = []
+    }
     if (tag === 'template') return null
     if (tag === 'template') return null
     if (isHTMLTag(tag)) return null
     if (isHTMLTag(tag)) return null
+    let name = tag.replace('Tres', '')
     let instance
     let instance
 
 
-    if (props === null) props = {}
-
-    if (props?.args) {
-      instance = new catalogue.value[tag.replace('Tres', '')](...props.args)
+    if (tag === 'primitive') {
+      if (props?.object === undefined) logError(`Tres primitives need a prop 'object'`)
+      const object = props.object as TresInstance
+      name = object.type
+      instance = Object.assign(object, { type: name, attach: props.attach, primitive: true })
     } else {
     } else {
-      instance = new catalogue.value[tag.replace('Tres', '')]()
+      const target = catalogue.value[name]
+      if (!target) {
+        logError(`${name} is not defined on the THREE namespace. Use extend to add it to the catalog.`)
+      }
+      instance = Object.assign(new target(...props.args), { type: name, attach: props.attach })
     }
     }
 
 
     if (instance.isCamera) {
     if (instance.isCamera) {
@@ -65,15 +76,22 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
 
 
     return instance
     return instance
   },
   },
-  insert(child, parent, anchor) {
-    if (scene === null && parent.isScene) scene = parent
-    if (parent === null) parent = scene as TresObject
+  insert(child, parent) {
+    if (
+      (child?.__vnode.type === 'TresGroup' || child?.__vnode.type === 'TresObject3D') &&
+      parent === null &&
+      !child?.__vnode?.ctx?.asyncResolved
+    ) {
+      fallback = child
+      return
+    }
+
+    if (!parent) parent = fallback as TresObject
+
     //vue core
     //vue core
     /*  parent.insertBefore(child, anchor || null) */
     /*  parent.insertBefore(child, anchor || null) */
-    if (parent?.isObject3D && child?.isObject3D) {
-      const index = anchor ? parent.children.indexOf(anchor) : 0
-      child.parent = parent
-      parent.children.splice(index, 0, child)
+    if (child?.isObject3D && parent?.isObject3D) {
+      parent.add(child)
       child.dispatchEvent({ type: 'added' })
       child.dispatchEvent({ type: 'added' })
     } else if (typeof child?.attach === 'string') {
     } else if (typeof child?.attach === 'string') {
       child.__previousAttach = child[parent?.attach]
       child.__previousAttach = child[parent?.attach]
@@ -84,7 +102,6 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
   },
   },
   remove(node) {
   remove(node) {
     if (!node) return
     if (!node) return
-
     // remove is only called on the node being removed and not on child nodes.
     // remove is only called on the node being removed and not on child nodes.
 
 
     if (node.isObject3D) {
     if (node.isObject3D) {
@@ -114,10 +131,6 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
       let finalKey = kebabToCamel(key)
       let finalKey = kebabToCamel(key)
       let target = root?.[finalKey]
       let target = root?.[finalKey]
 
 
-      if (!node.parent) {
-        node.parent = scene as TresObject
-      }
-
       if (root.type === 'BufferGeometry') {
       if (root.type === 'BufferGeometry') {
         root.setAttribute(
         root.setAttribute(
           kebabToCamel(key),
           kebabToCamel(key),
@@ -157,7 +170,6 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
     return node?.parent || null
     return node?.parent || null
   },
   },
   createText: () => noop('createText'),
   createText: () => noop('createText'),
-
   createComment: () => noop('createComment'),
   createComment: () => noop('createComment'),
 
 
   setText: () => noop('setText'),
   setText: () => noop('setText'),
@@ -169,5 +181,6 @@ export const nodeOps: RendererOptions<TresObject, TresObject> = {
 
 
   setScopeId: () => noop('setScopeId'),
   setScopeId: () => noop('setScopeId'),
   cloneNode: () => noop('cloneNode'),
   cloneNode: () => noop('cloneNode'),
+
   insertStaticContent: () => noop('insertStaticContent'),
   insertStaticContent: () => noop('insertStaticContent'),
 }
 }

+ 9 - 0
src/core/nodeOpts.test.ts

@@ -99,6 +99,11 @@ describe('nodeOps', () => {
     const parent: TresObject = new Scene()
     const parent: TresObject = new Scene()
     const child: TresObject = new Mesh()
     const child: TresObject = new Mesh()
 
 
+    // Fake vnodes
+    child.__vnode = {
+      type: 'TresMesh',
+    }
+
     // Test
     // Test
     nodeOps.insert(child, parent, null)
     nodeOps.insert(child, parent, null)
 
 
@@ -111,6 +116,10 @@ describe('nodeOps', () => {
     const parent = new Scene() as unknown as TresObject
     const parent = new Scene() as unknown as TresObject
     const child = new Mesh() as unknown as TresObject
     const child = new Mesh() as unknown as TresObject
 
 
+    // Fake vnodes
+    child.__vnode = {
+      type: 'TresMesh',
+    }
     nodeOps.insert(child, parent)
     nodeOps.insert(child, parent)
 
 
     // Test
     // Test