Skip to content

Commit 70f0308

Browse files
georginahalpernGeorgina
andauthored
[InspectorV2] Particle attractor PR comments (#17014)
Addressing comments from #17012 after it merged --------- Co-authored-by: Georgina <[email protected]>
1 parent a1e153d commit 70f0308

File tree

4 files changed

+36
-39
lines changed

4 files changed

+36
-39
lines changed

packages/dev/inspector-v2/src/components/properties/particles/attractor.tsx

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import type { Color3 } from "core/Maths";
77
import { Color4, Matrix } from "core/Maths";
88
import type { AbstractMesh } from "core/Meshes";
99
import { CreateSphere } from "core/Meshes";
10-
import type { Observer } from "core/Misc";
1110
import type { Attractor } from "core/Particles";
1211
import type { Scene } from "core/scene";
13-
import { useCallback, useEffect, useRef, useState, type FunctionComponent } from "react";
12+
import { useCallback, useEffect, useState } from "react";
13+
import type { FunctionComponent } from "react";
1414
import { SyncedSliderInput } from "shared-ui-components/fluent/primitives/syncedSlider";
1515
import { ToggleButton } from "shared-ui-components/fluent/primitives/toggleButton";
1616
import { useAsyncResource, useResource } from "../../../hooks/resourceHooks";
@@ -46,15 +46,14 @@ const CreateImpostor = (id: number, scene: Scene, attractor: Attractor, initialS
4646
return impostor;
4747
};
4848

49-
async function CreateTextRendererAsync(scene: Scene, impostor: AbstractMesh, index: number, color: Color3) {
49+
async function CreateTextRendererAsync(id: number, scene: Scene, impostor: AbstractMesh, color: Color3) {
5050
const sdfFontDefinition = await (await fetch("https://assets.babylonjs.com/fonts/roboto-regular.json")).text();
5151
const fontAsset = new FontAsset(sdfFontDefinition, "https://assets.babylonjs.com/fonts/roboto-regular.png");
5252

5353
const textRenderer = await TextRenderer.CreateTextRendererAsync(fontAsset, scene.getEngine());
54-
textRenderer.addParagraph("#" + index, {}, Matrix.Scaling(0.5, 0.5, 0.5).multiply(Matrix.Translation(0, 1, 0)));
54+
textRenderer.addParagraph("#" + id, {}, Matrix.Scaling(0.5, 0.5, 0.5).multiply(Matrix.Translation(0, 1, 0)));
5555
textRenderer.isBillboard = true;
5656
textRenderer.color = Color4.FromColor3(color, 1.0);
57-
textRenderer.render(scene.getViewMatrix(), scene.getProjectionMatrix());
5857
textRenderer.parent = impostor;
5958
return textRenderer;
6059
}
@@ -65,33 +64,32 @@ async function CreateTextRendererAsync(scene: Scene, impostor: AbstractMesh, ind
6564
* @returns
6665
*/
6766
export const AttractorComponent: FunctionComponent<AttractorProps> = (props) => {
68-
const { attractor, id, impostorScale, impostorMaterial, scene } = props;
67+
const { attractor, id, impostorScale, impostorMaterial, impostorColor, scene, onControl, isControlled } = props;
6968
const classes = useAttractorStyles();
7069
const [shown, setShown] = useState(true);
7170

72-
// Create observer and cleanup on unmount (we can't use useResource since Observer is not an IDisposable)
73-
const sceneOnAfterRenderObserverRef = useRef<Observer<Scene>>();
74-
useEffect(() => () => sceneOnAfterRenderObserverRef.current?.remove(), []);
75-
7671
// We only want to recreate the impostor mesh and associated if id, scene, or attractor/impostor changes
7772
const impostor = useResource(useCallback(() => CreateImpostor(id, scene, attractor, impostorScale, impostorMaterial), [id, scene, attractor]));
78-
const label = useAsyncResource(useCallback(async () => await CreateTextRendererAsync(scene, impostor, id, props.impostorColor), [scene, impostor, id]));
73+
const label = useAsyncResource(useCallback(async () => await CreateTextRendererAsync(id, scene, impostor, impostorColor), [id, scene, impostor]));
7974

8075
// If impostor, color, or label change, recreate the observer function so that it isnt hooked to old state
8176
useEffect(() => {
82-
sceneOnAfterRenderObserverRef.current?.remove();
83-
sceneOnAfterRenderObserverRef.current = scene.onAfterRenderObservable.add(() => {
77+
const onAfterRender = scene.onAfterRenderObservable.add(() => {
8478
attractor.position.copyFrom(impostor.position);
8579
if (label) {
86-
label.color = Color4.FromColor3(props.impostorColor);
80+
label.color = Color4.FromColor3(impostorColor);
8781
label.render(scene.getViewMatrix(), scene.getProjectionMatrix());
8882
}
8983
});
90-
}, [impostor, label, props.impostorColor]);
84+
return () => {
85+
onAfterRender.remove();
86+
};
87+
}, [impostor, scene, label, impostorColor]);
9188

89+
// If impostor or impostorScale change, update impostor scaling
9290
useEffect(() => {
9391
impostor.scaling.setAll(impostorScale);
94-
}, [impostorScale]);
92+
}, [impostor, impostorScale]);
9593

9694
return (
9795
<div className={classes.container}>
@@ -109,8 +107,8 @@ export const AttractorComponent: FunctionComponent<AttractorProps> = (props) =>
109107
<ToggleButton
110108
title="Add / remove position gizmo from particle attractor"
111109
enabledIcon={ArrowMoveFilled}
112-
value={props.isControlled(impostor)}
113-
onChange={(control: boolean) => props.onControl(control ? impostor : undefined)}
110+
value={isControlled(impostor)}
111+
onChange={(control: boolean) => onControl(control ? impostor : undefined)}
114112
/>
115113
</div>
116114
);

packages/dev/inspector-v2/src/components/properties/particles/attractorList.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { useResource } from "../../../hooks/resourceHooks";
1515
import { AttractorComponent } from "./attractor";
1616
type AttractorListProps = {
1717
scene: Scene;
18-
gizmoManager: GizmoManager;
1918
attractors: Array<Attractor>;
2019
system: ParticleSystem;
2120
};
@@ -52,7 +51,7 @@ export const AttractorList: FunctionComponent<AttractorListProps> = (props) => {
5251

5352
// All impostors share a scale and material/color (for now!)
5453
const [impostorScale, setImpostorScale] = useState(1);
55-
const [impostorColor, setImpostorColor] = useState<Color3>(() => Color3.White());
54+
const [impostorColor, setImpostorColor] = useState(() => Color3.White());
5655
const impostorMaterial = useResource(useCallback(() => CreateSharedMaterial(scene, impostorColor), [scene]));
5756

5857
// All impostors share a gizmoManager. controlledImpostor state ensures re-render of children so that their gizmoEnabled toggle is accurate

packages/dev/inspector-v2/src/components/properties/particles/particleSystemProperties.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { FactorGradient, ColorGradient as Color4Gradient, IValueGradient, ParticleSystem } from "core/index";
2-
import { GizmoManager } from "core/Gizmos";
32

43
import { Color3, Color4 } from "core/Maths/math.color";
54
import { useCallback } from "react";
@@ -9,6 +8,7 @@ import { useInterceptObservable } from "../../../hooks/instrumentationHooks";
98
import { useObservableState } from "../../../hooks/observableHooks";
109
import { Color4GradientList, FactorGradientList } from "shared-ui-components/fluent/hoc/gradientList";
1110
import { AttractorList } from "./attractorList";
11+
import { MessageBar } from "shared-ui-components/fluent/primitives/messageBar";
1212

1313
export const ParticleSystemEmissionProperties: FunctionComponent<{ particleSystem: ParticleSystem }> = (props) => {
1414
const { particleSystem: system } = props;
@@ -72,13 +72,18 @@ export const ParticleSystemColorProperties: FunctionComponent<{ particleSystem:
7272

7373
export const ParticleSystemAttractorProperties: FunctionComponent<{ particleSystem: ParticleSystem }> = (props) => {
7474
const { particleSystem: system } = props;
75-
const gizmoManager = new GizmoManager(system.getScene()!);
7675

7776
const attractors = useParticleSystemProperty(system, "attractors", "property", "addAttractor", "removeAttractor");
77+
const scene = system.getScene();
7878

7979
return (
8080
<>
81-
<AttractorList gizmoManager={gizmoManager} attractors={attractors} scene={system.getScene()!} system={system} />
81+
{scene ? (
82+
<AttractorList attractors={attractors} scene={scene} system={system} />
83+
) : (
84+
// Should never get here since sceneExplorer only displays if there is a scene, but adding UX in case that assumption changes in future
85+
<MessageBar intent="info" title="No Scene Available" message="Cannot display attractors without a scene" />
86+
)}
8287
</>
8388
);
8489
};

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

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { IDisposable } from "core/index";
22

3-
import { useRef, useEffect } from "react";
3+
import { useRef, useEffect, useState } from "react";
44

55
/**
66
* Custom hook to manage a resource with automatic disposal. The resource is created once initially, and recreated
@@ -44,46 +44,41 @@ export function useResource<T extends IDisposable>(factory: () => T): T {
4444
* @returns The created resource.
4545
*/
4646
export function useAsyncResource<T extends IDisposable>(factory: (abortSignal: AbortSignal) => Promise<T>): T | undefined {
47-
const resourceRef = useRef<T>();
47+
const [resource, setResource] = useState<T | undefined>();
4848
const factoryRef = useRef(factory);
4949

5050
// Update refs to capture latest values
5151
factoryRef.current = factory;
5252

5353
useEffect(() => {
54-
const abortController = new AbortController(); // Create AbortController
55-
const currentResource: T | undefined = resourceRef.current;
54+
const abortController = new AbortController();
5655

5756
// Dispose old resource if it exists
58-
currentResource?.dispose();
59-
resourceRef.current = undefined;
57+
resource?.dispose();
58+
setResource(undefined);
6059

6160
// Create new resource
6261
void (async () => {
6362
try {
64-
const newVal = await factory(abortController.signal); // Pass the signal
63+
const newVal = await factory(abortController.signal);
6564
if (!abortController.signal.aborted) {
66-
resourceRef.current = newVal;
65+
setResource(newVal); // This will trigger a re-render so the new resource is returned to caller
6766
} else {
6867
newVal.dispose();
6968
}
7069
} catch (error) {
71-
// Handle abortion gracefully
7270
if (error instanceof Error && error.name === "AbortError") {
73-
// Request was aborted, this is expected
7471
return;
7572
}
76-
// Optionally handle other errors here
77-
global.console.error("Failed to create async resource:", error);
7873
}
7974
})();
8075

8176
return () => {
82-
abortController.abort(); // Abort the operation
83-
resourceRef.current?.dispose();
84-
resourceRef.current = undefined;
77+
abortController.abort();
78+
resource?.dispose();
79+
setResource(undefined);
8580
};
8681
}, [factory]);
8782

88-
return resourceRef.current;
83+
return resource;
8984
}

0 commit comments

Comments
 (0)