Forráskód Böngészése

Merge pull request #717 from Tresjs/bugfix/706-v-if-component-detaching-from-parenet

fix: Implement `createComment` and `nextSibling` nodeOps

Fixes #706
Garrett Walker 11 hónapja
szülő
commit
181e4720c7

+ 4 - 0
playground/components.d.ts

@@ -16,9 +16,13 @@ declare module 'vue' {
     FBOCube: typeof import('./src/components/FBOCube.vue')['default']
     GraphPane: typeof import('./src/components/GraphPane.vue')['default']
     LocalOrbitControls: typeof import('./src/components/LocalOrbitControls.vue')['default']
+    Overlay: typeof import('./src/components/Overlay.vue')['default']
+    OverlayInfo: typeof import('./src/components/OverlayInfo.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     TakeOverLoopExperience: typeof import('./src/components/TakeOverLoopExperience.vue')['default']
+    TestResult: typeof import('./src/components/TestResult.vue')['default']
+    Tests: typeof import('./src/components/Tests.vue')['default']
     TestSphere: typeof import('./src/components/TestSphere.vue')['default']
     Text3D: typeof import('./src/components/Text3D.vue')['default']
     TheCameraOperator: typeof import('./src/components/TheCameraOperator.vue')['default']

+ 36 - 0
playground/src/components/OverlayInfo.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="overlay-info">
+    <slot></slot>
+  </div>
+</template>
+
+<style scoped>
+.overlay-info {
+  position: fixed;
+  top: 0;
+  left: 0;
+  margin: 10px;
+  padding: 16px;
+  max-width: 400px;
+  z-index: 1000;
+  font-family:
+    ui-sans-serif,
+    system-ui,
+    -apple-system,
+    BlinkMacSystemFont,
+    'Segoe UI',
+    Roboto,
+    'Helvetica Neue',
+    Arial,
+    'Noto Sans',
+    sans-serif,
+    'Apple Color Emoji',
+    'Segoe UI Emoji',
+    'Segoe UI Symbol',
+    'Noto Color Emoji';
+  font-size: small;
+  background-color: white;
+  border-radius: 6px;
+  overflow: auto;
+}
+</style>

+ 19 - 0
playground/src/components/TestResult.vue

@@ -0,0 +1,19 @@
+<script setup lang="ts">
+const props = defineProps<{
+  isPass: boolean
+  msg: string
+}>()
+</script>
+
+<template>
+  <span :class="props.isPass ? 'pass' : 'fail'">{{ props.isPass ? '✅' : '❌' }} {{ props.msg }}</span>
+</template>
+
+<style scoped>
+.pass {
+  color: green;
+}
+.fail {
+  color: red;
+}
+</style>

+ 31 - 0
playground/src/components/Tests.vue

@@ -0,0 +1,31 @@
+<script setup lang="ts">
+const props = withDefaults(defineProps<{
+  tests: { getPass: () => boolean, msg: string }[]
+}>(), {
+  tests: () => [],
+})
+
+const tests = shallowRef<{ isPass: boolean, msg: string }[]>([])
+watch(() => [props.tests], run, { immediate: true })
+
+function run() {
+  tests.value = []
+  for (const { getPass, msg } of props.tests) {
+    tests.value.push({ isPass: getPass(), msg })
+  }
+}
+
+defineExpose({
+  run,
+})
+</script>
+
+<template>
+  <div>
+    <ul>
+      <li v-for="test, i of tests" :key="i">
+        <TestResult :is-pass="test.isPass" :msg="test.msg" />
+      </li>
+    </ul>
+  </div>
+</template>

+ 25 - 0
playground/src/pages/basic/vIf/MyBox.vue

@@ -0,0 +1,25 @@
+<script setup lang="ts">
+import { useLoop } from '@tresjs/core'
+
+const groupRef = shallowRef()
+const showRef = shallowRef(false)
+const parentNameRef = shallowRef('')
+useLoop().onBeforeRender(({ elapsed }) => {
+  showRef.value = Math.floor(elapsed) % 2 === 1
+  parentNameRef.value = groupRef.value?.parent.name || ''
+})
+
+defineExpose({
+  show: showRef,
+  instance: groupRef,
+})
+</script>
+
+<template>
+  <TresGroup v-if="showRef" ref="groupRef">
+    <TresMesh>
+      <TresBoxGeometry :args="[1, 1, 1]" />
+      <TresMeshNormalMaterial />
+    </TresMesh>
+  </TresGroup>
+</template>

+ 60 - 0
playground/src/pages/basic/vIf/index.vue

@@ -0,0 +1,60 @@
+<script setup lang="ts">
+import { shallowRef } from 'vue'
+import { TresCanvas } from '@tresjs/core'
+import MyBox from './MyBox.vue'
+
+const boxRef = shallowRef()
+
+const tests = [
+  {
+    getPass: () => {
+      const show = boxRef.value?.show
+      const parentName = boxRef.value?.instance?.parent?.name || null
+      return !show || parentName === 'intended-parent'
+    },
+    msg: 'v-if is false or Box has intended parent',
+  },
+]
+
+const testsRef = shallowRef({ run: () => {} })
+let intervalId: ReturnType<typeof setInterval> | null = null
+onMounted(() => {
+  intervalId = setInterval(() => testsRef.value.run(), 100)
+})
+onUnmounted(() => intervalId && clearInterval(intervalId))
+</script>
+
+<template>
+  <TresCanvas clear-color="gray">
+    <TresPerspectiveCamera :position="[5, 5, 5]" :look-at="[1, 2, 3]" />
+    <TresMesh :position="[1, 2, 3]" name="intended-parent">
+      <MyBox ref="boxRef" />
+      <TresMesh
+        v-for="(_, i) of Array.from({ length: 8 }).fill(0)"
+        :key="i"
+        :position="[
+          i % 2 ? -0.5 : 0.5,
+          Math.floor(i / 2) % 2 ? -0.5 : 0.5,
+          Math.floor(i / 4) % 2 ? -0.5 : 0.5,
+        ]"
+      >
+        <TresBoxGeometry :args="[0.1, 0.1, 0.1]" />
+        <TresMeshBasicMaterial color="red" />
+      </TresMesh>
+    </TresMesh>
+    <TresDirectionalLight :position="[0, 2, 4]" :intensity="1.2" cast-shadow />
+  </TresCanvas>
+  <OverlayInfo>
+    <h1>v-if</h1>
+    <h2>Setup</h2>
+    <p>
+      In this scene, there is a Box with a <code>v-if</code>. Its <code>v-if</code> value is toggled on and off.
+      When visible, the box's 8 corners should appear at the centers of the red boxes.
+    </p>
+    <h2>Tests</h2>
+    <Tests ref="testsRef" :tests="tests" />
+    <h2>Issue</h2>
+    <a href="https://github.com/Tresjs/tres/issues/706#issuecomment-2146244326">
+      Toggle v-if on a Tres component declared in a separate SFC makes it detach from its parent</a>
+  </OverlayInfo>
+</template>

+ 5 - 0
playground/src/router/routes/basic.ts

@@ -44,4 +44,9 @@ export const basicRoutes = [
     name: 'Pierced Props',
     component: () => import('../../pages/basic/PiercedProps.vue'),
   },
+  {
+    path: '/basic/v-if',
+    name: 'v-if',
+    component: () => import('../../pages/basic/vIf/index.vue'),
+  },
 ]

