Skip to content

Commit 9f9e0b7

Browse files
authored
Inspector v2: Stub out stats pane (#16755)
In this PR, I'm stubbing out a new stats pane, which includes: 1. Factoring out an `AccordionPane` react component from the `PropertiesPane` component. The idea is that we will have several panes that will have the same basic accordion UI, and each can support the same type of extensibility as properties. 2. `PropertiesPane`/`PropertiesService` are slightly more complicated that the other panes in that the "context" of the pane can have different types (e.g. a Mesh or a Light). Given this, I kept the predicate stuff specific to Properties. Other panes like Stats can have a single type that is always the same (like Scene) and be a little simpler. 3. Added a new `StatsPane`/`StatsService` that otherwise follow the same model as properties. One other difference that I am trying with `StatsService` is to not bother having separate service definitions for the default/built-in content of the status pane. Instead it is defined directly in the `StatsService`, but new service definitions could be defined to extend the default content of the stats pane. This was based off a suggestion @deltakosh had in the original inspector v2 PR. ![image](https://github.com/user-attachments/assets/4c177ec7-e988-4eb5-8875-b423d93926c1)
1 parent b01aad6 commit 9f9e0b7

File tree

14 files changed

+426
-161
lines changed

14 files changed

+426
-161
lines changed

packages/dev/core/src/Instrumentation/engineInstrumentation.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ export class EngineInstrumentation implements IDisposable {
1919
private _onBeforeShaderCompilationObserver: Nullable<Observer<AbstractEngine>> = null;
2020
private _onAfterShaderCompilationObserver: Nullable<Observer<AbstractEngine>> = null;
2121

22+
private _disposed = false;
23+
2224
// Properties
2325
/**
2426
* Gets the perf counter used for GPU frame time
2527
*/
26-
public get gpuFrameTimeCounter(): Nullable<PerfCounter> {
28+
public get gpuFrameTimeCounter(): PerfCounter {
2729
return this.engine.getGPUFrameTimeCounter();
2830
}
2931

@@ -104,6 +106,10 @@ export class EngineInstrumentation implements IDisposable {
104106
* Dispose and release associated resources.
105107
*/
106108
public dispose() {
109+
if (this._disposed) {
110+
return;
111+
}
112+
107113
this.engine.onBeginFrameObservable.remove(this._onBeginFrameObserver);
108114
this._onBeginFrameObserver = null;
109115

@@ -117,5 +123,6 @@ export class EngineInstrumentation implements IDisposable {
117123
this._onAfterShaderCompilationObserver = null;
118124

119125
(<any>this.engine) = null;
126+
this._disposed = true;
120127
}
121128
}

packages/dev/core/src/Instrumentation/sceneInstrumentation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export class SceneInstrumentation implements IDisposable {
6666
private _onBeforeCameraRenderObserver: Nullable<Observer<Camera>> = null;
6767
private _onAfterCameraRenderObserver: Nullable<Observer<Camera>> = null;
6868

69+
private _disposed = false;
70+
6971
// Properties
7072
/**
7173
* Gets the perf counter used for active meshes evaluation time
@@ -551,6 +553,10 @@ export class SceneInstrumentation implements IDisposable {
551553
* Dispose and release associated resources.
552554
*/
553555
public dispose() {
556+
if (this._disposed) {
557+
return;
558+
}
559+
554560
this.scene.onAfterRenderObservable.remove(this._onAfterRenderObserver);
555561
this._onAfterRenderObserver = null;
556562

@@ -611,5 +617,6 @@ export class SceneInstrumentation implements IDisposable {
611617
this._onAfterCameraRenderObserver = null;
612618

613619
(<any>this.scene) = null;
620+
this._disposed = true;
614621
}
615622
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// eslint-disable-next-line import/no-internal-modules
2+
3+
import { Accordion, AccordionHeader, AccordionItem, AccordionPanel, makeStyles, Subtitle1, tokens } from "@fluentui/react-components";
4+
import { useMemo, useState, type ComponentType } from "react";
5+
6+
export type AccordionSection = Readonly<{
7+
/**
8+
* A unique identity for the section, which can be referenced by section content.
9+
*/
10+
identity: symbol;
11+
12+
/**
13+
* An optional order for the section, relative to other sections.
14+
* Defaults to 0.
15+
*/
16+
order?: number;
17+
18+
/**
19+
* An optional flag indicating whether the section should be collapsed by default.
20+
* Defaults to false.
21+
*/
22+
collapseByDefault?: boolean;
23+
}>;
24+
25+
export type AccordionSectionContent<ContextT> = Readonly<{
26+
/**
27+
* A unique key for the the content.
28+
*/
29+
key: string;
30+
31+
/**
32+
* The content that is added to individual sections.
33+
*/
34+
content: readonly Readonly<{
35+
/**
36+
* The section this content belongs to.
37+
*/
38+
section: symbol;
39+
40+
/**
41+
* An optional order for the content within the section.
42+
* Defaults to 0.
43+
*/
44+
order?: number;
45+
46+
/**
47+
* The React component that will be rendered for this content.
48+
*/
49+
component: ComponentType<{ context: ContextT }>;
50+
}>[];
51+
}>;
52+
53+
// eslint-disable-next-line @typescript-eslint/naming-convention
54+
const useStyles = makeStyles({
55+
rootDiv: {
56+
flex: 1,
57+
overflow: "hidden",
58+
display: "flex",
59+
flexDirection: "column",
60+
},
61+
placeholderDiv: {
62+
padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalM}`,
63+
},
64+
accordion: {
65+
overflowY: "auto",
66+
paddingBottom: tokens.spacingVerticalM,
67+
},
68+
panelDiv: {
69+
display: "flex",
70+
flexDirection: "column",
71+
overflow: "hidden",
72+
},
73+
});
74+
75+
export function AccordionPane<ContextT = unknown>(props: {
76+
sections: readonly AccordionSection[];
77+
sectionContent: readonly AccordionSectionContent<ContextT>[];
78+
context: ContextT;
79+
}) {
80+
const classes = useStyles();
81+
82+
const { sections, sectionContent, context } = props;
83+
84+
const [version, setVersion] = useState(0);
85+
86+
const visibleSections = useMemo(() => {
87+
// When any of this state changes, we should re-render the Accordion so the defaultOpenItems are re-evaluated.
88+
setVersion((prev) => prev + 1);
89+
90+
if (!context) {
91+
return [];
92+
}
93+
94+
return sections
95+
.map((section) => {
96+
// Get a flat list of the section content, preserving the key so it can be used when each component for each section is rendered.
97+
const contentForSection = sectionContent
98+
.flatMap((entry) => entry.content.map((content) => ({ key: entry.key, ...content })))
99+
.filter((content) => content.section === section.identity);
100+
101+
// If there is no content for this section, we skip it.
102+
if (contentForSection.length === 0) {
103+
return null; // No content for this section
104+
}
105+
106+
// Sort the content for this section by order, defaulting to 0 if not specified.
107+
contentForSection.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
108+
109+
// Return the section with its identity, collapseByDefault flag, and the content components to render.
110+
return {
111+
identity: section.identity,
112+
collapseByDefault: section.collapseByDefault ?? false,
113+
components: contentForSection.map((content) => ({ key: content.key, component: content.component })),
114+
};
115+
})
116+
.filter((section) => section !== null);
117+
}, [sections, sectionContent, context]);
118+
119+
return (
120+
<div className={classes.rootDiv}>
121+
{visibleSections.length > 0 && (
122+
<Accordion
123+
key={version}
124+
className={classes.accordion}
125+
collapsible
126+
multiple
127+
defaultOpenItems={visibleSections.filter((section) => !section.collapseByDefault).map((section) => section.identity.description)}
128+
>
129+
{visibleSections.map((section) => {
130+
return (
131+
<AccordionItem key={section.identity.description} value={section.identity.description}>
132+
<AccordionHeader expandIconPosition="end">
133+
<Subtitle1>{section.identity.description}</Subtitle1>
134+
</AccordionHeader>
135+
<AccordionPanel>
136+
<div className={classes.panelDiv}>
137+
{section.components.map((component) => {
138+
return <component.component key={component.key} context={context} />;
139+
})}
140+
</div>
141+
</AccordionPanel>
142+
</AccordionItem>
143+
);
144+
})}
145+
</Accordion>
146+
)}
147+
</div>
148+
);
149+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type CommonEntity = {
77
getClassName?: () => string;
88
};
99

10-
export const CommonGeneralProperties: FunctionComponent<{ entity: CommonEntity }> = ({ entity: commonEntity }) => {
10+
export const CommonGeneralProperties: FunctionComponent<{ context: CommonEntity }> = ({ context: commonEntity }) => {
1111
return (
1212
// TODO: Use the new Fluent property line shared components.
1313
<>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { FunctionComponent } from "react";
66
import { useInterceptObservable } from "../../hooks/instrumentationHooks";
77
import { useObservableState } from "../../hooks/observableHooks";
88

9-
export const MeshAdvancedProperties: FunctionComponent<{ entity: AbstractMesh }> = ({ entity: mesh }) => {
9+
export const MeshAdvancedProperties: FunctionComponent<{ context: AbstractMesh }> = ({ context: mesh }) => {
1010
// There is no observable for computeBonesUsingShaders, so we use an interceptor to listen for changes.
1111
const computeBonesUsingShaders = useObservableState(() => mesh.computeBonesUsingShaders, useInterceptObservable("property", mesh, "computeBonesUsingShaders"));
1212

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { FunctionComponent } from "react";
55

66
import { useObservableState } from "../../hooks/observableHooks";
77

8-
export const MeshGeneralProperties: FunctionComponent<{ entity: AbstractMesh }> = ({ entity: mesh }) => {
8+
export const MeshGeneralProperties: FunctionComponent<{ context: AbstractMesh }> = ({ context: mesh }) => {
99
// Use the observable to keep keep state up-to-date and re-render the component when it changes.
1010
const material = useObservableState(() => mesh.material, mesh.onMaterialChangedObservable);
1111

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { AbstractMesh } from "core/index";
33

44
import type { FunctionComponent } from "react";
55

6-
export const MeshTransformProperties: FunctionComponent<{ entity: AbstractMesh }> = ({ entity: mesh }) => {
6+
export const MeshTransformProperties: FunctionComponent<{ context: AbstractMesh }> = ({ context: mesh }) => {
77
return (
88
// TODO: Use the new Fluent property line shared components.
99
<>

0 commit comments

Comments
 (0)