panel.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. // Store the state of our inspector
  2. const state = {
  3. revision: null,
  4. scenes: new Map(),
  5. renderers: new Map(),
  6. objects: new Map()
  7. };
  8. // console.log('Panel script loaded');
  9. // Create a connection to the background page
  10. const backgroundPageConnection = chrome.runtime.connect({
  11. name: "three-devtools"
  12. });
  13. // Initialize the connection with the inspected tab ID
  14. backgroundPageConnection.postMessage({
  15. name: 'init',
  16. tabId: chrome.devtools.inspectedWindow.tabId
  17. });
  18. // Request the initial state from the bridge script
  19. backgroundPageConnection.postMessage({
  20. name: 'request-initial-state',
  21. tabId: chrome.devtools.inspectedWindow.tabId // Include tabId for routing
  22. });
  23. // console.log('Connected to background page with tab ID:', chrome.devtools.inspectedWindow.tabId);
  24. // Store renderer collapse states
  25. const rendererCollapsedState = new Map();
  26. // Clear state when panel is reloaded
  27. function clearState() {
  28. state.revision = null;
  29. state.scenes.clear();
  30. state.renderers.clear();
  31. state.objects.clear();
  32. const container = document.getElementById('scene-tree');
  33. if (container) {
  34. container.innerHTML = '';
  35. }
  36. }
  37. // Listen for messages from the background page
  38. backgroundPageConnection.onMessage.addListener(function (message) {
  39. // console.log('Panel received message:', message);
  40. if (message.id === 'three-devtools') {
  41. handleThreeEvent(message);
  42. }
  43. });
  44. function handleThreeEvent(message) {
  45. // console.log('Handling event:', message.type);
  46. switch (message.type) {
  47. case 'register':
  48. state.revision = message.detail.revision;
  49. updateUI();
  50. break;
  51. // Handle individual renderer observation
  52. case 'renderer':
  53. const detail = message.detail;
  54. // console.log('Observed object:', detail);
  55. // Only store each unique object once
  56. if (!state.objects.has(detail.uuid)) {
  57. state.objects.set(detail.uuid, detail);
  58. if (detail.isRenderer) {
  59. state.renderers.set(detail.uuid, detail);
  60. }
  61. else if (detail.isScene) {
  62. state.scenes.set(detail.uuid, detail);
  63. }
  64. updateUI();
  65. }
  66. break;
  67. // Handle a batch of objects for a specific scene
  68. case 'scene':
  69. const { sceneUuid, objects: batchObjects } = message.detail;
  70. console.log('Panel: Received scene batch for', sceneUuid, 'with', batchObjects.length, 'objects');
  71. // Clear existing objects belonging to this scene (or previously known descendants)
  72. // This is a simplified removal, assuming objects don't move between scenes
  73. const objectsToRemove = [];
  74. state.objects.forEach((obj, uuid) => {
  75. if (!obj.isRenderer && obj.uuid !== sceneUuid) { // Keep renderers and the scene root itself initially
  76. // Basic check: remove if parent was the scene OR if it's a known descendant (heuristic)
  77. // A more robust approach might involve storing/checking full ancestor paths
  78. if (obj.parent === sceneUuid || state.scenes.has(obj.parent)) {
  79. objectsToRemove.push(uuid);
  80. }
  81. }
  82. });
  83. objectsToRemove.forEach(uuid => {
  84. state.objects.delete(uuid);
  85. // Also remove from scenes/renderers maps if necessary, although unlikely for non-roots
  86. state.scenes.delete(uuid);
  87. // state.renderers.delete(uuid); // Renderers shouldn't be removed here
  88. });
  89. // Process the new batch
  90. batchObjects.forEach(objData => {
  91. state.objects.set(objData.uuid, objData);
  92. if (objData.isScene) {
  93. state.scenes.set(objData.uuid, objData); // Ensure scene is in the scenes map
  94. }
  95. // Renderers are handled by separate 'renderer' events
  96. });
  97. // Update UI once after processing the entire batch
  98. updateUI();
  99. break;
  100. case 'update':
  101. const update = message.detail;
  102. if (update.type === 'WebGLRenderer') {
  103. // console.log('Received renderer update:', { uuid: update.uuid, hasProperties: !!update.properties });
  104. const renderer = state.renderers.get(update.uuid);
  105. if (renderer) {
  106. // Always update the internal state
  107. renderer.properties = update.properties;
  108. // Check if the details section is currently open before updating DOM
  109. const summaryElement = document.querySelector(`.renderer-summary[data-uuid="${renderer.uuid}"]`);
  110. // Find the parent <details> element
  111. const detailsElement = summaryElement ? summaryElement.closest('details.renderer-container') : null;
  112. if (detailsElement && detailsElement.tagName === 'DETAILS') {
  113. // Update the summary line text content (size, calls, tris) within the summary element
  114. if (summaryElement) {
  115. const iconSpan = summaryElement.querySelector('.icon'); // Keep existing icon span for toggle
  116. const typeSpan = summaryElement.querySelector('.type');
  117. const labelSpan = summaryElement.querySelector('.label');
  118. if (iconSpan && labelSpan && typeSpan && renderer.properties) {
  119. const props = renderer.properties;
  120. const details = [`${props.width}x${props.height}`];
  121. if (props.info) {
  122. details.push(`${props.info.render.calls} calls`);
  123. details.push(`${props.info.render.triangles.toLocaleString()} tris`);
  124. }
  125. const displayName = `WebGLRenderer <span class="object-details">${details.join(' ・ ')}</span>`;
  126. labelSpan.innerHTML = displayName;
  127. }
  128. }
  129. // Update properties list only if details are open
  130. if (detailsElement.open) {
  131. const propsContainer = detailsElement.querySelector('.properties-list');
  132. if (propsContainer) {
  133. updateRendererProperties(renderer, propsContainer);
  134. }
  135. }
  136. }
  137. } else {
  138. // console.warn('Renderer update received for unknown UUID:', update.uuid);
  139. }
  140. }
  141. break;
  142. case 'committed':
  143. // Page was reloaded, clear state
  144. clearState();
  145. break;
  146. }
  147. }
  148. // Function to update just the renderer properties in the UI
  149. function updateRendererProperties(renderer, propsContainer) {
  150. const props = renderer.properties;
  151. // Clear existing properties from the specific container
  152. propsContainer.innerHTML = '';
  153. // Create the two-column grid container
  154. const gridContainer = document.createElement('div');
  155. gridContainer.className = 'properties-grid';
  156. const leftColumn = document.createElement('div');
  157. leftColumn.className = 'properties-column-left';
  158. const rightColumn = document.createElement('div');
  159. rightColumn.className = 'properties-column-right';
  160. // Function to create sections (no longer collapsible)
  161. function createSection(title, properties) {
  162. const section = document.createElement('div'); // Use div
  163. section.className = 'properties-section';
  164. const header = document.createElement('div'); // Use div for header
  165. header.className = 'properties-header';
  166. header.textContent = title;
  167. section.appendChild(header);
  168. properties.forEach(([name, value]) => {
  169. // Always create the element, use '-' for undefined values
  170. const displayValue = (value === undefined || value === null) ? '-' : value;
  171. const propElem = document.createElement('div');
  172. propElem.className = 'property-item';
  173. propElem.innerHTML = `
  174. <span class="property-name">${name}:</span>
  175. <span class="property-value">${displayValue}</span>
  176. `;
  177. section.appendChild(propElem);
  178. });
  179. return section;
  180. }
  181. // Basic properties section
  182. const basicProps = [
  183. ['Size', `${props.width}x${props.height}`],
  184. ['Drawing Buffer', `${props.drawingBufferWidth}x${props.drawingBufferHeight}`],
  185. ['Alpha', props.alpha],
  186. ['Antialias', props.antialias],
  187. ['Output Color Space', props.outputColorSpace],
  188. ['Tone Mapping', props.toneMapping],
  189. ['Tone Mapping Exposure', props.toneMappingExposure],
  190. ['Shadows', props.shadowMapEnabled ? `enabled (${props.shadowMapType})` : 'disabled'],
  191. ['Auto Clear', props.autoClear],
  192. ['Auto Clear Color', props.autoClearColor],
  193. ['Auto Clear Depth', props.autoClearDepth],
  194. ['Auto Clear Stencil', props.autoClearStencil],
  195. ['Local Clipping', props.localClippingEnabled],
  196. ['Physically Correct Lights', props.physicallyCorrectLights]
  197. ];
  198. leftColumn.appendChild(createSection('Properties', basicProps));
  199. // Define stats arrays outside the if block, using optional chaining and defaults
  200. const renderStats = [
  201. ['Frame', props.info?.render?.frame ?? '-'],
  202. ['Draw Calls', props.info?.render?.calls ?? '-'],
  203. ['Triangles', props.info?.render?.triangles?.toLocaleString() ?? '-'],
  204. ['Points', props.info?.render?.points ?? '-'],
  205. ['Lines', props.info?.render?.lines ?? '-'],
  206. ['Sprites', props.info?.render?.sprites ?? '-'],
  207. ['Geometries', props.info?.render?.geometries ?? '-']
  208. ];
  209. const memoryStats = [
  210. ['Geometries', props.info?.memory?.geometries ?? '-'],
  211. ['Textures', props.info?.memory?.textures ?? '-'],
  212. ['Shader Programs', props.info?.memory?.programs ?? '-'],
  213. ['Render Lists', props.info?.memory?.renderLists ?? '-'],
  214. ['Render Targets', props.info?.memory?.renderTargets ?? '-']
  215. ];
  216. // Always append stats sections
  217. rightColumn.appendChild(createSection('Render Stats', renderStats));
  218. rightColumn.appendChild(createSection('Memory', memoryStats));
  219. // Append columns to the grid container, and grid to the main props container
  220. gridContainer.appendChild(leftColumn);
  221. gridContainer.appendChild(rightColumn);
  222. propsContainer.appendChild(gridContainer);
  223. }
  224. // Function to get an object icon based on its type
  225. function getObjectIcon(obj) {
  226. if (obj.isScene) return '🌍';
  227. if (obj.isCamera) return '📷';
  228. if (obj.isLight) return '💡';
  229. if (obj.isMesh) return '🔷';
  230. if (obj.type === 'Group') return '📁';
  231. return '📦';
  232. }
  233. // Function to render an object and its children
  234. function renderObject(obj, container, level = 0) {
  235. const icon = getObjectIcon(obj);
  236. let displayName = obj.name || obj.type;
  237. // Handle Renderer Specifics
  238. if (obj.isRenderer) {
  239. // Create <details> element as the main container
  240. const detailsElement = document.createElement('details');
  241. detailsElement.className = 'renderer-container';
  242. detailsElement.setAttribute('data-uuid', obj.uuid);
  243. // Set initial state (default collapsed = true)
  244. detailsElement.open = !(rendererCollapsedState.get(obj.uuid) ?? true);
  245. // Add toggle listener to save state
  246. detailsElement.addEventListener('toggle', () => {
  247. rendererCollapsedState.set(obj.uuid, !detailsElement.open);
  248. });
  249. // Create the summary element (clickable header) - THIS IS THE FIRST CHILD
  250. const summaryElem = document.createElement('summary'); // USE <summary> tag
  251. summaryElem.className = 'tree-item renderer-summary'; // Acts as summary
  252. summaryElem.style.paddingLeft = `${level * 20}px`;
  253. // Update display name in the summary line
  254. if (obj.properties) {
  255. const props = obj.properties;
  256. const details = [`${props.width}x${props.height}`];
  257. if (props.info) {
  258. details.push(`${props.info.render.calls} calls`);
  259. details.push(`${props.info.render.triangles.toLocaleString()} tris`);
  260. }
  261. displayName = `WebGLRenderer <span class="object-details">${details.join(' ・ ')}</span>`;
  262. }
  263. // Use toggle icon instead of paint icon
  264. summaryElem.innerHTML = `<span class="icon toggle-icon"></span>
  265. <span class="label">${displayName}</span>
  266. <span class="type">${obj.type}</span>`;
  267. detailsElement.appendChild(summaryElem); // Append summary div FIRST
  268. // Create the container for properties inside <details> - THIS IS SECOND CHILD
  269. const propsContainer = document.createElement('div');
  270. propsContainer.className = 'properties-list';
  271. propsContainer.style.paddingLeft = summaryElem.style.paddingLeft.replace('px', '') + 24 + 'px';
  272. detailsElement.appendChild(propsContainer);
  273. container.appendChild(detailsElement); // Append details to the main container
  274. // Call updateRendererProperties to populate the container
  275. if (obj.properties) {
  276. updateRendererProperties(obj, propsContainer);
  277. }
  278. } else {
  279. // Default rendering for other object types
  280. const elem = document.createElement('div');
  281. elem.className = 'tree-item';
  282. elem.style.paddingLeft = `${level * 20}px`;
  283. elem.setAttribute('data-uuid', obj.uuid);
  284. let labelContent = `<span class="icon">${icon}</span>
  285. <span class="label">${displayName}</span>
  286. <span class="type">${obj.type}</span>`;
  287. if (obj.isScene) {
  288. // Add object count for scenes
  289. let objectCount = 0;
  290. function countObjects(uuid) {
  291. const object = state.objects.get(uuid);
  292. if (object) {
  293. objectCount++; // Increment count for the object itself
  294. if (object.children) {
  295. object.children.forEach(childId => countObjects(childId));
  296. }
  297. }
  298. }
  299. countObjects(obj.uuid);
  300. displayName = `${obj.name || obj.type} <span class="object-details">${objectCount} objects</span>`;
  301. labelContent = `<span class="icon">${icon}</span>
  302. <span class="label">${displayName}</span>
  303. <span class="type">${obj.type}</span>`;
  304. }
  305. elem.innerHTML = labelContent;
  306. container.appendChild(elem);
  307. }
  308. // Handle children (excluding children of renderers, as properties are shown in details)
  309. if (!obj.isRenderer && obj.children && obj.children.length > 0) {
  310. // Create a container for children
  311. const childContainer = document.createElement('div');
  312. childContainer.className = 'children';
  313. container.appendChild(childContainer);
  314. // Get all children and sort them by type for better organization
  315. const children = obj.children
  316. .map(childId => state.objects.get(childId))
  317. .filter(child => child !== undefined)
  318. .sort((a, b) => {
  319. // Sort order: Cameras, Lights, Groups, Meshes, Others
  320. const typeOrder = {
  321. isCamera: 1,
  322. isLight: 2,
  323. isGroup: 3,
  324. isMesh: 4
  325. };
  326. const aOrder = Object.entries(typeOrder).find(([key]) => a[key])?.['1'] || 5;
  327. const bOrder = Object.entries(typeOrder).find(([key]) => b[key])?.['1'] || 5;
  328. return aOrder - bOrder;
  329. });
  330. // Render each child
  331. children.forEach(child => {
  332. renderObject(child, childContainer, level + 1);
  333. });
  334. }
  335. }
  336. // Function to update the UI
  337. function updateUI() {
  338. const container = document.getElementById('scene-tree');
  339. if (!container) {
  340. console.error('Could not find scene-tree container!');
  341. return;
  342. }
  343. container.innerHTML = '';
  344. // Add version info if available
  345. if (state.revision) {
  346. const versionInfo = document.createElement('div');
  347. versionInfo.className = 'info-item';
  348. versionInfo.textContent = `Three.js r${state.revision}`;
  349. container.appendChild(versionInfo);
  350. }
  351. // Add renderers section
  352. if (state.renderers.size > 0) {
  353. const renderersSection = document.createElement('div');
  354. renderersSection.className = 'section';
  355. renderersSection.innerHTML = '<h3>Renderers</h3>';
  356. state.renderers.forEach(renderer => {
  357. renderObject(renderer, renderersSection);
  358. });
  359. container.appendChild(renderersSection);
  360. }
  361. // Add scenes section
  362. if (state.scenes.size > 0) {
  363. const scenesSection = document.createElement('div');
  364. scenesSection.className = 'section';
  365. scenesSection.innerHTML = '<h3>Scenes</h3>';
  366. state.scenes.forEach(scene => {
  367. renderObject(scene, scenesSection);
  368. });
  369. container.appendChild(scenesSection);
  370. }
  371. }
  372. // Initial UI update
  373. clearState();
  374. updateUI();
粤ICP备19079148号