diff --git a/.gitignore b/.gitignore index e6d4c90..d729568 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ dist .env .netlify +**/.env + # Env .env diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..633fc3f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "client: chrome", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + }, + { + "type": "node", + "request": "launch", + "name": "server: nuxt", + "outputCapture": "std", + "program": "${workspaceFolder}/node_modules/nuxt/bin/nuxt.mjs", + "args": [ + "dev playground" + ], + } + ], + "compounds": [ + { + "name": "fullstack: nuxt", + "configurations": [ + "server: nuxt", + "client: chrome" + ] + } + ] +} \ No newline at end of file diff --git a/client/.npmrc b/client/.npmrc new file mode 100644 index 0000000..c483022 --- /dev/null +++ b/client/.npmrc @@ -0,0 +1 @@ +shamefully-hoist=true \ No newline at end of file diff --git a/client/app.config.ts b/client/app.config.ts new file mode 100644 index 0000000..1bebf0a --- /dev/null +++ b/client/app.config.ts @@ -0,0 +1,15 @@ +export default defineAppConfig({ + ui: { + colors: { + primary: 'teal', + accent: 'yellow', + neutral: 'zinc', + }, + card: { + slots: { + root: 'hover:bg-linear-[115deg,#272727 .06%,#171717]', + body: 'p-2 sm:p-3', + }, + }, + }, +}) diff --git a/client/app.vue b/client/app.vue index f8eacfa..8dd09d3 100644 --- a/client/app.vue +++ b/client/app.vue @@ -1,5 +1,5 @@ diff --git a/client/assets/css/main.css b/client/assets/css/main.css new file mode 100644 index 0000000..38445ef --- /dev/null +++ b/client/assets/css/main.css @@ -0,0 +1,57 @@ +@import "tailwindcss"; +@import "@nuxt/ui-pro"; + +@theme static { + --font-display: 'Manrope', sans-serif; + --font-sans: 'Inter', sans-serif; + --font-mono: 'Fira Code', monospace; + --breakpoint-3xl: 1920px; + --ui-pattern-fg: color-mix(in oklab,var(--ui-text)5%,transparent); + --ui-pattern-bg: repeating-linear-gradient(315deg,var(--ui-pattern-fg)0,var(--ui-pattern-fg)1px,transparent 0,transparent 50%); + --ui-header-height: 2.5rem; + /* Technical blueprint styles */ + --ui-line-gap: 5px; + --ui-line-width: 1px; + --ui-line-offset: 172px; + --ui-line-color: var(--color-gray-200); + + /* Teal Palette */ + --color-teal-50: #f2fbf8; + --color-teal-100: #d3f4ea; + --color-teal-200: #a6e9d6; + --color-teal-300: #82dbc5; + --color-teal-400: #44bda2; + --color-teal-500: #2ba189; + --color-teal-600: #20816f; + --color-teal-700: #1d685b; + --color-teal-800: #1c534b; + --color-teal-900: #1b463f; + --color-teal-950: #0a2925; + + /* Yellow/Brown Palette */ + --color-yellow-50: #fff8eb; + --color-yellow-100: #feeac7; + --color-yellow-200: #fdd48a; + --color-yellow-300: #fbb03b; + --color-yellow-400: #fa9e25; + --color-yellow-500: #f47a0c; + --color-yellow-600: #b85607; + --color-yellow-700: #b3390a; + --color-yellow-800: #922b0e; + --color-yellow-900: #78250f; + --color-yellow-950: #451003; +} + +.dark { + --ui-line-color: var(--color-gray-800); +} + +.pattern-bg { + background-image: var(--ui-pattern-bg); + background-size: 10px 10px; + background-attachment: fixed; +} + +.noise-bg { + background-image: url("data:image/svg+xml,%3C!-- svg: first layer --%3E%3Csvg viewBox='0 0 250 250' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='4' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); +} diff --git a/client/components/AssetsMonitor.vue b/client/components/AssetsMonitor.vue new file mode 100644 index 0000000..b6cbc5b --- /dev/null +++ b/client/components/AssetsMonitor.vue @@ -0,0 +1,305 @@ + + + diff --git a/client/components/DevtoolsGraph.vue b/client/components/DevtoolsGraph.vue index 175bde6..337334f 100644 --- a/client/components/DevtoolsGraph.vue +++ b/client/components/DevtoolsGraph.vue @@ -6,17 +6,17 @@ const props = withDefaults(defineProps<{ value?: number unit?: string label?: string - color?: 'green' | 'yellow' + color?: 'primary' | 'warning' | 'error' }>(), { points: () => [], value: 0, unit: '', label: '', - color: 'green', + color: 'primary', }) const textColor = computed(() => { // '' - return props.color === 'yellow' ? 'text-[#827717] dark:text-[#EAB306]' : 'text-[#15803D] dark:text-[#34E676]' + return props.color === 'warning' ? 'text-[#827717] dark:text-[#EAB306]' : 'text-[#15803D] dark:text-[#34E676]' }) const height = 40 @@ -50,10 +50,11 @@ const pointsF = computed(() => props.points.map( border-none font-sans " + :class="`graph-${color}`" >
{{ Math.round(value) }} {{ unit }}
@@ -75,21 +76,24 @@ const pointsF = computed(() => props.points.map( diff --git a/client/components/PerformanceMonitor.vue b/client/components/PerformanceMonitor.vue index fd593ae..4d0dcc0 100644 --- a/client/components/PerformanceMonitor.vue +++ b/client/components/PerformanceMonitor.vue @@ -1,105 +1,259 @@ diff --git a/client/components/TheHeader.vue b/client/components/TheHeader.vue new file mode 100644 index 0000000..602884c --- /dev/null +++ b/client/components/TheHeader.vue @@ -0,0 +1,39 @@ + + + diff --git a/client/components/TreeGraph.vue b/client/components/TreeGraph.vue new file mode 100644 index 0000000..f9c7112 --- /dev/null +++ b/client/components/TreeGraph.vue @@ -0,0 +1,107 @@ + + + \ No newline at end of file diff --git a/client/components/TreeInspector.vue b/client/components/TreeInspector.vue new file mode 100644 index 0000000..bc8839e --- /dev/null +++ b/client/components/TreeInspector.vue @@ -0,0 +1,287 @@ + + + diff --git a/client/components/scene-graph/index.vue b/client/components/scene-graph/index.vue new file mode 100644 index 0000000..786428f --- /dev/null +++ b/client/components/scene-graph/index.vue @@ -0,0 +1,116 @@ + + + diff --git a/client/composables/useDevtoolsHook.ts b/client/composables/useDevtoolsHook.ts index a344653..51a07eb 100644 --- a/client/composables/useDevtoolsHook.ts +++ b/client/composables/useDevtoolsHook.ts @@ -1,8 +1,11 @@ -import type { TresContext, TresObject } from '@tresjs/core' +import type { TresObject } from '@tresjs/core' import type { Scene } from 'three' import type { SceneGraphObject, ProgramObject } from '../types' -import { reactive } from '#imports' -import type { UnwrapNestedRefs } from '#imports' +import { reactive, shallowReactive } from '#imports' +import { createSharedComposable } from '@vueuse/core' +import type { UnwrapNestedRefs } from 'vue' +import { getSceneGraph } from '../utils/graph' +import { extractAllAssets, type AssetInfo } from '../utils/assets' export interface FPSState { value: number @@ -47,22 +50,26 @@ export interface RendererState { export interface DevtoolsHookReturn { scene: { objects: number - graph: Record + graph: SceneGraphObject | null value: Scene | undefined + selected: TresObject | undefined + assets: AssetInfo[] } fps: FPSState memory: MemoryState renderer: RendererState } -const scene = reactive<{ +const scene = shallowReactive<{ objects: number - graph: Record + graph: SceneGraphObject | null value: Scene | undefined + selected: TresObject | undefined }>({ objects: 0, - graph: {}, + graph: null, value: undefined, + selected: undefined, }) const gl = { @@ -99,71 +106,6 @@ const gl = { }), } satisfies RendererType -const icons: Record = { - 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: SceneGraphObject = { - 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 -} - -function getSceneGraph(scene: TresObject) { - function buildGraph(object: TresObject, node: SceneGraphObject) { - object.children.forEach((child: TresObject) => { - const childNode = createNode(child) - node.children.push(childNode) - buildGraph(child, childNode) - }) - } - - const root = createNode(scene) - buildGraph(scene, root) - - return root -} - function countObjectsInScene(scene: Scene) { let count = 0 @@ -177,31 +119,100 @@ function countObjectsInScene(scene: Scene) { return count } -export function useDevtoolsHook(): DevtoolsHookReturn { +export interface DevtoolsState { + scene: { + objects: number + graph: SceneGraphObject | null + value: Scene | undefined + selected: TresObject | undefined + assets: AssetInfo[] + } + fps: FPSState + memory: MemoryState + renderer: RendererState +} + +function _useDevtoolsHook(): DevtoolsHookReturn { + const state: DevtoolsState = { + scene: shallowReactive({ + objects: 0, + graph: null, + value: undefined, + selected: undefined, + assets: [], + }), + fps: { + value: 0, + accumulator: [], + lastLoggedTime: Date.now(), + logInterval: 1000, + }, + memory: { + currentMem: 0, + averageMem: 0, + maxMemory: 0, + allocatedMem: 0, + accumulator: [], + lastLoggedTime: Date.now(), + logInterval: 1000, + }, + renderer: { + info: { + render: { + frame: 0, + calls: 0, + triangles: 0, + points: 0, + lines: 0, + }, + memory: { + geometries: 0, + textures: 0, + }, + programs: [], + }, + }, + } + + let lastSceneUuid: string | null = null + // Connect with Core const tresGlobalHook = { - cb(context: TresContext) { - scene.value = context.scene.value - scene.objects = countObjectsInScene(context.scene.value) - Object.assign(gl.renderer.info.render, context.renderer.value.info.render) - Object.assign(gl.renderer.info.memory, context.renderer.value.info.memory) - gl.renderer.info.programs = [...(context.renderer.value.info.programs || []) as unknown as ProgramObject[]] - Object.assign(gl.fps, context.perf.fps) - gl.fps.accumulator = [...context.perf.fps.accumulator] - Object.assign(gl.memory, context.perf.memory) - gl.memory.accumulator = [...context.perf.memory.accumulator] - scene.graph = getSceneGraph(context.scene.value as unknown as TresObject) - /* - console.log('Devtools hook updated', context.renderer.value.info.render.triangles) */ + cb({ context, performance }: { context: any; performance: any }) { + if (context.scene.value.children.length > 0) { + // Use scene UUID for lightweight change detection + const currentSceneUuid = context.scene.value.uuid + if (currentSceneUuid !== lastSceneUuid) { + state.scene.value = context.scene.value + state.scene.objects = countObjectsInScene(context.scene.value) + state.scene.graph = getSceneGraph(context.scene.value as unknown as TresObject) + state.scene.assets = extractAllAssets(context.scene.value) + lastSceneUuid = currentSceneUuid + } + } + else { + // Only clear if we currently have a scene + if (state.scene.value !== undefined) { + state.scene.value = undefined + state.scene.graph = null + state.scene.assets = [] + lastSceneUuid = null + } + } + + Object.assign(state.fps, performance.fps) + state.fps.accumulator = [...performance.fps.accumulator] + Object.assign(state.memory, performance.memory) + state.memory.accumulator = [...performance.memory.accumulator] + Object.assign(state.renderer.info.render, context.renderer.instance.info.render) + Object.assign(state.renderer.info.memory, context.renderer.instance.info.memory) + state.renderer.info.programs = [...(context.renderer.instance.info.programs || []) as unknown as ProgramObject[]] }, } window.parent.parent.__TRES__DEVTOOLS__ = tresGlobalHook - return { - scene, - fps: gl.fps, - memory: gl.memory, - renderer: gl.renderer, - } + return state } + +export const useDevtoolsHook = createSharedComposable(_useDevtoolsHook) diff --git a/client/nuxt.config.ts b/client/nuxt.config.ts index 9f45840..c9d61ca 100644 --- a/client/nuxt.config.ts +++ b/client/nuxt.config.ts @@ -3,9 +3,9 @@ import { resolve } from 'pathe' export default defineNuxtConfig({ modules: [ - '@nuxt/devtools-ui-kit', - '@unocss/nuxt', - '@nuxt/ui', + '@nuxt/ui-pro', + '@vueuse/nuxt', + /* '@nuxt/devtools-ui-kit', */ '@nuxt/icon', ], ssr: false, @@ -13,28 +13,28 @@ export default defineNuxtConfig({ app: { baseURL: '/__tres_nuxt_devtools', }, - - compatibilityDate: '2024-12-19', + css: ['~/assets/css/main.css'], nitro: { output: { publicDir: resolve(__dirname, '../dist/client'), }, }, - vite: { optimizeDeps: { include: [ '@nuxt/devtools-kit/iframe-client', ], }, + server: { + hmr: { + // Instead of go through proxy, we directly connect real port of the client app + clientPort: +(process.env.PORT || 3300), + }, + }, }, - icon: { - size: '24px', // default size applied - class: 'icon', // default class applied - aliases: { - mesh: 'carbon:cube', - }, + uiPro: { + license: process.env.NUXT_UI_PRO_LICENSE, }, }) diff --git a/client/package.json b/client/package.json index d102ad2..642b115 100644 --- a/client/package.json +++ b/client/package.json @@ -1,14 +1,4 @@ { - "name": "tres-nuxt-module", - "type": "module", - "private": true, - "devDependencies": { - "@nuxt/devtools-ui-kit": "^2.4.1", - "@nuxt/icon": "^1.10.2", - "@nuxt/ui": "^2.20.0", - "@unocss/nuxt": "^66.1.2" - }, - "dependencies": { - "vue": "^3.5.14" - } + "name": "tres-nuxt-devtools", + "private": true } diff --git a/client/pages/index.vue b/client/pages/index.vue index b6a20a2..75e5be2 100644 --- a/client/pages/index.vue +++ b/client/pages/index.vue @@ -1,93 +1,65 @@