diff --git a/src/app.ts b/src/app.ts index c4f1320..04b4ee8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -40,7 +40,8 @@ export class App { constructor() { const config = AppConfig.config; - log.setLevel('debug'); + log.setLevel('info'); + log.getLogger('App').setLevel('debug'); const canvas = document.createElement("canvas"); canvas.style.width = "100%"; canvas.style.height = "100%"; @@ -73,6 +74,8 @@ export class App { const havokPlugin = new HavokPlugin(true, havokInstance); scene.enablePhysics(new Vector3(0, -9.8, 0), havokPlugin); + scene.collisionsEnabled = true; + const camera: ArcRotateCamera = new ArcRotateCamera("Camera", Math.PI / 2, Math.PI / 2, 2, new Vector3(0, 1.6, 0), scene); camera.radius = 0; @@ -102,8 +105,6 @@ export class App { this.xr.baseExperience.onStateChangedObservable.add((state) => { if (state == WebXRState.IN_XR) { this.scene.audioEnabled = true; - - this.xr.baseExperience.camera.position = new Vector3(0, 1.6, 0); window.addEventListener(('pa-button-state-change'), (event: any) => { if (event.detail) { diff --git a/src/controllers/base.ts b/src/controllers/base.ts index 0902929..caaab3a 100644 --- a/src/controllers/base.ts +++ b/src/controllers/base.ts @@ -14,6 +14,7 @@ import {DiagramManager} from "../diagram/diagramManager"; import {DiagramEvent, DiagramEventType} from "../diagram/diagramEntity"; import log from "loglevel"; import {Controllers} from "./controllers"; +import {DiagramShapePhysics} from "../diagram/diagramShapePhysics"; export class Base { @@ -74,7 +75,11 @@ export class Base { }); } - this.initGrip(init.components['xr-standard-squeeze']); + + if (init.components['xr-standard-squeeze']) { + this.initGrip(init.components['xr-standard-squeeze']) + } + ; }); Controllers.controllerObserver.add((event) => { if (event.type == 'pulse') { @@ -153,7 +158,7 @@ export class Base { } transformNode.setParent(this.controller.motionController.rootMesh); this.grabbedMeshParentId = transformNode.id; - MeshConverter + DiagramShapePhysics .applyPhysics(newMesh, this.scene) .setMotionType(PhysicsMotionType.ANIMATED); diff --git a/src/diagram/diagramEntity.ts b/src/diagram/diagramEntity.ts index 1da0b27..153f041 100644 --- a/src/diagram/diagramEntity.ts +++ b/src/diagram/diagramEntity.ts @@ -1,5 +1,5 @@ import {Color3, Vector3} from "@babylonjs/core"; -import {BmenuState} from "../menus/MenuState"; +import {EditMenuState} from "../menus/MenuState"; export enum DiagramEventType { ADD, @@ -8,7 +8,8 @@ export enum DiagramEventType { DROP, DROPPED, CLEAR, - CHANGECOLOR + CHANGECOLOR, + COPY } @@ -16,7 +17,7 @@ export enum DiagramEventType { export type DiagramEvent = { type: DiagramEventType; - menustate?: BmenuState; + menustate?: EditMenuState; entity?: DiagramEntity; oldColor?: Color3; newColor?: Color3; diff --git a/src/diagram/diagramManager.ts b/src/diagram/diagramManager.ts index 75cb487..ef65ace 100644 --- a/src/diagram/diagramManager.ts +++ b/src/diagram/diagramManager.ts @@ -18,6 +18,8 @@ import log from "loglevel"; import {Controllers} from "../controllers/controllers"; import {DiaSounds} from "../util/diaSounds"; import {AppConfig} from "../util/appConfig"; +import {DiagramShapePhysics} from "./diagramShapePhysics"; +import {TextLabel} from "./textLabel"; export class DiagramManager { public readonly onDiagramEventObservable: Observable = new Observable(); @@ -63,7 +65,7 @@ export class DiagramManager { this.logger.debug("DiagramManager constructed"); } - public createCopy(mesh: AbstractMesh): AbstractMesh { + public createCopy(mesh: AbstractMesh, copy: boolean = false): AbstractMesh { let newMesh; if (!mesh.isAnInstance) { newMesh = new InstancedMesh("new", (mesh as Mesh)); @@ -77,10 +79,13 @@ export class DiagramManager { } else { this.logger.error("no rotation quaternion"); } - newMesh.scaling = AppConfig.config.createSnapVal; + if (copy) { + newMesh.scaling = mesh.scaling.clone(); + } else { + newMesh.scaling = AppConfig.config.createSnapVal; + } newMesh.material = mesh.material; newMesh.metadata = mesh.metadata; - return newMesh; } @@ -99,7 +104,7 @@ export class DiagramManager { if (event.parent) { mesh.parent = this.scene.getMeshById(event.parent); } - MeshConverter.applyPhysics(mesh, this.scene) + DiagramShapePhysics.applyPhysics(mesh, this.scene) .setMotionType(PhysicsMotionType.DYNAMIC); } @@ -111,8 +116,6 @@ export class DiagramManager { if (entity) { mesh = this.scene.getMeshById(entity.id); } - //const body = mesh?.physicsBody; - switch (event.type) { case DiagramEventType.CLEAR: break; @@ -120,12 +123,10 @@ export class DiagramManager { break; case DiagramEventType.DROP: this.getPersistenceManager()?.modify(mesh); - MeshConverter.updateTextNode(mesh, entity.text); - + TextLabel.updateTextNode(mesh, entity.text); break; case DiagramEventType.ADD: this.getPersistenceManager()?.add(mesh); - break; case DiagramEventType.MODIFY: this.getPersistenceManager()?.modify(mesh); diff --git a/src/diagram/diagramShapePhysics.ts b/src/diagram/diagramShapePhysics.ts new file mode 100644 index 0000000..bc6ff43 --- /dev/null +++ b/src/diagram/diagramShapePhysics.ts @@ -0,0 +1,50 @@ +import {AbstractMesh, PhysicsAggregate, PhysicsBody, PhysicsMotionType, PhysicsShapeType, Scene} from "@babylonjs/core"; +import {DiaSounds} from "../util/diaSounds"; +import log from "loglevel"; + +export class DiagramShapePhysics { + private static logger: log.Logger = log.getLogger('DiagramShapePhysics'); + + public static applyPhysics(mesh: AbstractMesh, scene: Scene): PhysicsBody { + if (!mesh?.metadata?.template) { + this.logger.error("applyPhysics: mesh.metadata.template is null", mesh); + return null; + } + if (!scene) { + this.logger.error("applyPhysics: mesh or scene is null"); + return null; + } + 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; + + } + const aggregate = new PhysicsAggregate(mesh, + shapeType, {mass: 20, restitution: .02, friction: .9}, scene); + aggregate.body.setCollisionCallbackEnabled(true); + aggregate.body.getCollisionObservable().add((event, state) => { + if (event.distance > .001 && !DiaSounds.instance.low.isPlaying) { + this.logger.debug(event, state); + DiaSounds.instance.low.play(); + } + + }, -1, false, this); + const body = aggregate.body; + body.setMotionType(PhysicsMotionType.ANIMATED); + body.setLinearDamping(.95); + body.setAngularDamping(.99); + body.setGravityFactor(0); + return aggregate.body; + } +} \ No newline at end of file diff --git a/src/diagram/meshConverter.ts b/src/diagram/meshConverter.ts index b6905bb..c4f2782 100644 --- a/src/diagram/meshConverter.ts +++ b/src/diagram/meshConverter.ts @@ -1,22 +1,8 @@ import {DiagramEntity} from "./diagramEntity"; -import { - AbstractMesh, - Color3, - DynamicTexture, - InstancedMesh, - Mesh, - MeshBuilder, - PhysicsAggregate, - PhysicsBody, - PhysicsMotionType, - PhysicsShapeType, - Quaternion, - Scene, - StandardMaterial -} from "@babylonjs/core"; +import {AbstractMesh, Color3, InstancedMesh, Mesh, Quaternion, Scene, StandardMaterial} from "@babylonjs/core"; import {v4 as uuidv4} from 'uuid'; import log from "loglevel"; - +import {TextLabel} from "./textLabel"; export class MeshConverter { private static logger = log.getLogger('MeshConverter'); @@ -44,7 +30,6 @@ export class MeshConverter { } return entity; } - public static fromDiagramEntity(entity: DiagramEntity, scene: Scene): AbstractMesh { if (!entity) { this.logger.error("fromDiagramEntity: entity is null"); @@ -63,22 +48,15 @@ export class MeshConverter { log.debug('error: mesh is an instance'); } else { mesh = new InstancedMesh(entity.id, (mesh as Mesh)); - } } else { log.debug('no mesh found for ' + entity.template + "-" + entity.color); } } - - if (mesh) { mesh.metadata = {template: entity.template}; - if (entity.position) { - mesh.position = entity.position; - - } if (entity.rotation) { if (mesh.rotationQuaternion) { @@ -86,7 +64,6 @@ export class MeshConverter { } else { mesh.rotation = entity.rotation; } - } if (entity.parent) { mesh.parent = scene.getNodeById(entity.parent); @@ -101,111 +78,11 @@ export class MeshConverter { } if (entity.text) { mesh.metadata.text = entity.text; - this.updateTextNode(mesh, entity.text); + TextLabel.updateTextNode(mesh, entity.text); } - /* - const sphereAggregate = new PhysicsAggregate(mesh, PhysicsShapeType.BOX, { - - mass: 10, - restitution: 0.1, - startAsleep: false - }, scene); - - */ } else { this.logger.error("fromDiagramEntity: mesh is null after it should have been created"); } - return mesh; - - } - - public static applyPhysics(mesh: AbstractMesh, scene: Scene): PhysicsBody { - if (!mesh?.metadata?.template || !scene) { - this.logger.error("applyPhysics: mesh or scene is null"); - return null; - } - 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; - - } - const aggregate = new PhysicsAggregate(mesh, - shapeType, {mass: 20, restitution: .2, friction: .9}, scene); - const body = aggregate.body; - body.setMotionType(PhysicsMotionType.ANIMATED); - body.setLinearDamping(.9); - body.setAngularDamping(.5); - body.setGravityFactor(0); - return aggregate.body; - - } - - public static updateTextNode(mesh: AbstractMesh, text: string) { - if (!mesh) { - this.logger.error("updateTextNode: mesh is null"); - return null; - } - let textNode = (mesh.getChildren((node) => { - return node.name == 'text' - })[0] as Mesh); - if (textNode) { - textNode.dispose(false, true); - } - if (!text) { - return null; - } - - //Set font - const height = 0.125; - const font_size = 24; - const font = "bold " + font_size + "px Arial"; - //Set height for dynamic texture - const DTHeight = 1.5 * font_size; //or set as wished - //Calc Ratio - const ratio = height / DTHeight; - - //Use a temporary dynamic texture to calculate the length of the text on the dynamic texture canvas - const temp = new DynamicTexture("DynamicTexture", 32, mesh.getScene()); - const tmpctx = temp.getContext(); - tmpctx.font = font; - const DTWidth = tmpctx.measureText(text).width + 8; - - //Calculate width the plane has to be - const planeWidth = DTWidth * ratio; - - //Create dynamic texture and write the text - const dynamicTexture = new DynamicTexture("DynamicTexture", { - width: DTWidth, - height: DTHeight - }, mesh.getScene(), false); - const mat = new StandardMaterial("mat", mesh.getScene()); - mat.diffuseTexture = dynamicTexture; - dynamicTexture.drawText(text, null, null, font, "#000000", "#ffffff", true); - - //Create plane and set dynamic texture as material - const plane = MeshBuilder.CreatePlane("text", {width: planeWidth, height: height}, mesh.getScene()); - plane.material = mat; - plane.billboardMode = Mesh.BILLBOARDMODE_ALL; - - - const yOffset = mesh.getBoundingInfo().boundingSphere.radius; - plane.parent = mesh; - plane.position.y = yOffset; - plane.scaling.y = 1 / mesh.scaling.y; - plane.scaling.x = 1 / mesh.scaling.x; - plane.scaling.z = 1 / mesh.scaling.z; - return plane; } } \ No newline at end of file diff --git a/src/diagram/textLabel.ts b/src/diagram/textLabel.ts new file mode 100644 index 0000000..91cd668 --- /dev/null +++ b/src/diagram/textLabel.ts @@ -0,0 +1,63 @@ +import {AbstractMesh, DynamicTexture, Mesh, MeshBuilder, StandardMaterial} from "@babylonjs/core"; +import log from "loglevel"; + +export class TextLabel { + private static logger: log.Logger = log.getLogger('TextLabel'); + + public static updateTextNode(mesh: AbstractMesh, text: string): AbstractMesh { + if (!mesh) { + this.logger.error("updateTextNode: mesh is null"); + return null; + } + let textNode = (mesh.getChildren((node) => { + return node.name == 'text' + })[0] as Mesh); + if (textNode) { + textNode.dispose(false, true); + } + if (!text) { + return null; + } + + //Set font + const height = 0.125; + const font_size = 24; + const font = "bold " + font_size + "px Arial"; + //Set height for dynamic texture + const DTHeight = 1.5 * font_size; //or set as wished + //Calc Ratio + const ratio = height / DTHeight; + + //Use a temporary dynamic texture to calculate the length of the text on the dynamic texture canvas + const temp = new DynamicTexture("DynamicTexture", 32, mesh.getScene()); + const tmpctx = temp.getContext(); + tmpctx.font = font; + const DTWidth = tmpctx.measureText(text).width + 8; + + //Calculate width the plane has to be + const planeWidth = DTWidth * ratio; + + //Create dynamic texture and write the text + const dynamicTexture = new DynamicTexture("DynamicTexture", { + width: DTWidth, + height: DTHeight + }, mesh.getScene(), false); + const mat = new StandardMaterial("mat", mesh.getScene()); + mat.diffuseTexture = dynamicTexture; + dynamicTexture.drawText(text, null, null, font, "#000000", "#ffffff", true); + + //Create plane and set dynamic texture as material + const plane = MeshBuilder.CreatePlane("text", {width: planeWidth, height: height}, mesh.getScene()); + plane.material = mat; + plane.billboardMode = Mesh.BILLBOARDMODE_ALL; + + + const yOffset = mesh.getBoundingInfo().boundingSphere.radius; + plane.parent = mesh; + plane.position.y = yOffset; + plane.scaling.y = 1 / mesh.scaling.y; + plane.scaling.x = 1 / mesh.scaling.x; + plane.scaling.z = 1 / mesh.scaling.z; + return plane; + } +} \ No newline at end of file diff --git a/src/menus/MenuState.ts b/src/menus/MenuState.ts index ec3e427..17e56b7 100644 --- a/src/menus/MenuState.ts +++ b/src/menus/MenuState.ts @@ -1,7 +1,8 @@ -export enum BmenuState { +export enum EditMenuState { NONE, LABELING, MODIFYING, // Editing an entity - REMOVING, // Removing an entity + REMOVING, + COPYING // Removing an entity } \ No newline at end of file diff --git a/src/menus/editMenu.ts b/src/menus/editMenu.ts index 0c68009..9ffd33e 100644 --- a/src/menus/editMenu.ts +++ b/src/menus/editMenu.ts @@ -9,20 +9,23 @@ import { } from "@babylonjs/core"; import {Button3D, GUI3DManager, StackPanel3D, TextBlock} from "@babylonjs/gui"; import {DiagramManager} from "../diagram/diagramManager"; -import {BmenuState} from "./MenuState"; +import {EditMenuState} from "./MenuState"; import {DiagramEvent, DiagramEventType} from "../diagram/diagramEntity"; import {MeshConverter} from "../diagram/meshConverter"; import log from "loglevel"; import {InputTextView} from "../information/inputTextView"; import {DiaSounds} from "../util/diaSounds"; import {CameraHelper} from "../util/cameraHelper"; +import {TextLabel} from "../diagram/textLabel"; +import {DiagramShapePhysics} from "../diagram/diagramShapePhysics"; export class EditMenu { - private state: BmenuState = BmenuState.NONE; + private state: EditMenuState = EditMenuState.NONE; private manager: GUI3DManager; private readonly scene: Scene; private textView: InputTextView; private textInput: HTMLElement; + private readonly logger: log.Logger = log.getLogger('EditMenu'); private gizmoManager: GizmoManager; private readonly xr: WebXRExperienceHelper; private readonly diagramManager: DiagramManager; @@ -44,10 +47,10 @@ export class EditMenu { case PointerEventTypes.POINTERPICK: if (pointerInfo.pickInfo?.pickedMesh?.metadata?.template && pointerInfo.pickInfo?.pickedMesh?.parent?.parent?.id != "toolbox") { - this.handleEventStateAction(pointerInfo).then(() => { - log.getLogger("bmenu").debug("handled"); + this.diagramEntityPicked(pointerInfo).then(() => { + this.logger.debug("handled"); }).catch((e) => { - log.getLogger("bmenu").error(e); + this.logger.error(e); }); break; } @@ -68,6 +71,8 @@ export class EditMenu { panel.addControl(this.makeButton("Modify", "modify")); panel.addControl(this.makeButton("Remove", "remove")); panel.addControl(this.makeButton("Add Label", "label")); + panel.addControl(this.makeButton("Copy", "copy")); + //panel.addControl(this.makeButton("Add Ring Cameras", "addRingCameras")); this.manager.controlScaling = .5; CameraHelper.setMenuPosition(panel.node, this.scene); @@ -78,7 +83,7 @@ export class EditMenu { if (mesh.metadata) { mesh.metadata.text = text; } else { - log.getLogger('bmenu').error("mesh has no metadata"); + this.logger.error("mesh has no metadata"); } this.diagramManager.onDiagramEventObservable.notifyObservers({ type: DiagramEventType.MODIFY, @@ -98,27 +103,30 @@ export class EditMenu { return button; } - private async handleEventStateAction(pointerInfo: PointerInfo) { + private async diagramEntityPicked(pointerInfo: PointerInfo) { const mesh = pointerInfo.pickInfo.pickedMesh; if (!mesh) { - log.warn("no mesh"); + this.logger.warn("no mesh"); return; } switch (this.state) { - case BmenuState.REMOVING: + case EditMenuState.REMOVING: this.remove(mesh); break; - case BmenuState.MODIFYING: + case EditMenuState.MODIFYING: this.setModify(mesh); break; - case BmenuState.LABELING: + case EditMenuState.LABELING: this.setLabeling(mesh); break; + case EditMenuState.COPYING: + this.setCopying(mesh); + break; } } private remove(mesh: AbstractMesh) { - log.debug("removing " + mesh?.id); + this.logger.debug("removing " + mesh?.id); const event: DiagramEvent = { type: DiagramEventType.REMOVE, entity: @@ -140,14 +148,24 @@ export class EditMenu { entity: MeshConverter.toDiagramEntity(mesh), } ) - log.debug(mesh.scaling); + this.logger.debug(mesh.scaling); }); } } } + private setCopying(mesh: AbstractMesh) { + if (mesh) { + const newMesh = this.diagramManager.createCopy(mesh); + DiagramShapePhysics.applyPhysics(newMesh, this.scene); + newMesh.parent = null; + } + this.logger.warn('copying not implemented', mesh); + //@todo implement + } + private setLabeling(mesh: AbstractMesh) { - log.debug("labeling " + mesh.id); + this.logger.debug("labeling " + mesh.id); let text = ""; if (mesh?.metadata?.text) { text = mesh.metadata.text; @@ -156,7 +174,7 @@ export class EditMenu { textInput.show(); textInput.onTextObservable.addOnce((value) => { this.persist(mesh, value.text); - MeshConverter.updateTextNode(mesh, value.text); + TextLabel.updateTextNode(mesh, value.text); }); } @@ -164,16 +182,19 @@ export class EditMenu { private handleClick(_info, state) { switch (state.currentTarget.name) { case "modify": - this.state = BmenuState.MODIFYING; + this.state = EditMenuState.MODIFYING; break; case "remove": - this.state = BmenuState.REMOVING; + this.state = EditMenuState.REMOVING; break; case "label": - this.state = BmenuState.LABELING; + this.state = EditMenuState.LABELING; + break; + case "copy": + this.state = EditMenuState.COPYING; break; default: - log.error("Unknown button"); + this.logger.error("Unknown button"); return; } this.manager.dispose(); diff --git a/src/util/diaSounds.ts b/src/util/diaSounds.ts index bf21c09..8ddce53 100644 --- a/src/util/diaSounds.ts +++ b/src/util/diaSounds.ts @@ -6,6 +6,7 @@ export class DiaSounds { private readonly scene: Scene; constructor(scene: Scene) { + this.scene = scene; this._enter = new Sound("enter", "./sounds.mp3", this.scene, null, { autoplay: false, @@ -28,7 +29,7 @@ export class DiaSounds { this._low = new Sound("low", "./sounds.mp3", this.scene, null, { autoplay: false, loop: false, - offset: 2, + offset: 3, length: 1.0 });