Mr.doob 10 месяцев назад
Родитель
Сommit
03bc87facb

+ 111 - 0
devtools/README.md

@@ -0,0 +1,111 @@
+# Three.js DevTools Extension
+
+This Chrome DevTools extension provides debugging capabilities for Three.js applications. It allows you to inspect scenes, objects, materials, and renderers, manipulate visibility, and monitor rendering performance.
+
+## Installation
+
+1. **Development Mode**:
+   - Open Chrome and navigate to `chrome://extensions/`
+   - Enable "Developer mode" (toggle in the top-right corner)
+   - Click "Load unpacked" and select the `devtools` directory
+   - The extension will now be available in Chrome DevTools when inspecting pages that use Three.js
+
+2. **Usage**:
+   - Open Chrome DevTools on a page using Three.js (F12 or Right-click > Inspect)
+   - Click on the "Three.js" tab in DevTools
+   - The panel will automatically detect and display Three.js scenes and renderers
+
+## Code Flow Overview
+
+### Extension Architecture
+
+The extension follows a standard Chrome DevTools extension architecture:
+
+1. **Background Script** (`background.js`): Manages the extension lifecycle and creates the DevTools panel
+2. **DevTools Script** (`devtools.js`): Creates the panel when the DevTools window opens
+3. **Content Script** (`content-script.js`): Injects the bridge into web pages and relays messages
+4. **Injected Bridge** (`inject.js` → `bridge.js`): Creates the communication layer between Three.js and DevTools
+5. **Panel UI** (`panel/*.js`, `panel/*.html`): The DevTools panel interface
+
+### Initialization Flow
+
+1. When a page loads, `content-script.js` injects `inject.js`
+2. `inject.js` injects `bridge.js` into the page
+3. `bridge.js` creates the `__THREE_DEVTOOLS__` global object
+4. When Three.js loads, it detects this object and sends initialization events
+
+### Bridge Operation (`bridge.js`)
+
+The bridge acts as the communication layer between Three.js and the DevTools panel:
+
+1. **Event Management**: Creates a custom event system to handle Three.js objects
+   - Uses `DevToolsEventTarget` to manage event listeners and backlog events
+   - Events include: `observe`, `update`, `remove`, `register`
+
+2. **Object Tracking**:
+   - `getObjectData()`: Extracts essential data from Three.js objects
+   - Maintains a map of all observed objects (`devTools.objects`)
+   - Automatically tracks scenes, objects, materials, and renderers
+
+3. **Scene Observation**:
+   - When Three.js sends an `observe` event for a scene, the bridge:
+     - Records the scene in `__observed_scenes`
+     - Traverses all child objects to populate the object hierarchy
+     - Sets up monitoring to track changes
+
+4. **Renderer Monitoring**:
+   - For WebGLRenderer instances:
+     - Tracks renderer properties, dimensions and draw calls
+     - Updates statistics periodically
+     - Extracts WebGL context information
+
+5. **Message Handling**:
+   - Listens for messages from the panel UI
+   - Processes commands like visibility toggling and scene traversal
+
+### Panel Interface (`panel/`)
+
+The panel UI provides the visual representation of the Three.js objects:
+
+1. **Tree View**: Displays hierarchical representation of scenes and objects
+2. **Properties Panel**: Shows detailed properties of selected objects
+3. **Performance Monitoring**: Displays renderer statistics and WebGL information
+
+## Key Features
+
+- **Scene Hierarchy Visualization**: Browse the complete scene graph
+- **Object Inspection**: View mesh, material, and geometry properties
+- **Visibility Control**: Toggle visibility of scene objects
+- **Renderer Statistics**: Monitor draw calls, triangles, and memory usage
+- **WebGL Information**: View context and capabilities information
+
+## Communication Flow
+
+1. **Three.js → Bridge**: Three.js detects the `__THREE_DEVTOOLS__` object and sends events
+2. **Bridge → Content Script**: Bridge posts messages to window
+3. **Content Script → DevTools Panel**: Content script relays messages to the DevTools panel
+4. **DevTools Panel → Content Script**: Panel sends commands back via messaging
+5. **Content Script → Bridge**: Content script relays commands to the bridge
+6. **Bridge → Three.js**: Bridge manipulates Three.js objects directly
+
+## Key Components
+
+- **DevToolsEventTarget**: Custom event system with backlogging for async loading
+- **Object Observation**: Tracks Three.js objects and their properties
+- **Scene Monitoring**: Periodically checks for changes in observed scenes
+- **WebGLRenderer Monitoring**: Tracks performance statistics for renderers
+- **Visibility Toggle**: Allows showing/hiding objects in the scene
+
+## Integration with Three.js
+
+The extension relies on Three.js having built-in support for DevTools. When Three.js detects the presence of `window.__THREE_DEVTOOLS__`, it sends events about scenes, renderers, and other objects to the extension.
+
+The bridge then processes these events, organizes the data, and provides a clean interface for the DevTools panel to display and interact with.
+
+## Development
+
+To modify the extension:
+
+1. Edit the relevant files in the `devtools` directory
+2. Reload the extension in `chrome://extensions/` by clicking the refresh icon
+3. Reopen DevTools to see your changes

+ 64 - 0
devtools/background.js

@@ -0,0 +1,64 @@
+// Store connections between devtools and tabs
+const connections = new Map();
+
+// Listen for connections from the devtools panel
+chrome.runtime.onConnect.addListener(port => {
+  let tabId;
+  
+  // Listen for messages from the devtools panel
+  port.onMessage.addListener(message => {
+    if (message.name === 'init') {
+      tabId = message.tabId;
+      connections.set(tabId, port);
+      console.log('DevTools connection initialized for tab:', tabId);
+    } else if ((message.name === 'traverse' || message.name === 'reload-scene') && tabId) {
+      console.log('Background: Forwarding', message.name, 'message to tab', tabId, 'with UUID:', message.uuid);
+      // Forward traverse or reload request to content script
+      chrome.tabs.sendMessage(tabId, message);
+    }
+  });
+
+  // Clean up when devtools is closed
+  port.onDisconnect.addListener(() => {
+    if (tabId) {
+      connections.delete(tabId);
+      console.log('DevTools connection closed for tab:', tabId);
+    }
+  });
+});
+
+// Listen for messages from the content script
+chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  if (sender.tab) {
+    const tabId = sender.tab.id;
+    const port = connections.get(tabId);
+    if (port) {
+      // Forward the message to the devtools panel
+      try {
+        port.postMessage(message);
+        // Send immediate response to avoid "message channel closed" error
+        sendResponse({ received: true });
+      } catch (e) {
+        console.error('Error posting message to devtools:', e);
+        // If the port is broken, clean up the connection
+        connections.delete(tabId);
+      }
+    }
+  }
+  return false; // Return false to indicate synchronous handling
+});
+
+// Listen for page navigation events
+chrome.webNavigation.onCommitted.addListener(details => {
+  const { tabId, frameId } = details;
+  const port = connections.get(tabId);
+  
+  if (port) {
+    console.log('Navigation in tab:', tabId, 'frame:', frameId);
+    port.postMessage({
+      id: 'three-devtools',
+      type: 'committed',
+      frameId: frameId
+    });
+  }
+}); 

+ 892 - 0
devtools/bridge.js

