diff --git a/README.md b/README.md index e20da8f6..9bd2933b 100644 --- a/README.md +++ b/README.md @@ -269,63 +269,6 @@ const App: React.FC = () => { } ``` -## Using context - -If you need to do something fancy with `scene`, `canvas`, or `engine`, there are a few ways: - -### react hooks - -```jsx -// use Hooks to get engine/canvas/scene -import { useBabylonEngine, useBabylonCanvas, useBabylonScene } from 'react-babylonjs' - -// later inside a functional component: - -export default () => { - const engine = useBabylonEngine() - const canvas = useBabylonCanvas() - const scene = useBabylonScene() - console.log({ engine, canvas, scene }) - - return ( -
See console
- ) -} - -```` - -### HOC - -```jsx -import { withBabylonJS, withScene } from 'react-babylonjs' - -const DemoComponent = ({ scene, engine, canvas }} => { - console.log({ scene, engine, canvas }) - return ( -
See console
- ) -} - -export default withBabylonJS(withScene(DemoComponent)) -``` - -### direct Consmuer - -```jsx -import { WithSceneContext } from 'react-babylonjs' - -const DemoComponent = ({ scene }} => { - const engine = scene.getEngine() - const canvas = engine.getCanvas() - console.log({ scene, engine, canvas }) - return ( -
See console
- ) -} - -export default () => ({DemoComponent}) -``` - ## Major Release History > v1.0.0 (2018-11-29) - Add code generation, HoC, context provider diff --git a/package.json b/package.json index a4d25834..d6796aec 100644 --- a/package.json +++ b/package.json @@ -110,9 +110,9 @@ "lodash.camelcase": "^4.3.0", "prettier": "^1.15.3", "prompt": "^1.0.0", - "react": "^16.8.6", - "react-dom": "^16.9.0", - "react-reconciler": "^0.21.0", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "react-reconciler": "^0.24.0", "replace-in-file": "^3.4.3", "rimraf": "^2.6.1", "rollup": "^1.1.0", diff --git a/src/Engine.tsx b/src/Engine.tsx index a9dc081f..49f29b3d 100644 --- a/src/Engine.tsx +++ b/src/Engine.tsx @@ -15,8 +15,6 @@ export const BabylonJSContext = createContext({ canvas: null }) -export const BabylonJSContextConsumer = BabylonJSContext.Consumer - type Omit = Pick>; export function withBabylonJS< @@ -34,8 +32,8 @@ export function withBabylonJS< }; } -export const useBabylonEngine = () => useContext(BabylonJSContext).engine -export const useBabylonCanvas = () => useContext(BabylonJSContext).canvas +export const useBabylonEngine = (): Nullable => useContext(BabylonJSContext).engine +export const useBabylonCanvas = (): Nullable => useContext(BabylonJSContext).canvas type EngineProps = { babylonJSContext: WithBabylonJSContext, diff --git a/src/ReactBabylonJSHostConfig.ts b/src/ReactBabylonJSHostConfig.ts index 50c5a9d2..fe9a0260 100644 --- a/src/ReactBabylonJSHostConfig.ts +++ b/src/ReactBabylonJSHostConfig.ts @@ -446,7 +446,7 @@ const ReactBabylonJSHostConfig: HostConfig< }, canHydrateInstance: (instance: any, type: string, props: Props): null | CreatedInstance => { - console.log("canHydrateInstance", instance, type, props) + // console.log("canHydrateInstance", instance, type, props) return null }, diff --git a/src/Scene.tsx b/src/Scene.tsx index 30d9798e..3bf7e787 100644 --- a/src/Scene.tsx +++ b/src/Scene.tsx @@ -5,11 +5,11 @@ * LICENSE.txt file in the root directory of this source tree. */ -import React, { createContext, useContext } from 'react'; -import ReactReconciler from "react-reconciler"; +import React, { createContext, useContext, useEffect, useState, useRef } from 'react'; +import ReactReconciler, { Reconciler } from "react-reconciler"; -import { WithBabylonJSContext, withBabylonJS } from './Engine'; -import { Scene as BabylonJSScene, Engine as BabylonJSEngine, Nullable, AbstractMesh, PointerInfo, PointerEventTypes, SceneOptions, Observer } from '@babylonjs/core'; +import { WithBabylonJSContext, withBabylonJS, BabylonJSContext } from './Engine'; +import { Scene as BabylonJSScene, Engine as BabylonJSEngine, Nullable, AbstractMesh, PointerInfo, PointerEventTypes, SceneOptions, Observer, EventState } from '@babylonjs/core'; import { applyUpdateToInstance } from "./UpdateInstance"; import ReactBabylonJSHostConfig, { Container } from './ReactBabylonJSHostConfig'; @@ -21,6 +21,7 @@ export interface WithSceneContext { engine: Nullable canvas: Nullable scene: Nullable + sceneReady: boolean } export declare type SceneEventArgs = { @@ -32,10 +33,10 @@ export declare type SceneEventArgs = { export const SceneContext = createContext({ engine: null, canvas: null, - scene: null + scene: null, + sceneReady: false }) - export const useBabylonScene = () => useContext(SceneContext).scene type Omit = Pick>; @@ -62,89 +63,74 @@ type SceneProps = { onScenePointerUp?: (evt: PointerInfo, scene: BabylonJSScene) => void onScenePointerMove?: (evt: PointerInfo, scene: BabylonJSScene) => void onSceneMount?: (sceneEventArgs: SceneEventArgs) => void + children: any, sceneOptions?: SceneOptions } & FiberSceneProps -class Scene extends React.Component { - private _scene: Nullable = null; - private _pointerDownObservable: Nullable> = null; - private _pointerUpObservable: Nullable> = null; - private _pointerMoveObservable: Nullable> = null; - - private _fiberRoot?: ReactReconciler.FiberRoot; - private _reactReconcilerBabylonJs = ReactReconciler(ReactBabylonJSHostConfig) - private _propsHandler = new FiberScenePropsHandler(); - - componentDidMount() { - const { babylonJSContext } = this.props - - if (!babylonJSContext) { - // we could try to create one here with existing props (ie: backwards compat?) - console.error('You are creating a scene without an Engine. \'SceneOnly\' will only work as a child of Engine, use \'Scene\' otherwise.') - return - } - - const { engine /*, canvas */ } = babylonJSContext; +const usePrevious = (value: T) => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; + +const Scene: React.FC = (props: SceneProps, context?: any) => { + const { engine } = useContext(BabylonJSContext) - this._scene = new BabylonJSScene(engine!, this.props.sceneOptions) - const updates : UpdatePayload = this._propsHandler.getPropertyUpdates(this._scene, {}, this.props as any, this._scene) + const [propsHandler]= useState(new FiberScenePropsHandler()); + const [sceneReady, setSceneReady] = useState(false); + const [scene, setScene] = useState>(null) + const [fiberRoot, setFiberRoot] = useState(null); + + // TODO: make this strongly typed + const [renderer, setRenderer] = useState>>(null); + const prevProps = usePrevious(props); + + useEffect(() => { + if (engine === null || scene === null || renderer === null || prevProps === undefined) { + return; + } + const updates : UpdatePayload = propsHandler.getPropertyUpdates(scene, prevProps, props, scene) if (updates !== null) { updates.forEach(propertyUpdate => { - applyUpdateToInstance(this._scene, propertyUpdate, 'scene') + applyUpdateToInstance(scene, propertyUpdate, 'scene') }) } - // TODO: Add keypress and other PointerEventTypes: - this._pointerDownObservable = this._scene.onPointerObservable.add((evt: PointerInfo) => { - - if(typeof this.props.onScenePointerDown === 'function') { - this.props.onScenePointerDown(evt, this._scene!) - } - - if (evt && evt.pickInfo && evt.pickInfo.hit && evt.pickInfo.pickedMesh) { - let mesh = evt.pickInfo.pickedMesh - - if (typeof this.props.onMeshPicked === 'function') { - this.props.onMeshPicked(mesh, this._scene!) - } else { - // console.log('onMeshPicked not being called') - } - } - }, PointerEventTypes.POINTERDOWN); + renderer.updateContainer( + + {props.children} + , + fiberRoot, + undefined, + () => { /* called after container is updated. we may want an external observable here */ } + ) + }) - // can only be assigned on init - if(typeof this.props.onScenePointerUp === 'function') { - this._pointerUpObservable = this._scene.onPointerObservable.add((evt: PointerInfo) => { - this.props.onScenePointerUp!(evt, this._scene!) - }, PointerEventTypes.POINTERUP) - }; + useEffect(() => { + // const onSceneReady = (eventData: BabylonJSScene, eventState: EventState) => { + // setSceneReady(true); + // } - // can only be assigned on init - if(typeof this.props.onScenePointerMove === 'function') { - this._pointerMoveObservable = this._scene.onPointerObservable.add((evt: PointerInfo) => { - this.props.onScenePointerMove!(evt, this._scene!) - }, PointerEventTypes.POINTERMOVE) - }; + const scene = new BabylonJSScene(engine!, props.sceneOptions) - if (typeof this.props.onSceneMount === 'function') { - this.props.onSceneMount({ - scene: this._scene, - canvas: this._scene.getEngine().getRenderingCanvas()! - }); - // TODO: console.error if canvas is not attached. runRenderLoop() is expected to be part of onSceneMount(). + // const onReadyObservable: Nullable> = scene.onReadyObservable.add(onSceneReady); + if (scene.isReady()) { + // scene.onReadyObservable.remove(onReadyObservable); + setSceneReady(true) + } else { + console.error('Scene is not ready. Report issue in react-babylonjs repo') } - // TODO: change enable physics to 'usePhysics' taking an object with a Vector3 and 'any'. - if (Array.isArray(this.props.enablePhysics)) { - this._scene.enablePhysics(this.props.enablePhysics[0], this.props.enablePhysics[1]); - } + setScene(scene); const isAsync = false // Disables experimental async rendering - + const container: Container = { - engine: this.props.babylonJSContext.engine, - canvas: this.props.babylonJSContext.canvas, - scene: this._scene, + engine: props.babylonJSContext.engine, + canvas: props.babylonJSContext.canvas, + scene: scene, rootInstance: { hostInstance: null, children: [], @@ -156,64 +142,97 @@ class Scene extends React.Component { } } - this._fiberRoot = this._reactReconcilerBabylonJs.createContainer(container, isAsync, false /* hydrate true == better HMR? */) - - this._reactReconcilerBabylonJs.injectIntoDevTools({ + const renderer = ReactReconciler(ReactBabylonJSHostConfig); + setRenderer(renderer) + const fiberRoot = renderer.createContainer(container, isAsync, false /* hydrate true == better HMR? */) + setFiberRoot(fiberRoot); + + renderer.injectIntoDevTools({ bundleType: process.env.NODE_ENV === 'production' ? 0 : 1, - version: '1.0.3', + version: '2.0.0', rendererPackageName: 'react-babylonjs' }) - // update the root Container - // console.log("updating rootContainer (1) reactElement") - return this._reactReconcilerBabylonJs.updateContainer( - - {this.props.children} - , this._fiberRoot, undefined /* TODO: try to dual-write for screen readers */, () => { /* empty */} - ) - } + const pointerDownObservable: Nullable> = scene.onPointerObservable.add( + (evt: PointerInfo) => { + if(typeof props.onScenePointerDown === 'function') { + props.onScenePointerDown(evt, scene) + } - componentDidUpdate (prevProps: any, prevState: any) { - const updates : UpdatePayload = this._propsHandler.getPropertyUpdates(this._scene!, prevProps, this.props as any, this._scene!) - if (updates !== null) { - updates.forEach(propertyUpdate => { - applyUpdateToInstance(this._scene, propertyUpdate, 'scene') - }) - } + if (evt && evt.pickInfo && evt.pickInfo.hit && evt.pickInfo.pickedMesh) { + let mesh = evt.pickInfo.pickedMesh + if (typeof props.onMeshPicked === 'function') { + props.onMeshPicked(mesh, scene) + } else { + // console.log('onMeshPicked not being called') + } + } + }, + PointerEventTypes.POINTERDOWN + ); - // In the docs it is mentioned that shouldComponentUpdate() may be treated as a hint one day - // avoid shouldComponentUpdate() => false, looks okay, but prop changes will lag behind 1 update. - this._reactReconcilerBabylonJs.updateContainer( - - {this.props.children} - , - this._fiberRoot!, - undefined, - () => { /* called after container is updated. we may want an external observable here */ } - ) - } + // can only be assigned on init + let pointerUpObservable: Nullable> = null; + if(typeof props.onScenePointerUp === 'function') { + pointerUpObservable = scene.onPointerObservable.add( + (evt: PointerInfo) => { + props.onScenePointerUp!(evt, scene) + }, + PointerEventTypes.POINTERUP + ) + }; - componentWillUnmount () { - if (this._pointerDownObservable) { - this._scene!.onPointerObservable.remove(this._pointerDownObservable); - } + // can only be assigned on init + let pointerMoveObservable: Nullable> = null; + if(typeof props.onScenePointerMove === 'function') { + pointerMoveObservable = scene.onPointerObservable.add( + (evt: PointerInfo) => { + props.onScenePointerMove!(evt, scene) + }, + PointerEventTypes.POINTERMOVE) + }; - if (this._pointerUpObservable) { - this._scene!.onPointerObservable.remove(this._pointerUpObservable); + if (typeof props.onSceneMount === 'function') { + props.onSceneMount({ + scene: scene, + canvas: scene.getEngine().getRenderingCanvas()! + }); + // TODO: console.error if canvas is not attached. runRenderLoop() is expected to be part of onSceneMount(). } - if (this._pointerMoveObservable) { - this._scene!.onPointerObservable.remove(this._pointerMoveObservable); + // TODO: change enable physics to 'usePhysics' taking an object with a Vector3 and 'any'. + // NOTE: must be enabled for updating container (cannot add impostors w/o physics enabled) + if (Array.isArray(props.enablePhysics)) { + scene.enablePhysics(props.enablePhysics[0], props.enablePhysics[1]); } - this._scene!.dispose() - } - render () { - return null; - } -} + // update the root Container + renderer.updateContainer( + + {props.children} + , fiberRoot, undefined, () => { /* empty */} + ) -// TODO: export a SceneOnly without engine and have this class create a default engine when not present. + return () => { + if (pointerDownObservable) { + scene.onPointerObservable.remove(pointerDownObservable); + } + + if (pointerUpObservable) { + scene.onPointerObservable.remove(pointerUpObservable); + } + + if (pointerMoveObservable) { + scene.onPointerObservable.remove(pointerMoveObservable); + } + + scene.dispose(); + } + }, + [/* no deps, so called only on un/mount */] + ) + + return null; +} -// for backwards compatibility we export a scene with an Engine. Engine is only needed with multi-scene. export default withBabylonJS(Scene) \ No newline at end of file diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 00000000..12d685b7 --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,25 @@ +import { useContext, useEffect } from 'react'; +import { Nullable, Observer, Scene, EventState } from '@babylonjs/core'; + +import { SceneContext } from './Scene' + +export type OnFrameRenderFn = (eventData: Scene, eventState: EventState) => void + +export function useBeforeRender(callback: OnFrameRenderFn, mask?: number, insertFirst?: boolean, callOnce?: boolean): void { + const {scene, sceneReady } = useContext(SceneContext); + + useEffect(() => { + if (sceneReady !== true || scene === null) { + return; + } + + const unregisterOnFirstCall: boolean = callOnce === true; + const sceneObserver: Nullable> = scene.onBeforeRenderObservable.add(callback, mask, insertFirst, undefined, unregisterOnFirstCall); + + if (unregisterOnFirstCall !== true) { + return () => { + scene.onBeforeRenderObservable.remove(sceneObserver); + } + } + }) +} \ No newline at end of file diff --git a/src/react-babylonjs.ts b/src/react-babylonjs.ts index b2c3db91..c7a36d9f 100644 --- a/src/react-babylonjs.ts +++ b/src/react-babylonjs.ts @@ -1,8 +1,9 @@ export * from "./generatedCode" export * from "./generatedProps" +export * from "./hooks" export * from "./customComponents" // TODO: Except for Skybox - these should not be exported. they are internal. -export { default as Engine, withBabylonJS, useBabylonEngine, useBabylonCanvas } from "./Engine" -export { default as Scene, withScene, WithSceneContext, SceneEventArgs, useBabylonScene } from "./Scene" +export { default as Engine, withBabylonJS, BabylonJSContext, useBabylonEngine, useBabylonCanvas } from "./Engine" +export { default as Scene, withScene, WithSceneContext, SceneContext, SceneEventArgs, useBabylonScene } from "./Scene" export { HostWithEvents, Model } from "./customHosts" diff --git a/stories/babylonjs/3-physics/hooks.stories.js b/stories/babylonjs/3-physics/hooks.stories.js new file mode 100644 index 00000000..d736ae30 --- /dev/null +++ b/stories/babylonjs/3-physics/hooks.stories.js @@ -0,0 +1,60 @@ +import React, { useContext, useRef } from 'react' +import { storiesOf } from '@storybook/react' +import { Engine, Scene as ReactScene, withScene, BabylonJSContext, SceneContext, useBeforeRender } from '../../../dist/react-babylonjs.es5' +import { Vector3, Color3 } from '@babylonjs/core/Maths/math' +import '../../style.css' + +const Scene = withScene(ReactScene) + +const ContextLogger = (props, context) => { + const ctx = useContext(BabylonJSContext) + console.log(`ctx-logger "${props.id}" BabylonJSContext is:`, ctx) + + const ctx2 = useContext(SceneContext) + console.log(`ctx-logger "${props.id}" SceneContext is:`, ctx2) + return null; +} + +const RotatingBoxScene = (props) => { + const boxRef = useRef(null); + + useBeforeRender((scene) => { + if (boxRef.current) { + var deltaTimeInMillis = scene.getEngine().getDeltaTime(); + + const rpm = props.rpm || 10; + boxRef.current.hostInstance.rotation.y += ((rpm / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000)); + } + }) + + return ( + <> + + + + + + + + ) +} + +const RenderHooks = (props, context) => { + return ( + + + + + + + + ) +} + +export default storiesOf('Physics and Hooks', module) + .add('Render Hooks', () => ( +
+ +
+ ) +) diff --git a/stories/babylonjs/3-physics/more-hooks.stories.js b/stories/babylonjs/3-physics/more-hooks.stories.js new file mode 100644 index 00000000..2ebe7984 --- /dev/null +++ b/stories/babylonjs/3-physics/more-hooks.stories.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react' +import { Engine, Scene, useBabylonEngine, useBabylonCanvas, useBabylonScene } from '../../../dist/react-babylonjs.es5' +import { Vector3 } from '@babylonjs/core' + +const MyScene = () => { + const engine = useBabylonEngine(); + const canvas = useBabylonCanvas(); + const scene = useBabylonScene(); + + // engine and canvas are null. they are not currently bridged. + // https://github.com/konvajs/react-konva/issues/188#issuecomment-478302062 + console.log('MyScene', { engine, canvas, scene }) + + return ( + <> + + + + + ) +} + +const EngineChild = () => { + const engine = useBabylonEngine(); + const canvas = useBabylonCanvas(); + + console.log('EngineChild', { engine, canvas}); + return null; +} + +const RenderHooks = () => { + return ( + + + + + + + ) +} + +export default storiesOf('Physics and Hooks', module) + .add('Convenience Hooks', () => ( +
+
Look at console.
+ +
+ ) +) \ No newline at end of file diff --git a/stories/babylonjs/3-physics/physics.stories.js b/stories/babylonjs/3-physics/physics.stories.js index 6273410f..42a313d0 100644 --- a/stories/babylonjs/3-physics/physics.stories.js +++ b/stories/babylonjs/3-physics/physics.stories.js @@ -74,7 +74,7 @@ const BouncyPlayground = () => { ) } -export default storiesOf('Physics + Hooks', module) +export default storiesOf('Physics and Hooks', module) .add('Bouncy Playground', () => (