Everything

This commit is contained in:
loucass003
2022-04-02 22:36:25 +02:00
parent 56402526f7
commit 4ae37944d9
44 changed files with 1143 additions and 33495 deletions

33519
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,10 +13,11 @@
"@types/react-dom": "^17.0.14",
"add": "^2.0.6",
"classnames": "^2.3.1",
"dreampact": "^0.1.10",
"flatbuffers": "^2.0.6",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-hook-form": "^7.29.0",
"react-modal": "^3.14.4",
"react-router-dom": "^6.2.2",
"react-scripts": "5.0.0",
"slimevr-protocol": "file:../SlimeVR-Server/slimevr_protocol",
@@ -49,7 +50,9 @@
]
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.0",
"@tauri-apps/cli": "^1.0.0-rc.8",
"@types/react-modal": "^3.13.1",
"autoprefixer": "^10.4.4",
"cross-env": "^7.0.3",
"postcss": "^8.4.12",

View File

@@ -1,4 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
WixTools
# Generated by Cargo
# will have compiled files and executables
/target/
WixTools

View File

@@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
}
fn main() {
tauri_build::build()
}

View File

@@ -1,10 +1,10 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
fn main() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
fn main() {
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -54,8 +54,8 @@
"windows": [
{
"title": "Slimevr UI",
"width": 1033,
"height": 639,
"width": 1150,
"height": 660,
"resizable": true,
"fullscreen": false
}

View File

@@ -6,25 +6,63 @@ import {
Route,
} from "react-router-dom";
import { Overview } from './components/Overview';
import { Manage } from './components/Manage';
import { BigButton } from './components/commons/BigButton';
import { QuickResetIcon, ResetIcon } from './components/commons/icon/ResetIcon';
import { useReset } from './hooks/reset';
import { Settings } from './components/Settings';
import { Button } from './components/commons/Button';
import { useLayout } from './hooks/layout';
import { BodyProportions } from './components/BodyProportions';
function Layout() {
const { layoutHeight, ref } = useLayout();
const { reset, timer, reseting } = useReset()
return (
<>
<div ref={ref} className='flex-grow' style={{ height: layoutHeight }}>
<div className="flex bg-primary-1 h-full ">
<div className="flex flex-grow gap-10 flex-col bg-primary-2 rounded-tr-3xl">
<Routes>
<Route path="/" element={<Overview/>}/>
<Route path="/proportions" element={<BodyProportions/>}/>
<Route path="/settings" element={<Settings/>}/>
</Routes>
</div>
<div className="flex flex-col px-8 w-60 gap-8 pb-5 overflow-y-auto">
<div className='flex'>
<BigButton text={"Fast reset"} icon={<QuickResetIcon/>} onClick={() => reset(true)} ></BigButton>
</div>
<div className='flex'>
<BigButton text={!reseting ? "Reset" : `${3 - timer}`} icon={<ResetIcon />} onClick={() => reset(false)} disabled={reseting}></BigButton>
</div>
<div className='flex flex-grow flex-col justify-end'>
<Button variant='primary' className='w-full'>Debug</Button>
</div>
</div>
</div>
</div>
</>
)
}
function App() {
const websocketAPI = useProvideWebsocketApi();
return (
<WebSocketApiContext.Provider value={websocketAPI}>
<Router>
<div className='bg-primary h-full w-full overflow-hidden'>
<div className='flex-col h-full'>
<Navbar></Navbar>
<div className='flex-grow h-full'>
<Routes>
<Route path="/" element={<Overview/>}/>
<Route path="manage" element={<Manage/>}/>
</Routes>
</div>
{!websocketAPI.isConnected && <div className='flex w-full h-full justify-center items-center text-white p-2'>Connection lost to server</div>}
{websocketAPI.isConnected &&
<>
<Navbar></Navbar>
<Layout></Layout>
</>
}
</div>
</div>
</Router>

View File

@@ -1,14 +0,0 @@
import { ReactChild } from "react";
export function BigButton({ text, icon }: { text: string, icon: ReactChild }) {
return (
<div className="flex flex-col rounded-md hover:bg-primary-4 bg-primary-3 py-10 gap-8 cursor-pointer">
<div className="flex justify-around">{icon}</div>
<div className="flex justify-around text-2xl text-white">{text}</div>
</div>
)
}

File diff suppressed because one or more lines are too long

28
src/components/Modal.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { ReactChild } from "react";
import ReactModal from "react-modal";
import { IconButton } from "./commons/ButtonIcon";
import { CrossIcon } from "./commons/icon/CrossIcon";
export function AppModal({ children, name, ...props }: { children?: ReactChild, name: ReactChild } & ReactModal.Props) {
return (
<ReactModal
{...props}
overlayClassName="fixed top-0 right-0 left-0 bottom-0 bg-black bg-opacity-60 flex justify-center items-center overflow-y-auto border-none"
className="items-center w-full max-w-2xl h-full md:h-auto relative rounded-lg bg-primary-3 shadow-lg text-white border-none"
>
<div className="flex justify-between items-start p-5 rounded-t border-b-2 border-primary-1">
<h3 className="text-xl font-semibold lg:text-2x">
{name}
</h3>
<div className="flex">
<IconButton icon={<CrossIcon></CrossIcon>} onClick={props.onRequestClose}></IconButton>
</div>
</div>
<div className="p-6">
{children}
</div>
</ReactModal>
)
}

View File

@@ -1,13 +1,15 @@
import classnames from 'classnames';
import { ReactChild } from 'react';
import {
useMatch,
NavLink,
} from "react-router-dom";
import { CubeIcon } from './icon/CubeIcon';
import { SlimeVRIcon } from './icon/SimevrIcon';
import { CubeIcon } from './commons/icon/CubeIcon';
import { GearIcon } from './commons/icon/GearIcon';
import { SlimeVRIcon } from './commons/icon/SimevrIcon';
export function NavButton({ to, children, match }: { to: string, children: React.ReactChild, match?: string }) {
export function NavButton({ to, children, match, icon }: { to: string, children: ReactChild, match?: string, icon: ReactChild }) {
const doesMatch = useMatch({
path: match || to,
@@ -16,7 +18,7 @@ export function NavButton({ to, children, match }: { to: string, children: React
return (
<NavLink to={to} className={classnames("flex flex-grow flex-row gap-3 py-3 px-8 rounded-t-md group ", { 'bg-primary-2': doesMatch, 'hover:bg-primary-3': !doesMatch })}>
<div className="flex align-middle justify-center justify-items-center flex-col">
<CubeIcon className={ classnames("fill-primary-3 group-hover:fill-white", { 'fill-white': doesMatch })}></CubeIcon>
<div className={classnames("fill-primary-3 group-hover:fill-white", { 'fill-misc-3': doesMatch })}>{icon}</div>
</div>
<div className="flex text-white text-md">{children}</div>
</NavLink >
@@ -32,13 +34,13 @@ export function Navbar() {
<div className="flex justify-around flex-col">
<SlimeVRIcon></SlimeVRIcon>
</div>
<div className="flex text-white text-xl justify-around flex-col">SlimeVR</div>
<div className="flex text-white text-xl justify-around flex-col font-bold">SlimeVR</div>
</div>
</div>
<div className="flex px-5 gap-5 pt-1">
<NavButton to="/">Overview</NavButton>
<NavButton to="manage">Manage Trackers</NavButton>
<div className="flex px-5 gap-2 pt-2">
<NavButton to="/" icon={<CubeIcon></CubeIcon>}>Overview</NavButton>
<NavButton to="/proportions" icon={<GearIcon></GearIcon>}>Body proportions</NavButton>
<NavButton to="/settings" icon={<GearIcon></GearIcon>}>Settings</NavButton>
</div>
</div>
)

View File

@@ -1,37 +1,43 @@
import { useState } from "react";
import { OutboundUnion } from "slimevr-protocol/dist/server";
import { useMemo, useState } from "react";
import { OutboundUnion, TrackerPosition } from "slimevr-protocol/dist/server";
import { DeviceStatusT } from "slimevr-protocol/dist/slimevr-protocol/server/device-status";
import { TrackersListT } from "slimevr-protocol/dist/slimevr-protocol/server/trackers-list";
import { useWebsocketAPI } from "../hooks/websocket-api";
import { BigButton } from "./BigButton";
import { QuickResetIcon, ResetIcon } from "./icon/ResetIcon";
import { TrackerCard } from "./TrackerCard";
import { TrackerCard } from "./tracker/TrackerCard";
export function Overview() {
const { usePacket } = useWebsocketAPI();
const [list, setTrackersList] = useState<DeviceStatusT[]>([]);
usePacket(OutboundUnion.TrackersList, (packet: TrackersListT) => {
setTrackersList(packet.trackers)
})
const unasignedTrackers = useMemo(() => list.filter(({ editable, mountingPosition }) => editable && mountingPosition === TrackerPosition.NONE), [list]);
return (
<div className="flex bg-primary-1 h-full">
<div className="flex flex-grow gap-10 flex-col bg-primary-2 rounded-tr-3xl">
<div className="flex text-white text-2xl px-8 pt-8">
Tracker Overview
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5 sm:grid-cols-1 overflow-y-auto px-8">
{list.map((trackerStatus, index) => <TrackerCard key={index} status={trackerStatus}/>)}
</div>
<div className="overflow-y-auto flex flex-col gap-8">
<div className="flex text-white text-2xl px-8 pt-8 font-bold">
Tracker Overview
</div>
<div className="flex flex-col px-8 xs:w-1/4 sm:w-1/4 gap-8">
<BigButton text="Quick reset" icon={<QuickResetIcon/>}></BigButton>
<BigButton text="Reset Position" icon={<ResetIcon />}></BigButton>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5 sm:grid-cols-1 px-8">
{list.filter(({ editable, mountingPosition }) => editable && mountingPosition !== TrackerPosition.NONE).map((trackerStatus, index) => <TrackerCard key={index} status={trackerStatus}/>)}
{/* {list.filter(({ computed }) => computed).map((trackerStatus, index) => <TrackerCard key={index} status={trackerStatus}/>)} */}
</div>
{unasignedTrackers.length > 0 &&
<>
<div className="flex text-white text-2xl px-8 pt-8 font-bold">
Unassigned Trackers
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5 sm:grid-cols-1 px-8">
{unasignedTrackers.map((trackerStatus, index) => <TrackerCard key={index} status={trackerStatus}/>)}
</div>
</>
}
</div>
)
}

109
src/components/Settings.tsx Normal file
View File

@@ -0,0 +1,109 @@
import { useWebsocketAPI } from "../hooks/websocket-api";
import { CheckBox } from "./commons/Checkbox";
import { useForm } from "react-hook-form";
import { useEffect } from "react";
import { FilteringSettings, FilteringSettingsT, InboundUnion, OutboundUnion, SettingsRequestT, SteamVRTrackersSettingT } from "slimevr-protocol/dist/server";
import { ChangeSettingsRequestT } from "slimevr-protocol/dist/slimevr-protocol/server/change-settings-request";
import { SettingsResponseT } from "slimevr-protocol/dist/slimevr-protocol/server/settings-response";
import { Select } from "./commons/Select";
import { NumberSelector } from "./commons/NumberSelector";
interface SettingsForm {
trackers: {
waist: boolean,
chest: boolean,
legs: boolean,
knees: boolean,
elbows: boolean,
}
filtering: {
type: number;
intensity: number;
ticks: number;
}
}
export function Settings() {
const { sendPacket, usePacket } = useWebsocketAPI();
const { register, reset, control, watch, handleSubmit } = useForm<SettingsForm>({ defaultValues: { filtering: { intensity: 0, ticks: 0 } } });
const onSubmit = (values: SettingsForm) => {
const settings = new ChangeSettingsRequestT();
if (values.trackers) {
const trackers = new SteamVRTrackersSettingT();
trackers.waist = values.trackers.waist;
trackers.chest = values.trackers.chest;
trackers.legs = values.trackers.legs;
trackers.knees = values.trackers.knees;
trackers.elbows = values.trackers.elbows;
settings.steamVrTrackers = trackers;
}
const filtering = new FilteringSettingsT();
filtering.type = values.filtering.type;
filtering.intensity = values.filtering.intensity;
filtering.ticks = values.filtering.ticks;
settings.filtering = filtering;
sendPacket(InboundUnion.ChangeSettingsRequest, settings)
}
useEffect(() => {
const subscription = watch(() => handleSubmit(onSubmit)());
return () => subscription.unsubscribe();
}, [watch])
useEffect(() => {
sendPacket(InboundUnion.SettingsRequest, new SettingsRequestT());
}, [])
usePacket(OutboundUnion.SettingsResponse, (settings: SettingsResponseT) => {
reset({
...(settings.steamVrTrackers ? {trackers: settings.steamVrTrackers} : {}),
...(settings.filtering ? {filtering: settings.filtering} : {})
})
})
return (
<form className="px-8 flex flex-col gap-4 pt-4">
<div className="flex text-white text-2xl font-bold">
Settings
</div>
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<div className="flex text-gray-300 text-xl font-bold gap-5">
SteamVR Trackers
</div>
<div className="flex text-gray-300 text-xl font-bold gap-5">
<CheckBox {...register('trackers.waist')} label="Waist" />
<CheckBox {...register('trackers.chest')} label="Chest"/>
<CheckBox {...register('trackers.legs')} label="Legs"/>
<CheckBox {...register('trackers.knees')} label="Knees"/>
<CheckBox {...register('trackers.elbows')} label="Elbows"/>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="flex text-gray-300 text-xl font-bold gap-5">
Trackers Filtering
</div>
<div className="flex text-gray-300 text-xl font-bold gap-5">
<Select {...register('filtering.type')} label="Filtering Type" options={[{ label: 'None', value: 0 }, { label: 'Interpolation', value: 1 }, {label: 'Extrapolation', value: 2 }]}></Select>
<NumberSelector variant="smol" control={control} name="filtering.intensity" label="Intensity" valueLabelFormat={(value) => `${value}%`} min={0} max={100} step={10}></NumberSelector>
<NumberSelector variant="smol" control={control} name="filtering.ticks" label="Ticks" min={0} max={80} step={1}></NumberSelector>
{/* <CheckBox {...register('trackers.chest')} label="Chest"/>
<CheckBox {...register('trackers.legs')} label="Legs"/>
<CheckBox {...register('trackers.knees')} label="Knees"/>
<CheckBox {...register('trackers.elbows')} label="Elbows"/> */}
</div>
</div>
</div>
</form>
)
}

View File

@@ -1,35 +0,0 @@
import { DeviceStatusT } from "slimevr-protocol/dist/server";
import { BatteryIcon } from "./icon/BatteryIcon";
import { CircleIcon } from "./icon/CircleIcon";
import { GearIcon } from "./icon/GearIcon";
import { WifiIcon } from "./icon/WifiIcon";
export function TrackerCard({ status }: { status: DeviceStatusT }) {
return (
<div className="flex rounded-l-md rounded-r-xl bg-green-400" >
<div className="flex bg-primary-3 rounded-r-md py-3 ml-1 px-5 w-full">
<div className="flex flex-grow flex-col">
<div className="flex text-white text-ellipsis">{status.name}</div>
<div className="flex flex-row gap-4">
<div className="flex gap-2">
<div className="flex flex-col justify-around">
<WifiIcon />
</div>
<div className="flex text-gray-400 text-sm">{status.ping} ms</div>
</div>
<div className="flex gap-2">
<div className="flex flex-col justify-around">
<BatteryIcon />
</div>
<div className="flex text-gray-400 text-sm">{((status.battery / 256) * 100).toFixed(0)} %</div>
</div>
</div>
</div>
<div className="flex flex-col justify-around">
<GearIcon />
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,11 @@
import classNames from "classnames";
import React, { ReactChild } from "react";
export function BigButton({ text, icon, disabled, onClick, ...props }: { text: string, icon: ReactChild } & React.AllHTMLAttributes<HTMLButtonElement>) {
return (
<button disabled={disabled} onClick={onClick} {...props} type="button" className={classNames("flex w-full flex-col rounded-md hover:bg-primary-5 bg-primary-4 py-10 gap-5 cursor-pointer items-center", { 'bg-gray-500 hover:bg-gray-500 cursor-not-allowed': disabled}, props.className)}>
<div className="flex justify-around">{icon}</div>
<div className="flex justify-around text-2xl text-white font-bold">{text}</div>
</button>
)
}

View File

@@ -0,0 +1,23 @@
import classNames from "classnames";
import { ReactChild, useMemo } from "react";
export function Button({ children, variant, disabled, ...props }: { children: ReactChild, variant: 'primary' | 'secondary' } & React.ButtonHTMLAttributes<HTMLButtonElement>) {
const classes = useMemo(() => {
const variantsMap = {
primary: 'text-white bg-primary-5 hover:bg-primary-1 focus:ring-4 focus:outline-none focus:ring-primary-2',
secondary: 'text-white hover:bg-primary-1 focus:ring-primary-2'
}
const variantsMapDisabled = {
primary: 'bg-gray-800 hover:bg-gray-800',
secondary: 'bg-gray-800 hover:bg-gray-800'
}
return classNames(variantsMap[variant], 'focus:ring-4 rounded-lg text-sm px-5 py-2.5 text-center font-medium', disabled ? variantsMapDisabled[variant] : false);
}, [variant, disabled])
return <button type="button" {...props} className={classes} disabled={disabled}>{children}</button>
}

View File

@@ -0,0 +1,11 @@
import classNames from "classnames";
import React, { ReactChild } from "react";
export function IconButton({ icon, className, ...props }: { icon: ReactChild, className?: string } & React.HTMLAttributes<HTMLDivElement>) {
return (
<div {...props} className={classNames("px-2 rounded-full h-8 w-8 flex justify-center items-center hover:bg-gray-500 fill-gray-200", className)}>
{icon}
</div>
)
}

View File

@@ -0,0 +1,12 @@
import { forwardRef, useId } from 'react'
export const CheckBox = forwardRef<HTMLInputElement, { label: string }>(({ label, ...props }, ref) => {
const id = useId();
return (
<div className="flex items-center gap-3">
<input ref={ref} id={id} {...props} className="flex flex-col rounded-sm" type="checkbox" />
<label htmlFor={id}>{label}</label>
</div>
)
});

View File

@@ -0,0 +1,46 @@
import classNames from "classnames";
import { useMemo } from "react";
import { Control, Controller } from "react-hook-form";
import { Button } from "./Button";
export function NumberSelector({ label, valueLabelFormat, control, name, min, max, step, variant }: { label: string, valueLabelFormat?: (value: number) => string, control: Control<any>, name: string, min: number, max: number, step: number, variant: 'smol' | 'big' }) {
const variantClass = useMemo(() => {
const variantsMap = {
smol: {
container: 'flex flex-col gap-1',
label: 'flex text-sm text-white',
value: 'flex justify-center items-center w-10 text-white text-lg font-bold'
},
big: {
container: 'flex flex-row gap-5 ',
label: 'flex flex-grow text-lg font-bold text-white justify-start items-center',
value: 'flex justify-center items-center w-16 text-white text-lg font-bold'
}
};
return variantsMap[variant];
}, [variant])
return (
<Controller
control={control}
name={name}
render={({ field: { onChange, value } }) => (
<div className={classNames(variantClass.container)}>
<div className={classNames(variantClass.label)}>{label}</div>
<div className="flex gap-3">
<div className="flex">
<Button variant="primary" onClick={() => onChange(value - step)} disabled={value <= min}>-</Button>
</div>
<div className={classNames(variantClass.value)}>{valueLabelFormat ? valueLabelFormat(value) : value}</div>
<div className="flex">
<Button variant="primary" onClick={() => onChange(value + step)} disabled={value >= max}>+</Button>
</div>
</div>
</div>
)}
/>
)
}

View File

@@ -0,0 +1,14 @@
import React from "react";
export type SelectOption = { label: string, value: any };
export const Select = React.forwardRef<HTMLSelectElement, { options: SelectOption[], label?: string }>(({ label, options, ...props }, ref) => {
return (
<div className="flex flex-col gap-1">
{label && <span className="text-sm">{label}</span>}
<select {...props} ref={ref} className="w-full mt-0 rounded-md bg-primary-5 border-primary-1 shadow-sm focus:border-gray-300 focus:ring focus:ring-gray-200 focus:ring-opacity-50">
{options && options.map(({ label, value }) => <option value={value} key={value}>{label}</option>)}
</select>
</div>
)
});

View File

@@ -0,0 +1,7 @@
export function ArrowDownIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" /></svg>
)
}

View File

@@ -0,0 +1,31 @@
import classNames from "classnames";
import { useMemo } from "react";
export function BatteryIcon({ value }: { value: number }) {
const col = useMemo(() => {
const colorsMap: { [key: number]: string } = {
0.4: 'fill-misc-1',
0.2: 'fill-misc-4',
0: 'fill-misc-2',
}
const val = Object.keys(colorsMap).filter(key => +key < value).sort((a, b) => +b - +a)[0];
return colorsMap[+val] || 'fill-gray-800';
}, [value])
return (
<svg width="19" height="9" viewBox="0 0 19 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0833 0H1.31203C0.995003 0.00131561 0.691347 0.121213 0.467167 0.333594C0.242986 0.545976 0.116428 0.83365 0.115039 1.134V7.383C0.114754 7.68458 0.240506 7.97399 0.464808 8.18799C0.689109 8.40198 0.993714 8.52315 1.31203 8.525H11.0833V0Z" fill="#4C3755"/>
<path d="M15.0005 8.525C15.3175 8.52368 15.6212 8.40379 15.8454 8.19141C16.0696 7.97902 16.1961 7.69135 16.1975 7.391V5.968H17.9972V2.558H16.1975V1.134C16.1961 0.83365 16.0696 0.545976 15.8454 0.333594C15.6212 0.121213 15.3175 0.00131561 15.0005 0H10.9672V8.525H15.0005Z" fill="#4C3755"/>
<mask id="mask0_4_39" style={{ 'maskType': 'alpha', }} maskUnits="userSpaceOnUse" x="0" y="0" width="18" height="9">
<path d="M11.0833 0H1.31203C0.995003 0.00131561 0.691347 0.121213 0.467167 0.333594C0.242986 0.545976 0.116428 0.83365 0.115039 1.134V7.383C0.114754 7.68458 0.240506 7.97399 0.464808 8.18799C0.689109 8.40198 0.993714 8.52315 1.31203 8.525H11.0833V0Z" fill="#4C3755"/>
<path d="M15.0005 8.525C15.3175 8.52368 15.6212 8.40379 15.8454 8.19141C16.0696 7.97902 16.1961 7.69135 16.1975 7.391V5.968H17.9972V2.558H16.1975V1.134C16.1961 0.83365 16.0696 0.545976 15.8454 0.333594C15.6212 0.121213 15.3175 0.00131561 15.0005 0H10.9672V8.525H15.0005Z" fill="#4C3755"/>
</mask>
<g mask="url(#mask0_4_39)" className={classNames(col, 'opacity-100')}>
<rect width={value * 18} height="9"/>
</g>
</svg>
);
}

View File

@@ -0,0 +1,5 @@
export function CrossIcon() {
return ( <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd"></path></svg>)
}

View File

@@ -2,8 +2,8 @@
export function GearIcon() {
return (
<svg width="20" height="20" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.00099 11.9C7.31948 11.9003 7.63041 11.997 7.89283 12.1775C8.15524 12.3579 8.35682 12.6137 8.47099 12.911C9.11419 12.757 9.72885 12.5018 10.292 12.155C10.2026 11.9531 10.1559 11.7348 10.155 11.514C10.1546 11.2516 10.2198 10.9933 10.3448 10.7626C10.4697 10.5319 10.6505 10.3362 10.8705 10.1932C11.0905 10.0503 11.3427 9.96464 11.6043 9.94417C11.8659 9.92369 12.1284 9.96902 12.368 10.076C12.7147 9.51424 12.9699 8.90091 13.124 8.259C12.8268 8.1447 12.5711 7.9431 12.3906 7.68071C12.2102 7.41833 12.1134 7.10745 12.113 6.789C12.1134 6.47055 12.2102 6.15968 12.3906 5.89729C12.5711 5.6349 12.8268 5.4333 13.124 5.319C12.97 4.6758 12.7148 4.06115 12.368 3.498C12.1284 3.60499 11.8659 3.65031 11.6043 3.62983C11.3427 3.60936 11.0905 3.52374 10.8705 3.38078C10.6505 3.23782 10.4697 3.04207 10.3448 2.81138C10.2198 2.58068 10.1546 2.32237 10.155 2.06C10.1543 1.83899 10.201 1.62041 10.292 1.419C9.72669 1.07498 9.11135 0.820948 8.46799 0.666C8.35405 0.963474 8.15253 1.21938 7.89007 1.39989C7.6276 1.5804 7.31654 1.67703 6.99799 1.677C6.67886 1.67742 6.36711 1.581 6.10393 1.40049C5.84075 1.21998 5.63853 0.963872 5.52399 0.666C4.882 0.821335 4.26872 1.07752 3.70699 1.425C3.83599 1.71551 3.87394 2.03828 3.81586 2.35079C3.75778 2.66329 3.60638 2.95087 3.38162 3.17563C3.15686 3.40039 2.86928 3.55178 2.55678 3.60987C2.24427 3.66795 1.9215 3.62999 1.63099 3.501C1.28471 4.06442 1.02957 4.67899 0.874993 5.322C1.17184 5.43574 1.42719 5.63685 1.60733 5.89878C1.78746 6.1607 1.8839 6.47111 1.8839 6.789C1.8839 7.10689 1.78746 7.4173 1.60733 7.67922C1.42719 7.94115 1.17184 8.14226 0.874993 8.256C1.02899 8.8992 1.28417 9.51385 1.63099 10.077C1.92149 9.94858 2.24405 9.91106 2.55627 9.96935C2.86849 10.0276 3.15577 10.179 3.38036 10.4036C3.60495 10.6282 3.75634 10.9155 3.81464 11.2277C3.87294 11.5399 3.83541 11.8625 3.70699 12.153C4.26924 12.4985 4.88243 12.7533 5.52399 12.908C5.6399 12.6107 5.84296 12.3554 6.10653 12.1755C6.37011 11.9956 6.68189 11.8996 7.00099 11.9ZM4.45399 6.817C4.4536 6.31238 4.60287 5.81897 4.88294 5.3992C5.16301 4.97942 5.56128 4.65214 6.02738 4.45876C6.49348 4.26537 7.00646 4.21457 7.50144 4.31277C7.99642 4.41098 8.45115 4.65378 8.80811 5.01046C9.16508 5.36715 9.40824 5.82169 9.50683 6.31659C9.60542 6.81149 9.55502 7.32451 9.362 7.79076C9.16898 8.25701 8.84201 8.65554 8.42246 8.93594C8.00291 9.21634 7.50962 9.366 7.00499 9.366C6.67012 9.36613 6.3385 9.3003 6.02907 9.17228C5.71964 9.04425 5.43846 8.85653 5.20158 8.61983C4.96469 8.38313 4.77675 8.1021 4.64848 7.79277C4.52021 7.48344 4.45412 7.15187 4.45399 6.817Z" fill="#AF91BE"/>
<svg width="20" height="20" viewBox="0 0 14 13" xmlns="http://www.w3.org/2000/svg">
<path d="M7.00099 11.9C7.31948 11.9003 7.63041 11.997 7.89283 12.1775C8.15524 12.3579 8.35682 12.6137 8.47099 12.911C9.11419 12.757 9.72885 12.5018 10.292 12.155C10.2026 11.9531 10.1559 11.7348 10.155 11.514C10.1546 11.2516 10.2198 10.9933 10.3448 10.7626C10.4697 10.5319 10.6505 10.3362 10.8705 10.1932C11.0905 10.0503 11.3427 9.96464 11.6043 9.94417C11.8659 9.92369 12.1284 9.96902 12.368 10.076C12.7147 9.51424 12.9699 8.90091 13.124 8.259C12.8268 8.1447 12.5711 7.9431 12.3906 7.68071C12.2102 7.41833 12.1134 7.10745 12.113 6.789C12.1134 6.47055 12.2102 6.15968 12.3906 5.89729C12.5711 5.6349 12.8268 5.4333 13.124 5.319C12.97 4.6758 12.7148 4.06115 12.368 3.498C12.1284 3.60499 11.8659 3.65031 11.6043 3.62983C11.3427 3.60936 11.0905 3.52374 10.8705 3.38078C10.6505 3.23782 10.4697 3.04207 10.3448 2.81138C10.2198 2.58068 10.1546 2.32237 10.155 2.06C10.1543 1.83899 10.201 1.62041 10.292 1.419C9.72669 1.07498 9.11135 0.820948 8.46799 0.666C8.35405 0.963474 8.15253 1.21938 7.89007 1.39989C7.6276 1.5804 7.31654 1.67703 6.99799 1.677C6.67886 1.67742 6.36711 1.581 6.10393 1.40049C5.84075 1.21998 5.63853 0.963872 5.52399 0.666C4.882 0.821335 4.26872 1.07752 3.70699 1.425C3.83599 1.71551 3.87394 2.03828 3.81586 2.35079C3.75778 2.66329 3.60638 2.95087 3.38162 3.17563C3.15686 3.40039 2.86928 3.55178 2.55678 3.60987C2.24427 3.66795 1.9215 3.62999 1.63099 3.501C1.28471 4.06442 1.02957 4.67899 0.874993 5.322C1.17184 5.43574 1.42719 5.63685 1.60733 5.89878C1.78746 6.1607 1.8839 6.47111 1.8839 6.789C1.8839 7.10689 1.78746 7.4173 1.60733 7.67922C1.42719 7.94115 1.17184 8.14226 0.874993 8.256C1.02899 8.8992 1.28417 9.51385 1.63099 10.077C1.92149 9.94858 2.24405 9.91106 2.55627 9.96935C2.86849 10.0276 3.15577 10.179 3.38036 10.4036C3.60495 10.6282 3.75634 10.9155 3.81464 11.2277C3.87294 11.5399 3.83541 11.8625 3.70699 12.153C4.26924 12.4985 4.88243 12.7533 5.52399 12.908C5.6399 12.6107 5.84296 12.3554 6.10653 12.1755C6.37011 11.9956 6.68189 11.8996 7.00099 11.9ZM4.45399 6.817C4.4536 6.31238 4.60287 5.81897 4.88294 5.3992C5.16301 4.97942 5.56128 4.65214 6.02738 4.45876C6.49348 4.26537 7.00646 4.21457 7.50144 4.31277C7.99642 4.41098 8.45115 4.65378 8.80811 5.01046C9.16508 5.36715 9.40824 5.82169 9.50683 6.31659C9.60542 6.81149 9.55502 7.32451 9.362 7.79076C9.16898 8.25701 8.84201 8.65554 8.42246 8.93594C8.00291 9.21634 7.50962 9.366 7.00499 9.366C6.67012 9.36613 6.3385 9.3003 6.02907 9.17228C5.71964 9.04425 5.43846 8.85653 5.20158 8.61983C4.96469 8.38313 4.77675 8.1021 4.64848 7.79277C4.52021 7.48344 4.45412 7.15187 4.45399 6.817Z"/>
</svg>
)
}

View File

@@ -0,0 +1,34 @@
import classNames from "classnames";
import { useMemo } from "react"
export function WifiIcon({ value }: { value: number }) {
const percent = useMemo(() => value ? Math.max(Math.min((value - -95) * (100 - 0) / (-40 - -95) + 0, 100)) / 100 : 0, [value])
const y = useMemo(() => percent ? (1 - percent) * 13 : 0 , [percent])
const col = useMemo(() => {
const colorsMap: { [key: number]: string } = {
0.4: 'fill-misc-1',
0.2: 'fill-misc-4',
0: 'fill-misc-2',
}
const val = Object.keys(colorsMap).filter(key => +key < percent).sort((a, b) => +b - +a)[0];
return colorsMap[+val] || 'fill-gray-800';
}, [percent])
return (
<svg width="16" height="13" viewBox="0 0 16 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.799 12.378L15.585 2.67801C13.3492 0.95947 10.6129 0.01903 7.793 1.00136e-05C4.9725 0.0172 2.23528 0.95782 0 2.67801L7.786 12.378L7.793 12.385L7.799 12.378Z" fill="#4C3755"/>
<mask id="mask0_0_1" style={{ 'maskType': 'alpha', }} maskUnits="userSpaceOnUse" x="0" width="16" height="13" className={classNames(col, 'opacity-100')} >
<path d="M0 2.712L7.782 12.392V12.407L7.795 12.396L15.577 2.716C13.3449 0.980306 10.6044 0.026036 7.777 0C4.95656 0.021826 2.22242 0.975276 0 2.712Z"/>
</mask>
<g mask="url(#mask0_0_1)" className={classNames(col)}>
<path style={{ transform: `translateY(${y}px)`}} d="M0 2.712L7.782 12.392V12.407L7.795 12.396L15.577 2.716C13.3449 0.980306 10.6044 0.026036 7.777 0C4.95656 0.021826 2.22242 0.975276 0 2.712Z"/>
</g>
</svg>
)
}

View File

@@ -1,10 +0,0 @@
export function BatteryIcon() {
return (
<svg width="18" height="9" viewBox="0 0 18 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.39 0L1.13298 0C0.832634 0.00131561 0.54496 0.121213 0.332579 0.333594C0.120197 0.545976 0.000300407 0.83365 -0.00101566 1.134V7.383C-0.00128555 7.68458 0.117848 7.97399 0.330344 8.18799C0.54284 8.40198 0.831413 8.52315 1.13298 8.525H10.39V0Z" fill="#50E897"/>
<path d="M14.211 8.525C14.5113 8.52368 14.799 8.40379 15.0114 8.19141C15.2238 7.97902 15.3437 7.69135 15.345 7.391V5.968H17.05V2.558H15.345V1.134C15.3437 0.83365 15.2238 0.545976 15.0114 0.333594C14.799 0.121213 14.5113 0.00131561 14.211 0L10.39 0V8.525H14.211Z" fill="#4C3755"/>
</svg>
)
}

View File

@@ -1,10 +0,0 @@
export function WifiIcon() {
return (
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.799 13.378L15.585 3.67801C13.3492 1.95947 10.6129 1.01903 7.793 1.00001C4.9725 1.0172 2.23528 1.95782 0 3.67801L7.786 13.378L7.793 13.385L7.799 13.378Z" fill="#4C3755"/>
<path d="M0.00700378 3.691L7.789 13.371V13.386L7.802 13.375L15.584 3.695C13.3519 1.95931 10.6114 1.00504 7.784 0.979004C4.96356 1.00083 2.22942 1.95428 0.00700378 3.691Z" fill="#50E897"/>
</svg>
)
}

View File

@@ -0,0 +1,77 @@
import classNames from "classnames";
import { useEffect, useMemo, useState } from "react";
import { DeviceStatusT, TrackerPosition } from "slimevr-protocol/dist/server";
import { TrackerStatus } from "slimevr-protocol/dist/slimevr-protocol/server/tracker-status";
import { IconButton } from "../commons/ButtonIcon";
import { BatteryIcon } from "../commons/icon/BatteryIcon";
import { GearIcon } from "../commons/icon/GearIcon";
import { WifiIcon } from "../commons/icon/WifiIcon";
import { TrackerSettings } from "./TrackerSettings";
import { Quaternion } from '../../maths/quaternion';
export function TrackerCard({ status }: { status: DeviceStatusT }) {
const [previousRot, setPreviousRot] = useState<{ x: number, y: number, z: number, w: number }>({ x: 0, y: 0, z: 0, w: 0 })
const [velocity, setVelocity] = useState<number>(0);
const statusClass = useMemo(() => {
const statusMap: { [key: number]: string } = {
[TrackerStatus.NONE]: 'bg-cyan-800',
[TrackerStatus.BUSY]: 'bg-misc-4',
[TrackerStatus.ERROR]: 'bg-misc-2',
[TrackerStatus.DISCONNECTED]: 'bg-gray-800',
[TrackerStatus.OCCLUDED]: 'bg-misc-4',
[TrackerStatus.OK]: 'bg-misc-1'
}
return statusMap[status.status];
}, [status]);
useEffect(() => {
if (status.rotation) {
const rot = Quaternion.from(status.rotation).mult(Quaternion.from(previousRot).inverse());
const dif = Math.min(100, (rot.x**2 + rot.y**2 + rot.z**2) * 2.5)
setVelocity(dif);
setPreviousRot(status.rotation);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status])
return (
<TrackerSettings status={status} >
<div className={classNames("flex rounded-l-md rounded-r-xl", statusClass)}>
<div className="flex bg-primary-4 rounded-r-md py-3 ml-1 pr-2 pl-4 w-full gap-3">
<div className="flex flex-grow flex-col truncate gap-2">
<div className="flex text-white font-bold">{!status.computed && status.editable ? TrackerPosition[status.mountingPosition] : status.name}</div>
<div className="flex flex-row gap-4 ">
<div className="flex gap-2 flex-grow">
<div className="flex flex-col justify-around">
<WifiIcon value={status.signal} />
</div>
<div className="flex text-gray-400 text-sm w-10">{status.ping} ms</div>
</div>
<div className="flex w-1/3 gap-2">
<div className="flex flex-col justify-around">
<BatteryIcon value={status.battery / 256}/>
</div>
<div className="flex text-gray-400 text-sm">{((status.battery / 255) * 100).toFixed(0)} %</div>
</div>
<div className="flex w-1/3 gap-0.5 justify-around flex-col">
<div className="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
<div className="bg-misc-3 h-1 rounded-full" style={{width: `${velocity * 100}%`}}></div>
</div>
</div>
</div>
</div>
{status.editable &&
<div className="flex flex-col flex-shrink justify-around">
<IconButton icon={<GearIcon/>}/>
</div>
}
</div>
</div>
</TrackerSettings>
)
}

View File

@@ -0,0 +1,71 @@
import { ReactChild, useMemo, useState } from "react";
import { AssignTrackerRequestT, DeviceStatusT, InboundUnion, TrackerPosition } from "slimevr-protocol/dist/server";
import { Button } from "../commons/Button";
import { AppModal } from "../Modal";
import { Select } from "../commons/Select";
import { useWebsocketAPI } from "../../hooks/websocket-api";
import { useForm } from "react-hook-form";
export function TrackerSettings({ status, children }: { status: DeviceStatusT, children: ReactChild }) {
const { sendPacket } = useWebsocketAPI();
const { register, handleSubmit, reset } = useForm({ defaultValues: { mountingPosition: 0, mountingRotation: 0 } });
const [open, setOpen] = useState(false);
const positions = useMemo(() => Object.keys(TrackerPosition).filter((position: string) => isNaN(+position)).map((role, index) =>( { label: role, value: index })), [])
const rotations = useMemo(() => [{ label: 'FRONT', value: 180 }, { label: 'LEFT', value: 90 }, { label: 'RIGHT', value: -90 }, { label: 'BACK', value: 0 }], [])
const handleSaveSettings = ({ mountingPosition, mountingRotation }: { mountingPosition: number, mountingRotation: number }) => {
const assignreq = new AssignTrackerRequestT();
assignreq.mountingRotation = mountingRotation;
assignreq.mountingPosition = mountingPosition;
assignreq.id = status.id;
sendPacket(InboundUnion.AssignTrackerRequest, assignreq, true).then((res) => {
if (res) setOpen(false);
});
}
const openSettings = () => {
if (!status.editable)
return;
setOpen(true)
reset({
mountingRotation: status.mountingRotation,
mountingPosition: status.mountingPosition
})
}
return (
<>
<div onClick={openSettings}>
{children}
</div>
<AppModal
isOpen={open}
onRequestClose={() => setOpen(false)}
name={<>{status.mountingPosition ? `${TrackerPosition[status.mountingPosition]} Settings` : 'Tracker Settings'}</>}
>
<form onSubmit={handleSubmit(handleSaveSettings)} className="flex flex-col gap-5">
<div className="flex flex-col gap-5">
<Select {...register("mountingPosition")} label="Tracker role" options={positions}></Select>
<Select {...register("mountingRotation")} label="Mounting Rotation" options={rotations}></Select>
</div>
<div className="flex items-center justify-between">
<Button variant="primary" type="submit" >Save</Button>
<Button variant="primary" type="button" onClick={() => setOpen(false)}>Close</Button>
</div>
</form>
</AppModal>
</>
)
}

5
src/gobals.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import * as react from 'react'
declare module 'react' {
function useId(): string;
}

32
src/hooks/layout.ts Normal file
View File

@@ -0,0 +1,32 @@
import { useEffect, useRef, useState } from "react";
export function useLayout() {
const [layoutHeight, setLayoutHeigt] = useState(window.innerHeight);
const ref = useRef<HTMLDivElement>(null);
const computeLayoutHeight = (windowHeight: number) => {
if (ref.current) {
setLayoutHeigt(windowHeight - ref.current.getBoundingClientRect().top)
}
}
const onWindowResize = () => {
computeLayoutHeight(window.innerHeight)
}
useEffect(() => {
window.addEventListener('resize', onWindowResize);
computeLayoutHeight(window.innerHeight)
return () => {
window.removeEventListener('resize', onWindowResize);
}
}, [])
return {
layoutHeight,
ref
}
}

45
src/hooks/reset.ts Normal file
View File

@@ -0,0 +1,45 @@
import { useEffect, useRef, useState } from "react";
import { InboundUnion, ResetRequestT } from "slimevr-protocol/dist/server";
import { useWebsocketAPI } from "./websocket-api";
export function useReset() {
const timerid = useRef<NodeJS.Timer | null>(null);
const [reseting, setReseting] = useState(false);
const [timer, setTimer] = useState(0);
const { sendPacket } = useWebsocketAPI();
const reset = (quick: boolean) => {
const req = new ResetRequestT();
req.quick = quick;
setReseting(true);
if (!quick) {
if (timerid.current)
clearInterval(timerid.current);
timerid.current = setInterval(() => {
setTimer((timer) => {
if (timer + 1 === 3) {
if (timerid.current)
clearInterval(timerid.current);
sendPacket(InboundUnion.ResetRequest, req, false)
.then(() => {
setTimer(0);
setReseting(false)
})
}
return timer + 1;
});
}, 1000);
} else {
sendPacket(InboundUnion.ResetRequest, req, false)
setReseting(false);
}
}
return {
reset,
timer,
reseting
}
}

15
src/hooks/timeout.ts Normal file
View File

@@ -0,0 +1,15 @@
import { useEffect } from 'react';
export const useTimeout = (fn: () => void, delay: number) => {
useEffect(() => {
const id = setTimeout(fn, delay);
return () => clearTimeout(id);
});
};
export const useInterval = (fn: () => void, delay: number) => {
useEffect(() => {
const id = setInterval(fn, delay);
return () => clearInterval(id);
});
};

View File

@@ -1,97 +1,163 @@
import { createContext, MutableRefObject, useContext, useEffect, useRef, useState } from "react";
import { ApplicationType, HandshakeRequestT, InboundPacketT, InboundUnion, OutboundPacket, OutboundUnion } from 'slimevr-protocol/dist/server'
import { AcknowledgementT, ApplicationType, ConnectionRequestT, InboundPacketT, InboundUnion, OutboundPacket, OutboundPacketT, OutboundUnion } from 'slimevr-protocol/dist/server'
import { Builder, ByteBuffer } from 'flatbuffers'
import { useInterval } from "./timeout";
export interface WebSocketApi {
isConnected: boolean,
eventlistenerRef: MutableRefObject<EventTarget>,
usePacket: <T>(type: OutboundUnion, callback: (packet: T) => void) => void
sendPacket: (type: InboundUnion, data: InboundPacketType, acknowledgeMe?: boolean) => Promise<boolean>
}
export const WebSocketApiContext = createContext<WebSocketApi>(undefined as any);
export type InboundPacketType = InboundPacketT['packet'];
export type OutboundPacketType = OutboundPacketT['packet'];
export function useProvideWebsocketApi(): WebSocketApi {
const packetCounterRef = useRef<number>(0);
const toAcknoledgePacketsRef = useRef<{ [key: number]: () => void }>([]);
const webSocketRef = useRef<WebSocket | null>(null);
const eventlistenerRef = useRef<EventTarget>(new EventTarget());
const [isConnected, setConnected] = useState(false);
useInterval(() => {
if (webSocketRef.current && !isConnected) {
disconnect();
connect();
console.log('Try reconnecting');
}
}, 3000);
const onConnected = (event: Event) => {
if (!webSocketRef.current) return ;
setConnected(true);
const conn = new ConnectionRequestT();
conn.applicationType = ApplicationType.UI;
let fbb = new Builder(1);
const hand = new HandshakeRequestT();
hand.applicationType = ApplicationType.UI;
const inbound = new InboundPacketT();
inbound.acknowledgeMe = true;
inbound.packet = hand;
inbound.packetType = InboundUnion.HandshakeRequest
fbb.finish(inbound.pack(fbb));
webSocketRef.current.send(fbb.asUint8Array());
sendPacket(InboundUnion.ConnectionRequest, conn);
}
const onConnectionClose = (event: Event) => {
setConnected(false);
packetCounterRef.current = 0;
toAcknoledgePacketsRef.current = [];
}
const onMessage = async (event: { data: Blob }) => {
if (!event.data.arrayBuffer)
return ;
const buffer = await event.data.arrayBuffer();
const fbb = new ByteBuffer(new Uint8Array(buffer));
const outbountPacket = OutboundPacket.getRootAsOutboundPacket(fbb).unpack();
eventlistenerRef.current?.dispatchEvent(new CustomEvent(OutboundUnion[outbountPacket.packetType], { detail: outbountPacket }))
if (outbountPacket.acknowledgeMe && webSocketRef.current) {
const fbb = new Builder();
const acknowledgement = new AcknowledgementT();
acknowledgement.packetId = outbountPacket.packetCounter;
fbb.finish(acknowledgement.pack(fbb));
webSocketRef.current.send(fbb.asUint8Array());
}
if (outbountPacket.packetType === OutboundUnion.slimevr_protocol_misc_Acknowledgement) {
const acknowledgement = outbountPacket.packet as AcknowledgementT;
const acknoledgePromise = toAcknoledgePacketsRef.current[acknowledgement.packetId];
if (!acknoledgePromise)
return;
delete toAcknoledgePacketsRef.current[acknowledgement.packetId];
acknoledgePromise()
}
}
useEffect(() => {
const sendPacket = async (type: InboundUnion, data: InboundPacketType, acknowledgeMe = false): Promise<boolean> => {
if (!webSocketRef.current)
throw new Error('No connection');
const fbb = new Builder(1);
const inbound = new InboundPacketT();
inbound.acknowledgeMe = acknowledgeMe;
inbound.packetCounter = packetCounterRef.current;
inbound.packet = data;
inbound.packetType = type;
fbb.finish(inbound.pack(fbb));
webSocketRef.current.send(fbb.asUint8Array());
if (acknowledgeMe) {
return await new Promise((resolve, reject) => {
// TODO implement retry
const timeoutId = setTimeout(() => {
reject(false);
}, 3000)
const acknoledged = () => {
clearTimeout(timeoutId);
resolve(true);
}
toAcknoledgePacketsRef.current[inbound.packetCounter] = acknoledged;
})
}
packetCounterRef.current++;
return true;
}
const connect = () => {
webSocketRef.current = new WebSocket('ws://localhost:21110');
// Connection opened
webSocketRef.current.addEventListener('open', onConnected);
webSocketRef.current.addEventListener('close', onConnectionClose);
webSocketRef.current.addEventListener('message', onMessage);
}
const disconnect = () => {
if (!webSocketRef.current) return ;
webSocketRef.current.removeEventListener('open', onConnected);
webSocketRef.current.removeEventListener('close', onConnectionClose);
webSocketRef.current.removeEventListener('message', onMessage);
}
useEffect(() => {
connect();
return () => {
if (!webSocketRef.current) return ;
webSocketRef.current.removeEventListener('open', onConnected);
webSocketRef.current.removeEventListener('close', onConnectionClose);
webSocketRef.current.removeEventListener('message', onMessage);
disconnect();
}
}, [])
return {
isConnected,
eventlistenerRef,
usePacket: <T>(type: OutboundUnion, callback: (packet: T) => void) => {
const onEvent = (event: CustomEventInit) => {
callback(event.detail.packet)
}
useEffect(() => {
eventlistenerRef.current.addEventListener(OutboundUnion[type], onEvent)
return () => {
eventlistenerRef.current.removeEventListener(OutboundUnion[type], onEvent)
}
}, [])
}
},
sendPacket
}
}

View File

@@ -3,7 +3,7 @@
@tailwind utilities;
body {
background-color: theme('colors.primary.2');
background-color: theme('colors.primary.1');
font-variant-numeric: tabular-nums;
height: 100vh;
width: 100vw;

View File

@@ -1,17 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import * as ReactDOMClient from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Modal from 'react-modal';
ReactDOM.render(
<React.StrictMode>
Modal.setAppElement('#root');
const container = document.getElementById('root');
if (container) {
const root = ReactDOMClient.createRoot(container);
root.render(
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
);
}

39
src/maths/quaternion.ts Normal file
View File

@@ -0,0 +1,39 @@
import { QuatT } from "slimevr-protocol/dist/server";
export class Quaternion {
constructor(public x: number, public y: number, public z: number, public w: number) {}
public inverse(): Quaternion {
const norm = this.norm();
if(norm > 0.0) {
const invNorm = 1.0 / norm;
return new Quaternion(-this.x * invNorm, -this.y * invNorm, -this.z * invNorm, this.w * invNorm);
}
// return an invalid result to flag the error
return new Quaternion(0,0, 0, 1);
}
norm(): number {
return this.w * this.w + this.x * this.x + this.y * this.y + this.z * this.z;
}
public mult(q: Quaternion): Quaternion {
const res = new Quaternion(0, 0, 0, 1);
const qw = q.w, qx = q.x, qy = q.y, qz = q.z;
res.x = this.x * qw + this.y * qz - this.z * qy + this.w * qx;
res.y = -this.x * qz + this.y * qw + this.z * qx + this.w * qy;
res.z = this.x * qy - this.y * qx + this.z * qw + this.w * qz;
res.w = -this.x * qx - this.y * qy - this.z * qz + this.w * qw;
return res;
}
static from(q: { x: number, y: number, z: number, w: number }): Quaternion {
return new Quaternion(q.x, q.y, q.z, q.w)
}
}

View File

@@ -1 +1 @@
/// <reference types="react-scripts" />
/// <reference types="react-scripts" />

View File

@@ -6,12 +6,20 @@ module.exports = {
primary: {
1: '#201527',
2: '#26192E',
3: '#362640',
4: '#AF91BE',
5: '#A44FED'
3: '#2F2037',
4: '#382740',
5: '#432E4D'
},
misc: {
1: '#50E897',
2: '#FF486E',
3: '#A44FED',
4: '#D5C055'
}
}
},
},
plugins: [],
}
plugins: [
require('@tailwindcss/forms'),
],
}

View File

@@ -1,26 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"strictExportPresence": false,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}