Skip to content

Commit 169ecc8

Browse files
ryantremGeorgina
andauthored
Inspector v2: Scene explorer expand tree for selected item and scroll to it if needed (#16784)
- When an entity is selected (for example from the property pane), the scene explorer tree is now expanded to include that item. - If the entity was externally selected (not from within scene explorer itself), we scroll to that item to make sure it is visible. - Also added the parent link property for Node to further test these changes. --------- Co-authored-by: Georgina <[email protected]>
1 parent 2354849 commit 169ecc8

File tree

7 files changed

+148
-23
lines changed

7 files changed

+148
-23
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// eslint-disable-next-line import/no-internal-modules
2+
import type { Node } from "core/index";
3+
4+
import type { FunctionComponent } from "react";
5+
6+
import { LinkPropertyLine } from "shared-ui-components/fluent/hoc/linkPropertyLine";
7+
8+
import { useInterceptObservable } from "../../hooks/instrumentationHooks";
9+
import { useObservableState } from "../../hooks/observableHooks";
10+
11+
export const NodeGeneralProperties: FunctionComponent<{ node: Node; setSelectedEntity: (entity: unknown) => void }> = (props) => {
12+
const { node, setSelectedEntity } = props;
13+
14+
const parent = useObservableState(() => node.parent, useInterceptObservable("property", node, "parent"));
15+
16+
return <>{parent && <LinkPropertyLine key="Parent" label="Parent" description={`The parent of this node.`} value={parent.name} onLink={() => setSelectedEntity(parent)} />}</>;
17+
};

packages/dev/inspector-v2/src/components/properties/transformNodeTransformProperties.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,17 @@ function useVector3Property<T extends object, K extends Vector3Keys<T>>(target:
2222
}
2323

2424
export const TransformNodeTransformProperties: FunctionComponent<{ node: TransformNode }> = (props) => {
25-
const { node: transformNode } = props;
25+
const { node } = props;
2626

27-
const position = useVector3Property(transformNode, "position");
28-
const rotation = useVector3Property(transformNode, "rotation");
29-
const scaling = useVector3Property(transformNode, "scaling");
27+
const position = useVector3Property(node, "position");
28+
const rotation = useVector3Property(node, "rotation");
29+
const scaling = useVector3Property(node, "scaling");
3030

3131
return (
3232
<>
33-
<Vector3PropertyLine key="PositionTransform" label="Position" value={position} onChange={(val) => (transformNode.position = val)} />
34-
<Vector3PropertyLine key="RotationTransform" label="Rotation" value={rotation} onChange={(val) => (transformNode.scaling = val)} />
35-
<Vector3PropertyLine key="ScalingTransform" label="Scaling" value={scaling} onChange={(val) => (transformNode.scaling = val)} />
33+
<Vector3PropertyLine key="PositionTransform" label="Position" value={position} onChange={(val) => (node.position = val)} />
34+
<Vector3PropertyLine key="RotationTransform" label="Rotation" value={rotation} onChange={(val) => (node.scaling = val)} />
35+
<Vector3PropertyLine key="ScalingTransform" label="Scaling" value={scaling} onChange={(val) => (node.scaling = val)} />
3636
</>
3737
);
3838
};

packages/dev/inspector-v2/src/components/scene/sceneExplorer.tsx

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
import type { IReadonlyObservable, Nullable, Scene } from "core/index";
33

44
import type { TreeItemValue, TreeOpenChangeData, TreeOpenChangeEvent } from "@fluentui/react-components";
5+
import type { ScrollToInterface } from "@fluentui/react-components/unstable";
56
import type { ComponentType, FunctionComponent } from "react";
67

78
import { Body1, Body1Strong, Button, FlatTree, FlatTreeItem, makeStyles, ToggleButton, tokens, Tooltip, TreeItemLayout } from "@fluentui/react-components";
89
import { VirtualizerScrollView } from "@fluentui/react-components/unstable";
910
import { MoviesAndTvRegular } from "@fluentui/react-icons";
1011

11-
import { useCallback, useEffect, useMemo, useState } from "react";
12+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1213
import { TraverseGraph } from "../../misc/graphUtils";
1314

1415
export type EntityBase = Readonly<{
@@ -37,6 +38,11 @@ export type SceneExplorerSection<T extends EntityBase> = Readonly<{
3738
*/
3839
getEntityChildren?: (entity: T) => readonly T[];
3940

41+
/**
42+
* An optional function that returns the parent of a given entity.
43+
*/
44+
getEntityParent?: (entity: T) => Nullable<T>;
45+
4046
/**
4147
* A function that returns the display name for a given entity.
4248
*/
@@ -122,7 +128,7 @@ type TreeItemData =
122128
type: "entity";
123129
entity: EntityBase;
124130
depth: number;
125-
parent: Nullable<TreeItemValue>;
131+
parent: TreeItemValue;
126132
hasChildren: boolean;
127133
title: string;
128134
icon?: ComponentType<{ entity: EntityBase }>;
@@ -189,11 +195,17 @@ export const SceneExplorer: FunctionComponent<{
189195
}> = (props) => {
190196
const classes = useStyles();
191197

192-
const { sections, commands, scene, selectedEntity, setSelectedEntity } = props;
198+
const { sections, commands, scene, selectedEntity } = props;
193199

194200
const [openItems, setOpenItems] = useState(new Set<TreeItemValue>());
195-
196201
const [sceneVersion, setSceneVersion] = useState(0);
202+
const scrollViewRef = useRef<ScrollToInterface>(null);
203+
// We only want to scroll to the selected item if it was externally selected (outside of SceneExplorer).
204+
const previousSelectedEntity = useRef(selectedEntity);
205+
const setSelectedEntity = (entity: unknown) => {
206+
previousSelectedEntity.current = entity;
207+
props.setSelectedEntity?.(entity);
208+
};
197209

198210
// For the filter, we should maybe to the traversal but use onAfterNode so that if the filter matches, we make sure to include the full parent chain.
199211
// Then just reverse the array of nodes before returning it.
@@ -235,31 +247,33 @@ export const SceneExplorer: FunctionComponent<{
235247

236248
const visibleItems = useMemo(() => {
237249
const visibleItems: TreeItemData[] = [];
238-
const entityParents = new Map<EntityBase, EntityBase>();
250+
const entityParents = new Map<number, TreeItemValue>();
239251

240252
visibleItems.push({
241253
type: "scene",
242254
scene: scene,
243255
});
244256

245257
for (const section of sections) {
258+
const rootEntities = section.getRootEntities(scene);
259+
246260
visibleItems.push({
247261
type: "section",
248262
sectionName: section.displayName,
249-
hasChildren: section.getRootEntities(scene).length > 0,
263+
hasChildren: rootEntities.length > 0,
250264
});
251265

252266
if (openItems.has(section.displayName)) {
253267
let depth = 1;
254268
TraverseGraph(
255-
section.getRootEntities(scene),
269+
rootEntities,
256270
(entity) => {
257271
if (openItems.has(entity.uniqueId) && section.getEntityChildren) {
258272
const children = section.getEntityChildren(entity);
259273
for (const child of children) {
260-
entityParents.set(child, entity);
274+
entityParents.set(child.uniqueId, entity.uniqueId);
261275
}
262-
return section.getEntityChildren(entity);
276+
return children;
263277
}
264278
return null;
265279
},
@@ -269,7 +283,7 @@ export const SceneExplorer: FunctionComponent<{
269283
type: "entity",
270284
entity,
271285
depth,
272-
parent: entityParents.get(entity)?.uniqueId ?? section.displayName,
286+
parent: entityParents.get(entity.uniqueId) ?? section.displayName,
273287
hasChildren: !!section.getEntityChildren && section.getEntityChildren(entity).length > 0,
274288
title: section.getEntityDisplayName(entity),
275289
icon: section.entityIcon,
@@ -285,6 +299,62 @@ export const SceneExplorer: FunctionComponent<{
285299
return visibleItems;
286300
}, [scene, sceneVersion, sections, openItems, itemsFilter]);
287301

302+
const getParentStack = useCallback(
303+
(entity: EntityBase) => {
304+
const parentStack: TreeItemValue[] = [];
305+
306+
for (const section of sections) {
307+
for (let parent = section.getEntityParent?.(entity); parent; parent = section.getEntityParent?.(parent)) {
308+
parentStack.push(parent.uniqueId);
309+
}
310+
311+
if (parentStack.length > 0 || section.getRootEntities(scene).includes(entity)) {
312+
parentStack.push(section.displayName);
313+
break;
314+
}
315+
}
316+
317+
return parentStack;
318+
},
319+
[scene, openItems, sections]
320+
);
321+
322+
// We only want the effect below to execute when the selectedEntity changes, so we use a ref to keep the latest version of getParentStack.
323+
const getParentStackRef = useRef(getParentStack);
324+
getParentStackRef.current = getParentStack;
325+
326+
const [isScrollToPending, setIsScrollToPending] = useState(false);
327+
328+
useEffect(() => {
329+
if (selectedEntity && selectedEntity !== previousSelectedEntity.current) {
330+
const entity = selectedEntity as EntityBase;
331+
if (entity.uniqueId != undefined) {
332+
const parentStack = getParentStackRef.current(entity);
333+
if (parentStack.length > 0) {
334+
const newOpenItems = new Set<TreeItemValue>(openItems);
335+
for (const parent of parentStack) {
336+
newOpenItems.add(parent);
337+
}
338+
setOpenItems(newOpenItems);
339+
setIsScrollToPending(true);
340+
}
341+
}
342+
}
343+
344+
previousSelectedEntity.current = selectedEntity;
345+
}, [selectedEntity, setOpenItems, setIsScrollToPending]);
346+
347+
// We need to wait for a render to complete before we can scroll to the item, hence the isScrollToPending.
348+
useEffect(() => {
349+
if (isScrollToPending) {
350+
const selectedItemIndex = visibleItems.findIndex((item) => item.type === "entity" && item.entity === selectedEntity);
351+
if (selectedItemIndex >= 0 && scrollViewRef.current) {
352+
scrollViewRef.current.scrollTo(selectedItemIndex, "smooth");
353+
setIsScrollToPending(false);
354+
}
355+
}
356+
}, [isScrollToPending, selectedEntity, visibleItems]);
357+
288358
const onOpenChange = useCallback(
289359
(event: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
290360
// This makes it so we only consider a click on the chevron to be expanding/collapsing an item, not clicking anywhere on the item.
@@ -298,7 +368,7 @@ export const SceneExplorer: FunctionComponent<{
298368
return (
299369
<div className={classes.rootDiv}>
300370
<FlatTree className={classes.tree} openItems={openItems} onOpenChange={onOpenChange} aria-label="Scene Explorer Tree">
301-
<VirtualizerScrollView numItems={visibleItems.length} itemSize={32} container={{ style: { overflowX: "hidden" } }}>
371+
<VirtualizerScrollView imperativeRef={scrollViewRef} numItems={visibleItems.length} itemSize={32} container={{ style: { overflowX: "hidden" } }}>
302372
{(index: number) => {
303373
const item = visibleItems[index];
304374

packages/dev/inspector-v2/src/inspector.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ import { useEffect, useRef } from "react";
1212
import { BuiltInsExtensionFeed } from "./extensibility/builtInsExtensionFeed";
1313
import { MakeModularTool } from "./modularTool";
1414
import { DebugServiceDefinition } from "./services/panes/debugService";
15+
import { CommonPropertiesServiceDefinition } from "./services/panes/properties/commonPropertiesService";
16+
import { MeshPropertiesServiceDefinition } from "./services/panes/properties/meshPropertiesService";
17+
import { NodePropertiesServiceDefinition } from "./services/panes/properties/nodePropertiesService";
18+
import { PropertiesServiceDefinition } from "./services/panes/properties/propertiesService";
19+
import { TransformNodePropertiesServiceDefinition } from "./services/panes/properties/transformNodePropertiesService";
1520
import { MaterialExplorerServiceDefinition } from "./services/panes/scene/materialExplorerService";
1621
import { NodeHierarchyServiceDefinition } from "./services/panes/scene/nodeExplorerService";
1722
import { SceneExplorerServiceDefinition } from "./services/panes/scene/sceneExplorerService";
1823
import { TextureHierarchyServiceDefinition } from "./services/panes/scene/texturesExplorerService";
19-
import { CommonPropertiesServiceDefinition } from "./services/panes/properties/commonPropertiesService";
20-
import { MeshPropertiesServiceDefinition } from "./services/panes/properties/meshPropertiesService";
21-
import { TransformNodePropertiesServiceDefinition } from "./services/panes/properties/transformNodePropertiesService";
22-
import { PropertiesServiceDefinition } from "./services/panes/properties/propertiesService";
2324
import { SettingsServiceDefinition } from "./services/panes/settingsService";
2425
import { StatsServiceDefinition } from "./services/panes/statsService";
2526
import { ToolsServiceDefinition } from "./services/panes/toolsService";
@@ -173,6 +174,7 @@ function _ShowInspector(scene: Nullable<Scene>, options: Partial<IInspectorOptio
173174
// Properties pane tab and related services.
174175
PropertiesServiceDefinition,
175176
CommonPropertiesServiceDefinition,
177+
NodePropertiesServiceDefinition,
176178
MeshPropertiesServiceDefinition,
177179
TransformNodePropertiesServiceDefinition,
178180

packages/dev/inspector-v2/src/services/panes/properties/meshPropertiesService.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const MeshPropertiesServiceDefinition: ServiceDefinition<[], [IProperties
3333
// "GENERAL" section.
3434
{
3535
section: GeneralPropertiesSectionIdentity,
36-
order: 1,
36+
order: 2,
3737
component: ({ context }) => <MeshGeneralProperties mesh={context} selectionService={selectionService} />,
3838
},
3939

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
2+
import type { IPropertiesService } from "./propertiesService";
3+
import type { ISelectionService } from "../../selectionService";
4+
5+
import { Node } from "core/node";
6+
7+
import { GeneralPropertiesSectionIdentity } from "./commonPropertiesService";
8+
import { PropertiesServiceIdentity } from "./propertiesService";
9+
import { SelectionServiceIdentity } from "../../selectionService";
10+
import { NodeGeneralProperties } from "../../../components/properties/nodeGeneralProperties";
11+
12+
export const NodePropertiesServiceDefinition: ServiceDefinition<[], [IPropertiesService, ISelectionService]> = {
13+
friendlyName: "Transform Node Properties",
14+
consumes: [PropertiesServiceIdentity, SelectionServiceIdentity],
15+
factory: (propertiesService, selectionService) => {
16+
const contentRegistration = propertiesService.addSectionContent({
17+
key: "Node Properties",
18+
predicate: (entity: unknown) => entity instanceof Node,
19+
content: [
20+
// "GENERAL" section.
21+
{
22+
section: GeneralPropertiesSectionIdentity,
23+
order: 1,
24+
component: ({ context }) => <NodeGeneralProperties node={context} setSelectedEntity={(entity) => (selectionService.selectedEntity = entity)} />,
25+
},
26+
],
27+
});
28+
29+
return {
30+
dispose: () => {
31+
contentRegistration.dispose();
32+
},
33+
};
34+
},
35+
};

packages/dev/inspector-v2/src/services/panes/scene/nodeExplorerService.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const NodeHierarchyServiceDefinition: ServiceDefinition<[], [ISceneExplor
1818
order: 0,
1919
getRootEntities: (scene) => scene.rootNodes,
2020
getEntityChildren: (node) => node.getChildren(),
21+
getEntityParent: (node) => node.parent,
2122
getEntityDisplayName: (node) => node.name,
2223
entityIcon: ({ entity: node }) =>
2324
node instanceof AbstractMesh ? (

0 commit comments

Comments
 (0)