Mr.doob 10 месяцев назад
Родитель
Сommit
1e071da6db
7 измененных файлов с 1233 добавлено и 1221 удалено
  1. 49 49
      devtools/background.js
  2. 660 660
      devtools/bridge.js
  3. 12 0
      devtools/content-script.js
  4. 2 2
      devtools/index.html
  5. 23 23
      devtools/manifest.json
  6. 27 27
      devtools/panel/panel.html
  7. 460 460
      devtools/panel/panel.js

+ 49 - 49
devtools/background.js

@@ -3,62 +3,62 @@ 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);
-    }
-  });
+	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);
-    }
-  });
+	// 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
+	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
-    });
-  }
+	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
+		});
+	}
 }); 

+ 660 - 660
devtools/bridge.js

@@ -5,668 +5,668 @@
 
 // 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;
-            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'));
-        }
-    }
-
-    // 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 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.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);
-                
-        // 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');
-    }
+	// 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;
+			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'));
+		}
+	}
+
+	// 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 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.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);
+				
+		// 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');
+	}
 
 } else {
 
-    console.log('DevTools: Bridge already initialized');
+	console.log('DevTools: Bridge already initialized');
 
 } 

+ 12 - 0
devtools/content-script.js

@@ -101,18 +101,24 @@ 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
@@ -131,18 +137,24 @@ 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

+ 2 - 2
devtools/index.html

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

+ 23 - 23
devtools/manifest.json

@@ -1,25 +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"
-  ]
+	"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"
+	]
 } 

+ 27 - 27
devtools/panel/panel.html

@@ -1,34 +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>
+	<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>
+	<div id="scene-tree"></div>
+	<script src="panel.js"></script>
 </body>
 </html> 

+ 460 - 460
devtools/panel/panel.js

@@ -1,22 +1,22 @@
 // Store the state of our inspector
 const state = {
-    revision: null,
-    scenes: new Map(),
-    renderers: new Map(),
-    objects: new Map()
+	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"
+	name: "three-devtools"
 });
 
 // Initialize the connection with the inspected tab ID
 backgroundPageConnection.postMessage({
-    name: 'init',
-    tabId: chrome.devtools.inspectedWindow.tabId
+	name: 'init',
+	tabId: chrome.devtools.inspectedWindow.tabId
 });
 
 console.log('Connected to background page with tab ID:', chrome.devtools.inspectedWindow.tabId);
@@ -30,496 +30,496 @@ 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
-    } );
+	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
+	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 = '';
-    }
+	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);
-    }
+	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;
-    }
+	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 = '';
-        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));
-    }
+	// 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 = '';
+		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 '📦';
+	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);
-        });
-    }
+	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);
-    }
+	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;
-    }
+	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);
 

粤ICP备19079148号