Skip to content

Inspector v2: Scene explorer expand tree for selected item and scroll to it if needed #16784

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// eslint-disable-next-line import/no-internal-modules
import type { Node } from "core/index";

import type { FunctionComponent } from "react";

import { LinkPropertyLine } from "shared-ui-components/fluent/hoc/linkPropertyLine";

import { useInterceptObservable } from "../../hooks/instrumentationHooks";
import { useObservableState } from "../../hooks/observableHooks";

export const NodeGeneralProperties: FunctionComponent<{ node: Node; setSelectedEntity: (entity: unknown) => void }> = (props) => {
const { node, setSelectedEntity } = props;

const parent = useObservableState(() => node.parent, useInterceptObservable("property", node, "parent"));

return <>{parent && <LinkPropertyLine key="Parent" label="Parent" description={`The parent of this node.`} value={parent.name} onLink={() => setSelectedEntity(parent)} />}</>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ function useVector3Property<T extends object, K extends Vector3Keys<T>>(target:
}

export const TransformNodeTransformProperties: FunctionComponent<{ node: TransformNode }> = (props) => {
const { node: transformNode } = props;
const { node } = props;

const position = useVector3Property(transformNode, "position");
const rotation = useVector3Property(transformNode, "rotation");
const scaling = useVector3Property(transformNode, "scaling");
const position = useVector3Property(node, "position");
const rotation = useVector3Property(node, "rotation");
const scaling = useVector3Property(node, "scaling");

return (
<>
<Vector3PropertyLine key="PositionTransform" label="Position" value={position} onChange={(val) => (transformNode.position = val)} />
<Vector3PropertyLine key="RotationTransform" label="Rotation" value={rotation} onChange={(val) => (transformNode.scaling = val)} />
<Vector3PropertyLine key="ScalingTransform" label="Scaling" value={scaling} onChange={(val) => (transformNode.scaling = val)} />
<Vector3PropertyLine key="PositionTransform" label="Position" value={position} onChange={(val) => (node.position = val)} />
<Vector3PropertyLine key="RotationTransform" label="Rotation" value={rotation} onChange={(val) => (node.scaling = val)} />
<Vector3PropertyLine key="ScalingTransform" label="Scaling" value={scaling} onChange={(val) => (node.scaling = val)} />
</>
);
};
92 changes: 81 additions & 11 deletions packages/dev/inspector-v2/src/components/scene/sceneExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import type { IReadonlyObservable, Nullable, Scene } from "core/index";

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

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

import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { TraverseGraph } from "../../misc/graphUtils";

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

/**
* An optional function that returns the parent of a given entity.
*/
getEntityParent?: (entity: T) => Nullable<T>;

/**
* A function that returns the display name for a given entity.
*/
Expand Down Expand Up @@ -122,7 +128,7 @@ type TreeItemData =
type: "entity";
entity: EntityBase;
depth: number;
parent: Nullable<TreeItemValue>;
parent: TreeItemValue;
hasChildren: boolean;
title: string;
icon?: ComponentType<{ entity: EntityBase }>;
Expand Down Expand Up @@ -189,11 +195,17 @@ export const SceneExplorer: FunctionComponent<{
}> = (props) => {
const classes = useStyles();

const { sections, commands, scene, selectedEntity, setSelectedEntity } = props;
const { sections, commands, scene, selectedEntity } = props;

const [openItems, setOpenItems] = useState(new Set<TreeItemValue>());

const [sceneVersion, setSceneVersion] = useState(0);
const scrollViewRef = useRef<ScrollToInterface>(null);
// We only want to scroll to the selected item if it was externally selected (outside of SceneExplorer).
const previousSelectedEntity = useRef(selectedEntity);
const setSelectedEntity = (entity: unknown) => {
previousSelectedEntity.current = entity;
props.setSelectedEntity?.(entity);
};

// 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.
// Then just reverse the array of nodes before returning it.
Expand Down Expand Up @@ -235,31 +247,33 @@ export const SceneExplorer: FunctionComponent<{

const visibleItems = useMemo(() => {
const visibleItems: TreeItemData[] = [];
const entityParents = new Map<EntityBase, EntityBase>();
const entityParents = new Map<number, TreeItemValue>();

visibleItems.push({
type: "scene",
scene: scene,
});

for (const section of sections) {
const rootEntities = section.getRootEntities(scene);

visibleItems.push({
type: "section",
sectionName: section.displayName,
hasChildren: section.getRootEntities(scene).length > 0,
hasChildren: rootEntities.length > 0,
});

if (openItems.has(section.displayName)) {
let depth = 1;
TraverseGraph(
section.getRootEntities(scene),
rootEntities,
(entity) => {
if (openItems.has(entity.uniqueId) && section.getEntityChildren) {
const children = section.getEntityChildren(entity);
for (const child of children) {
entityParents.set(child, entity);
entityParents.set(child.uniqueId, entity.uniqueId);
}
return section.getEntityChildren(entity);
return children;
}
return null;
},
Expand All @@ -269,7 +283,7 @@ export const SceneExplorer: FunctionComponent<{
type: "entity",
entity,
depth,
parent: entityParents.get(entity)?.uniqueId ?? section.displayName,
parent: entityParents.get(entity.uniqueId) ?? section.displayName,
hasChildren: !!section.getEntityChildren && section.getEntityChildren(entity).length > 0,
title: section.getEntityDisplayName(entity),
icon: section.entityIcon,
Expand All @@ -285,6 +299,62 @@ export const SceneExplorer: FunctionComponent<{
return visibleItems;
}, [scene, sceneVersion, sections, openItems, itemsFilter]);

const getParentStack = useCallback(
(entity: EntityBase) => {
const parentStack: TreeItemValue[] = [];

for (const section of sections) {
for (let parent = section.getEntityParent?.(entity); parent; parent = section.getEntityParent?.(parent)) {
parentStack.push(parent.uniqueId);
}

if (parentStack.length > 0 || section.getRootEntities(scene).includes(entity)) {
parentStack.push(section.displayName);
break;
}
}

return parentStack;
},
[scene, openItems, sections]
);

// We only want the effect below to execute when the selectedEntity changes, so we use a ref to keep the latest version of getParentStack.
const getParentStackRef = useRef(getParentStack);
getParentStackRef.current = getParentStack;

const [isScrollToPending, setIsScrollToPending] = useState(false);

useEffect(() => {
if (selectedEntity && selectedEntity !== previousSelectedEntity.current) {
const entity = selectedEntity as EntityBase;
if (entity.uniqueId != undefined) {
const parentStack = getParentStackRef.current(entity);
if (parentStack.length > 0) {
const newOpenItems = new Set<TreeItemValue>(openItems);
for (const parent of parentStack) {
newOpenItems.add(parent);
}
setOpenItems(newOpenItems);
setIsScrollToPending(true);
}
}
}

previousSelectedEntity.current = selectedEntity;
}, [selectedEntity, setOpenItems, setIsScrollToPending]);

// We need to wait for a render to complete before we can scroll to the item, hence the isScrollToPending.
useEffect(() => {
if (isScrollToPending) {
const selectedItemIndex = visibleItems.findIndex((item) => item.type === "entity" && item.entity === selectedEntity);
if (selectedItemIndex >= 0 && scrollViewRef.current) {
scrollViewRef.current.scrollTo(selectedItemIndex, "smooth");
setIsScrollToPending(false);
}
}
}, [isScrollToPending, selectedEntity, visibleItems]);

const onOpenChange = useCallback(
(event: TreeOpenChangeEvent, data: TreeOpenChangeData) => {
// This makes it so we only consider a click on the chevron to be expanding/collapsing an item, not clicking anywhere on the item.
Expand All @@ -298,7 +368,7 @@ export const SceneExplorer: FunctionComponent<{
return (
<div className={classes.rootDiv}>
<FlatTree className={classes.tree} openItems={openItems} onOpenChange={onOpenChange} aria-label="Scene Explorer Tree">
<VirtualizerScrollView numItems={visibleItems.length} itemSize={32} container={{ style: { overflowX: "hidden" } }}>
<VirtualizerScrollView imperativeRef={scrollViewRef} numItems={visibleItems.length} itemSize={32} container={{ style: { overflowX: "hidden" } }}>
{(index: number) => {
const item = visibleItems[index];

Expand Down
10 changes: 6 additions & 4 deletions packages/dev/inspector-v2/src/inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import { useEffect, useRef } from "react";
import { BuiltInsExtensionFeed } from "./extensibility/builtInsExtensionFeed";
import { MakeModularTool } from "./modularTool";
import { DebugServiceDefinition } from "./services/panes/debugService";
import { CommonPropertiesServiceDefinition } from "./services/panes/properties/commonPropertiesService";
import { MeshPropertiesServiceDefinition } from "./services/panes/properties/meshPropertiesService";
import { NodePropertiesServiceDefinition } from "./services/panes/properties/nodePropertiesService";
import { PropertiesServiceDefinition } from "./services/panes/properties/propertiesService";
import { TransformNodePropertiesServiceDefinition } from "./services/panes/properties/transformNodePropertiesService";
import { MaterialExplorerServiceDefinition } from "./services/panes/scene/materialExplorerService";
import { NodeHierarchyServiceDefinition } from "./services/panes/scene/nodeExplorerService";
import { SceneExplorerServiceDefinition } from "./services/panes/scene/sceneExplorerService";
import { TextureHierarchyServiceDefinition } from "./services/panes/scene/texturesExplorerService";
import { CommonPropertiesServiceDefinition } from "./services/panes/properties/commonPropertiesService";
import { MeshPropertiesServiceDefinition } from "./services/panes/properties/meshPropertiesService";
import { TransformNodePropertiesServiceDefinition } from "./services/panes/properties/transformNodePropertiesService";
import { PropertiesServiceDefinition } from "./services/panes/properties/propertiesService";
import { SettingsServiceDefinition } from "./services/panes/settingsService";
import { StatsServiceDefinition } from "./services/panes/statsService";
import { ToolsServiceDefinition } from "./services/panes/toolsService";
Expand Down Expand Up @@ -173,6 +174,7 @@ function _ShowInspector(scene: Nullable<Scene>, options: Partial<IInspectorOptio
// Properties pane tab and related services.
PropertiesServiceDefinition,
CommonPropertiesServiceDefinition,
NodePropertiesServiceDefinition,
MeshPropertiesServiceDefinition,
TransformNodePropertiesServiceDefinition,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const MeshPropertiesServiceDefinition: ServiceDefinition<[], [IProperties
// "GENERAL" section.
{
section: GeneralPropertiesSectionIdentity,
order: 1,
order: 2,
component: ({ context }) => <MeshGeneralProperties mesh={context} selectionService={selectionService} />,
},

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
import type { IPropertiesService } from "./propertiesService";
import type { ISelectionService } from "../../selectionService";

import { Node } from "core/node";

import { GeneralPropertiesSectionIdentity } from "./commonPropertiesService";
import { PropertiesServiceIdentity } from "./propertiesService";
import { SelectionServiceIdentity } from "../../selectionService";
import { NodeGeneralProperties } from "../../../components/properties/nodeGeneralProperties";

export const NodePropertiesServiceDefinition: ServiceDefinition<[], [IPropertiesService, ISelectionService]> = {
friendlyName: "Transform Node Properties",
consumes: [PropertiesServiceIdentity, SelectionServiceIdentity],
factory: (propertiesService, selectionService) => {
const contentRegistration = propertiesService.addSectionContent({
key: "Node Properties",
predicate: (entity: unknown) => entity instanceof Node,
content: [
// "GENERAL" section.
{
section: GeneralPropertiesSectionIdentity,
order: 1,
component: ({ context }) => <NodeGeneralProperties node={context} setSelectedEntity={(entity) => (selectionService.selectedEntity = entity)} />,
},
],
});

return {
dispose: () => {
contentRegistration.dispose();
},
};
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const NodeHierarchyServiceDefinition: ServiceDefinition<[], [ISceneExplor
order: 0,
getRootEntities: (scene) => scene.rootNodes,
getEntityChildren: (node) => node.getChildren(),
getEntityParent: (node) => node.parent,
getEntityDisplayName: (node) => node.name,
entityIcon: ({ entity: node }) =>
node instanceof AbstractMesh ? (
Expand Down