@@ -0,0 +1,892 @@
+/**
+ * This script injected by the installed three.js developer
+ * tools extension.
+ */
+
+// Only initialize if not already initialized
+if (!window.__THREE_DEVTOOLS__) {
+    // Create our custom EventTarget with logging
+    class DevToolsEventTarget extends EventTarget {
+        constructor() {
+            super();
+            this._ready = false;
+            this._backlog = [];
+            this.objects = new Map();
+            
+            // console.log('DevTools: Creating ThreeDevToolsTarget');
+        }
+
+        addEventListener(type, listener, options) {
+            // console.log('DevTools: Adding listener for:', type);
+            super.addEventListener(type, listener, options);
+
+            // If this is the first listener for a type, and we have backlogged events,
+            // check if we should process them
+            if (type !== 'devtools-ready' && this._backlog.length > 0) {
+                this.dispatchEvent(new CustomEvent('devtools-ready'));
+            }
+        }
+
+        dispatchEvent(event) {
+            console.log('DevTools: Dispatching event:', event.type);
+            if (this._ready || event.type === 'devtools-ready') {
+                if (event.type === 'devtools-ready') {
+                    this._ready = true;
+                    console.log('DevTools: Processing backlog:', this._backlog.length, 'events');
+                    const backlog = this._backlog;
+                    this._backlog = [];
+                    backlog.forEach(e => super.dispatchEvent(e));
+                }
+                return super.dispatchEvent(event);
+            } else {
+                console.log('DevTools: Backlogging event:', event.type);
+                this._backlog.push(event);
+                return false; // Return false to indicate synchronous handling
+            }
+        }
+
+        reset() {
+            console.log('DevTools: Resetting state');
+            
+            // Clear all monitoring intervals
+            this.objects.forEach((obj, uuid) => {
+                if (obj.isRenderer || obj.isScene) {
+                    const interval = monitoringIntervals.get(obj);
+                    if (interval) {
+                        clearInterval(interval);
+                        monitoringIntervals.delete(obj);
+                    }
+                }
+            });
+            
+            // Clear objects map
+            this.objects.clear();
+            
+            // Clear backlog
+            this._backlog = [];
+            
+            // Reset ready state
+            this._ready = false;
+            
+            // Clear observed arrays
+            observedScenes.length = 0;
+            observedRenderers.length = 0;
+        }
+    }
+
+    // Create and expose the __THREE_DEVTOOLS__ object
+    const devTools = new DevToolsEventTarget();
+    Object.defineProperty(window, '__THREE_DEVTOOLS__', {
+        value: devTools,
+        configurable: false,
+        enumerable: true,
+        writable: false
+    });
+
+    // Store monitoring intervals without polluting objects
+    const monitoringIntervals = new WeakMap();
+
+    // Declare arrays for tracking observed objects
+    const observedScenes = [];
+    const observedRenderers = [];
+
+    // Function to get renderer data
+    function getRendererData(renderer) {
+        try {
+            const webglInfo = getWebGLInfo(renderer);
+            const data = {
+                uuid: renderer.uuid || generateUUID(),
+                type: 'WebGLRenderer',
+                name: '',
+                visible: true,
+                isScene: false,
+                isObject3D: false,
+                isCamera: false,
+                isLight: false,
+                isMesh: false,
+                isRenderer: true,
+                parent: null,
+                children: [],
+                // Add renderer-specific properties
+                properties: {
+                    width: renderer.domElement ? renderer.domElement.clientWidth : 0,
+                    height: renderer.domElement ? renderer.domElement.clientHeight : 0,
+                    drawingBufferWidth: renderer.domElement ? renderer.domElement.width : 0,
+                    drawingBufferHeight: renderer.domElement ? renderer.domElement.height : 0,
+                    alpha: renderer.alpha || false,
+                    antialias: renderer.antialias || false,
+                    autoClear: renderer.autoClear,
+                    autoClearColor: renderer.autoClearColor,
+                    autoClearDepth: renderer.autoClearDepth,
+                    autoClearStencil: renderer.autoClearStencil,
+                    localClippingEnabled: renderer.localClippingEnabled,
+                    physicallyCorrectLights: renderer.physicallyCorrectLights,
+                    outputColorSpace: renderer.outputColorSpace,
+                    toneMapping: renderer.toneMapping,
+                    toneMappingExposure: renderer.toneMappingExposure,
+                    shadowMapEnabled: renderer.shadowMap ? renderer.shadowMap.enabled : false,
+                    shadowMapType: renderer.shadowMap ? renderer.shadowMap.type : 'None',
+                    // Get current info values
+                    info: {
+                        render: {
+                            frame: renderer.info.render.frame,
+                            calls: renderer.info.render.calls,
+                            triangles: renderer.info.render.triangles,
+                            points: renderer.info.render.points,
+                            lines: renderer.info.render.lines,
+                            geometries: renderer.info.render.geometries,
+                            sprites: renderer.info.render.sprites
+                        },
+                        memory: {
+                            geometries: renderer.info.memory.geometries,
+                            textures: renderer.info.memory.textures,
+                            programs: renderer.info.programs ? renderer.info.programs.length : 0,
+                            renderLists: renderer.info.memory.renderLists,
+                            renderTargets: renderer.info.memory.renderTargets
+                        },
+                        webgl: webglInfo || {
+                            version: 'unknown',
+                            gpu: 'unknown',
+                            vendor: 'unknown',
+                            maxTextures: 'unknown',
+                            maxAttributes: 'unknown',
+                            maxTextureSize: 'unknown',
+                            maxCubemapSize: 'unknown'
+                        }
+                    }
+                }
+            };
+            return data;
+        } catch (error) {
+            console.warn('DevTools: Error getting renderer data:', error);
+            return null;
+        }
+    }
+
+    // Function to get object hierarchy
+    function getObjectData(obj) {
+        try {
+            // Special case for WebGLRenderer
+            if (obj.isWebGLRenderer === true) {
+                return getRendererData(obj);
+            }
+
+            // Get descriptive name for the object
+            let descriptiveName = obj.name || '';
+            if (obj.isMesh) {
+                const geoType = obj.geometry ? obj.geometry.type : 'Unknown';
+                const matType = obj.material ? 
+                    (Array.isArray(obj.material) ? 
+                        obj.material.map(m => m.type).join(', ') : 
+                        obj.material.type) : 
+                    'Unknown';
+                descriptiveName = `${obj.type || 'Mesh'} <span class="object-details">${geoType} ${matType}</span>`;
+            } else if (obj.isLight) {
+                descriptiveName = `${obj.type || 'Light'}`;
+            } else if (obj.isCamera) {
+                descriptiveName = `${obj.type || 'Camera'}`;
+            } else {
+                descriptiveName = obj.type || obj.constructor.name;
+            }
+
+            const data = {
+                uuid: obj.uuid,
+                type: obj.type || obj.constructor.name,
+                name: descriptiveName,
+                visible: obj.visible !== undefined ? obj.visible : true,
+                isScene: obj.isScene === true,
+                isObject3D: obj.isObject3D === true,
+                isCamera: obj.isCamera === true,
+                isLight: obj.isLight === true,
+                isMesh: obj.isMesh === true,
+                isRenderer: obj.isWebGLRenderer === true,
+                parent: obj.parent ? obj.parent.uuid : null,
+                children: obj.children ? obj.children.map(child => child.uuid) : []
+            };
+            
+            // console.log('DevTools: Object data:', data);
+            return data;
+        } catch (error) {
+            console.warn('DevTools: Error getting object data:', error);
+            return null;
+        }
+    }
+
+    // Generate a UUID for objects that don't have one
+    function generateUUID() {
+        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+            const r = Math.random() * 16 | 0;
+            const v = c === 'x' ? r : (r & 0x3 | 0x8);
+            return v.toString(16);
+        });
+    }
+
+    // Listen for Three.js registration
+    devTools.addEventListener('register', (event) => {
+        console.log('DevTools: Three.js registered with revision:', event.detail.revision);
+        dispatchEvent('register', event.detail);
+    });
+
+    // Listen for object observations
+    devTools.addEventListener('observe', (event) => {
+        const obj = event.detail;
+        if (!obj) {
+            console.warn('DevTools: Received observe event with null/undefined detail');
+            return;
+        }
+        
+        console.log('DevTools: Received object:', {
+            type: obj.type || obj.constructor.name,
+            isWebGLRenderer: obj.isWebGLRenderer === true,
+            hasUUID: !!obj.uuid
+        });
+        
+        // Generate UUID if needed (especially for WebGLRenderer)
+        if (!obj.uuid) {
+            obj.uuid = generateUUID();
+            console.log('DevTools: Generated UUID for object:', obj.uuid);
+        }
+        
+        // Skip if already registered
+        if (devTools.objects.has(obj.uuid)) {
+            console.log('DevTools: Object already registered:', obj.uuid);
+            return;
+        }
+        
+        console.log('DevTools: Found Three.js object:', obj.type || obj.constructor.name);
+        
+        // Get data for this object
+        const data = getObjectData(obj);
+        if (data) {
+            console.log('DevTools: Got object data:', data);
+            
+            // If this is a renderer, start periodic updates
+            if (obj.isWebGLRenderer) {
+                console.log('DevTools: Starting periodic updates for renderer:', obj.uuid);
+                data.properties = getRendererProperties(obj);
+                observedRenderers.push(obj);
+                startRendererMonitoring(obj);
+            }
+            
+            // Store the object data
+            devTools.objects.set(obj.uuid, data);
+            dispatchEvent('observe', data);
+            
+            // If this is a scene, store the reference and traverse its children
+            if (obj.isScene) {
+                console.log('DevTools: Traversing scene children');
+                
+                // Store the scene reference locally
+                observedScenes.push(obj);
+                
+                // First observe all existing children
+                const processedObjects = new Set([obj.uuid]);
+                
+                function observeObject(object) {
+                    if (!processedObjects.has(object.uuid)) {
+                        processedObjects.add(object.uuid);
+                        const objectData = getObjectData(object);
+                        if (objectData) {
+                            devTools.objects.set(object.uuid, objectData);
+                            dispatchEvent('observe', objectData);
+                        }
+                        // Process children
+                        object.children.forEach(child => observeObject(child));
+                    }
+                }
+                
+                // Process all children
+                obj.children.forEach(child => observeObject(child));
+                
+                // Start monitoring for changes
+                startSceneMonitoring(obj);
+            }
+        }
+    });
+
+    // Function to get renderer properties
+    function getRendererProperties(renderer) {
+        const webglInfo = getWebGLInfo(renderer);
+        return {
+            width: renderer.domElement ? renderer.domElement.clientWidth : 0,
+            height: renderer.domElement ? renderer.domElement.clientHeight : 0,
+            drawingBufferWidth: renderer.domElement ? renderer.domElement.width : 0,
+            drawingBufferHeight: renderer.domElement ? renderer.domElement.height : 0,
+            alpha: renderer.alpha || false,
+            antialias: renderer.antialias || false,
+            autoClear: renderer.autoClear,
+            autoClearColor: renderer.autoClearColor,
+            autoClearDepth: renderer.autoClearDepth,
+            autoClearStencil: renderer.autoClearStencil,
+            localClippingEnabled: renderer.localClippingEnabled,
+            physicallyCorrectLights: renderer.physicallyCorrectLights,
+            outputColorSpace: renderer.outputColorSpace,
+            toneMapping: renderer.toneMapping,
+            toneMappingExposure: renderer.toneMappingExposure,
+            shadowMapEnabled: renderer.shadowMap ? renderer.shadowMap.enabled : false,
+            shadowMapType: renderer.shadowMap ? renderer.shadowMap.type : 'None',
+            info: {
+                render: {
+                    frame: renderer.info.render.frame,
+                    calls: renderer.info.render.calls,
+                    triangles: renderer.info.render.triangles,
+                    points: renderer.info.render.points,
+                    lines: renderer.info.render.lines,
+                    geometries: renderer.info.render.geometries,
+                    sprites: renderer.info.render.sprites
+                },
+                memory: {
+                    geometries: renderer.info.memory.geometries,
+                    textures: renderer.info.memory.textures,
+                    programs: renderer.info.programs ? renderer.info.programs.length : 0,
+                    renderLists: renderer.info.memory.renderLists,
+                    renderTargets: renderer.info.memory.renderTargets
+                },
+                webgl: webglInfo || {
+                    version: 'unknown',
+                    gpu: 'unknown',
+                    vendor: 'unknown',
+                    maxTextures: 'unknown',
+                    maxAttributes: 'unknown',
+                    maxTextureSize: 'unknown',
+                    maxCubemapSize: 'unknown'
+                }
+            }
+        };
+    }
+
+    // Function to start renderer monitoring
+    function startRendererMonitoring(renderer) {
+        // Clear any existing monitoring
+        const existingInterval = monitoringIntervals.get(renderer);
+        if (existingInterval) {
+            clearInterval(existingInterval);
+        }
+
+        // Function to monitor renderer properties
+        function monitorRendererProperties() {
+            try {
+                // Skip updates if devtools is not visible
+                if ( ! devTools.isVisible ) {
+                    return;
+                }
+
+                const data = devTools.objects.get( renderer.uuid );
+                if ( ! data ) {
+                    clearInterval( intervalId );
+                    monitoringIntervals.delete( renderer );
+                    return;
+                }
+
+                const newProperties = getRendererProperties( renderer );
+                if ( JSON.stringify( data.properties ) !== JSON.stringify( newProperties ) ) {
+                    data.properties = newProperties;
+                    dispatchEvent( 'update', data );
+                }
+
+            } catch ( error ) {
+
+                // If we get an "Extension context invalidated" error, stop monitoring
+                if ( error.message.includes( 'Extension context invalidated' ) ) {
+                    clearInterval( intervalId );
+                    monitoringIntervals.delete( renderer );
+                    devTools.reset();
+                    return;
+                }
+
+                console.warn( 'DevTools: Error in renderer monitoring:', error );
+
+            }
+        }
+
+        const intervalId = setInterval(monitorRendererProperties, 1000);
+        monitoringIntervals.set(renderer, intervalId);
+    }
+
+    // Start periodic renderer checks
+    console.log('DevTools: Starting periodic renderer checks');
+
+    // Function to check if bridge is available
+    function checkBridgeAvailability() {
+        const hasDevTools = window.hasOwnProperty('__THREE_DEVTOOLS__');
+        const devToolsValue = window.__THREE_DEVTOOLS__;
+
+        // If we have devtools and we're interactive or complete, trigger ready
+        if (hasDevTools && devToolsValue && (document.readyState === 'interactive' || document.readyState === 'complete')) {
+            devTools.dispatchEvent(new CustomEvent('devtools-ready'));
+        }
+    }
+
+    // Function to update renderer properties
+    function updateRendererProperties(renderer) {
+        const storedData = devTools.objects.get(renderer.uuid);
+        if (!storedData || !storedData.isRenderer) return;
+
+        const webglInfo = getWebGLInfo(renderer);
+        
+        // Get current info values directly from the renderer
+        const currentInfo = {
+            render: {
+                frame: renderer.info.render.frame,
+                calls: renderer.info.render.calls,
+                triangles: renderer.info.render.triangles,
+                points: renderer.info.render.points,
+                lines: renderer.info.render.lines,
+                geometries: renderer.info.render.geometries,
+                sprites: renderer.info.render.sprites
+            },
+            memory: {
+                geometries: renderer.info.memory.geometries,
+                textures: renderer.info.memory.textures,
+                programs: renderer.info.programs ? renderer.info.programs.length : 0,
+                renderLists: renderer.info.memory.renderLists,
+                renderTargets: renderer.info.memory.renderTargets
+            },
+            webgl: webglInfo || {
+                version: 'unknown',
+                gpu: 'unknown',
+                vendor: 'unknown',
+                maxTextures: 'unknown',
+                maxAttributes: 'unknown',
+                maxTextureSize: 'unknown',
+                maxCubemapSize: 'unknown'
+            }
+        };
+
+        const newProperties = {
+            width: renderer.domElement ? renderer.domElement.clientWidth : 0,
+            height: renderer.domElement ? renderer.domElement.clientHeight : 0,
+            drawingBufferWidth: renderer.domElement ? renderer.domElement.width : 0,
+            drawingBufferHeight: renderer.domElement ? renderer.domElement.height : 0,
+            alpha: renderer.alpha || false,
+            antialias: renderer.antialias || false,
+            autoClear: renderer.autoClear,
+            autoClearColor: renderer.autoClearColor,
+            autoClearDepth: renderer.autoClearDepth,
+            autoClearStencil: renderer.autoClearStencil,
+            localClippingEnabled: renderer.localClippingEnabled,
+            physicallyCorrectLights: renderer.physicallyCorrectLights,
+            outputColorSpace: renderer.outputColorSpace,
+            toneMapping: renderer.toneMapping,
+            toneMappingExposure: renderer.toneMappingExposure,
+            shadowMapEnabled: renderer.shadowMap ? renderer.shadowMap.enabled : false,
+            shadowMapType: renderer.shadowMap ? renderer.shadowMap.type : 'None',
+            info: currentInfo
+        };
+
+        // Always update since info values change frequently
+        storedData.properties = newProperties;
+        dispatchEvent('update', {
+            uuid: renderer.uuid,
+            type: 'WebGLRenderer',
+            properties: newProperties
+        });
+    }
+
+    // Watch for readyState changes
+    document.addEventListener('readystatechange', () => {
+        // console.log('DevTools: Document readyState changed to:', document.readyState);
+        if (document.readyState === 'loading') {
+            devTools.reset();
+        }
+        checkBridgeAvailability();
+    });
+
+    // Watch for page unload to reset state
+    window.addEventListener('beforeunload', () => {
+        devTools.reset();
+    });
+
+    // Listen for messages from the content script
+    window.addEventListener('message', function(event) {
+        // Only accept messages from the same frame
+        if (event.source !== window) return;
+
+        const message = event.data;
+        if (!message || message.id !== 'three-devtools') return;
+
+        // Handle traverse request
+        if (message.name === 'traverse' && message.uuid) {
+            // Find the scene in our objects
+            const scene = Array.from(devTools.objects.values())
+                .find(obj => obj.uuid === message.uuid && obj.isScene);
+            
+            if (scene) {
+                console.log('DevTools: Re-traversing scene:', scene.uuid);
+                // Find the actual scene object in the page
+                const actualScene = findObjectByUUID(message.uuid);
+                if (actualScene) {
+                    reloadSceneObjects(actualScene);
+                }
+            }
+        }
+        // Handle reload-scene request
+        else if (message.name === 'reload-scene' && message.uuid) {
+            console.log('DevTools: Received reload request for scene:', message.uuid);
+            const actualScene = findObjectByUUID(message.uuid);
+            if (actualScene) {
+                reloadSceneObjects(actualScene);
+            } else {
+                console.warn('DevTools: Could not find scene for reload:', message.uuid);
+            }
+        }
+        // Handle visibility toggle
+        else if (message.name === 'visibility' && message.uuid !== undefined) {
+            toggleVisibility(message.uuid, message.visible);
+        }
+    });
+
+    // Helper function to find a Three.js object by UUID
+    function findObjectByUUID(uuid) {
+        console.log('DevTools: Finding object by UUID:', uuid);
+        
+        // Check for scenes we've observed
+        const sceneData = Array.from(devTools.objects.values())
+            .find(obj => obj.uuid === uuid && obj.isScene);
+        
+        if (sceneData) {
+            // For scenes accessed through observe events, they are already available
+            // through the scene object reference passed to the observe handler
+            for (const observedScene of observedScenes) {
+                if (observedScene && observedScene.uuid === uuid) {
+                    console.log('DevTools: Found scene in observed scenes');
+                    return observedScene;
+                }
+            }
+        }
+        
+        console.warn('DevTools: Could not find object with UUID:', uuid);
+        return null;
+    }
+
+    // Function to process scene data
+    function processSceneData(sceneData) {
+                    // Process all objects from the JSON data
+                    if (sceneData.geometries) {
+                        Object.values(sceneData.geometries).forEach(geo => {
+                            if (geo.uuid && !devTools.objects.has(geo.uuid)) {
+                                devTools.objects.set(geo.uuid, {
+                                    uuid: geo.uuid,
+                                    type: geo.type,
+                                    name: '',
+                                    visible: true,
+                                    isGeometry: true,
+                                    parent: null,
+                                    children: []
+                                });
+                                dispatchEvent('observe', devTools.objects.get(geo.uuid));
+                            }
+                        });
+                    }
+                    
+                    if (sceneData.materials) {
+                        Object.values(sceneData.materials).forEach(mat => {
+                            if (mat.uuid && !devTools.objects.has(mat.uuid)) {
+                                devTools.objects.set(mat.uuid, {
+                                    uuid: mat.uuid,
+                                    type: mat.type,
+                                    name: '',
+                                    visible: true,
+                                    isMaterial: true,
+                                    parent: null,
+                                    children: []
+                                });
+                                dispatchEvent('observe', devTools.objects.get(mat.uuid));
+                            }
+                        });
+                    }
+                    
+                    if (sceneData.textures) {
+                        Object.values(sceneData.textures).forEach(tex => {
+                            if (tex.uuid && !devTools.objects.has(tex.uuid)) {
+                                devTools.objects.set(tex.uuid, {
+                                    uuid: tex.uuid,
+                                    type: 'Texture',
+                                    name: '',
+                                    visible: true,
+                                    isTexture: true,
+                                    parent: null,
+                                    children: []
+                                });
+                                dispatchEvent('observe', devTools.objects.get(tex.uuid));
+                            }
+                        });
+                    }
+                    
+                    // Process object hierarchy
+                    function processObject(obj) {
+                        if (!obj || !obj.uuid) return;
+                        
+                        const data = {
+                            uuid: obj.uuid,
+                            type: obj.type,
+                            name: obj.name || '',
+                            visible: obj.visible !== undefined ? obj.visible : true,
+                            isScene: obj.type === 'Scene',
+                            isObject3D: true,
+                            isCamera: obj.type.includes('Camera'),
+                            isLight: obj.type.includes('Light'),
+                            isMesh: obj.type === 'Mesh' || obj.type === 'SkinnedMesh',
+                            parent: obj.parent,
+                            children: obj.children || [],
+                            matrix: obj.matrix,
+                            material: obj.material,
+                            geometry: obj.geometry
+                        };
+                        
+                        if (!devTools.objects.has(obj.uuid)) {
+                            devTools.objects.set(obj.uuid, data);
+                            dispatchEvent('observe', data);
+                        }
+                        
+            if (obj.children) {
+                obj.children.forEach(processObject);
+            }
+        }
+        
+        processObject(sceneData.object);
+    }
+
+    function dispatchEvent(type, detail) {
+        try {
+            window.postMessage({
+                id: 'three-devtools',
+                type: type,
+                detail: detail
+            }, '*');
+        } catch (error) {
+            // If we get an "Extension context invalidated" error, stop all monitoring
+            if (error.message.includes('Extension context invalidated')) {
+                console.log('DevTools: Extension context invalidated, stopping monitoring');
+                devTools.reset();
+                return;
+            }
+            console.warn('DevTools: Error dispatching event:', error);
+        }
+    }
+
+    function getWebGLInfo(renderer) {
+        if (!renderer || !renderer.domElement) return null;
+        
+        const gl = renderer.domElement.getContext('webgl2') || renderer.domElement.getContext('webgl');
+        if (!gl) return null;
+
+        return {
+            version: gl.getParameter(gl.VERSION),
+            gpu: gl.getParameter(gl.RENDERER),
+            vendor: gl.getParameter(gl.VENDOR),
+            maxTextures: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
+            maxAttributes: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
+            maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
+            maxCubemapSize: gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)
+        };
+    }
+
+    // Add visibility toggle function
+    function toggleVisibility(uuid, visible) {
+        // Update our local state
+        const obj = devTools.objects.get(uuid);
+        if (!obj) return;
+        
+            obj.visible = visible;
+        console.log('DevTools: Setting visibility of', obj.type || obj.constructor.name, 'to', visible);
+        
+        // Find the actual Three.js object using our observed scenes
+        if (observedScenes && observedScenes.length > 0) {
+            for (const scene of observedScenes) {
+                    let found = false;
+                scene.traverse((object) => {
+                        if (object.uuid === uuid) {
+                            object.visible = visible;
+                            // If it's a light, update its helper visibility too
+                            if (object.isLight && object.helper) {
+                                object.helper.visible = visible;
+                            }
+                            found = true;
+                        console.log('DevTools: Updated visibility in scene object');
+                        }
+                    });
+                    if (found) break;
+            }
+        } else {
+            console.warn('DevTools: No observed scenes found for visibility toggle');
+        }
+    }
+
+    // Function to start scene monitoring
+    function startSceneMonitoring(scene) {
+        // Clear any existing monitoring
+        const existingInterval = monitoringIntervals.get(scene);
+        if (existingInterval) {
+            clearInterval(existingInterval);
+        }
+
+        // Set up monitoring interval
+        const intervalId = setInterval(() => {
+            try {
+                // Clear existing objects except renderers and the scene itself
+                devTools.objects.forEach((obj, uuid) => {
+                    if (!obj.isRenderer && uuid !== scene.uuid) {
+                        devTools.objects.delete(uuid);
+                        dispatchEvent('remove', { uuid });
+                    }
+                });
+                
+                // Traverse and recreate the entire object list
+                function traverseScene(object) {
+                    const objectData = getObjectData(object);
+                    if (objectData) {
+                        devTools.objects.set(object.uuid, objectData);
+                        dispatchEvent('observe', objectData);
+                        
+                        // Traverse children
+                        object.children.forEach(child => traverseScene(child));
+                    }
+                }
+                
+                // Start traversal from scene root
+                traverseScene(scene);
+            } catch (error) {
+                // If we get an "Extension context invalidated" error, stop monitoring
+                if (error.message.includes('Extension context invalidated')) {
+                    clearInterval(intervalId);
+                    monitoringIntervals.delete(scene);
+                    devTools.reset();
+                    return;
+                }
+                console.warn('DevTools: Error in scene monitoring:', error);
+            }
+        }, 1000);
+
+        monitoringIntervals.set(scene, intervalId);
+
+        // Clean up monitoring when scene is disposed
+        const originalDispose = scene.dispose;
+        scene.dispose = function() {
+            const intervalId = monitoringIntervals.get(this);
+            if (intervalId) {
+                clearInterval(intervalId);
+                monitoringIntervals.delete(this);
+            }
+            
+            if (originalDispose) {
+                originalDispose.call(this);
+            }
+        };
+    }
+
+    // Function to manually reload scene objects
+    function reloadSceneObjects(scene) {
+        console.log('DevTools: Manually reloading scene objects for scene:', scene.uuid);
+        
+        // Get a set of existing object IDs
+        const existingObjects = new Set(devTools.objects.keys());
+        
+        // Track new objects to avoid duplicates
+        const processedObjects = new Set();
+        
+        // Recursively observe all objects
+        function observeObject(object) {
+            if (!processedObjects.has(object.uuid)) {
+                processedObjects.add(object.uuid);
+                
+                console.log('DevTools: Processing object during reload:', object.type || object.constructor.name, object.uuid);
+                
+                // Get object data
+                const objectData = getObjectData(object);
+                if (objectData) {
+                    if (devTools.objects.has(object.uuid)) {
+                        // Update existing object
+                        const existingData = devTools.objects.get(object.uuid);
+                        existingData.children = objectData.children;
+                        dispatchEvent('update', existingData);
+                    } else {
+                        // Add new object
+                        devTools.objects.set(object.uuid, objectData);
+                        dispatchEvent('observe', objectData);
+                        console.log('DevTools: New object observed during reload:', object.type || object.constructor.name);
+                    }
+                }
+                
+                // Process children recursively
+                if (object.children && object.children.length > 0) {
+                    console.log('DevTools: Processing', object.children.length, 'children of', object.type || object.constructor.name);
+                    object.children.forEach(child => observeObject(child));
+                }
+            }
+        }
+        
+        // Start with the scene itself to ensure everything is traversed
+        observeObject(scene);
+        
+        console.log('DevTools: Scene reload complete. Processed', processedObjects.size, 'objects');
+    }
+
+    // Function to get simplified scene data
+    function getSimpleSceneData(scene) {
+        function getBasicObjectData(obj) {
+            const data = {
+                uuid: obj.uuid,
+                type: obj.type || obj.constructor.name,
+                name: obj.name || '',
+                visible: obj.visible !== undefined ? obj.visible : true,
+                position: obj.position ? { x: obj.position.x, y: obj.position.y, z: obj.position.z } : null,
+                children: []
+            };
+
+            // Add material info if present
+            if (obj.material) {
+                if (Array.isArray(obj.material)) {
+                    data.materials = obj.material.map(mat => ({
+                        uuid: mat.uuid,
+                        type: mat.type,
+                        name: mat.name || ''
+                    }));
+                } else {
+                    data.material = {
+                        uuid: obj.material.uuid,
+                        type: obj.material.type,
+                        name: obj.material.name || ''
+                    };
+                }
+            }
+
+            // Add geometry info if present
+            if (obj.geometry) {
+                data.geometry = {
+                    uuid: obj.geometry.uuid,
+                    type: obj.geometry.type,
+                    name: obj.geometry.name || ''
+                };
+            }
+
+            // Add specific properties based on object type
+            if (obj.isLight) {
+                data.intensity = obj.intensity;
+                data.color = obj.color ? obj.color.getHex() : null;
+            } else if (obj.isCamera) {
+                data.fov = obj.fov;
+                data.near = obj.near;
+                data.far = obj.far;
+            }
+
+            return data;
+        }
+
+        function traverseScene(obj) {
+            const data = getBasicObjectData(obj);
+            
+            if (obj.children && obj.children.length > 0) {
+                data.children = obj.children.map(child => traverseScene(child));
+            }
+            
+            return data;
+        }
+
+        return {
+            object: traverseScene(scene)
+        };
+    }
+} else {
+    console.log('DevTools: Bridge already initialized');
+} 

