Mr.doob 10 месяцев назад
Родитель
Сommit
68bf958da6
6 измененных файлов с 401 добавлено и 438 удалено
  1. 5 5
      devtools/background.js
  2. 30 115
      devtools/bridge.js
  3. 30 17
      devtools/content-script.js
  4. 159 0
      devtools/panel/panel.css
  5. 2 0
      devtools/panel/panel.html
  6. 175 301
      devtools/panel/panel.js

+ 5 - 5
devtools/background.js

@@ -1,4 +1,4 @@
-// Store connections between devtools and tabs
+// Map tab IDs to connections
 const connections = new Map();
 
 // Listen for connections from the devtools panel
@@ -10,11 +10,13 @@ chrome.runtime.onConnect.addListener(port => {
 		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);
+		} else if (message.name === 'request-initial-state' && tabId) {
+			chrome.tabs.sendMessage(tabId, message);
+		} else if (tabId === undefined) {
+			console.warn('Background: Message received from panel before init:', message);
 		}
 	});
 
@@ -22,7 +24,6 @@ chrome.runtime.onConnect.addListener(port => {
 	port.onDisconnect.addListener(() => {
 		if (tabId) {
 			connections.delete(tabId);
-			console.log('DevTools connection closed for tab:', tabId);
 		}
 	});
 });
@@ -54,7 +55,6 @@ chrome.webNavigation.onCommitted.addListener(details => {
 	const port = connections.get(tabId);
 	
 	if (port) {
-		console.log('Navigation in tab:', tabId, 'frame:', frameId);
 		port.postMessage({
 			id: 'three-devtools',
 			type: 'committed',

+ 30 - 115
devtools/bridge.js

@@ -40,18 +40,7 @@ if (!window.__THREE_DEVTOOLS__) {
 		}
 
 		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);
-					}
-				}
-			});
+			// console.log('DevTools: Resetting state');
 			
 			// Clear objects map
 			this.objects.clear();
