Updated UI to use Mantine.

This commit is contained in:
Michael Mainguy 2024-08-27 15:38:07 -05:00
parent b9152678b8
commit 2397ddcd4c
29 changed files with 1820 additions and 643 deletions

View File

@ -6,44 +6,23 @@
<meta content="An immersive vr diagramming experience based using webxr version 0.0.8-14 (2024-07-03T13:09:05.707Z) 4fdcc9694d3614be538e425110d1ab50cd20b302" <meta content="An immersive vr diagramming experience based using webxr version 0.0.8-14 (2024-07-03T13:09:05.707Z) 4fdcc9694d3614be538e425110d1ab50cd20b302"
name="description"> name="description">
<meta content="width=device-width, initial-scale=1, height=device-height" name="viewport"> <meta content="width=device-width, initial-scale=1, height=device-height" name="viewport">
<link href="/styles.css" rel="stylesheet"> <!--<link href="/styles.css" rel="stylesheet"> -->
<link href="/assets/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"> <link href="/assets/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png">
<link href="/assets/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"> <link href="/assets/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png">
<link href="/assets/favicon-96x96.png" rel="icon" sizes="96x96" type="image/png"> <link href="/assets/favicon-96x96.png" rel="icon" sizes="96x96" type="image/png">
<title>Deep Diagram</title> <title>Deep Diagram</title>
<link as="script" href="/newRelic.js" rel="preload"> <!-- <link as="script" href="/newRelic.js" rel="preload">
<script defer src="/newRelic.js"></script> <script defer src="/newRelic.js"></script> -->
<script defer src="/src/webApp.ts" type="module"></script>
<script defer src="/src/vrApp.ts" type="module"></script>
<link href="/manifest.webmanifest" rel="manifest"/> <link href="/manifest.webmanifest" rel="manifest"/>
<!--<script src='/niceware.js'></script>--> <!--<script src='/niceware.js'></script>-->
<style> <style>
#feed {
display: none;
}
#keyboardHelp {
display: none;
width: 665px;
height: 312px;
}
#keyboardHelp .button {
background-color: white;
width: 16px;
height: 16px;
display: inline-block;
text-align: center;
color: #000000;
}
#keyboardHelp div {
background: transparent;
}
</style> </style>
</head> </head>
<body> <body>
<img id="loadingGrid" src="/assets/grid6.jpg"/>
<script> <script>
if (typeof navigator.serviceWorker !== 'undefined') { if (typeof navigator.serviceWorker !== 'undefined') {
if (localStorage.getItem('serviceWorkerVersion') !== '11') { if (localStorage.getItem('serviceWorkerVersion') !== '11') {
@ -57,38 +36,18 @@
navigator.serviceWorker.register('/sw.js', {updateViaCache: 'none'}); navigator.serviceWorker.register('/sw.js', {updateViaCache: 'none'});
} }
</script> </script>
<script>
/*
var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition
var SpeechGrammarList = SpeechGrammarList || window.webkitSpeechGrammarList
var SpeechRecognitionEvent = SpeechRecognitionEvent || webkitSpeechRecognitionEvent
var recognition = new SpeechRecognition();
recognition.continuous = false;
recognition.lang = 'en-US';
recognition.interimResults = true;
recognition.maxAlternatives = 1;
recognition.onresult = function(event) {
console.log(event.results[0][0].transcript);
}
recognition.onend = function() {
console.log("recognition ended");
recognition.start();
}
console.log("starting recognition");
recognition.start();
*/
</script>
<div class="webApp" id="webApp"> <div class="webApp" id="webApp">
</div> </div>
<script defer src="/src/webApp.ts" type="module"></script>
<!--<video id="feed" controls="" autoplay="" name="media"><source src="https://listen.broadcastify.com/1drb2xhywkg8nvz.mp3?nc=49099&amp;xan=xtf9912b41c" type="audio/mpeg"></video> --> <!--<video id="feed" controls="" autoplay="" name="media"><source src="https://listen.broadcastify.com/1drb2xhywkg8nvz.mp3?nc=49099&amp;xan=xtf9912b41c" type="audio/mpeg"></video> -->
<!--
<div class="scene"> <div class="scene">
<canvas id="gameCanvas"></canvas> <canvas id="gameCanvas"></canvas>
</div> </div>
-->
<!--<script defer src="/src/vrApp.ts" type="module"></script>-->
</body> </body>
</html> </html>

1620
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "immersive", "name": "immersive",
"private": true, "private": true,
"version": "0.0.8-16", "version": "0.0.8-17",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
@ -17,6 +17,7 @@
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps" "havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps"
}, },
"dependencies": { "dependencies": {
"@auth0/auth0-react": "^2.2.4",
"@babylonjs/core": "^7.21.5", "@babylonjs/core": "^7.21.5",
"@babylonjs/gui": "^7.21.5", "@babylonjs/gui": "^7.21.5",
"@babylonjs/havok": "1.3.4", "@babylonjs/havok": "1.3.4",
@ -24,6 +25,10 @@
"@babylonjs/loaders": "^7.21.5", "@babylonjs/loaders": "^7.21.5",
"@babylonjs/materials": "^7.21.5", "@babylonjs/materials": "^7.21.5",
"@babylonjs/serializers": "^7.21.5", "@babylonjs/serializers": "^7.21.5",
"@emotion/react": "^11.13.0",
"@mantine/core": "7.12.0",
"@mantine/form": "7.12.0",
"@mantine/hooks": "7.12.0",
"@maptiler/client": "1.8.1", "@maptiler/client": "1.8.1",
"@picovoice/cobra-web": "^2.0.3", "@picovoice/cobra-web": "^2.0.3",
"@picovoice/eagle-web": "^1.0.0", "@picovoice/eagle-web": "^1.0.0",
@ -33,24 +38,25 @@
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
"axios": "^1.6.8", "axios": "^1.6.8",
"canvas-hypertxt": "1.0.3", "canvas-hypertxt": "1.0.3",
"events": "^3.3.0",
"hash-wasm": "4.11.0",
"hls.js": "^1.1.4", "hls.js": "^1.1.4",
"js-crypto-aes": "1.0.6",
"loglevel": "^1.9.1", "loglevel": "^1.9.1",
"meaningful-string": "^1.4.0",
"peer-lite": "2.0.2", "peer-lite": "2.0.2",
"pouchdb": "^8.0.1", "pouchdb": "^8.0.1",
"pouchdb-find": "^8.0.1", "pouchdb-find": "^8.0.1",
"query-string": "^8.1.0", "query-string": "^8.1.0",
"react-dom": "^18.2.0", "react-router-dom": "^6.26.1",
"@tabler/icons-react": "^3.14.0",
"recordrtc": "^5.6.0", "recordrtc": "^5.6.0",
"rfc4648": "^1.5.3", "rfc4648": "^1.5.3",
"round": "^2.0.1", "round": "^2.0.1",
"uuid": "^9.0.1",
"js-crypto-aes": "1.0.6",
"events": "^3.3.0",
"hash-wasm": "4.11.0",
"uint8-to-b64": "^1.0.2", "uint8-to-b64": "^1.0.2",
"meaningful-string": "^1.4.0", "uuid": "^9.0.1",
"websocket-ts": "^2.1.5", "websocket": "^1.0.34",
"websocket": "^1.0.34" "websocket-ts": "^2.1.5"
}, },
"devDependencies": { "devDependencies": {
"@types/dom-to-image": "^2.6.7", "@types/dom-to-image": "^2.6.7",

View File

@ -1,5 +1,5 @@
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.1.0/workbox-sw.js'); importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.1.0/workbox-sw.js');
const VERSION = '@@VERSION'; const VERSION = '0.0.8-18';
const CACHE = "deepdiagram"; const CACHE = "deepdiagram";
const IMAGEDELIVERY_CACHE = "deepdiagram-images"; const IMAGEDELIVERY_CACHE = "deepdiagram-images";
const MAPTILE_CACHE = 'maptiler'; const MAPTILE_CACHE = 'maptiler';

View File

@ -1,69 +0,0 @@
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

@ -1,32 +0,0 @@
import {useEffect, useState} from "react";
export function DiagramList({display, onClick}) {
const [dbList, setDbList] = useState([]);
useEffect(() => {
const listDb = async () => {
const data = await indexedDB.databases();
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

@ -1,9 +0,0 @@
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 alt="keyboard help" height="240" src="/assets/textures/keyboardhelp2.jpg" width="480"/>
<img alt="mouse help" height="240" src="/assets/textures/mousehelp.jpg" width="180"/>
</div>
)
}

View File

@ -1,27 +0,0 @@
import {viewOnly} from "../../util/functions/getPath";
import {QuestLink} from "./questLink";
export function MainMenu({onClick}) {
if (viewOnly()) {
return (
<div className="overlay mini" id="main">
<img alt="deep diagram logo" 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 alt="deep diagram logo" 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

@ -1,51 +0,0 @@
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

@ -1,26 +0,0 @@
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

@ -1,8 +0,0 @@
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

@ -1,8 +0,0 @@
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>
)
}

View File

@ -0,0 +1,29 @@
import {Indicator, Menu, Tooltip} from "@mantine/core";
import {useState} from "react";
export default function VrMenuItem({availableIcon, tip, label, onClick}) {
const [processing, setProcessing] = useState(true);
window.setTimeout(() => {
setProcessing(false);
}, 2000)
if (availableIcon) {
return (<Tooltip multiline={true} w={256}
label={tip} position="right">
<Indicator size={25} radius="lg" offset={-15} processing={processing} position="middle-end"
label={availableIcon}>
<Menu.Item
onClick={onClick || null}>
{label}</Menu.Item>
</Indicator>
</Tooltip>)
} else {
return (
<Tooltip multiline={true} w={256}
label={tip} position="right">
<Menu.Item
onClick={onClick || null}>
{label}</Menu.Item>
</Tooltip>)
}
}

View File

@ -0,0 +1,83 @@
import {Accordion, Card, Center} from "@mantine/core";
import PageTemplate from "../pageTemplate";
export default function About() {
return (
<PageTemplate>
<Card m={64}>
<Card.Section>
<Center>
<h1>VisuaLab VR: Unleash the Power of Immersive Diagramming</h1>
</Center>
</Card.Section>
<Card.Section>
<h3>Diagram complex systems effortlessly with VisuaLab VR fast, intuitive, and 40x more
effective.</h3>
</Card.Section>
<Card.Section>
<Accordion defaultValue="1">
<Accordion.Item key="1" value="1">
<Accordion.Control>What is VisuaLab VR?</Accordion.Control>
<Accordion.Panel>
Step into a world where complex ideas come to life in 3D. With VisuaLab VR, youre not
just
creating diagrams youre immersing yourself in them. Our cutting-edge VR technology
transforms the
way you visualize, understand, and communicate complex systems. Whether youre mapping
out intricate
workflows, designing sophisticated models, or explaining multifaceted processes,
VisuaLab VR makes
it easy.
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item key="2" value="2">
<Accordion.Control>Why Choose VisuaLab VR?</Accordion.Control>
<Accordion.Panel>
Immersive Experience: Traditional 2D diagrams cant compare to the power of full
immersion. When you
step into VisuaLab VR, you interact with your diagrams in three dimensions, making it 40
times more
effective at explaining complex systems. See every connection, understand every
relationship, and
explore every detail as if youre inside the system itself.
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item key="3" value="3">
<Accordion.Control>Fast and Intuitive</Accordion.Control>
<Accordion.Panel>
No steep learning curve here. VisuaLab VR is designed for speed and simplicity.
Drag, drop, and connect elements with ease, all in a visually rich environment. Create
stunning
diagrams in minutes, not hours.
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item key="4" value="4">
<Accordion.Control>Affordable Innovation</Accordion.Control>
<Accordion.Panel>
High-quality VR doesnt have to break the bank. VisuaLab VR offers premium
features at a fraction of the cost, making it accessible for everyone from solo creators
to large
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item key="5" value="5">
<Accordion.Control>The Science Behind It</Accordion.Control>
<Accordion.Panel>
Studies show that immersive environments increase the ability to
comprehension and retention by up to 40 times compared to traditional 2D methods. By
engaging
multiple senses and providing a spatial understanding, VisuaLab VR allows you to grasp
complex
systems faster and with greater clarity. Youre not just looking at a diagram; youre
experiencing
it. This deep engagement leads to quicker insights, more efficient problem-solving, and
better
decision-making.
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Card.Section>
</Card>
</PageTemplate>
)
}

View File

@ -0,0 +1,10 @@
import PageTemplate from "../pageTemplate";
export default function Documentation() {
return (
<PageTemplate>
<h1>Documentation</h1>
<p>Some information about the documentation</p>
</PageTemplate>
)
}

View File

@ -0,0 +1,10 @@
import PageTemplate from "../pageTemplate";
export default function Examples() {
return (
<PageTemplate>
<h1>Examples</h1>
<p>Some information about the examples</p>
</PageTemplate>
)
}

View File

@ -0,0 +1,22 @@
import {Card, SimpleGrid} from "@mantine/core";
import PageTemplate from "../pageTemplate";
export default function Pricing() {
return (
<PageTemplate>
<h1>Pricing</h1>
<SimpleGrid cols={{base: 1, sm: 2, md: 3}} spacing="lg">
<Card>
Free
</Card>
<Card>
Basic
</Card>
<Card>
Team
</Card>
</SimpleGrid>
<p>Some information about the pricing</p>
</PageTemplate>
)
}

34
src/react/pageHeader.tsx Normal file
View File

@ -0,0 +1,34 @@
import {Anchor, AppShell, Button, Group, Image} from "@mantine/core";
import React from "react";
import {Link} from "react-router-dom";
export default function PageHeader() {
const onClick = (e) => {
e.preventDefault();
console.log(e);
}
return (
<AppShell.Header p={10}>
<Group justify="space-between">
<Image w={64} src="/assets/ddd.svg"/>
<Group justify="flex-end">
<Button key="signup">Sign up for Free</Button>
<Group visibleFrom="sm">
<Anchor component={Link} key="examples" to="/examples" p={5} c="myColor" bg="none"
underline="hover">Examples</Anchor>
<Anchor component={Link} key="about" to="/" p={5} c="myColor" bg="none"
underline="hover">About</Anchor>
<Anchor component={Link} key="documentation" to="/documentation" p={5} c="myColor" bg="none"
underline="hover">Documentation</Anchor>
<Anchor component={Link} key="pricing" to="/pricing" p={5} c="myColor" bg="none"
underline="hover">Pricing</Anchor>
<Anchor component={Link} key="vrexperience" to="/db/local" p={5} c="myColor" bg="none"
underline="hover">VR Experience</Anchor>
</Group>
<Button>Login</Button>
</Group>
</Group>
</AppShell.Header>
)
}

View File

@ -0,0 +1,15 @@
import {AppShell} from "@mantine/core";
import PageHeader from "./pageHeader";
import React from "react";
export default function PageTemplate(props: { children: React.ReactNode }) {
return (
<AppShell
header={{height: 64}}>
<PageHeader/>
<AppShell.Main>
{props.children}
</AppShell.Main>
</AppShell>
)
}

View File

@ -0,0 +1,42 @@
import {Anchor, Button, Checkbox, Group, Modal, Pill, Stack, Textarea, TextInput} from "@mantine/core";
export default function CreateDiagramModal({createOpened, setCreateOpened}) {
const createDiagram = () => {
setCreateOpened(false);
}
return (
<Modal opened={createOpened} onClose={() => {
setCreateOpened(false)
}}>
<Stack>
<TextInput key="name" label="Name" placeholder="Enter diagram name" required/>
<Textarea key="description" label="Description" placeholder="Enter diagram description"/>
<Group>
<Checkbox w={250} key="private" label="Private" disabled={true}/>
<Pill>Basic</Pill>
</Group>
<Group>
<Checkbox w={250} key="encrypted" label="Encrypted" disabled={true}/>
<Pill>Pro</Pill>
</Group>
<Group>
<Checkbox w={250} key="invite" label="Invite Collaborators" disabled={true}/>
<Pill>Pro</Pill>
</Group>
<Group>
<Button key="create" onClick={createDiagram}>Create</Button>
<Anchor p={5} size="sm" key="cancel" onClick={(e) => {
e.preventDefault();
setCreateOpened(false)
}}>Cancel</Anchor>
</Group>
</Stack>
</Modal>
)
}

View File

@ -0,0 +1,42 @@
import {Button, Card, Modal, Paper, SimpleGrid, Stack} from "@mantine/core";
import React from "react";
export default function ManageDiagramsModal({manageOpened, setManageOpened}) {
const diagrams = [
{name: "Diagram 1", description: "Description 1"},
{name: "Diagram 1", description: "Description 1"},
{name: "Diagram 1", description: "Description 1"},
{name: "Diagram 1", description: "Description 1"},
{name: "Diagram 1", description: "Description 1"},
{name: "Diagram 1", description: "Description 1"},
]
const cards = diagrams.map((diagram) => {
return (<Card><h1>{diagram.name}</h1></Card>)
});
const buildCreateButton = () => {
if (diagrams.length < 6) {
return <Button size="lg" disabled={false}>Create</Button>
} else {
return (<Stack>
<Button size="lg" disabled={true}>Create</Button>
<Paper>You've reached the max number of diagrams for this Tier.</Paper>
<Button size="xl">Upgrade To Pro</Button>
</Stack>)
}
}
return (
<Modal opened={manageOpened} size="lg" onClose={() => {
setManageOpened(false)
}}>
<h1>Select a Diagram</h1>
<SimpleGrid cols={3}>
{cards}
</SimpleGrid>
{buildCreateButton()}
</Modal>
)
}

View File

@ -0,0 +1,99 @@
import VrApp from '../../vrApp';
import {useEffect, useState} from "react";
import {Affix, Burger, Group, Menu, useMantineTheme} from "@mantine/core";
import VrTemplate from "../vrTemplate";
import {IconStar} from "@tabler/icons-react";
import VrMenuItem from "../components/vrMenuItem";
import CreateDiagramModal from "./createDiagramModal";
import ManageDiagramsModal from "./manageDiagramsModal";
export default function VrExperience() {
const theme = useMantineTheme();
const [createDiagram, setCreateDiagram] = useState(false);
const [manageDiagrams, setManageDiagrams] = useState(false);
const [immersiveDisabled, setImmersiveDisabled] = useState(true);
useEffect(() => {
const vrApp = new VrApp(document.querySelector('#gameCanvas')); // code to run after render goes here
}, []);
navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
setImmersiveDisabled(!supported);
});
const availableInFree = () => {
return null
}
const availableInBasic = () => {
return <Group w={50}>Basic</Group>
}
const availableInPro = () => {
return <Group w={50}>Pro!<IconStar size={11}/></Group>
}
const enterImmersive = (e) => {
console.log('entering immersive mode');
e.preventDefault();
const event = new CustomEvent('enterXr', {bubbles: true});
window.dispatchEvent(event);
}
return (
<VrTemplate>
<Affix position={{top: 30, left: 60}}>
<Menu trigger="hover" openDelay={50} closeDelay={400}>
<Menu.Target>
<Burger size="xl"/>
</Menu.Target>
<Menu.Dropdown>
<VrMenuItem
tip={immersiveDisabled ? "Browser does not support WebXR. Immersive experience best viewed with Meta Quest headset" : "Enter Immersive Mode"}
onClick={enterImmersive}
label="Enter Immersive Mode"
availableIcon={availableInFree()}/>
<VrMenuItem
tip="Open a new window and automatically send experience to your Meta Quest headset"
onClick={() => {
window.open('https://www.oculus.com/open_url/?url=' + window.location.href, 'launchQuest', 'popup')
}}
label="Launch On Meta Quest"
availableIcon={availableInFree()}/>
<Menu.Divider/>
<VrMenuItem
tip="Edit data on desktop (Best for large amounts of text or images). After adding data, you can enter immersive mode to further refine the model."
label="Edit Data"
onClick={null}
availableIcon={availableInFree()}/>
<Menu.Divider/>
<VrMenuItem
tip="Create a new diagram from scratch"
label="Create"
onClick={() => {
setCreateDiagram(!createDiagram)
}}
availableIcon={availableInFree()}/>
<VrMenuItem
tip="Create a new diagram from predefined template"
label="Create From Template"
onClick={null}
availableIcon={availableInBasic()}/>
<VrMenuItem
tip="Manage Diagrams"
label="Manage"
onClick={() => {
setManageDiagrams(!manageDiagrams)
}}
availableIcon={availableInFree()}/>
<Menu.Divider/>
<VrMenuItem
tip="Share your model with others and collaborate in real time with others. This is a paid feature."
label="Share"
onClick={null}
availableIcon={availableInPro()}/>
</Menu.Dropdown>
</Menu>
</Affix>
<canvas id="gameCanvas" style={{width: '100%', height: '100vh'}}/>
<CreateDiagramModal createOpened={createDiagram} setCreateOpened={setCreateDiagram}/>
<ManageDiagramsModal manageOpened={manageDiagrams} setManageOpened={setManageDiagrams}/>
</VrTemplate>
)
}

32
src/react/theme.ts Normal file
View File

@ -0,0 +1,32 @@
import {createTheme, MantineColorsTuple} from "@mantine/core";
const myColor: MantineColorsTuple = [
'#fdfce5',
'#f8f6d3',
'#f0ecaa',
'#e7e17c',
'#e0d957',
'#dbd33e',
'#d9d02f',
'#c0b820',
'#aaa316',
'#938c03'
];
export const theme = createTheme({
colors: {
myColor
},
breakpoints: {
xs: '30em',
sm: '45em',
md: '64em',
lg: '74em',
xl: '90em',
},
primaryShade: 5,
autoContrast: true,
defaultRadius: 'md',
primaryColor: 'myColor',
/* Put your mantine themeM override here */
});

11
src/react/vrTemplate.tsx Normal file
View File

@ -0,0 +1,11 @@
import {Container} from "@mantine/core";
import React from "react";
export default function VrTemplate(props: { children: React.ReactNode }) {
return (
<Container fluid={true}>
{props.children}
</Container>
)
}

View File

@ -1,6 +1,10 @@
import {PasswordDialog} from "./components/passwordDialog";
import {Menu} from "./components/menu";
import "./styles.css"; import "./styles.css";
import '@mantine/core/styles.css';
import React from "react";
import {RouterProvider} from "react-router-dom";
import {webRouter} from "./webRouter";
import {theme} from "./theme";
import {MantineProvider} from "@mantine/core";
export default function WebApp() { export default function WebApp() {
document.addEventListener('promptpassword', () => { document.addEventListener('promptpassword', () => {
@ -9,10 +13,9 @@ export default function WebApp() {
(password as HTMLInputElement).style.display = 'block'; (password as HTMLInputElement).style.display = 'block';
} }
}); });
return (
<div> return (<MantineProvider defaultColorScheme="dark" theme={theme}>
<Menu/> <RouterProvider router={webRouter}/>
<PasswordDialog/> </MantineProvider>
</div>
) )
} }

30
src/react/webRouter.tsx Normal file
View File

@ -0,0 +1,30 @@
import React from "react";
import {createBrowserRouter} from "react-router-dom";
import About from "./marketing/about";
import Documentation from "./marketing/documentation";
import Examples from "./marketing/examples";
import Pricing from "./marketing/pricing";
import VrExperience from "./pages/vrExperience";
export const webRouter = createBrowserRouter([
{
path: "/",
element: (
<About/>
),
},
{
path: "/documentation",
element: (<Documentation/>)
}, {
path: "/examples",
element: (<Examples/>)
}, {
path: "/Pricing",
element: (<Pricing/>)
}, {
path: "/db/:db",
element: (<VrExperience/>)
}
])

View File

@ -30,27 +30,11 @@ export async function groundMeshObserver(ground: AbstractMesh,
enablePointerSelectionOnAllControllers: true enablePointerSelectionOnAllControllers: true
} }
}); });
window.addEventListener('enterXr', async (e: CustomEvent) => {
await xr.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
logger.debug("Entering XR Experience");
})
//xr.baseExperience.featuresManager.enableFeature(WebXRFeatureName.LAYERS, "latest", { preferMultiviewOnInit: true }, true, false); //xr.baseExperience.featuresManager.enableFeature(WebXRFeatureName.LAYERS, "latest", { preferMultiviewOnInit: true }, true, false);
const enterButton = (document.querySelector('#enterXR') as HTMLAnchorElement);
if (enterButton) {
try {
const vrSupported = await xr.baseExperience.sessionManager.isSessionSupportedAsync('immersive-vr');
if (vrSupported) {
enterButton.classList.remove('inactive');
enterButton.addEventListener('click', async (evt) => {
evt.preventDefault();
//const voice = new VoiceRecognizer();
logger.debug('entering XR');
const enter = await xr.baseExperience.enterXRAsync('immersive-vr', 'local-floor');
logger.debug(enter);
});
}
} catch (err) {
logger.error(err);
}
}
if (spinner) { if (spinner) {
spinner.hide(); spinner.hide();

View File

@ -12,16 +12,16 @@ import {exportGltf} from "./util/functions/exportGltf";
import {DefaultScene} from "./defaultScene"; import {DefaultScene} from "./defaultScene";
import {Introduction} from "./tutorial/introduction"; import {Introduction} from "./tutorial/introduction";
const webGpu = false; const webGpu = false;
log.setLevel('error', false); log.setLevel('error', false);
const canvas = (document.querySelector('#gameCanvas') as HTMLCanvasElement); export default class VrApp {
export class VrApp {
//preTasks = [havokModule]; //preTasks = [havokModule];
private logger: Logger = log.getLogger('App'); private logger: Logger = log.getLogger('App');
private _canvas: HTMLCanvasElement
constructor() { constructor(canvas: HTMLCanvasElement) {
this._canvas = canvas;
this.initializeEngine().then(() => { this.initializeEngine().then(() => {
this.logger.info('Engine initialized'); this.logger.info('Engine initialized');
}); });
@ -58,17 +58,18 @@ export class VrApp {
} }
private async initializeEngine() { private async initializeEngine() {
if (!this._canvas) {
console.error('Canvas not found');
return;
}
let engine: WebGPUEngine | Engine = null; let engine: WebGPUEngine | Engine = null;
if (webGpu) { if (webGpu) {
engine = new WebGPUEngine(canvas); engine = new WebGPUEngine(this._canvas);
await (engine as WebGPUEngine).initAsync(); await (engine as WebGPUEngine).initAsync();
} else { } else {
engine = new Engine(canvas, true); engine = new Engine(this._canvas, true);
} }
engine.setHardwareScalingLevel(1 / window.devicePixelRatio); engine.setHardwareScalingLevel(1 / window.devicePixelRatio);
window.onresize = () => {
engine.resize();
}
const scene = new Scene(engine); const scene = new Scene(engine);
DefaultScene.Scene = scene; DefaultScene.Scene = scene;
scene.ambientColor = new Color3(.1, .1, .1); scene.ambientColor = new Color3(.1, .1, .1);
@ -78,9 +79,7 @@ export class VrApp {
}); });
} }
} }
const vrApp = new VrApp();
buildQuestLink(); buildQuestLink();
function setMainCamera(scene: Scene) { function setMainCamera(scene: Scene) {
const CAMERA_NAME = 'Main Camera'; const CAMERA_NAME = 'Main Camera';
const camera: FreeCamera = new FreeCamera(CAMERA_NAME, const camera: FreeCamera = new FreeCamera(CAMERA_NAME,

View File

@ -1,5 +1,4 @@
import {createRoot} from "react-dom/client"; import * as ReactDOM from "react-dom/client";
import WebApp from "./react/webApp"; import WebApp from "./react/webApp";
const root = createRoot(document.getElementById('webApp')); ReactDOM.createRoot(document.getElementById('webApp')).render(WebApp());
root.render(WebApp());