+ 195 - 0
devtools/content-script.js

@@ -0,0 +1,195 @@
+// This script runs in the context of the web page
+console.log( 'Three.js DevTools: Content script loaded at document.readyState:', document.readyState );
+
+// Function to inject the bridge script
+function injectBridge( target = document ) {
+
+	const script = document.createElement( 'script' );
+	script.src = chrome.runtime.getURL( 'bridge.js' );
+	script.onload = function () {
+
+		this.remove();
+
+	};
+
+	( target.head || target.documentElement ).appendChild( script );
+
+}
+
+// Also inject into any existing iframes
+function injectIntoIframes() {
+
+	const iframes = document.querySelectorAll( 'iframe' );
+	iframes.forEach( iframe => {
+
+		try {
+
+			injectBridge( iframe.contentDocument );
+
+		} catch ( e ) {
+
+			// Ignore cross-origin iframe errors
+			console.log( 'DevTools: Could not inject into iframe:', e );
+
+		}
+
+	} );
+
+}
+
+// Initial injection
+injectBridge();
+injectIntoIframes();
+
+// Watch for new iframes being added
+const observer = new MutationObserver( mutations => {
+
+	mutations.forEach( mutation => {
+
+		mutation.addedNodes.forEach( node => {
+
+			if ( node.tagName === 'IFRAME' ) {
+
+				// Wait for iframe to load
+				node.addEventListener( 'load', () => {
+
+					try {
+
+						injectBridge( node.contentDocument );
+
+					} catch ( e ) {
+
+						// Ignore cross-origin iframe errors
+						console.log( 'DevTools: Could not inject into iframe:', e );
+
+					}
+
+				} );
+
+			}
+
+		} );
+
+	} );
+
+} );
+
+observer.observe( document.documentElement, {
+	childList: true,
+	subtree: true
+} );
+
+// Helper function to check if extension context is valid
+function isExtensionContextValid() {
+
+	try {
+
+		// This will throw if context is invalidated
+		chrome.runtime.getURL( '' );
+		return true;
+
+	} catch ( error ) {
+
+		return false;
+
+	}
+
+}
+
+// Handle messages from the main window
+function handleMainWindowMessage( event ) {
+
+	// Only accept messages from the same frame
+	if ( event.source !== window ) {
+		return;
+	}
+
+	const message = event.data;
+	if ( ! message || message.id !== 'three-devtools' ) {
+		return;
+	}
+
+	// Check extension context before sending message
+	if ( ! isExtensionContextValid() ) {
+		console.warn( 'Extension context invalidated, cannot send message' );
+		return;
+	}
+
+	// Add source information
+	const messageWithSource = {
+		...event.data,
+		source: event.source === window ? 'main' : 'iframe'
+	};
+
+	// Forward to background page
+	chrome.runtime.sendMessage( messageWithSource );
+
+}
+
+// Handle messages from iframes
+function handleIframeMessage( event ) {
+
+	// Skip messages from main window
+	if ( event.source === window ) {
+		return;
+	}
+
+	const message = event.data;
+	if ( ! message || message.id !== 'three-devtools' ) {
+		return;
+	}
+
+	// Check extension context before sending message
+	if ( ! isExtensionContextValid() ) {
+		console.warn( 'Extension context invalidated, cannot send message' );
+		return;
+	}
+
+	// Add source information
+	const messageWithSource = {
+		...event.data,
+		source: 'iframe'
+	};
+
+	// Forward to background page
+	chrome.runtime.sendMessage( messageWithSource );
+
+}
+
+// Handle messages from devtools
+function handleDevtoolsMessage( message, sender, sendResponse ) {
+
+	// Forward traverse requests to both main page and iframes
+	if ( message.name === 'traverse' || message.name === 'reload-scene' || message.name === 'visibility' ) {
+
+		console.log( 'Content script: Forwarding message to page:', message );
+		window.postMessage( message, '*' );
+
+		// Also try to forward to all iframes
+		const iframes = document.querySelectorAll( 'iframe' );
+		iframes.forEach( iframe => {
+
+			try {
+
+				iframe.contentWindow.postMessage( message, '*' );
+
+			} catch ( e ) {
+				// Ignore cross-origin iframe errors
+			}
+
+		} );
+
+		// Send immediate response to avoid "message channel closed" error
+		sendResponse( { received: true } );
+
+	}
+
+	// Return false to indicate synchronous handling
+	return false;
+
+}
+
+// Add event listeners
+window.addEventListener( 'message', handleMainWindowMessage, false );
+window.addEventListener( 'message', handleIframeMessage, false );
+chrome.runtime.onMessage.addListener( handleDevtoolsMessage );