@@ -70,7 +59,6 @@ if (!window.__THREE_DEVTOOLS__) {
 
 	// Create and expose the __THREE_DEVTOOLS__ object
 	const devTools = new DevToolsEventTarget();
-	devTools.isVisible = true; // Initialize visibility state
 	Object.defineProperty(window, '__THREE_DEVTOOLS__', {
 		value: devTools,
 		configurable: false,
@@ -78,9 +66,6 @@ if (!window.__THREE_DEVTOOLS__) {
 		writable: false
 	});
 
-	// Store monitoring intervals without polluting objects
-	const monitoringIntervals = new WeakMap();
-
 	// Declare arrays for tracking observed objects
 	const observedScenes = [];
 	const observedRenderers = [];
@@ -209,7 +194,7 @@ if (!window.__THREE_DEVTOOLS__) {
 
 	// Listen for Three.js registration
 	devTools.addEventListener('register', (event) => {
-		console.log('DevTools: Three.js registered with revision:', event.detail.revision);
+		// console.log('DevTools: Three.js registered with revision:', event.detail.revision);
 		dispatchEvent('register', event.detail);
 	});
 
@@ -221,34 +206,28 @@ if (!window.__THREE_DEVTOOLS__) {
 			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);
+			// 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);
+			// console.log('DevTools: Object already registered:', obj.uuid);
 			return;
 		}
 		
-		console.log('DevTools: Found Three.js object:', obj.type || obj.constructor.name);
+		// 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);
+			// 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);
+				// console.log('DevTools: Starting periodic updates for renderer:', obj.uuid);
 				data.properties = getRendererProperties(obj);
 				observedRenderers.push(obj);
 				startRendererMonitoring(obj);
@@ -260,7 +239,7 @@ if (!window.__THREE_DEVTOOLS__) {
 			
 			// If this is a scene, store the reference and traverse its children
 			if (obj.isScene) {
-				console.log('DevTools: Traversing scene children');
+				// console.log('DevTools: Traversing scene children');
 				
 				// Store the scene reference locally
 				observedScenes.push(obj);
@@ -293,13 +272,15 @@ if (!window.__THREE_DEVTOOLS__) {
 	// Function to get renderer properties
 	function getRendererProperties(renderer) {
 		const webglInfo = getWebGLInfo(renderer);
+		const context = renderer.getContext ? renderer.getContext() : null;
+		const contextAttributes = context ? context.getContextAttributes() : null;
 		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,
+			alpha: contextAttributes ? contextAttributes.alpha : false,
+			antialias: contextAttributes ? contextAttributes.antialias : false,
 			autoClear: renderer.autoClear,
 			autoClearColor: renderer.autoClearColor,
 			autoClearDepth: renderer.autoClearDepth,
@@ -343,25 +324,11 @@ if (!window.__THREE_DEVTOOLS__) {
 
 	// 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 ) {
-					// console.log('DevTools: Panel not visible, skipping renderer update'); // Optional debug log
-					return;
-				}
-
 				const data = devTools.objects.get( renderer.uuid );
 				if ( ! data ) {
-					clearInterval( intervalId );
-					monitoringIntervals.delete( renderer );
 					return;
 				}
 
@@ -377,24 +344,18 @@ if (!window.__THREE_DEVTOOLS__) {
 					oldProperties.drawingBufferHeight !== newProperties.drawingBufferHeight ||
 					JSON.stringify(oldProperties.info?.render) !== JSON.stringify(newProperties.info?.render) || // Compare render stats
 					JSON.stringify(oldProperties.info?.memory) !== JSON.stringify(newProperties.info?.memory) // Compare memory stats
-					// Add other comparisons if needed, or use full stringify as fallback:
-					// || JSON.stringify(oldProperties) !== JSON.stringify(newProperties)
 				);
 
 				if ( changed ) {
-					console.log('DevTools: Renderer properties changed, dispatching update for', renderer.uuid); // Log dispatched updates
 					data.properties = newProperties;
 					dispatchEvent( 'update', data );
 				} else {
-					// console.log('DevTools: Renderer properties unchanged for', renderer.uuid); // Optional: for debugging
 				}
 
 			} 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;
 				}
@@ -404,12 +365,11 @@ if (!window.__THREE_DEVTOOLS__) {
 			}
 		}
 
-		const intervalId = setInterval(monitorRendererProperties, 1000);
-		monitoringIntervals.set(renderer, intervalId);
+		// TODO: Trigger monitorRendererProperties some other way, e.g., on demand or via events?
 	}
 
 	// Start periodic renderer checks
-	console.log('DevTools: Starting periodic renderer checks');
+	// console.log('DevTools: Starting periodic renderer checks');
 
 	// Function to check if bridge is available
 	function checkBridgeAvailability() {
@@ -471,10 +431,14 @@ if (!window.__THREE_DEVTOOLS__) {
 		else if (message.name === 'visibility' && message.uuid !== undefined) {
 			toggleVisibility(message.uuid, message.visible);
 		}
-		// Handle visibility update from panel (via content script)
-		else if ( message.name === 'panel-visibility' ) {
-			devTools.isVisible = message.value;
-			// console.log( 'DevTools: Visibility set to', devTools.isVisible ); // Optional debug log
+		// Handle request for initial state from panel
+		else if ( message.name === 'request-initial-state' ) {
+			// console.log('DevTools: Received request-initial-state, resending objects...');
+			// Resend all known objects to the panel
+			devTools.objects.forEach( ( objectData ) => {
+				dispatchEvent('observe', objectData);
+			});
+			// console.log('DevTools: Finished resending objects.');
 		}
 	});
 
@@ -569,64 +533,15 @@ if (!window.__THREE_DEVTOOLS__) {
 
 	// 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);
+		// Keep track of known object UUIDs for this scene (excluding renderers)
+		const knownObjectUUIDs = new Set();
+		devTools.objects.forEach((obj, uuid) => {
+			if (!obj.isRenderer) {
+				knownObjectUUIDs.add(uuid);
 			}
-		}, 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);
-			}
-		};
+		// TODO: Trigger scene updates some other way?
 	}
 
 	// Function to manually reload scene objects
@@ -675,6 +590,6 @@ if (!window.__THREE_DEVTOOLS__) {
 
 } else {
 
-	console.log('DevTools: Bridge already initialized');
+	// console.log('DevTools: Bridge already initialized');
 
 } 

+ 30 - 17
devtools/content-script.js

@@ -1,5 +1,5 @@
 // This script runs in the context of the web page
-console.log( 'Three.js DevTools: Content script loaded at document.readyState:', document.readyState );
+// console.log( 'Three.js DevTools: Content script loaded at document_readyState:', document.readyState ); // Comment out
 
 // Function to inject the bridge script
 function injectBridge( target = document ) {
@@ -31,7 +31,7 @@ function injectIntoIframes() {
 		} catch ( e ) {
 
 			// Ignore cross-origin iframe errors
-			console.log( 'DevTools: Could not inject into iframe:', e );
+			// console.log( 'DevTools: Could not inject into iframe:', e ); // Comment out
 
 		}
 
@@ -62,7 +62,7 @@ const observer = new MutationObserver( mutations => {
 					} catch ( e ) {
 
 						// Ignore cross-origin iframe errors
-						// console.log( 'DevTools: Could not inject into iframe:', e );
+						// console.log( 'DevTools: Could not inject into iframe:', e ); // Comment out
 
 					}
 
@@ -205,23 +205,36 @@ function handleDevtoolsMessage( message, sender, sendResponse ) {
 
 }
 
-// Add event listeners
-window.addEventListener( 'message', handleMainWindowMessage, false );
-window.addEventListener( 'message', handleIframeMessage, false );
-chrome.runtime.onMessage.addListener( handleDevtoolsMessage );
+// Listener for messages forwarded from the background script (originating from panel)
+function handleBackgroundMessage( message, sender, sendResponse ) {
 
-// Listen for messages from the panel
-chrome.runtime.onMessage.addListener( ( message, sender, sendResponse ) => {
+	// Check if the message is one we need to forward to the bridge
+	// Only forward request-initial-state now
+	if ( message.name === 'request-initial-state' ) {
 
-	if ( message.name === 'visibility' ) {
+		// console.log( 'Content script: Forwarding message to bridge:', message.name );
+		window.postMessage( message, '*' ); // Forward the message as is to the page
 
-		// Forward visibility state to the injected script
-		window.postMessage( {
-			id: 'three-devtools',
-			name: 'panel-visibility', // Use a distinct name
-			value: message.value
-		}, '*' );
+		// Optional: Forward to iframes too, if needed (might cause duplicates if bridge is in iframe)
+		/*
+		const iframes = document.querySelectorAll('iframe');
+		iframes.forEach(iframe => {
+			try {
+				iframe.contentWindow.postMessage(message, '*');
+			} catch (e) {}
+		});
+		*/
 
 	}
+	// Keep channel open? No, this listener is synchronous for now.
+	// return true;
 
-} );
+}
+
+// Add event listeners
+window.addEventListener( 'message', handleMainWindowMessage, false );
+window.addEventListener( 'message', handleIframeMessage, false );
+// chrome.runtime.onMessage.addListener( handleDevtoolsMessage ); // This seems redundant/incorrectly placed in original code
+
+// Use a single listener for messages from the background script
+chrome.runtime.onMessage.addListener( handleBackgroundMessage );

+ 159 - 0
devtools/panel/panel.css

@@ -0,0 +1,159 @@
+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; Let summary handle this */
+	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;
+	padding-left: 16px; /* Align header with items */
+	user-select: none;
+	margin-bottom: 4px; /* Add space below header */
+}
+
+.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;
+}
+
+/* Styles for two-column layout */
+.properties-grid {
+	display: flex;
+	flex-direction: row;
+	gap: 24px; /* Space between columns */
+}
+.properties-column-left,
+.properties-column-right {
+	flex: 1; /* Each column takes equal width */
+}
+/* End styles for two-column layout */
+
+/* Style for clickable renderer summary */
+.renderer-summary {
+	cursor: pointer;
+}
+.renderer-summary:hover {
+	background: #e0e0e0;
+}
+
+/* Hide default details marker when using custom summary */
+details.renderer-container > summary.renderer-summary { /* Target summary */
+	list-style: none; /* Hide default arrow */
+	cursor: pointer; /* Make the summary div look clickable */
+}
+details.renderer-container > summary.renderer-summary::-webkit-details-marker {
+	display: none; /* Hide default arrow in WebKit */
+}
+
+/* Style for the toggle icon */
+.toggle-icon::before {
+	content: '▶'; /* Default: collapsed */
+	display: inline-block;
+	width: 1em;
+	margin-right: 2px;
+	opacity: 0.7;
+}
+details.renderer-container[open] > summary.renderer-summary .toggle-icon::before {
+	content: '▼'; /* Expanded */
+} 

+ 2 - 0
devtools/panel/panel.html

@@ -2,6 +2,8 @@
 <html>
 <head>
 	<meta charset="utf-8">
+	<title>Three.js DevTools</title>
+	<link rel="stylesheet" href="panel.css">
 	<style>
 		body {
 			margin: 0;

+ 175 - 301
devtools/panel/panel.js

@@ -6,7 +6,7 @@ const state = {
 	objects: new Map()
 };
 
-console.log('Panel script loaded');
+// console.log('Panel script loaded');
 
 // Create a connection to the background page
 const backgroundPageConnection = chrome.runtime.connect({
@@ -19,32 +19,16 @@ backgroundPageConnection.postMessage({
 	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
-	} );
+// Request the initial state from the bridge script
+backgroundPageConnection.postMessage({
+	name: 'request-initial-state',
+	tabId: chrome.devtools.inspectedWindow.tabId // Include tabId for routing
+});
 
-} );
+// console.log('Connected to background page with tab ID:', chrome.devtools.inspectedWindow.tabId);
 
-// Initial visibility state
-chrome.tabs.sendMessage( chrome.devtools.inspectedWindow.tabId, {
-	name: 'visibility',
-	value: isVisible
-} );
+// Store renderer collapse states
+const rendererCollapsedState = new Map();
 
 // Clear state when panel is reloaded
 function clearState() {
@@ -60,14 +44,14 @@ function clearState() {
 
 // Listen for messages from the background page
 backgroundPageConnection.onMessage.addListener(function (message) {
-	console.log('Panel received message:', message);
+	// console.log('Panel received message:', message);
 	if (message.id === 'three-devtools') {
 		handleThreeEvent(message);
 	}
 });
 
 function handleThreeEvent(message) {
-	console.log('Handling event:', message.type);
+	// console.log('Handling event:', message.type);
 	switch (message.type) {
 		case 'register':
 			state.revision = message.detail.revision;
@@ -76,7 +60,7 @@ function handleThreeEvent(message) {
 		
 		case 'observe':
 			const detail = message.detail;
-			console.log('Observed object:', detail);
+			// console.log('Observed object:', detail);
 			
 			// Only store each unique object once
 			if (!state.objects.has(detail.uuid)) {
@@ -96,16 +80,45 @@ function handleThreeEvent(message) {
 		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()
-				});
+				// console.log('Received renderer update:', { uuid: update.uuid, hasProperties: !!update.properties });
 				const renderer = state.renderers.get(update.uuid);
 				if (renderer) {
+					// Always update the internal state
 					renderer.properties = update.properties;
-					updateRendererProperties(renderer);
+
+					// Check if the details section is currently open before updating DOM
+					const summaryElement = document.querySelector(`.renderer-summary[data-uuid="${renderer.uuid}"]`);
+					// Find the parent <details> element
+					const detailsElement = summaryElement ? summaryElement.closest('details.renderer-container') : null;
+
+					if (detailsElement && detailsElement.tagName === 'DETAILS') {
+						// Update the summary line text content (size, calls, tris) within the summary element
+						if (summaryElement) {
+							const iconSpan = summaryElement.querySelector('.icon'); // Keep existing icon span for toggle
+							const typeSpan = summaryElement.querySelector('.type');
+							const labelSpan = summaryElement.querySelector('.label');
+							if (iconSpan && labelSpan && typeSpan && renderer.properties) {
+								const props = renderer.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`);
+								}
+								const displayName = `WebGLRenderer <span class="object-details">${details.join(' ・ ')}</span>`;
+								labelSpan.innerHTML = displayName;
+							}
+						}
+
+						// Update properties list only if details are open
+						if (detailsElement.open) {
+							const propsContainer = detailsElement.querySelector('.properties-list');
+							if (propsContainer) {
+								updateRendererProperties(renderer, propsContainer);
+							}
+						}
+					}
+				} else {
+					// console.warn('Renderer update received for unknown UUID:', update.uuid);
 				}
 			}
 			break;
@@ -118,76 +131,42 @@ function handleThreeEvent(message) {
 }
 
 // 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;
-
+function updateRendererProperties(renderer, propsContainer) {
 	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
+	// Clear existing properties from the specific container
 	propsContainer.innerHTML = '';
 
-	// Create collapsible sections
+	// Create the two-column grid container
+	const gridContainer = document.createElement('div');
+	gridContainer.className = 'properties-grid';
+
+	const leftColumn = document.createElement('div');
+	leftColumn.className = 'properties-column-left';
+
+	const rightColumn = document.createElement('div');
+	rightColumn.className = 'properties-column-right';
+
+	// Function to create sections (no longer collapsible)
 	function createSection(title, properties) {
-		const section = document.createElement('details');
+		const section = document.createElement('div'); // Use div
 		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');
+		const header = document.createElement('div'); // Use div for header
 		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);
-			}
+			// Always create the element, use '-' for undefined values
+			const displayValue = (value === undefined || value === null) ? '-' : value;
+			const propElem = document.createElement('div');
+			propElem.className = 'property-item';
+			propElem.innerHTML = `
+				<span class="property-name">${name}:</span>
+				<span class="property-value">${displayValue}</span>
+			`;
+			section.appendChild(propElem);
 		});
 
 		return section;
@@ -210,46 +189,35 @@ function updateRendererProperties(renderer) {
 		['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));
-		}
+	leftColumn.appendChild(createSection('Properties', basicProps));
+
+	// Define stats arrays outside the if block, using optional chaining and defaults
+	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 ?? '-']
+	];
 
-		// 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));
-	}
+	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 ?? '-']
+	];
+
+	// Always append stats sections
+	rightColumn.appendChild(createSection('Render Stats', renderStats));
+	rightColumn.appendChild(createSection('Memory', memoryStats));
+
+	// Append columns to the grid container, and grid to the main props container
+	gridContainer.appendChild(leftColumn);
+	gridContainer.appendChild(rightColumn);
+	propsContainer.appendChild(gridContainer);
 }
 
 // Function to get an object icon based on its type
@@ -265,57 +233,90 @@ function getObjectIcon(obj) {
 
 // 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`);
+	// Handle Renderer Specifics
+	if (obj.isRenderer) {
+		// Create <details> element as the main container
+		const detailsElement = document.createElement('details');
+		detailsElement.className = 'renderer-container';
+		detailsElement.setAttribute('data-uuid', obj.uuid);
+		// Set initial state (default collapsed = true)
+		detailsElement.open = !(rendererCollapsedState.get(obj.uuid) ?? true);
+		// Add toggle listener to save state
+		detailsElement.addEventListener('toggle', () => {
+			rendererCollapsedState.set(obj.uuid, !detailsElement.open);
+		});
+
+		// Create the summary element (clickable header) - THIS IS THE FIRST CHILD
+		const summaryElem = document.createElement('summary'); // USE <summary> tag
+		summaryElem.className = 'tree-item renderer-summary'; // Acts as summary
+		summaryElem.style.paddingLeft = `${level * 20}px`;
+		
+		// Update display name in the summary line
+		if (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>`;
 		}
-		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));
+		// Use toggle icon instead of paint icon
+		summaryElem.innerHTML = `<span class="icon toggle-icon"></span> 
+			<span class="label">${displayName}</span>
+			<span class="type">${obj.type}</span>`;
+		detailsElement.appendChild(summaryElem); // Append summary div FIRST
+
+		// Create the container for properties inside <details> - THIS IS SECOND CHILD
+		const propsContainer = document.createElement('div');
+		propsContainer.className = 'properties-list';
+		propsContainer.style.paddingLeft = summaryElem.style.paddingLeft.replace('px', '') + 24 + 'px';
+		detailsElement.appendChild(propsContainer);
+
+		container.appendChild(detailsElement); // Append details to the main container
+
+		// Call updateRendererProperties to populate the container
+		if (obj.properties) {
+			updateRendererProperties(obj, propsContainer);
+		}
+	} else {
+		// Default rendering for other object types
+		const elem = document.createElement('div');
+		elem.className = 'tree-item';
+		elem.style.paddingLeft = `${level * 20}px`;
+		elem.setAttribute('data-uuid', obj.uuid);
+		
+		let labelContent = `<span class="icon">${icon}</span>
+			<span class="label">${displayName}</span>
+			<span class="type">${obj.type}</span>`;
+
+		if (obj.isScene) {
+			// Add object count for scenes
+			let objectCount = 0;
+			function countObjects(uuid) {
+				const object = state.objects.get(uuid);
+				if (object) {
+					objectCount++; // Increment count for the object itself
+					if (object.children) {
+						object.children.forEach(childId => countObjects(childId));
+					}
 				}
 			}
+			countObjects(obj.uuid);
+			displayName = `${obj.name || obj.type} <span class="object-details">${objectCount} objects</span>`;
+			labelContent = `<span class="icon">${icon}</span>
+				<span class="label">${displayName}</span>
+				<span class="type">${obj.type}</span>`;
 		}
-		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);
+		elem.innerHTML = labelContent;
+		container.appendChild(elem);
 	}
 
-	// Handle children
-	if (obj.children && obj.children.length > 0) {
+	// Handle children (excluding children of renderers, as properties are shown in details)
+	if (!obj.isRenderer && obj.children && obj.children.length > 0) {
 		// Create a container for children
 		const childContainer = document.createElement('div');
 		childContainer.className = 'children';
@@ -396,133 +397,6 @@ function updateUI() {
 	}
 }
 
-// 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();

粤ICP备19079148号