Skip to content

Commit

Permalink
Add declarative grid support #173
Browse files Browse the repository at this point in the history
  • Loading branch information
brianzinn committed Dec 5, 2021
1 parent 07c7e04 commit 9b03813
Show file tree
Hide file tree
Showing 13 changed files with 253 additions and 114 deletions.
1 change: 1 addition & 0 deletions src/CreatedInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface InstanceMetadataParameter {
isMaterial?: boolean // indicates a custom component created by end-user has been created
isGUI3DControl?: boolean // does not work with 2D
isGUI2DControl?: boolean // does not work with 3D
isGUI2DGrid?: boolean
isTexture?: boolean
customType?: boolean // not used by code-gen
isCamera?: boolean
Expand Down
13 changes: 12 additions & 1 deletion src/CustomProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ export type Control3DCustomProps = {
onControlAdded?: (instance: CreatedInstance<any>) => void
} & CustomProps;

export type Control2DCustomProps = {
/**
* ???
*/
gridColumn?: number
/**
* ???
*/
gridRow?: number
} & CustomProps;

export type MaterialCustomProps = {
/**
* For attaching the same material to multiple meshes (by mesh name)
Expand Down Expand Up @@ -144,4 +155,4 @@ export type VirtualKeyboardCustomProps = {
/**
* A union of all CustomProps as a convenience typing and easier maintenance in other areas of code (ie: CreatedInstance and HostConfig)
*/
export type AnyCustomProps = CustomProps & (AbstractMeshCustomProps & ADTCustomProps & CameraCustomProps & Control3DCustomProps & GizmoCustomProps & GlowLayerCustomProps & VirtualKeyboardCustomProps & ShadowGeneratorCustomProps & MaterialCustomProps)
export type AnyCustomProps = CustomProps & (AbstractMeshCustomProps & ADTCustomProps & CameraCustomProps & Control2DCustomProps & Control3DCustomProps & GizmoCustomProps & GlowLayerCustomProps & VirtualKeyboardCustomProps & ShadowGeneratorCustomProps & MaterialCustomProps)
24 changes: 13 additions & 11 deletions src/ReactBabylonJSHostConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import { HasPropsHandlers, PropertyUpdate, UpdatePayload, PropsHandler, CustomPr
import { LifecycleListener } from "./LifecycleListener";
import { GeneratedParameter, CreationType } from './codeGenerationDescriptors';
import { applyUpdateToInstance, applyInitialPropsToCreatedInstance } from './UpdateInstance';
import { HostRegistrationStore } from './HostRegistrationStore';
import { DynamicHost, HostRegistrationStore } from './HostRegistrationStore';

import RowDefinitionLifecycleListener, { RowDefinition } from './customHosts/grid'
import { RowDefinition } from './customHosts/grid/rowDefinition';
import { ColumnDefinition } from './customHosts/grid/columnDefinition';
import { ValueAndUnit } from '@babylonjs/gui';

// ** TODO: switch to node module 'scheduler', but compiler is not finding 'require()' exports currently...
type HostCreatedInstance<T> = CreatedInstance<T> | undefined
Expand Down Expand Up @@ -313,14 +315,14 @@ const ReactBabylonJSHostConfig: HostConfig<

const classDefinition = (GENERATED as any)[`Fiber${underlyingClassName}`]

let dynamicRegisteredHost = undefined;
let dynamicRegisteredHost: DynamicHost<any, any> | undefined = undefined;
if (classDefinition === undefined) {
// ValueAndUnit does not have setters for value/isPixel, so did not generate (see ./customHosts/grid/)
// TODO: make a map of custom "known hosts"
const knownHostElements = ['rowDefinition', 'columnDefinition'];
if (knownHostElements.indexOf(underlyingClassName) !== -1) {
dynamicRegisteredHost = RowDefinition;
console.log('NEW dynamic registered host:', RowDefinition);
const ownDynamicHosts: Record<string, DynamicHost<ValueAndUnit, any>> = {
'rowDefinition': RowDefinition,
'columnDefinition': ColumnDefinition
};
if (underlyingClassName in ownDynamicHosts) {
dynamicRegisteredHost = ownDynamicHosts[underlyingClassName];
} else {
dynamicRegisteredHost = HostRegistrationStore.GetRegisteredHost(type);
}
Expand Down Expand Up @@ -355,6 +357,8 @@ const ReactBabylonJSHostConfig: HostConfig<
skipAutoAttach: props.skipAutoAttach,
attachGizmoToMesh: props.attachGizmoToMesh,
attachGizmoToNode: props.attachGizmoToNode,
gridColumn: props.gridColumn,
gridRow: props.gridRow
};

if (customProps.assignFrom !== undefined) {
Expand All @@ -365,7 +369,6 @@ const ReactBabylonJSHostConfig: HostConfig<
}
else if (dynamicRegisteredHost !== undefined) {
metadata = dynamicRegisteredHost.metadata;
console.log('NEW metadata:', metadata);
if (metadata.delayCreation !== true) {
babylonObject = dynamicRegisteredHost.hostFactory(scene!);
}
Expand Down Expand Up @@ -485,7 +488,6 @@ const ReactBabylonJSHostConfig: HostConfig<
lifecycleListener = new (CUSTOM_HOSTS as any)[metadataLifecycleListenerName + 'LifecycleListener'](scene, props);
} else if (dynamicRegisteredHost?.lifecycleListenerFactory) {
lifecycleListener = dynamicRegisteredHost.lifecycleListenerFactory(scene!, props);
console.log('NEW lifecycle listener:', lifecycleListener);
} else {
lifecycleListener = new CUSTOM_HOSTS.FallbackLifecycleListener(scene!, props);
}
Expand Down
2 changes: 1 addition & 1 deletion src/customComponents/Html.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function isObjectVisible(el: AbstractMesh, camera: Camera, occlude: AbstractMesh

let hit = camera.getScene().pickWithRay(ray, (m) => (occlude.length > 0 ? (occlude.indexOf(m) !== -1) : m !== el.parent) && m.name !== "skybox");

console.log(hit);
// console.log(hit);
return hit ? (hit.pickedMesh?.name === el.parent?.name || hit.distance * hit.distance >= Vector3.DistanceSquared(objectPos, cameraPos)) : false;
}

Expand Down
14 changes: 11 additions & 3 deletions src/customHosts/AdvancedDynamicTextureLifecycleListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import BaseLifecycleListener from './BaseLifecycleListener';
import { ADTCustomProps, VirtualKeyboardCustomProps } from '../CustomProps';
import { CreatedInstance } from '../CreatedInstance';
import { FiberAdvancedDynamicTextureProps } from '../generatedProps';
import { Children } from 'react';
import { Grid } from '@babylonjs/gui/2D/controls/grid';

export default class AdvancedDynamicTextureLifecycleListener extends BaseLifecycleListener<AdvancedDynamicTexture, FiberAdvancedDynamicTextureProps> {

Expand Down Expand Up @@ -49,13 +51,19 @@ export default class AdvancedDynamicTextureLifecycleListener extends BaseLifecyc
}
}

addControls(instance: CreatedInstance<AdvancedDynamicTexture>) {
addControls(instance: CreatedInstance<any>) {
// When there is a panel, it must be added before the children. Otherwise there is no UtilityLayer to attach to.
// 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 };
if(instance.metadata.isGUI2DGrid === true) {
const { gridRow, gridColumn } = child.customProps;
(instance.hostInstance as Grid).addControl(child.hostInstance, gridRow, gridColumn);
} else {
instance.hostInstance!.addControl(child.hostInstance);
}

child.state = child.state ? {...child.state, added: true} : { added: true };
}
})

Expand Down
10 changes: 8 additions & 2 deletions src/customHosts/GUI2DControlLifecycleListener.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Control } from '@babylonjs/gui/2D/controls/control.js';
import { Grid } from '@babylonjs/gui/2D/controls/grid.js';
import { VirtualKeyboard } from '@babylonjs/gui/2D/controls/virtualKeyboard.js';

import BaseLifecycleListener from './BaseLifecycleListener';
Expand Down Expand Up @@ -52,8 +53,13 @@ export default class GUI2DControlLifecycleListener extends BaseLifecycleListener
addControls(instance: CreatedInstance<any>) {
instance.children.forEach(child => {
if (child.metadata.isGUI2DControl === true) {
// console.warn(`calling [${instance.hostInstance.name}].addControl(${child.hostInstance.name})`);
instance.hostInstance.addControl(child.hostInstance);
if(instance.metadata.isGUI2DGrid === true) {
const { gridRow, gridColumn } = child.customProps;
(instance.hostInstance as Grid).addControl(child.hostInstance, gridRow, gridColumn);
} else {
instance.hostInstance!.addControl(child.hostInstance);
}

child.state = { added: true };
}
})
Expand Down
53 changes: 53 additions & 0 deletions src/customHosts/grid/columnDefinition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Scene } from "@babylonjs/core/scene.js";
import { Nullable } from "@babylonjs/core/types.js";
import { Grid } from "@babylonjs/gui/2D/controls/grid.js";
import { ValueAndUnit } from "@babylonjs/gui/2D/valueAndUnit.js";
import { RowOrColumnDefinitionProps, RowOrColumnDefinitionPropsHandler } from ".";
import { CreatedInstance } from "../../CreatedInstance";
import { DynamicHost } from "../../HostRegistrationStore";
import DeferredCreationLifecycleListener from "../DeferredCreationLifecycleListener";

/**
* We delay instantiation and when parented add a column definition to parent "Grid".
*/
export default class ColumnDefinitionLifecycleListener extends DeferredCreationLifecycleListener<ValueAndUnit, any> {

private _grid: Grid | undefined;

createInstance = (instance: CreatedInstance<ValueAndUnit>, scene: Scene, props: RowOrColumnDefinitionProps): Nullable<ValueAndUnit> => {
if (!this._grid) {
return null;
}
this._grid.addColumnDefinition(props.value, props.unit === ValueAndUnit.UNITMODE_PIXEL);
instance.hostInstance = this._grid.getColumnDefinition(this._grid.columnCount - 1)!;
return instance.hostInstance;
}

onParented(parent: CreatedInstance<any>, child: CreatedInstance<any>): any {
super.onParented(parent, child);
// TODO: search hierarchy for a grid, but it should always be parent.
this._grid = parent.hostInstance;
}
}

export const ColumnDefinition: DynamicHost<ValueAndUnit, RowOrColumnDefinitionProps> = {
hostElementName: "columnDefinition",

propHandlerInstance: new RowOrColumnDefinitionPropsHandler(),

hostFactory: (scene: Scene) => null,

lifecycleListenerFactory: (scene: Scene, props: any) => new ColumnDefinitionLifecycleListener(scene, props),

createInfo: {
"creationType": "Constructor",
"libraryLocation": "ValueAndUnit",
"namespace": "@babylonjs/gui",
"parameters": [/* value and unit! */]
},
// TODO: this should be "M"etadata
metadata: {
"className": "ColumnDefinition",
"delayCreation": true,
}
}
97 changes: 15 additions & 82 deletions src/customHosts/grid/index.ts
Original file line number Diff line number Diff line change
@@ -1,120 +1,53 @@
import { Key, ReactNode, Ref } from "react";
import { Scene } from "@babylonjs/core/scene.js";
import { Nullable } from "@babylonjs/core/types.js";
import { Grid } from "@babylonjs/gui/2D/controls/grid.js";
import { ValueAndUnit } from "@babylonjs/gui/2D/valueAndUnit.js";

import { CreatedInstance } from "../../CreatedInstance";
import { DynamicHost } from "../../HostRegistrationStore";
import { checkPrimitiveDiff, HasPropsHandlers, PropertyUpdate, PropsHandler } from "../../PropsHandler";
import DeferredCreationLifecycleListener from "../DeferredCreationLifecycleListener";


export type GridNode = {
key?: Key;
ref?: Ref<ReactNode>; // will return value and unit?
};

export type RowDefinitionProps = {
/**
* Numerical value to apply to height
*/
height: number
/**
* Indicating if the value is stored as pixel
*/
isPixel?: boolean
}

export type ColumnDefinitionProps = {
width: number
isPixel?: boolean
export type RowOrColumnDefinitionProps = {
value: number
unit?: number
}

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface IntrinsicElements {
rowDefinition: RowDefinitionProps & GridNode;
columnDefinition: ColumnDefinitionProps & GridNode;
rowDefinition: RowOrColumnDefinitionProps & GridNode;
columnDefinition: RowOrColumnDefinitionProps & GridNode;
}
}
}

