Refactored into pure functions for key diagram manager functionality.

This commit is contained in:
Michael Mainguy 2023-08-23 07:44:09 -05:00
parent be8306141f
commit c945423954
16 changed files with 423 additions and 306 deletions

View File

@ -46,10 +46,11 @@ export class App {
const scene = new Scene(engine); const scene = new Scene(engine);
const persistenceManager = new IndexdbPersistenceManager("diagram"); const persistenceManager = new IndexdbPersistenceManager("diagram");
const controllers = new Controllers(); const controllers = new Controllers();
const toolbox = new Toolbox(scene, controllers); const toolbox = new Toolbox(scene, controllers);
const diagramManager = new DiagramManager(scene, controllers, toolbox); const diagramManager = new DiagramManager(scene, controllers, toolbox);
diagramManager.setPersistenceManager(persistenceManager); diagramManager.setPersistenceManager(persistenceManager);
const config = new AppConfig(persistenceManager); const config = new AppConfig(persistenceManager);
const environment = new CustomEnvironment(scene, "default", config); const environment = new CustomEnvironment(scene, "default", config);

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1,16 +1,4 @@
import { import {AbstractMesh, Color3, InstancedMesh, Mesh, Observable, PhysicsMotionType, Scene} from "@babylonjs/core";
AbstractMesh,
ActionManager,
Color3,
ExecuteCodeAction,
InstancedMesh,
Mesh,
Observable,
PhysicsMotionType,
PlaySoundAction,
Scene,
Vector3
} from "@babylonjs/core";
import {DiagramEntity, DiagramEvent, DiagramEventType} from "./diagramEntity"; import {DiagramEntity, DiagramEvent, DiagramEventType} from "./diagramEntity";
import {IPersistenceManager} from "../integration/iPersistenceManager"; import {IPersistenceManager} from "../integration/iPersistenceManager";
import {MeshConverter} from "./meshConverter"; import {MeshConverter} from "./meshConverter";
@ -18,9 +6,13 @@ import log from "loglevel";
import {Controllers} from "../controllers/controllers"; import {Controllers} from "../controllers/controllers";
import {DiaSounds} from "../util/diaSounds"; import {DiaSounds} from "../util/diaSounds";
import {AppConfig} from "../util/appConfig"; import {AppConfig} from "../util/appConfig";
import {TextLabel} from "./textLabel";
import {Toolbox} from "../toolbox/toolbox"; 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 { export class DiagramManager {
@ -29,25 +21,20 @@ export class DiagramManager {
private persistenceManager: IPersistenceManager = null; private persistenceManager: IPersistenceManager = null;
private readonly toolbox: Toolbox; private readonly toolbox: Toolbox;
private readonly scene: Scene; 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) { constructor(scene: Scene, controllers: Controllers, toolbox: Toolbox) {
this.sounds = new DiaSounds(scene); this.sounds = new DiaSounds(scene);
this.scene = scene; this.scene = scene;
this.toolbox = toolbox; this.toolbox = toolbox;
this.controllers = controllers; this.controllers = controllers;
this.actionManager = new ActionManager(this.scene); this.presentationManager = new PresentationManager(this.scene);
this.actionManager.registerAction( this.diagramEntityActionManager = new DiagramEntityActionManager(this.scene, this.sounds, this.controllers);
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);
})
);
if (this.onDiagramEventObservable.hasObservers()) { if (this.onDiagramEventObservable.hasObservers()) {
this.logger.warn("onDiagramEventObservable already has Observers, you should be careful"); this.logger.warn("onDiagramEventObservable already has Observers, you should be careful");
} }
@ -56,6 +43,7 @@ export class DiagramManager {
}, -1, true, this, false); }, -1, true, this, false);
this.onDiagramEventObservable.add(this.onDiagramEvent, -1, true, this); this.onDiagramEventObservable.add(this.onDiagramEvent, -1, true, this);
this.logger.debug("DiagramManager constructed"); this.logger.debug("DiagramManager constructed");
scene.onMeshRemovedObservable.add((mesh) => { scene.onMeshRemovedObservable.add((mesh) => {
if (mesh?.metadata?.template) { if (mesh?.metadata?.template) {
if (mesh.metadata.template != '#connection-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 { public get config(): AppConfig {
return this._config; return this._config;
} }
public setPersistenceManager(persistenceManager: IPersistenceManager) { public setPersistenceManager(persistenceManager: IPersistenceManager) {
this.persistenceManager = persistenceManager; this.persistenceManager = persistenceManager;
this._config = new AppConfig(persistenceManager); this._config = new AppConfig(persistenceManager);
this.persistenceManager.updateObserver.add(this.onRemoteEvent, -1, true, this); this.persistenceManager.updateObserver.add(this.onRemoteEvent, -1, true, this);
} }
public createCopy(mesh: AbstractMesh, copy: boolean = false): AbstractMesh { public createCopy(mesh: AbstractMesh, copy: boolean = false): AbstractMesh {
let newMesh; let newMesh;
if (!mesh.isAnInstance) { if (!mesh.isAnInstance) {
@ -103,152 +76,43 @@ export class DiagramManager {
} else { } else {
newMesh = new InstancedMesh("new", (mesh as InstancedMesh).sourceMesh); newMesh = new InstancedMesh("new", (mesh as InstancedMesh).sourceMesh);
} }
newMesh.actionManager = this.actionManager; newMesh.actionManager = this.diagramEntityActionManager.manager;
newMesh.position = mesh.absolutePosition.clone(); newMesh.position = mesh.absolutePosition.clone();
if (mesh.absoluteRotationQuaternion) { if (mesh.absoluteRotationQuaternion) {
newMesh.rotation = mesh.absoluteRotationQuaternion.toEulerAngles().clone(); newMesh.rotation = mesh.absoluteRotationQuaternion.toEulerAngles().clone();
} else { } else {
this.logger.error("no rotation quaternion"); this.logger.error("no rotation quaternion");
} }
if (copy) { applyScaling(mesh, newMesh, copy, this.config.current?.createSnap);
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();
}
}
newMesh.material = mesh.material; newMesh.material = mesh.material;
newMesh.metadata = deepCopy(mesh.metadata);
newMesh.metadata = this.deepCopy(mesh.metadata);
if (this.config.current?.physicsEnabled) { if (this.config.current?.physicsEnabled) {
DiagramShapePhysics.applyPhysics(this.sounds, newMesh, this.scene); applyPhysics(this.sounds, newMesh, this.scene);
} }
this.persistenceManager.add(newMesh); this.persistenceManager.add(newMesh);
return newMesh; return newMesh;
} }
private deepCopy<T, U = T extends Array<infer V> ? 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<T>((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) { private onRemoteEvent(event: DiagramEntity) {
this.logger.debug(event); this.logger.debug(event);
const toolMesh = this.scene.getMeshById("tool-" + event.template + "-" + event.color); const toolMesh = this.scene.getMeshById("tool-" + event.template + "-" + event.color);
if (!toolMesh && (event.template != '#connection-template')) { if (!toolMesh && (event.template != '#connection-template')) {
log.debug('no mesh found for ' + event.template + "-" + event.color, 'adding it'); 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); this.toolbox.updateToolbox(event.color);
} }
const mesh = MeshConverter.fromDiagramEntity(event, this.scene); const mesh = MeshConverter.fromDiagramEntity(event, this.scene);
mesh.actionManager = this.actionManager; mesh.actionManager = this.diagramEntityActionManager.manager;
if (event.parent) { if (event.parent) {
mesh.parent = this.scene.getMeshById(event.parent); mesh.parent = this.scene.getMeshById(event.parent);
} }
if (this.config.current?.physicsEnabled) { 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) { private onDiagramEvent(event: DiagramEvent) {
this.logger.debug(event.type); diagramEventHandler(
const entity = event.entity; event, this.scene, this.toolbox, this.config.current.physicsEnabled,
let mesh; this.diagramEntityActionManager.manager, this.sounds, this.persistenceManager);
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;
}
} }
} }

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}

View File

View File

@ -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();
}
}

View File

@ -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<EntityTransform> = [];
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;
}
});
}
}

