From c9454239545b1bea25434bb10b4ea669528838b8 Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Wed, 23 Aug 2023 07:44:09 -0500 Subject: [PATCH] Refactored into pure functions for key diagram manager functionality. --- src/app.ts | 3 +- src/diagram/diagramEntityActionManager.ts | 28 +++ src/diagram/diagramEventHandler.ts | 95 ++++++++++ src/diagram/diagramManager.ts | 186 +++---------------- src/diagram/diagramShapePhysics.ts | 74 -------- src/diagram/functions/applyScaling.ts | 16 ++ src/diagram/functions/diagramShapePhysics.ts | 70 +++++++ src/diagram/functions/fromDiagramEntity.ts | 0 src/diagram/functions/toDiagramEntity.ts | 0 src/diagram/presentationManager.ts | 53 ++++++ src/diagram/presentationStep.ts | 45 +++++ src/menus/{baseMenu.ts => abstractMenu.ts} | 6 +- src/menus/configMenu.ts | 4 +- src/menus/integrationMenu.ts | 4 +- src/toolbox/toolbox.ts | 129 +++++++------ src/util/deepCopy.ts | 16 ++ 16 files changed, 423 insertions(+), 306 deletions(-) create mode 100644 src/diagram/diagramEntityActionManager.ts create mode 100644 src/diagram/diagramEventHandler.ts delete mode 100644 src/diagram/diagramShapePhysics.ts create mode 100644 src/diagram/functions/applyScaling.ts create mode 100644 src/diagram/functions/diagramShapePhysics.ts create mode 100644 src/diagram/functions/fromDiagramEntity.ts create mode 100644 src/diagram/functions/toDiagramEntity.ts create mode 100644 src/diagram/presentationManager.ts create mode 100644 src/diagram/presentationStep.ts rename src/menus/{baseMenu.ts => abstractMenu.ts} (77%) create mode 100644 src/util/deepCopy.ts diff --git a/src/app.ts b/src/app.ts index b4a20f1..9bfd63d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -46,10 +46,11 @@ export class App { const scene = new Scene(engine); const persistenceManager = new IndexdbPersistenceManager("diagram"); + const controllers = new Controllers(); const toolbox = new Toolbox(scene, controllers); - const diagramManager = new DiagramManager(scene, controllers, toolbox); + diagramManager.setPersistenceManager(persistenceManager); const config = new AppConfig(persistenceManager); const environment = new CustomEnvironment(scene, "default", config); diff --git a/src/diagram/diagramEntityActionManager.ts b/src/diagram/diagramEntityActionManager.ts new file mode 100644 index 0000000..da26e20 --- /dev/null +++ b/src/diagram/diagramEntityActionManager.ts @@ -0,0 +1,28 @@ +import {ActionManager, ExecuteCodeAction, PlaySoundAction, Scene} from "@babylonjs/core"; +import {DiaSounds} from "../util/diaSounds"; +import {Controllers} from "../controllers/controllers"; +import log from "loglevel"; + +export class DiagramEntityActionManager { + _actionManager: ActionManager; + private readonly logger = log.getLogger('DiagramEntityActionManager'); + + constructor(scene: Scene, sounds: DiaSounds, controllers: Controllers) { + this._actionManager = new ActionManager(scene); + this._actionManager.registerAction( + new PlaySoundAction(ActionManager.OnPointerOverTrigger, sounds.tick)); + this._actionManager.registerAction( + new ExecuteCodeAction(ActionManager.OnPointerOverTrigger, (evt) => { + controllers.controllerObserver.notifyObservers({ + type: 'pulse', + gripId: evt?.additionalData?.pickResult?.gripTransform?.id + }) + this.logger.debug(evt); + }) + ); + } + + public get manager() { + return this._actionManager; + } +} \ No newline at end of file diff --git a/src/diagram/diagramEventHandler.ts b/src/diagram/diagramEventHandler.ts new file mode 100644 index 0000000..b58fb5a --- /dev/null +++ b/src/diagram/diagramEventHandler.ts @@ -0,0 +1,95 @@ +import {DiagramEvent, DiagramEventType} from "./diagramEntity"; +import log from "loglevel"; +import {MeshConverter} from "./meshConverter"; +import {applyPhysics} from "./functions/diagramShapePhysics"; +import {ActionManager, Color3, PhysicsMotionType, Scene} from "@babylonjs/core"; +import {TextLabel} from "./textLabel"; +import {Toolbox} from "../toolbox/toolbox"; +import {DiaSounds} from "../util/diaSounds"; +import {IPersistenceManager} from "../integration/iPersistenceManager"; + + +export function diagramEventHandler(event: DiagramEvent, + scene: Scene, + toolbox: Toolbox, + physicsEnabled: boolean, + actionManager: ActionManager, + sounds: DiaSounds, + persistenceManager: IPersistenceManager) { + const entity = event.entity; + let mesh; + if (entity) { + mesh = scene.getMeshById(entity.id); + } + if (!mesh && event?.entity?.template) { + const toolMesh = scene.getMeshById("tool-" + event.entity.template + "-" + event.entity.color); + if (!toolMesh && event.type != DiagramEventType.CHANGECOLOR) { + log.debug('no mesh found for ' + event.entity.template + "-" + event.entity.color, 'adding it'); + toolbox.updateToolbox(event.entity.color); + } + mesh = MeshConverter.fromDiagramEntity(event.entity, scene); + if (mesh) { + mesh.actionManager = actionManager; + if (physicsEnabled) { + applyPhysics(sounds, mesh, scene, PhysicsMotionType.DYNAMIC); + } + } + } + switch (event.type) { + case DiagramEventType.CLEAR: + break; + case DiagramEventType.DROPPED: + break; + case DiagramEventType.DROP: + if (mesh.metadata.template.indexOf('#') > -1) { + persistenceManager.modify(mesh); + TextLabel.updateTextNode(mesh, entity.text); + } + + break; + case DiagramEventType.ADD: + persistenceManager.add(mesh); + if (!mesh.actionManager) { + mesh.actionManager = actionManager; + } + if (physicsEnabled) { + applyPhysics(sounds, mesh, scene); + } + + break; + case DiagramEventType.MODIFY: + persistenceManager.modify(mesh); + if (physicsEnabled) { + applyPhysics(sounds, mesh, scene); + } + + break; + case DiagramEventType.CHANGECOLOR: + if (!event.oldColor) { + if (!event.newColor) { + persistenceManager.changeColor(null, Color3.FromHexString(event.entity.color)); + this.logger.info("Received color change event, sending entity color as new color"); + } else { + this.logger.info("Received color change event, no old color, sending new color"); + persistenceManager.changeColor(null, event.newColor); + } + } else { + if (event.newColor) { + this.logger.info("changing color from " + event.oldColor + " to " + event.newColor); + persistenceManager.changeColor(event.oldColor, event.newColor); + } else { + this.logger.error("changing color from " + event.oldColor + ", but no new color found"); + } + } + + break; + case DiagramEventType.REMOVE: + if (mesh) { + persistenceManager.remove(mesh) + mesh?.physicsBody?.dispose(); + mesh.dispose(); + sounds.exit.play(); + } + break; + } +} diff --git a/src/diagram/diagramManager.ts b/src/diagram/diagramManager.ts index c020bb2..bc59455 100644 --- a/src/diagram/diagramManager.ts +++ b/src/diagram/diagramManager.ts @@ -1,16 +1,4 @@ -import { - AbstractMesh, - ActionManager, - Color3, - ExecuteCodeAction, - InstancedMesh, - Mesh, - Observable, - PhysicsMotionType, - PlaySoundAction, - Scene, - Vector3 -} from "@babylonjs/core"; +import {AbstractMesh, Color3, InstancedMesh, Mesh, Observable, PhysicsMotionType, Scene} from "@babylonjs/core"; import {DiagramEntity, DiagramEvent, DiagramEventType} from "./diagramEntity"; import {IPersistenceManager} from "../integration/iPersistenceManager"; import {MeshConverter} from "./meshConverter"; @@ -18,9 +6,13 @@ import log from "loglevel"; import {Controllers} from "../controllers/controllers"; import {DiaSounds} from "../util/diaSounds"; import {AppConfig} from "../util/appConfig"; -import {TextLabel} from "./textLabel"; import {Toolbox} from "../toolbox/toolbox"; -import {DiagramShapePhysics} from "./diagramShapePhysics"; +import {PresentationManager} from "./presentationManager"; +import {DiagramEntityActionManager} from "./diagramEntityActionManager"; +import {diagramEventHandler} from "./diagramEventHandler"; +import {deepCopy} from "../util/deepCopy"; +import {applyPhysics} from "./functions/diagramShapePhysics"; +import {applyScaling} from "./functions/applyScaling"; export class DiagramManager { @@ -29,25 +21,20 @@ export class DiagramManager { private persistenceManager: IPersistenceManager = null; private readonly toolbox: Toolbox; private readonly scene: Scene; - private sounds: DiaSounds; + private readonly sounds: DiaSounds; + private readonly controllers: Controllers; + private readonly diagramEntityActionManager: DiagramEntityActionManager + private presentationManager: PresentationManager; + private _config: AppConfig; constructor(scene: Scene, controllers: Controllers, toolbox: Toolbox) { this.sounds = new DiaSounds(scene); this.scene = scene; this.toolbox = toolbox; this.controllers = controllers; - this.actionManager = new ActionManager(this.scene); - this.actionManager.registerAction( - new PlaySoundAction(ActionManager.OnPointerOverTrigger, this.sounds.tick)); - this.actionManager.registerAction( - new ExecuteCodeAction(ActionManager.OnPointerOverTrigger, (evt) => { - this.controllers.controllerObserver.notifyObservers({ - type: 'pulse', - gripId: evt?.additionalData?.pickResult?.gripTransform?.id - }) - this.logger.debug(evt); - }) - ); + this.presentationManager = new PresentationManager(this.scene); + this.diagramEntityActionManager = new DiagramEntityActionManager(this.scene, this.sounds, this.controllers); + if (this.onDiagramEventObservable.hasObservers()) { this.logger.warn("onDiagramEventObservable already has Observers, you should be careful"); } @@ -56,6 +43,7 @@ export class DiagramManager { }, -1, true, this, false); this.onDiagramEventObservable.add(this.onDiagramEvent, -1, true, this); this.logger.debug("DiagramManager constructed"); + scene.onMeshRemovedObservable.add((mesh) => { if (mesh?.metadata?.template) { if (mesh.metadata.template != '#connection-template') { @@ -73,29 +61,14 @@ export class DiagramManager { }); } - private _config: AppConfig; - - private getPersistenceManager(): IPersistenceManager { - if (!this.persistenceManager) { - this.logger.warn("persistenceManager not set"); - return null; - } - return this.persistenceManager; - } - - private readonly actionManager: ActionManager; - private controllers: Controllers; - public get config(): AppConfig { return this._config; } - public setPersistenceManager(persistenceManager: IPersistenceManager) { this.persistenceManager = persistenceManager; this._config = new AppConfig(persistenceManager); this.persistenceManager.updateObserver.add(this.onRemoteEvent, -1, true, this); } - public createCopy(mesh: AbstractMesh, copy: boolean = false): AbstractMesh { let newMesh; if (!mesh.isAnInstance) { @@ -103,152 +76,43 @@ export class DiagramManager { } else { newMesh = new InstancedMesh("new", (mesh as InstancedMesh).sourceMesh); } - newMesh.actionManager = this.actionManager; + newMesh.actionManager = this.diagramEntityActionManager.manager; newMesh.position = mesh.absolutePosition.clone(); if (mesh.absoluteRotationQuaternion) { newMesh.rotation = mesh.absoluteRotationQuaternion.toEulerAngles().clone(); } else { this.logger.error("no rotation quaternion"); } - if (copy) { - newMesh.scaling = mesh.scaling.clone(); - } else { - if (this.config.current?.createSnap) { - newMesh.scaling.x = this.config.current?.createSnap; - newMesh.scaling.y = this.config.current?.createSnap; - newMesh.scaling.z = this.config.current?.createSnap; - } else { - newMesh.scaling = Vector3.One(); - } - - - } + applyScaling(mesh, newMesh, copy, this.config.current?.createSnap); newMesh.material = mesh.material; - - newMesh.metadata = this.deepCopy(mesh.metadata); + newMesh.metadata = deepCopy(mesh.metadata); if (this.config.current?.physicsEnabled) { - DiagramShapePhysics.applyPhysics(this.sounds, newMesh, this.scene); + applyPhysics(this.sounds, newMesh, this.scene); } - this.persistenceManager.add(newMesh); return newMesh; } - private deepCopy ? V : never>(source: T): T { - if (Array.isArray(source)) { - return source.map(item => (this.deepCopy(item))) as T & U[] - } - if (source instanceof Date) { - return new Date(source.getTime()) as T & Date - } - if (source && typeof source === 'object') { - return (Object.getOwnPropertyNames(source) as (keyof T)[]).reduce((o, prop) => { - Object.defineProperty(o, prop, Object.getOwnPropertyDescriptor(source, prop)!) - o[prop] = this.deepCopy(source[prop]) - return o - }, Object.create(Object.getPrototypeOf(source))) - } - return source - } - private onRemoteEvent(event: DiagramEntity) { this.logger.debug(event); const toolMesh = this.scene.getMeshById("tool-" + event.template + "-" + event.color); if (!toolMesh && (event.template != '#connection-template')) { log.debug('no mesh found for ' + event.template + "-" + event.color, 'adding it'); - //this.getPersistenceManager()?.changeColor(null, Color3.FromHexString(event.color)); this.toolbox.updateToolbox(event.color); } const mesh = MeshConverter.fromDiagramEntity(event, this.scene); - mesh.actionManager = this.actionManager; + mesh.actionManager = this.diagramEntityActionManager.manager; if (event.parent) { mesh.parent = this.scene.getMeshById(event.parent); } if (this.config.current?.physicsEnabled) { - DiagramShapePhysics.applyPhysics(this.sounds, mesh, this.scene, PhysicsMotionType.DYNAMIC); + applyPhysics(this.sounds, mesh, this.scene, PhysicsMotionType.DYNAMIC); } } private onDiagramEvent(event: DiagramEvent) { - this.logger.debug(event.type); - const entity = event.entity; - let mesh; - if (entity) { - mesh = this.scene.getMeshById(entity.id); - } - if (!mesh && event?.entity?.template) { - const toolMesh = this.scene.getMeshById("tool-" + event.entity.template + "-" + event.entity.color); - if (!toolMesh && event.type != DiagramEventType.CHANGECOLOR) { - log.debug('no mesh found for ' + event.entity.template + "-" + event.entity.color, 'adding it'); - this.toolbox.updateToolbox(event.entity.color); - } - mesh = MeshConverter.fromDiagramEntity(event.entity, this.scene); - if (mesh) { - mesh.actionManager = this.actionManager; - if (this.config.current.physicsEnabled) { - DiagramShapePhysics.applyPhysics(this.sounds, mesh, this.scene, PhysicsMotionType.DYNAMIC); - } - } - } - switch (event.type) { - case DiagramEventType.CLEAR: - break; - case DiagramEventType.DROPPED: - break; - case DiagramEventType.DROP: - if (mesh.metadata.template.indexOf('#') > -1) { - this.getPersistenceManager()?.modify(mesh); - TextLabel.updateTextNode(mesh, entity.text); - } - - break; - case DiagramEventType.ADD: - this.getPersistenceManager()?.add(mesh); - if (!mesh.actionManager) { - mesh.actionManager = this.actionManager; - } - if (this.config.current.physicsEnabled) { - DiagramShapePhysics - .applyPhysics(this.sounds, mesh, this.scene); - } - - break; - case DiagramEventType.MODIFY: - this.getPersistenceManager()?.modify(mesh); - if (this.config.current.physicsEnabled) { - DiagramShapePhysics - .applyPhysics(this.sounds, mesh, this.scene); - } - - break; - case DiagramEventType.CHANGECOLOR: - if (!event.oldColor) { - if (!event.newColor) { - this.getPersistenceManager()?.changeColor(null, Color3.FromHexString(event.entity.color)); - this.logger.info("Recieved color change event, sending entity color as new color"); - } else { - this.logger.info("Recieved color change event, no old color, sending new color"); - this.getPersistenceManager()?.changeColor(null, event.newColor); - } - } else { - if (event.newColor) { - this.logger.info("changing color from " + event.oldColor + " to " + event.newColor); - this.getPersistenceManager()?.changeColor(event.oldColor, event.newColor); - } else { - this.logger.error("changing color from " + event.oldColor + ", but no new color found"); - } - } - - break; - case DiagramEventType.REMOVE: - if (mesh) { - this.getPersistenceManager()?.remove(mesh) - mesh?.physicsBody?.dispose(); - mesh.dispose(); - this.sounds.exit.play(); - - } - break; - } + diagramEventHandler( + event, this.scene, this.toolbox, this.config.current.physicsEnabled, + this.diagramEntityActionManager.manager, this.sounds, this.persistenceManager); } } \ No newline at end of file diff --git a/src/diagram/diagramShapePhysics.ts b/src/diagram/diagramShapePhysics.ts deleted file mode 100644 index 59619f2..0000000 --- a/src/diagram/diagramShapePhysics.ts +++ /dev/null @@ -1,74 +0,0 @@ -import log from "loglevel"; -import {DiaSounds} from "../util/diaSounds"; -import {AbstractMesh, PhysicsAggregate, PhysicsMotionType, PhysicsShapeType, Scene} from "@babylonjs/core"; - -export class DiagramShapePhysics { - private static logger: log.Logger = log.getLogger('DiagramShapePhysics'); - - public static applyPhysics(sounds: DiaSounds, mesh: AbstractMesh, scene: Scene, motionType?: PhysicsMotionType) { - if (!mesh?.metadata?.template) { - this.logger.error("applyPhysics: mesh.metadata.template is null", mesh); - return; - } - if (mesh.metadata.template == '#connection-template') { - return; - } - if (!scene) { - this.logger.error("applyPhysics: mesh or scene is null"); - return; - } - - if (mesh.physicsBody) { - mesh.physicsBody.dispose(); - } - - let shapeType = PhysicsShapeType.BOX; - switch (mesh.metadata.template) { - case "#sphere-template": - shapeType = PhysicsShapeType.SPHERE; - break; - case "#cylinder-template": - shapeType = PhysicsShapeType.CYLINDER; - break; - case "#cone-template": - shapeType = PhysicsShapeType.CONVEX_HULL; - break; - - } - let mass = mesh.scaling.x * mesh.scaling.y * mesh.scaling.z * 10; - - const aggregate = new PhysicsAggregate(mesh, - shapeType, {mass: mass, restitution: .02, friction: .9}, scene); - const body = aggregate.body; - body.setLinearDamping(1.95); - body.setAngularDamping(1.99); - - if (motionType) { - body - .setMotionType(motionType); - } else { - if (mesh.parent) { - body - .setMotionType(PhysicsMotionType.ANIMATED); - } else { - body - .setMotionType(PhysicsMotionType.DYNAMIC); - } - } - body.setCollisionCallbackEnabled(true); - body.getCollisionObservable().add((event) => { - - if (event.impulse < 10 && event.impulse > 1) { - const sound = sounds.bounce; - sound.setVolume(event.impulse / 10); - sound.attachToMesh(mesh); - sound.play(); - } - }, -1, false, this); - //body.setMotionType(PhysicsMotionType.ANIMATED); - body.setLinearDamping(.95); - body.setAngularDamping(.99); - body.setGravityFactor(0); - - } -} \ No newline at end of file diff --git a/src/diagram/functions/applyScaling.ts b/src/diagram/functions/applyScaling.ts new file mode 100644 index 0000000..f68fb6a --- /dev/null +++ b/src/diagram/functions/applyScaling.ts @@ -0,0 +1,16 @@ +import {AbstractMesh, Vector3} from "@babylonjs/core"; + +export function applyScaling(oldMesh: AbstractMesh, + newMesh: AbstractMesh, + copy: boolean, + snap: number) { + if (copy) { + newMesh.scaling = oldMesh.scaling.clone(); + } else { + if (snap) { + newMesh.scaling.set(snap, snap, snap); + } else { + newMesh.scaling = Vector3.One(); + } + } +} \ No newline at end of file diff --git a/src/diagram/functions/diagramShapePhysics.ts b/src/diagram/functions/diagramShapePhysics.ts new file mode 100644 index 0000000..57d1125 --- /dev/null +++ b/src/diagram/functions/diagramShapePhysics.ts @@ -0,0 +1,70 @@ +import {DiaSounds} from "../../util/diaSounds"; +import {AbstractMesh, PhysicsAggregate, PhysicsMotionType, PhysicsShapeType, Scene} from "@babylonjs/core"; +import log from "loglevel"; + +export function applyPhysics(sounds: DiaSounds, mesh: AbstractMesh, scene: Scene, motionType?: PhysicsMotionType) { + const logger = log.getLogger('DiagramShapePhysics'); + if (!mesh?.metadata?.template) { + logger.error("applyPhysics: mesh.metadata.template is null", mesh); + return; + } + if (mesh.metadata.template == '#connection-template') { + return; + } + if (!scene) { + logger.error("applyPhysics: mesh or scene is null"); + return; + } + + if (mesh.physicsBody) { + mesh.physicsBody.dispose(); + } + + let shapeType = PhysicsShapeType.BOX; + switch (mesh.metadata.template) { + case "#sphere-template": + shapeType = PhysicsShapeType.SPHERE; + break; + case "#cylinder-template": + shapeType = PhysicsShapeType.CYLINDER; + break; + case "#cone-template": + shapeType = PhysicsShapeType.CONVEX_HULL; + break; + + } + let mass = mesh.scaling.x * mesh.scaling.y * mesh.scaling.z * 10; + + const aggregate = new PhysicsAggregate(mesh, + shapeType, {mass: mass, restitution: .02, friction: .9}, scene); + const body = aggregate.body; + body.setLinearDamping(1.95); + body.setAngularDamping(1.99); + + if (motionType) { + body + .setMotionType(motionType); + } else { + if (mesh.parent) { + body + .setMotionType(PhysicsMotionType.ANIMATED); + } else { + body + .setMotionType(PhysicsMotionType.DYNAMIC); + } + } + body.setCollisionCallbackEnabled(true); + body.getCollisionObservable().add((event) => { + + if (event.impulse < 10 && event.impulse > 1) { + const sound = sounds.bounce; + sound.setVolume(event.impulse / 10); + sound.attachToMesh(mesh); + sound.play(); + } + }, -1, false, this); + //body.setMotionType(PhysicsMotionType.ANIMATED); + body.setLinearDamping(.95); + body.setAngularDamping(.99); + body.setGravityFactor(0); +} \ No newline at end of file diff --git a/src/diagram/functions/fromDiagramEntity.ts b/src/diagram/functions/fromDiagramEntity.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/diagram/functions/toDiagramEntity.ts b/src/diagram/functions/toDiagramEntity.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/diagram/presentationManager.ts b/src/diagram/presentationManager.ts new file mode 100644 index 0000000..02b2367 --- /dev/null +++ b/src/diagram/presentationManager.ts @@ -0,0 +1,53 @@ +import {PresentationStep} from "./presentationStep"; +import log, {Logger} from "loglevel"; +import {Scene} from "@babylonjs/core"; + +export class PresentationManager { + _currentStep: PresentationStep = null; + private scene: Scene; + private logger: Logger = log.getLogger("PresentationManager"); + + constructor(scene: Scene) { + this.scene = scene; + } + + _steps: PresentationStep[] = []; + + public get steps(): PresentationStep[] { + return this._steps; + } + + public addStep(): PresentationStep { + const step = new PresentationStep(); + this._currentStep = step; + if (this._steps.length > 0) { + this._steps[this._steps.length - 1].next = step; + } else { + this.scene.getActiveMeshes().forEach((mesh) => { + if (mesh.metadata?.template) { + step.entities.push({ + entity: mesh, + endPosition: mesh.position.clone(), + endRotation: mesh.rotation.clone(), + endScaling: mesh.scaling.clone() + }) + step.duration = 1; + } + }); + } + this._steps.push(step); + return step; + } + + public play() { + this._currentStep.play(); + if (this._currentStep.next) { + this._currentStep = this._currentStep.next; + } + } + + public reset() { + this._currentStep = this._steps[0]; + this._steps[0].play(); + } +} \ No newline at end of file diff --git a/src/diagram/presentationStep.ts b/src/diagram/presentationStep.ts new file mode 100644 index 0000000..122b38c --- /dev/null +++ b/src/diagram/presentationStep.ts @@ -0,0 +1,45 @@ +import {AbstractMesh, Animation, Vector3} from "@babylonjs/core"; + +export type EntityTransform = { + entity: AbstractMesh, + endPosition?: Vector3, + endRotation?: Vector3, + endScaling?: Vector3 +} + +export class PresentationStep { + public id: string; + public name: string; + public duration: number = 2; + public entities: Array = []; + public next: PresentationStep; + private readonly fps: number = 30; + + private get endFrame(): number { + return this.fps * this.duration; + } + + public play() { + this.entities.forEach((entityTransform) => { + if (entityTransform.endPosition) { + + const transform = new Animation("transform", "position", + this.fps, Animation.ANIMATIONTYPE_VECTOR3, Animation.ANIMATIONLOOPMODE_CONSTANT); + const keyframes = [ + {frame: 0, value: entityTransform.entity.position}, + {frame: this.endFrame, value: entityTransform.endPosition} + ] + transform.setKeys(keyframes); + entityTransform.entity.animations.push(transform); + entityTransform.entity.getScene().beginAnimation(entityTransform.entity, 0, this.endFrame, false); + } + if (entityTransform.endRotation) { + //entityTransform.entity.rotation = entityTransform.endRotation; + } + if (entityTransform.endScaling) { + //entityTransform.entity.scaling = entityTransform.endScaling; + } + }); + + } +} \ No newline at end of file diff --git a/src/menus/baseMenu.ts b/src/menus/abstractMenu.ts similarity index 77% rename from src/menus/baseMenu.ts rename to src/menus/abstractMenu.ts index 5c4c9a0..c2fdd97 100644 --- a/src/menus/baseMenu.ts +++ b/src/menus/abstractMenu.ts @@ -1,7 +1,7 @@ import {Scene, WebXRExperienceHelper} from "@babylonjs/core"; import {Controllers} from "../controllers/controllers"; -export class BaseMenu { +export class AbstractMenu { protected scene: Scene; protected xr: WebXRExperienceHelper; protected controllers: Controllers; @@ -12,4 +12,8 @@ export class BaseMenu { this.controllers = controllers; } + public toggle() { + throw new Error("AbstractMenu.toggle() not implemented"); + } + } \ No newline at end of file diff --git a/src/menus/configMenu.ts b/src/menus/configMenu.ts index 454f492..561e9de 100644 --- a/src/menus/configMenu.ts +++ b/src/menus/configMenu.ts @@ -5,9 +5,9 @@ import log from "loglevel"; import {AppConfig} from "../util/appConfig"; import {Controllers} from "../controllers/controllers"; import {DiaSounds} from "../util/diaSounds"; -import {BaseMenu} from "./baseMenu"; +import {AbstractMenu} from "./abstractMenu"; -export class ConfigMenu extends BaseMenu { +export class ConfigMenu extends AbstractMenu { private sounds: DiaSounds; private configPlane: AbstractMesh = null; diff --git a/src/menus/integrationMenu.ts b/src/menus/integrationMenu.ts index 5683112..9cc172a 100644 --- a/src/menus/integrationMenu.ts +++ b/src/menus/integrationMenu.ts @@ -1,10 +1,10 @@ import {AbstractMesh, MeshBuilder, Scene, WebXRExperienceHelper} from "@babylonjs/core"; import {Controllers} from "../controllers/controllers"; -import {BaseMenu} from "./baseMenu"; +import {AbstractMenu} from "./abstractMenu"; import {AdvancedDynamicTexture, Grid, TextBlock} from "@babylonjs/gui"; import {CameraHelper} from "../util/cameraHelper"; -export class IntegrationMenu extends BaseMenu { +export class IntegrationMenu extends AbstractMenu { private plane: AbstractMesh = null; constructor(scene: Scene, xr: WebXRExperienceHelper, controllers: Controllers) { diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index 68023df..6abf402 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -16,17 +16,16 @@ import {AdvancedDynamicTexture, Button3D, ColorPicker, GUI3DManager, StackPanel3 import {Controllers} from "../controllers/controllers"; export enum ToolType { - BOX ="#box-template", - Sphere="#sphere-template", - Cylinder="#cylinder-template", - Cone ="#cone-template", - PLANE ="#plane-template", - OBJECT ="#object-template", + BOX = "#box-template", + SPHERE = "#sphere-template", + CYLINDER = "#cylinder-template", + CONE = "#cone-template", + PLANE = "#plane-template", + OBJECT = "#object-template", } export class Toolbox { private index = 0; - public static instance: Toolbox; private readonly scene: Scene; public readonly node: TransformNode; private readonly manager: GUI3DManager; @@ -60,7 +59,6 @@ export class Toolbox { this.buildToolbox(); - Toolbox.instance = this; if (!this.xObserver) { this.xObserver = this.controllers.controllerObserver.add((evt) => { if (evt.type == 'x-button') { @@ -75,72 +73,68 @@ export class Toolbox { } public buildTool(tool: ToolType, parent: AbstractMesh) { - let newItem: Mesh; const id = this.toolId(tool, (parent.material as StandardMaterial).diffuseColor); - const material = parent.material; - const toolname = "tool-" + id; - switch (tool) { - case ToolType.BOX: - newItem = MeshBuilder.CreateBox(toolname, {width: 1, height: 1, depth: 1}, this.scene); - break; - case ToolType.Sphere: - newItem = MeshBuilder.CreateSphere(toolname, {diameter: 1}, this.scene); - break; - case ToolType.Cylinder: - newItem = MeshBuilder.CreateCylinder(toolname, {height: 1, diameter: 1}, this.scene); - break; - case ToolType.Cone: - newItem = MeshBuilder.CreateCylinder(toolname, {diameterTop: 0, height: 1, diameterBottom: 1}, this.scene); - break; - case ToolType.PLANE: - newItem = MeshBuilder.CreatePlane(toolname, {width: 1, height: 1}, this.scene); - break; - case ToolType.OBJECT: - break; - } - if (newItem) { - newItem.material = material; - newItem.id = toolname - if (tool === ToolType.PLANE) { - newItem.material.backFaceCulling = false; - } - newItem.scaling = new Vector3(Toolbox.WIDGET_SIZE, - Toolbox.WIDGET_SIZE, - Toolbox.WIDGET_SIZE); - newItem.parent = parent; - if (!newItem.material) { - newItem.material = parent.material; - } - - if (newItem.metadata) { - newItem.metadata.template = tool; - } else { - newItem.metadata = {template: tool}; - } - const instance = new InstancedMesh("instance-" + id, newItem); - if (instance.metadata) { - instance.metadata.template = tool; - } else { - instance.metadata = {template: tool}; - } - instance.parent = parent; - newItem.setEnabled(false); - newItem.onEnabledStateChangedObservable.add(() => { - instance.setEnabled(false); - }); - return instance; - } else { + const newItem = this.buildMesh(tool, `tool-${id}`); + if (!newItem) { return null; } + newItem.material = parent.material; + if (tool === ToolType.PLANE) { + newItem.material.backFaceCulling = false; + } + newItem.scaling = new Vector3(Toolbox.WIDGET_SIZE, + Toolbox.WIDGET_SIZE, + Toolbox.WIDGET_SIZE); + newItem.parent = parent; + newItem.metadata = {template: tool}; + const instance = new InstancedMesh("instance-" + id, newItem); + instance.metadata = {template: tool}; + instance.parent = parent; + newItem.setEnabled(false); + newItem.onEnabledStateChangedObservable.add(() => { + instance.setEnabled(false); + }); + return instance; + + } + + private buildMesh(type: ToolType, toolname: string): Mesh { + switch (type) { + case ToolType.BOX: + return MeshBuilder.CreateBox(toolname, {width: 1, height: 1, depth: 1}, this.scene); + break; + case ToolType.SPHERE: + return MeshBuilder.CreateSphere(toolname, {diameter: 1}, this.scene); + break; + case ToolType.CYLINDER: + return MeshBuilder.CreateCylinder(toolname, {height: 1, diameter: 1}, this.scene); + break; + case ToolType.CONE: + return MeshBuilder.CreateCylinder(toolname, { + diameterTop: 0, + height: 1, + diameterBottom: 1 + }, this.scene); + break; + case ToolType.PLANE: + return MeshBuilder.CreatePlane(toolname, {width: 1, height: 1}, this.scene); + break; + case ToolType.OBJECT: + return null; + break; + } + return null; } private toolId(tool: ToolType, color: Color3) { return tool + "-" + color.toHexString(); } + private calculatePosition(i: number) { - return (i/this.gridsize)-.5-(1/this.gridsize/2); + return (i / this.gridsize) - .5 - (1 / this.gridsize / 2); } + private static WIDGET_SIZE = .1; private buildToolbox() { @@ -165,6 +159,7 @@ export class Toolbox { this.node.parent.setEnabled(false); } + public updateToolbox(color: string) { if (this.scene.getMeshById("toolbox-color-" + color)) { return; @@ -179,9 +174,13 @@ export class Toolbox { const depth = .2; const material = new StandardMaterial("material-" + color.toHexString(), this.scene); material.diffuseColor = color; - const mesh = MeshBuilder.CreateBox("toolbox-color-" + color.toHexString(), {width: width, height: .01, depth: depth}, this.scene); + const mesh = MeshBuilder.CreateBox("toolbox-color-" + color.toHexString(), { + width: width, + height: .01, + depth: depth + }, this.scene); mesh.material = material; - mesh.position.z = this.index++/4; + mesh.position.z = this.index++ / 4; mesh.parent = this.node; mesh.metadata = {tool: 'color'}; let i = 0; diff --git a/src/util/deepCopy.ts b/src/util/deepCopy.ts new file mode 100644 index 0000000..b7aa15d --- /dev/null +++ b/src/util/deepCopy.ts @@ -0,0 +1,16 @@ +export function deepCopy ? V : never>(source: T): T { + if (Array.isArray(source)) { + return source.map(item => (deepCopy(item))) as T & U[] + } + if (source instanceof Date) { + return new Date(source.getTime()) as T & Date + } + if (source && typeof source === 'object') { + return (Object.getOwnPropertyNames(source) as (keyof T)[]).reduce((o, prop) => { + Object.defineProperty(o, prop, Object.getOwnPropertyDescriptor(source, prop)!) + o[prop] = deepCopy(source[prop]) + return o + }, Object.create(Object.getPrototypeOf(source))) + } + return source +} \ No newline at end of file