Skip to content

Commit 79d03e2

Browse files
authored
Inspector v2: Scene explorer light commands (#17000)
This change adds the enable/disable command and the gizmo command to lights in scene explorer. <img width="660" height="377" alt="image" src="https://github.com/user-attachments/assets/a62e10a8-07ed-41dd-80f8-b00f1f9a87c1" /> - Remove the previously added `useObservableRenderer`. Using `useObservableState` avoids the anti-pattern and with returning a tuple, it's still compact and one less concept to understand. - Fixed an issue where action commands were not updating, though I ended up not using action commands in this PR. - Factored out a helper function to create gizmo commands. - Added a command for toggling light gizmos. - Added a command to toggle the enabled state of lights (e.g. turn light on/off).
1 parent 7f986dc commit 79d03e2

File tree

3 files changed

+92
-67
lines changed

3 files changed

+92
-67
lines changed

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { VirtualizerScrollView } from "@fluentui/react-components/unstable";
99
import { FilterRegular, MoviesAndTvRegular } from "@fluentui/react-icons";
1010

1111
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12-
import { useObservableRenderer, useObservableState } from "../../hooks/observableHooks";
12+
import { useObservableState } from "../../hooks/observableHooks";
1313
import { useResource } from "../../hooks/resourceHooks";
1414
import { TraverseGraph } from "../../misc/graphUtils";
1515

@@ -106,7 +106,7 @@ type Command<T extends EntityBase> = Partial<IDisposable> &
106106
/**
107107
* An observable that notifies when the command state changes.
108108
*/
109-
onChange?: IReadonlyObservable<void>;
109+
onChange?: IReadonlyObservable<unknown>;
110110
}>;
111111

