|
|
@@ -0,0 +1,712 @@
|
|
|
+/* global chrome */
|
|
|
+
|
|
|
+const CONNECTION_NAME = 'three-devtools';
|
|
|
+const STATE_POLLING_INTERVAL = 1000;
|
|
|
+
|
|
|
+// --- Utility Functions ---
|
|
|
+function getObjectIcon( obj ) {
|
|
|
+
|
|
|
+ if ( obj.isScene ) return '🌍';
|
|
|
+ if ( obj.isCamera ) return '📷';
|
|
|
+ if ( obj.isLight ) return '💡';
|
|
|
+ if ( obj.isInstancedMesh ) return '🔸';
|
|
|
+ if ( obj.isMesh ) return '🔷';
|
|
|
+ if ( obj.type === 'Group' ) return '📁';
|
|
|
+ return '📦';
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+function createPropertyRow( label, value ) {
|
|
|
+
|
|
|
+ const row = document.createElement( 'div' );
|
|
|
+ row.className = 'property-row';
|
|
|
+ row.style.display = 'flex';
|
|
|
+ row.style.justifyContent = 'space-between';
|
|
|
+ row.style.marginBottom = '2px';
|
|
|
+
|
|
|
+ const labelSpan = document.createElement( 'span' );
|
|
|
+ labelSpan.className = 'property-label';
|
|
|
+ labelSpan.textContent = `${label}`;
|
|
|
+ labelSpan.style.marginRight = '10px';
|
|
|
+ labelSpan.style.whiteSpace = 'nowrap';
|
|
|
+
|
|
|
+ const valueSpan = document.createElement( 'span' );
|
|
|
+ valueSpan.className = 'property-value';
|
|
|
+ const displayValue = ( value === undefined || value === null )
|
|
|
+ ? '–'
|
|
|
+ : ( typeof value === 'number' ? value.toLocaleString() : value );
|
|
|
+ valueSpan.textContent = displayValue;
|
|
|
+ valueSpan.style.textAlign = 'right';
|
|
|
+
|
|
|
+ row.appendChild( labelSpan );
|
|
|
+ row.appendChild( valueSpan );
|
|
|
+ return row;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+function createVectorRow( label, vector ) {
|
|
|
+
|
|
|
+ const row = document.createElement( 'div' );
|
|
|
+ row.className = 'property-row';
|
|
|
+ row.style.marginBottom = '2px';
|
|
|
+
|
|
|
+ // Pad label to ensure consistent alignment
|
|
|
+ const paddedLabel = label.padEnd( 16, ' ' ); // Pad to 16 characters
|
|
|
+ const content = `${paddedLabel} ${vector.x.toFixed( 3 )}\t${vector.y.toFixed( 3 )}\t${vector.z.toFixed( 3 )}`;
|
|
|
+ row.textContent = content;
|
|
|
+ row.style.fontFamily = 'monospace';
|
|
|
+ row.style.whiteSpace = 'pre';
|
|
|
+
|
|
|
+ return row;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// --- State ---
|
|
|
+const state = {
|
|
|
+ revision: null,
|
|
|
+ scenes: new Map(),
|
|
|
+ renderers: new Map(),
|
|
|
+ objects: new Map(),
|
|
|
+ selectedObject: null
|
|
|
+};
|
|
|
+
|
|
|
+// Floating details panel
|
|
|
+let floatingPanel = null;
|
|
|
+let mousePosition = { x: 0, y: 0 };
|
|
|
+
|
|
|
+
|
|
|
+// Create a connection to the background page
|
|
|
+const backgroundPageConnection = chrome.runtime.connect( {
|
|
|
+ name: CONNECTION_NAME
|
|
|
+} );
|
|
|
+
|
|
|
+// Initialize the connection with the inspected tab ID
|
|
|
+backgroundPageConnection.postMessage( {
|
|
|
+ name: MESSAGE_INIT,
|
|
|
+ tabId: chrome.devtools.inspectedWindow.tabId
|
|
|
+} );
|
|
|
+
|
|
|
+// Request the initial state from the bridge script
|
|
|
+backgroundPageConnection.postMessage( {
|
|
|
+ name: MESSAGE_REQUEST_STATE,
|
|
|
+ tabId: chrome.devtools.inspectedWindow.tabId
|
|
|
+} );
|
|
|
+
|
|
|
+// Function to scroll to canvas element
|
|
|
+function scrollToCanvas( rendererUuid ) {
|
|
|
+ backgroundPageConnection.postMessage( {
|
|
|
+ name: MESSAGE_SCROLL_TO_CANVAS,
|
|
|
+ uuid: rendererUuid,
|
|
|
+ tabId: chrome.devtools.inspectedWindow.tabId
|
|
|
+ } );
|
|
|
+}
|
|
|
+
|
|
|
+const intervalId = setInterval( () => {
|
|
|
+
|
|
|
+ backgroundPageConnection.postMessage( {
|
|
|
+ name: MESSAGE_REQUEST_STATE,
|
|
|
+ tabId: chrome.devtools.inspectedWindow.tabId
|
|
|
+ } );
|
|
|
+
|
|
|
+}, STATE_POLLING_INTERVAL );
|
|
|
+
|
|
|
+backgroundPageConnection.onDisconnect.addListener( () => {
|
|
|
+
|
|
|
+ clearInterval( intervalId );
|
|
|
+ clearState();
|
|
|
+
|
|
|
+} );
|
|
|
+
|
|
|
+// Function to request object details from the bridge
|
|
|
+function requestObjectDetails( uuid ) {
|
|
|
+ backgroundPageConnection.postMessage( {
|
|
|
+ name: MESSAGE_REQUEST_OBJECT_DETAILS,
|
|
|
+ uuid: uuid,
|
|
|
+ tabId: chrome.devtools.inspectedWindow.tabId
|
|
|
+ } );
|
|
|
+}
|
|
|
+
|
|
|
+// Function to highlight object in 3D scene
|
|
|
+function requestObjectHighlight( uuid ) {
|
|
|
+ backgroundPageConnection.postMessage( {
|
|
|
+ name: MESSAGE_HIGHLIGHT_OBJECT,
|
|
|
+ uuid: uuid,
|
|
|
+ tabId: chrome.devtools.inspectedWindow.tabId
|
|
|
+ } );
|
|
|
+}
|
|
|
+
|
|
|
+// Function to remove highlight from 3D scene
|
|
|
+function requestObjectUnhighlight() {
|
|
|
+ backgroundPageConnection.postMessage( {
|
|
|
+ name: MESSAGE_UNHIGHLIGHT_OBJECT,
|
|
|
+ tabId: chrome.devtools.inspectedWindow.tabId
|
|
|
+ } );
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+// Store renderer collapse states
|
|
|
+const rendererCollapsedState = new Map();
|
|
|
+
|
|
|
+// Static DOM elements (created once in initUI)
|
|
|
+let renderersSection = null;
|
|
|
+let scenesSection = null;
|
|
|
+let sceneDirty = true;
|
|
|
+
|
|
|
+// Helper function to create properties column for renderer
|
|
|
+function createRendererPropertiesColumn( props ) {
|
|
|
+
|
|
|
+ const propsCol = document.createElement( 'div' );
|
|
|
+ propsCol.className = 'properties-column';
|
|
|
+ const propsTitle = document.createElement( 'h4' );
|
|
|
+ propsTitle.textContent = 'Properties';
|
|
|
+ propsCol.appendChild( propsTitle );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Size', `${props.width}x${props.height}` ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Alpha', props.alpha ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Antialias', props.antialias ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Output Color Space', props.outputColorSpace ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Tone Mapping', props.toneMapping ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Tone Mapping Exposure', props.toneMappingExposure ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Shadows', props.shadows ? 'enabled' : 'disabled' ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Auto Clear', props.autoClear ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Auto Clear Color', props.autoClearColor ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Auto Clear Depth', props.autoClearDepth ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Auto Clear Stencil', props.autoClearStencil ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Local Clipping', props.localClipping ) );
|
|
|
+ propsCol.appendChild( createPropertyRow( 'Physically Correct Lights', props.physicallyCorrectLights ) );
|
|
|
+
|
|
|
+ return propsCol;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Helper function to create stats column for renderer
|
|
|
+function createRendererStatsColumn( info ) {
|
|
|
+
|
|
|
+ const statsCol = document.createElement( 'div' );
|
|
|
+ statsCol.className = 'stats-column';
|
|
|
+
|
|
|
+ // Render Stats
|
|
|
+ const renderTitle = document.createElement( 'h4' );
|
|
|
+ renderTitle.textContent = 'Render Stats';
|
|
|
+ statsCol.appendChild( renderTitle );
|
|
|
+ statsCol.appendChild( createPropertyRow( 'Frame', info.render.frame ) );
|
|
|
+ statsCol.appendChild( createPropertyRow( 'Draw Calls', info.render.calls ) );
|
|
|
+ statsCol.appendChild( createPropertyRow( 'Triangles', info.render.triangles ) );
|
|
|
+ statsCol.appendChild( createPropertyRow( 'Points', info.render.points ) );
|
|
|
+ statsCol.appendChild( createPropertyRow( 'Lines', info.render.lines ) );
|
|
|
+
|
|
|
+ // Memory
|
|
|
+ const memoryTitle = document.createElement( 'h4' );
|
|
|
+ memoryTitle.textContent = 'Memory';
|
|
|
+ memoryTitle.style.marginTop = '10px';
|
|
|
+ statsCol.appendChild( memoryTitle );
|
|
|
+ statsCol.appendChild( createPropertyRow( 'Geometries', info.memory.geometries ) );
|
|
|
+ statsCol.appendChild( createPropertyRow( 'Textures', info.memory.textures ) );
|
|
|
+ statsCol.appendChild( createPropertyRow( 'Shader Programs', info.memory.programs ) );
|
|
|
+
|
|
|
+ return statsCol;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Helper function to process scene batch updates
|
|
|
+function processSceneBatch( sceneUuid, batchObjects ) {
|
|
|
+
|
|
|
+ // 1. Identify UUIDs in the new batch
|
|
|
+ const newObjectUuids = new Set( batchObjects.map( obj => obj.uuid ) );
|
|
|
+
|
|
|
+ // 2. Identify current object UUIDs associated with this scene that are NOT renderers
|
|
|
+ const currentSceneObjectUuids = new Set();
|
|
|
+ state.objects.forEach( ( obj, uuid ) => {
|
|
|
+
|
|
|
+ // Use the _sceneUuid property we'll add below, or check if it's the scene root itself
|
|
|
+ if ( obj._sceneUuid === sceneUuid || uuid === sceneUuid ) {
|
|
|
+
|
|
|
+ currentSceneObjectUuids.add( uuid );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ // 3. Find UUIDs to remove (in current state for this scene, but not in the new batch)
|
|
|
+ const uuidsToRemove = new Set();
|
|
|
+ currentSceneObjectUuids.forEach( uuid => {
|
|
|
+
|
|
|
+ if ( ! newObjectUuids.has( uuid ) ) {
|
|
|
+
|
|
|
+ uuidsToRemove.add( uuid );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ // 4. Remove stale objects from state
|
|
|
+ uuidsToRemove.forEach( uuid => {
|
|
|
+
|
|
|
+ state.objects.delete( uuid );
|
|
|
+ // If a scene object itself was somehow removed (unlikely for root), clean up scenes map too
|
|
|
+ if ( state.scenes.has( uuid ) ) {
|
|
|
+
|
|
|
+ state.scenes.delete( uuid );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ // 5. Process the new batch: Add/Update objects and mark their scene association
|
|
|
+ batchObjects.forEach( objData => {
|
|
|
+
|
|
|
+ objData._sceneUuid = sceneUuid;
|
|
|
+ state.objects.set( objData.uuid, objData );
|
|
|
+
|
|
|
+ if ( objData.isScene && objData.uuid === sceneUuid ) {
|
|
|
+
|
|
|
+ state.scenes.set( objData.uuid, objData );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ sceneDirty = true;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Clear state when panel is reloaded
|
|
|
+function clearState() {
|
|
|
+
|
|
|
+ state.revision = null;
|
|
|
+ state.scenes.clear();
|
|
|
+ state.renderers.clear();
|
|
|
+ state.objects.clear();
|
|
|
+ sceneDirty = true;
|
|
|
+
|
|
|
+ // Hide floating panel
|
|
|
+ if ( floatingPanel ) {
|
|
|
+
|
|
|
+ floatingPanel.classList.remove( 'visible' );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Listen for messages from the background page
|
|
|
+backgroundPageConnection.onMessage.addListener( function ( message ) {
|
|
|
+
|
|
|
+ if ( message.id === MESSAGE_ID ) {
|
|
|
+
|
|
|
+ handleThreeEvent( message );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+} );
|
|
|
+
|
|
|
+function handleThreeEvent( message ) {
|
|
|
+
|
|
|
+ switch ( message.name ) {
|
|
|
+
|
|
|
+ case EVENT_REGISTER:
|
|
|
+ state.revision = message.detail.revision;
|
|
|
+ break;
|
|
|
+
|
|
|
+ case EVENT_RENDERER:
|
|
|
+ const detail = message.detail;
|
|
|
+ state.renderers.set( detail.uuid, detail );
|
|
|
+ state.objects.set( detail.uuid, detail );
|
|
|
+ updateRenderers();
|
|
|
+ break;
|
|
|
+
|
|
|
+ case EVENT_OBJECT_DETAILS:
|
|
|
+ state.selectedObject = message.detail;
|
|
|
+ showFloatingDetails( message.detail );
|
|
|
+ break;
|
|
|
+
|
|
|
+ case EVENT_SCENE:
|
|
|
+ const { sceneUuid, objects: batchObjects } = message.detail;
|
|
|
+ processSceneBatch( sceneUuid, batchObjects );
|
|
|
+ updateSceneTree();
|
|
|
+ break;
|
|
|
+
|
|
|
+ case EVENT_COMMITTED:
|
|
|
+ clearState();
|
|
|
+ updateRenderers();
|
|
|
+ updateSceneTree();
|
|
|
+ break;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+function renderRenderer( obj, container ) {
|
|
|
+
|
|
|
+ // 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
|
|
|
+ detailsElement.open = rendererCollapsedState.get( obj.uuid ) || false;
|
|
|
+
|
|
|
+ // 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
|
|
|
+
|
|
|
+ // Update display name in the summary line
|
|
|
+ const props = obj.properties;
|
|
|
+ const details = [ `${props.width}x${props.height}` ];
|
|
|
+ if ( props.info ) {
|
|
|
+
|
|
|
+ details.push( `${props.info.render.calls} draws` );
|
|
|
+ details.push( `${props.info.render.triangles.toLocaleString()} triangles` );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ const displayName = `${obj.type} <span class="object-details">${details.join( ' ・ ' )}</span>`;
|
|
|
+
|
|
|
+ // Use toggle icon instead of paint icon
|
|
|
+ const scrollButton = obj.canvasInDOM ?
|
|
|
+ `<button class="scroll-to-canvas-btn" data-canvas-uuid="${obj.uuid}" title="Scroll to canvas">🙂</button>` :
|
|
|
+ `<span class="scroll-to-canvas-placeholder" title="Canvas not in DOM">🫥</span>`;
|
|
|
+ summaryElem.innerHTML = `<span class="icon toggle-icon"></span>
|
|
|
+ <span class="label">${displayName}</span>
|
|
|
+ <span class="type">${obj.type}</span>
|
|
|
+ ${scrollButton}`;
|
|
|
+ detailsElement.appendChild( summaryElem );
|
|
|
+
|
|
|
+ const propsContainer = document.createElement( 'div' );
|
|
|
+ propsContainer.className = 'properties-list';
|
|
|
+ // Adjust padding calculation if needed, ensure it's a number before adding
|
|
|
+ const summaryPaddingLeft = parseFloat( summaryElem.style.paddingLeft ) || 0;
|
|
|
+ propsContainer.style.paddingLeft = `${summaryPaddingLeft + 20}px`; // Indent further
|
|
|
+
|
|
|
+ propsContainer.innerHTML = ''; // Clear placeholder
|
|
|
+
|
|
|
+ if ( obj.properties ) {
|
|
|
+
|
|
|
+ const props = obj.properties;
|
|
|
+ const info = props.info || { render: {}, memory: {} }; // Default empty objects if info is missing
|
|
|
+
|
|
|
+ const gridContainer = document.createElement( 'div' );
|
|
|
+ gridContainer.style.display = 'grid';
|
|
|
+ gridContainer.style.gridTemplateColumns = 'repeat(auto-fit, minmax(200px, 1fr))'; // Responsive columns
|
|
|
+ gridContainer.style.gap = '10px 20px'; // Row and column gap
|
|
|
+
|
|
|
+ gridContainer.appendChild( createRendererPropertiesColumn( props ) );
|
|
|
+ gridContainer.appendChild( createRendererStatsColumn( info ) );
|
|
|
+ propsContainer.appendChild( gridContainer );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ propsContainer.textContent = 'No properties available.';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ detailsElement.appendChild( propsContainer );
|
|
|
+
|
|
|
+ // Add click handler for scroll to canvas button
|
|
|
+ const scrollBtn = detailsElement.querySelector( '.scroll-to-canvas-btn' );
|
|
|
+ if ( scrollBtn ) {
|
|
|
+ scrollBtn.addEventListener( 'click', ( event ) => {
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+ scrollToCanvas( obj.uuid );
|
|
|
+ } );
|
|
|
+ }
|
|
|
+
|
|
|
+ container.appendChild( detailsElement ); // Append details to the main container
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Function to render an object and its children
|
|
|
+function renderObject( obj, container, level = 0, parentInvisible = false ) {
|
|
|
+
|
|
|
+ const icon = getObjectIcon( obj );
|
|
|
+ let displayName = obj.name || obj.type;
|
|
|
+
|
|
|
+ // 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 );
|
|
|
+
|
|
|
+ // Apply opacity for invisible objects or if parent is invisible
|
|
|
+ if ( obj.visible === false || parentInvisible ) {
|
|
|
+
|
|
|
+ elem.style.opacity = '0.5';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 = - 1;
|
|
|
+ function countObjects( uuid ) {
|
|
|
+
|
|
|
+ const object = state.objects.get( uuid );
|
|
|
+ if ( object && object.name !== '__THREE_DEVTOOLS_HIGHLIGHT__' ) {
|
|
|
+
|
|
|
+ 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>`;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ elem.innerHTML = labelContent;
|
|
|
+
|
|
|
+ // Add mouseenter handler to request object details and highlight in 3D
|
|
|
+ elem.addEventListener( 'mouseenter', () => {
|
|
|
+ requestObjectDetails( obj.uuid );
|
|
|
+ // Only highlight if object and all parents are visible
|
|
|
+ if ( obj.visible !== false && ! parentInvisible ) {
|
|
|
+
|
|
|
+ requestObjectHighlight( obj.uuid );
|
|
|
+
|
|
|
+ }
|
|
|
+ } );
|
|
|
+
|
|
|
+ // Add mouseleave handler to remove 3D highlight
|
|
|
+ elem.addEventListener( 'mouseleave', () => {
|
|
|
+ requestObjectUnhighlight();
|
|
|
+ } );
|
|
|
+
|
|
|
+ container.appendChild( elem );
|
|
|
+
|
|
|
+ // 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';
|
|
|
+ container.appendChild( childContainer );
|
|
|
+
|
|
|
+ // Get all children and sort them by type for better organization
|
|
|
+ const children = obj.children
|
|
|
+ .map( childId => state.objects.get( childId ) )
|
|
|
+ .filter( child => child !== undefined && child.name !== '__THREE_DEVTOOLS_HIGHLIGHT__' )
|
|
|
+ .sort( ( a, b ) => {
|
|
|
+
|
|
|
+ const getTypeOrder = ( obj ) => {
|
|
|
+ if ( obj.isCamera ) return 1;
|
|
|
+ if ( obj.isLight ) return 2;
|
|
|
+ if ( obj.isGroup ) return 3;
|
|
|
+ if ( obj.isMesh ) return 4;
|
|
|
+ return 5;
|
|
|
+ };
|
|
|
+
|
|
|
+ const aOrder = getTypeOrder( a );
|
|
|
+ const bOrder = getTypeOrder( b );
|
|
|
+
|
|
|
+ return aOrder - bOrder;
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ // Render each child
|
|
|
+ children.forEach( child => {
|
|
|
+
|
|
|
+ renderObject( child, childContainer, level + 1, parentInvisible || obj.visible === false );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Build the static DOM shell (called once)
|
|
|
+function initUI() {
|
|
|
+
|
|
|
+ const container = document.getElementById( 'scene-tree' );
|
|
|
+
|
|
|
+ const header = document.createElement( 'div' );
|
|
|
+ header.className = 'header';
|
|
|
+ header.style.display = 'flex';
|
|
|
+ header.style.justifyContent = 'space-between';
|
|
|
+
|
|
|
+ const miscSpan = document.createElement( 'span' );
|
|
|
+ miscSpan.innerHTML = '<a href="https://docs.google.com/forms/d/e/1FAIpQLSdw1QcgXNiECYiPx6k0vSQRiRe0FmByrrojV4fgeL5zzXIiCw/viewform?usp=preview" target="_blank">+</a>';
|
|
|
+
|
|
|
+ const manifest = chrome.runtime.getManifest();
|
|
|
+
|
|
|
+ const manifestVersionSpan = document.createElement( 'span' );
|
|
|
+ manifestVersionSpan.textContent = `${manifest.version}`;
|
|
|
+ manifestVersionSpan.style.opacity = '0.5';
|
|
|
+
|
|
|
+ header.appendChild( miscSpan );
|
|
|
+ header.appendChild( manifestVersionSpan );
|
|
|
+ container.appendChild( header );
|
|
|
+
|
|
|
+ const sectionsContainer = document.createElement( 'div' );
|
|
|
+ sectionsContainer.className = 'sections-container';
|
|
|
+ container.appendChild( sectionsContainer );
|
|
|
+
|
|
|
+ renderersSection = document.createElement( 'div' );
|
|
|
+ renderersSection.className = 'section';
|
|
|
+ renderersSection.style.display = 'none';
|
|
|
+ sectionsContainer.appendChild( renderersSection );
|
|
|
+
|
|
|
+ scenesSection = document.createElement( 'div' );
|
|
|
+ scenesSection.className = 'section';
|
|
|
+ scenesSection.style.display = 'none';
|
|
|
+ sectionsContainer.appendChild( scenesSection );
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Update only the renderers section
|
|
|
+function updateRenderers() {
|
|
|
+
|
|
|
+ if ( state.renderers.size > 0 ) {
|
|
|
+
|
|
|
+ renderersSection.style.display = '';
|
|
|
+ renderersSection.innerHTML = '<h3>Renderers</h3>';
|
|
|
+
|
|
|
+ state.renderers.forEach( renderer => {
|
|
|
+
|
|
|
+ renderRenderer( renderer, renderersSection );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ renderersSection.style.display = 'none';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Rebuild the scene tree only when dirty
|
|
|
+function updateSceneTree() {
|
|
|
+
|
|
|
+ if ( ! sceneDirty ) return;
|
|
|
+
|
|
|
+ sceneDirty = false;
|
|
|
+
|
|
|
+ if ( state.scenes.size > 0 ) {
|
|
|
+
|
|
|
+ scenesSection.style.display = '';
|
|
|
+ scenesSection.innerHTML = '<h3>Scenes</h3>';
|
|
|
+
|
|
|
+ state.scenes.forEach( scene => {
|
|
|
+
|
|
|
+ renderObject( scene, scenesSection );
|
|
|
+
|
|
|
+ } );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ scenesSection.style.display = 'none';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Create floating details panel
|
|
|
+function createFloatingPanel() {
|
|
|
+
|
|
|
+ if ( floatingPanel ) return floatingPanel;
|
|
|
+
|
|
|
+ floatingPanel = document.createElement( 'div' );
|
|
|
+ floatingPanel.className = 'floating-details';
|
|
|
+ document.body.appendChild( floatingPanel );
|
|
|
+
|
|
|
+ return floatingPanel;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Show floating details panel
|
|
|
+function showFloatingDetails( objectData ) {
|
|
|
+
|
|
|
+ const panel = createFloatingPanel();
|
|
|
+
|
|
|
+ // Clear previous content
|
|
|
+ panel.innerHTML = '';
|
|
|
+
|
|
|
+ if ( objectData.position ) {
|
|
|
+
|
|
|
+ panel.appendChild( createVectorRow( 'Position', objectData.position ) );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( objectData.rotation ) {
|
|
|
+
|
|
|
+ panel.appendChild( createVectorRow( 'Rotation', objectData.rotation ) );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( objectData.scale ) {
|
|
|
+
|
|
|
+ panel.appendChild( createVectorRow( 'Scale', objectData.scale ) );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // Position panel near mouse
|
|
|
+ updateFloatingPanelPosition();
|
|
|
+
|
|
|
+ // Show panel
|
|
|
+ panel.classList.add( 'visible' );
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Update floating panel position
|
|
|
+function updateFloatingPanelPosition() {
|
|
|
+
|
|
|
+ if ( ! floatingPanel || ! floatingPanel.classList.contains( 'visible' ) ) return;
|
|
|
+
|
|
|
+ const offset = 15; // Offset from cursor
|
|
|
+ let x = mousePosition.x + offset;
|
|
|
+ let y = mousePosition.y + offset;
|
|
|
+
|
|
|
+ // Prevent panel from going off-screen
|
|
|
+ const panelRect = floatingPanel.getBoundingClientRect();
|
|
|
+ const maxX = window.innerWidth - panelRect.width - 10;
|
|
|
+ const maxY = window.innerHeight - panelRect.height - 10;
|
|
|
+
|
|
|
+ if ( x > maxX ) x = mousePosition.x - panelRect.width - offset;
|
|
|
+ if ( y > maxY ) y = mousePosition.y - panelRect.height - offset;
|
|
|
+
|
|
|
+ floatingPanel.style.left = `${Math.max( 10, x )}px`;
|
|
|
+ floatingPanel.style.top = `${Math.max( 10, y )}px`;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// Track mouse position
|
|
|
+document.addEventListener( 'mousemove', ( event ) => {
|
|
|
+
|
|
|
+ mousePosition.x = event.clientX;
|
|
|
+ mousePosition.y = event.clientY;
|
|
|
+ updateFloatingPanelPosition();
|
|
|
+
|
|
|
+} );
|
|
|
+
|
|
|
+// Hide panel when mouse leaves the tree area
|
|
|
+document.addEventListener( 'mouseover', ( event ) => {
|
|
|
+
|
|
|
+ if ( floatingPanel && ! event.target.closest( '.tree-item' ) ) {
|
|
|
+
|
|
|
+ floatingPanel.classList.remove( 'visible' );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+} );
|
|
|
+
|
|
|
+// Initial UI setup
|
|
|
+initUI();
|