gui: More info in tracker table and IMU visualizer (#450)

This commit is contained in:
0forks
2023-01-18 22:22:36 +03:00
committed by GitHub
parent 3374e45d62
commit 564be6112a
32 changed files with 6214 additions and 358 deletions

View File

@@ -7,6 +7,7 @@
"@fluent/react": "^0.14.1",
"@fontsource/poppins": "^4.5.8",
"@formatjs/intl-localematcher": "^0.2.32",
"@react-three/fiber": "^8.10.0",
"@tauri-apps/api": "^1.2.0",
"@vitejs/plugin-react": "^3.0.0",
"browserslist": "^4.18.1",
@@ -25,7 +26,6 @@
"postcss-normalize": "^10.0.1",
"postcss-preset-env": "^7.0.1",
"prompts": "^2.4.2",
"quaternion": "^1.4.0",
"react": "^18.0.0",
"react-dev-utils": "^12.0.0",
"react-dom": "^18.0.0",
@@ -34,6 +34,7 @@
"react-router-dom": "^6.2.2",
"semver": "^7.3.5",
"solarxr-protocol": "file:../solarxr-protocol",
"three": "^0.148.0",
"typescript": "^4.6.3"
},
"scripts": {
@@ -71,6 +72,7 @@
"@types/react": "18.0.25",
"@types/react-dom": "^18.0.5",
"@types/react-modal": "3.13.1",
"@types/three": "^0.148.0",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"autoprefixer": "^10.4.4",

View File

@@ -87,9 +87,25 @@ navbar-settings = sewtings
bvh-start_recording = wecowd bvh
bvh-recording = wecowding...
## Overlay settings
overlay-is_visible_label = show owovelay in steawmvr
overlay-is_mirrored_label = dispway owovelay as miwwow
## Widget: Overlay settings
widget-overlay = owovelay
widget-overlay-is_visible_label = show owovelay in steawmvr
widget-overlay-is_mirrored_label = dispway owovelay as miwwow
## Widget: Developer settings
widget-developer_mode = devwowwewow mode
widget-developer_mode-high_contrast = high contwast
widget-developer_mode-precise_rotation = pwecise wotation
widget-developer_mode-fast_data_feed = fast data feed
widget-developer_mode-filter_slimes_and_hmd = fiwtew swimes a-and HMD
widget-developer_mode-sort_by_name = sowt by nyame
widget-developer_mode-raw_slime_rotation = waw wotation
widget-developer_mode-more_info = mowe info
## Widget: IMU Visualizer
widget-imu_visualizer = wotation
widget-imu_visualizer-rotation_raw = waw
widget-imu_visualizer-rotation_preview = pwewiew
## Tracker status
tracker-status-none = no stawtus
@@ -104,6 +120,9 @@ tracker-table-column-name = nayme
tracker-table-column-type = type
tracker-table-column-battery = battewy
tracker-table-column-ping = pyng
tracker-table-column-tps = tps
tracker-table-column-temperature = temp. °C
tracker-table-column-linear-acceleration = accew. X/Y/Z
tracker-table-column-rotation = wotaytion x/y/z
tracker-table-column-position = pawsytion x/y/z
tracker-table-column-url = uawl

View File

@@ -87,9 +87,25 @@ navbar-settings = Settings
bvh-start_recording = Record BVH
bvh-recording = Recording...
## Overlay settings
overlay-is_visible_label = Show Overlay in SteamVR
overlay-is_mirrored_label = Display Overlay as Mirror
## Widget: Overlay settings
widget-overlay = Overlay
widget-overlay-is_visible_label = Show Overlay in SteamVR
widget-overlay-is_mirrored_label = Display Overlay as Mirror
## Widget: Developer settings
widget-developer_mode = Developer Mode
widget-developer_mode-high_contrast = High contrast
widget-developer_mode-precise_rotation = Precise rotation
widget-developer_mode-fast_data_feed = Fast data feed
widget-developer_mode-filter_slimes_and_hmd = Filter slimes and HMD
widget-developer_mode-sort_by_name = Sort by name
widget-developer_mode-raw_slime_rotation = Raw rotation
widget-developer_mode-more_info = More info
## Widget: IMU Visualizer
widget-imu_visualizer = Rotation
widget-imu_visualizer-rotation_raw = Raw
widget-imu_visualizer-rotation_preview = Preview
## Tracker status
tracker-status-none = No Status
@@ -104,6 +120,9 @@ tracker-table-column-name = Name
tracker-table-column-type = Type
tracker-table-column-battery = Battery
tracker-table-column-ping = Ping
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
tracker-table-column-rotation = Rotation X/Y/Z
tracker-table-column-position = Position X/Y/Z
tracker-table-column-url = URL

View File

@@ -87,9 +87,25 @@ navbar-settings = Ajustes
bvh-start_recording = Grabar BVH
bvh-recording = Grabando...
## Overlay settings
overlay-is_visible_label = Mostrar interfaz en SteamVR
overlay-is_mirrored_label = Mostrar interfaz reflejada
## Widget: Overlay settings
widget-overlay = Overlay
widget-overlay-is_visible_label = Mostrar interfaz en SteamVR
widget-overlay-is_mirrored_label = Mostrar interfaz reflejada
## Widget: Developer settings
widget-developer_mode = Developer Mode
widget-developer_mode-high_contrast = High contrast
widget-developer_mode-precise_rotation = Precise rotation
widget-developer_mode-fast_data_feed = Fast data feed
widget-developer_mode-filter_slimes_and_hmd = Filter slimes and HMD
widget-developer_mode-sort_by_name = Sort by name
widget-developer_mode-raw_slime_rotation = Raw rotation
widget-developer_mode-more_info = More info
## Widget: IMU Visualizer
widget-imu_visualizer = Rotation
widget-imu_visualizer-rotation_raw = Raw
widget-imu_visualizer-rotation_preview = Preview
## Tracker status
tracker-status-none = Sin estado
@@ -104,6 +120,9 @@ tracker-table-column-name = Nombre
tracker-table-column-type = Tipo
tracker-table-column-battery = Batería
tracker-table-column-ping = Latencia
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
tracker-table-column-rotation = Rotación X/Y/Z
tracker-table-column-position = Posición X/Y/Z
tracker-table-column-url = URL

View File

@@ -87,9 +87,25 @@ navbar-settings = Réglages
bvh-start_recording = Enregistrer BVH
bvh-recording = Enregistrement...
## Overlay settings
overlay-is_visible_label = Superposer le squelette dans SteamVR
overlay-is_mirrored_label = Afficher le squelette en tant que miroir
## Widget: Overlay settings
widget-overlay = Squelette
widget-overlay-is_visible_label = Superposer le squelette dans SteamVR
widget-overlay-is_mirrored_label = Afficher le squelette en tant que miroir
## Widget: Developer settings
widget-developer_mode = Developer Mode
widget-developer_mode-high_contrast = High contrast
widget-developer_mode-precise_rotation = Precise rotation
widget-developer_mode-fast_data_feed = Fast data feed
widget-developer_mode-filter_slimes_and_hmd = Filter slimes and HMD
widget-developer_mode-sort_by_name = Sort by name
widget-developer_mode-raw_slime_rotation = Raw rotation
widget-developer_mode-more_info = More info
## Widget: IMU Visualizer
widget-imu_visualizer = Rotation
widget-imu_visualizer-rotation_raw = Raw
widget-imu_visualizer-rotation_preview = Preview
## Tracker status
tracker-status-none = Pas de statut
@@ -104,6 +120,9 @@ tracker-table-column-name = Nom
tracker-table-column-type = Type
tracker-table-column-battery = Batterie
tracker-table-column-ping = Ping
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
tracker-table-column-rotation = Rotation X/Y/Z
tracker-table-column-position = Position X/Y/Z
tracker-table-column-url = URL

View File

@@ -87,9 +87,25 @@ navbar-settings = Impostazioni
bvh-start_recording = Registra BVH
bvh-recording = Registrazione in corso...
## Overlay settings
overlay-is_visible_label = Mostra Overlay in SteamVR
overlay-is_mirrored_label = Includi uno specchio nel Overlay
## Widget: Overlay settings
widget-overlay = Overlay
widget-overlay-is_visible_label = Mostra Overlay in SteamVR
widget-overlay-is_mirrored_label = Includi uno specchio nel Overlay
## Widget: Developer settings
widget-developer_mode = Developer Mode
widget-developer_mode-high_contrast = High contrast
widget-developer_mode-precise_rotation = Precise rotation
widget-developer_mode-fast_data_feed = Fast data feed
widget-developer_mode-filter_slimes_and_hmd = Filter slimes and HMD
widget-developer_mode-sort_by_name = Sort by name
widget-developer_mode-raw_slime_rotation = Raw rotation
widget-developer_mode-more_info = More info
## Widget: IMU Visualizer
widget-imu_visualizer = Rotation
widget-imu_visualizer-rotation_raw = Raw
widget-imu_visualizer-rotation_preview = Preview
## Tracker status
tracker-status-none = Nessuno Stato
@@ -104,6 +120,9 @@ tracker-table-column-name = Nome
tracker-table-column-type = Tipologia
tracker-table-column-battery = Batteria
tracker-table-column-ping = Ping
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
tracker-table-column-rotation = Rotazione X/Y/Z
tracker-table-column-position = Rotazione X/Y/Z
tracker-table-column-url = URL

View File

@@ -87,9 +87,25 @@ navbar-settings = 設定
bvh-start_recording = BVHレコーディング
bvh-recording = レコーディング中...
## Overlay settings
overlay-is_visible_label = SteamVRでオーバーレイを表示する
overlay-is_mirrored_label = オーバーレイをミラーとして表示する
## Widget: Overlay settings
widget-overlay = オーバーレイ設定
widget-overlay-is_visible_label = SteamVRでオーバーレイを表示する
widget-overlay-is_mirrored_label = オーバーレイをミラーとして表示する
## Widget: Developer settings
widget-developer_mode = 開発者モード
widget-developer_mode-high_contrast = ハイ コントラスト
widget-developer_mode-precise_rotation = 正確な回転角度を表示
widget-developer_mode-fast_data_feed = 高速表示モード
widget-developer_mode-filter_slimes_and_hmd = SlimeVRとHMDのみを表示
widget-developer_mode-sort_by_name = 表示名順
widget-developer_mode-raw_slime_rotation = 元の回転角度
widget-developer_mode-more_info = 他情報
## Widget: IMU Visualizer
widget-imu_visualizer = Rotation
widget-imu_visualizer-rotation_raw = Raw
widget-imu_visualizer-rotation_preview = Preview
## Tracker status
tracker-status-none = ステータスなし
@@ -104,6 +120,9 @@ tracker-table-column-name = Name
tracker-table-column-type = Type
tracker-table-column-battery = Battery
tracker-table-column-ping = Ping
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
tracker-table-column-rotation = Rotation X/Y/Z
tracker-table-column-position = Position X/Y/Z
tracker-table-column-url = URL

View File

@@ -87,9 +87,25 @@ navbar-settings = 설정
bvh-start_recording = BVH 기록
bvh-recording = 기록중...
## Overlay settings
overlay-is_visible_label = SteamVR에서 오버레이 표시
overlay-is_mirrored_label = 오버레이 반전
## Widget: Overlay settings
widget-overlay = 오버레이
widget-overlay-is_visible_label = SteamVR에서 오버레이 표시
widget-overlay-is_mirrored_label = 오버레이 반전
## Widget: Developer settings
widget-developer_mode = 개발자 모드
widget-developer_mode-high_contrast = High contrast
widget-developer_mode-precise_rotation = Precise rotation
widget-developer_mode-fast_data_feed = Fast data feed
widget-developer_mode-filter_slimes_and_hmd = Filter slimes and HMD
widget-developer_mode-sort_by_name = Sort by name
widget-developer_mode-raw_slime_rotation = Raw rotation
widget-developer_mode-more_info = More info
## Widget: IMU Visualizer
widget-imu_visualizer = Rotation
widget-imu_visualizer-rotation_raw = Raw
widget-imu_visualizer-rotation_preview = Preview
## Tracker status
tracker-status-none = No Status
@@ -104,6 +120,9 @@ tracker-table-column-name = 이름
tracker-table-column-type = 타입
tracker-table-column-battery = 배터리
tracker-table-column-ping = Ping
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
tracker-table-column-rotation = X/Y/Z 회전
tracker-table-column-position = X/Y/Z 위치
tracker-table-column-url = URL

View File

@@ -87,9 +87,25 @@ navbar-settings = Ustawienia
bvh-start_recording = Nagraj BVH
bvh-recording = Nagrywam...
## Overlay settings
overlay-is_visible_label = Pokaż Overlay w SteamVR
overlay-is_mirrored_label = Pokaż Overlay jako Lustro
## Widget: Overlay settings
widget-overlay = Overlay
widget-overlay-is_visible_label = Pokaż Overlay w SteamVR
widget-overlay-is_mirrored_label = Pokaż Overlay jako Lustro
## Widget: Developer settings
widget-developer_mode = Tryb Dewelopera
widget-developer_mode-high_contrast = High contrast
widget-developer_mode-precise_rotation = Precise rotation
widget-developer_mode-fast_data_feed = Fast data feed
widget-developer_mode-filter_slimes_and_hmd = Filter slimes and HMD
widget-developer_mode-sort_by_name = Sort by name
widget-developer_mode-raw_slime_rotation = Raw rotation
widget-developer_mode-more_info = More info
## Widget: IMU Visualizer
widget-imu_visualizer = Rotation
widget-imu_visualizer-rotation_raw = Raw
widget-imu_visualizer-rotation_preview = Preview
## Tracker status
tracker-status-none = Brak Statusu
@@ -104,6 +120,9 @@ tracker-table-column-name = Nazwa
tracker-table-column-type = Typ
tracker-table-column-battery = Bateria
tracker-table-column-ping = Ping
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
tracker-table-column-rotation = Rotacja X/Y/Z
tracker-table-column-position = Pozycja X/Y/Z
tracker-table-column-url = URL

View File

@@ -87,9 +87,25 @@ navbar-settings = Opções
bvh-start_recording = Gravar BVH
bvh-recording = Gravando...
## Overlay settings
overlay-is_visible_label = Mostrar Overlay na SteamVR
overlay-is_mirrored_label = Mostrar Overlay como espelho
## Widget: Overlay settings
widget-overlay = Overlay
widget-overlay-is_visible_label = Mostrar Overlay na SteamVR
widget-overlay-is_mirrored_label = Mostrar Overlay como espelho
## Widget: Developer settings
widget-developer_mode = Modo de desenvolvedor
widget-developer_mode-high_contrast = High contrast
widget-developer_mode-precise_rotation = Precise rotation
widget-developer_mode-fast_data_feed = Fast data feed
widget-developer_mode-filter_slimes_and_hmd = Filter slimes and HMD
widget-developer_mode-sort_by_name = Sort by name
widget-developer_mode-raw_slime_rotation = Raw rotation
widget-developer_mode-more_info = More info
## Widget: IMU Visualizer
widget-imu_visualizer = Rotation
widget-imu_visualizer-rotation_raw = Raw
widget-imu_visualizer-rotation_preview = Preview
## Tracker status
tracker-status-none = Sem Status
@@ -104,6 +120,9 @@ tracker-table-column-name = Nome
tracker-table-column-type = Tipo
tracker-table-column-battery = Bateria
tracker-table-column-ping = Ping
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
tracker-table-column-rotation = Rotação X/Y/Z
tracker-table-column-position = Posição X/Y/Z
tracker-table-column-url = URL

View File

@@ -87,9 +87,25 @@ navbar-settings = Cài đặt
bvh-start_recording = Ghi BVH
bvh-recording = Đang ghi...
## Overlay settings
overlay-is_visible_label = Xem overlay trên SteamVR
overlay-is_mirrored_label = Xem overlay trong gương
## Widget: Overlay settings
widget-overlay = Overlay
widget-overlay-is_visible_label = Xem overlay trên SteamVR
widget-overlay-is_mirrored_label = Xem overlay trong gương
## Widget: Developer settings
widget-developer_mode = Chế độ nhà phát triển
widget-developer_mode-high_contrast = High contrast
widget-developer_mode-precise_rotation = Precise rotation
widget-developer_mode-fast_data_feed = Fast data feed
widget-developer_mode-filter_slimes_and_hmd = Filter slimes and HMD
widget-developer_mode-sort_by_name = Sort by name
widget-developer_mode-raw_slime_rotation = Raw rotation
widget-developer_mode-more_info = More info
## Widget: IMU Visualizer
widget-imu_visualizer = Rotation
widget-imu_visualizer-rotation_raw = Raw
widget-imu_visualizer-rotation_preview = Preview
## Tracker status
tracker-status-none = Không có tình trạng
@@ -104,6 +120,9 @@ tracker-table-column-name = Tên
tracker-table-column-type = Loại
tracker-table-column-battery = Pin
tracker-table-column-ping = Ping
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
tracker-table-column-rotation = Chiều chuyển X/Y/Z
tracker-table-column-position = Vị trí X/Y/Z
tracker-table-column-url = URL

View File

@@ -87,9 +87,25 @@ navbar-settings = 设置
bvh-start_recording = 录制 BVH 文件
bvh-recording = 录制中...
## Overlay settings
overlay-is_visible_label = 在 SteamVR 中显示覆盖层
overlay-is_mirrored_label = 镜像显示覆盖层
## Widget: Overlay settings
widget-overlay = 覆盖层
widget-overlay-is_visible_label = 在 SteamVR 中显示覆盖层
widget-overlay-is_mirrored_label = 镜像显示覆盖层
## Widget: Developer settings
widget-developer_mode = 开发者模式
widget-developer_mode-high_contrast = High contrast
widget-developer_mode-precise_rotation = Precise rotation
widget-developer_mode-fast_data_feed = Fast data feed
widget-developer_mode-filter_slimes_and_hmd = Filter slimes and HMD
widget-developer_mode-sort_by_name = Sort by name
widget-developer_mode-raw_slime_rotation = Raw rotation
widget-developer_mode-more_info = More info
## Widget: IMU Visualizer
widget-imu_visualizer = Rotation
widget-imu_visualizer-rotation_raw = Raw
widget-imu_visualizer-rotation_preview = Preview
## Tracker status
tracker-status-none = 无状态
@@ -104,6 +120,9 @@ tracker-table-column-name = 名字
tracker-table-column-type = 类型
tracker-table-column-battery = 电量
tracker-table-column-ping = 延迟
tracker-table-column-tps = TPS
tracker-table-column-temperature = Temp. °C
tracker-table-column-linear-acceleration = Accel. X/Y/Z
tracker-table-column-rotation = 旋转 X/Y/Z
tracker-table-column-position = 位置 X/Y/Z
tracker-table-column-url = 地址

View File

@@ -1,11 +1,13 @@
import classNames from 'classnames';
import { ReactNode } from 'react';
import { ResetType } from 'solarxr-protocol';
import { useConfig } from '../hooks/config';
import { useLayout } from '../hooks/layout';
import { BVHButton } from './BVHButton';
import { ResetButton } from './home/ResetButton';
import { Navbar } from './Navbar';
import { TopBar } from './TopBar';
import { DeveloperModeWidget } from './widgets/DeveloperModeWidget';
import { OverlayWidget } from './widgets/OverlayWidget';
export function MainLayoutRoute({
@@ -19,6 +21,7 @@ export function MainLayoutRoute({
}) {
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
const { layoutWidth, ref: refw } = useLayout<HTMLDivElement>();
const { config } = useConfig();
return (
<>
@@ -59,6 +62,11 @@ export function MainLayoutRoute({
<div className="w-full">
<OverlayWidget></OverlayWidget>
</div>
{config?.debug && (
<div className="w-full">
<DeveloperModeWidget></DeveloperModeWidget>
</div>
)}
</div>
)}
</div>

View File

@@ -0,0 +1,17 @@
export function WarningIcon(props: any) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
{...props}
className={`w-7 h-7 ${props.className || ''}`}
>
<path
fillRule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clipRule="evenodd"
/>
</svg>
);
}

View File

@@ -1,11 +1,10 @@
import Quaternion from 'quaternion';
import { useMemo, useState } from 'react';
import { AssignTrackerRequestT, BodyPart, RpcMessage } from 'solarxr-protocol';
import { FlatDeviceTracker } from '../../../../hooks/app';
import { useOnboarding } from '../../../../hooks/onboarding';
import { useTrackers } from '../../../../hooks/tracker';
import { useWebsocketAPI } from '../../../../hooks/websocket-api';
import { QuaternionToQuatT } from '../../../../maths/quaternion';
import { MountingOrientationDegreesToQuatT } from '../../../../maths/quaternion';
import { ArrowLink } from '../../../commons/ArrowLink';
import { Button } from '../../../commons/Button';
import { TipBox } from '../../../commons/TipBox';
@@ -41,13 +40,13 @@ export function ManualMountingPage() {
[assignedTrackers]
);
const onDirectionSelected = (mountingOrientation: number) => {
const onDirectionSelected = (mountingOrientationDegrees: number) => {
(trackerPartGrouped[selectedRole] || []).forEach((td) => {
const assignreq = new AssignTrackerRequestT();
assignreq.bodyPosition = td.tracker.info?.bodyPart || BodyPart.NONE;
assignreq.mountingOrientation = QuaternionToQuatT(
Quaternion.fromEuler(0, +mountingOrientation, 0)
assignreq.mountingOrientation = MountingOrientationDegreesToQuatT(
mountingOrientationDegrees
);
assignreq.trackerId = td.tracker.trackerId;
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);

View File

@@ -3,21 +3,28 @@ import { Typography } from '../commons/Typography';
export function TrackerBattery({
value,
voltage,
disabled,
textColor = 'secondary',
}: {
value: number;
voltage?: number | null;
disabled?: boolean;
textColor?: string;
}) {
const percent = value * 100;
return (
<div className="flex gap-2">
<div className="flex flex-col justify-around">
<BatteryIcon value={value} disabled={disabled} />
<BatteryIcon value={percent} disabled={disabled} />
</div>
{!disabled && (
<div className="w-10">
<Typography color="secondary">
{(value * 100).toFixed(0)} %
</Typography>
<Typography color={textColor}>{percent.toFixed(0)} %</Typography>
{voltage && (
<Typography color={textColor}>{voltage.toFixed(2)} V</Typography>
)}
</div>
)}
{disabled && (

View File

@@ -44,10 +44,11 @@ function TrackerBig({
/>
)}
<div className="flex gap-2">
{device.hardwareStatus.rssi && device.hardwareStatus.ping && (
{(device.hardwareStatus.rssi != null ||
device.hardwareStatus.ping != null) && (
<TrackerWifi
rssi={device.hardwareStatus.rssi}
ping={device.hardwareStatus.ping}
rssi={device.hardwareStatus.rssi || 0}
ping={device.hardwareStatus.ping || 0}
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
></TrackerWifi>
)}
@@ -90,10 +91,11 @@ function TrackerSmol({
)}
</div>
<div className="flex flex-col justify-center items-center">
{device.hardwareStatus.rssi && device.hardwareStatus.ping && (
{(device.hardwareStatus.rssi != null ||
device.hardwareStatus.ping != null) && (
<TrackerWifi
rssi={device.hardwareStatus.rssi}
ping={device.hardwareStatus.ping}
rssi={device.hardwareStatus.rssi || 0}
ping={device.hardwareStatus.ping || 0}
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
></TrackerWifi>
)}

View File

@@ -1,5 +1,4 @@
import { IPv4 } from 'ip-num/IPNumber';
import Quaternion from 'quaternion';
import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useParams } from 'react-router-dom';
@@ -7,8 +6,10 @@ import { AssignTrackerRequestT, BodyPart, RpcMessage } from 'solarxr-protocol';
import { useDebouncedEffect } from '../../hooks/timeout';
import { useTrackerFromId } from '../../hooks/tracker';
import { useWebsocketAPI } from '../../hooks/websocket-api';
import { DEG_TO_RAD, RAD_TO_DEG } from '../../maths/angle';
import { FixEuler, GetYaw, QuaternionToQuatT } from '../../maths/quaternion';
import {
getYawInDegrees,
MountingOrientationDegreesToQuatT,
} from '../../maths/quaternion';
import { ArrowLink } from '../commons/ArrowLink';
import { Button } from '../commons/Button';
import { FootIcon } from '../commons/icon/FootIcon';
@@ -19,6 +20,10 @@ import { SingleTrackerBodyAssignmentMenu } from './SingleTrackerBodyAssignmentMe
import { TrackerCard } from './TrackerCard';
import { CheckBox } from '../commons/Checkbox';
import { useLocalization } from '@fluent/react';
import { IMUVisualizerWidget } from '../widgets/IMUVisualizerWidget';
import { useConfig } from '../../hooks/config';
import { WarningIcon } from '../commons/icon/WarningIcon';
import classNames from 'classnames';
export const rotationToQuatMap = {
FRONT: 180,
@@ -36,6 +41,7 @@ const rotationsLabels = {
export function TrackerSettingsPage() {
const { l10n } = useLocalization();
const { config } = useConfig();
const { sendRPCPacket } = useWebsocketAPI();
const [firstLoad, setFirstLoad] = useState(false);
@@ -59,18 +65,13 @@ export function TrackerSettingsPage() {
const tracker = useTrackerFromId(trackernum, deviceid);
const onDirectionSelected = (mountingOrientation: number) => {
const onDirectionSelected = (mountingOrientationDegrees: number) => {
if (!tracker) return;
const assignreq = new AssignTrackerRequestT();
assignreq.mountingOrientation = QuaternionToQuatT(
Quaternion.fromEuler(
0,
0,
FixEuler(+mountingOrientation) * DEG_TO_RAD,
'XZY'
)
assignreq.mountingOrientation = MountingOrientationDegreesToQuatT(
mountingOrientationDegrees
);
assignreq.bodyPosition = tracker?.tracker.info?.bodyPart || BodyPart.NONE;
assignreq.trackerId = tracker?.tracker.trackerId;
@@ -92,9 +93,9 @@ export function TrackerSettingsPage() {
setSelectBodypart(false);
};
const currRotation = useMemo(() => {
const currRotationDegrees = useMemo(() => {
return tracker?.tracker.info?.mountingOrientation
? FixEuler(GetYaw(tracker.tracker.info?.mountingOrientation) * RAD_TO_DEG)
? getYawInDegrees(tracker?.tracker.info?.mountingOrientation)
: rotationToQuatMap.FRONT;
}, [tracker?.tracker.info?.mountingOrientation]);
@@ -108,10 +109,9 @@ export function TrackerSettingsPage() {
return;
const assignreq = new AssignTrackerRequestT();
assignreq.bodyPosition = tracker?.tracker.info?.bodyPart || BodyPart.NONE;
assignreq.mountingOrientation = assignreq.mountingOrientation =
QuaternionToQuatT(
Quaternion.fromEuler(0, 0, FixEuler(+currRotation) * DEG_TO_RAD, 'XZY')
);
assignreq.mountingOrientation =
MountingOrientationDegreesToQuatT(currRotationDegrees);
assignreq.displayName = trackerName;
assignreq.trackerId = tracker?.tracker.trackerId;
assignreq.allowDriftCompensation = allowDriftCompensation;
@@ -222,6 +222,11 @@ export function TrackerSettingsPage() {
</Typography>
</div>
</div>
{tracker?.tracker && config?.debug && (
<IMUVisualizerWidget
tracker={tracker?.tracker}
></IMUVisualizerWidget>
)}
</div>
<div className="flex flex-col flex-grow bg-background-70 rounded-lg p-5 gap-3">
<ArrowLink to="/">
@@ -241,8 +246,18 @@ export function TrackerSettingsPage() {
</Typography>
<div className="flex justify-between bg-background-80 w-full p-3 rounded-lg">
<div className="flex gap-3 items-center">
<FootIcon></FootIcon>
<Typography>
{tracker?.tracker.info?.bodyPart !== BodyPart.NONE && (
<FootIcon></FootIcon>
)}
{tracker?.tracker.info?.bodyPart === BodyPart.NONE && (
<WarningIcon className="text-yellow-300" />
)}
<Typography
color={classNames({
'text-yellow-300':
tracker?.tracker.info?.bodyPart === BodyPart.NONE,
})}
>
{l10n.getString(
'body_part-' +
BodyPart[tracker?.tracker.info?.bodyPart || BodyPart.NONE]
@@ -270,7 +285,7 @@ export function TrackerSettingsPage() {
<div className="flex gap-3 items-center">
<FootIcon></FootIcon>
<Typography>
{l10n.getString(rotationsLabels[currRotation])}
{l10n.getString(rotationsLabels[currRotationDegrees])}
</Typography>
</div>
<div className="flex">

View File

@@ -4,24 +4,31 @@ import { Typography } from '../commons/Typography';
export function TrackerWifi({
rssi,
ping,
rssiShowNumeric,
disabled,
textColor = 'secondary',
}: {
rssi: number;
ping: number;
rssiShowNumeric?: boolean;
disabled?: boolean;
textColor?: string;
}) {
return (
<div className="flex gap-2">
<div className="flex flex-col justify-around ">
<div className="flex gap-2 whitespace-nowrap">
<div className="flex flex-col justify-around">
<WifiIcon value={rssi} disabled={disabled} />
</div>
{!disabled && (
<div className="w-10">
<Typography color="secondary">{ping} ms</Typography>
<div className="w-12">
<Typography color={textColor}>{ping} ms</Typography>
{rssiShowNumeric && (
<Typography color={textColor}>{rssi} dBm</Typography>
)}
</div>
)}
{disabled && (
<div className="flex flex-col justify-center w-10">
<div className="flex flex-col justify-center w-12">
<div className="w-7 h-1 bg-background-30 rounded-full"></div>
</div>
)}

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { IPv4 } from 'ip-num/IPNumber';
import { MouseEventHandler, ReactNode, useState } from 'react';
import { MouseEventHandler, ReactNode, useState, useMemo } from 'react';
import {
TrackerDataT,
TrackerIdT,
@@ -14,8 +14,45 @@ import { TrackerBattery } from './TrackerBattery';
import { TrackerStatus } from './TrackerStatus';
import { TrackerWifi } from './TrackerWifi';
import { useLocalization } from '@fluent/react';
import { formatVector3 } from '../utils/formatting';
import { useConfig } from '../../hooks/config';
export function TrackerNameCol({ tracker }: { tracker: TrackerDataT }) {
enum DisplayColumn {
NAME,
TYPE,
BATTERY,
PING,
TPS,
ROTATION,
TEMPERATURE,
LINEAR_ACCELERATION,
POSITION,
URL,
}
const displayColumns: { [k: string]: boolean } = {
[DisplayColumn.NAME]: true,
[DisplayColumn.TYPE]: true,
[DisplayColumn.BATTERY]: true,
[DisplayColumn.PING]: true,
[DisplayColumn.TPS]: true,
[DisplayColumn.ROTATION]: true,
[DisplayColumn.TEMPERATURE]: true,
[DisplayColumn.LINEAR_ACCELERATION]: true,
[DisplayColumn.POSITION]: true,
[DisplayColumn.URL]: true,
};
const isSlime = ({ device }: FlatDeviceTracker) =>
device?.hardwareInfo?.manufacturer === 'SlimeVR';
const getDeviceName = ({ device }: FlatDeviceTracker) =>
device?.customName?.toString() || '';
const getTrackerName = ({ tracker }: FlatDeviceTracker) =>
tracker?.info?.customName?.toString() || '';
export function TrackerNameCell({ tracker }: { tracker: TrackerDataT }) {
const { useName } = useTracker(tracker);
const name = useName();
@@ -33,17 +70,28 @@ export function TrackerNameCol({ tracker }: { tracker: TrackerDataT }) {
);
}
export function TrackerRotCol({ tracker }: { tracker: TrackerDataT }) {
const { useRotationDegrees } = useTracker(tracker);
export function TrackerRotCell({
tracker,
precise,
color,
referenceAdjusted,
}: {
tracker: TrackerDataT;
precise?: boolean;
color?: string;
referenceAdjusted?: boolean;
}) {
const { useRawRotationEulerDegrees, useRefAdjRotationEulerDegrees } =
useTracker(tracker);
const rot = useRotationDegrees();
const rot = referenceAdjusted
? useRefAdjRotationEulerDegrees()
: useRawRotationEulerDegrees();
return (
<Typography color="secondary">
<Typography color={color}>
<span className="whitespace-nowrap">
{`${rot.pitch.toFixed(0)} / ${rot.yaw.toFixed(0)} / ${rot.roll.toFixed(
0
)}`}
{formatVector3(rot, precise ? 2 : 0)}
</span>
</Typography>
);
@@ -90,7 +138,7 @@ export function RowContainer({
'min-h-[50px] flex flex-col justify-center px-3',
rounded === 'left' && 'rounded-l-lg',
rounded === 'right' && 'rounded-r-lg',
hover ? 'bg-background-50' : 'bg-background-60'
hover ? 'bg-background-50 cursor-pointer' : 'bg-background-60'
)}
>
{children}
@@ -108,168 +156,218 @@ export function TrackersTable({
}) {
const { l10n } = useLocalization();
const [hoverTracker, setHoverTracker] = useState<TrackerIdT | null>(null);
const { config } = useConfig();
const trackerEqual = (id: TrackerIdT | null) =>
id?.trackerNum == hoverTracker?.trackerNum &&
(!id?.deviceId || id.deviceId.id == hoverTracker?.deviceId?.id);
const filteringEnabled =
config?.debug && config?.devSettings?.filterSlimesAndHMD;
const sortingEnabled = config?.debug && config?.devSettings?.sortByName;
// TODO: fix memo
const filteredSortedTrackers = useMemo(() => {
const list = filteringEnabled
? flatTrackers.filter((t) => getDeviceName(t) === 'HMD' || isSlime(t))
: flatTrackers;
if (sortingEnabled) {
list.sort((a, b) => getTrackerName(a).localeCompare(getTrackerName(b)));
}
return list;
}, [flatTrackers, filteringEnabled, sortingEnabled]);
const fontColor = config?.devSettings?.highContrast ? 'primary' : 'secondary';
const moreInfo = config?.devSettings?.moreInfo;
const hasTemperature = !!filteredSortedTrackers.find(
({ tracker }) => Number(tracker?.temp?.temp) != 0
);
displayColumns[DisplayColumn.TEMPERATURE] = hasTemperature || false;
displayColumns[DisplayColumn.POSITION] = moreInfo || false;
displayColumns[DisplayColumn.LINEAR_ACCELERATION] = moreInfo || false;
displayColumns[DisplayColumn.URL] = moreInfo || false;
const displayColumnsKeys = Object.keys(displayColumns).filter(
(k) => displayColumns[k]
);
const firstColumnId = +displayColumnsKeys[0];
const lastColumnId = +displayColumnsKeys[displayColumnsKeys.length - 1];
function column({
id,
label,
labelClassName,
row,
}: {
id: DisplayColumn;
label: string;
labelClassName?: string;
row: (data: FlatDeviceTracker) => ReactNode | null;
}) {
let rounded: 'left' | 'right' | 'none' = 'none';
if (firstColumnId === id) rounded = 'left';
else if (lastColumnId === id) rounded = 'right';
if (!displayColumns[id]) return <></>;
return (
<div
className={classNames('flex flex-col gap-1', {
'flex-grow': lastColumnId === id,
})}
>
<div className={`flex px-3 whitespace-nowrap ${labelClassName}`}>
{label}
</div>
{filteredSortedTrackers.map((data, index) => (
<RowContainer
rounded={rounded}
key={index}
tracker={data.tracker}
onClick={() => clickedTracker(data.tracker)}
hover={trackerEqual(data.tracker.trackerId)}
onMouseOver={() => setHoverTracker(data.tracker.trackerId)}
onMouseOut={() => setHoverTracker(null)}
>
{row(data) || <></>}
</RowContainer>
))}
</div>
);
}
return (
<div className="flex w-full overflow-x-auto py-2">
<div className="flex flex-col gap-1">
<div className="flex px-3">
{l10n.getString('tracker-table-column-name')}
</div>
{flatTrackers.map(({ tracker }, index) => (
<RowContainer
key={index}
rounded="left"
tracker={tracker}
onClick={() => clickedTracker(tracker)}
hover={trackerEqual(tracker.trackerId)}
onMouseOver={() => setHoverTracker(tracker.trackerId)}
onMouseOut={() => setHoverTracker(null)}
>
<TrackerNameCol tracker={tracker}></TrackerNameCol>
</RowContainer>
))}
</div>
<div className="flex flex-col gap-1">
<div className="flex px-3">
{l10n.getString('tracker-table-column-type')}
</div>
{flatTrackers.map(({ device, tracker }, index) => (
<RowContainer
key={index}
tracker={tracker}
onClick={() => clickedTracker(tracker)}
hover={trackerEqual(tracker.trackerId)}
onMouseOver={() => setHoverTracker(tracker.trackerId)}
onMouseOut={() => setHoverTracker(null)}
>
<Typography color="secondary">
{device?.hardwareInfo?.manufacturer || '--'}
</Typography>
</RowContainer>
))}
</div>
<div className="flex flex-col gap-1">
<div className="flex px-3">
{l10n.getString('tracker-table-column-battery')}
</div>
{flatTrackers.map(({ device, tracker }, index) => (
<RowContainer
key={index}
tracker={tracker}
onClick={() => clickedTracker(tracker)}
hover={trackerEqual(tracker.trackerId)}
onMouseOver={() => setHoverTracker(tracker.trackerId)}
onMouseOut={() => setHoverTracker(null)}
>
{(device &&
device.hardwareStatus &&
device.hardwareStatus.batteryPctEstimate && (
<TrackerBattery
value={device.hardwareStatus.batteryPctEstimate / 100}
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
/>
)) || <></>}
</RowContainer>
))}
</div>
<div className="flex flex-col gap-1">
<div className="flex px-3">
{l10n.getString('tracker-table-column-ping')}
</div>
{flatTrackers.map(({ device, tracker }, index) => (
<RowContainer
key={index}
tracker={tracker}
onClick={() => clickedTracker(tracker)}
hover={trackerEqual(tracker.trackerId)}
onMouseOver={() => setHoverTracker(tracker.trackerId)}
onMouseOut={() => setHoverTracker(null)}
>
{(device &&
device.hardwareStatus &&
device.hardwareStatus.rssi &&
device.hardwareStatus.ping && (
<TrackerWifi
rssi={device.hardwareStatus.rssi}
ping={device.hardwareStatus.ping}
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
></TrackerWifi>
)) || <></>}
</RowContainer>
))}
</div>
<div className="flex flex-col gap-1">
<div className="flex px-3 whitespace-nowrap">
{l10n.getString('tracker-table-column-rotation')}
</div>
{flatTrackers.map(({ tracker }, index) => (
<RowContainer
key={index}
tracker={tracker}
onClick={() => clickedTracker(tracker)}
hover={trackerEqual(tracker.trackerId)}
onMouseOver={() => setHoverTracker(tracker.trackerId)}
onMouseOut={() => setHoverTracker(null)}
>
<TrackerRotCol tracker={tracker} />
</RowContainer>
))}
</div>
<div className="flex flex-col gap-1">
<div className="flex px-3 whitespace-nowrap">
{l10n.getString('tracker-table-column-position')}
</div>
{flatTrackers.map(({ tracker }, index) => (
<RowContainer
key={index}
tracker={tracker}
onClick={() => clickedTracker(tracker)}
hover={trackerEqual(tracker.trackerId)}
onMouseOver={() => setHoverTracker(tracker.trackerId)}
onMouseOut={() => setHoverTracker(null)}
>
{(tracker.position && (
<Typography color="secondary">
<span className="whitespace-nowrap">
{`${tracker.position?.x.toFixed(
0
)} / ${tracker.position?.y.toFixed(
0
)} / ${tracker.position?.z.toFixed(0)}`}
</span>
</Typography>
)) || <></>}
</RowContainer>
))}
</div>
<div className="flex flex-col gap-1 flex-grow">
<div className="flex px-3">
{l10n.getString('tracker-table-column-url')}
</div>
{column({
id: DisplayColumn.NAME,
label: l10n.getString('tracker-table-column-name'),
row: ({ tracker }) => (
<TrackerNameCell tracker={tracker}></TrackerNameCell>
),
})}
{flatTrackers.map(({ device, tracker }, index) => (
<RowContainer
key={index}
rounded="right"
{column({
id: DisplayColumn.TYPE,
label: l10n.getString('tracker-table-column-type'),
row: ({ device }) => (
<Typography color={fontColor}>
{device?.hardwareInfo?.manufacturer || '--'}
</Typography>
),
})}
{column({
id: DisplayColumn.BATTERY,
label: l10n.getString('tracker-table-column-battery'),
row: ({ device, tracker }) =>
device?.hardwareStatus?.batteryPctEstimate && (
<TrackerBattery
value={device.hardwareStatus.batteryPctEstimate / 100}
voltage={device.hardwareStatus?.batteryVoltage}
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
textColor={fontColor}
/>
),
})}
{column({
id: DisplayColumn.PING,
label: l10n.getString('tracker-table-column-ping'),
row: ({ device, tracker }) =>
(device?.hardwareStatus?.rssi != null ||
device?.hardwareStatus?.ping != null) && (
<TrackerWifi
rssi={device?.hardwareStatus?.rssi || 0}
rssiShowNumeric
ping={device?.hardwareStatus?.ping || 0}
disabled={tracker.status === TrackerStatusEnum.DISCONNECTED}
textColor={fontColor}
></TrackerWifi>
),
})}
{column({
id: DisplayColumn.TPS,
label: l10n.getString('tracker-table-column-tps'),
row: ({ device }) => (
<Typography color={fontColor}>
{(device?.hardwareStatus?.tps != null && (
<>{device.hardwareStatus.tps || 0}</>
)) || <></>}
</Typography>
),
})}
{column({
id: DisplayColumn.ROTATION,
label: l10n.getString('tracker-table-column-rotation'),
labelClassName: classNames({
'w-44': config?.devSettings?.preciseRotation,
'w-32': !config?.devSettings?.preciseRotation,
}),
row: ({ tracker }) => (
<TrackerRotCell
tracker={tracker}
onClick={() => clickedTracker(tracker)}
hover={trackerEqual(tracker.trackerId)}
onMouseOver={() => setHoverTracker(tracker.trackerId)}
onMouseOut={() => setHoverTracker(null)}
>
<Typography color="secondary">
udp://
{IPv4.fromNumber(
device?.hardwareInfo?.ipAddress?.addr || 0
).toString()}
precise={config?.devSettings?.preciseRotation}
referenceAdjusted={!config?.devSettings?.rawSlimeRotation}
color={fontColor}
/>
),
})}
{column({
id: DisplayColumn.TEMPERATURE,
label: l10n.getString('tracker-table-column-temperature'),
row: ({ tracker }) =>
tracker.temp?.temp != 0 && (
<Typography color={fontColor}>
<span className="whitespace-nowrap">
{`${tracker.temp?.temp.toFixed(2)}`}
</span>
</Typography>
</RowContainer>
))}
</div>
),
})}
{column({
id: DisplayColumn.LINEAR_ACCELERATION,
label: l10n.getString('tracker-table-column-linear-acceleration'),
labelClassName: 'w-36',
row: ({ tracker }) =>
tracker.linearAcceleration && (
<Typography color={fontColor}>
<span className="whitespace-nowrap">
{formatVector3(tracker.linearAcceleration, 1)}
</span>
</Typography>
),
})}
{column({
id: DisplayColumn.POSITION,
label: l10n.getString('tracker-table-column-position'),
labelClassName: 'w-36',
row: ({ tracker }) =>
tracker.position && (
<Typography color={fontColor}>
<span className="whitespace-nowrap">
{formatVector3(tracker.position, 1)}
</span>
</Typography>
),
})}
{column({
id: DisplayColumn.URL,
label: l10n.getString('tracker-table-column-url'),
row: ({ device }) => (
<Typography color={fontColor}>
udp://
{IPv4.fromNumber(
device?.hardwareInfo?.ipAddress?.addr || 0
).toString()}
</Typography>
),
})}
</div>
);
}

View File

@@ -2,3 +2,7 @@ import { BodyPart } from 'solarxr-protocol';
export const bodypartToString = (id: BodyPart) =>
BodyPart[id].replace(/_/g, ' ');
type Vector3 = { x: number; y: number; z: number };
export const formatVector3 = ({ x, y, z }: Vector3, precision = 0) =>
`${x.toFixed(precision)} / ${y.toFixed(precision)} / ${z.toFixed(precision)}`;

View File

@@ -0,0 +1,83 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useConfig } from '../../hooks/config';
import { useWebsocketAPI } from '../../hooks/websocket-api';
import { CheckBox } from '../commons/Checkbox';
import { useLocalization } from '@fluent/react';
import { Typography } from '../commons/Typography';
export interface DeveloperModeWidgetForm {
highContrast: boolean;
preciseRotation: boolean;
fastDataFeed: boolean;
filterSlimesAndHMD: boolean;
sortByName: boolean;
rawSlimeRotation: boolean;
moreInfo: boolean;
}
export function DeveloperModeWidget() {
const { l10n } = useLocalization();
const { config, setConfig } = useConfig();
const { reconnect } = useWebsocketAPI();
const { reset, control, handleSubmit, watch } =
useForm<DeveloperModeWidgetForm>({
defaultValues: {
highContrast: false,
preciseRotation: false,
fastDataFeed: false,
filterSlimesAndHMD: false,
sortByName: false,
rawSlimeRotation: false,
moreInfo: false,
},
});
useEffect(() => {
reset(config?.devSettings || {});
}, []);
useEffect(() => {
const subscription = watch(() => handleSubmit(onSubmit)());
return () => subscription.unsubscribe();
}, []);
const onSubmit = async (formData: DeveloperModeWidgetForm) => {
const needReconnect =
config?.devSettings?.fastDataFeed !== formData.fastDataFeed;
await setConfig({ devSettings: formData });
if (needReconnect) reconnect();
};
const makeToggle = ([name, label]: string[], index: number) => (
<CheckBox
key={index}
control={control}
variant="toggle"
name={name}
label={l10n.getString(`widget-developer_mode-${label}`)}
></CheckBox>
);
const toggles = {
highContrast: 'high_contrast',
preciseRotation: 'precise_rotation',
fastDataFeed: 'fast_data_feed',
filterSlimesAndHMD: 'filter_slimes_and_hmd',
sortByName: 'sort_by_name',
rawSlimeRotation: 'raw_slime_rotation',
moreInfo: 'more_info',
};
return (
<form className="bg-background-60 flex flex-col w-full rounded-md px-2">
<div className="mt-2 px-1">
<Typography color="secondary">
{l10n.getString('widget-developer_mode')}
</Typography>
</div>
{Object.entries(toggles).map(makeToggle)}
</form>
);
}

View File

@@ -0,0 +1,169 @@
import { useMemo, useState } from 'react';
import { TrackerDataT } from 'solarxr-protocol';
import { useTracker } from '../../hooks/tracker';
import { Typography } from '../commons/Typography';
import { formatVector3 } from '../utils/formatting';
import { Canvas, useThree } from '@react-three/fiber';
import * as THREE from 'three';
import { CanvasTexture, PerspectiveCamera } from 'three';
import { Button } from '../commons/Button';
import { QuatObject } from '../../maths/quaternion';
import { useLocalization } from '@fluent/react';
const groundColor = '#4444aa';
const R = '#f01662';
const G = '#92ff1a';
const B = '#00b0ff';
const scale = 1.4;
const width = 2 * scale;
const height = 1 * scale;
const depth = 3 * scale;
const defaultFaces = ['Right', 'Left', 'Top', 'Bottom', 'Back', 'Front'];
const faceParams = [
{ scaleX: depth, scaleY: height, color: R }, // left right
{ scaleX: width, scaleY: depth, color: G }, // top bottom
{ scaleX: width, scaleY: height, color: B }, // front back
];
type FaceTypeProps = {
index: number;
scaleX: number;
scaleY: number;
resolution?: number;
font?: string;
opacity?: number;
color?: string;
hoverColor?: string;
textColor?: string;
strokeColor?: string;
faces?: string[];
};
const FaceMaterial = ({
index,
scaleX = 1,
scaleY = 1,
resolution = 128,
font = 'bold 46px monospace',
faces = defaultFaces,
color = '#f0f0f0',
textColor = 'black',
strokeColor = 'black',
opacity = 1,
}: FaceTypeProps) => {
const gl = useThree((state) => state.gl);
const texture = useMemo(() => {
const canvas = document.createElement('canvas');
canvas.width = resolution * scaleX;
canvas.height = resolution * scaleY;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const context = canvas.getContext('2d')!;
context.fillStyle = color;
context.fillRect(0, 0, canvas.width, canvas.height);
context.strokeStyle = strokeColor;
context.lineWidth = 2;
context.strokeRect(0, 0, canvas.width, canvas.height);
context.font = font;
context.textAlign = 'center';
context.fillStyle = textColor;
context.fillText(
faces[index].toUpperCase(),
canvas.width / 2,
canvas.height / 2 + 20
);
return new CanvasTexture(canvas);
}, [index, faces, font, color, textColor, strokeColor]);
return (
<meshLambertMaterial
map={texture}
map-encoding={gl.outputEncoding}
map-anisotropy={gl.capabilities.getMaxAnisotropy() || 1}
attach={`material-${index}`}
color={'white'}
transparent
opacity={opacity}
/>
);
};
const FaceCube = (props: Partial<FaceTypeProps>) => (
<mesh>
{[...Array(6)].map((_, i) => (
<FaceMaterial key={i} {...props} index={i} {...faceParams[(i / 2) | 0]} />
))}
<boxGeometry args={[width, height, depth]} />
</mesh>
);
function SceneRenderer({ x, y, z, w }: QuatObject) {
return (
<Canvas
className="container"
style={{ height: 200, background: 'transparent' }}
onCreated={({ camera }) => {
(camera as PerspectiveCamera).fov = 60;
}}
>
<ambientLight intensity={0.5} />
<spotLight position={[20, 20, 20]} angle={0.09} penumbra={1} />
<group quaternion={new THREE.Quaternion(x, y, z, w)}>
<FaceCube />
<axesHelper args={[10]} />
</group>
<mesh position={[0, -3, 0]} rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[50, 50, 10, 10]} />
<meshBasicMaterial
wireframe
color={groundColor}
transparent
opacity={0.2}
side={THREE.DoubleSide}
/>
</mesh>
</Canvas>
);
}
export function IMUVisualizerWidget({ tracker }: { tracker: TrackerDataT }) {
const { l10n } = useLocalization();
const [enabled, setEnabled] = useState(false);
const quat = tracker?.rotationIdentityAdjusted || new THREE.Quaternion();
const { useRawRotationEulerDegrees, useIdentAdjRotationEulerDegrees } =
useTracker(tracker);
return (
<div className="bg-background-70 flex flex-col p-3 rounded-lg gap-2">
<Typography variant="section-title">
{l10n.getString('widget-imu_visualizer')}
</Typography>
<div className="flex justify-between">
<Typography color="secondary">
{l10n.getString('widget-imu_visualizer-rotation_raw')}
</Typography>
<Typography>
{formatVector3(useRawRotationEulerDegrees(), 2)}
</Typography>
</div>
<div className="flex justify-between">
<Typography color="secondary">
{l10n.getString('widget-imu_visualizer-rotation_preview')}
</Typography>
<Typography>
{formatVector3(useIdentAdjRotationEulerDegrees(), 2)}
</Typography>
</div>
{!enabled && (
<Button variant="secondary" onClick={() => setEnabled(true)}>
{l10n.getString('widget-imu_visualizer-rotation_preview')}
</Button>
)}
{enabled && <SceneRenderer {...quat}></SceneRenderer>}
</div>
);
}

View File

@@ -8,6 +8,7 @@ import {
} from '../../hooks/pubSub';
import { CheckBox } from '../commons/Checkbox';
import { useLocalization } from '@fluent/react';
import { Typography } from '../commons/Typography';
export function OverlayWidget() {
const { l10n } = useLocalization();
@@ -64,17 +65,22 @@ export function OverlayWidget() {
return (
<form className="bg-background-60 flex flex-col w-full rounded-md px-2">
<div className="mt-2 px-1">
<Typography color="secondary">
{l10n.getString('widget-overlay')}
</Typography>
</div>
<CheckBox
control={control}
name="isVisible"
variant="toggle"
label={l10n.getString('overlay-is_visible_label')}
label={l10n.getString('widget-overlay-is_visible_label')}
></CheckBox>
<CheckBox
control={control}
name="isMirrored"
variant="toggle"
label={l10n.getString('overlay-is_mirrored_label')}
label={l10n.getString('widget-overlay-is_mirrored_label')}
></CheckBox>
</form>
);

View File

@@ -10,16 +10,14 @@ import {
} from 'react';
import { useNavigate } from 'react-router-dom';
import {
DataFeedConfigT,
DataFeedMessage,
DataFeedUpdateT,
DeviceDataMaskT,
DeviceDataT,
StartDataFeedT,
TrackerDataMaskT,
TrackerDataT,
} from 'solarxr-protocol';
import { useConfig } from './config';
import { useDataFeedConfig } from './datafeed-config';
import { useWebsocketAPI } from './websocket-api';
export interface FlatDeviceTracker {
@@ -52,6 +50,7 @@ export function useProvideAppContext(): AppContext {
const { sendDataFeedPacket, useDataFeedPacket, isConnected } =
useWebsocketAPI();
const { config } = useConfig();
const { dataFeedConfig } = useDataFeedConfig();
const navigate = useNavigate();
const [state, dispatch] = useReducer<Reducer<AppState, AppStateAction>>(
reducer,
@@ -62,24 +61,8 @@ export function useProvideAppContext(): AppContext {
useEffect(() => {
if (isConnected) {
const trackerData = new TrackerDataMaskT();
trackerData.position = true;
trackerData.rotation = true;
trackerData.info = true;
trackerData.status = true;
trackerData.temp = true;
const dataMask = new DeviceDataMaskT();
dataMask.deviceData = true;
dataMask.trackerData = trackerData;
const config = new DataFeedConfigT();
config.dataMask = dataMask;
config.minimumTimeSinceLast = 100;
config.syntheticTrackersMask = trackerData;
const startDataFeed = new StartDataFeedT();
startDataFeed.dataFeeds = [config];
startDataFeed.dataFeeds = [dataFeedConfig];
sendDataFeedPacket(DataFeedMessage.StartDataFeed, startDataFeed);
}
}, [isConnected]);

View File

@@ -7,6 +7,7 @@ import {
} from '@tauri-apps/api/fs';
import { createContext, useContext, useRef, useState } from 'react';
import { DeveloperModeWidgetForm } from '../components/widgets/DeveloperModeWidget';
export interface WindowConfig {
width: number;
@@ -20,6 +21,7 @@ export interface Config {
lang: string;
doneOnboarding: boolean;
watchNewDevices: boolean;
devSettings: DeveloperModeWidgetForm;
}
export interface ConfigContext {

View File

@@ -0,0 +1,37 @@
import {
DataFeedConfigT,
DeviceDataMaskT,
TrackerDataMaskT,
} from 'solarxr-protocol';
import { useConfig } from './config';
export function useDataFeedConfig() {
const { config } = useConfig();
const fastDataFeed = config?.debug && config?.devSettings?.fastDataFeed;
const feedMaxTps = fastDataFeed ? 40 : 10;
const trackerData = new TrackerDataMaskT();
trackerData.position = true;
trackerData.rotation = true;
trackerData.info = true;
trackerData.status = true;
trackerData.temp = true;
trackerData.linearAcceleration = true;
trackerData.rotationReferenceAdjusted = true;
trackerData.rotationIdentityAdjusted = true;
const dataMask = new DeviceDataMaskT();
dataMask.deviceData = true;
dataMask.trackerData = trackerData;
const dataFeedConfig = new DataFeedConfigT();
dataFeedConfig.dataMask = dataMask;
dataFeedConfig.minimumTimeSinceLast = 1000 / feedMaxTps;
dataFeedConfig.syntheticTrackersMask = trackerData;
return {
dataFeedConfig,
feedMaxTps,
};
}

View File

@@ -1,9 +1,14 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { BodyPart, TrackerDataT, TrackerStatus } from 'solarxr-protocol';
import { RAD_TO_DEG } from '../maths/angle';
import { QuaternionFromQuatT } from '../maths/quaternion';
import {
QuaternionFromQuatT,
QuaternionToEulerDegrees,
} from '../maths/quaternion';
import { useAppContext } from './app';
import { useLocalization } from '@fluent/react';
import { useDataFeedConfig } from './datafeed-config';
import { Quaternion, Vector3 } from 'three';
import { Vector3FromVec3fT } from '../maths/vector3';
export function useTrackers() {
const { trackers } = useAppContext();
@@ -39,13 +44,7 @@ export function useTrackers() {
export function useTracker(tracker: TrackerDataT) {
const { l10n } = useLocalization();
const computeRot = (rot: { x: number; y: number; z: number; w: number }) =>
QuaternionFromQuatT({
x: rot.x || 0,
y: rot.y || 0,
z: rot.z || 0,
w: rot.w || 1,
}).toEuler();
const { feedMaxTps } = useDataFeedConfig();
return {
useName: () =>
@@ -57,54 +56,61 @@ export function useTracker(tracker: TrackerDataT) {
);
return tracker.info?.displayName || 'NONE';
}, [tracker.info]),
useRotation: () =>
useRawRotationEulerDegrees: () =>
useMemo(
() => computeRot(tracker.rotation || { x: 0, y: 0, z: 0, w: 1 }),
() => QuaternionToEulerDegrees(tracker?.rotation),
[tracker.rotation]
),
useRotationDegrees: () =>
useMemo(() => {
const { yaw, pitch, roll } = computeRot(
tracker.rotation || { x: 0, y: 0, z: 0, w: 1 }
);
return {
yaw: yaw * RAD_TO_DEG,
pitch: pitch * RAD_TO_DEG,
roll: roll * RAD_TO_DEG,
};
}, [tracker.rotation]),
useRefAdjRotationEulerDegrees: () =>
useMemo(
() => QuaternionToEulerDegrees(tracker?.rotationReferenceAdjusted),
[tracker.rotationReferenceAdjusted]
),
useIdentAdjRotationEulerDegrees: () =>
useMemo(
() => QuaternionToEulerDegrees(tracker?.rotationIdentityAdjusted),
[tracker.rotationIdentityAdjusted]
),
useVelocity: () => {
const previousRot = useRef<{
x: number;
y: number;
z: number;
w: number;
}>(tracker.rotation || { x: 0, y: 0, z: 0, w: 1 });
const previousRot = useRef<Quaternion>(
QuaternionFromQuatT(tracker.rotation)
);
const previousAcc = useRef<Vector3>(
Vector3FromVec3fT(tracker.linearAcceleration)
);
const [velocity, setVelocity] = useState<number>(0);
const [rots, setRotation] = useState<number[]>([]);
const [deltas] = useState<number[]>([]);
useEffect(() => {
if (tracker.rotation) {
const rot = QuaternionFromQuatT(tracker.rotation).mul(
QuaternionFromQuatT(previousRot.current).inverse()
const rot = QuaternionFromQuatT(tracker.rotation).multiply(
previousRot.current.clone().invert()
);
const dif = Math.min(1, (rot.x ** 2 + rot.y ** 2 + rot.z ** 2) * 2.5);
// Use sum of rotation of last 3 frames (0.3sec) for smoother movement and better detection of slow movement.
if (rots.length === 3) {
rots.shift();
const acc = Vector3FromVec3fT(tracker.linearAcceleration).sub(
previousAcc.current
);
const dif = Math.min(
1,
(rot.x ** 2 + rot.y ** 2 + rot.z ** 2) * 2.5 +
(acc.x ** 2 + acc.y ** 2 + acc.z ** 2) / 1000
);
// Use sum of the rotation and acceleration delta vector lengths over 0.3sec
// for smoother movement and better detection of slow movement.
if (deltas.length >= 0.3 * feedMaxTps) {
deltas.shift();
}
rots.push(dif);
setRotation(rots);
deltas.push(dif);
setVelocity(
Math.min(
1,
Math.max(
0,
rots.reduce((a, b) => a + b)
deltas.reduce((a, b) => a + b)
)
)
);
previousRot.current = tracker.rotation;
previousRot.current = QuaternionFromQuatT(tracker.rotation);
previousAcc.current = Vector3FromVec3fT(tracker.linearAcceleration);
}
}, [tracker.rotation]);

View File

@@ -17,6 +17,7 @@ import { useInterval } from './timeout';
export interface WebSocketApi {
isConnected: boolean;
isFirstConnection: boolean;
reconnect: () => void;
useRPCPacket: <T>(type: RpcMessage, callback: (packet: T) => void) => void;
useDataFeedPacket: <T>(
type: DataFeedMessage,
@@ -51,9 +52,8 @@ export function useProvideWebsocketApi(): WebSocketApi {
useInterval(() => {
if (webSocketRef.current && !isConnected) {
disconnect();
connect();
console.log('Try reconnecting');
console.log('Attempting to reconnect');
reconnect();
}
}, 3000);
@@ -178,6 +178,11 @@ export function useProvideWebsocketApi(): WebSocketApi {
}
};
const reconnect = () => {
disconnect();
connect();
};
useEffect(() => {
connect();
return () => {
@@ -188,6 +193,7 @@ export function useProvideWebsocketApi(): WebSocketApi {
return {
isConnected,
isFirstConnection,
reconnect,
useDataFeedPacket: <T>(
type: DataFeedMessage,
callback: (packet: T) => void

View File

@@ -1,23 +1,15 @@
import Quaternion from 'quaternion';
import { Euler, Quaternion } from 'three';
import { QuatT } from 'solarxr-protocol';
import { DEG_TO_RAD } from './angle';
export function QuaternionFromQuatT(q: {
x: number;
y: number;
z: number;
w: number;
}) {
return new Quaternion(q.w, q.x, q.y, q.z);
export type QuatObject = { x: number; y: number; z: number; w: number };
export function QuaternionFromQuatT(q?: QuatObject | null) {
return q ? new Quaternion(q.x, q.y, q.z, q.w) : new Quaternion();
}
export function QuaternionToQuatT(q: {
x: number;
y: number;
z: number;
w: number;
}) {
export function QuaternionToQuatT(q: QuatObject) {
const quat = new QuatT();
quat.x = q.x;
quat.y = q.y;
quat.z = q.z;
@@ -25,39 +17,37 @@ export function QuaternionToQuatT(q: {
return quat;
}
export function FixEuler(yaw: number) {
if (yaw > 180) {
yaw *= -1;
yaw += 180;
}
return Math.round(yaw);
export function MountingOrientationDegreesToQuatT(
mountingOrientationDegrees: number
) {
return QuaternionToQuatT(
new Quaternion().setFromEuler(
new Euler(0, +mountingOrientationDegrees * DEG_TO_RAD, 0)
)
)
}
export function GetYaw(q: { x: number; y: number; z: number; w: number }) {
const squareX = q.x * q.x,
squareY = q.y * q.y,
squareZ = q.z * q.z,
squareW = q.w * q.w;
const RAD_TO_DEG = 180 / Math.PI;
// This value is 1 if the quaternion is a unit (normalized) quaternion,
// otherwise this will be a factor to compensate for the singularity pole checks
const correctionFactor = squareX + squareY + squareZ + squareW;
// This value is to test for a singularity, it will be 0.5 at the north singularity,
// -0.5 at the south singularity, and anything else at any other value
const singularityTest = q.x * q.y + q.z * q.w;
export function getYawInDegrees(q?: QuatObject) {
if (!q) return 0;
// Singularity cutoff points are 0.499 (86.3 degrees) to compensate for error
if (singularityTest > 0.499 * correctionFactor) {
// Handle the singularity at the attitude of 90 degrees (north pole)
return 2 * Math.atan2(q.x, q.w);
} else if (singularityTest < -0.499 * correctionFactor) {
// Handle the singularity at the attitude of -90 degrees (south pole)
return -2 * Math.atan2(q.x, q.w);
}
// X Y Z Result
// back: 0 0 0 0
// front: -180 0.. -180 180
// left: 0 90 0 90
// right: 0 -90 0 -90
// Otherwise calculate the yaw normally
return Math.atan2(
2 * q.y * q.w - 2 * q.x * q.z,
squareX - squareY - squareZ + squareW
);
const angles = new Euler().setFromQuaternion(QuaternionFromQuatT(q));
return angles.y | 0
? Math.round(angles.y * RAD_TO_DEG)
: Math.round(-angles.z * RAD_TO_DEG);
}
export function QuaternionToEulerDegrees(q?: QuatObject | null) {
const angles = { x: 0, y: 0, z: 0 };
if (!q) return angles;
const a = new Euler().setFromQuaternion(new Quaternion(q.x, q.y, q.z, q.w));
return { x: a.x * RAD_TO_DEG, y: a.y * RAD_TO_DEG, z: a.z * RAD_TO_DEG };
}

16
gui/src/maths/vector3.ts Normal file
View File

@@ -0,0 +1,16 @@
import { Vec3fT } from 'solarxr-protocol';
import { Vector3 } from 'three';
export type Vector3Object = { x: number; y: number; z: number; };
export function Vector3FromVec3fT(vec?: Vector3Object | null) {
return vec ? new Vector3(vec.x, vec.y, vec.z) : new Vector3();
}
export function Vector3ToVec3fT(q: Vector3Object) {
const vec = new Vec3fT();
vec.x = q.x;
vec.y = q.y;
vec.z = q.z;
return vec;
}

5216
package-lock.json generated

File diff suppressed because it is too large Load Diff