瀏覽代碼

feat: highlighted mesh

alvarosabu 1 年之前
父節點
當前提交
26c58a0588

+ 1 - 0
playground/components.d.ts

@@ -8,6 +8,7 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     AnimatedModel: typeof import('./src/components/AnimatedModel.vue')['default']
+    BlenderCube: typeof import('./src/components/BlenderCube.vue')['default']
     CameraOperator: typeof import('./src/components/CameraOperator.vue')['default']
     Cameras: typeof import('./src/components/Cameras.vue')['default']
     copy: typeof import('./src/components/TheBasic copy.vue')['default']

+ 13 - 0
playground/src/components/BlenderCube.vue

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

+ 9 - 11
playground/src/components/TheExperience.vue

@@ -21,6 +21,10 @@ const wireframe = ref(true)
 const canvas = ref()
 const meshRef = ref()
 
+const { isVisible } = useControls({
+  isVisible: true,
+})
+
 watchEffect(() => {
   if (meshRef.value) {
     console.log(meshRef.value)
@@ -29,12 +33,12 @@ watchEffect(() => {
 </script>
 
 <template>
+  <TresLeches />
   <TresCanvas
     v-bind="gl"
     ref="canvas"
     window-size
     class="awiwi"
-    render-mode="on-demand"
     :style="{ background: '#008080' }"
   >
     <TresPerspectiveCamera
@@ -51,16 +55,9 @@ watchEffect(() => {
       <TresConeGeometry :args="[1, 1.5, 3]" />
       <TresMeshToonMaterial color="#82DBC5" />
     </TresMesh>
-    <TresMesh
-      :position="[0, 4, 0]"
-      cast-shadow
-    >
-      <TresBoxGeometry :args="[1.5, 1.5, 1.5]" />
-      <TresMeshToonMaterial
-        color="#4F4F4F"
-        :wireframe="wireframe"
-      />
-    </TresMesh>
+    <Suspense>
+      <BlenderCube :position="[0, 5, 0]" />
+    </Suspense>
     <TresMesh
       ref="meshRef"
       :rotation="[-Math.PI / 2, 0, Math.PI / 2]"
@@ -72,6 +69,7 @@ watchEffect(() => {
         color="#D3FC8A"
       />
     </TresMesh>
+    <TheSphere v-if="isVisible" />
     <TresAxesHelper :args="[1]" />
     <TresDirectionalLight
       :position="[0, 2, 4]"

+ 1 - 8
playground/src/pages/rendering-modes/index.vue

@@ -2,19 +2,12 @@
 import { TresCanvas } from '@tresjs/core'
 
 import Scene from './scene.vue'
-
-const clearColor = ref('#82DBC5')
-
-setTimeout(() => {
-  clearColor.value = '#000000'
-}, 3000)
 </script>
 
 <template>
   <TresCanvas
     :clear-color="clearColor"
-    render-mode="manual"
-    @render="() => console.log('onRender')"
+    render-mode="on-demand"
   >
     <Scene />
   </TresCanvas>

+ 3 - 2
playground/src/pages/rendering-modes/scene.vue

@@ -5,14 +5,14 @@ import { OrbitControls } from '@tresjs/cientos'
 const { invalidate, advance } = useTres()
 
 function onControlChange() {
-  advance()
+  invalidate()
 }
 
 const positionX = ref(0)
 const showMesh = ref(true)
 
 setTimeout(() => {
-  positionX.value = 1
+/*   positionX.value = 1 */
   /*   showMesh.value = false */
 
 }, 3000)
@@ -20,6 +20,7 @@ setTimeout(() => {
 
 <template>
   <OrbitControls @change="onControlChange" />
+  <TresPerspectiveCamera :position="[0, 0, 5]" />
   <TresGridHelper />
   <TresMesh
     v-if="showMesh"

+ 1 - 1
plugins/vite-plugin-tres/client/.nuxt/manifest/latest.json

@@ -1 +1 @@
-{"id":"dev","timestamp":1704897175593}
+{"id":"dev","timestamp":1705252415652}

+ 1 - 1
plugins/vite-plugin-tres/client/.nuxt/manifest/meta/dev.json

@@ -1 +1 @@
-{"id":"dev","timestamp":1704897175593,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}
+{"id":"dev","timestamp":1705252415652,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}

+ 3 - 3
plugins/vite-plugin-tres/client/.nuxt/nitro.json

@@ -1,5 +1,5 @@
 {
-  "date": "2024-01-10T14:32:59.738Z",
+  "date": "2024-01-14T17:13:45.236Z",
   "preset": "nitro-dev",
   "framework": {
     "name": "nuxt",
@@ -9,9 +9,9 @@
     "nitro": "2.8.1"
   },
   "dev": {
-    "pid": 8422,
+    "pid": 89321,
     "workerAddress": {
-      "socketPath": "/var/folders/66/14k3nnbx1g505216sq4xdfdc0000gn/T/nitro/worker-8422-2.sock"
+      "socketPath": "/var/folders/66/14k3nnbx1g505216sq4xdfdc0000gn/T/nitro/worker-89321-2.sock"
     }
   }
 }

+ 2 - 2
plugins/vite-plugin-tres/client/components/CodeView.vue

@@ -1,7 +1,7 @@
 <script setup>
-const { internal } = useDevtoolsHook()
+const { selectedObject } = useDevtoolsHook()
 
-const code = computed(() => JSON.stringify(internal.selectedObject, null, 2).trim())
+const code = computed(() => JSON.stringify(selectedObject, null, 2).trim())
 </script>
 
 <template>

+ 12 - 7
plugins/vite-plugin-tres/client/components/SceneGraphItem.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { computed } from 'vue'
+import { computed, ref } from 'vue'
 import { useDevtoolsHook } from '../composables/useDevtoolsHook'
 import type { SceneGraphObject } from '../types'
 
@@ -10,7 +10,9 @@ const props = withDefaults(defineProps<{
   depth: 0,
 })
 
-const { internal, highlightObject, selectObject } = useDevtoolsHook()
+const isExpanded = ref(false)
+
+const { highlightObject, unhighlightObject, selectObject } = useDevtoolsHook()
 
 function roundNumber(num: number) {
   return Math.round((num + Number.EPSILON) * 100) / 100
@@ -18,11 +20,9 @@ function roundNumber(num: number) {
 
 function handleClick() {
   isExpanded.value = !isExpanded.value
-  highlightObject(props.item)
+  isExpanded.value ? highlightObject(props.item) : unhighlightObject()
   selectObject(props.item)
 }
-
-const isExpanded = computed(() => internal?.selectedObject?.uuid === props.item.uuid || false)
 </script>
 
 <template>
@@ -37,9 +37,13 @@ const isExpanded = computed(() => internal?.selectedObject?.uuid === props.item.
         v-if="depth > 0"
         class="h-1 border-b border-gray-300 w-4"
       />
-      <div class="flex gap-2 items-center -mb2.5">
+      <div
+        v-if="depth > 0"
+        class="flex gap-2 items-center -mb2.5"
+      >
         <Icon :name="item.icon" />
-        <!-- <i :class="item.icon" /> -->{{ item.type }} <UBadge
+        <!-- <i :class="item.icon" /> -->{{ item.type }} 
+        <UBadge
           v-if="item.name "
           variant="soft"
         >
@@ -149,6 +153,7 @@ const isExpanded = computed(() => internal?.selectedObject?.uuid === props.item.
             </UBadge>
 
             <UBadge
+              v-if="item.material.color"
               color="gray"
               variant="soft"
             > 

+ 0 - 10
plugins/vite-plugin-tres/client/components/TheHeader.vue

@@ -49,16 +49,6 @@ const route = useRoute()
         target="_blank"
         to="https://github.com/Tresjs/nuxt"
       />
-      <UButton
-        variant="solid"
-          
-        size="sm"
-        target="_blank"
-        class="ml2"
-        to="https://github.com/Tresjs/nuxt/issues/new?labels=enhancement&template=feature_request.yml"
-      >
-        Request feature
-      </UButton>
     </div>
   </header>
 </template>

+ 48 - 21
plugins/vite-plugin-tres/client/components/inspector/InspectorBranch.vue

@@ -1,10 +1,13 @@
 <script setup lang="ts">
+import { toRaw } from 'vue'
+
 const props = defineProps<{
   entry: any
   depth?: number
 }>()
 
 const isObject = value => value && typeof value === 'object' && !Array.isArray(value)
+const isString = value => typeof value === 'string'
 
 const isArray = value => Array.isArray(value)
 
@@ -16,7 +19,6 @@ const data = computed(() => {
     return {
       key,
       value,
-      uuid: Math.random().toString(36).substring(7),
       expandable: isObject(value) || isArray(value),
     }
   })
@@ -26,53 +28,72 @@ const data = computed(() => {
 const collapsedKeys = reactive({})
 
 // Toggle the collapsed state
-const toggleCollapse = (uuid) => {
-  collapsedKeys[uuid] = !collapsedKeys[uuid]
+const toggleCollapse = (key) => {
+  collapsedKeys[key] = !collapsedKeys[key]
 }
 
-const isExpanded = uuid => collapsedKeys[uuid]
+const isExpanded = key => collapsedKeys[key]
 
-const editableKeys = reactive({})
+function changeValue(key, value) {
+  console.log('changeValue', key, value)
+}
 </script>
 
 <template>
   <div
-    v-for="item in data"
-    :key="item.key"
+    v-for="(item, index) in data"
+    :key="item.key + index"
     class="pb1 text-sm"
   >
     <!-- Check if the item is expandable (either an object or an array) -->
-    <template v-if="item.expandable && depth < 1">
+    <template v-if="item.expandable && depth < 2">
       <div
         class="flex items-center"
-        @click="toggleCollapse(item.uuid)"
+        @click="toggleCollapse(item.key)"
       >
         <Icon
           class="mr-2"
-          :name="isExpanded(item.uuid) ? 'i-carbon-caret-down' : 'i-carbon-caret-right'"
+          :name="isExpanded(item.key) ? 'i-carbon-caret-down' : 'i-carbon-caret-right'"
         />
-        <span class="text-purple-400"> {{ item.key }}</span>  : {{ isArray(item.value) ? 'Array' : 'Object' }}
+        <span class="text-gray-400"> {{ item.key }}</span>  : {{ isArray(item.value) ? `Array(${item.value.length})` : 'Object' }}
       </div>
       <div
-        v-show="isExpanded(item.uuid)"
+        v-show="isExpanded(item.key)"
         class="pl-8 py2"
       >
         <!-- Handle Objects -->
         <template v-if="isObject(item.value)">
-          <!--  <InspectorBranch
-            :entry="item.value"
+          <InspectorBranch
+            :key="item.key"
+            :entry="{ ...item.value }"
             :depth="depth + 1"
-          /> -->
+          />
         </template>
         <!-- Handle Arrays -->
         <template v-if="isArray(item.value)">
+          <span class="text-gray-500 -ml-4">[<span class="text-gray-400">{{ item.value.length > 0 ? '{' : '' }}</span></span>
           <div
             v-for="(elem, index) in item.value"
             :key="index"
+            class="pl-4"
           >
+            <span
+              v-if="index > 0"
+              class="text-gray-400  -ml-4"
+            >{</span>
+            <InspectorBranch
+              :key="index"
+              :entry="{ ...elem }"
+              :depth="depth + 1"
+            />   
             <!-- This assumes you want to show each array element. Adjust as needed. -->
             <!--  <InspectorBranch :entry="elem" /> -->
+            <span
+              v-if="index < item.value.length - 1"
+              class="text-gray-400 -ml-4"
+            >},</span>
           </div>
+          <span class="text-gray-500"><span class="text-gray-400">{{ item.value.length > 0 ? '}' : '' }}</span>]</span>
         </template>
       </div>
     </template>
@@ -81,13 +102,19 @@ const editableKeys = reactive({})
       <div class="flex gap-1">
         <label
           for=""
-          class="text-purple-400"
+          class="text-gray-400"
         >{{ item.key }}</label> :
-        <input
-          class="text-red-400"
-          :value="item.value"
-          :type="typeof item.value"
-        >
+        <UBadge
+          color="gray"
+          variant="soft"
+        > 
+          {{ isString(item.value) ? '"' : '' }}  {{ item.value }}   {{ isString(item.value) ? '"' : '' }}
+        </UBadge>
+        <!-- <input
+          :value="toRaw(item.value)"
+          type="text"
+          @input="changeValue(item.key, $event.target.value)"
+        > -->
       </div>
     </template>
   </div>

+ 4 - 4
plugins/vite-plugin-tres/client/components/inspector/InspectorState.vue

@@ -20,18 +20,18 @@ const tabs = [
 ]
 const currentTab = ref(0)
 
-const { internal } = useDevtoolsHook()
+const { selectedObject } = useDevtoolsHook()
 </script>
 
 <template>
   <header class="border-b border-base p4 text-gray-400 flex justify-between">
     <div>
-      {{ internal?.selectedObject.type }} 
+      {{ selectedObject.type }} 
       <UBadge
-        v-if="internal.selectedObject.name "
+        v-if="selectedObject.name "
         variant="soft"
       >
-        {{ internal.selectedObject.name }}
+        {{ selectedObject.name }}
       </UBadge>
     </div>
     <div class="flex gap-4">

+ 5 - 2
plugins/vite-plugin-tres/client/components/inspector/InspectorTree.vue

@@ -1,10 +1,13 @@
 <script setup lang="ts">
-const { internal } = useDevtoolsHook()
+import { watch } from 'vue'
+
+const { selectedObject } = useDevtoolsHook()
 </script>
 
 <template>
   <InspectorBranch
-    :entry="internal.selectedObject"
+    :key="selectedObject.uuid"
+    :entry="selectedObject"
     :depth="0"
   />
 </template>

+ 161 - 9
plugins/vite-plugin-tres/client/composables/useDevtoolsHook.ts

@@ -1,9 +1,34 @@
-import { reactive, ref, toRefs } from 'vue'
-import type { DevtoolsContextPayload, DevtoolsPerformancePayload, DevtoolsEvent } from '@tresjs/core'
+import type { ComputedRef } from 'vue'
+import { computed, nextTick, reactive, ref, toRefs } from 'vue'
+import type { DevtoolsContextPayload, DevtoolsPerformancePayload, DevtoolsEvent, TresObject } from '@tresjs/core'
+import type { Mesh, Object3D } from 'three'
+import { DoubleSide, MeshBasicMaterial } from 'three'
+import type { GraphObject } from '../types'
+import { HightlightMesh } from '../utils/highlightedMesh'
 
-const state = reactive({
+interface DevtoolsState {
+  connected: boolean
+  scene: Object3D
+  selectedObject: Object3D | null
+  prevInstance: Object3D | null
+  highlightMesh: Mesh | null
+  invalidate: (frames?: number) => void
+}
+interface DevtoolsHookReturn {
+  state: DevtoolsState
+  graph: ComputedRef<GraphObject>
+  highlightObject: (object: TresObject) => void
+  unhighlightObject: () => void
+  selectObject: (object: TresObject) => void
+}
+
+const state = reactive<DevtoolsState>({
   connected: false,
-  scene: {},
+  scene: {} as Object3D,
+  selectedObject: null as Object3D | null,
+  prevInstance: null as Object3D | null,
+  highlightMesh: null as Mesh | null,
+  invalidate: () => {},
 })
 
 function handlePerformanceEvent(payload: DevtoolsPerformancePayload) {
@@ -11,13 +36,134 @@ function handlePerformanceEvent(payload: DevtoolsPerformancePayload) {
 }
 
 function handleContextEvent(payload: DevtoolsContextPayload) {
-  state.scene = { ...payload.scene }
-/*   console.count('handleContextEvent') */
+  payload.scene.traverse((children) => {
+    if (payload.scene.__vnode) delete children.__vnode
+    delete children.userData.tres__context
+    delete children.userData.tres__root
+  })
+  const { _vnode, ...rest } = payload.scene
+  state.scene = { 
+    ...rest, 
+    getObjectByProperty: payload.scene.getObjectByProperty, 
+    traverse: payload.scene.traverse, 
+  }
+  if (state.selectedObject) {
+    selectObject(state.selectedObject)
+  }
+
+  // On-demand rendering
+  state.invalidate = payload.invalidate
+}
+
+const icons: Record<string, string> = {
+  scene: 'i-carbon-web-services-container',
+  perspectivecamera: 'i-carbon-video',
+  mesh: 'i-carbon-cube',
+  group: 'i-carbon-group-objects',
+  ambientlight: 'i-carbon-light',
+  directionallight: 'i-carbon-light',
+  spotlight: 'i-iconoir-project-curve-3d',
+  position: 'i-iconoir-axes',
+  rotation: 'i-carbon-rotate-clockwise',
+  scale: 'i-iconoir-ellipse-3d-three-points',
+  bone: 'i-ph-bone',
+  skinnedmesh: 'carbon:3d-print-mesh',
+}
+
+function createNode(object: TresObject) {
+  const node: GraphObject = {
+    uuid: object.uuid,
+    name: object.name,
+    type: object.type,
+    icon: icons[object.type.toLowerCase()] || 'i-carbon-cube',
+    position: {
+      x: object.position.x,
+      y: object.position.y,
+      z: object.position.z,
+    },
+    rotation: {
+      x: object.rotation.x,
+      y: object.rotation.y,
+      z: object.rotation.z,
+    },
+    children: [],
+  }
+
+  if (object.type === 'Mesh') {
+    node.material = object.material
+    node.geometry = object.geometry
+    node.scale = {
+      x: object.scale.x,
+      y: object.scale.y,
+      z: object.scale.z,
+    }
+  }
+
+  if (object.type.includes('Light')) {
+    node.color = object.color.getHexString()
+    node.intensity = object.intensity
+  }
+  return node
+}
+
+const graph = computed(() => {
+  const traverse = (object: TresObject, node: GraphObject) => {
+
+    object.children.forEach((child) => {
+      const childNode = createNode(child)
+      if (child.type !== 'HightlightMesh') {
+        node.children.push(childNode)
+      }
+      traverse(child, childNode)
+    })
+  }
+  const root = createNode(state.scene)
+  traverse(state.scene, root)
+
+  return root
+})
+
+function createHighlightMesh(object: Object3D): Mesh {
+  const highlightMaterial = new MeshBasicMaterial({
+    color: 0xa7e6d7, // Highlight color
+    transparent: true,
+    opacity: 0.2,
+    depthTest: false, // So the highlight is always visible
+    side: DoubleSide, // To e
+  })
+  // Clone the geometry of the object. You might need a more complex approach 
+  // if the object's geometry is not straightforward.
+  const highlightMesh = new HightlightMesh(state.invalidate, object.geometry.clone(), highlightMaterial)
+
+  return highlightMesh
 }
 
-export function useDevtoolsHook() {
-  if (window.parent.parent.__TRES__DEVTOOLS__) return
+function highlightObject(object: TresObject) {
+  const instance = state.scene.getObjectByProperty('uuid', object.uuid)
+  unhighlightObject()
   
+  if (instance && instance.isMesh) {
+    const newHighlightMesh = createHighlightMesh(instance)
+    instance.add(newHighlightMesh)
+
+    state.highlightMesh = newHighlightMesh
+    state.prevInstance = instance
+  }
+}
+
+function unhighlightObject() {
+  if (state.prevInstance && state.highlightMesh && state.highlightMesh.parent) {
+    state.prevInstance.remove(state.highlightMesh)
+  }
+}
+
+function selectObject(object: TresObject) {
+  const instance = state.scene.getObjectByProperty('uuid', object.uuid)
+  state.selectedObject = {}
+  state.selectedObject = { ...instance }
+}
+
+export function useDevtoolsHook(): DevtoolsHookReturn {
   function cb(event: DevtoolsEvent) {
     state.connected = true
 
@@ -34,10 +180,16 @@ export function useDevtoolsHook() {
     cb,
   }
 
-  window.parent.parent.__TRES__DEVTOOLS__ = tresGlobalHook
+  if (!window.parent.parent.__TRES__DEVTOOLS__) {
+    window.parent.parent.__TRES__DEVTOOLS__ = tresGlobalHook
+  }
 
   return {
     state,
+    graph,
     ...toRefs(state),
+    highlightObject,
+    unhighlightObject,
+    selectObject,
   }
 } 

+ 7 - 5
plugins/vite-plugin-tres/client/pages/index.vue

@@ -5,16 +5,18 @@ import { useDevtoolsHook } from '../composables/useDevtoolsHook'
 
 const { scene, connected, state } = useDevtoolsHook()
 
-watch(scene, () => {
+/* watch(scene, () => {
   console.log('scene changed', scene.value)
-})
+}, {
+  immediate: true,
+}) */
 
-// Scene Graph
+// Scene scene
 </script>
 
 <template>
   <div class="panel-grids-center h-full">
-    <!-- <div class="max-w-300 w-full px20 ma">
+    <div class="max-w-300 w-full px20 ma">
       <div class="flex flex-wrap gap2">
         <RouterLink
           to="/scene-graph"
@@ -37,7 +39,7 @@ watch(scene, () => {
           <code>Performance</code>
         </RouterLink>
       </div>
-    </div> -->
+    </div>
     <!-- <div
       v-if="scene.objects > 0"
       class="flex flex-col gap-2"

+ 6 - 5
plugins/vite-plugin-tres/client/pages/scene-graph.vue

@@ -1,7 +1,8 @@
 <script setup lang="ts">
 import { Pane, Splitpanes } from 'splitpanes'
+import { useDevtoolsHook } from '../composables/useDevtoolsHook'
 
-const { scene, internal } = useDevtoolsHook()
+const { selectedObject, graph } = useDevtoolsHook()
 </script>
 
 <template>
@@ -12,14 +13,14 @@ const { scene, internal } = useDevtoolsHook()
       class="h-full p4 overflow-y-scroll"
       min-size="20"
     >
-      <!-- <div v-if="scene.objects > 0">
-        <SceneGraphItem :item="scene.graph" />
-      </div> -->
+      <div v-if="graph">
+        <SceneGraphItem :item="graph" />
+      </div>
     </Pane>
     <Pane
       class="h-full"
     >
-      <!-- <InspectorState v-if="internal?.selectedObject" /> -->
+      <InspectorState v-if="selectedObject" />
     </Pane>
   </Splitpanes>
 </template>

+ 2 - 2
plugins/vite-plugin-tres/client/types/index.ts

@@ -1,6 +1,6 @@
 import type { BufferGeometry, Material, Scene, WebGLRenderer } from 'three'
 
-export interface SceneGraphObject {
+export interface GraphObject {
   name: string
   type: string
   icon: string
@@ -23,7 +23,7 @@ export interface SceneGraphObject {
   intensity?: number
   material?: Material
   geometry?: BufferGeometry
-  children: SceneGraphObject[]
+  children: GraphObject[]
   [key: string]: any
 }
 

+ 8 - 1
plugins/vite-plugin-tres/client/utils/highlightedMesh.ts

@@ -3,9 +3,13 @@ import * as THREE from 'three'
 export class HightlightMesh extends THREE.Mesh {
   type = 'HightlightMesh'
   createTime: number
-  constructor(...args: THREE.Mesh['args']) {
+  invalidate: (frames?: number) => void
+  constructor( invalidate: (frames?: number) => void, ...args: THREE.Mesh['args']) {
     super(...args)
     this.createTime = Date.now()
+    this.invalidate = invalidate
+
+    invalidate()
   }
 
   onBeforeRender() {
@@ -20,5 +24,8 @@ export class HightlightMesh extends THREE.Mesh {
 
     // Apply the scale factor
     this.scale.set(scaleFactor, scaleFactor, scaleFactor)
+
+    this.invalidate()
   }
+  
 }

+ 28 - 6
src/composables/useRenderer/index.ts

@@ -1,5 +1,5 @@
 import { Color, MathUtils, WebGLRenderer } from 'three'
-import { shallowRef, watchEffect, onUnmounted, type MaybeRef, computed, watch, nextTick } from 'vue'
+import { shallowRef, watchEffect, onUnmounted, type MaybeRef, computed, watch, nextTick, ref } from 'vue'
 import {
   toValue,
   unrefElement,
@@ -178,11 +178,17 @@ export function useRenderer(
 
   const { resume, onLoop } = useRenderLoop()
 
+  const retries = ref(2)
   function sendDevtoolEvent(
     payload: DevtoolsContextPayload | DevtoolsPerformancePayload,
     type: 'context' | 'performance' = 'context',
   ) {
-    if (!window.__TRES__DEVTOOLS__) return
+    if (!window.__TRES__DEVTOOLS__ || retries.value > 0) {
+      setTimeout(() => {
+        retries.value--
+        sendDevtoolEvent(payload, type)
+      }, 100)
+    }
 
     window.__TRES__DEVTOOLS__?.cb({
       id: MathUtils.generateUUID(),
@@ -192,13 +198,29 @@ export function useRenderer(
     })
   }
 
-  onLoop(() => {
+  // Devtools
+  let accumulatedTime = 0
+  const interval = 1 // Interval in milliseconds, e.g., 1000 ms = 1 second
+
+  onLoop(({ delta }) => {
     if (camera.value && !toValue(disableRender) && internal.frames.value > 0) {
       renderer.value.render(scene, camera.value)
       emit('render', renderer.value)
-      /* sendDevtoolEvent({
-        scene,
-      }) */
+
+      // Accumulate the delta time
+      accumulatedTime += delta
+    
+      // Check if the accumulated time is greater than or equal to the interval
+      if (accumulatedTime >= interval) {
+      /* window.__TRES__DEVTOOLS__.cb(toProvide) */
+        sendDevtoolEvent({
+          scene,
+          invalidate: invalidateOnDemand,
+        })
+        // Reset the accumulated time
+        accumulatedTime = 0
+      }
+    
     }
 
     // Reset priority

+ 1 - 1
src/devtools/index.ts

@@ -1,2 +1,2 @@
 export { registerTresDevtools } from './plugin'
-export * from './types'
+export * from './types'

+ 2 - 5
src/devtools/types.ts

@@ -50,11 +50,8 @@ export interface DevtoolsPerformancePayload {
 
 // Context Devtools
 export interface DevtoolsContextPayload {
-  scene: {
-    objects: number
-    graph: Record<string, unknown>
-    value: Scene | undefined
-  }
+  scene: Scene | undefined
+  invalidate: (frames: number) => void
 }
 
 export interface DevtoolsEvent<T extends DevtoolsPerformancePayload | DevtoolsContextPayload> {