diff --git a/public/assets/models/person.stl b/public/assets/models/person.stl new file mode 100644 index 0000000..4b2f609 Binary files /dev/null and b/public/assets/models/person.stl differ diff --git a/server/server.js b/server/server.js index 811885f..ef7b97d 100644 --- a/server/server.js +++ b/server/server.js @@ -96,7 +96,6 @@ async function start() { conn.connection.sendUTF('{ "type": "error", "netAddr": "' + hash + '" }'); }); logger.info((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.', connections.length); - }); setInterval(() => { const message = `{ "count": ${connections.size} }` diff --git a/src/diagram/diagramManager.ts b/src/diagram/diagramManager.ts index f4119b8..b352f07 100644 --- a/src/diagram/diagramManager.ts +++ b/src/diagram/diagramManager.ts @@ -27,12 +27,12 @@ export class DiagramManager { private _moving: number = 10; private _i: number = 0; - constructor() { + constructor(readyObservable: Observable) { this._me = getMe(); this._scene = DefaultScene.Scene; this._config = new AppConfig(); this._controllers = new Controllers(); - this._diagramMenuManager = new DiagramMenuManager(this.onDiagramEventObservable, this._controllers, this._config); + this._diagramMenuManager = new DiagramMenuManager(this.onDiagramEventObservable, this._controllers, this._config, readyObservable); this._diagramEntityActionManager = buildEntityActionManager(this._controllers); this.onDiagramEventObservable.add(this.onDiagramEvent, DiagramEventObserverMask.FROM_DB, true, this); @@ -97,6 +97,7 @@ export class DiagramManager { }); this._logger.debug("DiagramManager constructed"); + } public get actionManager(): AbstractActionManager { @@ -140,7 +141,7 @@ export class DiagramManager { private onDiagramEvent(event: DiagramEvent) { - let diagramObject = this._diagramObjects.get(event.entity.id); + let diagramObject = this._diagramObjects.get(event?.entity?.id); switch (event.type) { case DiagramEventType.ADD: if (diagramObject) { @@ -160,7 +161,7 @@ export class DiagramManager { if (diagramObject) { diagramObject.dispose(); } - this._diagramObjects.delete(event.entity.id); + this._diagramObjects.delete(event?.entity?.id); break; case DiagramEventType.MODIFY: this._logger.debug(event); diff --git a/src/diagram/diagramMenuManager.ts b/src/diagram/diagramMenuManager.ts index 2c3de0c..91f2827 100644 --- a/src/diagram/diagramMenuManager.ts +++ b/src/diagram/diagramMenuManager.ts @@ -26,7 +26,7 @@ export class DiagramMenuManager { private _logger = log.getLogger('DiagramMenuManager'); private _connectionPreview: ConnectionPreview; - constructor(notifier: Observable, controllers: Controllers, config: AppConfig) { + constructor(notifier: Observable, controllers: Controllers, config: AppConfig, readyObservable: Observable) { this._scene = DefaultScene.Scene; @@ -38,7 +38,7 @@ export class DiagramMenuManager { const event = {type: DiagramEventType.MODIFY, entity: {id: evt.id, text: evt.text}} this._notifier.notifyObservers(event, DiagramEventObserverMask.FROM_DB); }); - this.toolbox = new Toolbox(); + this.toolbox = new Toolbox(readyObservable); this.scaleMenu = new ScaleMenu2(this._notifier); if (viewOnly()) { this.toolbox.handleMesh.setEnabled(false); diff --git a/src/diagram/functions/buildMeshFromDiagramEntity.ts b/src/diagram/functions/buildMeshFromDiagramEntity.ts index d318c6b..87c0018 100644 --- a/src/diagram/functions/buildMeshFromDiagramEntity.ts +++ b/src/diagram/functions/buildMeshFromDiagramEntity.ts @@ -62,6 +62,7 @@ function createNewInstanceIfNecessary(entity: DiagramEntity, scene: Scene): Abst case DiagramTemplates.CYLINDER: case DiagramTemplates.CONE: case DiagramTemplates.PLANE: + case DiagramTemplates.PERSON: const toolMesh = scene.getMeshById("tool-" + entity.template + "-" + entity.color); if (toolMesh && !oldMesh) { newMesh = new InstancedMesh(entity.id, (toolMesh as Mesh)); diff --git a/src/diagram/types/diagramEntity.ts b/src/diagram/types/diagramEntity.ts index b0ff0ed..d9be6df 100644 --- a/src/diagram/types/diagramEntity.ts +++ b/src/diagram/types/diagramEntity.ts @@ -30,6 +30,7 @@ export enum DiagramTemplates { CONE = "#cone-template", IMAGE = "#image-template", PLANE = "#plane-template", + PERSON = "#person-template" } export type DiagramEvent = { diff --git a/src/integration/pouchdbPersistenceManager.ts b/src/integration/pouchdbPersistenceManager.ts index c6331e1..15d0775 100644 --- a/src/integration/pouchdbPersistenceManager.ts +++ b/src/integration/pouchdbPersistenceManager.ts @@ -28,6 +28,8 @@ export class PouchdbPersistenceManager { private _encryption = new Encryption(); private _encKey = null; private _diagramManager: DiagramManager; + private _salt: string; + private _failCount: number = 0; constructor() { document.addEventListener('passwordset', (evt) => { @@ -37,7 +39,7 @@ export class PouchdbPersistenceManager { this._logger.debug('Initialized'); }); } - console.log(evt); + this._logger.debug(evt); }); } @@ -45,6 +47,14 @@ export class PouchdbPersistenceManager { this._diagramManager = diagramManager; diagramManager.onDiagramEventObservable.add((evt) => { this._logger.debug(evt); + if (!evt?.entity) { + this._logger.warn('no entity'); + return; + } + if (!evt?.entity?.id) { + this._logger.warn('no entity id'); + return; + } switch (evt.type) { case DiagramEventType.REMOVE: this.remove(evt.entity.id); @@ -105,6 +115,9 @@ export class PouchdbPersistenceManager { this._logger.warn('CONFLICTS!', doc._conflicts); } if (this._encKey) { + if (!doc.encrypted) { + this._logger.warn("current local doc is not encrypted, encrypting"); + } await this._encryption.encryptObject(entity); const newDoc = { _id: doc._id, @@ -114,6 +127,9 @@ export class PouchdbPersistenceManager { this.db.put(newDoc) } else { if (doc) { + if (doc.encrypted) { + this._logger.error("current local doc is encrypted, but encryption key is missing... saving in plaintext"); + } const newDoc = {_id: doc._id, _rev: doc._rev, ...entity}; this.db.put(newDoc); } else { @@ -125,6 +141,10 @@ export class PouchdbPersistenceManager { if (err.status == 404) { try { if (this._encKey) { + if (!this._encryption.ready) { + this._logger.error('Encryption not ready, there is a potential problem when this happens, we will generate a new salt which may cause data loss and/or slowness'); + await this._encryption.setPassword(this._encKey); + } await this._encryption.encryptObject(entity); const newDoc = { _id: entity.id, @@ -132,13 +152,16 @@ export class PouchdbPersistenceManager { } this.db.put(newDoc); } else { + this._logger.info('no encryption key, saving in plaintext'); const newEntity = {_id: entity.id, ...entity}; this.db.put(newEntity); } } catch (err2) { + this._logger.error("Unable to save document"); this._logger.error(err2); } } else { + this._logger.error("Unknown error with document get from db"); this._logger.error(err); } } @@ -155,20 +178,27 @@ export class PouchdbPersistenceManager { try { const doc = await this.db.get('metadata'); if (doc.encrypted) { + if (!this._salt && doc.encrypted.salt) { + this._logger.warn('Missing Salt'); + this._salt = doc.encrypted.salt; + } if (!this._encKey) { const promptPassword = new CustomEvent('promptpassword', {detail: 'Please enter password'}); document.dispatchEvent(promptPassword); return false; } if (!this._encryption.ready) { + this._logger.warn("Encryption not ready, setting password"); await this._encryption.setPassword(this._encKey, doc.encrypted.salt); } const decrypted = await this._encryption.decryptToObject(doc.encrypted.encrypted, doc.encrypted.iv); if (decrypted.friendly) { + this._logger.info("Storing Document friendly name in local storage, decrypted"); localStorage.setItem(current, decrypted.friendly); } } else { if (doc && doc.friendly) { + this._logger.info("Storing Document friendly name in local storage"); localStorage.setItem(current, doc.friendly); } if (doc && doc.camera) { @@ -192,7 +222,7 @@ export class PouchdbPersistenceManager { await this.db.put(newDoc); } } else { - this._logger.debug('no friendly name found'); + this._logger.warn('no friendly name found'); } } } @@ -225,12 +255,9 @@ export class PouchdbPersistenceManager { } private async sendLocalDataToScene() { - let salt = null; - const clear = localStorage.getItem('clearLocal'); try { - const all = await this.db.allDocs({include_docs: true}); for (const dbEntity of all.rows) { this._logger.debug(dbEntity.doc); @@ -265,8 +292,14 @@ export class PouchdbPersistenceManager { switch (err.message) { case 'WebCrypto_DecryptionFailure: ': case 'Invalid data type!': - const promptPassword = new CustomEvent('promptpassword', {detail: 'Please enter password'}); - document.dispatchEvent(promptPassword); + this._failCount++; + if (this._failCount < 5) { + const promptPassword = new CustomEvent('promptpassword', {detail: 'Please enter password'}); + document.dispatchEvent(promptPassword); + } else { + this._logger.error('Too many decryption failures, Ignoring... This may compromise your data security'); + window.alert('Too many decryption failures, Ignoring... This may compromise your data security'); + } } this._logger.error(err); } diff --git a/src/integration/presence.ts b/src/integration/presence.ts index 9276555..f1122c1 100644 --- a/src/integration/presence.ts +++ b/src/integration/presence.ts @@ -94,15 +94,15 @@ export class Presence { this._logger.warn('user not found', data.user); const newUser = MeshBuilder.CreateDisc(data.user.id, {radius: 0.3}, scene); const node = new TransformNode(data.netAddr, scene); + const material = new StandardMaterial(data.user.id + 'mat', scene); material.diffuseColor = new Color3(0, 0, 1); material.backFaceCulling = false; + newUser.material = material; newUser.parent = node; newUser.rotation.x = Math.PI / 2; newUser.position.y = 0.01; - node.position = xyztovec(data.user.base.position); - node.rotation = xyztovec(data.user.base.rotation); } } this._logger.debug('user update', data.user); diff --git a/src/toolbox/functions/buildColor.ts b/src/toolbox/functions/buildColor.ts index 4e332da..461063a 100644 --- a/src/toolbox/functions/buildColor.ts +++ b/src/toolbox/functions/buildColor.ts @@ -12,7 +12,7 @@ import {enumKeys} from "../../util/functions/enumKeys"; import {ToolType} from "../types/toolType"; import {buildTool} from "./buildTool"; -export function buildColor(color: Color3, scene: Scene, parent: TransformNode, index: number, toolMap: Map): Node { +export async function buildColor(color: Color3, scene: Scene, parent: TransformNode, index: number, toolMap: Map): Promise { const width = .1; const height = .1; const material = new StandardMaterial("material-" + color.toHexString(), scene); @@ -38,7 +38,7 @@ export function buildColor(color: Color3, scene: Scene, parent: TransformNode, i let i = 0; const tools = []; for (const tool of enumKeys(ToolType)) { - const newItem = buildTool(ToolType[tool], colorBoxMesh, material); + const newItem = await buildTool(ToolType[tool], colorBoxMesh, material); if (newItem) { //buildColorPicker(scene, color, newItem, material, i, colorChangeObservable); newItem.position = new Vector3(calculatePosition(++i), .1, 0); diff --git a/src/toolbox/functions/buildMesh.ts b/src/toolbox/functions/buildMesh.ts index 5a7f72e..9a732b7 100644 --- a/src/toolbox/functions/buildMesh.ts +++ b/src/toolbox/functions/buildMesh.ts @@ -1,11 +1,13 @@ import {ToolType} from "../types/toolType"; -import {Mesh, MeshBuilder, Scene} from "@babylonjs/core"; +import {Mesh, MeshBuilder, Scene, SceneLoader} from "@babylonjs/core"; +import {DefaultScene} from "../../defaultScene"; const detail = { tesselation: 16, subdivisions: 5 } -export function buildMesh(type: ToolType, toolname: string, scene: Scene): Mesh { + +export async function buildMesh(type: ToolType, toolname: string, scene: Scene): Promise { switch (type) { case ToolType.BOX: return MeshBuilder.CreateBox(toolname, {width: 1, height: 1, depth: 1}, scene); @@ -34,7 +36,11 @@ export function buildMesh(type: ToolType, toolname: string, scene: Scene): Mesh diameterBottom: 1, tessellation: detail.tesselation }, scene); - + case ToolType.PERSON: + const result = await SceneLoader.ImportMeshAsync(null, '/assets/models/', 'person.stl', DefaultScene.Scene); + result.meshes[0].id = toolname; + result.meshes[0].name = toolname; + return result.meshes[0] as Mesh; case ToolType.PLANE: return MeshBuilder.CreatePlane(toolname, {width: 1, height: 1}, scene); diff --git a/src/toolbox/functions/buildTool.ts b/src/toolbox/functions/buildTool.ts index f3d5c89..e70cc93 100644 --- a/src/toolbox/functions/buildTool.ts +++ b/src/toolbox/functions/buildTool.ts @@ -4,7 +4,7 @@ import {buildMesh} from "./buildMesh"; const WIDGET_SIZE = .1; -export function buildTool(tool: ToolType, colorParent: AbstractMesh, material: Material) { +export async function buildTool(tool: ToolType, colorParent: AbstractMesh, material: Material) { let id = "ID"; let color = "#000000"; switch (material.getClassName()) { @@ -21,7 +21,7 @@ export function buildTool(tool: ToolType, colorParent: AbstractMesh, material: M } - const newItem = buildMesh(tool, `tool-${id}`, colorParent.getScene()); + const newItem = await buildMesh(tool, `tool-${id}`, colorParent.getScene()); if (!newItem) { return null; } diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index 250b840..0b75288 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -1,4 +1,4 @@ -import {AbstractMesh, Color3, InstancedMesh, Node, Scene, TransformNode, Vector3} from "@babylonjs/core"; +import {AbstractMesh, Color3, InstancedMesh, Node, Observable, Scene, TransformNode, Vector3} from "@babylonjs/core"; import {buildColor} from "./functions/buildColor"; import log from "loglevel"; import {Handle} from "../objects/handle"; @@ -19,13 +19,16 @@ export class Toolbox { private readonly _handle: Handle; private readonly _scene: Scene; - constructor() { + constructor(readyObservable: Observable) { this._scene = DefaultScene.Scene; this._toolboxBaseNode = new TransformNode("toolbox", this._scene); this._handle = new Handle(this._toolboxBaseNode, 'Toolbox'); this._toolboxBaseNode.position.y = .2; this._toolboxBaseNode.scaling = new Vector3(0.6, 0.6, 0.6); - this.buildToolbox(); + this.buildToolbox().then(() => { + readyObservable.notifyObservers(true); + this._logger.info('Toolbox built'); + }); Toolbox._instance = this; } private index = 0; @@ -46,9 +49,9 @@ export class Toolbox { return this._tools.has(mesh.id); } - private buildToolbox() { + private async buildToolbox() { this.setupPointerObservable(); - this.buildColorPicker(); + await this.buildColorPicker(); if (this._toolboxBaseNode.parent) { const platform = this._scene.getMeshById("platform"); if (platform) { @@ -94,10 +97,10 @@ export class Toolbox { node.isEnabled(false) == true }; - private buildColorPicker() { + private async buildColorPicker() { let initial = true; for (const c of colors) { - const cnode = buildColor(Color3.FromHexString(c), this._scene, this._toolboxBaseNode, this.index++, this._tools); + const cnode = await buildColor(Color3.FromHexString(c), this._scene, this._toolboxBaseNode, this.index++, this._tools); if (initial) { initial = false; for (const id of cnode.metadata.tools) { diff --git a/src/toolbox/types/toolType.ts b/src/toolbox/types/toolType.ts index 43062ae..5737994 100644 --- a/src/toolbox/types/toolType.ts +++ b/src/toolbox/types/toolType.ts @@ -5,4 +5,5 @@ export enum ToolType { CONE = "#cone-template", PLANE = "#plane-template", OBJECT = "#object-template", + PERSON = "#person-template" } \ No newline at end of file diff --git a/src/vrApp.ts b/src/vrApp.ts index d22c055..bb6cd3a 100644 --- a/src/vrApp.ts +++ b/src/vrApp.ts @@ -1,4 +1,4 @@ -import {Color3, Engine, FreeCamera, Scene, Vector3, WebGPUEngine} from "@babylonjs/core"; +import {Color3, Engine, FreeCamera, Observable, Scene, Vector3, WebGPUEngine} from "@babylonjs/core"; import '@babylonjs/loaders'; import {DiagramManager} from "./diagram/diagramManager"; import log, {Logger} from "loglevel"; @@ -16,7 +16,7 @@ import {Introduction} from "./tutorial/introduction"; const webGpu = false; -log.setLevel('error', false); +log.setLevel('debug', false); const canvas = (document.querySelector('#gameCanvas') as HTMLCanvasElement); export class VrApp { @@ -30,7 +30,39 @@ export class VrApp { }); } + public async initialize(scene: Scene) { + //const mesh = SceneLoader.ImportMesh(null, '/assets/models/', 'person.stl', DefaultScene.Scene); + setMainCamera(scene); + const spinner = new Spinner(); + spinner.show(); + const diagramReadyObservable = new Observable(); + const diagramManager = new DiagramManager(diagramReadyObservable); + diagramReadyObservable.add((ready) => { + if (ready) { + initDb(diagramManager); + } else { + this.logger.error('DiagramManager not ready'); + } + }); + initEnvironment(diagramManager, spinner); + const gamepadManager = new GamepadManager(scene); + addSceneInspector(); + //const camMenu = new CameraMenu(scene); + const el = document.querySelector('#download'); + if (el) { + el.addEventListener('click', () => { + exportGltf(); + }) + } + if (!localStorage.getItem('tutorialCompleted')) { + this.logger.info('Starting tutorial'); + const intro = new Introduction(); + } + this.logger.info('Render loop started'); + } + private async initializeEngine() { + let engine: WebGPUEngine | Engine = null; if (webGpu) { engine = new WebGPUEngine(canvas); @@ -56,28 +88,6 @@ export class VrApp { }); } - public async initialize(scene: Scene) { - setMainCamera(scene); - const spinner = new Spinner(); - spinner.show(); - const diagramManager = new DiagramManager(); - await initDb(diagramManager); - initEnvironment(diagramManager, spinner); - const gamepadManager = new GamepadManager(scene); - addSceneInspector(); - //const camMenu = new CameraMenu(scene); - const el = document.querySelector('#download'); - if (el) { - el.addEventListener('click', () => { - exportGltf(); - }) - } - if (!localStorage.getItem('tutorialCompleted')) { - this.logger.info('Starting tutorial'); - const intro = new Introduction(); - } - this.logger.info('Render loop started'); - } } @@ -95,10 +105,7 @@ async function initDb(diagramManager: DiagramManager) { const db = new PouchdbPersistenceManager(); //const userManager = new UserManager(db.onUserObservable); db.setDiagramManager(diagramManager); - await db.initialize(); - - } function initEnvironment(diagramManager: DiagramManager, spinner: Spinner) {