112112
type ActionCommand<T extends EntityBase> = Command<T> & {
@@ -192,23 +192,31 @@ const useStyles = makeStyles({
192192
const ActionCommand: FunctionComponent<{ command: ActionCommand<EntityBase>; entity: EntityBase }> = (props) => {
193193
const { command } = props;
194194

195-
useObservableRenderer(command.onChange);
195+
// eslint-disable-next-line @typescript-eslint/naming-convention
196+
const [displayName, Icon, execute] = useObservableState(
197+
useCallback(() => [command.displayName, command.icon, command.execute] as const, [command]),
198+
command.onChange
199+
);
196200

197201
return (
198-
<Tooltip key={command.displayName} content={command.displayName} relationship="label">
199-
<Button icon={<command.icon />} appearance="subtle" onClick={() => command.execute()} />
202+
<Tooltip content={displayName} relationship="label" positioning={"after"}>
203+
<Button icon={<Icon />} appearance="subtle" onClick={() => execute()} />
200204
</Tooltip>
201205
);
202206
};
203207

204208
const ToggleCommand: FunctionComponent<{ command: ToggleCommand<EntityBase>; entity: EntityBase }> = (props) => {
205209
const { command } = props;
206210

207-
useObservableRenderer(command.onChange);
211+
// eslint-disable-next-line @typescript-eslint/naming-convention
212+
const [displayName, Icon, isEnabled] = useObservableState(
213+
useCallback(() => [command.displayName, command.icon, command.isEnabled] as const, [command]),
214+
command.onChange
215+
);
208216

209217
return (
210-
<Tooltip content={command.displayName} relationship="label">
211-
<ToggleButton icon={<command.icon />} appearance="transparent" checked={command.isEnabled} onClick={() => (command.isEnabled = !command.isEnabled)} />
218+
<Tooltip content={displayName} relationship="label" positioning={"after"}>
219+
<ToggleButton icon={<Icon />} appearance="transparent" checked={isEnabled} onClick={() => (command.isEnabled = !command.isEnabled)} />
212220
</Tooltip>
213221
);
214222
};

packages/dev/inspector-v2/src/hooks/observableHooks.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import type { ObservableCollection } from "../misc/observableCollection";
44

55
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
66

7-
import { UniqueIdGenerator } from "core/Misc/uniqueIdGenerator";
8-
97
/**
108
* Returns the current value of the accessor and updates it when the specified event is fired on the specified element.
119
* @param accessor A function that returns the current value.
@@ -76,17 +74,6 @@ export function useObservableState<T>(accessor: () => T, ...observables: Array<I
7674
return current;
7775
}
7876

79-
/**
80-
* Triggers a re-render when any of the observables fire.
81-
* @param observables The observables to listen for changes on.
82-
*/
83-
export function useObservableRenderer(...observables: Array<IReadonlyObservable | null | undefined>) {
84-
useObservableState(
85-
useCallback(() => UniqueIdGenerator.UniqueId, []),
86-
...observables
87-
);
88-
}
89-
9077
/**
9178
* Returns a copy of the items in the collection and updates it when the collection changes.
9279
* @param collection The collection to observe.

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

Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Gizmo, Nullable } from "core/index";
1+
import type { Gizmo, IObserver, Nullable } from "core/index";
22
import type { ServiceDefinition } from "../../../modularity/serviceDefinition";
33
import type { ISceneContext } from "../../sceneContext";
44
import type { ISceneExplorerService } from "./sceneExplorerService";
@@ -11,6 +11,8 @@ import {
1111
Cone16Regular,
1212
EyeOffRegular,
1313
EyeRegular,
14+
FlashlightOffRegular,
15+
FlashlightRegular,
1416
LightbulbRegular,
1517
VideoFilled,
1618
VideoRegular,
@@ -19,6 +21,7 @@ import {
1921
import { Camera } from "core/Cameras/camera";
2022
import { FrameGraphUtils } from "core/FrameGraph/frameGraphUtils";
2123
import { CameraGizmo } from "core/Gizmos/cameraGizmo";
24+
import { LightGizmo } from "core/Gizmos/lightGizmo";
2225
import { Light } from "core/Lights/light";
2326
import { AbstractMesh } from "core/Meshes/abstractMesh";
2427
import { TransformNode } from "core/Meshes/transformNode";
@@ -161,7 +164,6 @@ export const NodeExplorerServiceDefinition: ServiceDefinition<[], [ISceneExplore
161164
},
162165
});
163166

164-
const gizmos = new Set<Gizmo>();
165167
let utilityLayer: Nullable<UtilityLayerRenderer> = null;
166168
const getOrCreateUtilityLayer = () => {
167169
if (!utilityLayer) {
@@ -170,76 +172,104 @@ export const NodeExplorerServiceDefinition: ServiceDefinition<[], [ISceneExplore
170172
return utilityLayer;
171173
};
172174

173-
const cameraGizmoCommandRegistration = sceneExplorerService.addCommand({
174-
predicate: (entity: unknown) => entity instanceof Camera,
175-
getCommand: (camera) => {
176-
const onChangeObservable = new Observable<void>();
175+
function addGizmoCommand<NodeT extends Node, GizmoT extends Gizmo>(
176+
nodeClass: abstract new (...args: any[]) => NodeT,
177+
gizmoClass: new (...args: ConstructorParameters<typeof Gizmo>) => GizmoT,
178+
gizmoMap: WeakMap<NodeT, GizmoT>,
179+
onGizmoCreated: (node: NodeT, gizmo: GizmoT) => void
180+
) {
181+
return sceneExplorerService.addCommand({
182+
predicate: (entity: unknown): entity is NodeT => entity instanceof nodeClass,
183+
getCommand: (node) => {
184+
const onChangeObservable = new Observable<void>();
177185

178-
const getGizmo = () => {
179-
return camera.reservedDataStore?.cameraGizmo as Nullable<CameraGizmo>;
180-
};
186+
const getGizmo = () => {
187+
return gizmoMap.get(node);
188+
};
181189

182-
const createGizmo = () => {
183-
const gizmo = new CameraGizmo(getOrCreateUtilityLayer());
184-
gizmo.camera = camera;
185-
gizmo.material.reservedDataStore = { hidden: true };
190+
let nodeDisposedObserver: Nullable<IObserver> = null;
186191

187-
gizmos.add(gizmo);
188-
if (!camera.reservedDataStore) {
189-
camera.reservedDataStore = {};
190-
}
191-
camera.reservedDataStore.cameraGizmo = gizmo;
192+
const disposeGizmo = () => {
193+
const gizmo = getGizmo();
194+
if (gizmo) {
195+
gizmoMap.delete(node);
196+
gizmo.dispose();
197+
nodeDisposedObserver?.remove();
198+
onChangeObservable.notifyObservers();
199+
}
200+
};
192201

193-
onChangeObservable.notifyObservers();
202+
const createGizmo = () => {
203+
const gizmo = new gizmoClass(getOrCreateUtilityLayer());
204+
onGizmoCreated(node, gizmo);
205+
gizmoMap.set(node, gizmo);
206+
nodeDisposedObserver = node.onDisposeObservable.addOnce(disposeGizmo);
207+
onChangeObservable.notifyObservers();
208+
return gizmo;
209+
};
194210

195-
return gizmo;
196-
};
211+
return {
212+
type: "toggle",
213+
get displayName() {
214+
return `Turn ${getGizmo() ? "Off" : "On"} Gizmo`;
215+
},
216+
icon: () => (getGizmo() ? <Cone16Filled /> : <Cone16Regular />),
217+
get isEnabled() {
218+
return !!getGizmo();
219+
},
220+
set isEnabled(enabled: boolean) {
221+
if (enabled) {
222+
if (!getGizmo()) {
223+
createGizmo();
224+
}
225+
} else {
226+
disposeGizmo();
227+
}
228+
},
229+
onChange: onChangeObservable,
230+
dispose: () => {
231+
onChangeObservable.clear();
232+
},
233+
};
234+
},
235+
});
236+
}
197237

198-
const disposeGizmo = () => {
199-
const gizmo = getGizmo();
200-
if (gizmo) {
201-
gizmos.delete(gizmo);
202-
delete camera.reservedDataStore.cameraGizmo;
203-
gizmo.dispose();
204-
}
205-
206-
onChangeObservable.notifyObservers();
207-
};
238+
const cameraGizmos = new WeakMap<Camera, CameraGizmo>();
239+
const cameraGizmoCommandRegistration = addGizmoCommand(Camera, CameraGizmo, cameraGizmos, (camera, gizmo) => (gizmo.camera = camera));
208240

241+
const lightEnabledCommandRegistration = sceneExplorerService.addCommand({
242+
predicate: (entity: unknown): entity is Light => entity instanceof Light,
243+
getCommand: (light) => {
209244
return {
210245
type: "toggle",
211246
get displayName() {
212-
return `Turn ${getGizmo() ? "Off" : "On"} Gizmo`;
247+
return `Turn Light ${light.isEnabled() ? "Off" : "On"}`;
213248
},
214-
icon: () => (getGizmo() ? <Cone16Filled /> : <Cone16Regular />),
249+
icon: () => (light.isEnabled() ? <FlashlightRegular /> : <FlashlightOffRegular />),
215250
get isEnabled() {
216-
return !!getGizmo();
251+
return !light.isEnabled();
217252
},
218253
set isEnabled(enabled: boolean) {
219-
if (enabled) {
220-
if (!getGizmo()) {
221-
createGizmo();
222-
}
223-
} else {
224-
disposeGizmo();
225-
}
226-
},
227-
onChange: onChangeObservable,
228-
dispose: () => {
229-
onChangeObservable.clear();
254+
light.setEnabled(!enabled);
230255
},
256+
onChange: light.onEnabledStateChangedObservable,
231257
};
232258
},
233259
});
234260

261+
const lightGizmos = new WeakMap<Light, LightGizmo>();
262+
const lightGizmoCommandRegistration = addGizmoCommand(Light, LightGizmo, lightGizmos, (light, gizmo) => (gizmo.light = light));
263+
235264
return {
236265
dispose: () => {
237266
sectionRegistration.dispose();
238-
gizmos.forEach((gizmo) => gizmo.dispose());
239267
utilityLayer?.dispose();
240268
abstractMeshVisibilityCommandRegistration.dispose();
241269
activeCameraCommandRegistration.dispose();
242270
cameraGizmoCommandRegistration.dispose();
271+
lightEnabledCommandRegistration.dispose();
272+
lightGizmoCommandRegistration.dispose();
243273
},
244274
};
245275
},

0 commit comments

Comments
 (0)