export class CustomRowDefinitionPropsHandler implements PropsHandler<RowDefinitionProps> {
getPropertyUpdates(oldProps: RowDefinitionProps, newProps: RowDefinitionProps): PropertyUpdate[] | null {
export class RowOrColumnDefinitionPropsHandlers implements PropsHandler<RowOrColumnDefinitionProps> {
getPropertyUpdates(oldProps: RowOrColumnDefinitionProps, newProps: RowOrColumnDefinitionProps): PropertyUpdate[] | null {
const changedProps: PropertyUpdate[] = []
checkPrimitiveDiff(oldProps.height, newProps.height, 'height', changedProps)
checkPrimitiveDiff(oldProps.isPixel, newProps.isPixel, 'isPixel', changedProps)
checkPrimitiveDiff(oldProps.value, newProps.value, 'value', changedProps);
checkPrimitiveDiff(oldProps.unit, newProps.unit, 'unit', changedProps);
return changedProps.length === 0 ? null : changedProps;
}
}

/**
* Handles property updates.
*/
export class RowDefinitionPropsHandler implements HasPropsHandlers<RowDefinitionProps> {
private propsHandlers: PropsHandler<RowDefinitionProps>[];
export class RowOrColumnDefinitionPropsHandler implements HasPropsHandlers<RowOrColumnDefinitionProps> {
private propsHandlers: PropsHandler<RowOrColumnDefinitionProps>[];

constructor() {
this.propsHandlers = [
// NOTE: Cannot set property isPixel of [object Object] which has only a getter
// new CustomRowDefinitionPropsHandler()
new RowOrColumnDefinitionPropsHandlers()
];
}

getPropsHandlers(): PropsHandler<RowDefinitionProps>[] {
getPropsHandlers(): PropsHandler<RowOrColumnDefinitionProps>[] {
return this.propsHandlers;
}

addPropsHandler(propHandler: PropsHandler<RowDefinitionProps>): void {
addPropsHandler(propHandler: PropsHandler<RowOrColumnDefinitionProps>): void {
this.propsHandlers.push(propHandler);
}
}

export const RowDefinition: DynamicHost<ValueAndUnit, RowDefinitionProps> = {
hostElementName: "rowDefinition",

propHandlerInstance: new RowDefinitionPropsHandler(),

hostFactory: (scene: Scene) => null,

lifecycleListenerFactory: (scene: Scene, props: any) => new RowDefinitionLifecycleListener(scene, props),

createInfo: {
"creationType": "Constructor",
"libraryLocation": "ValueAndUnit",
"namespace": "@babylonjs/gui",
"parameters": [/* value and unit! */]
},
// TODO: this should be "M"etadata
metadata: {
"className": "RowDefinition",
"delayCreation": true,
}
}

/**
* We delay instantiation and when parented add a row definition to parent "Grid".
*/
export default class RowDefinitionLifecycleListener extends DeferredCreationLifecycleListener<ValueAndUnit, any> {

private _grid: Grid | undefined;

createInstance = (instance: CreatedInstance<ValueAndUnit>, scene: Scene, props: RowDefinitionProps): Nullable<ValueAndUnit> => {
if (!this._grid) {
return null;
}
// these should be set from the props handler. TODO: test.
this._grid.addRowDefinition(props.height, props.isPixel);
instance.hostInstance = this._grid.getRowDefinition(this._grid.rowCount - 1)!;
console.log('added row:', instance.hostInstance);
return instance.hostInstance;
}

onParented(parent: CreatedInstance<any>, child: CreatedInstance<any>): any {
super.onParented(parent, child);
// TODO: search hierarchy for a grid, but it should always be parent.
console.log('adding row definition to:', parent.metadata, parent.hostInstance);
this._grid = parent.hostInstance;
}
}
Loading

0 comments on commit 9b03813

Please sign in to comment.