Skip to content

Commit

Permalink
add 'assignFrom' for dynamic assignment to parent properties #114
Browse files Browse the repository at this point in the history
  • Loading branch information
brianzinn committed Jan 31, 2021
1 parent 538c5da commit defebad
Show file tree
Hide file tree
Showing 24 changed files with 490 additions and 369 deletions.
4 changes: 4 additions & 0 deletions src/CreatedInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export type CustomProps = {
* Assign to this property on the parent. Parent property is cleared on umnount.
*/
assignTo?: string | string[]
/**
* Assigned from this existing property on the parent. Will assign this host element to a parent property that contains an existing instance (no new instances created and no dispose called).
*/
assignFrom?: string
/**
* for VRExperienceHelper
*/
Expand Down
79 changes: 45 additions & 34 deletions src/ReactBabylonJSHostConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,17 +298,39 @@ const ReactBabylonJSHostConfig: HostConfig<
let dynamicRegisteredHost = undefined;
if (classDefinition === undefined) {
dynamicRegisteredHost = HostRegistrationStore.GetRegisteredHost(type);
}
}

if (classDefinition === undefined && dynamicRegisteredHost === undefined) {
throw new Error(`Cannot generate type '${type}/${underlyingClassName}' inside 'react-babylonjs' (ie: no DOM rendering on HTMLCanvas)`)
}

let metadata: CreatedInstanceMetadata;
let babylonObject: any | undefined = undefined
let disposeInstanceOnUnmount = true;

if (dynamicRegisteredHost !== undefined) {
// TODO: Add these to just the 'type' of component they apply to.
let customProps: CustomProps = {
childrenAsContent: props.childrenAsContent === true, // ie: Button3D.container instead of .addControl()
createForParentMesh: props.createForParentMesh === true, // AdvancedDynamicTexture attached to parent mesh (TODO: add forMeshByName="")
onControlAdded: typeof props.onControlAdded === "function" ? props.onControlAdded : undefined,
connectControlNames: props.connectControlNames, // VirtualKeyboard to connect inputs by name.
defaultKeyboard: props.defaultKeyboard === true,
linkToTransformNodeByName: props.linkToTransformNodeByName,
shadowCasters: props.shadowCasters,
shadowCastersExcluding: props.shadowCastersExcluding,
attachToMeshesByName: props.attachToMeshesByName, // for materials - otherwise will attach to first parent that accepts materials
assignTo: props.assignTo, // here a lifecycle listener can dynamically attach to another property (ie: Mesh to DynamicTerrain -> 'mesh.material')
assignFrom: props.assignFrom,
disposeInstanceOnUnmount: props.assignFrom === undefined
}

if (customProps.assignFrom !== undefined) {
// will be assigned once parented in lifecyclelistener
metadata = dynamicRegisteredHost !== undefined
? dynamicRegisteredHost.metadata
: classDefinition.Metadata;
}
else if (dynamicRegisteredHost !== undefined)
{
metadata = dynamicRegisteredHost.metadata;
babylonObject = dynamicRegisteredHost.hostFactory(scene!);
} else {
Expand All @@ -322,7 +344,7 @@ const ReactBabylonJSHostConfig: HostConfig<
// instanceof will check prototype and derived classes (ie: can assign Mesh instance to a Node)
if (props.fromInstance instanceof clazz) {
babylonObject = props.fromInstance;
disposeInstanceOnUnmount = props.disposeInstanceOnUnmount === true;
customProps.disposeInstanceOnUnmount = props.disposeInstanceOnUnmount === true;
} else {
// prevent assigning incorrect type.
console.error('fromInstance wrong type.', props.fromInstance, clazz);
Expand Down Expand Up @@ -398,58 +420,47 @@ const ReactBabylonJSHostConfig: HostConfig<
? dynamicRegisteredHost.propHandlerInstance
: new (GENERATED as any)[`Fiber${underlyingClassName}`]()

let lifecycleListener: LifecycleListener<any> | undefined = undefined

// TODO: Add these to just the 'type' of component they apply to.
let customProps: CustomProps = {
childrenAsContent: props.childrenAsContent === true, // ie: Button3D.container instead of .addControl()
createForParentMesh: props.createForParentMesh === true, // AdvancedDynamicTexture attached to parent mesh (TODO: add forMeshByName="")
onControlAdded: typeof props.onControlAdded === "function" ? props.onControlAdded : undefined,
connectControlNames: props.connectControlNames, // VirtualKeyboard to connect inputs by name.
defaultKeyboard: props.defaultKeyboard === true,
linkToTransformNodeByName: props.linkToTransformNodeByName,
shadowCasters: props.shadowCasters,
shadowCastersExcluding: props.shadowCastersExcluding,
attachToMeshesByName: props.attachToMeshesByName, // for materials - otherwise will attach to first parent that accepts materials
assignTo: props.assignTo, // here a lifecycle listener can dynamically attach to another property (ie: Mesh to DynamicTerrain -> 'mesh.material')
disposeInstanceOnUnmount
}

// Consider these being dynamically attached to a list, much like PropsHandlers<T>
let metadataLifecycleListenerName: string | undefined;
if (metadata.isMaterial === true) {
lifecycleListener = new CUSTOM_HOSTS.MaterialsLifecycleListener();
metadataLifecycleListenerName = 'Materials';
} else if (metadata.isTexture === true) { // must be before .isGUI2DControl, since ADT/FullScreenUI declare both.
lifecycleListener = new CUSTOM_HOSTS.TexturesLifecycleListener();
metadataLifecycleListenerName = 'Textures';
} else if (metadata.isGUI3DControl === true) {
lifecycleListener = new CUSTOM_HOSTS.GUI3DControlLifecycleListener(scene);
metadataLifecycleListenerName = 'GUI3DControl';
} else if (metadata.isGUI2DControl === true) {
lifecycleListener = new CUSTOM_HOSTS.GUI2DControlLifecycleListener();
metadataLifecycleListenerName = 'GUI2DControl';
} else if (metadata.isCamera === true) {
lifecycleListener = new CUSTOM_HOSTS.CameraLifecycleListener(scene, props, scene!.getEngine().getRenderingCanvas() as HTMLCanvasElement);
} else if (metadata.isNode) {
lifecycleListener = new CUSTOM_HOSTS.NodeLifecycleListener();
} else if (metadata.isBehavior) {
lifecycleListener = new CUSTOM_HOSTS.BehaviorLifecycleListener();
metadataLifecycleListenerName = 'Camera';
} else if (metadata.isNode === true) {
metadataLifecycleListenerName = 'Node';
} else if (metadata.isBehavior === true) {
metadataLifecycleListenerName = 'Behavior';
}

let lifecycleListener: LifecycleListener<any>;
// here we dynamically assign listeners for specific types.
// TODO: need to double-check because we are using 'camelCase'
if ((CUSTOM_HOSTS as any)[underlyingClassName + "LifecycleListener"] !== undefined) {
lifecycleListener = new (CUSTOM_HOSTS as any)[underlyingClassName + "LifecycleListener"](scene, props);
} else if (metadataLifecycleListenerName !== undefined) {
lifecycleListener = new (CUSTOM_HOSTS as any)[metadataLifecycleListenerName + 'LifecycleListener'](scene, props);
} else {
lifecycleListener = new CUSTOM_HOSTS.FallbackLifecycleListener(scene!, props);
}

let createdReference = createCreatedInstance(underlyingClassName, babylonObject, fiberObject, metadata, customProps, lifecycleListener);

if (lifecycleListener && lifecycleListener.onCreated) {
if (lifecycleListener.onCreated) {
lifecycleListener.onCreated(createdReference, scene!);
}

// Here we dynamically attach known props handlers. Will be adding more in code generation for GUI - also for lifecycle mgmt.
// Here we dynamically attach known props handlers. This is a better way to have mixins and dynamic props handling via composition (and registration).
if (createdReference.metadata && createdReference.metadata.isTargetable === true) {
fiberObject.addPropsHandler(new CUSTOM_HOSTS.TargetPropsHandler(scene!));
}

if (metadata.delayCreation !== true) {
if (metadata.delayCreation !== true && customProps.assignFrom === undefined) {
applyInitialPropsToCreatedInstance(createdReference, props);

// This property is only needed by `applyPropsToRef`, so if the propsHandlers can be made available there in another way then we don't need this property.
Expand All @@ -462,7 +473,7 @@ const ReactBabylonJSHostConfig: HostConfig<
}

// TODO: make this an opt-in -- testing inspectable metadata (and our Custom Props, which we want to be more specific to Type):
// TODO: use {} instead of NULL and use the late-binding from 'v3'.
// TODO: use {} instead of NULL and use the late-binding from 'v3' branch (also for deferred creation/assignFrom).
if (createdReference.hostInstance) {
Object.defineProperty(createdReference.hostInstance, 'metadata-className', {
get() { return createdReference.metadata.className; },
Expand Down
78 changes: 29 additions & 49 deletions src/customHosts/AdvancedDynamicTextureLifecycleListener.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,47 @@
import { CreatedInstance } from "../CreatedInstance"
import { LifecycleListener } from "../LifecycleListener"
import { Color3, Scene, StandardMaterial, Mesh } from "@babylonjs/core"
import { FiberAdvancedDynamicTextureProps } from "../generatedProps"
import { AdvancedDynamicTexture } from "@babylonjs/gui/2D/advancedDynamicTexture";
import { Color3, StandardMaterial, Mesh } from '@babylonjs/core'
import { AdvancedDynamicTexture } from '@babylonjs/gui/2D/advancedDynamicTexture';

import BaseLifecycleListener from './BaseLifecycleListener';
import { CreatedInstance } from '../CreatedInstance'
import { FiberAdvancedDynamicTextureProps } from '../generatedProps'


export default class AdvancedDynamicTextureLifecycleListener implements LifecycleListener<AdvancedDynamicTexture> {
protected props: FiberAdvancedDynamicTextureProps;
protected scene: Scene

constructor(scene: Scene, props: any) {
this.scene = scene
this.props = props
}

onParented(parent: CreatedInstance<any>, child: CreatedInstance<any>): any { /* empty */}

onChildAdded(child: CreatedInstance<any>, parent: CreatedInstance<any>): any { /* empty */}
export default class AdvancedDynamicTextureLifecycleListener extends BaseLifecycleListener<AdvancedDynamicTexture, FiberAdvancedDynamicTextureProps> {

onMount(instance: CreatedInstance<AdvancedDynamicTexture>): void {
instance.state = {added: true}; // allow children to attach
this.addControls(instance)
this.addControls(instance);

if (instance.customProps.createForParentMesh) {
// console.log('for parent mesh', instance.parent ? instance.parent.babylonJsObject : 'error: no parent object')

let mesh: Mesh = instance.parent!.hostInstance // should crawl parent hierarchy for a mesh
let mesh: Mesh = instance.parent!.hostInstance; // should crawl parent hierarchy for a mesh
// console.error('we will be attaching the mesh:', mesh.name, mesh);

const material = new StandardMaterial("AdvancedDynamicTextureMaterial", mesh.getScene())
material.backFaceCulling = false
material.diffuseColor = Color3.Black()
material.specularColor = Color3.Black()
const material = new StandardMaterial('AdvancedDynamicTextureMaterial', mesh.getScene());
material.backFaceCulling = false;
material.diffuseColor = Color3.Black();
material.specularColor = Color3.Black();

if(instance.hostInstance === undefined) {
console.error('missing instance')
console.error('missing instance');
} else {
if (this.props.hasAlpha) {
material.diffuseTexture = instance.hostInstance
material.emissiveTexture = instance.hostInstance
instance.hostInstance.hasAlpha = true
material.diffuseTexture = instance.hostInstance;
material.emissiveTexture = instance.hostInstance;
instance.hostInstance.hasAlpha = true;
} else {
material.emissiveTexture = instance.hostInstance
material.opacityTexture = instance.hostInstance
material.emissiveTexture = instance.hostInstance;
material.opacityTexture = instance.hostInstance;
}
}

mesh.material = material
mesh.material = material;

// set to true unless explicitly not wanted.
// connects the texture to a hosting mesh to enable interactions
let supportPointerMove = (this.props as any).supportPointerMove !== false ? true : false

instance.hostInstance!.attachToMesh(mesh, supportPointerMove)
instance.hostInstance!.attachToMesh(mesh, supportPointerMove);
}
}

Expand All @@ -62,45 +50,37 @@ export default class AdvancedDynamicTextureLifecycleListener implements Lifecycl
// This project before 'react-reconciler' was added from parent up the tree. 'react-reconciler' wants to do the opposite.
instance.children.forEach(child => {
if (child.metadata.isGUI2DControl === true) {
instance.hostInstance!.addControl(child.hostInstance)
child.state = { added: true }
instance.hostInstance!.addControl(child.hostInstance);
child.state = { added: true };
}
})

if (instance.customProps.connectControlNames !== undefined && Array.isArray(instance.customProps.connectControlNames)) {
let controlNames: string[] = instance.customProps.connectControlNames
let root = instance
let controlNames: string[] = instance.customProps.connectControlNames;
let root = instance;
while (root.parent !== null) {
root = root.parent
root = root.parent;
}
this.connect(
instance,
root,
controlNames
)
);
}

instance.children.forEach(child => {
this.addControls(child)
this.addControls(child);
})
}

connect(keyboard: CreatedInstance<any>, searchInstance: CreatedInstance<any>, controlNames: string[]) {
if (searchInstance.metadata.isGUI2DControl && searchInstance.hostInstance && controlNames.indexOf(searchInstance.hostInstance.name) !== -1) {
// console.log(keyboard.hostInstance, '.connect(->', searchInstance.hostInstance)
keyboard.hostInstance.connect(searchInstance.hostInstance)
keyboard.hostInstance.connect(searchInstance.hostInstance);
}

searchInstance.children.forEach(child =>
this.connect(
keyboard,
child,
controlNames
)
)
searchInstance.children.forEach(child => this.connect(keyboard,child,controlNames));
}

onUnmount(): void {/* empty */}
}

/**
Expand Down
32 changes: 32 additions & 0 deletions src/customHosts/BaseLifecycleListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Scene } from '@babylonjs/core';
import { CreatedInstance } from '../CreatedInstance';
import { LifecycleListener } from '../LifecycleListener';
import { applyInitialPropsToCreatedInstance } from '../UpdateInstance';

export default abstract class BaseLifecycleListener<T, U> implements LifecycleListener<T> {

constructor(protected scene: Scene, protected props: U) {/* empty */}

onParented(parent: CreatedInstance<any>, child: CreatedInstance<any>): void {
if (child.customProps.assignFrom !== undefined) {
if (parent.hostInstance[child.customProps.assignFrom] === undefined) {
console.error(`Cannot find existing property ${child.customProps.assignFrom} on parent component (check your 'assignFrom')`)
} else {
// TODO: should we try to verify types like we do in 'fromInstance'?
child.hostInstance = parent.hostInstance[child.customProps.assignFrom];
if (child.deferredCreationProps && child.propsHandlers) {
applyInitialPropsToCreatedInstance(child, child.deferredCreationProps);
} else {
console.warn('cannot assign deferred props. they are lost.');
}
child.deferredCreationProps = undefined;
}
}
}

onChildAdded(child: CreatedInstance<any>, parent: CreatedInstance<any>): void { /* empty */};

onMount(instance: CreatedInstance<T>): void { /* empty */};

onUnmount(): void {/* empty */};
}
25 changes: 9 additions & 16 deletions src/customHosts/BaseShadowGeneratorLifecycleListener.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import { CreatedInstance } from "../CreatedInstance"
import { Scene, AbstractMesh, Observer, Nullable, DirectionalLight, ShadowGenerator } from "@babylonjs/core"
import DeferredCreationLifecycleListener from "./DeferredCreationLifecycleListener"
import { Scene, AbstractMesh, Observer, Nullable, DirectionalLight, ShadowGenerator } from '@babylonjs/core';

import { CreatedInstance } from '../CreatedInstance';
import DeferredCreationLifecycleListener from './DeferredCreationLifecycleListener';

/**
* Create a Shadow Generator (CascadedShadowGenerator extends ShadowGenerator, so add/remove shadow casters is from parent class)
*/
export default abstract class BaseShadowGeneratorLifecycleListener<T extends ShadowGenerator> extends DeferredCreationLifecycleListener<T> {
export default abstract class BaseShadowGeneratorLifecycleListener<T extends ShadowGenerator, U> extends DeferredCreationLifecycleListener<T, U> {

private onMeshAddedObservable: Nullable<Observer<AbstractMesh>> = null;
private onMeshRemovedObservable: Nullable<Observer<AbstractMesh>> = null;

constructor(scene: Scene, props: any) {
super(scene, props);
}

abstract createShadowGenerator: (mapSize: number, light: DirectionalLight, useFullFloatFirst?: boolean) => T;

abstract get generatorType(): string;
Expand All @@ -26,9 +23,9 @@ export default abstract class BaseShadowGeneratorLifecycleListener<T extends Sha
if (tmp.metadata.isShadowLight) {
// console.log(`Creating ${this.generatorType} size: ${props.mapSize} with light`, tmp.hostInstance);
instance.hostInstance = result = this.createShadowGenerator(props.mapSize, tmp.hostInstance, props.useFullFloatFirst);
break
break;
}
tmp = tmp.parent
tmp = tmp.parent;
}

if (instance.hostInstance === undefined) {
Expand All @@ -38,7 +35,7 @@ export default abstract class BaseShadowGeneratorLifecycleListener<T extends Sha

if (instance.customProps.shadowCasters) {
if (!Array.isArray(instance.customProps.shadowCasters)) {
console.error("Shadow casters must be an array (of strings).", instance.customProps.shadowCasters);
console.error('Shadow casters must be an array (of strings).', instance.customProps.shadowCasters);
return null;
}

Expand All @@ -64,7 +61,7 @@ export default abstract class BaseShadowGeneratorLifecycleListener<T extends Sha
})
} else if (instance.customProps.shadowCastersExcluding) {
if (!Array.isArray(instance.customProps.shadowCastersExcluding)) {
console.error("Shadow casters excluding must be an array (of strings).", instance.customProps.shadowCastersExcluding);
console.error('Shadow casters excluding must be an array (of strings).', instance.customProps.shadowCastersExcluding);
} else {
let shadowCastersExcluding: string[] = instance.customProps.shadowCastersExcluding;

Expand Down Expand Up @@ -92,10 +89,6 @@ export default abstract class BaseShadowGeneratorLifecycleListener<T extends Sha
return result;
}

onParented(parent: CreatedInstance<any>, child: CreatedInstance<any>): any {/* empty */}

onChildAdded(child: CreatedInstance<any>, parent: CreatedInstance<any>): any {/* empty */}

onUnmount(): void {
if (this.onMeshAddedObservable !== null) {
this.scene.onNewMeshAddedObservable.remove(this.onMeshAddedObservable);
Expand Down
Loading

0 comments on commit defebad

Please sign in to comment.