+ 18 - 0
devtools/devtools.js

@@ -0,0 +1,18 @@
+try {
+
+	chrome.devtools.panels.create(
+		'Three.js',
+		null,
+		'panel/panel.html',
+		function ( panel ) {
+
+			console.log( 'Three.js DevTools panel created' );
+
+		}
+	);
+
+} catch ( error ) {
+
+	console.error( 'Failed to create Three.js panel:', error );
+
+}

+ 9 - 0
devtools/index.html

@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <script src="devtools.js"></script>
+</head>
+<body>
+</body>
+</html>

+ 109 - 0
devtools/inject.js

@@ -0,0 +1,109 @@
+// Create the __THREE_DEVTOOLS__ object immediately
+window.__THREE_DEVTOOLS__ = new EventTarget();
+
+// Store references to observed objects
+window.__THREE_DEVTOOLS__.objects = new Map();
+
+// Track visibility state
+window.__THREE_DEVTOOLS__.isVisible = true;
+
+// Listen for visibility updates from devtools panel
+window.__THREE_DEVTOOLS__.addEventListener( 'visibility', ( event ) => {
+
+	window.__THREE_DEVTOOLS__.isVisible = event.detail;
+	window.postMessage( {
+		id: 'three-devtools',
+		type: 'visibility',
+		state: event.detail
+	}, '*' );
+
+} );
+
+// Function to get renderer data
+function getRendererData( renderer ) {
+
+	return {
+		uuid: renderer.uuid,
+		type: 'WebGLRenderer',
+		name: 'WebGLRenderer',
+		isRenderer: true,
+		properties: {
+			width: renderer.domElement.width,
+			height: renderer.domElement.height,
+			drawCalls: renderer.info.render.calls,
+			triangles: renderer.info.render.triangles
+		}
+	};
+
+}
+
+// Function to get object hierarchy
+function getObjectData( obj ) {
+
+	if ( obj.isWebGLRenderer === true ) {
+
+		return getRendererData( obj );
+
+	}
+
+	return {
+		uuid: obj.uuid,
+		type: obj.type,
+		name: obj.name || obj.type,
+		isScene: obj.type === 'Scene',
+		isRenderer: false,
+		parent: obj.parent ? obj.parent.uuid : null,
+		children: obj.children ? obj.children.map( child => child.uuid ) : []
+	};
+
+}
+
+// Listen for Three.js registration
+window.__THREE_DEVTOOLS__.addEventListener( 'register', ( event ) => {
+
+	console.log( 'Three.js registered:', event.detail );
+	window.postMessage( {
+		type: 'FROM_THREE_INSPECTOR',
+		subType: 'register',
+		detail: event.detail
+	}, '*' );
+
+} );
+
+// Listen for object observations
+window.__THREE_DEVTOOLS__.addEventListener( 'observe', ( event ) => {
+
+	const obj = event.detail;
+	console.log( 'Three.js object observed:', obj.type );
+	const data = getObjectData( obj );
+	window.__THREE_DEVTOOLS__.objects.set( obj.uuid, data );
+
+	window.postMessage( {
+		type: 'FROM_THREE_INSPECTOR',
+		subType: 'observe',
+		detail: data
+	}, '*' );
+
+	// If this is a scene, also traverse its children
+	if ( obj.type === 'Scene' ) {
+
+		console.log( 'Traversing scene children' );
+		obj.traverse( ( child ) => {
+
+			if ( child !== obj ) {
+
+				const childData = getObjectData( child );
+				window.__THREE_DEVTOOLS__.objects.set( child.uuid, childData );
+				window.postMessage( {
+					type: 'FROM_THREE_INSPECTOR',
+					subType: 'observe',
+					detail: childData
+				}, '*' );
+
+			}
+
+		} );
+
+	}
+
+} );

