refactored web interface, updated image update code.

This commit is contained in:
Michael Mainguy 2024-08-03 19:12:32 -05:00
parent 1d6c82a16a
commit a07b53f2a7
21 changed files with 505 additions and 455 deletions

View File

@ -11,11 +11,11 @@ body {
color: #4444ee; color: #4444ee;
} }
.scene { .scene {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
} }
#gameCanvas { #gameCanvas {
@ -26,184 +26,6 @@ body {
background: transparent; background: transparent;
} }
div.overlay {
position: absolute;
display: block;
transform: translate(-50%, -50%);
z-index: 12;
height: 120px;
}
div.overlay div {
width: 100%;
margin: 10px;
text-align: center;
}
div.overlay div a {
display: flex;
text-decoration: none;
padding-top: 1em;
padding-left: 5px;
vertical-align: middle;
border-color: #FFD700;
border-style: outset;
border-width: 2px;
border-radius: 10px;
}
div.overlay div a:visited, div.overlay div a:link {
color: white;
background-color: #999922;
height: 2em;
}
div.overlay div a:hover {
background-color: #FFD700;
color: #000000;
}
div.overlay div a {
}
div.overlay input {
display: inline-block;
margin: 10px auto;
text-decoration: none;
border-color: #FFD700;
border-style: solid;
border-width: 1px;
padding: 10px;
width: 200px;
}
div#create {
left: 600px;
top: 400px;
transform: translate(-50%, -50%);
z-index: 14;
width: 320px;
height: 344px;
border: 3px inset #FFD700;
display: none;
background-color: #000;
}
div.overlay div a.cancel {
font-size: small;
font-weight: lighter;
font-style: italic;
background-color: #222211;
color: #EEC755;
}
#diagramListContent ul {
list-style-type: none;
text-align: left;
padding: 0;
padding-inline-start: 0;
}
#diagramList {
overflow: scroll;
}
#diagramList > h1 {
width: 100%;
text-align: center;
}
#diagramListContent li {
margin: 12px;
}
#diagramListContent li a {
width: 90%;
}
div.overlay div a.cancel:hover {
background-color: #EEC700;
color: #000000;
}
#main.mini {
left: 100px;
top: 200px;
width: 160px;
}
#main.mini img, #tutorial img {
width: 160px;
height: 60px;
}
#main.mini div a, #tutorial div a {
}
h1 {
font-size: x-large;
font-weight: bolder;
text-align: center;
color: #F9F9E9;
}
#tutorial {
z-index: 15;
left: 100px;
top: 750px;
width: 160px;
height: 210px;
}
#password {
display: none;
left: 50%;
top: 50%;
}
#diagramList {
left: 340px;
top: 400px;
height: 500px;
background-color: #000;
padding: 5px;
}
#create {
left: 500px;
top: 340px;
}
#create div {
margin: 0;
margin-top: 10px;
}
#closekey, #closekey a:active, #closekey a:visited, #closekey a:link {
position: relative;
color: #ffffff;
}
#enterXR {
}
div.overlay div.inactive a {
background-color: #222222;
color: #555555;
border-color: #222222;
cursor: not-allowed;
}
#enterXR.inactive {
}
#loadingGrid { #loadingGrid {
position: relative; position: relative;

View File