+ 37 - 4
src/core/nodeOps.ts

@@ -1,5 +1,5 @@
 import type { RendererOptions } from 'vue'
-import { BufferAttribute } from 'three'
+import { BufferAttribute, Object3D } from 'three'
 import { isFunction } from '@alvarosabu/utils'
 import type { Camera } from 'three'
 import type { TresContext } from '../composables'
@@ -293,10 +293,43 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
     }
   }
 
-  function parentNode(node: TresObject) {
+  function parentNode(node: TresObject): TresObject | null {
     return node?.parent || null
   }
 
+  /**
+   * createComment
+   *
+   * Creates a comment object that can be used to represent a commented out string in a vue template
+   * Used by Vue's internal runtime as a placeholder for v-if'd elements
+   *
+   * @param comment Any commented out string contaiend in a vue template, typically this is `v-if`
+   * @returns TresObject
+   */
+  function createComment(comment: string): TresObject {
+    const commentObj = new Object3D() as TresObject
+
+    // Set name and type to comment
+    // TODO: Add a custom type for comments instead of reusing Object3D. Comments should be light weight and not exist in the scene graph
+    commentObj.name = comment
+    commentObj.__tres = { type: 'Comment' }
+
+    // Without this we have errors in other nodeOp functions that come across this object
+    commentObj.__tres.root = scene?.__tres.root as TresContext
+
+    return commentObj
+  }
+
+  // nextSibling - Returns the next sibling of a TresObject
+  function nextSibling(node: TresObject) {
+    if (!node) { return null }
+
+    const parent = node.parent || scene
+    const index = parent.children.indexOf(node)
+
+    return parent.children[index + 1] || null
+  }
+
   return {
     insert,
     remove,
@@ -304,10 +337,10 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
     patchProp,
     parentNode,
     createText: () => noop('createText'),
-    createComment: () => noop('createComment'),
+    createComment,
     setText: () => noop('setText'),
     setElementText: () => noop('setElementText'),
-    nextSibling: () => noop('nextSibling'),
+    nextSibling,
     querySelector: () => noop('querySelector'),
     setScopeId: () => noop('setScopeId'),
     cloneNode: () => noop('cloneNode'),