Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import type { Color3 } from "core/Maths";
import { Color4, Matrix } from "core/Maths";
import type { AbstractMesh } from "core/Meshes";
import { CreateSphere } from "core/Meshes";
import type { Observer } from "core/Misc";
import type { Attractor } from "core/Particles";
import type { Scene } from "core/scene";
import { useCallback, useEffect, useRef, useState, type FunctionComponent } from "react";
import { useCallback, useEffect, useState, type FunctionComponent } from "react";
import { SyncedSliderInput } from "shared-ui-components/fluent/primitives/syncedSlider";
import { ToggleButton } from "shared-ui-components/fluent/primitives/toggleButton";
import { useAsyncResource, useResource } from "../../../hooks/resourceHooks";
Expand Down Expand Up @@ -54,7 +53,6 @@ async function CreateTextRendererAsync(scene: Scene, impostor: AbstractMesh, ind
textRenderer.addParagraph("#" + index, {}, Matrix.Scaling(0.5, 0.5, 0.5).multiply(Matrix.Translation(0, 1, 0)));
textRenderer.isBillboard = true;
textRenderer.color = Color4.FromColor3(color, 1.0);
textRenderer.render(scene.getViewMatrix(), scene.getProjectionMatrix());
textRenderer.parent = impostor;
return textRenderer;
}
Expand All @@ -69,29 +67,28 @@ export const AttractorComponent: FunctionComponent<AttractorProps> = (props) =>
const classes = useAttractorStyles();
const [shown, setShown] = useState(true);

// Create observer and cleanup on unmount (we can't use useResource since Observer is not an IDisposable)
const sceneOnAfterRenderObserverRef = useRef<Observer<Scene>>();
useEffect(() => () => sceneOnAfterRenderObserverRef.current?.remove(), []);

// We only want to recreate the impostor mesh and associated if id, scene, or attractor/impostor changes
const impostor = useResource(useCallback(() => CreateImpostor(id, scene, attractor, impostorScale, impostorMaterial), [id, scene, attractor]));
const label = useAsyncResource(useCallback(async () => await CreateTextRendererAsync(scene, impostor, id, props.impostorColor), [scene, impostor, id]));

// If impostor, color, or label change, recreate the observer function so that it isnt hooked to old state
useEffect(() => {
sceneOnAfterRenderObserverRef.current?.remove();
sceneOnAfterRenderObserverRef.current = scene.onAfterRenderObservable.add(() => {
const onAfterRender = scene.onAfterRenderObservable.add(() => {
attractor.position.copyFrom(impostor.position);
if (label) {
label.color = Color4.FromColor3(props.impostorColor);
label.render(scene.getViewMatrix(), scene.getProjectionMatrix());
}
});
return () => {
scene.onAfterRenderObservable.remove(onAfterRender);
};
}, [impostor, label, props.impostorColor]);

// If impostor or impostorScale change, update impostor scaling
useEffect(() => {
impostor.scaling.setAll(impostorScale);
}, [impostorScale]);
}, [impostor, impostorScale]);

return (
<div className={classes.container}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { useResource } from "../../../hooks/resourceHooks";
import { AttractorComponent } from "./attractor";
type AttractorListProps = {
scene: Scene;
gizmoManager: GizmoManager;
attractors: Array<Attractor>;
system: ParticleSystem;
};
Expand Down Expand Up @@ -52,7 +51,7 @@ export const AttractorList: FunctionComponent<AttractorListProps> = (props) => {

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

// All impostors share a gizmoManager. controlledImpostor state ensures re-render of children so that their gizmoEnabled toggle is accurate
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { FactorGradient, ColorGradient as Color4Gradient, IValueGradient, ParticleSystem } from "core/index";
import { GizmoManager } from "core/Gizmos";

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

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

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

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

return (
<>
<AttractorList gizmoManager={gizmoManager} attractors={attractors} scene={system.getScene()!} system={system} />
{scene ? (
<AttractorList attractors={attractors} scene={scene} system={system} />
) : (
// Should never get here since sceneExplorer only displays if there is a scene, but adding UX in case that assumption changes in future
<MessageBar intent="info" title="No Scene Available" message="Cannot display attractors without a scene" />
)}
</>
);
};
Expand Down
31 changes: 14 additions & 17 deletions packages/dev/inspector-v2/src/hooks/resourceHooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IDisposable } from "core/index";

import { useRef, useEffect } from "react";
import { useRef, useEffect, useState } from "react";

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

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

useEffect(() => {
const abortController = new AbortController(); // Create AbortController
const currentResource: T | undefined = resourceRef.current;
const abortController = new AbortController();
let isMounted = true;

// Dispose old resource if it exists
currentResource?.dispose();
resourceRef.current = undefined;
resource?.dispose();
setResource(undefined);

// Create new resource
void (async () => {
try {
const newVal = await factory(abortController.signal); // Pass the signal
if (!abortController.signal.aborted) {
resourceRef.current = newVal;
const newVal = await factory(abortController.signal);
if (isMounted && !abortController.signal.aborted) {
setResource(newVal); // This will trigger a re-render so the new resource is returned to caller
} else {
newVal.dispose();
}
} catch (error) {
// Handle abortion gracefully
if (error instanceof Error && error.name === "AbortError") {
// Request was aborted, this is expected
return;
}
// Optionally handle other errors here
global.console.error("Failed to create async resource:", error);
}
})();

return () => {
abortController.abort(); // Abort the operation
resourceRef.current?.dispose();
resourceRef.current = undefined;
isMounted = false;
abortController.abort();
resource?.dispose();
setResource(undefined);
};
}, [factory]);

return resourceRef.current;
return resource;
}
Loading