panel.js 21 KB

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