View File

@ -1,7 +1,7 @@
import {Scene, WebXRExperienceHelper} from "@babylonjs/core"; import {Scene, WebXRExperienceHelper} from "@babylonjs/core";
import {Controllers} from "../controllers/controllers"; import {Controllers} from "../controllers/controllers";
export class BaseMenu { export class AbstractMenu {
protected scene: Scene; protected scene: Scene;
protected xr: WebXRExperienceHelper; protected xr: WebXRExperienceHelper;
protected controllers: Controllers; protected controllers: Controllers;
@ -12,4 +12,8 @@ export class BaseMenu {
this.controllers = controllers; this.controllers = controllers;
} }
public toggle() {
throw new Error("AbstractMenu.toggle() not implemented");
}
} }

View File

@ -5,9 +5,9 @@ import log from "loglevel";
import {AppConfig} from "../util/appConfig"; import {AppConfig} from "../util/appConfig";
import {Controllers} from "../controllers/controllers"; import {Controllers} from "../controllers/controllers";
import {DiaSounds} from "../util/diaSounds"; 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 sounds: DiaSounds;
private configPlane: AbstractMesh = null; private configPlane: AbstractMesh = null;

View File

@ -1,10 +1,10 @@
import {AbstractMesh, MeshBuilder, Scene, WebXRExperienceHelper} from "@babylonjs/core"; import {AbstractMesh, MeshBuilder, Scene, WebXRExperienceHelper} from "@babylonjs/core";
import {Controllers} from "../controllers/controllers"; import {Controllers} from "../controllers/controllers";
import {BaseMenu} from "./baseMenu"; import {AbstractMenu} from "./abstractMenu";
import {AdvancedDynamicTexture, Grid, TextBlock} from "@babylonjs/gui"; import {AdvancedDynamicTexture, Grid, TextBlock} from "@babylonjs/gui";
import {CameraHelper} from "../util/cameraHelper"; import {CameraHelper} from "../util/cameraHelper";
export class IntegrationMenu extends BaseMenu { export class IntegrationMenu extends AbstractMenu {
private plane: AbstractMesh = null; private plane: AbstractMesh = null;
constructor(scene: Scene, xr: WebXRExperienceHelper, controllers: Controllers) { constructor(scene: Scene, xr: WebXRExperienceHelper, controllers: Controllers) {

View File

@ -16,17 +16,16 @@ import {AdvancedDynamicTexture, Button3D, ColorPicker, GUI3DManager, StackPanel3
import {Controllers} from "../controllers/controllers"; import {Controllers} from "../controllers/controllers";
export enum ToolType { export enum ToolType {
BOX ="#box-template", BOX = "#box-template",
Sphere="#sphere-template", SPHERE = "#sphere-template",
Cylinder="#cylinder-template", CYLINDER = "#cylinder-template",
Cone ="#cone-template", CONE = "#cone-template",
PLANE ="#plane-template", PLANE = "#plane-template",
OBJECT ="#object-template", OBJECT = "#object-template",
} }
export class Toolbox { export class Toolbox {
private index = 0; private index = 0;
public static instance: Toolbox;
private readonly scene: Scene; private readonly scene: Scene;
public readonly node: TransformNode; public readonly node: TransformNode;
private readonly manager: GUI3DManager; private readonly manager: GUI3DManager;
@ -60,7 +59,6 @@ export class Toolbox {
this.buildToolbox(); this.buildToolbox();
Toolbox.instance = this;
if (!this.xObserver) { if (!this.xObserver) {
this.xObserver = this.controllers.controllerObserver.add((evt) => { this.xObserver = this.controllers.controllerObserver.add((evt) => {
if (evt.type == 'x-button') { if (evt.type == 'x-button') {
@ -75,72 +73,68 @@ export class Toolbox {
} }
public buildTool(tool: ToolType, parent: AbstractMesh) { public buildTool(tool: ToolType, parent: AbstractMesh) {
let newItem: Mesh;
const id = this.toolId(tool, (parent.material as StandardMaterial).diffuseColor); 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, const newItem = this.buildMesh(tool, `tool-${id}`);
Toolbox.WIDGET_SIZE, if (!newItem) {
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 {
return null; 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) { private toolId(tool: ToolType, color: Color3) {
return tool + "-" + color.toHexString(); return tool + "-" + color.toHexString();
} }
private calculatePosition(i: number) { 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 static WIDGET_SIZE = .1;
private buildToolbox() { private buildToolbox() {
@ -165,6 +159,7 @@ export class Toolbox {
this.node.parent.setEnabled(false); this.node.parent.setEnabled(false);
} }
public updateToolbox(color: string) { public updateToolbox(color: string) {
if (this.scene.getMeshById("toolbox-color-" + color)) { if (this.scene.getMeshById("toolbox-color-" + color)) {
return; return;
@ -179,9 +174,13 @@ export class Toolbox {
const depth = .2; const depth = .2;
const material = new StandardMaterial("material-" + color.toHexString(), this.scene); const material = new StandardMaterial("material-" + color.toHexString(), this.scene);
material.diffuseColor = color; 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.material = material;
mesh.position.z = this.index++/4; mesh.position.z = this.index++ / 4;
mesh.parent = this.node; mesh.parent = this.node;
mesh.metadata = {tool: 'color'}; mesh.metadata = {tool: 'color'};
let i = 0; let i = 0;

16
src/util/deepCopy.ts Normal file
View File

@ -0,0 +1,16 @@
export function deepCopy<T, U = T extends Array<infer V> ? 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<T>((o, prop) => {
Object.defineProperty(o, prop, Object.getOwnPropertyDescriptor(source, prop)!)
o[prop] = deepCopy(source[prop])
return o
}, Object.create(Object.getPrototypeOf(source)))
}
return source
}