@ -1,11 +1,9 @@
import {AbstractMesh, KeyboardEventTypes, MeshBuilder, Scene} from "@babylonjs/core"; import {AbstractMesh, KeyboardEventTypes, Scene} from "@babylonjs/core";
import {Rigplatform} from "./rigplatform"; import {Rigplatform} from "./rigplatform";
import {ControllerEventType, Controllers} from "./controllers"; import {Controllers} from "./controllers";
import {DiagramManager} from "../diagram/diagramManager"; import {DiagramManager} from "../diagram/diagramManager";
import {GridMaterial} from "@babylonjs/materials";
import {wheelHandler} from "./functions/wheelHandler"; import {wheelHandler} from "./functions/wheelHandler";
import log, {Logger} from "loglevel"; import log, {Logger} from "loglevel";
import {isDiagramEntity} from "../diagram/functions/isDiagramEntity";
export class WebController { export class WebController {
private readonly scene: Scene; private readonly scene: Scene;
@ -30,15 +28,15 @@ export class WebController {
this.diagramManager = diagramManager; this.diagramManager = diagramManager;
this.controllers = controllers; this.controllers = controllers;
this.canvas = document.querySelector('#gameCanvas'); this.canvas = document.querySelector('#gameCanvas');
this.referencePlane = MeshBuilder.CreatePlane('referencePlane', {size: 10}, this.scene); //this.referencePlane = MeshBuilder.CreatePlane('referencePlane', {size: 10}, this.scene);
this.referencePlane.setEnabled(false); //this.referencePlane.setEnabled(false);
this.referencePlane.visibility = 0.5; //this.referencePlane.visibility = 0.5;
const material = new GridMaterial('grid', this.scene); /*const material = new GridMaterial('grid', this.scene);
material.gridRatio = 1; material.gridRatio = 1;
material.backFaceCulling = false; material.backFaceCulling = false;
material.antialias = true; material.antialias = true;
this.referencePlane.material = material; this.referencePlane.material = material;
*/
this.scene.onKeyboardObservable.add((kbInfo) => { this.scene.onKeyboardObservable.add((kbInfo) => {
this.logger.debug(kbInfo); this.logger.debug(kbInfo);
@ -88,13 +86,15 @@ export class WebController {
this.speed *= .5; this.speed *= .5;
break; break;
case " ": case " ":
if (kbInfo.event.ctrlKey) { /*if (kbInfo.event.ctrlKey) {
if (this.controllers) { if (this.controllers) {
this.controllers.controllerObservable.notifyObservers( this.controllers.controllerObservable.notifyObservers(
{type: ControllerEventType.X_BUTTON, value: 1} {type: ControllerEventType.X_BUTTON, value: 1}
) )
} }
} }
*/
break; break;
default: default:
@ -109,11 +109,13 @@ export class WebController {
if (kbInfo.type == 1) { if (kbInfo.type == 1) {
//this.referencePlane.setEnabled(true); //this.referencePlane.setEnabled(true);
} else { } else {
this.referencePlane.setEnabled(false); /* this.referencePlane.setEnabled(false);
if (this.pickedMesh) { if (this.pickedMesh) {
this.pickedMesh.showBoundingBox = false; this.pickedMesh.showBoundingBox = false;
this.pickedMesh = null; this.pickedMesh = null;
} }
*/
} }
} }
}); });
@ -121,11 +123,11 @@ export class WebController {
this.scene.onPointerUp = () => { this.scene.onPointerUp = () => {
this.mouseDown = false; this.mouseDown = false;
this.rig.turn(0); this.rig.turn(0);
if (this.pickedMesh) { /*if (this.pickedMesh) {
this.referencePlane.setEnabled(false); this.referencePlane.setEnabled(false);
this.pickedMesh.showBoundingBox = false; this.pickedMesh.showBoundingBox = false;
this.pickedMesh = null; this.pickedMesh = null;
} }*/
}; };
@ -162,7 +164,8 @@ export class WebController {
}); });
this.scene.onPointerDown = (evt, state) => { this.scene.onPointerDown = (evt, state) => {
if (evt.pointerType == "mouse") { if (evt.pointerType == "mouse") {
if (evt.shiftKey) { this.mouseDown = true;
/*if (evt.shiftKey) {
//setMenuPosition(this.referencePlane, this.scene, new Vector3(0, 0, 5)); //setMenuPosition(this.referencePlane, this.scene, new Vector3(0, 0, 5));
//this.referencePlane.rotation = scene.activeCamera.absoluteRotation.toEulerAngles(); //this.referencePlane.rotation = scene.activeCamera.absoluteRotation.toEulerAngles();
this.pickedMesh = state.pickedMesh; this.pickedMesh = state.pickedMesh;
@ -173,15 +176,15 @@ export class WebController {
this.pickedMesh.rotation = scene.activeCamera.absoluteRotation.toEulerAngles(); this.pickedMesh.rotation = scene.activeCamera.absoluteRotation.toEulerAngles();
this.referencePlane.setEnabled(true); this.referencePlane.setEnabled(true);
} else { } else {
this.mouseDown = true;
/*
if (state.pickedMesh) { if (state.pickedMesh) {
new ClickMenu(state.pickedMesh, state.gripTransform, this.diagramManager.onDiagramEventObservable); new ClickMenu(state.pickedMesh, state.gripTransform, this.diagramManager.onDiagramEventObservable);
} }
*/
} } */
} }
}; };
this.scene.onPointerMove = (evt) => { this.scene.onPointerMove = (evt) => {
@ -191,7 +194,7 @@ export class WebController {
if (this.mouseDown) { if (this.mouseDown) {
this.rig.turn(evt.movementX); this.rig.turn(evt.movementX);
} }
const meshPickInfo = scene.pick(this.scene.pointerX, this.scene.pointerY, (mesh) => { /*const meshPickInfo = scene.pick(this.scene.pointerX, this.scene.pointerY, (mesh) => {
return isDiagramEntity(mesh); return isDiagramEntity(mesh);
}); });
const planePickInfo = scene.pick(this.scene.pointerX, this.scene.pointerY, (mesh) => { const planePickInfo = scene.pick(this.scene.pointerX, this.scene.pointerY, (mesh) => {
@ -208,16 +211,17 @@ export class WebController {
} }
} else { } else {
if (this.mesh) { if (this.mesh) {
/*this.diagramManager.onDiagramEventObservable.notifyObservers({ this.diagramManager.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.MODIFY, type: DiagramEventType.MODIFY,
entity: toDiagramEntity(this.mesh) entity: toDiagramEntity(this.mesh)
}, DiagramEventObserverMask.ALL); */ }, DiagramEventObserverMask.ALL);
} }
this.mesh = null; this.mesh = null;
} }
if (this.pickedMesh && planePickInfo.hit) { if (this.pickedMesh && planePickInfo.hit) {
this.pickedMesh.position = planePickInfo.pickedPoint; this.pickedMesh.position = planePickInfo.pickedPoint;
} }
*/
} }
} }

