Added copy capability.

This commit is contained in:
Michael Mainguy 2023-08-01 18:17:20 -05:00
parent 397c87eeba
commit afba69042c
10 changed files with 186 additions and 165 deletions

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

63
src/diagram/textLabel.ts Normal file
View File

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

View File

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

View File

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

View File

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