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', () => (
+
+ )
+)
\ 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', () => (