diff --git a/packages/dev/inspector-v2/src/components/properties/nodeGeneralProperties.tsx b/packages/dev/inspector-v2/src/components/properties/nodeGeneralProperties.tsx new file mode 100644 index 00000000000..4c27eb10225 --- /dev/null +++ b/packages/dev/inspector-v2/src/components/properties/nodeGeneralProperties.tsx @@ -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 && setSelectedEntity(parent)} />}; +}; diff --git a/packages/dev/inspector-v2/src/components/properties/transformNodeTransformProperties.tsx b/packages/dev/inspector-v2/src/components/properties/transformNodeTransformProperties.tsx index f57fcc810a0..bf554066091 100644 --- a/packages/dev/inspector-v2/src/components/properties/transformNodeTransformProperties.tsx +++ b/packages/dev/inspector-v2/src/components/properties/transformNodeTransformProperties.tsx @@ -22,17 +22,17 @@ function useVector3Property>(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 ( <> - (transformNode.position = val)} /> - (transformNode.scaling = val)} /> - (transformNode.scaling = val)} /> + (node.position = val)} /> + (node.scaling = val)} /> + (node.scaling = val)} /> ); }; diff --git a/packages/dev/inspector-v2/src/components/scene/sceneExplorer.tsx b/packages/dev/inspector-v2/src/components/scene/sceneExplorer.tsx index cf454262e47..f3b9c3741d8 100644 --- a/packages/dev/inspector-v2/src/components/scene/sceneExplorer.tsx +++ b/packages/dev/inspector-v2/src/components/scene/sceneExplorer.tsx @@ -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<{ @@ -37,6 +38,11 @@ export type SceneExplorerSection = Readonly<{ */ getEntityChildren?: (entity: T) => readonly T[]; + /** + * An optional function that returns the parent of a given entity. + */ + getEntityParent?: (entity: T) => Nullable; + /** * A function that returns the display name for a given entity. */ @@ -122,7 +128,7 @@ type TreeItemData = type: "entity"; entity: EntityBase; depth: number; - parent: Nullable; + parent: TreeItemValue; hasChildren: boolean; title: string; icon?: ComponentType<{ entity: EntityBase }>; @@ -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()); - const [sceneVersion, setSceneVersion] = useState(0); + const scrollViewRef = useRef(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. @@ -235,7 +247,7 @@ export const SceneExplorer: FunctionComponent<{ const visibleItems = useMemo(() => { const visibleItems: TreeItemData[] = []; - const entityParents = new Map(); + const entityParents = new Map(); visibleItems.push({ type: "scene", @@ -243,23 +255,25 @@ export const SceneExplorer: FunctionComponent<{ }); 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; }, @@ -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, @@ -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(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. @@ -298,7 +368,7 @@ export const SceneExplorer: FunctionComponent<{ return (
- + {(index: number) => { const item = visibleItems[index]; diff --git a/packages/dev/inspector-v2/src/inspector.tsx b/packages/dev/inspector-v2/src/inspector.tsx index 362a402221a..38b36059d8d 100644 --- a/packages/dev/inspector-v2/src/inspector.tsx +++ b/packages/dev/inspector-v2/src/inspector.tsx @@ -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"; @@ -173,6 +174,7 @@ function _ShowInspector(scene: Nullable, options: Partial , }, diff --git a/packages/dev/inspector-v2/src/services/panes/properties/nodePropertiesService.tsx b/packages/dev/inspector-v2/src/services/panes/properties/nodePropertiesService.tsx new file mode 100644 index 00000000000..e5ae80de75e --- /dev/null +++ b/packages/dev/inspector-v2/src/services/panes/properties/nodePropertiesService.tsx @@ -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 }) => (selectionService.selectedEntity = entity)} />, + }, + ], + }); + + return { + dispose: () => { + contentRegistration.dispose(); + }, + }; + }, +}; diff --git a/packages/dev/inspector-v2/src/services/panes/scene/nodeExplorerService.tsx b/packages/dev/inspector-v2/src/services/panes/scene/nodeExplorerService.tsx index b80142ffe3f..4209ef08b7b 100644 --- a/packages/dev/inspector-v2/src/services/panes/scene/nodeExplorerService.tsx +++ b/packages/dev/inspector-v2/src/services/panes/scene/nodeExplorerService.tsx @@ -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 ? (