Skip to content

Commit

Permalink
Create Tunnel (#168)
Browse files Browse the repository at this point in the history
* feat: ✨ tunneling components form one renderer to another via valtio proxy state
* remove unnecessary imports in tunnel.tsx
* fix: 🐛 changed from valtio to zustand as state for tunnel, since valtio would loose refs in some cases
* fix: 🐛 fix html loosing anchor node - camera transformation matrix is 0 on first render, led to vec being [NaN, NaN], therefore just added an isNaN check.
* fix: 🐛 use key/value store instead of array for tunnel store to fix removing of components. improvement: filtering of tunnels at exit
* fix: 🐛 fix missing rerender on removal of tunnelentrance. using spread operator on remove
  • Loading branch information
dennemark authored Nov 8, 2021
1 parent ca5c942 commit 059968b
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 1 deletion.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"react-dom": ">=17"
},
"dependencies": {
"react-reconciler": "^0.26.2"
"react-reconciler": "^0.26.2",
"zustand": "^3.6.1"
}
}
81 changes: 81 additions & 0 deletions src/customComponents/Tunnel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useEffect, ReactElement, Fragment} from 'react';
import create from "zustand"


type Store = {
store: {[key:string]: ReactElement},
add:(key: string, el:ReactElement)=>void,
remove:(key: string)=>void
}

/**
* A tunnel allows to render components of one renderer inside another.
* I.e. babylonjs components normally need to live within Engine component.
* A tunnel entrance allows to position components in a different renderer, such as ReactDOM
* and move it to the tunnel exit, that must exist within Engine component.
*
* The nice thing is, even refs can be used outside of Engine context.
*
* The createTunnel function creates a tunnel entrance and exit component.
* The tunnel works one-directional.
* TunnelEntrance only accepts components that are allowed to live within the renderer of TunnelExit.
* Multiple entrances and exits are possible.
*
* If components need to be rendererd the other way around, a second Tunnel is needed.
*
*/
const createTunnel = () => {

const useStore = create<Store>((set, get)=> ({
store: {},
add: (key, el) => set((state)=>{
return {...state, store: {...state.store, [key]: el}}}),
remove: (key) => set((state)=>{
if(key in state.store){
delete state.store[key]
}
return {...state, store: {...state.store}}})
}))

/**
* Tunnel Entrance
* @param uid a unique identifier - similar to key. if same uid exists in app, only one tunnel entrance will end in tunnel exit
* @returns nothing
*/
const TunnelEntrance = ({uid, children}: {uid: string,children: ReactElement}) => {
const add = useStore(state => state.add)
const remove = useStore(state => state.remove)
useEffect(()=>{
add(uid, children);
return ()=> {
remove(uid)
}
},[children])

return <></>
}

/**
* Tunnel Exit
* @param uids optionally add uids of tunnel entrances to only show components of these entrances
* @returns Components of Tunnel Entrance
*/
const TunnelExit = ({uids}: {uids?: string[]}) => {
const state = useStore(state => state.store)


return <>{Object.keys(state)
.filter(key => uids ? uids.includes(key) : true)
.map((key) => {
return <Fragment key={key}>
{state[key]}
</Fragment>
})
}
</>
}

return {TunnelEntrance, TunnelExit};
}

export default createTunnel;
1 change: 1 addition & 0 deletions src/customComponents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
// export {default as ModelLifecycleListener} from "../customHosts/ModelLifecycleListener"
export {default as Skybox} from "./Skybox";
export {default as Model} from "./Model";
export {default as createTunnel} from "./Tunnel";
export {default as Html} from "./Html";
92 changes: 92 additions & 0 deletions storybook/stories/babylonjs/Basic/tunnel.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useEffect, useRef, useState } from 'react';
import '@babylonjs/inspector';
import { Engine, Scene, useBeforeRender, createTunnel} from '../../../../dist/react-babylonjs';
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
import { Color3 } from '@babylonjs/core/Maths/math.color';
import '../../style.css'

export default { title: 'Babylon Basic' };


/**
* A tunnel allows to render components of one renderer inside another.
* I.e. babylonjs components normally need to live within Engine component.
* A tunnel entrance allows to position components in a different renderer, such as ReactDOM
* and move it to the tunnel exit, that must exist within Engine component.
*
* The nice thing is, even refs can be used outside of Engine context.
*
* The createTunnel function creates a tunnel entrance and exit component.
* The tunnel works one-directional.
* TunnelEntrance only accepts components that are allowed to live within the renderer of TunnelExit.
* Multiple entrances and exits are possible.
*
* If components need to be rendererd the other way around, a second Tunnel is needed.
*
*/
const {TunnelEntrance, TunnelExit} = createTunnel();

const rpm = 5;


const WithTunnel = ({uids})=>{
const [_, setReady] = useState(false);
const ref = useRef(null);
useBeforeRender((scene) => {

if (ref.current !== null) {
const deltaTimeInMillis = scene.getEngine().getDeltaTime();
ref.current.rotation.y += ((rpm / 60) * Math.PI * 2 * (deltaTimeInMillis / 1000));
}
})

useEffect(() => {
setReady(true);
}, [ref.current])

return <transformNode name="transform-node" ref={ref}>
<TunnelExit uids={uids}/>
</transformNode>

}


export const CreateTunnel = () => {

/** ref to tunnel is possible */
const ref = useRef(null)

const [position, setPosition] = useState(1)

useEffect(()=>{
if(ref.current){
ref.current.position.x = Math.abs(position-4);
}
},[position])

return <div style={{ flex: 1, display: 'flex' }}>
<button onClick={()=>setPosition(Math.abs(position-1))}>Set Position</button>
{/** Multiple Tunnel Entrances */}
<TunnelEntrance uid="hyperloop">
<box name="box" ref={ref} position={new Vector3(0, position, 0)}>
<standardMaterial name="mat" diffuseColor={Color3.Blue() } specularColor={Color3.Black()} />
</box>
</TunnelEntrance>
<TunnelEntrance uid="subway">
<box name="box" position={new Vector3(position, 0, 1)}>
<standardMaterial name="mat" diffuseColor={Color3.Red() } specularColor={Color3.Black()} />
</box>
</TunnelEntrance>
<Engine antialias adaptToDeviceRatio canvasId='babylonJS'>
<Scene>
<freeCamera name='camera1' position={new Vector3(0, 5, -10)}
setTarget={[Vector3.Zero()]} />
<WithTunnel uids={["subway"]}/>
<transformNode name="node" position={new Vector3(5, 0, 0)}>
<WithTunnel />
</transformNode>
<hemisphericLight name='light1' intensity={0.7} direction={Vector3.Up()} />
</Scene>
</Engine>
</div>
}

0 comments on commit 059968b

Please sign in to comment.