From fbcbdc36528d4d8b4bebfcf16baba7fc4c59e039 Mon Sep 17 00:00:00 2001 From: andretchen0 Date: Sun, 10 Nov 2024 05:53:03 -0600 Subject: [PATCH] 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 Co-authored-by: Tino Koch <17991193+Tinoooo@users.noreply.github.com> --- docs/.vitepress/config/en.ts | 1 + docs/advanced/attach.md | 190 ++++++++++++++++++ playground/vue/.eslintrc-auto-import.json | 9 +- playground/vue/auto-imports.d.ts | 7 +- .../advanced/attachBufferGeometry/index.vue | 47 +++++ .../index.vue | 0 .../advanced/attachPostProcessing/Effects.vue | 39 ++++ .../advanced/attachPostProcessing/index.vue | 29 +++ playground/vue/src/router/routes/advanced.ts | 16 +- src/utils/index.test.ts | 72 +++++++ 10 files changed, 405 insertions(+), 5 deletions(-) create mode 100644 docs/advanced/attach.md create mode 100644 playground/vue/src/pages/advanced/attachBufferGeometry/index.vue rename playground/vue/src/pages/advanced/{materialArray => attachMaterialArray}/index.vue (100%) create mode 100644 playground/vue/src/pages/advanced/attachPostProcessing/Effects.vue create mode 100644 playground/vue/src/pages/advanced/attachPostProcessing/index.vue diff --git a/docs/.vitepress/config/en.ts b/docs/.vitepress/config/en.ts index 007ee1852..3ce52eaa3 100644 --- a/docs/.vitepress/config/en.ts +++ b/docs/.vitepress/config/en.ts @@ -45,6 +45,7 @@ export const enConfig: LocaleSpecificConfig = { { 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', diff --git a/docs/advanced/attach.md b/docs/advanced/attach.md new file mode 100644 index 000000000..50f0849f3 --- /dev/null +++ b/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 `` to a `` +* adding a `` to a `` +* adding one or more ``s to a parent `` + +::: + +## 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 + +``` + +## 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 ` + + +``` + +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 + +``` + +## "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 + + + +``` + +This will attach `bar` at `foo.ab.cd.ef`: + +```html + + + +``` + +### Usage + +As a concrete example, you can use "pierced" `attach` to add custom `BufferAttribute`s: + +```vue + + + +``` + +## 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 + + + +``` diff --git a/playground/vue/.eslintrc-auto-import.json b/playground/vue/.eslintrc-auto-import.json index b2abc8dbd..e60beb5e9 100644 --- a/playground/vue/.eslintrc-auto-import.json +++ b/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 } } diff --git a/playground/vue/auto-imports.d.ts b/playground/vue/auto-imports.d.ts index d298c3cca..acb726426 100644 --- a/playground/vue/auto-imports.d.ts +++ b/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') } diff --git a/playground/vue/src/pages/advanced/attachBufferGeometry/index.vue b/playground/vue/src/pages/advanced/attachBufferGeometry/index.vue new file mode 100644 index 000000000..821cbd60f --- /dev/null +++ b/playground/vue/src/pages/advanced/attachBufferGeometry/index.vue @@ -0,0 +1,47 @@ + + + diff --git a/playground/vue/src/pages/advanced/materialArray/index.vue b/playground/vue/src/pages/advanced/attachMaterialArray/index.vue similarity index 100% rename from playground/vue/src/pages/advanced/materialArray/index.vue rename to playground/vue/src/pages/advanced/attachMaterialArray/index.vue diff --git a/playground/vue/src/pages/advanced/attachPostProcessing/Effects.vue b/playground/vue/src/pages/advanced/attachPostProcessing/Effects.vue new file mode 100644 index 000000000..59f9c923b --- /dev/null +++ b/playground/vue/src/pages/advanced/attachPostProcessing/Effects.vue @@ -0,0 +1,39 @@ + + + diff --git a/playground/vue/src/pages/advanced/attachPostProcessing/index.vue b/playground/vue/src/pages/advanced/attachPostProcessing/index.vue new file mode 100644 index 000000000..77fba53b9 --- /dev/null +++ b/playground/vue/src/pages/advanced/attachPostProcessing/index.vue @@ -0,0 +1,29 @@ + + + diff --git a/playground/vue/src/router/routes/advanced.ts b/playground/vue/src/router/routes/advanced.ts index 607076752..2535355b2 100644 --- a/playground/vue/src/router/routes/advanced.ts +++ b/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', diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index 211e109c0..9a0f7fc77 100644 --- a/src/utils/index.test.ts +++ b/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', () => {