+ 25 - 0
devtools/manifest.json

@@ -0,0 +1,25 @@
+{
+  "manifest_version": 3,
+  "name": "Three.js DevTools",
+  "version": "1.0",
+  "description": "Developer tools extension for Three.js",
+  "devtools_page": "index.html",
+  "background": {
+    "service_worker": "background.js",
+    "type": "module"
+  },
+  "content_scripts": [{
+    "matches": ["<all_urls>"],
+    "js": ["content-script.js"],
+    "all_frames": true,
+    "run_at": "document_start"
+  }],
+  "web_accessible_resources": [{
+    "resources": ["bridge.js"],
+    "matches": ["<all_urls>"]
+  }],
+  "permissions": [
+    "activeTab",
+    "webNavigation"
+  ]
+} 

+ 34 - 0
devtools/panel/panel.html

@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <style>
+        body {
+            margin: 0;
+            padding: 10px;
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+        }
+        #scene-tree {
+            width: 100%;
+            height: 100%;
+            overflow: auto;
+        }
+        .tree-item {
+            padding: 4px;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+        }
+        .tree-item:hover {
+            background-color: #f0f0f0;
+        }
+        .tree-item.selected {
+            background-color: #e0e0e0;
+        }
+    </style>
+</head>
+<body>
+    <div id="scene-tree"></div>
+    <script src="panel.js"></script>
+</body>
+</html> 

