Skip to content

Inspector v2: Add scene node to scene explorer #16777

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
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
60 changes: 51 additions & 9 deletions packages/dev/inspector-v2/src/components/scene/sceneExplorer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// eslint-disable-next-line import/no-internal-modules
import type { IDisposable, Nullable, Scene } from "core/index";
import type { IReadonlyObservable, Nullable, Scene } from "core/index";

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

import { Body1, Body1Strong, Button, FlatTree, FlatTreeItem, makeStyles, tokens, ToggleButton, Tooltip, TreeItemLayout } from "@fluentui/react-components";
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 { TraverseGraph } from "../../misc/graphUtils";
Expand Down Expand Up @@ -47,9 +48,14 @@ export type SceneExplorerSection<T extends EntityBase> = Readonly<{
entityIcon?: ComponentType<{ entity: T }>;

/**
* A function that watches for changes in the scene and calls the provided callbacks when entities are added or removed.
* A function that returns an array of observables for when entities are added to the scene.
*/
watch: (scene: Scene, onAdded: (entity: T) => void, onRemoved: (entity: T) => void) => IDisposable;
getEntityAddedObservables: (scene: Scene) => readonly IReadonlyObservable<T>[];

/**
* A function that returns an array of observables for when entities are removed from the scene.
*/
getEntityRemovedObservables: (scene: Scene) => readonly IReadonlyObservable<T>[];
}>;

