Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions devtools/panel/panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
149 changes: 97 additions & 52 deletions devtools/panel/panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -276,6 +279,7 @@ function clearState() {
state.scenes.clear();
state.renderers.clear();
state.objects.clear();
treeExpandedState.clear();
sceneDirty = true;

// Hide floating panel
Expand Down Expand Up @@ -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 = `<span class="icon">${icon}</span>
<span class="label">${displayName}</span>
<span class="type">${obj.type}</span>`;
return getTypeOrder( a ) - getTypeOrder( b );

} )
: [];

const hasChildren = children.length > 0;

if ( obj.isScene ) {

Expand All @@ -466,70 +477,104 @@ function renderObject( obj, container, level = 0, parentInvisible = false ) {

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;
const togglePart = hasChildren
? '<span class="tree-toggle"></span>'
: '<span class="tree-toggle-placeholder"></span>';

// 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}<span class="icon">${icon}</span>
<span class="label">${displayName}</span>
<span class="type">${obj.type}</span>`;

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)
Expand Down
2 changes: 1 addition & 1 deletion src/materials/MeshBasicMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion src/materials/MeshLambertMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion src/materials/MeshPhongMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion src/materials/MeshStandardMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion src/materials/MeshToonMaterial.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down