Skip to content

Commit 2a4e27e

Browse files
mrdoobgithub-advanced-security[bot]cesmoakDarkenssesclaude
authored
Added new DevTools (#30870)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Chris Smoak <chris.smoak+@gmail.com> Co-authored-by: Jasiel Guillén <darkensses@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c73eff8 commit 2a4e27e

15 files changed

Lines changed: 2188 additions & 0 deletions

devtools/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Three.js DevTools Extension
2+
3+
This Chrome DevTools extension provides debugging capabilities for Three.js applications. It allows you to inspect scenes, objects, materials, and renderers.
4+
5+
## Installation
6+
7+
1. **Development Mode**:
8+
- Open Chrome and navigate to `chrome://extensions/`
9+
- Enable "Developer mode" (toggle in the top-right corner)
10+
- Click "Load unpacked" and select the `devtools` directory
11+
- The extension will now be available in Chrome DevTools when inspecting pages that use Three.js
12+
13+
2. **Usage**:
14+
- Open Chrome DevTools on a page using Three.js (F12 or Right-click > Inspect)
15+
- Click on the "Three.js" tab in DevTools
16+
- The panel will automatically detect and display Three.js scenes and renderers found on the page.
17+
18+
## Code Flow Overview
19+
20+
### Extension Architecture
21+
22+
The extension follows a standard Chrome DevTools extension architecture:
23+
24+
1. **Background Script** (`background.js`): Manages the extension lifecycle and communication ports between the panel and content script.
25+
2. **DevTools Script** (`devtools.js`): Creates the panel when the DevTools window opens.
26+
3. **Panel UI** (`panel/panel.html`, `panel/panel.js`, `panel/panel.css`): The DevTools panel interface that displays the data.
27+
4. **Content Script** (`content-script.js`): Injected into the web page. Relays messages between the background script and the bridge script.
28+
5. **Bridge Script** (`bridge.js`): Injected into the page's main world via the manifest. Directly interacts with the Three.js instance, detects objects, gathers data, and communicates back via the content script.
29+
30+
### Initialization Flow
31+
32+
1. When a page loads, Chrome injects `bridge.js` into the page's main world (including iframes).
33+
2. `bridge.js` creates the `window.__THREE_DEVTOOLS__` global object.
34+
3. When the DevTools panel is opened, `panel.js` connects to `background.js` (`init`) and immediately requests the current state (`request-state`).
35+
4. `background.js` relays the state request to `content-script.js`, which posts it to `bridge.js`.
36+
5. `bridge.js` responds by sending back observed renderer data (`renderer` message) and batched scene data (`scene` message).
37+
6. Three.js detects `window.__THREE_DEVTOOLS__` and sends registration/observation events to the bridge script as objects are created or the library initializes.
38+
39+
### Bridge Operation (`bridge.js`)
40+
41+
The bridge acts as the communication layer between the Three.js instance on the page and the DevTools panel:
42+
43+
1. **Event Management**: Creates a custom event target (`DevToolsEventTarget`) to manage communication readiness and backlog events before the panel connects.
44+
2. **Object Tracking**:
45+
- `getObjectData()`: Extracts essential data (UUID, type, name, parent, children, etc.) from Three.js objects.
46+
- Maintains a local map (`devTools.objects`) of all observed objects.
47+
48+
3. **Initial Observation & Batching**:
49+
- When Three.js sends an `observe` event (via `window.__THREE_DEVTOOLS__.dispatchEvent`):
50+
- If it's a renderer, its data is collected and sent immediately via a `'renderer'` message.
51+
- If it's a scene, the bridge traverses the entire scene graph, collects data for the scene and all descendants, stores them locally, and sends them to the panel in a single `'scene'` batch message.
52+
53+
4. **State Request Handling**:
54+
- When the panel sends `request-state` (on load/reload), the bridge iterates its known objects and sends back the current renderer data (`'renderer'`) and scene data (`'scene'` batch).
55+
56+
5. **Message Handling**:
57+
- Listens for messages from the panel (relayed via content script) like `request-state`.
58+
59+
### Panel Interface (`panel/`)
60+
61+
The panel UI provides the visual representation of the Three.js objects:
62+
63+
1. **Tree View**: Displays hierarchical representation of scenes and objects.
64+
2. **Renderer Details**: Shows properties and statistics for renderers in a collapsible section.
65+
66+
## Key Features
67+
68+
- **Scene Hierarchy Visualization**: Browse the complete scene graph.
69+
- **Object Inspection**: View basic object properties (type, name).
70+
- **Renderer Details**: View properties, render stats, and memory usage for `WebGLRenderer` instances.
71+
72+
## Communication Flow
73+
74+
1. **Panel ↔ Background ↔ Content Script**: Standard extension messaging for panel initialization and state requests (`init`, `request-state`).
75+
2. **Three.js → Bridge**: Three.js detects `window.__THREE_DEVTOOLS__` and uses its `dispatchEvent` method (sending `'register'`, `'observe'`).
76+
3. **Bridge → Content Script**: Bridge uses `window.postMessage` to send data (`'register'`, `'renderer'`, `'scene'`, `'update'`) to the content script.
77+
4. **Content Script → Background**: Content script uses `chrome.runtime.sendMessage` to relay messages from the bridge to the background.
78+
5. **Background → Panel**: Background script uses the established port connection (`port.postMessage`) to send data to the panel.
79+
80+
## Key Components
81+
82+
- **DevToolsEventTarget**: Custom event system with backlogging for async loading.
83+
- **Object Observation & Batching**: Efficiently tracks and sends scene graph data.
84+
- **Renderer Property Display**: Shows detailed statistics for renderers.
85+
86+
## Integration with Three.js
87+
88+
The extension relies on Three.js having built-in support for DevTools. When Three.js detects the presence of `window.__THREE_DEVTOOLS__`, it interacts with it, primarily by dispatching events.
89+
90+
The bridge script listens for these events, organizes the data, and provides it to the DevTools panel.
91+
92+
## Development
93+
94+
To modify the extension:
95+
96+
1. Edit the relevant files in the `devtools` directory.
97+
2. Go to `chrome://extensions/`, find the unpacked extension, and click the reload icon.
98+
3. Close and reopen DevTools on the inspected page to see your changes.

devtools/background.js

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/* global chrome */
2+
3+
importScripts( 'constants.js' );
4+
5+
// Map tab IDs to connections
6+
const connections = new Map();
7+
8+
// Handle extension icon clicks in the toolbar
9+
chrome.action.onClicked.addListener( ( tab ) => {
10+
11+
// Send scroll-to-canvas message to the content script (no UUID = scroll to first canvas)
12+
chrome.tabs.sendMessage( tab.id, {
13+
name: MESSAGE_SCROLL_TO_CANVAS,
14+
tabId: tab.id
15+
} ).catch( () => {
16+
17+
// Ignore error - tab might not have the content script injected
18+
console.log( 'Could not send scroll-to-canvas message to tab', tab.id );
19+
20+
} );
21+
22+
} );
23+
24+
// Listen for connections from the devtools panel
25+
chrome.runtime.onConnect.addListener( port => {
26+
27+
let tabId;
28+
29+
// Messages that should be forwarded to content script
30+
const forwardableMessages = new Set( [
31+
MESSAGE_REQUEST_STATE,
32+
MESSAGE_REQUEST_OBJECT_DETAILS,
33+
MESSAGE_SCROLL_TO_CANVAS,
34+
MESSAGE_HIGHLIGHT_OBJECT,
35+
MESSAGE_UNHIGHLIGHT_OBJECT
36+
] );
37+
38+
// Listen for messages from the devtools panel
39+
port.onMessage.addListener( message => {
40+
41+
if ( message.name === MESSAGE_INIT ) {
42+
43+
tabId = message.tabId;
44+
connections.set( tabId, port );
45+
46+
} else if ( forwardableMessages.has( message.name ) && tabId ) {
47+
48+
chrome.tabs.sendMessage( tabId, message );
49+
50+
} else if ( tabId === undefined ) {
51+
52+
console.warn( 'Background: Message received from panel before init:', message );
53+
54+
}
55+
56+
} );
57+
58+
// Clean up when devtools is closed
59+
port.onDisconnect.addListener( () => {
60+
61+
if ( tabId ) {
62+
63+
connections.delete( tabId );
64+
65+
}
66+
67+
} );
68+
69+
} );
70+
71+
// Listen for messages from the content script
72+
chrome.runtime.onMessage.addListener( ( message, sender, sendResponse ) => {
73+
74+
if ( message.scheme ) {
75+
76+
chrome.action.setIcon( {
77+
path: {
78+
128: `icons/128-${message.scheme}.png`
79+
}
80+
} );
81+
82+
}
83+
84+
if ( sender.tab ) {
85+
86+
const tabId = sender.tab.id;
87+
88+
// If three.js is detected, show a badge
89+
if ( message.name === MESSAGE_REGISTER && message.detail && message.detail.revision ) {
90+
91+
const revision = String( message.detail.revision );
92+
const number = revision.replace( /\D+$/, '' );
93+
const isDev = revision.includes( 'dev' );
94+
95+
chrome.action.setBadgeText( { tabId: tabId, text: number } ).catch( () => {
96+
97+
// Ignore error - tab might have been closed
98+
99+
} );
100+
chrome.action.setBadgeTextColor( { tabId: tabId, color: '#ffffff' } ).catch( () => {
101+
102+
// Ignore error - tab might have been closed
103+
104+
} );
105+
chrome.action.setBadgeBackgroundColor( { tabId: tabId, color: isDev ? '#ff0098' : '#049ef4' } ).catch( () => {
106+
107+
// Ignore error - tab might have been closed
108+
109+
} );
110+
111+
}
112+
113+
const port = connections.get( tabId );
114+
if ( port ) {
115+
116+
// Forward the message to the devtools panel
117+
try {
118+
119+
port.postMessage( message );
120+
// Send immediate response to avoid "message channel closed" error
121+
sendResponse( { received: true } );
122+
123+
} catch ( e ) {
124+
125+
console.error( 'Error posting message to devtools:', e );
126+
// If the port is broken, clean up the connection
127+
connections.delete( tabId );
128+
129+
}
130+
131+
}
132+
133+
}
134+
135+
return false; // Return false to indicate synchronous handling
136+
137+
} );
138+
139+
// Listen for page navigation events
140+
chrome.webNavigation.onCommitted.addListener( details => {
141+
142+
const { tabId, frameId } = details;
143+
144+
// Clear badge on navigation, only for top-level navigation
145+
if ( frameId === 0 ) {
146+
147+
chrome.action.setBadgeText( { tabId: tabId, text: '' } ).catch( () => {
148+
149+
// Ignore error - tab might have been closed
150+
151+
} );
152+
153+
}
154+
155+
const port = connections.get( tabId );
156+
157+
if ( port ) {
158+
159+
port.postMessage( {
160+
id: MESSAGE_ID,
161+
name: MESSAGE_COMMITTED,
162+
frameId: frameId
163+
} );
164+
165+
}
166+
167+
} );
168+
169+
// Clear badge when a tab is closed
170+
chrome.tabs.onRemoved.addListener( ( tabId ) => {
171+
172+
chrome.action.setBadgeText( { tabId: tabId, text: '' } ).catch( () => {
173+
174+
// Ignore error - tab is already gone
175+
176+
} );
177+
178+
// Clean up connection if it exists for the closed tab
179+
if ( connections.has( tabId ) ) {
180+
181+
connections.delete( tabId );
182+
183+
}
184+
185+
} );

0 commit comments

Comments
 (0)