type EntityCommandBase<T extends EntityBase> = Readonly<{
Expand Down Expand Up @@ -106,6 +112,7 @@ type ToggleCommand<T extends EntityBase> = EntityCommandBase<T> &
export type SceneExplorerEntityCommand<T extends EntityBase> = ActionCommand<T> | ToggleCommand<T>;

type TreeItemData =
| { type: "scene"; scene: Scene }
| {
type: "section";
sectionName: string;
Expand All @@ -130,11 +137,14 @@ const useStyles = makeStyles({
flexDirection: "column",
},
tree: {
margin: tokens.spacingHorizontalXS,
margin: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
rowGap: 0,
overflow: "hidden",
flex: 1,
},
sceneTreeItemLayout: {
padding: 0,
},
});

const ActionCommand: FunctionComponent<{ command: ActionCommand<EntityBase>; entity: EntityBase; scene: Scene }> = (props) => {
Expand Down Expand Up @@ -210,11 +220,15 @@ export const SceneExplorer: FunctionComponent<{
}
};

const watchTokens = sections.map((section) => section.watch(scene, onSceneItemAdded, onSceneItemRemoved));
const addObservers = sections.flatMap((section) => section.getEntityAddedObservables(scene).map((observable) => observable.add(onSceneItemAdded)));
const removeObservers = sections.flatMap((section) => section.getEntityRemovedObservables(scene).map((observable) => observable.add(onSceneItemRemoved)));

return () => {
for (const token of watchTokens) {
token.dispose();
for (const observer of addObservers) {
observer.remove();
}
for (const observer of removeObservers) {
observer.remove();
}
};
}, [sections, openItems]);
Expand All @@ -223,6 +237,11 @@ export const SceneExplorer: FunctionComponent<{
const visibleItems: TreeItemData[] = [];
const entityParents = new Map<EntityBase, EntityBase>();

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

for (const section of sections) {
visibleItems.push({
type: "section",
Expand Down Expand Up @@ -283,7 +302,30 @@ export const SceneExplorer: FunctionComponent<{
{(index: number) => {
const item = visibleItems[index];

if (item.type === "section") {
if (item.type === "scene") {
return (
<FlatTreeItem
key="scene"
value="scene"
itemType="leaf"
parentValue={undefined}
aria-level={1}
aria-setsize={1}
aria-posinset={1}
onClick={() => setSelectedEntity?.(scene)}
>
<TreeItemLayout
iconBefore={<MoviesAndTvRegular />}
className={classes.sceneTreeItemLayout}
style={scene === selectedEntity ? { backgroundColor: tokens.colorNeutralBackground1Selected } : undefined}
>
<Body1Strong wrap={false} truncate>
Scene
</Body1Strong>
</TreeItemLayout>
</FlatTreeItem>
);
} else if (item.type === "section") {
return (
<FlatTreeItem
key={item.sectionName}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
import type { IPropertiesService } from "./propertiesService";

import { PropertiesServiceIdentity } from "./propertiesService";
import { Scene } from "core/scene";

import { CommonGeneralProperties } from "../../../components/properties/commonGeneralProperties";
import { PropertiesServiceIdentity } from "./propertiesService";

type CommonEntity = {
id?: number;
Expand All @@ -25,6 +27,11 @@ export const CommonPropertiesServiceDefinition: ServiceDefinition<[], [IProperti
const contentRegistration = propertiesService.addSectionContent({
key: "Common Properties",
predicate: (entity: unknown): entity is CommonEntity => {
// Common properties are not useful for the scene.
if (entity instanceof Scene) {
return false;
}

const commonEntity = entity as CommonEntity;
return commonEntity.id !== undefined || commonEntity.name !== undefined || commonEntity.uniqueId !== undefined || commonEntity.getClassName !== undefined;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// eslint-disable-next-line import/no-internal-modules
import type { Observer } from "core/index";

import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
import type { ISceneExplorerService } from "./sceneExplorerService";

Expand All @@ -18,29 +15,8 @@ export const MaterialExplorerServiceDefinition: ServiceDefinition<[], [ISceneExp
getRootEntities: (scene) => scene.materials,
getEntityDisplayName: (material) => material.name,
entityIcon: () => <PaintBrushRegular />,
watch: (scene, onAdded, onRemoved) => {
const observers: Observer<any>[] = [];

observers.push(
scene.onNewMaterialAddedObservable.add((material) => {
onAdded(material);
})
);

observers.push(
scene.onMaterialRemovedObservable.add((material) => {
onRemoved(material);
})
);

return {
dispose: () => {
for (const observer of observers) {
observer.remove();
}
},
};
},
getEntityAddedObservables: (scene) => [scene.onNewMaterialAddedObservable],
getEntityRemovedObservables: (scene) => [scene.onMaterialRemovedObservable],
});

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// eslint-disable-next-line import/no-internal-modules
import type { IReadonlyObservable, Node, Scene } from "core/index";

import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
import type { ISceneExplorerService } from "./sceneExplorerService";

import { BoxRegular, BranchRegular, CameraRegular, EyeRegular, EyeOffRegular, LightbulbRegular } from "@fluentui/react-icons";
import { BoxRegular, BranchRegular, CameraRegular, EyeOffRegular, EyeRegular, LightbulbRegular } from "@fluentui/react-icons";

import { Camera } from "core/Cameras/camera";
import { Light } from "core/Lights/light";
Expand Down Expand Up @@ -34,44 +31,28 @@ export const NodeHierarchyServiceDefinition: ServiceDefinition<[], [ISceneExplor
) : (
<></>
),
watch: (scene, onAdded, onRemoved) => {
const observers = [
...(
[
scene.onNewMeshAddedObservable,
scene.onNewTransformNodeAddedObservable,
scene.onNewCameraAddedObservable,
scene.onNewLightAddedObservable,
] as IReadonlyObservable<Node>[]
).map((observable) => observable.add((node) => onAdded(node))),
...(
[
scene.onMeshRemovedObservable,
scene.onTransformNodeRemovedObservable,
scene.onCameraRemovedObservable,
scene.onLightRemovedObservable,
] as IReadonlyObservable<Node>[]
).map((observable) => observable.add((node) => onRemoved(node))),
] as const;

return {
dispose: () => {
for (const observer of observers) {
observer.remove();
}
},
};
},
getEntityAddedObservables: (scene) => [
scene.onNewMeshAddedObservable,
scene.onNewTransformNodeAddedObservable,
scene.onNewCameraAddedObservable,
scene.onNewLightAddedObservable,
],
getEntityRemovedObservables: (scene) => [
scene.onMeshRemovedObservable,
scene.onTransformNodeRemovedObservable,
scene.onCameraRemovedObservable,
scene.onLightRemovedObservable,
],
});

const visibilityCommandRegistration = sceneExplorerService.addCommand({
type: "toggle",
order: 0,
predicate: (entity: unknown): entity is AbstractMesh => entity instanceof AbstractMesh && entity.getTotalVertices() > 0,
isEnabled: (scene: Scene, mesh: AbstractMesh) => {
isEnabled: (scene, mesh) => {
return mesh.isVisible;
},
setEnabled: (scene: Scene, mesh: AbstractMesh, enabled: boolean) => {
setEnabled: (scene, mesh, enabled) => {
mesh.isVisible = enabled;
},
displayName: "Show/Hide Mesh",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// eslint-disable-next-line import/no-internal-modules
import type { Observer } from "core/index";

import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
import type { ISceneExplorerService } from "./sceneExplorerService";

Expand All @@ -18,29 +15,8 @@ export const TextureHierarchyServiceDefinition: ServiceDefinition<[], [ISceneExp
getRootEntities: (scene) => scene.textures,
getEntityDisplayName: (texture) => texture.displayName || texture.name || `Unnamed Texture (${texture.uniqueId})`,
entityIcon: () => <ImageRegular />,
watch: (scene, onAdded, onRemoved) => {
const observers: Observer<any>[] = [];

observers.push(
scene.onNewTextureAddedObservable.add((texture) => {
onAdded(texture);
})
);

observers.push(
scene.onTextureRemovedObservable.add((texture) => {
onRemoved(texture);
})
);

return {
dispose: () => {
for (const observer of observers) {
scene.onNewTextureAddedObservable.remove(observer);
}
},
};
},
getEntityAddedObservables: (scene) => [scene.onNewTextureAddedObservable],
getEntityRemovedObservables: (scene) => [scene.onTextureRemovedObservable],
});

return {
Expand Down
Loading