diff --git a/devtools/panel/panel.css b/devtools/panel/panel.css index 3de915c501ed77..a5ac3df2ea1e9d 100644 --- a/devtools/panel/panel.css +++ b/devtools/panel/panel.css @@ -85,6 +85,31 @@ hr { margin-left: 0; } +/* Collapsible scene tree nodes */ +details.tree-node > summary.tree-item { + list-style: none; +} +details.tree-node > summary.tree-item::-webkit-details-marker { + display: none; +} + +.tree-toggle, +.tree-toggle-placeholder { + display: inline-block; + width: 1em; + margin-right: 2px; + text-align: center; + flex-shrink: 0; +} +.tree-toggle::before { + content: '▶'; + font-size: 0.7em; + opacity: 0.6; +} +details.tree-node[open] > summary.tree-item .tree-toggle::before { + content: '▼'; +} + /* Style for clickable renderer summary */ .renderer-summary { cursor: pointer; diff --git a/devtools/panel/panel.js b/devtools/panel/panel.js index 66411e17952c6f..2c142e767c0929 100644 --- a/devtools/panel/panel.js +++ b/devtools/panel/panel.js @@ -147,6 +147,9 @@ function requestObjectUnhighlight() { // Store renderer collapse states const rendererCollapsedState = new Map(); +// Store scene tree expanded states (uuid -> boolean). Defaults to expanded. +const treeExpandedState = new Map(); + // Static DOM elements (created once in initUI) let renderersSection = null; let scenesSection = null; @@ -276,6 +279,7 @@ function clearState() { state.scenes.clear(); state.renderers.clear(); state.objects.clear(); + treeExpandedState.clear(); sceneDirty = true; // Hide floating panel @@ -427,22 +431,29 @@ 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 ); + // Collect renderable children (renderers do not show children in the tree) + const children = ( ! obj.isRenderer && obj.children ) + ? obj.children + .map( childId => state.objects.get( childId ) ) + .filter( child => child !== undefined && child.name !== '__THREE_DEVTOOLS_HIGHLIGHT__' ) + .sort( ( a, b ) => { - // Apply opacity for invisible objects or if parent is invisible - if ( obj.visible === false || parentInvisible ) { + const getTypeOrder = ( o ) => { - elem.style.opacity = '0.5'; + if ( o.isCamera ) return 1; + if ( o.isLight ) return 2; + if ( o.isGroup ) return 3; + if ( o.isMesh ) return 4; + return 5; - } + }; - let labelContent = `${icon} - ${displayName} - ${obj.type}`; + return getTypeOrder( a ) - getTypeOrder( b ); + + } ) + : []; + + const hasChildren = children.length > 0; if ( obj.isScene ) { @@ -466,70 +477,104 @@ function renderObject( obj, container, level = 0, parentInvisible = false ) { countObjects( obj.uuid ); displayName = `${obj.name || obj.type} ${objectCount} objects`; - labelContent = `${icon} - ${displayName} - ${obj.type}`; } - elem.innerHTML = labelContent; + const togglePart = hasChildren + ? '' + : ''; - // 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 ) { + const labelContent = `${togglePart}${icon} + ${displayName} + ${obj.type}`; - requestObjectHighlight( obj.uuid ); + let header; // the element receiving hover/highlight handlers - } - } ); + if ( hasChildren ) { - // Add mouseleave handler to remove 3D highlight - elem.addEventListener( 'mouseleave', () => { - requestObjectUnhighlight(); - } ); + const node = document.createElement( 'details' ); + node.className = 'tree-node'; + node.setAttribute( 'data-uuid', obj.uuid ); - container.appendChild( elem ); + // Default to expanded unless the user has collapsed this node before + const stored = treeExpandedState.get( obj.uuid ); + node.open = stored === undefined ? true : stored; - // Handle children (excluding children of renderers, as properties are shown in details) - if ( ! obj.isRenderer && obj.children && obj.children.length > 0 ) { + node.addEventListener( 'toggle', () => { - // Create a container for children - const childContainer = document.createElement( 'div' ); - childContainer.className = 'children'; - container.appendChild( childContainer ); + treeExpandedState.set( obj.uuid, node.open ); - // 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 summary = document.createElement( 'summary' ); + summary.className = 'tree-item'; + summary.style.paddingLeft = `${level * 20}px`; + + if ( obj.visible === false || parentInvisible ) { - const aOrder = getTypeOrder( a ); - const bOrder = getTypeOrder( b ); + summary.style.opacity = '0.5'; - return aOrder - bOrder; + } - } ); + summary.innerHTML = labelContent; + node.appendChild( summary ); + + const childContainer = document.createElement( 'div' ); + childContainer.className = 'children'; + node.appendChild( childContainer ); + + container.appendChild( node ); - // Render each child children.forEach( child => { renderObject( child, childContainer, level + 1, parentInvisible || obj.visible === false ); } ); + header = summary; + + } else { + + const elem = document.createElement( 'div' ); + elem.className = 'tree-item'; + elem.style.paddingLeft = `${level * 20}px`; + elem.setAttribute( 'data-uuid', obj.uuid ); + + if ( obj.visible === false || parentInvisible ) { + + elem.style.opacity = '0.5'; + + } + + elem.innerHTML = labelContent; + + container.appendChild( elem ); + + header = elem; + } + // Add mouseenter handler to request object details and highlight in 3D + header.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 + header.addEventListener( 'mouseleave', () => { + + requestObjectUnhighlight(); + + } ); + } // Build the static DOM shell (called once) diff --git a/src/materials/MeshBasicMaterial.js b/src/materials/MeshBasicMaterial.js index e2f2aaa4ef099c..eb11ed13b5e413 100644 --- a/src/materials/MeshBasicMaterial.js +++ b/src/materials/MeshBasicMaterial.js @@ -62,7 +62,7 @@ class MeshBasicMaterial extends Material { /** * The light map. Requires a second set of UVs. * - * `lightMap` represents luminance data, and the texture must be assigned + * `lightMap` represents pre-baked illuminance data, and the texture must be assigned * a {@link Texture#colorSpace}. Most `lightMap` textures set * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats * such as `.exr` or `.hdr`. diff --git a/src/materials/MeshLambertMaterial.js b/src/materials/MeshLambertMaterial.js index 7e27c162a01ed6..a27f5065c25503 100644 --- a/src/materials/MeshLambertMaterial.js +++ b/src/materials/MeshLambertMaterial.js @@ -72,7 +72,7 @@ class MeshLambertMaterial extends Material { /** * The light map. Requires a second set of UVs. * - * `lightMap` represents luminance data, and the texture must be assigned + * `lightMap` represents pre-baked illuminance data, and the texture must be assigned * a {@link Texture#colorSpace}. Most `lightMap` textures set * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats * such as `.exr` or `.hdr`. diff --git a/src/materials/MeshPhongMaterial.js b/src/materials/MeshPhongMaterial.js index aa5b56bef4a329..182488cdb125f8 100644 --- a/src/materials/MeshPhongMaterial.js +++ b/src/materials/MeshPhongMaterial.js @@ -87,7 +87,7 @@ class MeshPhongMaterial extends Material { /** * The light map. Requires a second set of UVs. * - * `lightMap` represents luminance data, and the texture must be assigned + * `lightMap` represents pre-baked illuminance data, and the texture must be assigned * a {@link Texture#colorSpace}. Most `lightMap` textures set * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats * such as `.exr` or `.hdr`. diff --git a/src/materials/MeshStandardMaterial.js b/src/materials/MeshStandardMaterial.js index 0bde1d771a38bb..ab9b81c3d97912 100644 --- a/src/materials/MeshStandardMaterial.js +++ b/src/materials/MeshStandardMaterial.js @@ -112,7 +112,7 @@ class MeshStandardMaterial extends Material { /** * The light map. Requires a second set of UVs. * - * `lightMap` represents luminance data, and the texture must be assigned + * `lightMap` represents pre-baked illuminance data, and the texture must be assigned * a {@link Texture#colorSpace}. Most `lightMap` textures set * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats * such as `.exr` or `.hdr`. diff --git a/src/materials/MeshToonMaterial.js b/src/materials/MeshToonMaterial.js index 6169941859c7a4..7d3362852e299b 100644 --- a/src/materials/MeshToonMaterial.js +++ b/src/materials/MeshToonMaterial.js @@ -75,7 +75,7 @@ class MeshToonMaterial extends Material { /** * The light map. Requires a second set of UVs. * - * `lightMap` represents luminance data, and the texture must be assigned + * `lightMap` represents pre-baked illuminance data, and the texture must be assigned * a {@link Texture#colorSpace}. Most `lightMap` textures set * `texture.colorSpace = LinearSRGBColorSpace` and use float-type formats * such as `.exr` or `.hdr`.