|
| 1 | +import { makeStyles, tokens } from "@fluentui/react-components"; |
| 2 | +import { ArrowMoveFilled, EyeFilled, EyeOffFilled } from "@fluentui/react-icons"; |
| 3 | +import { FontAsset } from "addons/msdfText/fontAsset"; |
| 4 | +import { TextRenderer } from "addons/msdfText/textRenderer"; |
| 5 | +import type { StandardMaterial } from "core/Materials"; |
| 6 | +import type { Color3 } from "core/Maths"; |
| 7 | +import { Color4, Matrix } from "core/Maths"; |
| 8 | +import type { AbstractMesh } from "core/Meshes"; |
| 9 | +import { CreateSphere } from "core/Meshes"; |
| 10 | +import type { Observer } from "core/Misc"; |
| 11 | +import type { Attractor } from "core/Particles"; |
| 12 | +import type { Scene } from "core/scene"; |
| 13 | +import { useCallback, useEffect, useRef, useState, type FunctionComponent } from "react"; |
| 14 | +import { SyncedSliderInput } from "shared-ui-components/fluent/primitives/syncedSlider"; |
| 15 | +import { ToggleButton } from "shared-ui-components/fluent/primitives/toggleButton"; |
| 16 | +import { useAsyncResource, useResource } from "../../../hooks/resourceHooks"; |
| 17 | + |
| 18 | +type AttractorProps = { |
| 19 | + attractor: Attractor; |
| 20 | + id: number; |
| 21 | + impostorScale: number; |
| 22 | + impostorColor: Color3; |
| 23 | + impostorMaterial: StandardMaterial; |
| 24 | + scene: Scene; |
| 25 | + isControlled: (impostor: AbstractMesh) => boolean; |
| 26 | + onControl: (impostor?: AbstractMesh) => void; |
| 27 | +}; |
| 28 | + |
| 29 | +const useAttractorStyles = makeStyles({ |
| 30 | + container: { |
| 31 | + // top-level div used for lineContainer, in UI overhaul update to just use linecontainer |
| 32 | + width: "100%", |
| 33 | + display: "flex", // Makes this a flex container |
| 34 | + flexDirection: "row", // Arranges children horizontally, main-axis=horizontal |
| 35 | + padding: `${tokens.spacingVerticalXS} 0px`, |
| 36 | + borderBottom: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, |
| 37 | + }, |
| 38 | +}); |
| 39 | + |
| 40 | +const CreateImpostor = (id: number, scene: Scene, attractor: Attractor, initialScale: number, initialMaterial: StandardMaterial) => { |
| 41 | + const impostor = CreateSphere("Attractor impostor #" + id, { diameter: 1 }, scene); |
| 42 | + impostor.scaling.setAll(initialScale); |
| 43 | + impostor.position.copyFrom(attractor.position); |
| 44 | + impostor.material = initialMaterial; |
| 45 | + impostor.reservedDataStore = { hidden: true }; |
| 46 | + return impostor; |
| 47 | +}; |
| 48 | + |
| 49 | +async function CreateTextRendererAsync(scene: Scene, impostor: AbstractMesh, index: number, color: Color3) { |
| 50 | + const sdfFontDefinition = await (await fetch("https://assets.babylonjs.com/fonts/roboto-regular.json")).text(); |
| 51 | + const fontAsset = new FontAsset(sdfFontDefinition, "https://assets.babylonjs.com/fonts/roboto-regular.png"); |
| 52 | + |
| 53 | + 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))); |
| 55 | + textRenderer.isBillboard = true; |
| 56 | + textRenderer.color = Color4.FromColor3(color, 1.0); |
| 57 | + textRenderer.render(scene.getViewMatrix(), scene.getProjectionMatrix()); |
| 58 | + textRenderer.parent = impostor; |
| 59 | + return textRenderer; |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Represents the UX of an attractor, a sphere with a color/size whose position matches that of the underlying attractor |
| 64 | + * @param props |
| 65 | + * @returns |
| 66 | + */ |
| 67 | +export const AttractorComponent: FunctionComponent<AttractorProps> = (props) => { |
| 68 | + const { attractor, id, impostorScale, impostorMaterial, scene } = props; |
| 69 | + const classes = useAttractorStyles(); |
| 70 | + const [shown, setShown] = useState(true); |
| 71 | + |
| 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 | + |
| 76 | + // We only want to recreate the impostor mesh and associated if id, scene, or attractor/impostor changes |
| 77 | + 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])); |
| 79 | + |
| 80 | + // If impostor, color, or label change, recreate the observer function so that it isnt hooked to old state |
| 81 | + useEffect(() => { |
| 82 | + sceneOnAfterRenderObserverRef.current?.remove(); |
| 83 | + sceneOnAfterRenderObserverRef.current = scene.onAfterRenderObservable.add(() => { |
| 84 | + attractor.position.copyFrom(impostor.position); |
| 85 | + if (label) { |
| 86 | + label.color = Color4.FromColor3(props.impostorColor); |
| 87 | + label.render(scene.getViewMatrix(), scene.getProjectionMatrix()); |
| 88 | + } |
| 89 | + }); |
| 90 | + }, [impostor, label, props.impostorColor]); |
| 91 | + |
| 92 | + useEffect(() => { |
| 93 | + impostor.scaling.setAll(impostorScale); |
| 94 | + }, [impostorScale]); |
| 95 | + |
| 96 | + return ( |
| 97 | + <div className={classes.container}> |
| 98 | + <SyncedSliderInput value={attractor.strength} onChange={(value) => (attractor.strength = value)} min={-10} max={10} step={0.1} /> |
| 99 | + <ToggleButton |
| 100 | + title="Show / hide particle attractor." |
| 101 | + enabledIcon={EyeFilled} |
| 102 | + disabledIcon={EyeOffFilled} |
| 103 | + value={shown} |
| 104 | + onChange={(show: boolean) => { |
| 105 | + show ? (impostor.visibility = 1) : (impostor.visibility = 0); |
| 106 | + setShown(show); |
| 107 | + }} |
| 108 | + /> |
| 109 | + <ToggleButton |
| 110 | + title="Add / remove position gizmo from particle attractor" |
| 111 | + enabledIcon={ArrowMoveFilled} |
| 112 | + value={props.isControlled(impostor)} |
| 113 | + onChange={(control: boolean) => props.onControl(control ? impostor : undefined)} |
| 114 | + /> |
| 115 | + </div> |
| 116 | + ); |
| 117 | +}; |
0 commit comments