+ 544 - 0
devtools/panel/panel.js

@@ -0,0 +1,544 @@
+// Store the state of our inspector
+const state = {
+    revision: null,
+    scenes: new Map(),
+    renderers: new Map(),
+    objects: new Map()
+};
+
+console.log('Panel script loaded');
+
+// Create a connection to the background page
+const backgroundPageConnection = chrome.runtime.connect({
+    name: "three-devtools"
+});
+
+// Initialize the connection with the inspected tab ID
+backgroundPageConnection.postMessage({
+    name: 'init',
+    tabId: chrome.devtools.inspectedWindow.tabId
+});
+
+console.log('Connected to background page with tab ID:', chrome.devtools.inspectedWindow.tabId);
+
+// Store collapsed states
+const collapsedSections = new Map();
+
+// Initialize visibility state
+let isVisible = true;
+
+// Listen for visibility changes
+document.addEventListener( 'visibilitychange', () => {
+
+    isVisible = ! document.hidden;
+    
+    // Send visibility state to content script
+    chrome.tabs.sendMessage( chrome.devtools.inspectedWindow.tabId, {
+        name: 'visibility',
+        value: isVisible
+    } );
+
+} );
+
+// Initial visibility state
+chrome.tabs.sendMessage( chrome.devtools.inspectedWindow.tabId, {
+    name: 'visibility',
+    value: isVisible
+} );
+
+// Clear state when panel is reloaded
+function clearState() {
+    state.revision = null;
+    state.scenes.clear();
+    state.renderers.clear();
+    state.objects.clear();
+    const container = document.getElementById('scene-tree');
+    if (container) {
+        container.innerHTML = '';
+    }
+}
+
+// Listen for messages from the background page
+backgroundPageConnection.onMessage.addListener(function (message) {
+    console.log('Panel received message:', message);
+    if (message.id === 'three-devtools') {
+        handleThreeEvent(message);
+    }
+});
+
+function handleThreeEvent(message) {
+    console.log('Handling event:', message.type);
+    switch (message.type) {
+        case 'register':
+            state.revision = message.detail.revision;
+            updateUI();
+            break;
+        
+        case 'observe':
+            const detail = message.detail;
+            console.log('Observed object:', detail);
+            
+            // Only store each unique object once
+            if (!state.objects.has(detail.uuid)) {
+                state.objects.set(detail.uuid, detail);
+                
+                if (detail.isRenderer) {
+                    state.renderers.set(detail.uuid, detail);
+                }
+                else if (detail.isScene) {
+                    state.scenes.set(detail.uuid, detail);
+                }
+                
+                updateUI();
+            }
+            break;
+            
+        case 'update':
+            const update = message.detail;
+            if (update.type === 'WebGLRenderer') {
+                console.log('Received renderer update:', {
+                    uuid: update.uuid,
+                    hasProperties: !!update.properties,
+                    hasInfo: !!(update.properties && update.properties.info),
+                    timestamp: new Date().toISOString()
+                });
+                const renderer = state.renderers.get(update.uuid);
+                if (renderer) {
+                    renderer.properties = update.properties;
+                    updateRendererProperties(renderer);
+                }
+            }
+            break;
+            
+        case 'committed':
+            // Page was reloaded, clear state
+            clearState();
+            break;
+    }
+}
+
+// Function to update just the renderer properties in the UI
+function updateRendererProperties(renderer) {
+    // Find the renderer's properties container
+    const rendererElement = document.querySelector(`[data-uuid="${renderer.uuid}"]`);
+    if (!rendererElement) return;
+
+    const props = renderer.properties;
+    
+    // Update the renderer summary line
+    const label = rendererElement.querySelector('.label');
+    if (label) {
+        let detailsText = '';
+        if (props) {
+            const details = [`${props.width}x${props.height}`];
+            if (props.info) {
+                details.push(`${props.info.render.calls} calls`);
+                details.push(`${props.info.render.triangles.toLocaleString()} tris`);
+            }
+            detailsText = `<span class="object-details">${details.join(' ・ ')}</span>`;
+        }
+        label.innerHTML = `WebGLRenderer ${detailsText}`;
+    }
+
+    // Find or create properties container
+    let propsContainer = rendererElement.nextElementSibling;
+    if (!propsContainer || !propsContainer.classList.contains('properties-list')) {
+        propsContainer = document.createElement('div');
+        propsContainer.className = 'properties-list';
+        propsContainer.style.paddingLeft = rendererElement.style.paddingLeft.replace('px', '') + 24 + 'px';
+        rendererElement.parentNode.insertBefore(propsContainer, rendererElement.nextSibling);
+    }
+    
+    // Store current collapse states before clearing
+    const currentSections = propsContainer.querySelectorAll('details');
+    currentSections.forEach(section => {
+        const sectionKey = `${renderer.uuid}-${section.querySelector('summary').textContent}`;
+        collapsedSections.set(sectionKey, !section.open);
+    });
+    
+    // Clear existing properties
+    propsContainer.innerHTML = '';
+
+    // Create collapsible sections
+    function createSection(title, properties) {
+        const section = document.createElement('details');
+        section.className = 'properties-section';
+        
+        // Check if this section was previously collapsed
+        const sectionKey = `${renderer.uuid}-${title}`;
+        const wasCollapsed = collapsedSections.get(sectionKey);
+        // Start collapsed by default unless explicitly opened before
+        section.open = wasCollapsed === undefined ? false : !wasCollapsed;
+
+        const header = document.createElement('summary');
+        header.className = 'properties-header';
+        header.textContent = title;
+        section.appendChild(header);
+
+        // Add change listener to store collapse state
+        section.addEventListener('toggle', () => {
+            collapsedSections.set(sectionKey, !section.open);
+        });
+
+        properties.forEach(([name, value]) => {
+            if (value !== undefined) {
+                const propElem = document.createElement('div');
+                propElem.className = 'property-item';
+                propElem.innerHTML = `
+                    <span class="property-name">${name}:</span>
+                    <span class="property-value">${value}</span>
+                `;
+                section.appendChild(propElem);
+            }
+        });
+
+        return section;
+    }
+
+    // Basic properties section
+    const basicProps = [
+        ['Size', `${props.width}x${props.height}`],
+        ['Drawing Buffer', `${props.drawingBufferWidth}x${props.drawingBufferHeight}`],
+        ['Alpha', props.alpha],
+        ['Antialias', props.antialias],
+        ['Output Color Space', props.outputColorSpace],
+        ['Tone Mapping', props.toneMapping],
+        ['Tone Mapping Exposure', props.toneMappingExposure],
+        ['Shadows', props.shadowMapEnabled ? `enabled (${props.shadowMapType})` : 'disabled'],
+        ['Auto Clear', props.autoClear],
+        ['Auto Clear Color', props.autoClearColor],
+        ['Auto Clear Depth', props.autoClearDepth],
+        ['Auto Clear Stencil', props.autoClearStencil],
+        ['Local Clipping', props.localClippingEnabled],
+        ['Physically Correct Lights', props.physicallyCorrectLights]
+    ];
+    propsContainer.appendChild(createSection('Properties', basicProps));
+
+    // Add real-time stats if available
+    if (props.info) {
+        // WebGL Info section
+        if (props.info.webgl) {
+            const webglInfo = [
+                ['Version', props.info.webgl.version],
+                ['GPU', props.info.webgl.gpu],
+                ['Vendor', props.info.webgl.vendor],
+                ['Max Textures', props.info.webgl.maxTextures],
+                ['Max Attributes', props.info.webgl.maxAttributes],
+                ['Max Texture Size', props.info.webgl.maxTextureSize],
+                ['Max Cubemap Size', props.info.webgl.maxCubemapSize]
+            ];
+            propsContainer.appendChild(createSection('WebGL', webglInfo));
+        }
+
+        // Render info section
+        const renderStats = [
+            ['Frame', props.info.render.frame],
+            ['Draw Calls', props.info.render.calls],
+            ['Triangles', props.info.render.triangles.toLocaleString()],
+            ['Points', props.info.render.points],
+            ['Lines', props.info.render.lines],
+            ['Sprites', props.info.render.sprites],
+            ['Geometries', props.info.render.geometries]
+        ];
+        propsContainer.appendChild(createSection('Render Stats', renderStats));
+
+        // Memory info section
+        const memoryStats = [
+            ['Geometries', props.info.memory.geometries],
+            ['Textures', props.info.memory.textures],
+            ['Shader Programs', props.info.memory.programs],
+            ['Render Lists', props.info.memory.renderLists],
+            ['Render Targets', props.info.memory.renderTargets]
+        ];
+        propsContainer.appendChild(createSection('Memory', memoryStats));
+    }
+}
+
+// Function to get an object icon based on its type
+function getObjectIcon(obj) {
+    if (obj.isScene) return '🌍';
+    if (obj.isRenderer) return '🎨';
+    if (obj.isCamera) return '📷';
+    if (obj.isLight) return '💡';
+    if (obj.isMesh) return obj.materialType === 'MeshBasicMaterial' ? '⬜' : '🔷';
+    if (obj.type === 'Group') return '📁';
+    return '📦';
+}
+
+// Function to render an object and its children
+function renderObject(obj, container, level = 0) {
+    const elem = document.createElement('div');
+    elem.className = 'tree-item';
+    elem.style.paddingLeft = `${level * 20}px`;
+    elem.setAttribute('data-uuid', obj.uuid);
+    
+    const icon = getObjectIcon(obj);
+    let displayName = obj.name || obj.type;
+    
+    // Add renderer properties if available
+    if (obj.isRenderer && obj.properties) {
+        const props = obj.properties;
+        const details = [`${props.width}x${props.height}`];
+        if (props.info) {
+            details.push(`${props.info.render.calls} calls`);
+            details.push(`${props.info.render.triangles.toLocaleString()} tris`);
+        }
+        displayName = `WebGLRenderer <span class="object-details">${details.join(' ・ ')}</span>`;
+    }
+    
+    // Add object count for scenes
+    if (obj.isScene) {
+        let objectCount = -1;
+        // Count all descendants recursively
+        function countObjects(uuid) {
+            const object = state.objects.get(uuid);
+            if (object) {
+                objectCount++;
+                if (object.children) {
+                    object.children.forEach(childId => countObjects(childId));
+                }
+            }
+        }
+        countObjects(obj.uuid);
+        displayName = `${displayName} <span class="object-details">${objectCount} objects</span>`;
+    }
+        
+    elem.innerHTML = `
+        <span class="icon">${icon}</span>
+        <span class="label">${displayName}</span>
+        <span class="type">${obj.type}</span>
+    `;
+    
+    container.appendChild(elem);
+
+    // Add renderer properties using the updateRendererProperties function
+    if (obj.isRenderer && obj.properties) {
+        updateRendererProperties(obj);
+    }
+
+    // Handle children
+    if (obj.children && obj.children.length > 0) {
+        // Create a container for children
+        const childContainer = document.createElement('div');
+        childContainer.className = 'children';
+        container.appendChild(childContainer);
+
+        // Get all children and sort them by type for better organization
+        const children = obj.children
+            .map(childId => state.objects.get(childId))
+            .filter(child => child !== undefined)
+            .sort((a, b) => {
+                // Sort order: Cameras, Lights, Groups, Meshes, Others
+                const typeOrder = {
+                    isCamera: 1,
+                    isLight: 2,
+                    isGroup: 3,
+                    isMesh: 4
+                };
+                const aOrder = Object.entries(typeOrder).find(([key]) => a[key])?.['1'] || 5;
+                const bOrder = Object.entries(typeOrder).find(([key]) => b[key])?.['1'] || 5;
+                return aOrder - bOrder;
+            });
+
+        // Render each child
+        children.forEach(child => {
+            renderObject(child, childContainer, level + 1);
+        });
+    }
+}
+
+// Function to update the UI
+function updateUI() {
+    console.log('Updating UI with state:', {
+        revision: state.revision,
+        scenesCount: state.scenes.size,
+        renderersCount: state.renderers.size,
+        objectsCount: state.objects.size
+    });
+
+    const container = document.getElementById('scene-tree');
+    if (!container) {
+        console.error('Could not find scene-tree container!');
+        return;
+    }
+    container.innerHTML = '';
+
+    // Add version info if available
+    if (state.revision) {
+        const versionInfo = document.createElement('div');
+        versionInfo.className = 'info-item';
+        versionInfo.textContent = `Three.js r${state.revision}`;
+        container.appendChild(versionInfo);
+    }
+
+    // Add renderers section
+    if (state.renderers.size > 0) {
+        const renderersSection = document.createElement('div');
+        renderersSection.className = 'section';
+        renderersSection.innerHTML = '<h3>Renderers</h3>';
+        
+        state.renderers.forEach(renderer => {
+            renderObject(renderer, renderersSection);
+        });
+        
+        container.appendChild(renderersSection);
+    }
+
+    // Add scenes section
+    if (state.scenes.size > 0) {
+        const scenesSection = document.createElement('div');
+        scenesSection.className = 'section';
+        scenesSection.innerHTML = '<h3>Scenes</h3>';
+        
+        state.scenes.forEach(scene => {
+            renderObject(scene, scenesSection);
+        });
+        
+        container.appendChild(scenesSection);
+    }
+}
+
+// Add styles
+const style = document.createElement('style');
+style.textContent = `
+    body {
+        margin: 0;
+        padding: 16px;
+        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+        font-size: 12px;
+        color: #333;
+        background: #fff;
+    }
+    .info-item {
+        padding: 8px 12px;
+        background: #f5f5f5;
+        border-radius: 4px;
+        margin-bottom: 16px;
+        font-family: monospace;
+        color: #666;
+    }
+    .section {
+        margin-bottom: 24px;
+    }
+    .section h3 {
+        margin: 0 0 8px 0;
+        font-size: 11px;
+        text-transform: uppercase;
+        color: #666;
+        font-weight: 500;
+        border-bottom: 1px solid #eee;
+        padding-bottom: 4px;
+    }
+    .tree-item {
+        display: flex;
+        align-items: center;
+        padding: 2px;
+        cursor: pointer;
+        border-radius: 4px;
+    }
+    .tree-item:hover {
+        background: #e0e0e0;
+    }
+    .tree-item .icon {
+        margin-right: 4px;
+        opacity: 0.7;
+    }
+    .tree-item .label {
+        flex: 1;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+    .tree-item .label .object-details {
+        color: #aaa;
+        margin-left: 4px;
+        font-weight: normal;
+    }
+    .tree-item .type {
+        margin-left: 8px;
+        opacity: 0.5;
+        font-size: 0.9em;
+    }
+    .children {
+        margin-left: 0;
+    }
+    
+    .properties-list {
+        font-family: monospace;
+        font-size: 11px;
+        margin: 4px 0;
+        padding: 4px 0;
+        border-left: 1px solid #eee;
+    }
+    
+    .properties-section {
+        margin-bottom: 8px;
+    }
+    
+    .properties-header {
+        color: #666;
+        font-weight: bold;
+        padding: 4px 0;
+        cursor: pointer;
+        user-select: none;
+    }
+    
+    .properties-header:hover {
+        background-color: #f5f5f5;
+    }
+    
+    .property-item {
+        padding: 2px 16px;
+        display: flex;
+        align-items: center;
+    }
+    
+    .property-name {
+        color: #666;
+        margin-right: 8px;
+        min-width: 120px;
+    }
+    
+    .property-value {
+        color: #333;
+    }
+    
+    .visibility-btn {
+        background: none;
+        border: none;
+        cursor: pointer;
+        padding: 2px 6px;
+        font-size: 12px;
+        opacity: 0.5;
+        border-radius: 4px;
+        margin-right: 4px;
+    }
+    
+    .visibility-btn:hover {
+        background: #e0e0e0;
+        opacity: 1;
+    }
+    
+    .tree-item:hover .visibility-btn {
+        opacity: 0.8;
+    }
+`;
+document.head.appendChild(style);
+
+// Initial UI update
+clearState();
+updateUI();
+
+// Function to send commands to the background script
+function sendCommand(name, data = {}) {
+    backgroundPageConnection.postMessage({
+        name: name,
+        ...data
+    });
+}
+
+// Function to reload scene - keep this for potential future use via other UI controls
+function reloadScene(uuid) {
+    console.log('Panel: Sending reload-scene command for scene:', uuid);
+    sendCommand('reload-scene', { uuid });
+}

BIN
devtools/screenshot.png


粤ICP备19079148号