panel.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. /* global chrome */
  2. // --- Utility Functions ---
  3. function getObjectIcon(obj) {
  4. if (obj.isScene) return '🌍';
  5. if (obj.isCamera) return '📷';
  6. if (obj.isLight) return '💡';
  7. if (obj.isInstancedMesh) return '🔸';
  8. if (obj.isMesh) return '🔷';
  9. if (obj.type === 'Group') return '📁';
  10. return '📦';
  11. }
  12. function createPropertyRow(label, value) {
  13. const row = document.createElement('div');
  14. row.className = 'property-row';
  15. row.style.display = 'flex';
  16. row.style.justifyContent = 'space-between';
  17. row.style.marginBottom = '2px';
  18. const labelSpan = document.createElement('span');
  19. labelSpan.className = 'property-label';
  20. labelSpan.textContent = `${label}`;
  21. labelSpan.style.marginRight = '10px';
  22. labelSpan.style.whiteSpace = 'nowrap';
  23. const valueSpan = document.createElement('span');
  24. valueSpan.className = 'property-value';
  25. const displayValue = (value === undefined || value === null)
  26. ? '–'
  27. : (typeof value === 'number' ? value.toLocaleString() : value);
  28. valueSpan.textContent = displayValue;
  29. valueSpan.style.textAlign = 'right';
  30. row.appendChild(labelSpan);
  31. row.appendChild(valueSpan);
  32. return row;
  33. }
  34. function createVectorRow(label, vector) {
  35. const row = document.createElement('div');
  36. row.className = 'property-row';
  37. row.style.marginBottom = '2px';
  38. // Pad label to ensure consistent alignment
  39. const paddedLabel = label.padEnd(16, ' '); // Pad to 16 characters
  40. const content = `${paddedLabel} ${vector.x.toFixed(3)}\t${vector.y.toFixed(3)}\t${vector.z.toFixed(3)}`;
  41. row.textContent = content;
  42. row.style.fontFamily = 'monospace';
  43. row.style.whiteSpace = 'pre';
  44. return row;
  45. }
  46. // --- State ---
  47. const state = {
  48. revision: null,
  49. scenes: new Map(),
  50. renderers: new Map(),
  51. objects: new Map(),
  52. selectedObject: null
  53. };
  54. // Floating details panel
  55. let floatingPanel = null;
  56. let mousePosition = { x: 0, y: 0 };
  57. // Create a connection to the background page
  58. const backgroundPageConnection = chrome.runtime.connect( {
  59. name: 'three-devtools'
  60. } );
  61. // Initialize the connection with the inspected tab ID
  62. backgroundPageConnection.postMessage( {
  63. name: 'init',
  64. tabId: chrome.devtools.inspectedWindow.tabId
  65. } );
  66. // Request the initial state from the bridge script
  67. backgroundPageConnection.postMessage( {
  68. name: 'request-state',
  69. tabId: chrome.devtools.inspectedWindow.tabId
  70. } );
  71. // Function to scroll to canvas element
  72. function scrollToCanvas( rendererUuid ) {
  73. backgroundPageConnection.postMessage( {
  74. name: 'scroll-to-canvas',
  75. uuid: rendererUuid,
  76. tabId: chrome.devtools.inspectedWindow.tabId
  77. } );
  78. }
  79. const intervalId = setInterval( () => {
  80. backgroundPageConnection.postMessage( {
  81. name: 'request-state',
  82. tabId: chrome.devtools.inspectedWindow.tabId
  83. } );
  84. }, 1000 );
  85. backgroundPageConnection.onDisconnect.addListener( () => {
  86. console.log( 'Panel: Connection to background page lost' );
  87. clearInterval( intervalId );
  88. clearState();
  89. } );
  90. // Function to request object details from the bridge
  91. function requestObjectDetails( uuid ) {
  92. backgroundPageConnection.postMessage( {
  93. name: 'request-object-details',
  94. uuid: uuid,
  95. tabId: chrome.devtools.inspectedWindow.tabId
  96. } );
  97. }
  98. // Store renderer collapse states
  99. const rendererCollapsedState = new Map();
  100. // Clear state when panel is reloaded
  101. function clearState() {
  102. state.revision = null;
  103. state.scenes.clear();
  104. state.renderers.clear();
  105. state.objects.clear();
  106. const container = document.getElementById( 'scene-tree' );
  107. if ( container ) {
  108. container.innerHTML = '';
  109. }
  110. // Hide floating panel
  111. if ( floatingPanel ) {
  112. floatingPanel.classList.remove( 'visible' );
  113. }
  114. }
  115. // Listen for messages from the background page
  116. backgroundPageConnection.onMessage.addListener( function ( message ) {
  117. if ( message.id === 'three-devtools' ) {
  118. handleThreeEvent( message );
  119. }
  120. } );
  121. function handleThreeEvent( message ) {
  122. switch ( message.name ) {
  123. case 'register':
  124. state.revision = message.detail.revision;
  125. break;
  126. // Handle individual renderer observation
  127. case 'renderer':
  128. const detail = message.detail;
  129. // Only store each unique object once
  130. if ( ! state.objects.has( detail.uuid ) ) {
  131. state.objects.set( detail.uuid, detail );
  132. state.renderers.set( detail.uuid, detail );
  133. }
  134. // Update or add the renderer in the state map
  135. state.renderers.set( detail.uuid, detail ); // Ensure the latest detail is always stored
  136. // Also update the generic objects map if renderers are stored there too
  137. state.objects.set( detail.uuid, detail );
  138. break;
  139. // Handle object details response
  140. case 'object-details':
  141. state.selectedObject = message.detail;
  142. console.log( 'Panel: Received object details:', message.detail );
  143. showFloatingDetails( message.detail );
  144. break;
  145. // Handle a batch of objects for a specific scene
  146. case 'scene':
  147. const { sceneUuid, objects: batchObjects } = message.detail;
  148. console.log( 'Panel: Received scene batch for', sceneUuid, 'with', batchObjects.length, 'objects' );
  149. // 1. Identify UUIDs in the new batch
  150. const newObjectUuids = new Set( batchObjects.map( obj => obj.uuid ) );
  151. // 2. Identify current object UUIDs associated with this scene that are NOT renderers
  152. const currentSceneObjectUuids = new Set();
  153. state.objects.forEach( ( obj, uuid ) => {
  154. // Use the _sceneUuid property we'll add below, or check if it's the scene root itself
  155. if ( obj._sceneUuid === sceneUuid || uuid === sceneUuid ) {
  156. currentSceneObjectUuids.add( uuid );
  157. }
  158. } );
  159. // 3. Find UUIDs to remove (in current state for this scene, but not in the new batch)
  160. const uuidsToRemove = new Set();
  161. currentSceneObjectUuids.forEach( uuid => {
  162. if ( ! newObjectUuids.has( uuid ) ) {
  163. uuidsToRemove.add( uuid );
  164. }
  165. } );
  166. // 4. Remove stale objects from state
  167. uuidsToRemove.forEach( uuid => {
  168. state.objects.delete( uuid );
  169. // If a scene object itself was somehow removed (unlikely for root), clean up scenes map too
  170. if ( state.scenes.has( uuid ) ) {
  171. state.scenes.delete( uuid );
  172. }
  173. } );
  174. // 5. Process the new batch: Add/Update objects and mark their scene association
  175. batchObjects.forEach( objData => {
  176. // Add a private property to track which scene this object belongs to
  177. objData._sceneUuid = sceneUuid;
  178. state.objects.set( objData.uuid, objData );
  179. // Ensure the scene root is in the scenes map
  180. if ( objData.isScene && objData.uuid === sceneUuid ) {
  181. state.scenes.set( objData.uuid, objData );
  182. }
  183. // Note: Renderers are handled separately by 'renderer' events and shouldn't appear in scene batches.
  184. } );
  185. break;
  186. case 'committed':
  187. // Page was reloaded, clear state
  188. clearState();
  189. break;
  190. }
  191. updateUI();
  192. }
  193. function renderRenderer( obj, container ) {
  194. // Create <details> element as the main container
  195. const detailsElement = document.createElement( 'details' );
  196. detailsElement.className = 'renderer-container';
  197. detailsElement.setAttribute( 'data-uuid', obj.uuid );
  198. // Set initial state
  199. detailsElement.open = rendererCollapsedState.get( obj.uuid ) || false;
  200. // Add toggle listener to save state
  201. detailsElement.addEventListener( 'toggle', () => {
  202. rendererCollapsedState.set( obj.uuid, detailsElement.open );
  203. } );
  204. // Create the summary element (clickable header) - THIS IS THE FIRST CHILD
  205. const summaryElem = document.createElement( 'summary' ); // USE <summary> tag
  206. summaryElem.className = 'tree-item renderer-summary'; // Acts as summary
  207. // Update display name in the summary line
  208. const props = obj.properties;
  209. const details = [ `${props.width}x${props.height}` ];
  210. if ( props.info ) {
  211. details.push( `${props.info.render.calls} draws` );
  212. details.push( `${props.info.render.triangles.toLocaleString()} triangles` );
  213. }
  214. const displayName = `${obj.type} <span class="object-details">${details.join( ' ・ ' )}</span>`;
  215. // Use toggle icon instead of paint icon
  216. const scrollButton = obj.canvasInDOM ?
  217. `<button class="scroll-to-canvas-btn" data-canvas-uuid="${obj.uuid}" title="Scroll to canvas">🙂</button>` :
  218. `<span class="scroll-to-canvas-placeholder" title="Canvas not in DOM">󠀠🫥</span>`;
  219. summaryElem.innerHTML = `<span class="icon toggle-icon"></span>
  220. <span class="label">${displayName}</span>
  221. <span class="type">${obj.type}</span>
  222. ${scrollButton}`;
  223. detailsElement.appendChild( summaryElem );
  224. const propsContainer = document.createElement( 'div' );
  225. propsContainer.className = 'properties-list';
  226. // Adjust padding calculation if needed, ensure it's a number before adding
  227. const summaryPaddingLeft = parseFloat( summaryElem.style.paddingLeft ) || 0;
  228. propsContainer.style.paddingLeft = `${summaryPaddingLeft + 20}px`; // Indent further
  229. propsContainer.innerHTML = ''; // Clear placeholder
  230. if ( obj.properties ) {
  231. const props = obj.properties;
  232. const info = props.info || { render: {}, memory: {} }; // Default empty objects if info is missing
  233. const gridContainer = document.createElement( 'div' );
  234. gridContainer.style.display = 'grid';
  235. gridContainer.style.gridTemplateColumns = 'repeat(auto-fit, minmax(200px, 1fr))'; // Responsive columns
  236. gridContainer.style.gap = '10px 20px'; // Row and column gap
  237. // --- Column 1: Properties ---
  238. const propsCol = document.createElement( 'div' );
  239. propsCol.className = 'properties-column';
  240. const propsTitle = document.createElement( 'h4' );
  241. propsTitle.textContent = 'Properties';
  242. propsCol.appendChild( propsTitle );
  243. propsCol.appendChild( createPropertyRow( 'Size', `${props.width}x${props.height}` ) );
  244. propsCol.appendChild( createPropertyRow( 'Alpha', props.alpha ) );
  245. propsCol.appendChild( createPropertyRow( 'Antialias', props.antialias ) );
  246. propsCol.appendChild( createPropertyRow( 'Output Color Space', props.outputColorSpace ) );
  247. propsCol.appendChild( createPropertyRow( 'Tone Mapping', props.toneMapping ) );
  248. propsCol.appendChild( createPropertyRow( 'Tone Mapping Exposure', props.toneMappingExposure ) );
  249. propsCol.appendChild( createPropertyRow( 'Shadows', props.shadows ? 'enabled' : 'disabled' ) ); // Display string
  250. propsCol.appendChild( createPropertyRow( 'Auto Clear', props.autoClear ) );
  251. propsCol.appendChild( createPropertyRow( 'Auto Clear Color', props.autoClearColor ) );
  252. propsCol.appendChild( createPropertyRow( 'Auto Clear Depth', props.autoClearDepth ) );
  253. propsCol.appendChild( createPropertyRow( 'Auto Clear Stencil', props.autoClearStencil ) );
  254. propsCol.appendChild( createPropertyRow( 'Local Clipping', props.localClipping ) );
  255. propsCol.appendChild( createPropertyRow( 'Physically Correct Lights', props.physicallyCorrectLights ) );
  256. gridContainer.appendChild( propsCol );
  257. // --- Column 2: Render Stats & Memory ---
  258. const statsCol = document.createElement( 'div' );
  259. statsCol.className = 'stats-column';
  260. // Render Stats
  261. const renderTitle = document.createElement( 'h4' );
  262. renderTitle.textContent = 'Render Stats';
  263. statsCol.appendChild( renderTitle );
  264. statsCol.appendChild( createPropertyRow( 'Frame', info.render.frame ) );
  265. statsCol.appendChild( createPropertyRow( 'Draw Calls', info.render.calls ) );
  266. statsCol.appendChild( createPropertyRow( 'Triangles', info.render.triangles ) );
  267. statsCol.appendChild( createPropertyRow( 'Points', info.render.points ) );
  268. statsCol.appendChild( createPropertyRow( 'Lines', info.render.lines ) );
  269. // Memory
  270. const memoryTitle = document.createElement( 'h4' );
  271. memoryTitle.textContent = 'Memory';
  272. memoryTitle.style.marginTop = '10px'; // Add space before Memory section
  273. statsCol.appendChild( memoryTitle );
  274. statsCol.appendChild( createPropertyRow( 'Geometries', info.memory.geometries ) ); // Memory Geometries
  275. statsCol.appendChild( createPropertyRow( 'Textures', info.memory.textures ) );
  276. statsCol.appendChild( createPropertyRow( 'Shader Programs', info.memory.programs ) );
  277. gridContainer.appendChild( statsCol );
  278. propsContainer.appendChild( gridContainer );
  279. } else {
  280. propsContainer.textContent = 'No properties available.';
  281. }
  282. detailsElement.appendChild( propsContainer );
  283. // Add click handler for scroll to canvas button
  284. const scrollBtn = detailsElement.querySelector( '.scroll-to-canvas-btn' );
  285. if ( scrollBtn ) {
  286. scrollBtn.addEventListener( 'click', ( event ) => {
  287. event.preventDefault();
  288. event.stopPropagation();
  289. scrollToCanvas( obj.uuid );
  290. } );
  291. }
  292. container.appendChild( detailsElement ); // Append details to the main container
  293. }
  294. // Function to render an object and its children
  295. function renderObject( obj, container, level = 0 ) {
  296. const icon = getObjectIcon( obj );
  297. let displayName = obj.name || obj.type;
  298. // Default rendering for other object types
  299. const elem = document.createElement( 'div' );
  300. elem.className = 'tree-item';
  301. elem.style.paddingLeft = `${level * 20}px`;
  302. elem.setAttribute( 'data-uuid', obj.uuid );
  303. let labelContent = `<span class="icon">${icon}</span>
  304. <span class="label">${displayName}</span>
  305. <span class="type">${obj.type}</span>`;
  306. if ( obj.isScene ) {
  307. // Add object count for scenes
  308. let objectCount = - 1;
  309. function countObjects( uuid ) {
  310. const object = state.objects.get( uuid );
  311. if ( object ) {
  312. objectCount ++; // Increment count for the object itself
  313. if ( object.children ) {
  314. object.children.forEach( childId => countObjects( childId ) );
  315. }
  316. }
  317. }
  318. countObjects( obj.uuid );
  319. displayName = `${obj.name || obj.type} <span class="object-details">${objectCount} objects</span>`;
  320. labelContent = `<span class="icon">${icon}</span>
  321. <span class="label">${displayName}</span>
  322. <span class="type">${obj.type}</span>`;
  323. }
  324. elem.innerHTML = labelContent;
  325. // Add mouseover handler to request object details
  326. elem.addEventListener( 'mouseover', ( event ) => {
  327. event.stopPropagation(); // Prevent bubbling to parent elements
  328. requestObjectDetails( obj.uuid );
  329. } );
  330. container.appendChild( elem );
  331. // Handle children (excluding children of renderers, as properties are shown in details)
  332. if ( ! obj.isRenderer && obj.children && obj.children.length > 0 ) {
  333. // Create a container for children
  334. const childContainer = document.createElement( 'div' );
  335. childContainer.className = 'children';
  336. container.appendChild( childContainer );
  337. // Get all children and sort them by type for better organization
  338. const children = obj.children
  339. .map( childId => state.objects.get( childId ) )
  340. .filter( child => child !== undefined )
  341. .sort( ( a, b ) => {
  342. const getTypeOrder = ( obj ) => {
  343. if ( obj.isCamera ) return 1;
  344. if ( obj.isLight ) return 2;
  345. if ( obj.isGroup ) return 3;
  346. if ( obj.isMesh ) return 4;
  347. return 5;
  348. };
  349. const aOrder = getTypeOrder( a );
  350. const bOrder = getTypeOrder( b );
  351. return aOrder - bOrder;
  352. } );
  353. // Render each child
  354. children.forEach( child => {
  355. renderObject( child, childContainer, level + 1 );
  356. } );
  357. }
  358. }
  359. // Function to update the UI
  360. function updateUI() {
  361. const container = document.getElementById( 'scene-tree' );
  362. container.innerHTML = '';
  363. const header = document.createElement( 'div' );
  364. header.className = 'header';
  365. header.style.display = 'flex'; // Use flexbox
  366. header.style.justifyContent = 'space-between'; // Align items left and right
  367. const miscSpan = document.createElement( 'span' );
  368. miscSpan.innerHTML = '<a href="https://docs.google.com/forms/d/e/1FAIpQLSdw1QcgXNiECYiPx6k0vSQRiRe0FmByrrojV4fgeL5zzXIiCw/viewform?usp=preview" target="_blank">+</a>';
  369. const manifest = chrome.runtime.getManifest();
  370. const manifestVersionSpan = document.createElement( 'span' );
  371. manifestVersionSpan.textContent = `${manifest.version}`;
  372. manifestVersionSpan.style.opacity = '0.5'; // Make it less prominent
  373. header.appendChild( miscSpan );
  374. header.appendChild( manifestVersionSpan );
  375. container.appendChild( header );
  376. const hasRenderers = state.renderers.size > 0;
  377. const hasScenes = state.scenes.size > 0;
  378. // Create sections container if both renderers and scenes exist (for responsive layout)
  379. let sectionsContainer = container;
  380. if ( hasRenderers && hasScenes ) {
  381. sectionsContainer = document.createElement( 'div' );
  382. sectionsContainer.className = 'sections-container';
  383. container.appendChild( sectionsContainer );
  384. }
  385. // Add renderers section
  386. if ( hasRenderers ) {
  387. const renderersSection = document.createElement( 'div' );
  388. renderersSection.className = 'section';
  389. renderersSection.innerHTML = '<h3>Renderers</h3>';
  390. state.renderers.forEach( renderer => {
  391. renderRenderer( renderer, renderersSection );
  392. } );
  393. sectionsContainer.appendChild( renderersSection );
  394. }
  395. // Add scenes section
  396. if ( hasScenes ) {
  397. const scenesSection = document.createElement( 'div' );
  398. scenesSection.className = 'section';
  399. scenesSection.innerHTML = '<h3>Scenes</h3>';
  400. state.scenes.forEach( scene => {
  401. renderObject( scene, scenesSection );
  402. } );
  403. sectionsContainer.appendChild( scenesSection );
  404. }
  405. }
  406. // Create floating details panel
  407. function createFloatingPanel() {
  408. if ( floatingPanel ) return floatingPanel;
  409. floatingPanel = document.createElement( 'div' );
  410. floatingPanel.className = 'floating-details';
  411. document.body.appendChild( floatingPanel );
  412. return floatingPanel;
  413. }
  414. // Show floating details panel
  415. function showFloatingDetails( objectData ) {
  416. const panel = createFloatingPanel();
  417. // Clear previous content
  418. panel.innerHTML = '';
  419. if ( objectData.position ) {
  420. panel.appendChild( createVectorRow( 'Position', objectData.position ) );
  421. }
  422. if ( objectData.rotation ) {
  423. panel.appendChild( createVectorRow( 'Rotation', objectData.rotation ) );
  424. }
  425. if ( objectData.scale ) {
  426. panel.appendChild( createVectorRow( 'Scale', objectData.scale ) );
  427. }
  428. // Position panel near mouse
  429. updateFloatingPanelPosition();
  430. // Show panel
  431. panel.classList.add( 'visible' );
  432. }
  433. // Update floating panel position
  434. function updateFloatingPanelPosition() {
  435. if ( ! floatingPanel || ! floatingPanel.classList.contains( 'visible' ) ) return;
  436. const offset = 15; // Offset from cursor
  437. let x = mousePosition.x + offset;
  438. let y = mousePosition.y + offset;
  439. // Prevent panel from going off-screen
  440. const panelRect = floatingPanel.getBoundingClientRect();
  441. const maxX = window.innerWidth - panelRect.width - 10;
  442. const maxY = window.innerHeight - panelRect.height - 10;
  443. if ( x > maxX ) x = mousePosition.x - panelRect.width - offset;
  444. if ( y > maxY ) y = mousePosition.y - panelRect.height - offset;
  445. floatingPanel.style.left = `${Math.max( 10, x )}px`;
  446. floatingPanel.style.top = `${Math.max( 10, y )}px`;
  447. }
  448. // Track mouse position
  449. document.addEventListener( 'mousemove', ( event ) => {
  450. mousePosition.x = event.clientX;
  451. mousePosition.y = event.clientY;
  452. updateFloatingPanelPosition();
  453. } );
  454. // Hide panel when mouse leaves the tree area
  455. document.addEventListener( 'mouseover', ( event ) => {
  456. if ( floatingPanel && ! event.target.closest( '.tree-item' ) ) {
  457. floatingPanel.classList.remove( 'visible' );
  458. }
  459. } );
  460. // Initial UI update
  461. clearState();
  462. updateUI();
粤ICP备19079148号