From 8c20e0810e7f24ef0cbca77a239bd306ad8f6f3e Mon Sep 17 00:00:00 2001 From: Simone De Vittorio Date: Fri, 24 Jan 2025 17:14:05 +0100 Subject: [PATCH] chore(general,components): add zustand for store reactivity --- package-lock.json | 38 +++++++++++-- packages/library/package.json | 9 +-- packages/library/src/core/Scene.tsx | 38 ++++++++----- packages/library/src/core/hooks.tsx | 46 ---------------- packages/library/src/core/index.ts | 2 +- packages/library/src/core/store.ts | 69 +++++++++++++++++++++++ packages/library/src/types/types.ts | 2 +- packages/library/src/web/Engine.tsx | 85 ++++++++++++++--------------- 8 files changed, 174 insertions(+), 115 deletions(-) delete mode 100644 packages/library/src/core/hooks.tsx create mode 100644 packages/library/src/core/store.ts diff --git a/package-lock.json b/package-lock.json index 82f8ddd..b590a89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2160,8 +2160,7 @@ "version": "7.40.2", "resolved": "https://registry.npmjs.org/@babylonjs/core/-/core-7.40.2.tgz", "integrity": "sha512-QkS1J02KHHGJO741IVLubngh5pedSLgq37OgIavSHGWM9Rk2ukGiRTAqbC6hmsyKpGCWHAEOJhU8Eh5OuH4WGQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@babylonjs/gui": { "version": "7.40.2", @@ -18803,6 +18802,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "packages/common": { "name": "@dvmstudios/reactylon-common", "license": "MIT", @@ -18810,6 +18838,7 @@ "acorn": "^8.11.3" }, "devDependencies": { + "@babylonjs/core": "^7.40.2", "nodemon": "^3.1.3" }, "engines": { @@ -18831,13 +18860,14 @@ }, "packages/library": { "name": "reactylon", - "version": "1.0.6", + "version": "1.1.5", "license": "MIT", "dependencies": { "acorn": "^8.11.3", "its-fine": "^1.2.5", "lodash": "^4.17.21", - "react-reconciler": "^0.29.2" + "react-reconciler": "^0.29.2", + "zustand": "^5.0.3" }, "devDependencies": { "@babel/core": "^7.17.3", diff --git a/packages/library/package.json b/packages/library/package.json index 1725b00..113ec13 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -29,17 +29,18 @@ "acorn": "^8.11.3", "its-fine": "^1.2.5", "lodash": "^4.17.21", - "react-reconciler": "^0.29.2" + "react-reconciler": "^0.29.2", + "zustand": "^5.0.3" }, "peerDependencies": { "@babylonjs/core": "^7.40.2 ", "@babylonjs/gui": "^7.40.2 ", "@babylonjs/havok": "^1.3.7", "@babylonjs/react-native": "^1.8.6", - "react": "^18.2.0", - "react-dom": "^18.3.1", "@types/react": "^18.2.0", - "@types/react-dom": "^18.3.1" + "@types/react-dom": "^18.3.1", + "react": "^18.2.0", + "react-dom": "^18.3.1" }, "peerDependenciesMeta": { "react-dom": { diff --git a/packages/library/src/core/Scene.tsx b/packages/library/src/core/Scene.tsx index d8cbe04..5d00ee9 100644 --- a/packages/library/src/core/Scene.tsx +++ b/packages/library/src/core/Scene.tsx @@ -2,11 +2,12 @@ import React, { useEffect, useRef } from 'react'; import { Scene as BabylonScene, SceneOptions, WebXRDefaultExperienceOptions, HavokPlugin, Vector3, Nullable, Camera } from '@babylonjs/core'; import { GUI3DManager } from '@babylonjs/gui'; import HavokPhysics from '@babylonjs/havok'; -import { SceneContext, EngineContextType } from './hooks'; +import { SceneContext, EngineStore, Store, createBabylonStore } from './store'; import { RootContainer } from '@types'; import Reactylon from '../reconciler'; import { type ContextBridge, useContextBridge } from 'its-fine'; import { type CameraProps } from '@props'; +import { type StoreApi } from 'zustand'; type SceneProps = React.PropsWithChildren<{ /** @@ -25,17 +26,19 @@ type SceneProps = React.PropsWithChildren<{ * @internal * This prop is only for internal use and should not be passed to this component. */ - _context?: EngineContextType; + _context?: EngineStore; }>; //FIXME: replace global var with a singleton Manager export let activeScene: BabylonScene | null = null; export const Scene: React.FC = ({ children, sceneOptions, onSceneReady, isGui3DManager, xrDefaultExperienceOptions, physicsOptions, _context, ...rest }) => { - const { engine, isMultipleCanvas, isMultipleScene } = _context as EngineContextType; + const { engine, isMultipleCanvas, isMultipleScene } = _context as EngineStore; const rootContainer = useRef>(null); const isFirstRender = useRef(false); + const store = useRef>(); + // Returns a bridged context provider that forwards context const Bridge: ContextBridge = useContextBridge(); @@ -125,13 +128,17 @@ export const Scene: React.FC = ({ children, sceneOptions, onSceneRea /* --------------------------------------------------------------------------------------- */ /* RECONCILER ------------------------------------------------------------------------------------------ */ - rootContainer.current = { + store.current = createBabylonStore({ engine, scene, canvas, isMultipleCanvas, isMultipleScene, xrExperience, + }); + + rootContainer.current = { + ...store.current.getState(), metadata: { children: [], }, @@ -140,7 +147,7 @@ export const Scene: React.FC = ({ children, sceneOptions, onSceneRea // Renders children with bridged context into a secondary renderer Reactylon.render( - {children} + {children} , rootContainer.current!, ); @@ -156,16 +163,17 @@ export const Scene: React.FC = ({ children, sceneOptions, onSceneRea useEffect(() => { if (!isFirstRender.current) { - const { scene, xrExperience, canvas } = rootContainer.current!; - // Renders children with bridged context into a secondary renderer - Reactylon.render( - - {children} - , - rootContainer.current!, - ); - } else { - isFirstRender.current = false; + if (store.current) { + // Renders children with bridged context into a secondary renderer + Reactylon.render( + + {children} + , + rootContainer.current!, + ); + } else { + isFirstRender.current = false; + } } }); diff --git a/packages/library/src/core/hooks.tsx b/packages/library/src/core/hooks.tsx deleted file mode 100644 index 8d3c913..0000000 --- a/packages/library/src/core/hooks.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { createContext, useContext } from 'react'; -import { type Nullable, Engine, Scene, WebXRDefaultExperience } from '@babylonjs/core'; - -export type EngineContextType = { - engine: Nullable; - isMultipleCanvas: boolean; - isMultipleScene: boolean; -}; - -export type SceneContextType = EngineContextType & { - scene: Nullable; - canvas: Nullable; - xrExperience: Nullable; - //sceneReady: boolean; -}; - -export const SceneContext = createContext({ - engine: null, - isMultipleCanvas: false, - isMultipleScene: false, - scene: null, - canvas: null, - xrExperience: null, - //sceneReady: false, -}); -SceneContext.displayName = 'SceneContext'; - -/** - * Get the engine from the context. - */ -export const useEngine = (): Engine => useContext(SceneContext).engine as Engine; - -/** - * Get the scene from the context. - */ -export const useScene = (): Scene => useContext(SceneContext).scene as Scene; - -/** - * Get the canvas DOM element from the context. - */ -export const useCanvas = (): Nullable => useContext(SceneContext).canvas; - -/** - * Get the XR experience from the context. - */ -export const useXrExperience = (): WebXRDefaultExperience => useContext(SceneContext).xrExperience as WebXRDefaultExperience; diff --git a/packages/library/src/core/index.ts b/packages/library/src/core/index.ts index a0c588a..69a98af 100644 --- a/packages/library/src/core/index.ts +++ b/packages/library/src/core/index.ts @@ -1,2 +1,2 @@ -export * from './hooks'; +export * from './store'; export * from './Scene'; diff --git a/packages/library/src/core/store.ts b/packages/library/src/core/store.ts new file mode 100644 index 0000000..8a04b43 --- /dev/null +++ b/packages/library/src/core/store.ts @@ -0,0 +1,69 @@ +import { createStore, StoreApi, useStore } from 'zustand'; +import { type Nullable, Engine, Scene, WebXRDefaultExperience } from '@babylonjs/core'; +import { createContext, useContext } from 'react'; + +export type EngineStore = { + engine: Engine; + isMultipleCanvas: boolean; + isMultipleScene: boolean; +}; + +export type Store = EngineStore & { + scene: Scene; + canvas: HTMLCanvasElement | WebGLRenderingContext; + xrExperience: Nullable; + //sceneReady: boolean; +}; + +export const SceneContext = createContext | null>(null); +SceneContext.displayName = 'SceneContext'; + +export const createBabylonStore = (initialProps: Store) => { + return createStore()(_set => initialProps); +}; + +/** + * Get the Babylon context. + */ +const useBabylonContext = (selector: (state: Store) => T): T => { + const store = useContext(SceneContext); + if (!store) { + throw new Error('Missing SceneContext.Provider in the tree'); + } + return useStore(store, selector); +}; + +/** + * Get the engine from the context. + */ +export function useEngine(): Engine; +export function useEngine(selector: (engine: Engine) => T): T; + +export function useEngine(selector?: (engine: Engine) => T): T | Engine { + return useBabylonContext(state => (selector ? selector(state.engine) : state.engine)); +} + +/** + * Get the scene from the context. + */ +export function useScene(): Scene; +export function useScene(selector: (scene: Scene) => T): T; + +export function useScene(selector?: (scene: Scene) => T): T | Scene { + return useBabylonContext(state => (selector ? selector(state.scene) : state.scene)); +} + +/** + * Get the canvas DOM element from the context. + */ +export const useCanvas = () => useBabylonContext(state => state.canvas); + +/** + * Get the XR experience from the context. + */ +export function useXrExperience(): WebXRDefaultExperience; +export function useXrExperience(selector: (xrExperience: WebXRDefaultExperience) => T): T; + +export function useXrExperience(selector?: (xrExperience: WebXRDefaultExperience) => T): T | WebXRDefaultExperience { + return useBabylonContext(state => (selector ? selector(state.xrExperience!) : state.xrExperience!)); +} diff --git a/packages/library/src/types/types.ts b/packages/library/src/types/types.ts index 3d14b16..0810b68 100644 --- a/packages/library/src/types/types.ts +++ b/packages/library/src/types/types.ts @@ -76,7 +76,7 @@ export type UpdatePayload = { export type RootContainer = { engine: Engine; scene: Scene; - canvas: HTMLCanvasElement; + canvas: HTMLCanvasElement | WebGLRenderingContext; isMultipleCanvas: boolean; isMultipleScene: boolean; xrExperience: Nullable; diff --git a/packages/library/src/web/Engine.tsx b/packages/library/src/web/Engine.tsx index efdd2f5..74cf92c 100644 --- a/packages/library/src/web/Engine.tsx +++ b/packages/library/src/web/Engine.tsx @@ -2,7 +2,7 @@ import React, { useEffect, Children, useState, useRef, isValidElement, cloneElem import { Engine as BabylonEngine, NullEngine, type EngineOptions, Scene, EventState, type NullEngineOptions } from '@babylonjs/core'; import CustomLoadingScreen from './CustomLoadingScreen'; import { FiberProvider } from 'its-fine'; -import { EngineContextType } from '../core/hooks'; +import { type EngineStore } from '../core/store'; import { Logger } from '@dvmstudios/reactylon-common'; export type EngineProps = React.PropsWithChildren<{ @@ -35,7 +35,7 @@ export const Engine: React.FC = ({ isMultipleCanvas, ...rest }) => { - const [context, setContext] = useState(null); + const [context, setContext] = useState(null); const engineRef = useRef<{ engine: BabylonEngine; onResizeWindow: () => void; @@ -47,55 +47,52 @@ export const Engine: React.FC = ({ const isMultipleScene = children.length > 1; useEffect(() => { - async function initializeScene() { - let canvas = null; - if (!isMultipleCanvas) { - canvas = canvasRef.current; - } else { - if (isMultipleScene) { - Children.forEach(rest.children, child => { - if (isValidElement(child)) { - if (!child.props.canvas) { - Logger.error( - `Engine - initializeScene - Each Scene component requires a corresponding canvas element. Ensure that you provide one canvas for every Scene you are using.`, - ); - } + let canvas = null; + if (!isMultipleCanvas) { + canvas = canvasRef.current; + } else { + if (isMultipleScene) { + Children.forEach(rest.children, child => { + if (isValidElement(child)) { + if (!child.props.canvas) { + Logger.error( + `Engine - initializeScene - Each Scene component requires a corresponding canvas element. Ensure that you provide one canvas for every Scene you are using.`, + ); } - }); - } - // fake canvas to work with multiple scenes (Babylon.js constraint) - canvas = document.createElement('canvas'); + } + }); } + // fake canvas to work with multiple scenes (Babylon.js constraint) + canvas = document.createElement('canvas'); + } - /* --------------------------------------------------------------------------------------- */ - /* ENGINE - ------------------------------------------------------------------------------------------ */ - const engine = process.env.NODE_ENV === 'test' ? new NullEngine(_nullEngineOptions) : new BabylonEngine(canvas, antialias, engineOptions, adaptToDeviceRatio); - if (loader) { - engine.loadingScreen = new CustomLoadingScreen(canvas as HTMLCanvasElement, loader); - } - engine.runRenderLoop(() => { - const camera = engine!.activeView?.camera; - engine!.scenes.forEach(scene => { - if (!scene.activeCamera) { - // meantime you are setting a camera - Logger.warn('Engine - runRenderLoop - Waiting for active camera...'); - } - if (scene.cameras?.length > 0) { - if (!isMultipleScene || scene.activeCamera === camera) { - scene.render(); - } + /* --------------------------------------------------------------------------------------- */ + /* ENGINE + ------------------------------------------------------------------------------------------ */ + const engine = process.env.NODE_ENV === 'test' ? new NullEngine(_nullEngineOptions) : new BabylonEngine(canvas, antialias, engineOptions, adaptToDeviceRatio); + if (loader) { + engine.loadingScreen = new CustomLoadingScreen(canvas as HTMLCanvasElement, loader); + } + engine.runRenderLoop(() => { + const camera = engine!.activeView?.camera; + engine!.scenes.forEach(scene => { + if (!scene.activeCamera) { + // meantime you are setting a camera + Logger.warn('Engine - runRenderLoop - Waiting for active camera...'); + } + if (scene.cameras?.length > 0) { + if (!isMultipleScene || scene.activeCamera === camera) { + scene.render(); } - }); + } }); + }); - engineRef.current.engine = engine; - engineRef.current.onResizeWindow = () => engine.resize(); - window.addEventListener('resize', engineRef.current.onResizeWindow); + engineRef.current.engine = engine; + engineRef.current.onResizeWindow = () => engine.resize(); + window.addEventListener('resize', engineRef.current.onResizeWindow); - setContext({ engine, isMultipleCanvas: !!isMultipleCanvas, isMultipleScene }); - } - initializeScene(); + setContext({ engine, isMultipleCanvas: !!isMultipleCanvas, isMultipleScene }); return () => { if (engineRef.current.engine) {