View File

@ -13,6 +13,7 @@ import {ConnectionPreview} from "../menus/connectionPreview";
import {ScaleMenu2} from "../menus/ScaleMenu2"; import {ScaleMenu2} from "../menus/ScaleMenu2";
import {CameraMenu} from "../menus/cameraMenu"; import {CameraMenu} from "../menus/cameraMenu";
import {viewOnly} from "../util/functions/getPath"; import {viewOnly} from "../util/functions/getPath";
import {GroupMenu} from "../menus/groupMenu";
export class DiagramMenuManager { export class DiagramMenuManager {
@ -21,6 +22,7 @@ export class DiagramMenuManager {
public readonly configMenu: ConfigMenu; public readonly configMenu: ConfigMenu;
private readonly _notifier: Observable<DiagramEvent>; private readonly _notifier: Observable<DiagramEvent>;
private readonly _inputTextView: InputTextView; private readonly _inputTextView: InputTextView;
private _groupMenu: GroupMenu;
private readonly _scene: Scene; private readonly _scene: Scene;
private _cameraMenu: CameraMenu; private _cameraMenu: CameraMenu;
private _logger = log.getLogger('DiagramMenuManager'); private _logger = log.getLogger('DiagramMenuManager');
@ -28,8 +30,6 @@ export class DiagramMenuManager {
constructor(notifier: Observable<DiagramEvent>, controllers: Controllers, config: AppConfig, readyObservable: Observable<boolean>) { constructor(notifier: Observable<DiagramEvent>, controllers: Controllers, config: AppConfig, readyObservable: Observable<boolean>) {
this._scene = DefaultScene.Scene; this._scene = DefaultScene.Scene;
this._notifier = notifier; this._notifier = notifier;
this._inputTextView = new InputTextView(controllers); this._inputTextView = new InputTextView(controllers);
this.configMenu = new ConfigMenu(config); this.configMenu = new ConfigMenu(config);
@ -92,6 +92,7 @@ export class DiagramMenuManager {
const clickMenu = new ClickMenu(mesh); const clickMenu = new ClickMenu(mesh);
clickMenu.onClickMenuObservable.add((evt: ActionEvent) => { clickMenu.onClickMenuObservable.add((evt: ActionEvent) => {
this._logger.debug(evt); this._logger.debug(evt);
switch (evt.source.id) { switch (evt.source.id) {
case "remove": case "remove":
this.notifyAll({type: DiagramEventType.REMOVE, entity: {id: clickMenu.mesh.id}}); this.notifyAll({type: DiagramEventType.REMOVE, entity: {id: clickMenu.mesh.id}});
@ -105,6 +106,9 @@ export class DiagramMenuManager {
case "size": case "size":
this.scaleMenu.show(clickMenu.mesh); this.scaleMenu.show(clickMenu.mesh);
break; break;
case "group":
this._groupMenu = new GroupMenu(clickMenu.mesh);
break;
case "close": case "close":
this.scaleMenu.hide(); this.scaleMenu.hide();
break; break;

View File

@ -19,6 +19,7 @@ export function toDiagramEntity(mesh: AbstractMesh): DiagramEntity {
entity.position = vectoxys(mesh.absolutePosition); entity.position = vectoxys(mesh.absolutePosition);
entity.rotation = vectoxys(mesh.absoluteRotationQuaternion.toEulerAngles()); entity.rotation = vectoxys(mesh.absoluteRotationQuaternion.toEulerAngles());
entity.last_seen = new Date(); entity.last_seen = new Date();
entity.image = mesh?.metadata?.image;
entity.template = mesh?.metadata?.template; entity.template = mesh?.metadata?.template;
entity.text = mesh?.metadata?.text; entity.text = mesh?.metadata?.text;
entity.from = mesh?.metadata?.from; entity.from = mesh?.metadata?.from;

View File

@ -131,6 +131,10 @@ export class PouchdbPersistenceManager {
if (!entity) { if (!entity) {
return; return;
} }
if (entity.template == '#image-template' && !entity.image) {
this._logger.error('no image data', entity);
return;
}
if (this._encKey && !this._encryption.ready) { if (this._encKey && !this._encryption.ready) {
await this._encryption.setPassword(this._encKey); await this._encryption.setPassword(this._encKey);
} }

View File

@ -1,7 +1,7 @@
import {AbstractMesh, ActionEvent, Observable, Scene, TransformNode, Vector3} from "@babylonjs/core"; import {AbstractMesh, ActionEvent, Observable, Scene, TransformNode, Vector3} from "@babylonjs/core";
import {Button} from "../objects/Button"; import {Button} from "../objects/Button";
import {positionNode} from "./functions/positionNode";
const POINTER_UP = "pointerup"; import {isUp} from "./functions/isUp";
export class ClickMenu { export class ClickMenu {
private readonly _mesh: AbstractMesh; private readonly _mesh: AbstractMesh;
@ -46,7 +46,7 @@ export class ClickMenu {
} }
}, -1, false, this, false); }, -1, false, this, false);
this.makeNewButton("Close", "close", scene, x += .11) this.makeNewButton("Group", "group", scene, x += .11)
.onPointerObservable.add((eventData) => { .onPointerObservable.add((eventData) => {
if (isUp(eventData)) { if (isUp(eventData)) {
this.onClickMenuObservable.notifyObservers(eventData); this.onClickMenuObservable.notifyObservers(eventData);
@ -54,16 +54,14 @@ export class ClickMenu {
} }
}, -1, false, this, false); }, -1, false, this, false);
this.makeNewButton("Close", "close", scene, x += .11)
const platform = scene.getMeshByName("platform"); .onPointerObservable.add((eventData) => {
const ray = scene.activeCamera.getForwardRay(1); if (isUp(eventData)) {
ray.direction.y = 0; this.onClickMenuObservable.notifyObservers(eventData);
const fpos = scene.activeCamera.globalPosition.clone().add(ray.direction.scale(1)); this.dispose();
this._transformNode.position = fpos; }
this._transformNode.position.y -= .4; }, -1, false, this, false);
this._transformNode.lookAt(scene.activeCamera.globalPosition); positionNode(this._transformNode);
this._transformNode.rotate(Vector3.Up(), Math.PI);
this._transformNode.setParent(platform);
} }
public get mesh(): AbstractMesh { public get mesh(): AbstractMesh {
@ -85,7 +83,3 @@ export class ClickMenu {
return button; return button;
} }
} }
function isUp(event: ActionEvent): boolean {
return event?.sourceEvent?.type == POINTER_UP;
}

View File

@ -0,0 +1,7 @@
import {ActionEvent} from "@babylonjs/core";
const POINTER_UP = "pointerup";
export function isUp(event: ActionEvent): boolean {
return event?.sourceEvent?.type == POINTER_UP;
}

View File

@ -0,0 +1,15 @@
import {TransformNode, Vector3} from "@babylonjs/core";
import {DefaultScene} from "../../defaultScene";
export function positionNode(transformNode: TransformNode) {
const scene = DefaultScene.Scene;
const platform = scene.getMeshByName("platform");
const ray = scene.activeCamera.getForwardRay(1);
ray.direction.y = 0;
const fpos = scene.activeCamera.globalPosition.clone().add(ray.direction.scale(1));
transformNode.position = fpos;
transformNode.position.y -= .4;
transformNode.lookAt(scene.activeCamera.globalPosition);
transformNode.rotate(Vector3.Up(), Math.PI);
transformNode.setParent(platform);
}

37
src/menus/groupMenu.ts Normal file
View File

@ -0,0 +1,37 @@
import {AbstractMesh, Scene, TransformNode, Vector3} from "@babylonjs/core";
import {Button} from "../objects/Button";
import {positionNode} from "./functions/positionNode";
import {isUp} from "./functions/isUp";
export class GroupMenu {
private readonly _mesh: AbstractMesh;
private readonly _scene: Scene;
private _transformNode: TransformNode;
constructor(mesh: AbstractMesh) {
this._mesh = mesh;
this._scene = mesh.getScene();
this._transformNode = new TransformNode("graoupTransform", this._scene);
positionNode(this._transformNode);
const button = this.buildButton("Done", "groupdone", this._scene, 0);
button.onPointerObservable.add((eventData) => {
if (isUp(eventData)) {
this.dispose();
}
}, -1, false, this, false);
}
private buildButton(name: string, id: string, scene: Scene, x: number): Button {
const button = new Button(name, id, scene)
button.transform.scaling = new Vector3(.2, .2, .2);
button.transform.rotate(Vector3.Up(), Math.PI);
const transform = button.transform;
transform.parent = this._transformNode;
transform.position.x = x;
return button;
}
private dispose() {
this._transformNode.dispose(false, true);
}
}

View File

@ -0,0 +1,69 @@
import axios from "axios";
export function CreateMenu({display, toggleCreateMenu}) {
const onCreateClick = (evt) => {
evt.preventDefault();
const name = (document.querySelector('#createName') as HTMLInputElement).value;
let password = (document.querySelector('#createPassword') as HTMLInputElement).value;
const password2 = (document.querySelector('#createPassword2') as HTMLInputElement).value;
if (password !== password2) {
window.alert('Passwords do not match');
return;
}
const id = window.crypto.randomUUID().replace(/-/g, '_');
if (password.length == 0) {
password = id;
}
const encrypted = (password != id);
localStorage.setItem(id, name);
if (name && name.length > 4) {
axios.post(import.meta.env.VITE_CREATE_ENDPOINT,
{
"_id": "org.couchdb.user:" + id,
"name": id,
"password": password,
"roles": ["readers"],
"type": "user"
}
).then(response => {
console.log(response);
const evt = new CustomEvent('dbcreated', {
detail: {
id: id,
name: name,
password: password,
encrypted: encrypted
}
});
document.dispatchEvent(evt);
}).catch(error => {
console.error(error);
});
} else {
window.alert('Name must be longer than 4 characters');
}
}
return (
<div className="overlay" id="create" style={{'display': display}}>
<div>
<div>
<label htmlFor="createName">Diagram Name</label>
<input id="createName" placeholder="Enter a name for your diagram" type="text"/></div>
<div>
<label htmlFor="createPassoword">Optional Password</label>
<input id="createPassword" placeholder="(Optional) Password" type="password"/>
</div>
<div>
<label htmlFor="createPassword2">Repeat Password</label>
<input id="createPassword2" placeholder="(Optional) Password" type="password"/></div>
<div><a href="#" id="createActionLink" onClick={onCreateClick}>Create</a></div>
<div><a className="cancel" onClick={toggleCreateMenu} href="#" id="cancelCreateLink">Cancel</a></div>
</div>
</div>
)
}

View File

@ -0,0 +1,33 @@
import {useEffect, useState} from "react";
export function DiagramList({display, onClick}) {
const [dbList, setDbList] = useState([]);
useEffect(() => {
const listDb = async () => {
const data = await indexedDB.databases();
let i = 0;
setDbList(data.filter((item) => item.name.indexOf('_pouch_') > -1).map((item) => {
const dbid = item.name.replace('_pouch_', '');
let friendlyName = localStorage.getItem(dbid);
if (!friendlyName) {
friendlyName = dbid;
}
return {key: dbid, name: friendlyName}
}));
};
listDb();
}, []);
return (
<div className="overlay" id="diagramList" style={{'display': display}}>
<h1>Diagrams</h1>
<div id="startCreate"><a href="#" id="startCreateLink" onClick={onClick}>New</a></div>
<div id="diagramListContent">
<ul>
{dbList.map((item) => <li key={item.key}><a href={`/db/${item.key}`}>{item.name}</a></li>)}
</ul>
</div>
</div>
)
}

View File

@ -0,0 +1,9 @@
export function KeyboardHelp({display, onClick}) {
return (
<div className="overlay" id="keyboardHelp" style={{'display': display}}>
<div id="closekey"><a href="#" onClick={onClick}>X</a></div>
<img height="240" src="/assets/textures/keyboardhelp2.jpg" width="480"/>
<img height="240" src="/assets/textures/mousehelp.jpg" width="180"/>
</div>
)
}

View File

@ -0,0 +1,27 @@
import {viewOnly} from "../../util/functions/getPath";
import {QuestLink} from "./questLink";
export function MainMenu({onClick}) {
if (viewOnly()) {
return (
<div className="overlay mini" id="main">
<img height="120" src="/assets/ddd.svg" width="320"/>
<div id="enterXR" className="inactive"><a href="#" id="enterVRLink">Enter VR</a></div>
<QuestLink/>
<div id="download"><a href="#" id="downloadLink">Download Model</a></div>
</div>)
} else {
return (
<div className="overlay mini" id="main">
<img height="120" src="/assets/ddd.svg" width="320"/>
<div id="enterXR" className="inactive"><a href="#" id="enterVRLink">Enter VR</a></div>
<QuestLink/>
<div id="diagrams"><a href="#" id="diagramsLink" onClick={onClick}>Diagrams</a></div>
<div id="imageUpload"><a href="#" id="imageUploadLink" onClick={onClick}>Upload Image</a></div>
<div id="download"><a href="#" id="downloadLink">Download Model</a></div>
</div>
)
}
}

View File

@ -0,0 +1,51 @@
import {useState} from "react";
import {uploadImage} from "../functions/uploadImage";
import {MainMenu} from "./mainMenu";
import {CreateMenu} from "./createMenu";
import {DiagramList} from "./diagramList";
export function Menu() {
const [createState, setCreateState] = useState('none');
const [desktopTutorialState, setDesktopTutorialState] = useState('none');
const [diagramListState, setDiagramListState] = useState('none');
function handleCreateClick(evt: React.MouseEvent<HTMLAnchorElement>) {
evt.preventDefault();
setCreateState(createState == 'none' ? 'block' : 'none');
}
function handleDesktopTutorialClick(evt: React.MouseEvent<HTMLAnchorElement>) {
evt.preventDefault();
setDesktopTutorialState(desktopTutorialState == 'none' ? 'block' : 'none');
}
function handleDiagramListClick(evt: React.MouseEvent<HTMLAnchorElement>) {
evt.preventDefault();
if (!evt.currentTarget.id) {
return;
}
switch (evt.currentTarget.id) {
case 'imageUploadLink':
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = uploadImage;
document.body.appendChild(input);
input.click();
break;
default:
setDiagramListState(diagramListState == 'none' ? 'block' : 'none');
}
}
return (
<div>
<MainMenu onClick={handleDiagramListClick}/>
<CreateMenu display={createState} toggleCreateMenu={handleCreateClick}/>
<DiagramList onClick={handleCreateClick} display={diagramListState}/>
</div>
)
}

View File

@ -0,0 +1,26 @@
export function PasswordDialog() {
const onsubmitClick = (evt) => {
evt.preventDefault();
const password = (document.querySelector('#passwordInput') as HTMLInputElement).value;
if (password.length < 4) {
window.alert('Password must be longer than 4 characters');
} else {
const event = new CustomEvent('passwordset', {detail: password});
document.dispatchEvent(event);
(document.querySelector('#password') as HTMLInputElement).style.display = 'none';
}
}
const onCancelClick = (evt) => {
evt.preventDefault();
(document.querySelector('#password') as HTMLInputElement).style.display = 'none';
}
return (
<div className="overlay" id="password">
<div>
<div><input autoComplete="on" id="passwordInput" placeholder="Enter password" type="password"/></div>
<div><a href="#" id="passwordActionLink" onClick={onsubmitClick}>Enter</a></div>
<div><a className="cancel" href="#" onClick={onCancelClick} id="cancelPasswordLink">Cancel</a></div>
</div>
</div>
)
}

View File

@ -0,0 +1,8 @@
export function QuestLink() {
const link = "https://www.oculus.com/open_url/?url=https://www.deepdiagram.com" + document.location.pathname;
return (
<div id="questLaunch">
<a href={link} target="_blank">Launch On Quest</a>
</div>
)
}

View File

@ -0,0 +1,8 @@
export function TutorialMenu({onClick}) {
return (
<div className="overlay" id="tutorial">
<h1>Help</h1>
<div id="desktopTutorial"><a href="#" id="desktopLink" onClick={onClick}>Desktop</a></div>
</div>
)
}

148
src/react/styles.css Normal file
View File

@ -0,0 +1,148 @@
ul {
list-style-type: none;
padding-inline-start: 0;
}
li {
margin: 5px;
}
label {
color: #999922;
width: 100%;
font-size: medium;
font-weight: bold;
display: inline-block;
}
a {
display: block;
padding: 5px;
text-decoration: none;
vertical-align: middle;
border-color: #FFD700;
border-style: outset;
border-width: 2px;
border-radius: 10px;
text-align: center;
line-height: 36px;
margin: 5px;
}
a.cancel:link {
font-size: smaller;
font-weight: lighter;
font-style: italic;
border-color: #E0C000;
color: #FFEFBB;
background-color: #888822;
}
a:visited, a:link {
color: white;
background-color: #999922;
}
a:hover {
background-color: #FFD700;
color: #000000;
}
li a {
text-align: left;
border-radius: 2px;
background-color: #338822;
}
input {
display: inline-block;
margin: 10px auto;
text-decoration: none;
border-color: #FFD700;
border-style: solid;
border-width: 1px;
padding: 10px;
width: 200px;
}
h1 {
font-size: x-large;
font-weight: bolder;
text-align: center;
color: #F9F9E9;
}
div.overlay {
position: absolute;
display: block;
border: 1px solid #555555;
transform: translate(-50%, -50%);
z-index: 12;
height: 120px;
padding: 10px;
}
div#create {
left: 390px;
top: 400px;
transform: translate(-50%, -50%);
z-index: 14;
width: 320px;
height: 344px;
border: 1px solid #222222;
display: none;
background-color: #000;
}
#diagramList {
overflow: scroll;
left: 390px;
top: 400px;
height: 500px;
background-color: #000;
padding: 5px;
}
#main.mini {
left: 110px;
top: 50%;
height: fit-content;
background-color: #000;
}
#main.mini img, #tutorial img {
width: 160px;
height: 60px;
}
#tutorial {
z-index: 15;
left: 100px;
top: 750px;
width: 160px;
height: 210px;
}
#password {
display: none;
left: 50%;
top: 50%;
}
#closekey, #closekey a:active, #closekey a:visited, #closekey a:link {
position: relative;
color: #ffffff;
}
div.inactive a {
background-color: #222222;
color: #555555;
border-color: #222222;
cursor: not-allowed;
}

View File

@ -1,232 +1,12 @@
import {useEffect, useState} from "react"; import {PasswordDialog} from "./components/passwordDialog";
import {uploadImage} from "./functions/uploadImage"; import {Menu} from "./components/menu";
import {viewOnly} from "../util/functions/getPath"; import "./styles.css";
import axios from "axios";
function MainMenu({onClick}) {
if (viewOnly()) {
return (
<div className="overlay mini" id="main">
<img height="120" src="/assets/ddd.svg" width="320"/>
<div id="enterXR" className="inactive"><a href="#" id="enterVRLink">Enter VR</a></div>
<QuestLink/>
<div id="download"><a href="#" id="downloadLink">Download Model</a></div>
</div>)
} else {
return (
<div className="overlay mini" id="main">
<img height="120" src="/assets/ddd.svg" width="320"/>
<div id="enterXR" className="inactive"><a href="#" id="enterVRLink">Enter VR</a></div>
<QuestLink/>
<div id="diagrams"><a href="#" id="diagramsLink" onClick={onClick}>Diagrams</a></div>
<div id="imageUpload"><a href="#" id="imageUploadLink" onClick={onClick}>Upload Image</a></div>
<div id="download"><a href="#" id="downloadLink">Download Model</a></div>
</div>
)
}
}
function PasswordDialog() {
const onsubmitClick = (evt) => {
evt.preventDefault();
const password = (document.querySelector('#passwordInput') as HTMLInputElement).value;
if (password.length < 4) {
window.alert('Password must be longer than 4 characters');
} else {
const event = new CustomEvent('passwordset', {detail: password});
document.dispatchEvent(event);
document.querySelector('#password').style.display = 'none';
}
}
const onCancelClick = (evt) => {
evt.preventDefault();
document.querySelector('#password').style.display = 'none';
}
return (
<div className="overlay" id="password">
<div>
<div><input autoComplete="on" id="passwordInput" placeholder="Enter password" type="password"/></div>
<div><a href="#" id="passwordActionLink" onClick={onsubmitClick}>Enter</a></div>
<div><a className="cancel" href="#" onClick={onCancelClick} id="cancelPasswordLink">Cancel</a></div>
</div>
</div>
)
}
function CreateMenu({display, toggleCreateMenu}) {
const onCreateClick = (evt) => {
evt.preventDefault();
const name = (document.querySelector('#createName') as HTMLInputElement).value;
let password = (document.querySelector('#createPassword') as HTMLInputElement).value;
const password2 = (document.querySelector('#createPassword2') as HTMLInputElement).value;
if (password !== password2) {
window.alert('Passwords do not match');
return;
}
const id = window.crypto.randomUUID().replace(/-/g, '_');
if (password.length == 0) {
password = id;
}
const encrypted = (password != id);
localStorage.setItem(id, name);
if (name && name.length > 4) {
axios.post(import.meta.env.VITE_CREATE_ENDPOINT,
{
"_id": "org.couchdb.user:" + id,
"name": id,
"password": password,
"roles": ["readers"],
"type": "user"
}
).then(response => {
console.log(response);
const evt = new CustomEvent('dbcreated', {
detail: {
id: id,
name: name,
password: password,
encrypted: encrypted
}
});
document.dispatchEvent(evt);
}).catch(error => {
console.error(error);
});
} else {
window.alert('Name must be longer than 4 characters');
}
}
return (
<div className="overlay" id="create" style={{'display': display}}>
<div>
<div><input id="createName" placeholder="Enter a name for your diagram" type="text"/></div>
<div><input id="createPassword" placeholder="(Optional) Password" type="password"/></div>
<div><input id="createPassword2" placeholder="(Optional) Password" type="password"/></div>
<div><a href="#" id="createActionLink" onClick={onCreateClick}>Create</a></div>
<div><a className="cancel" onClick={toggleCreateMenu} href="#" id="cancelCreateLink">Cancel</a></div>
</div>
</div>
)
}
function TutorialMenu({onClick}) {
return (
<div className="overlay" id="tutorial">
<h1>Help</h1>
<div id="desktopTutorial"><a href="#" id="desktopLink" onClick={onClick}>Desktop</a></div>
</div>
)
}
function KeyboardHelp({display, onClick}) {
return (
<div className="overlay" id="keyboardHelp" style={{'display': display}}>
<div id="closekey"><a href="#" onClick={onClick}>X</a></div>
<img height="240" src="/assets/textures/keyboardhelp2.jpg" width="480"/>
<img height="240" src="/assets/textures/mousehelp.jpg" width="180"/>
</div>
)
}
function QuestLink() {
const link = "https://www.oculus.com/open_url/?url=https://www.deepdiagram.com" + document.location.pathname;
return (
<div id="questLaunch">
<a href={link} target="_blank">Launch On Quest</a>
</div>
)
}
function DiagramList({display, onClick}) {
const [dbList, setDbList] = useState([]);
useEffect(() => {
const listDb = async () => {
const data = await indexedDB.databases();
let i = 0;
setDbList(data.filter((item) => item.name.indexOf('_pouch_') > -1).map((item) => {
const dbid = item.name.replace('_pouch_', '');
let friendlyName = localStorage.getItem(dbid);
if (!friendlyName) {
friendlyName = dbid;
}
return {key: dbid, name: friendlyName}
}));
};
listDb();
}, []);
return (
<div className="overlay" id="diagramList" style={{'display': display}}>
<h1>Diagrams</h1>
<div id="startCreate"><a href="#" id="startCreateLink" onClick={onClick}>New</a></div>
<div id="diagramListContent">
<ul>
{dbList.map((item) => <li key={item.key}><a href={`/db/${item.key}`}>{item.name}</a></li>)}
</ul>
</div>
</div>
)
}
function Menu() {
const [createState, setCreateState] = useState('none');
const [desktopTutorialState, setDesktopTutorialState] = useState('none');
const [diagramListState, setDiagramListState] = useState('none');
function handleCreateClick(evt: React.MouseEvent<HTMLAnchorElement>) {
evt.preventDefault();
setCreateState(createState == 'none' ? 'block' : 'none');
}
function handleDesktopTutorialClick(evt: React.MouseEvent<HTMLAnchorElement>) {
evt.preventDefault();
setDesktopTutorialState(desktopTutorialState == 'none' ? 'block' : 'none');
}
function handleDiagramListClick(evt: React.MouseEvent<HTMLAnchorElement>) {
evt.preventDefault();
if (!evt.currentTarget.id) {
return;
}
switch (evt.currentTarget.id) {
case 'imageUploadLink':
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = uploadImage;
document.body.appendChild(input);
input.click();
break;
default:
setDiagramListState(diagramListState == 'none' ? 'block' : 'none');
}
}
return (
<div>
<MainMenu onClick={handleDiagramListClick}/>
<CreateMenu display={createState} toggleCreateMenu={handleCreateClick}/>
<DiagramList onClick={handleCreateClick} display={diagramListState}/>
</div>
)
}
export default function WebApp() { export default function WebApp() {
document.addEventListener('promptpassword', (evt) => { document.addEventListener('promptpassword', (evt) => {
const password = document.querySelector('#password'); const password = document.querySelector('#password');
if (password) { if (password) {
password.style.display = 'block'; (password as HTMLInputElement).style.display = 'block';
} }
}); });
return ( return (

View File

@ -17,7 +17,9 @@ export async function buildColor(color: Color3, scene: Scene, parent: TransformN
const height = .1; const height = .1;
const material = new StandardMaterial("material-" + color.toHexString(), scene); const material = new StandardMaterial("material-" + color.toHexString(), scene);
material.diffuseColor = color; material.diffuseColor = color;
material.ambientColor = color;
//material.roughness = 1;
material.specularPower = 64;
// material.ambientColor = color; // material.ambientColor = color;
//material.roughness = .1; //material.roughness = .1;
//material.maxSimultaneousLights = 2; //material.maxSimultaneousLights = 2;

View File

@ -1,13 +1,13 @@
import { import {
Color3, Color3,
GroundMesh, GroundMesh,
HemisphericLight,
Material, Material,
MeshBuilder, MeshBuilder,
Observable, Observable,
PBRMaterial, PBRMaterial,
PhysicsAggregate, PhysicsAggregate,
PhysicsShapeType, PhysicsShapeType,
PointLight,
PointsCloudSystem, PointsCloudSystem,
Scene, Scene,
Sound, Sound,
@ -33,14 +33,15 @@ export class CustomEnvironment {
if (loading) { if (loading) {
loading.remove(); loading.remove();
} }
this.scene.ambientColor = new Color3(1, 1, 1); this.scene.ambientColor = new Color3(.1, .1, .1);
//const light = new HemisphericLight("light1", new Vector3(1, 2, 1), this.scene); const light = new HemisphericLight("light1", new Vector3(.5, 1, 1).normalize(), this.scene);
//light.groundColor = new Color3(.1, .1, .1) light.groundColor = new Color3(0, 0, 0);
//light.diffuse = new Color3(1, 1, 1); light.diffuse = new Color3(1, 1, 1);
light.intensity = .8;
//light.setDirectionToTarget(new Vector3(.4, .5, .5).normalize()); //light.setDirectionToTarget(new Vector3(.4, .5, .5).normalize());
//light.intensity = .7; //light.intensity = .7;
const light = new PointLight("light1", new Vector3(0, 10, 10), this.scene); //const light = new PointLight("light1", new Vector3(0, 10, 10), this.scene);
const light2 = new PointLight("light1", new Vector3(0, 10, -10), this.scene); //const light2 = new PointLight("light1", new Vector3(0, 10, -10), this.scene);
const physics = new CustomPhysics(this.scene, config); const physics = new CustomPhysics(this.scene, config);
physics physics
.initializeAsync() .initializeAsync()