Skip to content

Commit ab8dd7e

Browse files
authored
Inspector v2: Add scene node to scene explorer (#16777)
Two things in this PR: 1. Add a top level scene node to scene explorer. 2. Replace the scene explorer section "watch" function with just an array of observables. This is what I originally wanted to do, but it was hard to do because of the lack of covariance in Observable<T>. It's now easy to do with the covariant IReadonlyObservable<T>. ![image](https://github.com/user-attachments/assets/ba8b106e-3afa-4907-8b03-0ec6f8506362)
1 parent 8efa639 commit ab8dd7e

File tree

5 files changed

+78
-96
lines changed

5 files changed

+78
-96
lines changed

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

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// eslint-disable-next-line import/no-internal-modules
2-
import type { IDisposable, Nullable, Scene } from "core/index";
2+
import type { IReadonlyObservable, Nullable, Scene } from "core/index";
33

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

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

1011
import { useCallback, useEffect, useMemo, useState } from "react";
1112
import { TraverseGraph } from "../../misc/graphUtils";
@@ -47,9 +48,14 @@ export type SceneExplorerSection<T extends EntityBase> = Readonly<{
4748
entityIcon?: ComponentType<{ entity: T }>;
4849

4950
/**
50-
* A function that watches for changes in the scene and calls the provided callbacks when entities are added or removed.
51+
* A function that returns an array of observables for when entities are added to the scene.
5152
*/
52-
watch: (scene: Scene, onAdded: (entity: T) => void, onRemoved: (entity: T) => void) => IDisposable;
53+
getEntityAddedObservables: (scene: Scene) => readonly IReadonlyObservable<T>[];
54+
55+
/**
56+
* A function that returns an array of observables for when entities are removed from the scene.
57+
*/
58+
getEntityRemovedObservables: (scene: Scene) => readonly IReadonlyObservable<T>[];
5359
}>;
5460

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

108114
type TreeItemData =
115+
| { type: "scene"; scene: Scene }
109116
| {
110117
type: "section";
111118
sectionName: string;
@@ -130,11 +137,14 @@ const useStyles = makeStyles({
130137
flexDirection: "column",
131138
},
132139
tree: {
133-
margin: tokens.spacingHorizontalXS,
140+
margin: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
134141
rowGap: 0,
135142
overflow: "hidden",
136143
flex: 1,
137144
},
145+
sceneTreeItemLayout: {
146+
padding: 0,
147+
},
138148
});
139149

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

213-
const watchTokens = sections.map((section) => section.watch(scene, onSceneItemAdded, onSceneItemRemoved));
223+
const addObservers = sections.flatMap((section) => section.getEntityAddedObservables(scene).map((observable) => observable.add(onSceneItemAdded)));
224+
const removeObservers = sections.flatMap((section) => section.getEntityRemovedObservables(scene).map((observable) => observable.add(onSceneItemRemoved)));
214225

215226
return () => {
216-
for (const token of watchTokens) {
217-
token.dispose();
227+
for (const observer of addObservers) {
228+
observer.remove();
229+
}
230+
for (const observer of removeObservers) {
231+
observer.remove();
218232
}
219233
};
220234
}, [sections, openItems]);
@@ -223,6 +237,11 @@ export const SceneExplorer: FunctionComponent<{
223237
const visibleItems: TreeItemData[] = [];
224238
const entityParents = new Map<EntityBase, EntityBase>();
225239

240+
visibleItems.push({
241+
type: "scene",
242+
scene: scene,
243+
});
244+
226245
for (const section of sections) {
227246
visibleItems.push({
228247
type: "section",
@@ -283,7 +302,30 @@ export const SceneExplorer: FunctionComponent<{
283302
{(index: number) => {
284303
const item = visibleItems[index];
285304

286-
if (item.type === "section") {
305+
if (item.type === "scene") {
306+
return (
307+
<FlatTreeItem
308+
key="scene"
309+
value="scene"
310+
itemType="leaf"
311+
parentValue={undefined}
312+
aria-level={1}
313+
aria-setsize={1}
314+
aria-posinset={1}
315+
onClick={() => setSelectedEntity?.(scene)}
316+
>
317+
<TreeItemLayout
318+
iconBefore={<MoviesAndTvRegular />}
319+
className={classes.sceneTreeItemLayout}
320+
style={scene === selectedEntity ? { backgroundColor: tokens.colorNeutralBackground1Selected } : undefined}
321+
>
322+
<Body1Strong wrap={false} truncate>
323+
Scene
324+
</Body1Strong>
325+
</TreeItemLayout>
326+
</FlatTreeItem>
327+
);
328+
} else if (item.type === "section") {
287329
return (
288330
<FlatTreeItem
289331
key={item.sectionName}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
22
import type { IPropertiesService } from "./propertiesService";
33

4-
import { PropertiesServiceIdentity } from "./propertiesService";
4+
import { Scene } from "core/scene";
5+
56
import { CommonGeneralProperties } from "../../../components/properties/commonGeneralProperties";
7+
import { PropertiesServiceIdentity } from "./propertiesService";
68

79
type CommonEntity = {
810
id?: number;
@@ -25,6 +27,11 @@ export const CommonPropertiesServiceDefinition: ServiceDefinition<[], [IProperti
2527
const contentRegistration = propertiesService.addSectionContent({
2628
key: "Common Properties",
2729
predicate: (entity: unknown): entity is CommonEntity => {
30+
// Common properties are not useful for the scene.
31+
if (entity instanceof Scene) {
32+
return false;
33+
}
34+
2835
const commonEntity = entity as CommonEntity;
2936
return commonEntity.id !== undefined || commonEntity.name !== undefined || commonEntity.uniqueId !== undefined || commonEntity.getClassName !== undefined;
3037
},

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

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
// eslint-disable-next-line import/no-internal-modules
2-
import type { Observer } from "core/index";
3-
41
import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
52
import type { ISceneExplorerService } from "./sceneExplorerService";
63

@@ -18,29 +15,8 @@ export const MaterialExplorerServiceDefinition: ServiceDefinition<[], [ISceneExp
1815
getRootEntities: (scene) => scene.materials,
1916
getEntityDisplayName: (material) => material.name,
2017
entityIcon: () => <PaintBrushRegular />,
21-
watch: (scene, onAdded, onRemoved) => {
22-
const observers: Observer<any>[] = [];
23-
24-
observers.push(
25-
scene.onNewMaterialAddedObservable.add((material) => {
26-
onAdded(material);
27-
})
28-
);
29-
30-
observers.push(
31-
scene.onMaterialRemovedObservable.add((material) => {
32-
onRemoved(material);
33-
})
34-
);
35-
36-
return {
37-
dispose: () => {
38-
for (const observer of observers) {
39-
observer.remove();
40-
}
41-
},
42-
};
43-
},
18+
getEntityAddedObservables: (scene) => [scene.onNewMaterialAddedObservable],
19+
getEntityRemovedObservables: (scene) => [scene.onMaterialRemovedObservable],
4420
});
4521

4622
return {

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

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
// eslint-disable-next-line import/no-internal-modules
2-
import type { IReadonlyObservable, Node, Scene } from "core/index";
3-
41
import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
52
import type { ISceneExplorerService } from "./sceneExplorerService";
63

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

96
import { Camera } from "core/Cameras/camera";
107
import { Light } from "core/Lights/light";
@@ -34,44 +31,28 @@ export const NodeHierarchyServiceDefinition: ServiceDefinition<[], [ISceneExplor
3431
) : (
3532
<></>
3633
),
37-
watch: (scene, onAdded, onRemoved) => {
38-
const observers = [
39-
...(
40-
[
41-
scene.onNewMeshAddedObservable,
42-
scene.onNewTransformNodeAddedObservable,
43-
scene.onNewCameraAddedObservable,
44-
scene.onNewLightAddedObservable,
45-
] as IReadonlyObservable<Node>[]
46-
).map((observable) => observable.add((node) => onAdded(node))),
47-
...(
48-
[
49-
scene.onMeshRemovedObservable,
50-
scene.onTransformNodeRemovedObservable,
51-
scene.onCameraRemovedObservable,
52-
scene.onLightRemovedObservable,
53-
] as IReadonlyObservable<Node>[]
54-
).map((observable) => observable.add((node) => onRemoved(node))),
55-
] as const;
56-
57-
return {
58-
dispose: () => {
59-
for (const observer of observers) {
60-
observer.remove();
61-
}
62-
},
63-
};
64-
},
34+
getEntityAddedObservables: (scene) => [
35+
scene.onNewMeshAddedObservable,
36+
scene.onNewTransformNodeAddedObservable,
37+
scene.onNewCameraAddedObservable,
38+
scene.onNewLightAddedObservable,
39+
],
40+
getEntityRemovedObservables: (scene) => [
41+
scene.onMeshRemovedObservable,
42+
scene.onTransformNodeRemovedObservable,
43+
scene.onCameraRemovedObservable,
44+
scene.onLightRemovedObservable,
45+
],
6546
});
6647

6748
const visibilityCommandRegistration = sceneExplorerService.addCommand({
6849
type: "toggle",
6950
order: 0,
7051
predicate: (entity: unknown): entity is AbstractMesh => entity instanceof AbstractMesh && entity.getTotalVertices() > 0,
71-
isEnabled: (scene: Scene, mesh: AbstractMesh) => {
52+
isEnabled: (scene, mesh) => {
7253
return mesh.isVisible;
7354
},
74-
setEnabled: (scene: Scene, mesh: AbstractMesh, enabled: boolean) => {
55+
setEnabled: (scene, mesh, enabled) => {
7556
mesh.isVisible = enabled;
7657
},
7758
displayName: "Show/Hide Mesh",

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

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
// eslint-disable-next-line import/no-internal-modules
2-
import type { Observer } from "core/index";
3-
41
import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
52
import type { ISceneExplorerService } from "./sceneExplorerService";
63

@@ -18,29 +15,8 @@ export const TextureHierarchyServiceDefinition: ServiceDefinition<[], [ISceneExp
1815
getRootEntities: (scene) => scene.textures,
1916
getEntityDisplayName: (texture) => texture.displayName || texture.name || `Unnamed Texture (${texture.uniqueId})`,
2017
entityIcon: () => <ImageRegular />,
21-
watch: (scene, onAdded, onRemoved) => {
22-
const observers: Observer<any>[] = [];
23-
24-
observers.push(
25-
scene.onNewTextureAddedObservable.add((texture) => {
26-
onAdded(texture);
27-
})
28-
);
29-
30-
observers.push(
31-
scene.onTextureRemovedObservable.add((texture) => {
32-
onRemoved(texture);
33-
})
34-
);
35-
36-
return {
37-
dispose: () => {
38-
for (const observer of observers) {
39-
scene.onNewTextureAddedObservable.remove(observer);
40-
}
41-
},
42-
};
43-
},
18+
getEntityAddedObservables: (scene) => [scene.onNewTextureAddedObservable],
19+
getEntityRemovedObservables: (scene) => [scene.onTextureRemovedObservable],
4420
});
4521

4622
return {

0 commit comments

Comments
 (0)