Prechádzať zdrojové kódy

docs: attach (#869)

* test(utils.resolve): add tests for camelCase, array indices

* docs(attach): add playground demos for attach

* docs(attach): add documentation for attach prop

* Update docs/advanced/attach.md

Co-authored-by: Tino Koch <17991193+Tinoooo@users.noreply.github.com>

---------

Co-authored-by: Alvaro Saburido <alvaro.saburido@gmail.com>
Co-authored-by: Tino Koch <17991193+Tinoooo@users.noreply.github.com>
andretchen0 6 mesiacov pred
rodič
commit
fbcbdc3652

+ 1 - 0
docs/.vitepress/config/en.ts

@@ -45,6 +45,7 @@ export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {
           { text: 'Extending', link: '/advanced/extending' },
           { text: 'Primitives', link: '/advanced/primitive' },
           { text: 'Scaling Performance 🚀', link: '/advanced/performance' },
+          { text: 'Attach', link: '/advanced/attach' },
           {
             text: 'Caveats',
             link: '/advanced/caveats',

+ 190 - 0
docs/advanced/attach.md

@@ -0,0 +1,190 @@
+# `attach` 🖇
+
+Using the `attach` prop, you can tell Tres exactly where you want to insert a child into its parent.
+
+:::info
+
+The `attach` prop is not required for many common cases. For instance:
+
+* adding a single `<Material>` to a `<Mesh>`
+* adding a `<Geometry>` to a `<Mesh>`
+* adding one or more `<Mesh>`s to a parent `<Mesh>`
+
+:::
+
+## Background
+
+Tres tries to automatically determine where to insert child tag into its parent. For example, in this code, Tres will:
+
+* automatically insert the geometry into `parent.geometry`
+* automatically insert the material into `parent.material`
+
+```vue
+<template>
+  <TresMesh name="parent">
+    <TresBoxGeometry />
+    <TresMeshNormalMaterial />
+  </TresMesh>
+</template>
+```
+
+## Problem
+
+Tres covers common cases, like above. But it doesn't cover every possible case.
+
+When Tres doesn't automatically choose the proper insertion location for a child, one solution is to fall back to procedural code in `<script>`.
+
+Here's how you might add multiple materials to a mesh using `<script>`:
+
+```vue
+<script setup lang="ts">
+import { MeshBasicMaterial } from 'three'
+import { onMounted, shallowRef } from 'vue'
+
+const meshRef = shallowRef()
+onMounted(() => {
+  meshRef.value.material = [
+    new MeshBasicMaterial({ color: 'red' }),
+    new MeshBasicMaterial({ color: 'orange' }),
+    new MeshBasicMaterial({ color: 'yellow' }),
+    new MeshBasicMaterial({ color: 'green' }),
+    new MeshBasicMaterial({ color: 'blue' }),
+    new MeshBasicMaterial({ color: 'purple' }),
+  ]
+})
+</script>
+
+<template>
+  <TresMesh ref="meshRef">
+    <TresBoxGeometry />
+  </TresMesh>
+</template>
+```
+
+But this workaround means:
+
+* your materials aren't managed by Tres
+* your code is imperative, not declarative
+* your code is non-reactive by default
+
+## Solution
+
+The `attach` prop lets you specify where an object will be added to the parent object using declarative code.
+
+## Usage
+
+Here's the example above, rewritten declaratively using `attach`:
+
+```vue
+<template>
+  <TresMesh>
+    <TresBoxGeometry />
+    <TresMeshBasicMaterial color="red" attach="material-0" />
+    <TresMeshBasicMaterial color="orange" attach="material-1" />
+    <TresMeshBasicMaterial color="yellow" attach="material-2" />
+    <TresMeshBasicMaterial color="green" attach="material-3" />
+    <TresMeshBasicMaterial color="blue" attach="material-4" />
+    <TresMeshBasicMaterial color="purple" attach="material-5" />
+  </TresMesh>
+</template>
+```
+
+## "Pierced" `attach`
+
+You can deeply attach a child to a parent by "piercing" – i.e., using a kebab-case string.
+
+### Pseudocode
+
+First, here are a few simple pseudocode examples. This will attach `bar` at `foo.ab.cd`:
+
+```html
+<foo>
+  <bar attach="ab-cd" />
+</foo>
+```
+
+This will attach `bar` at `foo.ab.cd.ef`:
+
+```html
+<foo>
+  <bar attach="ab-cd-ef" />
+</foo>
+```
+
+### Usage
+
+As a concrete example, you can use "pierced" `attach` to add custom `BufferAttribute`s:
+
+```vue
+<script setup lang="ts">
+import { TresCanvas } from '@tresjs/core'
+
+const positions = new Float32Array([-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0])
+</script>
+
+<template>
+  <TresCanvas clear-color="gray">
+    <TresMesh :scale="0.3333">
+      <TresBufferGeometry>
+        <TresBufferAttribute
+          attach="attributes-position"
+          :count="positions.length / 3"
+          :array="positions"
+          :itemSize="3"
+        />
+      </TresBufferGeometry>
+      <TresMeshBasicMaterial color="red" />
+    </TresMesh>
+  </TresCanvas>
+</template>
+```
+
+## Arrays
+
+You can attach within arrays by using array indices in the `attach` string.
+
+### Usage
+
+For example, you can use array indices to attach `THREE` post-processing passes to the `THREE.EffectComposer.passes` array:
+
+```vue
+<script lang="ts" setup>
+import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
+import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
+import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass'
+import { UnrealBloomPass } from 'three-stdlib'
+import { extend, useLoop, useTres } from '@tresjs/core'
+import { shallowRef } from 'vue'
+
+extend({ EffectComposer, OutputPass, UnrealBloomPass, RenderPass })
+const { renderer, scene, camera, sizes } = useTres()
+const composer = shallowRef<EffectComposer>()
+
+useLoop().render(() => {
+  if (composer.value) {
+    composer.value!.render()
+  }
+})
+</script>
+
+<template>
+  <TresEffectComposer
+    ref="composer"
+    :args="[renderer]"
+    :set-size="[sizes.width.value, sizes.height.value]"
+  >
+    <TresRenderPass
+      :args="[scene, camera]"
+      attach="passes-0"
+    />
+    <TresUnrealBloomPass
+      :args="[undefined, 0.5, 0.1, 0]"
+      attach="passes-1"
+    />
+    <TresOutputPass
+      attach="passes-2"
+      :set-size="[sizes.width.value, sizes.height.value]"
+    />
+  </TresEffectComposer>
+</template>
+```

+ 8 - 1
playground/vue/.eslintrc-auto-import.json

@@ -62,6 +62,13 @@
     "watch": true,
     "watchEffect": true,
     "watchPostEffect": true,
-    "watchSyncEffect": true
+    "watchSyncEffect": true,
+    "DirectiveBinding": true,
+    "MaybeRef": true,
+    "MaybeRefOrGetter": true,
+    "onWatcherCleanup": true,
+    "useId": true,
+    "useModel": true,
+    "useTemplateRef": true
   }
 }

+ 6 - 1
playground/vue/auto-imports.d.ts

@@ -3,6 +3,7 @@
 // @ts-nocheck
 // noinspection JSUnusedGlobalSymbols
 // Generated by unplugin-auto-import
+// biome-ignore lint: disable
 export {}
 declare global {
   const EffectScope: typeof import('vue')['EffectScope']
@@ -35,6 +36,7 @@ declare global {
   const onServerPrefetch: typeof import('vue')['onServerPrefetch']
   const onUnmounted: typeof import('vue')['onUnmounted']
   const onUpdated: typeof import('vue')['onUpdated']
+  const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
   const provide: typeof import('vue')['provide']
   const reactive: typeof import('vue')['reactive']
   const readonly: typeof import('vue')['readonly']
@@ -52,7 +54,10 @@ declare global {
   const useAttrs: typeof import('vue')['useAttrs']
   const useCssModule: typeof import('vue')['useCssModule']
   const useCssVars: typeof import('vue')['useCssVars']
+  const useId: typeof import('vue')['useId']
+  const useModel: typeof import('vue')['useModel']
   const useSlots: typeof import('vue')['useSlots']
+  const useTemplateRef: typeof import('vue')['useTemplateRef']
   const watch: typeof import('vue')['watch']
   const watchEffect: typeof import('vue')['watchEffect']
   const watchPostEffect: typeof import('vue')['watchPostEffect']
@@ -61,6 +66,6 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
+  export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
   import('vue')
 }

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 3 - 0
playground/vue/src/pages/advanced/attachBufferGeometry/index.vue


+ 0 - 0
playground/vue/src/pages/advanced/materialArray/index.vue → playground/vue/src/pages/advanced/attachMaterialArray/index.vue


+ 39 - 0
playground/vue/src/pages/advanced/attachPostProcessing/Effects.vue

@@ -0,0 +1,39 @@
+<script lang="ts" setup>
+import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
+import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
+import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass'
+import { UnrealBloomPass } from 'three-stdlib'
+import { extend, useLoop, useTres } from '@tresjs/core'
+import { shallowRef } from 'vue'
+
+extend({ EffectComposer, OutputPass, UnrealBloomPass, RenderPass })
+const { renderer, scene, camera, sizes } = useTres()
+const composer = shallowRef<EffectComposer>()
+
+useLoop().render(() => {
+  if (composer.value) {
+    composer.value!.render()
+  }
+})
+</script>
+
+<template>
+  <TresEffectComposer
+    ref="composer"
+    :args="[renderer]"
+    :set-size="[sizes.width.value, sizes.height.value]"
+  >
+    <TresRenderPass
+      :args="[scene, camera]"
+      attach="passes-0"
+    />
+    <TresUnrealBloomPass
+      :args="[undefined, 0.5, 0.1, 0]"
+      attach="passes-1"
+    />
+    <TresOutputPass
+      attach="passes-2"
+      :set-size="[sizes.width.value, sizes.height.value]"
+    />
+  </TresEffectComposer>
+</template>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 5 - 0
playground/vue/src/pages/advanced/attachPostProcessing/index.vue


+ 13 - 3
playground/vue/src/router/routes/advanced.ts

@@ -25,9 +25,19 @@ export const advancedRoutes = [
     component: () => import('../../pages/advanced/suspense/index.vue'),
   },
   {
-    path: '/advanced/material-array',
-    name: 'Material array',
-    component: () => import('../../pages/advanced/materialArray/index.vue'),
+    path: '/advanced/attach-material-array',
+    name: 'attach: Material array',
+    component: () => import('../../pages/advanced/attachMaterialArray/index.vue'),
+  },
+  {
+    path: '/advanced/attach-buffer-geometry',
+    name: 'attach: BufferGeometry',
+    component: () => import('../../pages/advanced/attachBufferGeometry/index.vue'),
+  },
+  {
+    path: '/advanced/attach-post-processing',
+    name: 'attach: Post-processing',
+    component: () => import('../../pages/advanced/attachPostProcessing/index.vue'),
   },
   {
     path: '/advanced/device-pixel-ratio',

+ 72 - 0
src/utils/index.test.ts

@@ -134,6 +134,78 @@ describe('resolve', () => {
     expect(utils.resolve(instance, 'ab-cd-xx-yy-zz').key).toBe('xxYyZz')
     expect(utils.resolve(instance, 'ab-cd-xx-yy-zz').key).toBe('xxYyZz')
   })
+
+  it('finds camelCase fields if camelCase is passed', () => {
+    const instance = { aBcDe: { fGhIj: { kLm: 0 } } }
+
+    // NOTE: This is the usual case. No camel case. Only kebab.
+    let result = utils.resolve(instance, 'a-bc-de-f-gh-ij-k-lm')
+    expect(result.target).toBe(instance.aBcDe.fGhIj)
+    expect(result.key).toBe('kLm')
+
+    result = utils.resolve(instance, 'aBcDe-fGhIj-kLm')
+    expect(result.target).toBe(instance.aBcDe.fGhIj)
+    expect(result.key).toBe('kLm')
+
+    result = utils.resolve(instance, 'a-bcDe-fGhIj-kLm')
+    expect(result.target).toBe(instance.aBcDe.fGhIj)
+    expect(result.key).toBe('kLm')
+
+    result = utils.resolve(instance, 'aBc-de-f-gh-ij-k-lm')
+    expect(result.target).toBe(instance.aBcDe.fGhIj)
+    expect(result.key).toBe('kLm')
+  })
+
+  describe('array indices', () => {
+    it('traverses arrays if indices exist', () => {
+      const instance = { ab: { cd: [{}, {}, { ef: { gh: { ij: 0, kl: [{}, { xx: {} }] } } }] } }
+
+      let result = utils.resolve(instance, 'ab-cd-0-ef-gh-ij-xx-yy-zz')
+      expect(result.target).toBe(instance.ab.cd[0])
+      expect(result.key).toBe('efGhIjXxYyZz')
+
+      result = utils.resolve(instance, 'ab-cd-1-ef-gh-ij-xx-yy-zz')
+      expect(result.target).toBe(instance.ab.cd[1])
+      expect(result.key).toBe('efGhIjXxYyZz')
+
+      result = utils.resolve(instance, 'ab-cd-2-ef-gh-ij-xx-yy-zz')
+      expect(result.target).toBe(instance.ab.cd[2].ef.gh.ij)
+      expect(result.key).toBe('xxYyZz')
+
+      result = utils.resolve(instance, 'ab-cd-2-ef-gh-kl-0-xx-yy-zz')
+      expect(result.target).toBe(instance.ab.cd[2].ef.gh.kl[0])
+      expect(result.key).toBe('xxYyZz')
+
+      result = utils.resolve(instance, 'ab-cd-2-ef-gh-kl-1-xx-yy-zz')
+      expect(result.target).toBe(instance.ab.cd[2].ef.gh.kl[1].xx)
+      expect(result.key).toBe('yyZz')
+    })
+
+    it('adds non-existant array indices to the key', () => {
+      const instance = { ab: { cd: [{}, {}, { ef: { gh: { ij: 0, kl: [{}, { xx: {} }] } } }] } }
+
+      let result = utils.resolve(instance, 'ab-cd-2-ef-gh-kl-2-xx-yy-zz')
+      expect(result.target).toBe(instance.ab.cd[2].ef.gh.kl)
+      expect(result.key).toBe('2XxYyZz')
+
+      result = utils.resolve(instance, 'ab-cd-2-ef-gh-iiii-2-xx-yy-zz')
+      expect(result.target).toBe(instance.ab.cd[2].ef.gh)
+      expect(result.key).toBe('iiii2XxYyZz')
+
+      result = utils.resolve(instance, 'ab-cd-3-ef-gh')
+      expect(result.target).toBe(instance.ab.cd)
+      expect(result.key).toBe('3EfGh')
+
+      result = utils.resolve(instance, 'ab-cd-2-ef-gh-kl-2')
+      expect(result.target).toBe(instance.ab.cd[2].ef.gh.kl)
+      expect(result.key).toBe('2')
+
+      // NOTE: This leads to ambiguity.
+      result = utils.resolve(instance, '0-1-12-24')
+      expect(result.target).toBe(instance)
+      expect(result.key).toBe('011224')
+    })
+  })
 })
 
 describe('setPixelRatio', () => {

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov