Skip to content

Commit

Permalink
Add custom props handler (and chroma-js story). (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
brianzinn committed Mar 27, 2020
1 parent 8848151 commit 16cc9d2
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 17 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"@types/react-reconciler": "^0.16.0",
"babel-loader": "^8.0.6",
"cannon": "^0.6.2",
"chroma-js": "^2.1.0",
"colors": "^1.3.3",
"commitizen": "^3.0.5",
"coveralls": "^3.0.2",
Expand Down
144 changes: 142 additions & 2 deletions src/PropsHandler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Vector3, Color3, Color4 } from '@babylonjs/core/Maths/math'
import { Control } from '@babylonjs/gui/2D/controls/control'
import { Observable, FresnelParameters, BaseTexture } from '@babylonjs/core'
import { type } from 'os'
import { Observable, FresnelParameters, BaseTexture, Nullable } from '@babylonjs/core'

// TODO: type/value need to be joined, as the method will have multiple.
export interface PropertyUpdate {
Expand All @@ -26,6 +25,121 @@ export interface HasPropsHandlers<U> {
addPropsHandler(propHandler: PropsHandler<U>): void
}

export type PropertyUpdateProcessResult<T> = {
processed: boolean,
value: Nullable<T>
}

/**
* NOTE: the applyAnimatedValues from react-spring always has `oldProp` undefined, so we force set anything provided.
* Would be more efficient to only handle the props passed in.
*/
export interface ICustomPropsHandler<T, U> {
/**
* Friendly name to identify the handler.
*/
readonly name: string;

/**
* The type of prop (ie: Vector3, Color3) to register for
*/
readonly propChangeType: string;

/**
* Can be used to influence the ordering the handler fires compared to other custom handlers.
* @todo not implemented (future enhancement)
*/
readonly order: number;

/**
* Like a visitor, except if 'true' is returned the call chain is broken.
* So, if you want to override a 'Vector3' with a string and the type is string
* then the regular handler will be bypassed.
* @param propChangeType
*/
accept(newProp: T): boolean

// Check old vs new and return proper value, if any.
process(oldProp: T | undefined, newProp: T): PropertyUpdateProcessResult<U>

}

export class CustomPropsHandler {

private static _registeredPropsHandlers: Record<string, ICustomPropsHandler<any, any>[]> = {};

/**
* Register a new props handler
*
* @param handler to register for props (a handler can only be registered once per )
* @returns a reference that can be used to unregister.
*/
public static RegisterPropsHandler(propsHandler: ICustomPropsHandler<any, any>): ICustomPropsHandler<any, any> {
const propsChangeType: string = propsHandler.propChangeType;

if (!Array.isArray(CustomPropsHandler._registeredPropsHandlers[propsChangeType])) {
CustomPropsHandler._registeredPropsHandlers[propsChangeType] = [];
}

const registeredHandlers: ICustomPropsHandler<any, any>[] = CustomPropsHandler._registeredPropsHandlers[propsChangeType];

const match = registeredHandlers.find(h => h === propsHandler);
if (match !== undefined) {
console.warn(`Handler can only be registered once per type [${propsChangeType}]`);
return match;
}

registeredHandlers.push(propsHandler);
return propsHandler;
}

/**
* Unregister a props handler that was previously registered.
*
* @param propsHandler
*
* @returns if the props handler was found and unregistered
*/
public static UnregisterPropsHandler(propsHandlerToUnregister: ICustomPropsHandler<any, any>): boolean {
const propsChangeType: string = propsHandlerToUnregister.propChangeType;

if (!Array.isArray(CustomPropsHandler._registeredPropsHandlers[propsChangeType])) {
console.warn(`cannot find ${propsHandlerToUnregister.name} to unregister.`)
return false;
}

const registeredHandlers: ICustomPropsHandler<any, any>[] = CustomPropsHandler._registeredPropsHandlers[propsChangeType];

const index: number = registeredHandlers.indexOf(propsHandlerToUnregister);

if (index === -1) {
console.warn(`cannot find ${propsHandlerToUnregister.name} to unregister.`)
return false;
}

CustomPropsHandler._registeredPropsHandlers[propsChangeType] = registeredHandlers.slice(index, 1);
return true;
}

public static HandlePropsChange(propsChangeType: PropChangeType, oldProp: any, newProp: any): PropertyUpdateProcessResult<any> {
const registeredHandlers: ICustomPropsHandler<any, any>[] = CustomPropsHandler._registeredPropsHandlers[propsChangeType];
const notProcessed: PropertyUpdateProcessResult<any> = { processed: false, value: null};
if (registeredHandlers === undefined) {
return notProcessed;
}

for (const handler of registeredHandlers) {
if (handler.accept(newProp)) {
const propertyUpdatedProcessResult: PropertyUpdateProcessResult<any> = handler.process(oldProp, newProp);
// console.log(`handler '${handler.name}'custom prop processing result:`, propertyUpdatedProcessResult);
return propertyUpdatedProcessResult;
}
}

return notProcessed;
}
}

export enum PropChangeType {
Primitive = "Primitive",
Vector3 = "Vector3",
Expand All @@ -40,7 +154,25 @@ export enum PropChangeType {
Texture = "Texture"
}

const handledCustomProp = (changeType: PropChangeType, oldProp: any, newProp: any, propertyName: string, propertyType: string, changedProps: PropertyUpdate[]): boolean => {
const processedResult = CustomPropsHandler.HandlePropsChange(changeType, oldProp, newProp);
if (processedResult.processed) {
// console.log(`handled ${PropChangeType.Color3} on ${propertyName} - bypassing built-in handler - new Value: ${JSON.stringify(processedResult.value ?? {})}`);
changedProps.push({
propertyName,
type: propertyType,
changeType,
value: processedResult.value!
})
}
return processedResult.processed;
}

export const checkVector3Diff = (oldProp: Vector3 | undefined, newProp: Vector3 | undefined, propertyName: string, propertyType: string, changedProps: PropertyUpdate[]): void => {
if (handledCustomProp(PropChangeType.Vector3, oldProp, newProp, propertyName, propertyType, changedProps)) {
return;
}

if (newProp && (!oldProp || !oldProp.equals(newProp))) {
changedProps.push({
propertyName,
Expand All @@ -52,6 +184,10 @@ export const checkVector3Diff = (oldProp: Vector3 | undefined, newProp: Vector3
}

export const checkColor3Diff = (oldProp: Color3 | undefined, newProp: Color3 | undefined, propertyName: string, propertyType: string, changedProps: PropertyUpdate[]): void => {
if (handledCustomProp(PropChangeType.Color3, oldProp, newProp, propertyName, propertyType, changedProps)) {
return;
}

if (newProp && (!oldProp || !oldProp.equals(newProp))) {
changedProps.push({
propertyName,
Expand All @@ -63,6 +199,10 @@ export const checkColor3Diff = (oldProp: Color3 | undefined, newProp: Color3 | u
}

export const checkColor4Diff = (oldProp: Color4 | undefined, newProp: Color4 | undefined, propertyName: string, propertyType: string, changedProps: PropertyUpdate[]): void => {
if (handledCustomProp(PropChangeType.Color4, oldProp, newProp, propertyName, propertyType, changedProps)) {
return;
}

// Color4.equals() not added until PR #5517
if (newProp && (!oldProp || oldProp.r !== newProp.r || oldProp.g !== newProp.g || oldProp.b !== newProp.b || oldProp.a !== newProp.a)) {
changedProps.push({
Expand Down
1 change: 0 additions & 1 deletion src/ReactBabylonJSHostConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ function createCreatedInstance<T, U extends HasPropsHandlers<any>>(
} as CreatedInstance<T>
}


/**
* remove instance's children recursively
*
Expand Down
13 changes: 6 additions & 7 deletions src/customComponents/AdvancedDynamicTextureLifecycleListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { AdvancedDynamicTexture } from "@babylonjs/gui/2D/advancedDynamicTexture


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

constructor(scene: Scene, props: any) {
this.scene = scene
Expand Down Expand Up @@ -102,8 +102,7 @@ export default class AdvancedDynamicTextureLifecycleListener implements Lifecycl
onUnmount(): void {/* empty */}
}

export class ADTFullscreenUILifecycleListener extends AdvancedDynamicTextureLifecycleListener {
constructor(scene: Scene, props: any) {
super(scene, props);
}
}
/**
* This is attached by convention in react-reconciler HostConfig.
*/
export class ADTFullscreenUILifecycleListener extends AdvancedDynamicTextureLifecycleListener {/* empty */}
12 changes: 12 additions & 0 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useContext, useEffect } from 'react';
import { Nullable, Observer, Scene, EventState } from '@babylonjs/core';

import { SceneContext } from './Scene'
import { ICustomPropsHandler, CustomPropsHandler } from './PropsHandler';

export type OnFrameRenderFn = (eventData: Scene, eventState: EventState) => void

Expand All @@ -23,3 +24,14 @@ export function useBeforeRender(callback: OnFrameRenderFn, mask?: number, insert
}
})
}

export function useCustomPropsHandler(propsHandler: ICustomPropsHandler<any, any>/*, deps?: React.DependencyList | undefined*/): void {
// running inside useEffect is too late for initial props
CustomPropsHandler.RegisterPropsHandler(propsHandler);
useEffect(() => {
return () => {
// console.warn('de-registering on unmount', propsHandler.name);
CustomPropsHandler.UnregisterPropsHandler(propsHandler);
}
}, [])
}
15 changes: 8 additions & 7 deletions src/react-babylonjs.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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 * from "./generatedCode";
export * from "./generatedProps";
export * from "./hooks";
export * from "./customComponents"; // TODO: Except for Skybox - these should not be exported. they are internal.
export * from "./PropsHandler";

export { default as Engine, withBabylonJS, BabylonJSContext, useBabylonEngine, useBabylonCanvas } from "./Engine"
export { default as Scene, withScene, WithSceneContext, SceneContext, 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 } from "./customHosts"
export { HostWithEvents } from "./customHosts";
Loading

0 comments on commit 16cc9d2

Please sign in to comment.