Compare commits

...

86 Commits

Author SHA1 Message Date
Louka
472f6d8fc0 add accessories wip 2023-07-29 02:19:08 -04:00
Erimel
d89a4c2704 Send any computed tracker via VMC for props (#719)
Co-authored-by: Uriel <urielfontan2002@gmail.com>
2023-07-27 15:43:34 +03:00
Erimel
0ad237a219 Improve defaults SkeletonValues & add ratios in gui (#743) 2023-07-27 15:42:28 +03:00
Uriel
9c980f06f9 Make manual proportion list grow if there is space (#784)
Co-authored-by: lucas lelievre <loucass003@gmail.com>
2023-07-27 15:41:47 +03:00
Collin
f607693d83 Fix toe snap (#793) 2023-07-25 02:53:46 -04:00
Butterscotch!
27faa1908c Fix Autobone comment on the wrong line (#791) 2023-07-23 13:19:33 -04:00
Uriel
39cd1f9ba9 Autobone config migration is in wrong version (#792) 2023-07-22 03:49:27 -04:00
Erimel
c59f20a79a make sentences consistents (#789) 2023-07-20 16:30:58 -04:00
Erimel
e7de25dfab Clarify "extended" skeleton toggles in GUI (#779) 2023-07-20 15:39:24 -04:00
Erimel
74f1d8ed61 Drift compensation tracker and HMD requirement (#750) 2023-07-20 15:32:52 -04:00
Erimel
ad41c46092 Bundle & Receive OSCTrackers (#773) 2023-07-20 15:25:14 -04:00
Erimel
f3346bbeee Make displayName consistent via hash (#775) 2023-07-20 15:14:52 -04:00
Erimel
ca6f82492d Refresh headShift when setting trackers (#781) 2023-07-20 15:10:08 -04:00
Uriel
02acc6ede1 Improvements on the Autobone GUI (#776)
Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com>
2023-07-20 11:19:43 +03:00
Uriel
d9c631fcf6 Fix CI release bundling (#777) 2023-07-20 11:15:55 +03:00
Uriel
ea9df2c31f Add accessibility settings (#774) 2023-07-20 11:14:51 +03:00
Collin
e18bd2d382 Better tap detection (#778) 2023-07-20 11:06:48 +03:00
Uriel
b6a681b1bb Save config and logs on appdata folder (#770) 2023-07-20 11:02:52 +03:00
Uriel
793dd374f8 Update appstream metadata (#762) 2023-07-20 11:00:52 +03:00
Uriel
97b617bb24 Add me to GUI codeowners (#782) 2023-07-20 10:59:47 +03:00
lucas lelievre
1aa32b8264 Fix Scroll on mobile (#785) 2023-07-20 10:59:23 +03:00
Uriel
5c2c6749c3 Move SteamVR code to desktop subproject (#755) 2023-07-19 16:32:00 -04:00
Uriel
e472b12e83 Add rust file logging (#752) 2023-07-19 16:11:48 -04:00
MarcoM
75fc1c37d3 fix: filter non imu in connected tracker (#763) 2023-07-17 17:54:16 -04:00
Erimel
9c4b9b401e Don't create new skeletons (#761) 2023-07-15 23:37:14 -04:00
Erimel
4243951214 Prioritize non-computed head tracker (#751)
Co-authored-by: Uriel <urielfontan2002@gmail.com>
2023-07-15 23:07:39 -04:00
Collin
2f61d5b4b8 Mocap mode (#749)
Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com>
Co-authored-by: Erimel <marioluigivideo@gmail.com>
Co-authored-by: Uriel <urielfontan2002@gmail.com>
2023-07-14 22:45:18 +03:00
Uriel
35e6b8b721 Manual mounting overhaul (#745) 2023-07-14 22:30:26 +03:00
Eiren Rain
82db6a1ff5 Separate the server in subprojects (#727) 2023-07-14 20:22:49 +02:00
Uriel
17be65d2a2 Merge branch 'main' into subprojects 2023-07-10 15:21:02 -03:00
Erimel
db2f7fbd49 Don't log non-imu trackers' drift (#759) 2023-07-09 20:36:12 -04:00
Uriel
95f1bfd52f Merge branch 'main' into subprojects 2023-07-09 21:30:10 -03:00
Erimel
7511e0098e Fix chest tracker SteamVR battery (#772)
Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com>
2023-07-09 20:17:46 -04:00
Uriel
f0f2731387 second fix 2023-07-07 21:47:16 -03:00
Uriel
5f3182e2c6 fix merge 2023-07-07 21:43:53 -03:00
Uriel
8b4a2843a1 Merge branch 'main' into subprojects 2023-07-07 19:54:50 -03:00
Uriel
efd20ee7b2 Merge branch 'main' into subprojects 2023-07-03 23:10:19 -03:00
ImUrX
c076ebabb3 Merge branch 'main' into subprojects 2023-07-02 17:06:53 -03:00
Uriel
9e650dad08 Merge branch 'main' into subprojects 2023-07-01 23:05:57 -07:00
Uriel
dc22b503e8 Merge branch 'main' into subprojects 2023-06-29 19:04:07 -03:00
Uriel
0bd6a9002a Merge branch 'main' into subprojects 2023-06-26 20:26:28 -03:00
Uriel
9c7558cae8 it works then 2023-06-26 23:07:10 +00:00
ImUrX
d89a53ef44 Merge branch 'main' into subprojects 2023-06-26 17:51:22 -03:00
ImUrX
525f29f3c5 add copy script 2023-06-26 17:50:11 -03:00
Butterscotch!
c94d71c24f Disable gradle config cache 2023-06-24 21:50:37 -04:00
Butterscotch!
31ff3f4868 Re-revert broken dependency update 2023-06-24 21:25:14 -04:00
Butterscotch!
a6911e072c Fix workflow targeting wrong build directory 2023-06-24 20:43:26 -04:00
Butterscotch!
bf062c9b65 Merge remote-tracking branch 'upstream/main' into pr/727 2023-06-24 20:12:21 -04:00
ImUrX
8945e05354 just delete it then lol 2023-06-16 16:15:12 -03:00
ImUrX
ea3cdb7658 pls work? 2023-06-16 16:07:45 -03:00
ImUrX
ae40121a31 pls work 2023-06-16 15:01:17 -03:00
ImUrX
f64a45fb2e fix unresolved reference from merge 2023-06-16 14:50:25 -03:00
ImUrX
cb19aa17cc forgot server folder 2023-06-16 14:48:56 -03:00
ImUrX
4564671b38 Merge branch 'main' into subprojects 2023-06-16 14:45:26 -03:00
ImUrX
60f74d6d5c fix tests 2023-06-16 14:44:14 -03:00
ImUrX
4450260dd0 make softlink valid 2023-06-15 18:02:19 -03:00
ImUrX
55f030a145 spotless 2023-06-15 17:52:09 -03:00
ImUrX
3038de8a5f Merge branch 'main' into subprojects 2023-06-15 17:39:25 -03:00
ImUrX
338e153834 keep fixing gradle script 2023-06-15 16:33:07 -03:00
ImUrX
544efb6efe fetch tags in gh actions 2023-06-15 15:59:43 -03:00
ImUrX
0e64f1241f Merge branch 'main' into subprojects 2023-06-15 15:38:23 -03:00
ImUrX
ffe530dc94 move bridges to desktop 2023-06-15 15:37:21 -03:00
ImUrX
a8ce510f70 fix soft link 2023-06-15 01:16:32 -03:00
ImUrX
1b17fcbec3 android 2023-06-15 01:16:32 -03:00
ImUrX
01f1d2ee56 im having so much fun 2023-06-15 01:16:31 -03:00
ImUrX
d14a7bb5e7 it runs 2023-06-15 01:16:30 -03:00
ImUrX
a14a2ea253 im ruining everything 2023-06-15 01:16:27 -03:00
Butterscotch!
b67de108e7 Bump versions 2023-06-13 20:30:44 -04:00
Butterscotch!
e4a4f38c15 Disable cache on WebView 2023-06-13 20:30:44 -04:00
Butterscotch!
858354eee8 Revert broken dependency update 2023-06-13 20:30:44 -04:00
Butterscotch!
fcb736d371 Fix gradle build somewhat 2023-06-13 20:30:44 -04:00
Butterscotch!
f6d8026761 Don't cache & try more to allow zooming on GUI 2023-06-13 20:30:44 -04:00
Butterscotch!
cb5e27875c Update Gradle stuff & fix compilation + proguard 2023-06-13 20:30:44 -04:00
Butterscotch!
72f506822a Update Gradle stuff 2023-06-13 20:30:43 -04:00
Butterscotch!
4e942fded5 Revert "Work around TransformNode race condition"
This reverts commit 23a2abbbdcc9a070c6fe907ec56bc2d10a607b27.
2023-06-13 20:30:43 -04:00
Butterscotch!
463e558e7f Enable WebView debugging 2023-06-13 20:30:43 -04:00
Butterscotch!
a1db52144a Work around TransformNode race condition 2023-06-13 20:30:43 -04:00
Butterscotch!
81b7ea0967 Fix screen rotation crashing 2023-06-13 20:30:43 -04:00
Butterscotch!
a489e32828 Fix Gradle build 2023-06-13 20:30:43 -04:00
Butterscotch!
6eaf04ba64 Update version code 2023-06-13 20:30:42 -04:00
Butterscotch!
b8ceaa6bc0 Fix OSC Android fuckery 2023-06-13 20:30:42 -04:00
Butterscotch!
6b7c47d36c Add GUI webview & remove titlebar 2023-06-13 20:30:42 -04:00
Butterscotch!
c0f8fb1758 Add locally hosted web GUI 2023-06-13 20:30:42 -04:00
Butterscotch!
07600f0133 Set version number and add icons 2023-06-13 20:30:42 -04:00
Butterscotch!
3634bedef1 Reduce minimum SDK version to 26 2023-06-13 20:30:42 -04:00
Butterscotch!
e864487246 Add basic support for Android
- Add Android build scripts
- Add Android GUI and `MainActivity`
- Fix Java records
- Fix `Path.of` errors
- Update Main and MainActivity to match better
2023-06-13 20:30:42 -04:00
350 changed files with 7488 additions and 4488 deletions

6
.github/CODEOWNERS vendored
View File

@@ -1,9 +1,9 @@
# Global code owner
* @Eirenliel
# Make Loucas the owner of all GUI stuff
/gui/ @loucass003
/package-lock.json @loucass003
# Make Loucas and Uriel the owners of all GUI stuff
/gui/ @loucass003 @ImUrX
/package-lock.json @loucass003 @ImUrX
# Uriel and Erimel responsible for i18n
/gui/public/i18n/ @ImUrX @Louka3000

View File

@@ -22,6 +22,8 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Get tags
run: git fetch --tags origin --recurse-submodules=no
- name: Set up JDK 17
uses: actions/setup-java@v3
@@ -32,6 +34,9 @@ jobs:
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- run: mkdir ./gui/dist && touch ./gui/dist/somefile
shell: bash
- name: Check code formatting
run: ./gradlew spotlessCheck
@@ -46,6 +51,8 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Get tags
run: git fetch --tags origin --recurse-submodules=no
- name: Set up JDK 17
uses: actions/setup-java@v3
@@ -65,7 +72,7 @@ jobs:
# Artifact name
name: "SlimeVR-Server" # optional, default is artifact
# A file, directory or wildcard pattern that describes what to upload
path: server/build/libs/*
path: server/desktop/build/libs/*
- name: Upload to draft release
uses: softprops/action-gh-release@v1
@@ -74,7 +81,7 @@ jobs:
draft: true
generate_release_notes: true
files: |
server/build/libs/*
server/desktop/build/libs/*
bundle-linux:
@@ -89,7 +96,7 @@ jobs:
- uses: actions/download-artifact@v3
with:
name: "SlimeVR-Server"
path: server/build/libs/
path: server/desktop/build/libs/
- name: Set up Linux dependencies
uses: awalsh128/cache-apt-pkgs-action@latest
@@ -134,7 +141,7 @@ jobs:
cd target/release/bundle/appimage
chmod a+x slimevr*.AppImage
./slimevr*.AppImage --appimage-extract
cp $( git rev-parse --show-toplevel )/server/build/libs/slimevr.jar squashfs-root/slimevr.jar
cp $( git rev-parse --show-toplevel )/server/desktop/build/libs/slimevr.jar squashfs-root/slimevr.jar
chmod 644 squashfs-root/slimevr.jar
appimagetool squashfs-root slimevr*.AppImage
@@ -172,7 +179,7 @@ jobs:
- uses: actions/download-artifact@v3
with:
name: "SlimeVR-Server"
path: server/build/libs/
path: server/desktop/build/libs/
- name: Cache cargo dependencies
uses: Swatinem/rust-cache@v2
@@ -193,7 +200,7 @@ jobs:
- name: Modify Application
run: |
cd target/release/bundle/macos/slimevr.app/Contents/MacOS
cp $( git rev-parse --show-toplevel )/server/build/libs/slimevr.jar ./
cp $( git rev-parse --show-toplevel )/server/desktop/build/libs/slimevr.jar ./
cd ../../../../dmg/
./bundle_dmg.sh --volname slimevr --icon slimevr 180 170 --app-drop-link 480 170 \
--window-size 660 400 --hide-extension ../macos/slimevr.app \
@@ -237,7 +244,7 @@ jobs:
- uses: actions/download-artifact@v3
with:
name: "SlimeVR-Server"
path: server/build/libs/
path: server/desktop/build/libs/
- name: Cache cargo dependencies
uses: Swatinem/rust-cache@v2
@@ -260,8 +267,8 @@ jobs:
run: |
mkdir SlimeVR
cp gui/src-tauri/icons/icon.ico ./SlimeVR/run.ico
cp server/build/libs/slimevr.jar ./SlimeVR/slimevr.jar
cp server/resources/* ./SlimeVR/
cp server/desktop/build/libs/slimevr.jar ./SlimeVR/slimevr.jar
cp server/core/resources/* ./SlimeVR/
cp target/release/slimevr.exe ./SlimeVR/
7z a -tzip SlimeVR-win64.zip ./SlimeVR/

3
.gitignore vendored
View File

@@ -36,3 +36,6 @@ build/
# direnv has been claimed for Nix usage
.direnv/
# Ignore Android local properties
local.properties

413
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,3 +11,7 @@ edition = "2021"
license = "MIT OR Apache-2.0"
rust-version = "1.65" # This version stabilized GATs and let-else
repository = "https://github.com/SlimeVR/SlimeVR-Server"
[profile.release]
lto = "thin"
strip = "debuginfo" # Only affects Unix binaries with DWARF

View File

@@ -1,26 +1,3 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlinx:atomicfu-gradle-plugin:0.20.2")
}
}
plugins {
kotlin("jvm") version "1.8.21"
}
subprojects {
plugins.apply("kotlinx-atomicfu")
}
dependencies {
implementation(kotlin("stdlib-jdk8"))
}
repositories {
mavenCentral()
}
kotlin {
jvmToolchain(17)
id("org.ajoberstar.grgit") version "5.2.0"
}

View File

@@ -30,7 +30,7 @@ work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
<url type="contribute">https://github.com/SlimeVR/SlimeVR-Server/blob/main/CONTRIBUTING.md</url>
<url type="contact">https://discord.gg/SlimeVR</url>
<recommends>
<display_length compare="ge">880</display_length>
<display_length compare="ge">300</display_length>
</recommends>
<supports>
<control>pointing</control>
@@ -44,8 +44,7 @@ work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
<description>
<p>
<code>SlimeVR</code>
is a set of open hardware sensors and open source software that facilitates full-body
SlimeVR is a set of open hardware sensors and open source software that facilitates full-body
tracking (FBT) in virtual reality. With no base station required, SlimeVR makes wireless
VR FBT affordable and comfortable.
</p>
@@ -60,6 +59,17 @@ work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
</screenshot>
</screenshots>
<releases>
<release version="0.8.3" date="2023-07-09"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.3</url></release>
<release version="0.8.2" date="2023-07-09"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.2</url></release>
<release version="0.8.2-rc.1" type="development" date="2023-07-07"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.2-rc.1</url></release>
<release version="0.8.1" date="2023-07-04"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.1</url></release>
<release version="0.8.0" date="2023-06-22"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.0</url></release>
<release version="0.8.0-rc.3" type="development" date="2023-06-20"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.0-rc.3</url></release>
<release version="0.8.0-rc.2" type="development" date="2023-06-15"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.0-rc.2</url></release>
<release version="0.8.0-rc.1" type="development" date="2023-06-01"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.8.0-rc.1</url></release>
<release version="0.7.1" date="2023-04-14"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.7.1</url></release>
<release version="0.7.0" date="2023-04-11"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.7.0</url></release>
<release version="0.6.3" date="2023-02-22"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.6.3</url></release>
<release version="0.6.2" date="2023-02-17"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.6.2</url></release>
<release version="0.6.1" date="2023-02-12"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.6.1</url></release>
<release version="0.6.0" date="2023-01-05"><url>https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.6.0</url></release>

View File

@@ -8,3 +8,12 @@ org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAME
kotlin.code.style=official
# https://github.com/Kotlin/kotlinx-atomicfu#atomicfu-compiler-plugin
kotlinx.atomicfu.enableJvmIrTransformation=true
android.useAndroidX=true
android.nonTransitiveRClass=true
org.gradle.unsafe.configuration-cache=false
kotlinVersion=1.9.0-RC
spotlessVersion=6.12.0
shadowJarVersion=8.1.1
buildconfigVersion=3.1.0

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

7
gradlew vendored
View File

@@ -85,6 +85,9 @@ done
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -194,10 +197,6 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in

5
gui/.gitignore vendored
View File

@@ -25,5 +25,6 @@ yarn-error.log*
*.log
/dist
# vite
/dist
/stats.html

View File

@@ -29,10 +29,11 @@
"react-modal": "3.15.1",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.2.2",
"semver": "^7.5.0",
"semver": "^7.5.3",
"solarxr-protocol": "file:../solarxr-protocol",
"three": "^0.148.0",
"typescript": "^4.6.3"
"ts-pattern": "^5.0.1",
"typescript": "^5.1.6"
},
"scripts": {
"start": "vite --force",
@@ -64,29 +65,30 @@
]
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.0",
"@tailwindcss/forms": "^0.5.3",
"@tauri-apps/cli": "^1.4.0",
"@types/file-saver": "^2.0.5",
"@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.59.6",
"@typescript-eslint/parser": "^5.59.0",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"autoprefixer": "^10.4.4",
"cross-env": "^7.0.3",
"eslint": "^8.18.0",
"eslint": "^8.44.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-import-resolver-typescript": "^3.1.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-react": "^7.30.1",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.12",
"prettier": "^2.7.1",
"postcss": "^8.4.24",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"rollup-plugin-visualizer": "^5.9.2",
"tailwind-gradient-mask-image": "^1.0.0",
"tailwindcss": "^3.3.1",
"vite": "^4.0.3"
"tailwindcss": "^3.3.2",
"vite": "^4.3.9"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -42,6 +42,7 @@ body_part-LEFT_HAND = Left hand
body_part-LEFT_UPPER_LEG = Left thigh
body_part-LEFT_LOWER_LEG = Left ankle
body_part-LEFT_FOOT = Left foot
body_part-ACCESSORY = Accessory
## Proportions
skeleton_bone-NONE = None
@@ -109,6 +110,9 @@ widget-overlay-is_mirrored_label = Display Overlay as Mirror
## Widget: Drift compensation
widget-drift_compensation-clear = Clear drift compensation
## Widget: Clear Reset Mounting
widget-clear_mounting = Clear reset mounting
## Widget: Developer settings
widget-developer_mode = Developer Mode
widget-developer_mode-high_contrast = High contrast
@@ -147,9 +151,15 @@ tracker-table-column-url = URL
## Tracker rotation
tracker-rotation-front = Front
tracker-rotation-front_left = Front-Left
tracker-rotation-front_right = Front-Right
tracker-rotation-left = Left
tracker-rotation-right = Right
tracker-rotation-back = Back
tracker-rotation-back_left = Back-Left
tracker-rotation-back_right = Back-Right
tracker-rotation-custom = Custom
tracker-rotation-overriden = (overriden by mounting reset)
## Tracker information
tracker-infos-manufacturer = Manufacturer
@@ -249,6 +259,8 @@ settings-sidebar-osc_router = OSC router
settings-sidebar-osc_trackers = VRChat OSC Trackers
settings-sidebar-utils = Utilities
settings-sidebar-serial = Serial console
settings-sidebar-appearance = Appearance
settings-sidebar-notifications = Notifications
## SteamVR settings
settings-general-steamvr = SteamVR
@@ -314,11 +326,23 @@ settings-general-fk_settings-leg_fk = Leg tracking
settings-general-fk_settings-arm_fk = Arm tracking
settings-general-fk_settings-arm_fk-description = Force arms to be tracked from the HMD even if positional hand data is available.
settings-general-fk_settings-arm_fk-force_arms = Force arms from HMD
settings-general-fk_settings-skeleton_settings = Skeleton settings
settings-general-fk_settings-skeleton_settings-toggles = Skeleton toggles
settings-general-fk_settings-skeleton_settings-description = Toggle skeleton settings on or off. It is recommended to leave these on.
settings-general-fk_settings-skeleton_settings-extended_spine = Extended spine
settings-general-fk_settings-skeleton_settings-extended_pelvis = Extended pelvis
settings-general-fk_settings-skeleton_settings-extended_knees = Extended knee
settings-general-fk_settings-skeleton_settings-extended_spine_model = Extended spine model
settings-general-fk_settings-skeleton_settings-extended_pelvis_model = Extended pelvis model
settings-general-fk_settings-skeleton_settings-extended_knees_model = Extended knee model
settings-general-fk_settings-skeleton_settings-ratios = Skeleton ratios
settings-general-fk_settings-skeleton_settings-ratios-description = Change the values of skeleton settings. You may need to adjust your proportions after changing these.
settings-general-fk_settings-skeleton_settings-impute_waist_from_chest_hip = Impute waist from chest to hip
settings-general-fk_settings-skeleton_settings-impute_waist_from_chest_legs = Impute waist from chest to legs
settings-general-fk_settings-skeleton_settings-impute_hip_from_chest_legs = Impute hip from chest to legs
settings-general-fk_settings-skeleton_settings-impute_hip_from_waist_legs = Impute hip from waist to legs
settings-general-fk_settings-skeleton_settings-interp_hip_legs = Average the hip's yaw and roll with the legs'
settings-general-fk_settings-skeleton_settings-interp_knee_tracker_ankle = Average the knee trackers' yaw and roll with the ankles'
settings-general-fk_settings-self_localization-title = Mocap mode
settings-general-fk_settings-self_localization-description = Mocap Mode allows the skeleton to roughly track its own position without a headset or other trackers. Note that this requires feet and head trackers to work and is still experimental.
settings-general-fk_settings-vive_emulation-title = Vive emulation
settings-general-fk_settings-vive_emulation-description = Emulate the waist tracker problems that Vive trackers have. This is a joke and makes tracking worse.
settings-general-fk_settings-vive_emulation-label = Enable Vive emulation
@@ -326,13 +350,19 @@ settings-general-fk_settings-vive_emulation-label = Enable Vive emulation
## Gesture control settings (tracker tapping)
settings-general-gesture_control = Gesture control
settings-general-gesture_control-subtitle = Tap based resets
settings-general-gesture_control-description = Allows for resets to be triggered by tapping a tracker. The tracker highest up on your torso is used for Yaw Reset, the tracker highest up on your left leg is used for Full Reset, and the tracker highest up on your right leg is used for Mounting Reset. It should be mentioned that taps must happen within 0.6 seconds to be registered.
settings-general-gesture_control-description = Allows for resets to be triggered by tapping a tracker. The tracker highest up on your torso is used for Yaw Reset, the tracker highest up on your left leg is used for Full Reset, and the tracker highest up on your right leg is used for Mounting Reset. Taps must occur within the time limit of 0.3 seconds times the number of taps to be recognized.
# This is a unit: 3 taps, 2 taps, 1 tap
# $amount (Number) - Amount of taps (touches to the tracker's case)
settings-general-gesture_control-taps = { $amount ->
[one] 1 tap
*[other] { $amount } taps
}
# This is a unit: 3 trackers, 2 trackers, 1 tracker
# $amount (Number) - Amount of trackers
settings-general-gesture_control-trackers = { $amount ->
[one] 1 tracker
*[other] { $amount } trackers
}
settings-general-gesture_control-yawResetEnabled = Enable tap to yaw reset
settings-general-gesture_control-yawResetDelay = Yaw reset delay
settings-general-gesture_control-yawResetTaps = Taps for yaw reset
@@ -342,23 +372,37 @@ settings-general-gesture_control-fullResetTaps = Taps for full reset
settings-general-gesture_control-mountingResetEnabled = Enable tap to reset mounting
settings-general-gesture_control-mountingResetDelay = Mounting reset delay
settings-general-gesture_control-mountingResetTaps = Taps for mounting reset
# The number of trackers that can have higher acceleration before a tap is rejected
settings-general-gesture_control-numberTrackersOverThreshold = Trackers over threshold
settings-general-gesture_control-numberTrackersOverThreshold-description = Increase this value if tap detection is not working. Do not increase it above what is needed to make tap detection work as it would cause more false positives.
## Interface settings
settings-general-interface = Interface
## Appearance settings
settings-interface-appearance = Appearance
settings-general-interface-dev_mode = Developer Mode
settings-general-interface-dev_mode-description = This mode can be useful if you need in-depth data or to interact with connected trackers on a more advanced level.
settings-general-interface-dev_mode-label = Developer Mode
settings-general-interface-serial_detection = Serial device detection
settings-general-interface-serial_detection-description = This option will show a pop-up every time you plug a new serial device that could be a tracker. It helps improving the setup process of a tracker.
settings-general-interface-serial_detection-label = Serial device detection
settings-general-interface-feedback_sound = Feedback sound
settings-general-interface-feedback_sound-description = This option will play a sound when a reset is triggered
settings-general-interface-feedback_sound-label = Feedback sound
settings-general-interface-feedback_sound-volume = Feedback sound volume
settings-general-interface-theme = Color theme
settings-general-interface-lang = Select language
settings-general-interface-lang-description = Change the default language you want to use.
settings-general-interface-lang-placeholder = Select the language to use
# Keep the font name untranslated
settings-interface-appearance-font = GUI font
settings-interface-appearance-font-description = This changes the font used by the interface.
settings-interface-appearance-font-placeholder = Default font
settings-interface-appearance-font-os_font = OS font
settings-interface-appearance-font-slime_font = Default font
settings-interface-appearance-font_size = Base font scaling
settings-interface-appearance-font_size-description = This affects the font size of the whole interface except this settings panel.
## Notification settings
settings-interface-notifications = Notifications
settings-general-interface-serial_detection = Serial device detection
settings-general-interface-serial_detection-description = This option will show a pop-up every time you plug a new serial device that could be a tracker. It helps improving the setup process of a tracker.
settings-general-interface-serial_detection-label = Serial device detection
settings-general-interface-feedback_sound = Feedback sound
settings-general-interface-feedback_sound-description = This option will play a sound when a reset is triggered.
settings-general-interface-feedback_sound-label = Feedback sound
settings-general-interface-feedback_sound-volume = Feedback sound volume
## Serial settings
settings-serial = Serial Console
@@ -410,6 +454,8 @@ settings-osc-vrchat = VRChat OSC Trackers
settings-osc-vrchat-description =
Change VRChat-specific settings to receive HMD data and send
tracker data for FBT without SteamVR (ex. Quest standalone).
This also allows you to receive trackers under the OSCTrackers standard.
This will also send accessory trackers' rotations for custom avatars.
settings-osc-vrchat-enable = Enable
settings-osc-vrchat-enable-description = Toggle the sending and receiving of data.
settings-osc-vrchat-enable-label = Enable
@@ -437,12 +483,12 @@ settings-osc-vmc = Virtual Motion Capture
# This cares about multilines
settings-osc-vmc-description =
Change settings specific to the VMC (Virtual Motion Capture) protocol
to send SlimeVR's bone data and receive bone data from other apps.
to send and received bone and tracker data from and to other apps.
settings-osc-vmc-enable = Enable
settings-osc-vmc-enable-description = Toggle the sending and receiving of data.
settings-osc-vmc-enable-label = Enable
settings-osc-vmc-network = Network ports
settings-osc-vmc-network-description = Set the ports for listening and sending data via VMC
settings-osc-vmc-network-description = Set the ports for listening and sending data via VMC.
settings-osc-vmc-network-port_in =
.label = Port In
.placeholder = Port in (default: 39540)
@@ -450,10 +496,10 @@ settings-osc-vmc-network-port_out =
.label = Port Out
.placeholder = Port out (default: 39539)
settings-osc-vmc-network-address = Network address
settings-osc-vmc-network-address-description = Choose which address to send out data at via VMC
settings-osc-vmc-network-address-description = Choose which address to send out data at via VMC.
settings-osc-vmc-network-address-placeholder = IPV4 address
settings-osc-vmc-vrm = VRM Model
settings-osc-vmc-vrm-description = Load a VRM model to allow head anchor and enable a higher compatibility with other applications
settings-osc-vmc-vrm-description = Load a VRM model to allow head anchor and enable a higher compatibility with other applications.
settings-osc-vmc-vrm-model_unloaded = No model loaded
settings-osc-vmc-vrm-model_loaded = { $titled ->
*[false] Untitled model loaded
@@ -695,7 +741,10 @@ onboarding-choose_proportions-description = Body proportions are used to know th
onboarding-choose_proportions-auto_proportions = Automatic proportions
# Italized text
onboarding-choose_proportions-auto_proportions-subtitle = Recommended
onboarding-choose_proportions-auto_proportions-description = This will guess your proportions by recording a sample of your movements and passing it through an algorithm
onboarding-choose_proportions-auto_proportions-descriptionv2 =
This will guess your proportions by recording a sample of your movements and passing it through an algorithm.
<b>This requires having your HMD connected to SlimeVR!</b>
onboarding-choose_proportions-manual_proportions = Manual proportions
# Italized text
onboarding-choose_proportions-manual_proportions-subtitle = For small touches
@@ -729,6 +778,18 @@ onboarding-automatic_proportions-requirements-description =
Your trackers and headset are working properly within the SlimeVR server.
Your headset is reporting positional data to the SlimeVR server (this generally means having SteamVR running and connected to SlimeVR using SlimeVR's SteamVR driver).
onboarding-automatic_proportions-requirements-next = I have read the requirements
onboarding-automatic_proportions-check_height-title = Check your height
onboarding-automatic_proportions-check_height-description = We use your height as a basis of our measurements by using the HMD's height as an approximation of your actual height, but it's better to check if they are right yourself!
# All the text is in bold!
onboarding-automatic_proportions-check_height-calculation_warning = Please press the button while standing <u>upright</u> to calculate your height. You have 3 seconds after you press the button!
onboarding-automatic_proportions-check_height-fetch_height = I'm standing!
# Context is that the height is unknown
onboarding-automatic_proportions-check_height-unknown = Unknown
# Shows an element below it
onboarding-automatic_proportions-check_height-height = Your height is
# Shows an element below it
onboarding-automatic_proportions-check_height-hmd_height = And HMD height is
onboarding-automatic_proportions-check_height-next_step = They are fine
onboarding-automatic_proportions-start_recording-title = Get ready to move
onboarding-automatic_proportions-start_recording-description = We're now going to record some specific poses and moves. These will be prompted in the next screen. Be ready to start when the button is pressed!
onboarding-automatic_proportions-start_recording-next = Start Recording
@@ -757,6 +818,10 @@ onboarding-automatic_proportions-verify_results-redo = Redo recording
onboarding-automatic_proportions-verify_results-confirm = They're correct
onboarding-automatic_proportions-done-title = Body measured and saved.
onboarding-automatic_proportions-done-description = Your body proportions' calibration is complete!
onboarding-automatic_proportions-error_modal =
<b>Warning:</b> An error was found while estimating proportions!
Please <docs>check the docs</docs> or join our <discord>Discord</discord> for help ^_^
onboarding-automatic_proportions-error_modal-confirm = Understood!
## Home
home-no_trackers = No trackers detected or assigned

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 998 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

View File

@@ -30,7 +30,8 @@ serde_json = "1"
serde = { version = "1", features = ["derive"] }
tauri = { version = "1.4", features = ["devtools", "dialog", "dialog-save", "fs-all", "os-all", "path-all", "shell-execute", "shell-open", "window-close", "window-maximize", "window-minimize", "window-set-resizable", "window-set-size", "window-set-title", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri-runtime = "0.14"
pretty_env_logger = "0.5"
flexi_logger = "0.25"
log-panics = { version = "2", features = ["with-backtrace"] }
log = "0.4"
clap = { version = "4.0.29", features = ["derive"] }
clap-verbosity-flag = "2"
@@ -38,7 +39,7 @@ rand = "0.8.5"
tempfile = "3"
which = "4.3"
glob = "0.3"
open = "4"
open = "5"
shadow-rs = { version = "0.23", default-features = false }
const_format = "0.2.30"
cfg-if = "1"

View File

@@ -9,6 +9,7 @@ use std::time::Duration;
use std::time::Instant;
use clap::Parser;
use color_eyre::Result;
use state::WindowState;
use tauri::api::process::{Command, CommandChild};
use tauri::Manager;
@@ -38,22 +39,55 @@ fn update_window_state(
Ok(())
}
fn main() {
#[tauri::command]
fn logging(msg: String) {
log::info!(target: "webview", "{}", msg)
}
#[tauri::command]
fn erroring(msg: String) {
log::error!(target: "webview", "{}", msg)
}
#[tauri::command]
fn warning(msg: String) {
log::warn!(target: "webview", "{}", msg)
}
fn main() -> Result<()> {
log_panics::init();
let hook = panic::take_hook();
// Make an error dialog box when panicking
panic::set_hook(Box::new(|panic_info| {
println!("{}", panic_info);
panic::set_hook(Box::new(move |panic_info| {
show_error(&panic_info.to_string());
hook(panic_info);
}));
let cli = Cli::parse();
let tauri_context = tauri::generate_context!();
// Set up loggers and global handlers
{
if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "info")
}
pretty_env_logger::init();
}
let _logger = {
use flexi_logger::{
Age, Cleanup, Criterion, Duplicate, FileSpec, Logger, Naming, WriteMode,
};
use tauri::api::path::app_log_dir;
Logger::try_with_env_or_str("info")?
.log_to_file(FileSpec::default().directory(
app_log_dir(tauri_context.config()).expect("We need a log dir"),
))
.format_for_files(util::logger_format)
.format_for_stderr(util::logger_format)
.rotate(
Criterion::Age(Age::Day),
Naming::Timestamps,
Cleanup::KeepLogFiles(2),
)
.duplicate_to_stderr(Duplicate::All)
.write_mode(WriteMode::BufferAndFlush)
.start()?
};
// Ensure child processes die when spawned on windows
// and then check for WebView2's existence
@@ -86,7 +120,7 @@ fn main() {
if confirm {
open::that("https://docs.slimevr.dev/server-setup/installing-and-connecting.html#install-the-latest-slimevr-installer").unwrap();
}
return;
return Ok(());
}
}
@@ -106,7 +140,7 @@ fn main() {
.or_else(|| valid_java_paths().first().map(|x| x.0.to_owned()));
let Some(java_bin) = java_bin else {
show_error(&format!("Couldn't find a compatible Java version, please download Java {} or higher", MINIMUM_JAVA_VERSION));
return;
return Ok(());
};
log::info!("Using Java binary: {:?}", java_bin);
@@ -124,7 +158,12 @@ fn main() {
let exit_flag_terminated = exit_flag.clone();
let build_result = tauri::Builder::default()
.invoke_handler(tauri::generate_handler![update_window_state])
.invoke_handler(tauri::generate_handler![
update_window_state,
logging,
erroring,
warning
])
.setup(move |app| {
let window_state =
WindowState::open_state(app.path_resolver().app_config_dir().unwrap())
@@ -190,7 +229,7 @@ fn main() {
WindowEvent::Resized(_) => std::thread::sleep(std::time::Duration::from_nanos(1)),
_ => (),
})
.build(tauri::generate_context!());
.build(tauri_context);
match build_result {
Ok(app) => {
app.run(move |app_handle, event| match event {
@@ -246,4 +285,6 @@ fn main() {
show_error(&error.to_string());
}
}
Ok(())
}

View File

@@ -10,6 +10,8 @@ use std::{
use clap::Parser;
use const_format::concatcp;
use flexi_logger::{DeferredNow, style};
use log::Record;
use shadow_rs::shadow;
use tempfile::Builder;
@@ -216,3 +218,25 @@ pub fn valid_java_paths() -> Vec<(OsString, i32)> {
})
.collect()
}
pub fn logger_format(
w: &mut dyn std::io::Write,
_now: &mut DeferredNow,
record: &Record,
) -> Result<(), std::io::Error> {
let level = record.level();
let module_path = record.module_path().unwrap_or("<unnamed>");
// optionally print target
let target = if module_path.starts_with(record.target()) {
"".to_string()
} else {
format!(", {}", record.target())
};
write!(
w,
"{} [{}{target}] {}",
style(level).paint(level.to_string()),
record.module_path().unwrap_or("<unnamed>"),
style(level).paint(record.args().to_string())
)
}

View File

@@ -30,7 +30,7 @@
"deb": {
"depends": ["openjdk-17-jre-headless"],
"files": {
"/usr/share/slimevr/slimevr.jar": "../../server/build/libs/slimevr.jar"
"/usr/share/slimevr/slimevr.jar": "../../server/desktop/build/libs/slimevr.jar"
}
},
"appimage": {

View File

@@ -50,10 +50,13 @@ import { open } from '@tauri-apps/api/shell';
import semver from 'semver';
import { useBreakpoint } from './hooks/breakpoint';
import { VRModePage } from './components/vr-mode/VRModePage';
import { InterfaceSettings } from './components/settings/pages/InterfaceSettings';
import { error, log } from './utils/logging';
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
export const VersionContext = createContext('');
export const DOCS_SITE = 'https://docs.slimevr.dev/';
export const DOCS_SITE = 'https://docs.slimevr.dev';
export const SLIMEVR_DISCORD = 'https://discord.gg/slimevr';
function Layout() {
const { loading } = useConfig();
@@ -103,6 +106,7 @@ function Layout() {
<Route path="osc/router" element={<OSCRouterSettings />} />
<Route path="osc/vrchat" element={<VRCOSCSettings />} />
<Route path="osc/vmc" element={<VMCSettings />} />
<Route path="interface" element={<InterfaceSettings />} />
</Route>
<Route
path="/onboarding"
@@ -166,19 +170,19 @@ export default function App() {
setUpdateFound(releases[0].tag_name);
}
}
fetchReleases().catch(() => console.error('failed to fetch releases'));
fetchReleases().catch(() => error('failed to fetch releases'));
}, []);
if (window.__TAURI_METADATA__) {
useEffect(() => {
os.type()
.then((type) => document.body.classList.add(type.toLowerCase()))
.catch(console.error);
.catch(error);
return () => {
os.type()
.then((type) => document.body.classList.remove(type.toLowerCase()))
.catch(console.error);
.catch(error);
};
}, []);
}
@@ -192,6 +196,7 @@ export default function App() {
if ('stderr' === eventType) {
// This strange invocation is what lets us lose the line information in the console
// See more here: https://stackoverflow.com/a/48994308
// These two are fine to keep with console.log, they are server logs
setTimeout(
console.log.bind(
console,
@@ -210,11 +215,11 @@ export default function App() {
)
);
} else if (eventType === 'error') {
console.error('Error: %s', s);
error('Error: %s', s);
} else if (eventType === 'terminated') {
console.error('Server Process Terminated: %s', s);
error('Server Process Terminated: %s', s);
} else if (eventType === 'other') {
console.log('Other process event: %s', s);
log('Other process event: %s', s);
}
}
);

View File

@@ -0,0 +1,25 @@
import { useLocalization } from '@fluent/react';
import { ClearMountingResetRequestT, RpcMessage } from 'solarxr-protocol';
import { useWebsocketAPI } from '../hooks/websocket-api';
import { BigButton } from './commons/BigButton';
import { TrashIcon } from './commons/icon/TrashIcon';
export function ClearMountingButton() {
const { l10n } = useLocalization();
const { sendRPCPacket } = useWebsocketAPI();
const clearMounting = () => {
const record = new ClearMountingResetRequestT();
sendRPCPacket(RpcMessage.ClearMountingResetRequest, record);
};
return (
<BigButton
text={l10n.getString('widget-clear_mounting')}
icon={<TrashIcon width={20} />}
onClick={clearMounting}
>
{}
</BigButton>
);
}

View File

@@ -77,7 +77,7 @@ export function MainLayoutRoute({
>
<div
className={classNames(
'flex flex-col rounded-xl w-full overflow-hidden mobile:overflow-y-auto',
'flex flex-col rounded-xl w-full overflow-clip mobile:overflow-y-auto',
background && 'bg-background-70'
)}
>

View File

@@ -6,6 +6,7 @@ import { Typography } from './commons/Typography';
import { open } from '@tauri-apps/api/shell';
import semver from 'semver';
import { GH_REPO, VersionContext } from '../App';
import { error } from '../utils/logging';
export function VersionUpdateModal() {
const { l10n } = useLocalization();
@@ -24,7 +25,7 @@ export function VersionUpdateModal() {
);
}
} catch {
console.error('failed to parse new version');
error('failed to parse new version');
}
return (

View File

@@ -10,17 +10,19 @@ import { useConfig } from '../hooks/config';
import {
ResetType,
RpcMessage,
SettingsRequestT,
SettingsResponseT,
StatusData,
} from 'solarxr-protocol';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { parseStatusToLocale, useStatusContext } from '../hooks/status-system';
import { useWebsocketAPI } from '../hooks/websocket-api';
import { useAppContext } from '../hooks/app';
import { ClearMountingButton } from './ClearMountingButton';
export function WidgetsComponent() {
const { config } = useConfig();
const { useRPCPacket } = useWebsocketAPI();
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const [driftCompensationEnabled, setDriftCompensationEnabled] =
useState(false);
const { trackers } = useAppContext();
@@ -31,6 +33,10 @@ export function WidgetsComponent() {
[statuses]
);
useEffect(() => {
sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT());
}, []);
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
if (settings.driftCompensation != null)
setDriftCompensationEnabled(settings.driftCompensation.enabled);
@@ -41,9 +47,8 @@ export function WidgetsComponent() {
<div className="grid grid-cols-2 gap-2 w-full [&>*:nth-child(odd):last-of-type]:col-span-full">
<ResetButton type={ResetType.Yaw} variant="big"></ResetButton>
<ResetButton type={ResetType.Full} variant="big"></ResetButton>
{config?.debug && (
<ResetButton type={ResetType.Mounting} variant="big"></ResetButton>
)}
<ResetButton type={ResetType.Mounting} variant="big"></ResetButton>
<ClearMountingButton></ClearMountingButton>
<BVHButton></BVHButton>
<TrackingPauseButton></TrackingPauseButton>
{driftCompensationEnabled && (
@@ -53,7 +58,7 @@ export function WidgetsComponent() {
<div className="w-full">
<OverlayWidget></OverlayWidget>
</div>
<div className="w-full flex flex-col max-h-[33%] gap-3 overflow-y-auto mb-2">
<div className="w-full flex flex-col gap-3 mb-2">
{unprioritizedStatuses.map((status) => (
<Localized
id={`status_system-${StatusData[status.dataType]}`}

View File

@@ -0,0 +1,14 @@
import { open } from '@tauri-apps/api/shell';
import { ReactNode } from 'react';
export function A({ href, children }: { href: string; children?: ReactNode }) {
return (
<a
href="javascript:void(0)"
onClick={() => open(href).catch(() => window.open(href, '_blank'))}
className="underline"
>
{children}
</a>
);
}

View File

@@ -19,7 +19,7 @@ export function BigButton({
{...props}
type="button"
className={classNames(
'flex flex-col justify-center rounded-md py-3 gap-1 px-3 cursor-pointer items-center ',
'flex flex-col justify-center rounded-md py-3 gap-1 px-3 cursor-pointer items-center',
{
'bg-background-60 hover:bg-background-60 cursor-not-allowed text-background-40 fill-background-40':
disabled,
@@ -30,7 +30,7 @@ export function BigButton({
)}
>
<div className="flex justify-around">{icon}</div>
<div className="flex text-default flex-grow">{text}</div>
<div className="flex text-default flex-grow items-center">{text}</div>
</button>
);
}

View File

@@ -84,6 +84,9 @@ export const mapPart: Record<
<UpperLegIcon width={width} flipped></UpperLegIcon>
),
[BodyPart.WAIST]: ({ width }) => <WaistIcon width={width}></WaistIcon>,
[BodyPart.ACCESSORY]: ({ width }) => (
<SlimeVRIcon width={width}></SlimeVRIcon>
),
};
export function BodyPartIcon({

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames';
import React, { ReactNode, useMemo } from 'react';
import { NavLink } from 'react-router-dom';
import { LoaderIcon } from './icon/LoaderIcon';
import { LoaderIcon, SlimeState } from './icon/LoaderIcon';
function ButtonContent({
loading,
@@ -29,7 +29,7 @@ function ButtonContent({
</div>
{loading && (
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center fill-background-10">
<LoaderIcon youSpinMeRightRoundBabyRightRound></LoaderIcon>
<LoaderIcon slimeState={SlimeState.JUMPY}></LoaderIcon>
</div>
)}
</>

View File

@@ -6,11 +6,12 @@ import {
UseFormGetValues,
useWatch,
} from 'react-hook-form';
import { a11yClick } from '../utils/a11y';
import { a11yClick } from '../../utils/a11y';
export interface DropdownItem {
label: string;
value: string;
fontName?: string;
}
export type DropdownDirection = 'up' | 'down';
@@ -130,7 +131,7 @@ export function Dropdown({
onKeyDown={(ev) => a11yClick(ev) && setOpen((open) => !open)}
tabIndex={0}
>
<div className="flex-grow">
<div className="flex-grow text-standard">
{items.find((i) => i.value == value)?.label || placeholder}
</div>
<div
@@ -176,6 +177,7 @@ export function Dropdown({
<ul className="py-1 text-sm flex flex-col">
{items.map((item) => (
<li
style={item.fontName ? { fontFamily: item.fontName } : {}}
className={classNames(
'py-2 px-4 min-w-max cursor-pointer',
variant == 'primary' &&

View File

@@ -10,6 +10,7 @@ export function NumberSelector({
min,
max,
step,
disabled = false,
}: {
label: string;
valueLabelFormat?: (value: number) => string;
@@ -18,6 +19,7 @@ export function NumberSelector({
min: number;
max: number;
step: number | ((value: number, add: boolean) => number);
disabled?: boolean;
}) {
const stepFn =
typeof step === 'function'
@@ -38,12 +40,12 @@ export function NumberSelector({
variant="tertiary"
rounded
onClick={() => onChange(stepFn(value, false))}
disabled={stepFn(value, false) < min}
disabled={stepFn(value, false) < min || disabled}
>
-
</Button>
</div>
<div className="flex flex-grow justify-center items-center w-10">
<div className="flex flex-grow justify-center items-center w-10 text-standard">
{valueLabelFormat ? valueLabelFormat(value) : value}
</div>
<div className="flex">
@@ -51,7 +53,7 @@ export function NumberSelector({
variant="tertiary"
rounded
onClick={() => onChange(stepFn(value, true))}
disabled={stepFn(value, true) > max}
disabled={stepFn(value, true) > max || disabled}
>
+
</Button>

View File

@@ -12,7 +12,7 @@ export function PersonFrontIcon({ width }: { width?: number }) {
<image
height={'105%'}
x="8.5%"
href="/images/front-standing-pose.png"
href="/images/front-standing-pose.webp"
></image>
{/* <path d="M84.53 224.074C83.953 230.874 88.569 266.874 90.951 280.984C92.085 287.671 95.195 298.565 94.076 304.349C92.476 312.411 92.017 322.843 92.896 328.918C93.451 332.607 95.196 349.618 92.696 355.845C91.389 359.108 88.996 375.832 88.996 375.832C82.756 391.587 86.278 390.812 86.278 390.812C88.21 393.183 91.519 390.998 91.519 390.998C92.1549 391.464 92.9388 391.682 93.7241 391.612C94.5094 391.542 95.2421 391.188 95.785 390.616C97.949 392.407 100.471 390.396 100.471 390.396C103.189 391.807 105.71 389.205 105.71 389.205C107.271 389.991 107.653 388.998 107.653 388.998C112.337 388.698 105.039 373.706 105.039 373.706C103.291 360.242 106.773 352.748 106.773 352.748C118.178 318.926 118.758 309.948 114.199 297.204C112.915 293.524 112.59 292.067 113.181 290.47C114.547 286.783 113.551 271.953 115.217 266.064C118.431 254.706 121.602 225.903 123.254 212.464C125.475 194.364 115.388 170.088 115.388 170.088C113.179 160.21 116.418 125.016 116.418 125.016C120.941 132.054 120.768 144.477 120.768 144.477C120.05 157.506 131.294 177.42 131.294 177.42C136.694 185.649 138.742 193.456 138.742 194.036C138.742 196.407 138.223 202.145 138.223 202.145L138.43 207.145C138.803 209.721 139.034 212.316 139.123 214.918C138.28 227.953 140.35 225.501 140.35 225.501C142.098 225.501 144.018 215.011 144.018 215.011C144.018 217.711 143.357 225.811 144.818 228.869C146.564 232.512 147.848 228.244 147.871 227.387C148.333 210.787 149.33 215.138 149.33 215.138C150.301 228.602 151.494 231.644 153.63 230.591C155.25 229.818 153.769 214.433 153.769 214.433C156.544 223.572 158.649 225.027 158.649 225.027C163.229 228.243 160.397 219.361 159.76 217.602C156.371 208.256 156.267 205.017 156.267 205.017C160.501 213.417 163.692 213.104 163.692 213.104C167.822 211.786 160.083 199.894 155.548 194.197C153.234 191.297 150.248 187.408 149.384 185.097C147.973 181.188 146.907 168.62 146.907 168.62C146.48 153.79 142.813 147.348 142.813 147.348C136.544 137.314 135.365 118.598 135.365 118.598L135.09 87C132.89 65.445 117.01 65.29 117.01 65.29C100.957 62.9 98.723 57.714 98.723 57.714C95.323 52.821 97.266 43.44 97.266 43.44C100.087 41.145 101.175 35.053 101.175 35.053C105.859 31.461 105.63 26.205 103.466 26.262C101.73 26.308 102.123 24.87 102.123 24.87C105.052 1.208 84.046 0 84.046 0H80.836C80.836 0 59.821 1.208 62.746 24.864C62.746 24.864 63.139 26.304 61.388 26.256C59.23 26.199 59.029 31.456 63.696 35.047C63.696 35.047 64.783 41.137 67.605 43.434C67.605 43.434 69.548 52.814 66.148 57.708C66.148 57.708 63.922 62.894 47.861 65.284C47.861 65.284 31.952 65.44 29.788 86.994L29.488 118.594C29.488 118.594 28.331 137.311 22.038 147.344C22.038 147.344 18.389 153.787 17.967 168.616C17.967 168.616 16.898 181.184 15.492 185.093C14.635 187.393 11.653 191.276 9.32001 194.193C4.74601 199.878 -2.94199 211.745 1.17101 213.1C1.17101 213.1 4.37901 213.412 8.59601 205.013C8.59601 205.013 8.50901 208.229 5.12501 217.598C4.46001 219.334 1.63201 228.217 6.21301 225.024C6.21301 225.024 8.33501 223.567 11.093 214.43C11.093 214.43 9.61301 229.815 11.26 230.588C13.412 231.642 14.586 228.599 15.56 215.135C15.56 215.135 16.56 210.787 17.017 227.384C17.04 228.241 18.295 232.509 20.049 228.866C21.529 225.811 20.864 217.727 20.864 215.008C20.864 215.008 22.764 225.498 24.536 225.498C24.536 225.498 26.624 227.95 25.767 214.915C25.628 212.786 26.375 208.415 26.467 207.142L26.667 202.142C26.667 202.142 26.146 196.417 26.146 194.033C26.146 193.442 28.194 185.646 33.594 177.417C33.594 177.417 44.826 157.494 44.103 144.474C44.103 144.474 43.947 132.051 48.47 125.013C48.47 125.013 51.68 160.205 49.505 170.085C49.505 170.085 39.405 194.358 41.629 212.461C43.27 225.937 46.435 254.702 49.657 266.061C51.34 271.938 50.345 286.761 51.693 290.467C52.301 292.076 51.982 293.558 50.675 297.201C46.141 309.947 46.718 318.925 58.123 352.745C58.123 352.745 61.633 360.239 59.859 373.703C59.859 373.703 52.572 388.695 57.239 388.995C57.239 388.995 57.604 389.988 59.182 389.202C59.182 389.202 61.703 391.802 64.427 390.393C64.427 390.393 66.95 392.407 69.106 390.613C69.6451 391.185 70.3751 391.54 71.158 391.61C71.9409 391.681 72.7225 391.462 73.355 390.995C73.355 390.995 76.664 393.227 78.63 390.809C78.63 390.809 82.123 391.584 75.904 375.829C75.904 375.829 73.522 359.129 72.209 355.842C69.709 349.621 71.474 332.57 72.009 328.915C72.87 322.806 72.409 312.398 70.835 304.346C69.684 298.575 72.801 287.679 73.952 280.981C76.317 266.881 80.952 230.881 80.373 224.071L82.288 224.743C83.0863 224.756 83.8692 224.522 84.53 224.074Z" /> */}
<circle

View File

@@ -0,0 +1,72 @@
import classNames from 'classnames';
import { Control, Controller } from 'react-hook-form';
export function Range({
control,
name,
values,
min,
max,
step,
// input props
...props
}: {
control: Control<any>;
name: string;
max: number;
min: number;
step: number;
values: { value: number; label: string; defaultValue?: boolean }[];
} & React.HTMLProps<HTMLInputElement>) {
return (
<Controller
control={control}
name={name}
render={({ field: { onChange, ref, name, value } }) => (
<label className="text-standard w-full text-center flex items-center flex-col">
<input
type="range"
className=" text-background-10 border-accent-background-30"
style={{
width: 'calc(88% - 0.5vw)',
}}
name={name}
ref={ref}
value={value}
onChange={onChange}
list={`${name}-datalist`}
min={min}
max={max}
step={step}
{...props}
/>
<datalist id={`${name}-datalist`} className="">
{values.map(({ value }, i) => (
<option key={i}>{value}</option>
))}
</datalist>
<div className="w-full flex flex-nowrap overflow-clip">
{Array((max - min) / step + 1)
.fill(0)
.map((_v, i) => {
const value = values.find(
({ value }) => i * step + min === value
);
return (
<span
key={i}
className={classNames(
'flex-1',
value?.defaultValue && 'text-status-success'
)}
>
{value?.label}
</span>
);
})}
</div>
</label>
)}
/>
);
}

View File

@@ -0,0 +1,16 @@
export function BellIcon({ width = 24 }: { width?: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
strokeWidth={1.5}
width={width}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
/>
</svg>
);
}

View File

@@ -1,4 +1,10 @@
export function PawIcon({ width = 24 }: { width?: number }) {
export function PawIcon({
width = 24,
transform = '',
}: {
width?: number;
transform?: string;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -7,7 +13,7 @@ export function PawIcon({ width = 24 }: { width?: number }) {
viewBox="0 0 512 512"
>
<path
transform="scale(0.75, 0.75) translate(96, 96)"
transform={`${transform} scale(0.75, 0.75) translate(96, 96)`}
d="M490.39 182.75c-5.55-13.19-14.77-22.7-26.67-27.49l-.16-.06a46.46 46.46 0 00-17-3.2h-.64c-27.24.41-55.05 23.56-69.19 57.61-10.37 24.9-11.56 51.68-3.18 71.64 5.54 13.2 14.78 22.71 26.73 27.5l.13.05a46.53 46.53 0 0017 3.2c27.5 0 55.6-23.15 70-57.65 10.24-24.87 11.37-51.63 2.98-71.6zM381.55 329.61c-15.71-9.44-30.56-18.37-40.26-34.41C314.53 250.8 298.37 224 256 224s-58.57 26.8-85.39 71.2c-9.72 16.06-24.6 25-40.36 34.48-18.07 10.86-36.74 22.08-44.8 44.16a66.93 66.93 0 00-4.65 25c0 35.95 28 65.2 62.4 65.2 17.75 0 36.64-6.15 56.63-12.66 19.22-6.26 39.09-12.73 56.27-12.73s37 6.47 56.15 12.73C332.2 457.85 351 464 368.8 464c34.35 0 62.3-29.25 62.3-65.2a67 67 0 00-4.75-25c-8.06-22.1-26.74-33.33-44.8-44.19zM150 188.85c11.9 14.93 27 23.15 42.52 23.15a42.88 42.88 0 006.33-.47c32.37-4.76 52.54-44.26 45.92-90C242 102.3 234.6 84.39 224 71.11 212.12 56.21 197 48 181.49 48a42.88 42.88 0 00-6.33.47c-32.37 4.76-52.54 44.26-45.92 90 2.76 19.2 10.16 37.09 20.76 50.38zm163.16 22.68a42.88 42.88 0 006.33.47c15.53 0 30.62-8.22 42.52-23.15 10.59-13.29 17.95-31.18 20.75-50.4 6.62-45.72-13.55-85.22-45.92-90a42.88 42.88 0 00-6.33-.47C315 48 299.88 56.21 288 71.11c-10.6 13.28-18 31.19-20.76 50.44-6.62 45.72 13.55 85.22 45.92 89.98zM111.59 308.8l.14-.05c11.93-4.79 21.16-14.29 26.69-27.48 8.38-20 7.2-46.75-3.15-71.65C120.94 175.16 92.85 152 65.38 152a46.4 46.4 0 00-17 3.2l-.14.05c-11.9 4.75-21.13 14.29-26.66 27.48-8.38 20-7.2 46.75 3.15 71.65C39.06 288.84 67.15 312 94.62 312a46.4 46.4 0 0016.97-3.2z"
></path>
</svg>

View File

@@ -0,0 +1,26 @@
export function SlimeUpIcon({ width = 60 }: { width?: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
version="1.1"
viewBox="0 0 16 65.837"
>
<g transform="translate(-57.584 -23.825)">
<rect
width="13.926"
height="42.377"
x="59.526"
y="35.434"
strokeWidth="0.265"
rx="6.963"
ry="3.423"
></rect>
<path
strokeWidth="0.195"
d="M59.587 23.923c.514 0 .978.382 1.313 1.001.336.62.402 1.512.543 2.425l1.376 8.937v40.902l-1.38 8.95c-.14.917-.206 1.805-.542 2.425-.335.619-.8 1.001-1.314 1.001-1.028 0-2.14-1.59-1.856-3.427l1.38-8.949V36.286l-1.377-8.937c-.281-1.826.828-3.426 1.857-3.426z"
></path>
</g>
</svg>
);
}

View File

@@ -1,6 +1,11 @@
import { useLocalization } from '@fluent/react';
import { useMemo } from 'react';
import { ResetRequestT, ResetType, RpcMessage } from 'solarxr-protocol';
import {
ResetRequestT,
ResetType,
RpcMessage,
StatusData,
} from 'solarxr-protocol';
import { useConfig } from '../../hooks/config';
import { useCountdown } from '../../hooks/countdown';
import { useWebsocketAPI } from '../../hooks/websocket-api';
@@ -12,6 +17,7 @@ import {
YawResetIcon,
FullResetIcon,
} from '../commons/icon/ResetIcon';
import { useStatusContext } from '../../hooks/status-system';
export function ResetButton({
type,
@@ -24,8 +30,18 @@ export function ResetButton({
}) {
const { l10n } = useLocalization();
const { sendRPCPacket } = useWebsocketAPI();
const { statuses } = useStatusContext();
const { config } = useConfig();
const needsFullReset = useMemo(
() =>
type === ResetType.Mounting &&
Object.values(statuses).some(
(status) => status.dataType === StatusData.StatusTrackerReset
),
[statuses]
);
const reset = () => {
const req = new ResetRequestT();
req.resetType = type;
@@ -75,7 +91,7 @@ export function ResetButton({
maybePlaySoundOnResetStarted(type);
}}
variant="primary"
disabled={isCounting}
disabled={isCounting || needsFullReset}
>
<div className="relative">
<div className="opacity-0 h-0">{text}</div>
@@ -91,7 +107,7 @@ export function ResetButton({
startCountdown();
maybePlaySoundOnResetStarted(type);
}}
disabled={isCounting}
disabled={isCounting || needsFullReset}
></BigButton>
),
};

View File

@@ -293,6 +293,19 @@ export function BodyAssignment({
direction="left"
/>
</div>
{advanced && (
<div className="flex flex-col gap-2">
<TrackerPartCard
onlyAssigned={onlyAssigned}
roleError={rolesWithErrors[BodyPart.ACCESSORY]?.label}
td={trackerPartGrouped[BodyPart.ACCESSORY]}
role={BodyPart.ACCESSORY}
onClick={() => onRoleSelected(BodyPart.ACCESSORY)}
direction="left"
/>
</div>
)}
</div>
}
></BodyInteractions>

View File

@@ -59,7 +59,7 @@ export function ConnectTrackersPage() {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { layoutHeight, ref } = useLayout<HTMLDivElement>();
const { useConnectedTrackers } = useTrackers();
const { useConnectedIMUTrackers } = useTrackers();
const { applyProgress, state } = useOnboarding();
const navigate = useNavigate();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
@@ -68,9 +68,9 @@ export function ConnectTrackersPage() {
applyProgress(0.4);
const connectedTrackers = useConnectedTrackers();
const connectedIMUTrackers = useConnectedIMUTrackers();
const bnoExists = useBnoExists(connectedTrackers);
const bnoExists = useBnoExists(connectedIMUTrackers);
useEffect(() => {
if (!state.wifi) {
@@ -222,7 +222,7 @@ export function ConnectTrackersPage() {
<div className="flex flex-col xs:flex-grow">
<Typography color="secondary" bold>
{l10n.getString('onboarding-connect_tracker-connected_trackers', {
amount: connectedTrackers.length,
amount: connectedIMUTrackers.length,
})}
</Typography>
@@ -237,8 +237,8 @@ export function ConnectTrackersPage() {
>
<div className="grid lg:grid-cols-2 md:grid-cols-1 gap-2 pr-1">
{Array.from({
...connectedTrackers,
length: Math.max(connectedTrackers.length, 1),
...connectedIMUTrackers,
length: Math.max(connectedIMUTrackers.length, 1),
}).map((tracker, index) => (
<div key={index}>
{!tracker && (

View File

@@ -36,7 +36,7 @@ export function HomePage() {
></div>
<img
className="absolute"
src="/images/slime-girl.png"
src="/images/slime-girl.webp"
style={{
width: '35%',
maxWidth: 800,
@@ -46,7 +46,7 @@ export function HomePage() {
/>
<img
className="absolute"
src="/images/slimes.png"
src="/images/slimes.webp"
style={{
width: '35%',
maxWidth: 800,

View File

@@ -17,6 +17,7 @@ import { BodyDisplay } from '../../commons/BodyDisplay';
import { useWebsocketAPI } from '../../../hooks/websocket-api';
import classNames from 'classnames';
import { useBreakpoint } from '../../../hooks/breakpoint';
import { log } from '../../../utils/logging';
export function ResetTutorialPage() {
const { isMobile } = useBreakpoint('mobile');
@@ -82,7 +83,7 @@ export function ResetTutorialPage() {
RpcMessage.ResetResponse,
({ status, resetType }: ResetResponseT) => {
if (status !== ResetStatus.STARTED) return;
console.log(status);
log(status);
if (resetType === RESET_TYPE_ORDER[curIndex]) {
setCurIndex(curIndex + 1);
}

View File

@@ -12,12 +12,12 @@ export function WifiCredsPage() {
const { l10n } = useLocalization();
const { applyProgress, state } = useOnboarding();
const { control, handleSubmit, submitWifiCreds, formState } = useWifiForm();
const { useConnectedTrackers } = useTrackers();
const connectedTrackers = useConnectedTrackers();
const { useConnectedIMUTrackers } = useTrackers();
const connectedIMUTrackers = useConnectedIMUTrackers();
applyProgress(0.2);
const bnoExists = useBnoExists(connectedTrackers);
const bnoExists = useBnoExists(connectedIMUTrackers);
return (
<form

View File

@@ -11,9 +11,9 @@ import { ExtensionArrow } from './ExtensionArrow';
export function AssignmentTutorialPage() {
const { l10n } = useLocalization();
const { applyProgress } = useOnboarding();
const { useConnectedTrackers } = useTrackers();
const connectedTrackers = useConnectedTrackers();
const bnoExists = useBnoExists(connectedTrackers);
const { useConnectedIMUTrackers } = useTrackers();
const connectedIMUTrackers = useConnectedIMUTrackers();
const bnoExists = useBnoExists(connectedIMUTrackers);
applyProgress(0.46);

View File

@@ -16,6 +16,7 @@ import { Recording } from './autobone-steps/Recording';
import { StartRecording } from './autobone-steps/StartRecording';
import { VerifyResultsStep } from './autobone-steps/VerifyResults';
import { useCountdown } from '../../../../hooks/countdown';
import { CheckHeight } from './autobone-steps/СheckHeight';
export function AutomaticProportionsPage() {
const { l10n } = useLocalization();
@@ -53,6 +54,7 @@ export function AutomaticProportionsPage() {
steps={[
{ type: 'numbered', component: PutTrackersOnStep },
{ type: 'numbered', component: RequirementsStep },
{ type: 'numbered', component: CheckHeight },
{ type: 'numbered', component: StartRecording },
{ type: 'fullsize', component: Recording },
{ type: 'numbered', component: VerifyResultsStep },

View File

@@ -6,7 +6,6 @@ import {
useEffect,
useRef,
UIEvent,
MouseEvent,
useMemo,
} from 'react';
import {
@@ -17,6 +16,8 @@ import {
import { useLocaleConfig } from '../../../../i18n/config';
import { Typography } from '../../../commons/Typography';
import { ArrowDownIcon, ArrowUpIcon } from '../../../commons/icon/ArrowIcons';
import { useBreakpoint } from '../../../../hooks/breakpoint';
import { debounce } from '../../../../hooks/timeout';
function IncrementButton({
children,
@@ -52,15 +53,21 @@ export function BodyProportions({
const { bodyParts, dispatch, state, setRatioMode } = useManualProportions();
const { l10n } = useLocalization();
const { currentLocales } = useLocaleConfig();
const { isTall } = useBreakpoint('tall');
const srcollerRef = useRef<HTMLDivElement | null>(null);
const offsetItems = isTall ? 2 : 1;
const itemsToDisplay = offsetItems * 2 + 1;
const itemHeight = 80;
const scrollHeight = itemHeight * itemsToDisplay;
const scrollerRef = useRef<HTMLDivElement | null>(null);
const cmFormat = Intl.NumberFormat(currentLocales, {
style: 'unit',
unit: 'centimeter',
maximumFractionDigits: 1,
});
const percentageFormat = Intl.NumberFormat(currentLocales, {
const percentageFormat = new Intl.NumberFormat(currentLocales, {
style: 'percent',
maximumFractionDigits: 1,
});
@@ -78,41 +85,48 @@ export function BodyProportions({
}, [type]);
useEffect(() => {
if (srcollerRef.current && bodyParts.length > 0) {
moveToIndex(1);
if (scrollerRef.current && bodyParts.length > 0) {
selectId(bodyParts[offsetItems].label);
}
}, [srcollerRef, bodyParts.length]);
}, [scrollerRef, bodyParts.length]);
const handleUIEvent = (e: UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
const itemHeight = target.offsetHeight / 3;
const itemHeight = target.offsetHeight / itemsToDisplay;
const atSnappingPoint = target.scrollTop % itemHeight === 0;
const index = Math.round(target.scrollTop / itemHeight);
const elem = scrollerRef.current?.childNodes[
index + offsetItems
] as HTMLDivElement;
elem.scrollIntoView({ behavior: 'smooth', block: 'center' });
if (atSnappingPoint) {
const index = target.scrollTop / itemHeight;
const elem = srcollerRef.current?.childNodes[index + 1] as HTMLDivElement;
const elem = scrollerRef.current?.childNodes[
index + offsetItems
] as HTMLDivElement;
const id = elem.getAttribute('itemid');
if (id) selectNew(id);
}
};
const clickPart = (id: string) => (e: MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement;
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
const moveToId = (id: string) => {
if (!scrollerRef.current) return;
const index = bodyParts.findIndex(({ label }) => label === id);
scrollerRef.current.scrollTo({
top: index * itemHeight,
behavior: 'smooth',
});
};
const clickPart = (id: string) => () => {
moveToId(id);
selectNew(id);
};
const moveToIndex = (index: number, smooth = true) => {
// We add one because of the offset placeholder
const elem = srcollerRef.current?.childNodes[index + 1] as HTMLDivElement;
elem?.scrollIntoView({
behavior: smooth ? 'smooth' : 'auto',
block: 'center',
});
const id = elem.getAttribute('itemid');
const selectId = (id: string) => {
moveToId(id);
if (id) selectNew(id);
};
@@ -170,15 +184,12 @@ export function BodyProportions({
}, [state]);
const move = (action: 'next' | 'prev') => {
const elem = srcollerRef.current?.querySelector(
const elem = scrollerRef.current?.querySelector(
`div[itemid=${state.currentLabel}]`
);
const moveId = (id: string, elem: HTMLDivElement) => {
elem?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
const moveId = (id: string) => {
moveToId(id);
selectNew(id);
};
@@ -186,14 +197,14 @@ export function BodyProportions({
const prevElem = elem?.previousSibling as HTMLDivElement;
const prevId = prevElem.getAttribute('itemid');
if (!prevId) return;
moveId(prevId, prevElem);
moveId(prevId);
}
if (action === 'next') {
const nextElem = elem?.nextSibling as HTMLDivElement;
const nextId = nextElem.getAttribute('itemid');
if (!nextId) return;
moveId(nextId, nextElem);
moveId(nextId);
}
};
@@ -264,7 +275,7 @@ export function BodyProportions({
className={classNames(
'h-12 w-32 rounded-lg bg-background-60 flex flex-col justify-center',
'items-center fill-background-10',
srcollerRef?.current?.scrollTop ?? 0 > 0
scrollerRef?.current?.scrollTop ?? 0 > 0
? 'opacity-100 active:bg-accent-background-30'
: 'opacity-50'
)}
@@ -273,11 +284,17 @@ export function BodyProportions({
</div>
</div>
<div
ref={srcollerRef}
onScroll={handleUIEvent}
className="h-60 flex-grow flex-col overflow-y-auto snap-y snap-mandatory snap-always no-scrollbar"
ref={scrollerRef}
onScroll={debounce(handleUIEvent, 150)} // Debounce at 150ms to match the animation speed and prevent snaping between two animations
className={classNames(
'flex-grow flex-col overflow-y-auto',
'no-scrollbar'
)}
style={{ height: scrollHeight }}
>
<div className="h-20 snap-start "></div>
{Array.from({ length: offsetItems }).map((_, index) => (
<div style={{ height: itemHeight }} key={index}></div>
))}
{bodyParts.map((part) => {
const { label, value: originalValue, type, ...props } = part;
const value =
@@ -292,7 +309,8 @@ export function BodyProportions({
key={label}
itemID={label}
onClick={clickPart(label)}
className="snap-start h-20 flex-col flex justify-center"
style={{ height: itemHeight }}
className="flex-col flex justify-center"
>
<div
className={classNames(
@@ -320,7 +338,13 @@ export function BodyProportions({
</div>
);
})}
<div className="h-20 snap-start"></div>
{Array.from({ length: offsetItems }).map((_, index) => (
<div
className="h-20"
style={{ height: itemHeight }}
key={index}
></div>
))}
</div>
<div className="flex justify-center">
<div
@@ -328,9 +352,9 @@ export function BodyProportions({
className={classNames(
'h-12 w-32 rounded-lg bg-background-60 flex flex-col justify-center',
'items-center fill-background-10',
srcollerRef?.current?.scrollTop !==
(srcollerRef?.current?.scrollHeight ?? 0) -
(srcollerRef?.current?.offsetHeight ?? 0)
scrollerRef?.current?.scrollTop !==
(scrollerRef?.current?.scrollHeight ?? 0) -
(scrollerRef?.current?.offsetHeight ?? 0)
? 'opacity-100 active:bg-accent-background-30'
: 'opacity-50'
)}

View File

@@ -1,6 +1,6 @@
import { useOnboarding } from '../../../../hooks/onboarding';
import { useLocalization } from '@fluent/react';
import { useState } from 'react';
import { Localized, useLocalization } from '@fluent/react';
import { useMemo, useState } from 'react';
import classNames from 'classnames';
import { Typography } from '../../../commons/Typography';
import { Button } from '../../../commons/Button';
@@ -14,6 +14,12 @@ import saveAs from 'file-saver';
import { save } from '@tauri-apps/api/dialog';
import { writeTextFile } from '@tauri-apps/api/fs';
import { useIsTauri } from '../../../../hooks/breakpoint';
import { useAppContext } from '../../../../hooks/app';
import { error } from '../../../../utils/logging';
export const MIN_HEIGHT = 0.4;
export const MAX_HEIGHT = 4;
export const DEFAULT_HEIGHT = 1.5;
export function ProportionsChoose() {
const isTauri = useIsTauri();
@@ -21,6 +27,26 @@ export function ProportionsChoose() {
const { applyProgress, state } = useOnboarding();
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const [animated, setAnimated] = useState(false);
const { computedTrackers } = useAppContext();
const hmdTracker = useMemo(
() =>
computedTrackers.find(
(tracker) =>
tracker.tracker.trackerId?.trackerNum === 1 &&
tracker.tracker.trackerId.deviceId?.id === undefined
),
[computedTrackers]
);
const beneathFloor = useMemo(
() =>
!(
hmdTracker?.tracker.position &&
hmdTracker.tracker.position.y >= MIN_HEIGHT
),
[hmdTracker?.tracker.position?.y]
);
useRPCPacket(
RpcMessage.SkeletonConfigResponse,
@@ -42,7 +68,7 @@ export function ProportionsChoose() {
path ? writeTextFile(path, JSON.stringify(data)) : undefined
)
.catch((err) => {
console.error(err);
error(err);
});
} else {
saveAs(blob, 'body-proportions.json');
@@ -125,7 +151,7 @@ export function ProportionsChoose() {
<img
onMouseEnter={() => setAnimated(() => true)}
onAnimationEnd={() => setAnimated(() => false)}
src="/images/slimetower.png"
src="/images/slimetower.webp"
className={classNames(
'absolute w-[100px] -right-2 -top-24',
animated && 'animate-[bounce_1s_1]'
@@ -144,15 +170,23 @@ export function ProportionsChoose() {
</Typography>
</div>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-choose_proportions-auto_proportions-description'
)}
</Typography>
<Localized
id="onboarding-choose_proportions-auto_proportions-descriptionv2"
elems={{ b: <b></b> }}
>
<Typography
color="secondary"
whitespace="whitespace-pre-line"
>
Description for autobone
</Typography>
</Localized>
</div>
</div>
<Button
variant="primary"
// Check if we are in dev mode and just let it be used
disabled={beneathFloor && import.meta.env.PROD}
to="/onboarding/body-proportions/auto"
className="self-start mt-auto"
state={{ alonePage: state.alonePage }}

View File

@@ -0,0 +1,64 @@
import { Localized, useLocalization } from '@fluent/react';
import ReactModal from 'react-modal';
import { BaseModal } from '../../../../commons/BaseModal';
import { WarningBox } from '../../../../commons/TipBox';
import { Button } from '../../../../commons/Button';
import { A } from '../../../../commons/A';
import { DOCS_SITE, SLIMEVR_DISCORD } from '../../../../../App';
export function AutoboneErrorModal({
isOpen = true,
onClose,
...props
}: {
/**
* Is the parent/sibling component opened?
*/
isOpen: boolean;
/**
* Function to trigger when closed or accepted
*/
onClose: () => void;
} & ReactModal.Props) {
const { l10n } = useLocalization();
return (
<BaseModal
isOpen={isOpen}
shouldCloseOnOverlayClick
shouldCloseOnEsc
onRequestClose={onClose}
className={props.className}
overlayClassName={props.overlayClassName}
>
<div className="flex w-full h-full flex-col ">
<div className="flex w-full flex-col flex-grow items-center gap-3">
<Localized
id="onboarding-automatic_proportions-error_modal"
elems={{
b: <b></b>,
docs: (
<A
href={`${DOCS_SITE}/server/body-config.html#common-issues--debugging`}
></A>
),
discord: <A href={SLIMEVR_DISCORD}></A>,
}}
>
<WarningBox>
<b>Warning:</b> An autobone error happened!
</WarningBox>
</Localized>
<div className="flex flex-row gap-3 pt-5 place-content-center">
<Button variant="primary" onClick={onClose}>
{l10n.getString(
'onboarding-automatic_proportions-error_modal-confirm'
)}
</Button>
</div>
</div>
</div>
</BaseModal>
);
}

View File

@@ -1,22 +1,49 @@
import { useEffect } from 'react';
import { useAutobone } from '../../../../../hooks/autobone';
import { ReactNode, useEffect, useState } from 'react';
import { ProcessStatus, useAutobone } from '../../../../../hooks/autobone';
import { ProgressBar } from '../../../../commons/ProgressBar';
import { TipBox } from '../../../../commons/TipBox';
import { Typography } from '../../../../commons/Typography';
import { useLocalization } from '@fluent/react';
import { P, match } from 'ts-pattern';
import { AutoboneErrorModal } from './AutoboneErrorModal';
export function Recording({ nextStep }: { nextStep: () => void }) {
export function Recording({
nextStep,
resetSteps,
}: {
nextStep: () => void;
resetSteps: () => void;
}) {
const { l10n } = useLocalization();
const { progress, hasCalibration, hasRecording } = useAutobone();
const { progress, hasCalibration, hasRecording, eta } = useAutobone();
const [modalOpen, setModalOpen] = useState(false);
useEffect(() => {
if (progress === 1 && hasCalibration) {
if (
hasRecording === ProcessStatus.REJECTED ||
hasCalibration === ProcessStatus.REJECTED
) {
setModalOpen(true);
}
if (progress !== 1) return;
if (
hasRecording === ProcessStatus.FULFILLED &&
hasCalibration === ProcessStatus.FULFILLED
) {
nextStep();
}
}, [progress, hasCalibration]);
}, [progress, hasCalibration, hasRecording]);
return (
<div className="flex flex-col items-center w-full justify-between">
<AutoboneErrorModal
isOpen={modalOpen}
onClose={() => {
setModalOpen(false);
resetSteps();
}}
></AutoboneErrorModal>
<div className="flex gap-1 flex-col justify-center items-center">
<div className="flex text-status-critical justify-center items-center gap-1">
<div className="w-2 h-2 rounded-lg bg-status-critical"></div>
@@ -51,19 +78,39 @@ export function Recording({ nextStep }: { nextStep: () => void }) {
<TipBox>{l10n.getString('tips-do_not_move_heels')}</TipBox>
</div>
<div className="flex flex-col gap-2 items-center w-full max-w-[150px]">
<ProgressBar progress={progress} height={2}></ProgressBar>
<ProgressBar
progress={progress}
height={2}
colorClass={match([hasCalibration, hasRecording])
.returnType<string | undefined>()
.with(
P.union(
[ProcessStatus.REJECTED, P._],
[P._, ProcessStatus.REJECTED]
),
() => 'bg-status-critical'
)
.with(
[ProcessStatus.FULFILLED, ProcessStatus.FULFILLED],
() => 'bg-status-success'
)
.otherwise(() => undefined)}
></ProgressBar>
<Typography color="secondary">
{!hasCalibration && hasRecording
? l10n.getString(
{match([hasCalibration, hasRecording])
.returnType<ReactNode>()
.with([ProcessStatus.PENDING, ProcessStatus.FULFILLED], () =>
l10n.getString(
'onboarding-automatic_proportions-recording-processing'
)
: l10n.getString(
)
.with([ProcessStatus.PENDING, ProcessStatus.PENDING], () =>
l10n.getString(
'onboarding-automatic_proportions-recording-timer',
{
// TODO: The progress should be communicated by the server in SolarXR
time: Math.round(20 * (1 - progress)),
}
)}
{ time: Math.round(eta) }
)
)
.otherwise(() => '')}
</Typography>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import { useAutobone } from '../../../../../hooks/autobone';
import { ProcessStatus, useAutobone } from '../../../../../hooks/autobone';
import { Button } from '../../../../commons/Button';
import { Typography } from '../../../../commons/Typography';
import { useLocalization } from '@fluent/react';
@@ -69,13 +69,14 @@ export function VerifyResultsStep({
<Typography bold>{(value * 100).toFixed(2)} CM</Typography>
</div>
))}
{!hasCalibration && hasRecording && (
<Typography>
{l10n.getString(
'onboarding-automatic-proportions-verify-results-processing'
)}
</Typography>
)}
{hasCalibration === ProcessStatus.PENDING &&
hasRecording === ProcessStatus.FULFILLED && (
<Typography>
{l10n.getString(
'onboarding-automatic-proportions-verify-results-processing'
)}
</Typography>
)}
</div>
</div>
</div>

View File

@@ -0,0 +1,176 @@
import {
AutoBoneSettingsT,
ChangeSettingsRequestT,
HeightRequestT,
HeightResponseT,
RpcMessage,
} from 'solarxr-protocol';
import { useWebsocketAPI } from '../../../../../hooks/websocket-api';
import { Button } from '../../../../commons/Button';
import { Typography } from '../../../../commons/Typography';
import { Localized, useLocalization } from '@fluent/react';
import { useForm } from 'react-hook-form';
import { useMemo, useState } from 'react';
import { NumberSelector } from '../../../../commons/NumberSelector';
import { DEFAULT_HEIGHT, MIN_HEIGHT } from '../ProportionsChoose';
import { useLocaleConfig } from '../../../../../i18n/config';
import { useCountdown } from '../../../../../hooks/countdown';
interface HeightForm {
height: number;
hmdHeight: number;
}
export function CheckHeight({
nextStep,
prevStep,
variant,
}: {
nextStep: () => void;
prevStep: () => void;
variant: 'onboarding' | 'alone';
}) {
const { l10n } = useLocalization();
const { control, handleSubmit, setValue } = useForm<HeightForm>();
const [fetchedHeight, setFetchedHeight] = useState(false);
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const { timer, isCounting, startCountdown } = useCountdown({
duration: 3,
onCountdownEnd: () => {
setFetchedHeight(true);
sendRPCPacket(RpcMessage.HeightRequest, new HeightRequestT());
},
});
const { currentLocales } = useLocaleConfig();
const mFormat = useMemo(
() =>
new Intl.NumberFormat(currentLocales, {
style: 'unit',
unit: 'meter',
maximumFractionDigits: 2,
}),
[currentLocales]
);
const sFormat = useMemo(
() => new Intl.RelativeTimeFormat(currentLocales, { style: 'short' }),
[currentLocales]
);
useRPCPacket(
RpcMessage.HeightResponse,
({ hmdHeight, estimatedFullHeight }: HeightResponseT) => {
setValue('height', estimatedFullHeight || DEFAULT_HEIGHT);
setValue('hmdHeight', hmdHeight);
}
);
const onSubmit = (values: HeightForm) => {
const changeSettings = new ChangeSettingsRequestT();
const autobone = new AutoBoneSettingsT();
autobone.targetFullHeight = values.height;
autobone.targetHmdHeight = values.hmdHeight;
changeSettings.autoBoneSettings = autobone;
sendRPCPacket(RpcMessage.ChangeSettingsRequest, changeSettings);
nextStep();
};
return (
<>
<div className="flex flex-col flex-grow">
<div className="flex flex-grow flex-col gap-4">
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-automatic_proportions-check_height-title'
)}
</Typography>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-automatic_proportions-check_height-description'
)}
</Typography>
<Localized
id="onboarding-automatic_proportions-check_height-calculation_warning"
elems={{ u: <span className="underline"></span> }}
>
<Typography color="secondary" bold>
Press the button to get your height!
</Typography>
</Localized>
<Button
variant="primary"
className="mt-2"
onClick={startCountdown}
disabled={isCounting}
>
{isCounting
? sFormat.format(timer, 'second')
: l10n.getString(
'onboarding-automatic_proportions-check_height-fetch_height'
)}
</Button>
</div>
<form className="flex flex-col self-center items-center justify-center">
<NumberSelector
control={control}
name="height"
label={l10n.getString(
'onboarding-automatic_proportions-check_height-height'
)}
valueLabelFormat={(value) =>
isNaN(value)
? l10n.getString(
'onboarding-automatic_proportions-check_height-unknown'
)
: mFormat.format(value)
}
min={MIN_HEIGHT}
max={4}
step={0.01}
/>
<NumberSelector
control={control}
name="hmdHeight"
label={l10n.getString(
'onboarding-automatic_proportions-check_height-hmd_height'
)}
valueLabelFormat={(value) =>
isNaN(value)
? l10n.getString(
'onboarding-automatic_proportions-check_height-unknown'
)
: mFormat.format(value)
}
min={MIN_HEIGHT}
max={4}
step={0.01}
disabled={true}
/>
</form>
</div>
<div className="flex gap-3 mobile:justify-between">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
onClick={prevStep}
>
{l10n.getString('onboarding-automatic_proportions-prev_step')}
</Button>
<Button
variant="primary"
onClick={handleSubmit(onSubmit)}
disabled={!fetchedHeight}
>
{l10n.getString(
'onboarding-automatic_proportions-check_height-next_step'
)}
</Button>
</div>
</div>
</>
);
}

View File

@@ -12,6 +12,7 @@ import { BodyAssignment } from '../../BodyAssignment';
import { MountingSelectionMenu } from './MountingSelectionMenu';
import { useLocalization } from '@fluent/react';
import { useBreakpoint } from '../../../../hooks/breakpoint';
import { Quaternion } from 'three';
export function ManualMountingPage() {
const { isMobile } = useBreakpoint('mobile');
@@ -41,7 +42,7 @@ export function ManualMountingPage() {
[assignedTrackers]
);
const onDirectionSelected = (mountingOrientationDegrees: number) => {
const onDirectionSelected = (mountingOrientationDegrees: Quaternion) => {
(trackerPartGrouped[selectedRole] || []).forEach((td) => {
const assignreq = new AssignTrackerRequestT();
@@ -52,6 +53,7 @@ export function ManualMountingPage() {
assignreq.trackerId = td.tracker.trackerId;
assignreq.allowDriftCompensation =
td.tracker.info?.allowDriftCompensation ?? true;
assignreq.accessoryId = td.tracker.info?.accessoryId || 0;
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
});
@@ -62,6 +64,7 @@ export function ManualMountingPage() {
return (
<>
<MountingSelectionMenu
bodyPart={selectedRole}
isOpen={selectedRole !== BodyPart.NONE}
onClose={() => setSelectRole(BodyPart.NONE)}
onDirectionSelected={onDirectionSelected}

View File

@@ -87,7 +87,7 @@ export function MountingChoose() {
<img
onMouseEnter={() => setAnimated(() => true)}
onAnimationEnd={() => setAnimated(() => false)}
src="/images/boxslime.png"
src="/images/boxslime.webp"
className={classNames(
'absolute w-[100px] -right-2 -top-10',
animated && 'animate-[bounce_1s_1]'

View File

@@ -3,31 +3,156 @@ import { MouseEventHandler } from 'react';
import ReactModal from 'react-modal';
import { useElemSize, useLayout } from '../../../../hooks/layout';
import { Button } from '../../../commons/Button';
import { AnkleIcon } from '../../../commons/icon/AnkleIcon';
import { Typography } from '../../../commons/Typography';
import { rotationToQuatMap } from '../../../tracker/TrackerSettings';
import { useLocalization } from '@fluent/react';
import { FootIcon } from '../../../commons/icon/FootIcon';
import { rotationToQuatMap } from '../../../../maths/quaternion';
import { Quaternion } from 'three';
import { SlimeUpIcon } from '../../../commons/icon/SlimeUpIcon';
import { BodyPart } from 'solarxr-protocol';
import { PawIcon } from '../../../commons/icon/PawIcon';
import { useLocaleConfig } from '../../../../i18n/config';
function MoutingOrientationCard({
orientation,
onClick,
// All body parts that are right or left, are by default left!
export const mapPart: Record<
BodyPart,
({
width,
currentLocales,
}: {
width?: number;
currentLocales: string[];
}) => JSX.Element
> = {
[BodyPart.CHEST]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.HEAD]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.HIP]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.LEFT_FOOT]: ({ width, currentLocales }) =>
currentLocales.includes('en-x-owo') ? (
<PawIcon
width={width ? width * 0.75 : undefined}
transform="translate(40, -50)"
></PawIcon>
) : (
<FootIcon width={width}></FootIcon>
),
[BodyPart.LEFT_HAND]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.LEFT_LOWER_ARM]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.LEFT_LOWER_LEG]: ({ width, currentLocales }) =>
currentLocales.includes('en-x-owo') ? (
<PawIcon
width={width ? width * 0.75 : undefined}
transform="translate(40, -50)"
></PawIcon>
) : (
<FootIcon width={width}></FootIcon>
),
[BodyPart.LEFT_SHOULDER]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.LEFT_UPPER_ARM]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.LEFT_UPPER_LEG]: ({ width, currentLocales }) =>
currentLocales.includes('en-x-owo') ? (
<PawIcon
width={width ? width * 0.75 : undefined}
transform="translate(40, -50)"
></PawIcon>
) : (
<FootIcon width={width}></FootIcon>
),
[BodyPart.NECK]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.NONE]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.RIGHT_FOOT]: ({ width, currentLocales }) =>
currentLocales.includes('en-x-owo') ? (
<PawIcon
width={width ? width * 0.75 : undefined}
transform="translate(40, -50)"
></PawIcon>
) : (
<FootIcon width={width} flipped></FootIcon>
),
[BodyPart.RIGHT_HAND]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.RIGHT_LOWER_ARM]: ({ width }) => (
<FootIcon width={width}></FootIcon>
),
[BodyPart.RIGHT_LOWER_LEG]: ({ width, currentLocales }) =>
currentLocales.includes('en-x-owo') ? (
<PawIcon
width={width ? width * 0.75 : undefined}
transform="translate(40, -50)"
></PawIcon>
) : (
<FootIcon width={width} flipped></FootIcon>
),
[BodyPart.RIGHT_SHOULDER]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.RIGHT_UPPER_ARM]: ({ width }) => (
<FootIcon width={width}></FootIcon>
),
[BodyPart.RIGHT_UPPER_LEG]: ({ width, currentLocales }) =>
currentLocales.includes('en-x-owo') ? (
<PawIcon
width={width ? width * 0.75 : undefined}
transform="translate(40, -50)"
></PawIcon>
) : (
<FootIcon width={width} flipped></FootIcon>
),
[BodyPart.WAIST]: ({ width }) => <FootIcon width={width}></FootIcon>,
[BodyPart.ACCESSORY]: ({ width }) => <FootIcon width={width}></FootIcon>,
};
export function MountingBodyPartIcon({
bodyPart = BodyPart.NONE,
width = 24,
}: {
orientation: string;
onClick?: MouseEventHandler<HTMLDivElement>;
bodyPart?: BodyPart;
width?: number;
}) {
// FIXME: Dont use AnkleIcon for this please
const { currentLocales } = useLocaleConfig();
return mapPart[bodyPart]({ width, currentLocales });
}
function PieSliceOfFeet({
onClick,
id,
d,
noText = false,
trackerTransform,
trackerWidth = 10,
}: {
onClick?: MouseEventHandler<SVGGElement>;
id: string;
d: string;
noText?: boolean;
trackerTransform: string;
trackerWidth?: number;
}) {
const { l10n } = useLocalization();
return (
<div
<g
onClick={onClick}
className="xs:h-32 mobile:h-20 bg-background-60 rounded-md flex justify-between p-4 hover:bg-background-50"
className={classNames('group fill-background-10 stroke-background-10')}
>
<div className="flex flex-col justify-center">
<Typography variant="main-title">{orientation}</Typography>
</div>
<div className="flex flex-col justify-center fill-white">
<AnkleIcon width={58}></AnkleIcon>
</div>
</div>
<path
d={d}
className={classNames(
'fill-background-40 opacity-50 stroke-background-90',
'group-hover:fill-background-30 group-active:fill-background-20'
)}
transform="translate(125 125)"
id={id}
></path>
<text dy="-5" strokeWidth="1">
<textPath xlinkHref={`#${id}`} startOffset="50%" textAnchor="middle">
{!noText ? l10n.getString(id) : ''}
</textPath>
</text>
<g
transform={trackerTransform}
className="fill-none stroke-none group-hover:fill-accent-background-20"
>
<SlimeUpIcon width={trackerWidth}></SlimeUpIcon>
</g>
</g>
);
}
@@ -35,10 +160,12 @@ export function MountingSelectionMenu({
isOpen = true,
onClose,
onDirectionSelected,
bodyPart,
}: {
isOpen: boolean;
onClose: () => void;
onDirectionSelected: (direction: number) => void;
onDirectionSelected: (direction: Quaternion) => void;
bodyPart?: BodyPart;
}) {
const { l10n } = useLocalization();
const { ref: refTrackers, layoutHeight: trackersHeight } =
@@ -68,23 +195,82 @@ export function MountingSelectionMenu({
ref={refTrackers}
style={{ height: trackersHeight - optionsHeight }}
>
<div className="grid xs:grid-cols-2 xs:grid-rows-2 mobile:grid-cols-1 gap-6 w-full">
<MoutingOrientationCard
orientation={l10n.getString('tracker-rotation-left')}
onClick={() => onDirectionSelected(rotationToQuatMap.LEFT)}
/>
<MoutingOrientationCard
orientation={l10n.getString('tracker-rotation-right')}
onClick={() => onDirectionSelected(rotationToQuatMap.RIGHT)}
/>
<MoutingOrientationCard
orientation={l10n.getString('tracker-rotation-front')}
onClick={() => onDirectionSelected(rotationToQuatMap.FRONT)}
/>
<MoutingOrientationCard
orientation={l10n.getString('tracker-rotation-back')}
onClick={() => onDirectionSelected(rotationToQuatMap.BACK)}
/>
<div className="flex justify-center items-center gap-6 w-full">
<svg
width="400"
viewBox="0 0 250 250"
className="fill-background-40"
>
<g transform="translate(80, 0)" className="fill-background-10">
<MountingBodyPartIcon width={100} bodyPart={bodyPart} />
</g>
<g strokeWidth="4" className="stroke-background-90">
<PieSliceOfFeet
d="M0 0-89 44A99 99 0 0 1-89-44Z"
onClick={() => onDirectionSelected(rotationToQuatMap.LEFT)}
id="tracker-rotation-left"
trackerTransform="translate(75, 0) scale(-1, 1)"
></PieSliceOfFeet>
<PieSliceOfFeet
d="M0 0-89-44A99 99 0 0 1-44-89Z"
onClick={() =>
onDirectionSelected(rotationToQuatMap.FRONT_LEFT)
}
id="tracker-rotation_left_front"
noText={true}
trackerTransform="translate(-2, 175) rotate(-135)"
trackerWidth={7}
></PieSliceOfFeet>
<PieSliceOfFeet
onClick={() => onDirectionSelected(rotationToQuatMap.FRONT)}
d="M0 0-44-89A99 99 0 0 1 44-89Z"
id="tracker-rotation-front"
trackerTransform="translate(0, 75) rotate(-90)"
></PieSliceOfFeet>
<PieSliceOfFeet
d="M0 0 44-89A99 99 0 0 1 89-44Z"
onClick={() =>
onDirectionSelected(rotationToQuatMap.FRONT_RIGHT)
}
id="tracker-rotation-front_right"
noText={true}
trackerTransform="translate(73, 0) rotate(-45)"
trackerWidth={7}
></PieSliceOfFeet>
<PieSliceOfFeet
d="M0 0 89-44A99 99 0 0 1 89 44Z"
onClick={() => onDirectionSelected(rotationToQuatMap.RIGHT)}
id="tracker-rotation-right"
trackerTransform="translate(175,0)"
></PieSliceOfFeet>
<PieSliceOfFeet
d="M0 0 89 44A99 99 0 0 1 44 89Z"
onClick={() =>
onDirectionSelected(rotationToQuatMap.BACK_RIGHT)
}
id="tracker-rotation-back_right"
noText={true}
trackerTransform="translate(252, 75) rotate(45)"
trackerWidth={7}
></PieSliceOfFeet>
<PieSliceOfFeet
d="M0 0 44 89A99 99 0 0 1-44 89Z"
onClick={() => onDirectionSelected(rotationToQuatMap.BACK)}
id="tracker-rotation-back"
trackerTransform="translate(250, 175) rotate(90)"
></PieSliceOfFeet>
<PieSliceOfFeet
d="M0 0-44 89A99 99 0 0 1-89 44Z"
onClick={() =>
onDirectionSelected(rotationToQuatMap.BACK_LEFT)
}
id="tracker-rotation-back_left"
noText={true}
trackerTransform="translate(177, 250) rotate(135)"
trackerWidth={7}
></PieSliceOfFeet>
</g>
</svg>
</div>
</div>
</div>

View File

@@ -43,7 +43,7 @@ export function MountingResetStep({
{isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img
src="/images/mounting-reset-pose.png"
src="/images/mounting-reset-pose.webp"
width={125}
alt="mounting reset ski pose"
/>
@@ -67,7 +67,7 @@ export function MountingResetStep({
{!isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img
src="/images/mounting-reset-pose.png"
src="/images/mounting-reset-pose.webp"
width={125}
alt="mounting reset ski pose"
/>

View File

@@ -40,7 +40,7 @@ export function PreparationStep({
{isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img
src="/images/reset-pose.png"
src="/images/reset-pose.webp"
width={100}
alt="Reset position"
/>
@@ -62,7 +62,7 @@ export function PreparationStep({
</div>
{!isMobile && (
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
<img src="/images/reset-pose.png" width={90} alt="Reset position" />
<img src="/images/reset-pose.webp" width={90} alt="Reset position" />
</div>
)}
</div>

View File

@@ -192,6 +192,7 @@ export function TrackersAssignPage() {
assignreq.trackerId = trackerId;
assignreq.allowDriftCompensation =
tracker?.tracker?.info?.allowDriftCompensation ?? true;
assignreq.accessoryId = 1; // TODO
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
};

View File

@@ -20,6 +20,10 @@ export function SettingSelectorMobile() {
label: l10n.getString('settings-sidebar-general'),
value: { url: '/settings/trackers', scrollTo: 'steamvr' },
},
{
label: l10n.getString('settings-sidebar-interface'),
value: { url: '/settings/interface', scrollTo: 'appearance' },
},
{
label: l10n.getString('settings-sidebar-osc_router'),
value: { url: '/settings/osc/router', scrollTo: 'router' },

View File

@@ -1,6 +1,7 @@
import classNames from 'classnames';
import { ReactNode, useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { useBreakpoint } from '../../hooks/breakpoint';
export function SettingsPageLayout({
children,
@@ -11,6 +12,7 @@ export function SettingsPageLayout({
} & React.HTMLAttributes<HTMLDivElement>) {
const pageRef = useRef<HTMLDivElement | null>(null);
const { state } = useLocation();
const { isMobile } = useBreakpoint('mobile');
useEffect(() => {
const typedState: { scrollTo: string } = state;
@@ -27,8 +29,12 @@ export function SettingsPageLayout({
'.overflow-y-auto'
) as HTMLElement | null;
if (closestScroll) {
// The 45 is just enough padding for making the scroll look perfect
closestScroll.scroll({ top: elem.offsetTop - 45, behavior: 'smooth' });
// The 40 is just enough padding for making the scroll look perfect
const topPadding = isMobile ? 80 : 40;
closestScroll.scroll({
top: elem.offsetTop - topPadding,
behavior: 'smooth',
});
}
}
}, [state]);

View File

@@ -63,33 +63,43 @@ export function SettingsSidebar() {
<SettingsLink to="/settings/trackers" scrollTo="gestureControl">
{l10n.getString('settings-sidebar-gesture_control')}
</SettingsLink>
<SettingsLink to="/settings/trackers" scrollTo="interface">
{l10n.getString('settings-sidebar-interface')}
</SettingsLink>
</div>
</div>
<div className="flex flex-col gap-3">
<Typography variant="section-title">OSC</Typography>
<div className="flex flex-col gap-2">
<SettingsLink to="/settings/osc/router" scrollTo="router">
{l10n.getString('settings-sidebar-osc_router')}
</SettingsLink>
<SettingsLink to="/settings/osc/vrchat" scrollTo="vrchat">
{l10n.getString('settings-sidebar-osc_trackers')}
</SettingsLink>
<SettingsLink to="/settings/osc/vmc" scrollTo="vmc">
VMC
</SettingsLink>
</div>
</div>
<div className="flex flex-col gap-3">
<Typography variant="section-title">
{l10n.getString('settings-sidebar-utils')}
{l10n.getString('settings-sidebar-interface')}
</Typography>
<div className="flex flex-col gap-2">
<SettingsLink to="/settings/serial">
{l10n.getString('settings-sidebar-serial')}
<SettingsLink to="/settings/interface" scrollTo="notifications">
{l10n.getString('settings-sidebar-notifications')}
</SettingsLink>
<SettingsLink to="/settings/interface" scrollTo="appearance">
{l10n.getString('settings-sidebar-appearance')}
</SettingsLink>
</div>
<div className="flex flex-col gap-3">
<Typography variant="section-title">OSC</Typography>
<div className="flex flex-col gap-2">
<SettingsLink to="/settings/osc/router" scrollTo="router">
{l10n.getString('settings-sidebar-osc_router')}
</SettingsLink>
<SettingsLink to="/settings/osc/vrchat" scrollTo="vrchat">
{l10n.getString('settings-sidebar-osc_trackers')}
</SettingsLink>
<SettingsLink to="/settings/osc/vmc" scrollTo="vmc">
VMC
</SettingsLink>
</div>
</div>
<div className="flex flex-col gap-3">
<Typography variant="section-title">
{l10n.getString('settings-sidebar-utils')}
</Typography>
<div className="flex flex-col gap-2">
<SettingsLink to="/settings/serial">
{l10n.getString('settings-sidebar-serial')}
</SettingsLink>
</div>
</div>
</div>
</div>

View File

@@ -7,6 +7,7 @@ import {
FilteringSettingsT,
FilteringType,
LegTweaksSettingsT,
ModelRatiosT,
ModelSettingsT,
ModelTogglesT,
RpcMessage,
@@ -19,13 +20,10 @@ import { useConfig } from '../../../hooks/config';
import { useWebsocketAPI } from '../../../hooks/websocket-api';
import { useLocaleConfig } from '../../../i18n/config';
import { CheckBox } from '../../commons/Checkbox';
import { SquaresIcon } from '../../commons/icon/SquaresIcon';
import { SteamIcon } from '../../commons/icon/SteamIcon';
import { WrenchIcon } from '../../commons/icon/WrenchIcons';
import { LangSelector } from '../../commons/LangSelector';
import { NumberSelector } from '../../commons/NumberSelector';
import { Radio } from '../../commons/Radio';
import { ThemeSelector } from '../../commons/ThemeSelector';
import { Typography } from '../../commons/Typography';
import {
SettingsPageLayout,
@@ -60,6 +58,15 @@ interface SettingsForm {
viveEmulation: boolean;
toeSnap: boolean;
footPlant: boolean;
selfLocalization: boolean;
};
ratios: {
imputeWaistFromChestHip: number;
imputeWaistFromChestLegs: number;
imputeHipFromChestLegs: number;
imputeHipFromWaistLegs: number;
interpHipLegs: number;
interpKneeTrackerAnkle: number;
};
tapDetection: {
mountingResetEnabled: boolean;
@@ -71,17 +78,11 @@ interface SettingsForm {
yawResetTaps: number;
fullResetTaps: number;
mountingResetTaps: number;
numberTrackersOverThreshold;
};
legTweaks: {
correctionStrength: number;
};
interface: {
devmode: boolean;
watchNewDevices: boolean;
feedbackSound: boolean;
feedbackSoundVolume: number;
theme: string;
};
}
const defaultValues = {
@@ -103,6 +104,15 @@ const defaultValues = {
viveEmulation: false,
toeSnap: false,
flootPlant: true,
selfLocalization: false,
},
ratios: {
imputeWaistFromChestHip: 0.3,
imputeWaistFromChestLegs: 0.2,
imputeHipFromChestLegs: 0.45,
imputeHipFromWaistLegs: 0.4,
interpHipLegs: 0.25,
interpKneeTrackerAnkle: 0.85,
},
filtering: { amount: 0.1, type: FilteringType.NONE },
driftCompensation: {
@@ -120,25 +130,19 @@ const defaultValues = {
yawResetTaps: 2,
fullResetTaps: 3,
mountingResetTaps: 3,
numberTrackersOverThreshold: 1,
},
legTweaks: { correctionStrength: 0.3 },
interface: {
devmode: false,
watchNewDevices: true,
feedbackSound: true,
feedbackSoundVolume: 0.5,
theme: 'slime',
},
};
export function GeneralSettings() {
const { l10n } = useLocalization();
const { config, setConfig } = useConfig();
const { config } = useConfig();
// const { state } = useLocation();
const { currentLocales } = useLocaleConfig();
// const pageRef = useRef<HTMLFormElement | null>(null);
const percentageFormat = Intl.NumberFormat(currentLocales, {
const percentageFormat = new Intl.NumberFormat(currentLocales, {
style: 'percent',
maximumFractionDigits: 0,
});
@@ -163,22 +167,44 @@ export function GeneralSettings() {
}
const modelSettings = new ModelSettingsT();
const toggles = new ModelTogglesT();
toggles.floorClip = values.toggles.floorClip;
toggles.skatingCorrection = values.toggles.skatingCorrection;
toggles.extendedKnee = values.toggles.extendedKnee;
toggles.extendedPelvis = values.toggles.extendedPelvis;
toggles.extendedSpine = values.toggles.extendedSpine;
toggles.forceArmsFromHmd = values.toggles.forceArmsFromHmd;
toggles.viveEmulation = values.toggles.viveEmulation;
toggles.toeSnap = values.toggles.toeSnap;
toggles.footPlant = values.toggles.footPlant;
const legTweaks = new LegTweaksSettingsT();
legTweaks.correctionStrength = values.legTweaks.correctionStrength;
if (values.toggles) {
const toggles = new ModelTogglesT();
toggles.floorClip = values.toggles.floorClip;
toggles.skatingCorrection = values.toggles.skatingCorrection;
toggles.extendedKnee = values.toggles.extendedKnee;
toggles.extendedPelvis = values.toggles.extendedPelvis;
toggles.extendedSpine = values.toggles.extendedSpine;
toggles.forceArmsFromHmd = values.toggles.forceArmsFromHmd;
toggles.viveEmulation = values.toggles.viveEmulation;
toggles.toeSnap = values.toggles.toeSnap;
toggles.footPlant = values.toggles.footPlant;
toggles.selfLocalization = values.toggles.selfLocalization;
modelSettings.toggles = toggles;
}
if (values.ratios) {
const ratios = new ModelRatiosT();
ratios.imputeWaistFromChestHip =
values.ratios.imputeWaistFromChestHip || -1;
ratios.imputeWaistFromChestLegs =
values.ratios.imputeWaistFromChestLegs || -1;
ratios.imputeHipFromChestLegs =
values.ratios.imputeHipFromChestLegs || -1;
ratios.imputeHipFromWaistLegs =
values.ratios.imputeHipFromWaistLegs || -1;
ratios.interpHipLegs = values.ratios.interpHipLegs || -1;
ratios.interpKneeTrackerAnkle =
values.ratios.interpKneeTrackerAnkle || -1;
modelSettings.ratios = ratios;
}
if (values.legTweaks) {
const legTweaks = new LegTweaksSettingsT();
legTweaks.correctionStrength = values.legTweaks.correctionStrength;
modelSettings.legTweaks = legTweaks;
}
modelSettings.toggles = toggles;
modelSettings.legTweaks = legTweaks;
settings.modelSettings = modelSettings;
const tapDetection = new TapDetectionSettingsT();
@@ -192,6 +218,8 @@ export function GeneralSettings() {
values.tapDetection.mountingResetEnabled;
tapDetection.mountingResetDelay = values.tapDetection.mountingResetDelay;
tapDetection.mountingResetTaps = values.tapDetection.mountingResetTaps;
tapDetection.numberTrackersOverThreshold =
values.tapDetection.numberTrackersOverThreshold;
tapDetection.setupMode = false;
settings.tapDetectionSettings = tapDetection;
@@ -207,14 +235,6 @@ export function GeneralSettings() {
settings.driftCompensation = driftCompensation;
sendRPCPacket(RpcMessage.ChangeSettingsRequest, settings);
setConfig({
debug: values.interface.devmode,
watchNewDevices: values.interface.watchNewDevices,
feedbackSound: values.interface.feedbackSound,
feedbackSoundVolume: values.interface.feedbackSoundVolume,
theme: values.interface.theme,
});
};
useEffect(() => {
@@ -227,15 +247,7 @@ export function GeneralSettings() {
}, []);
useRPCPacket(RpcMessage.SettingsResponse, (settings: SettingsResponseT) => {
const formData: DefaultValues<SettingsForm> = {
interface: {
devmode: config?.debug,
watchNewDevices: config?.watchNewDevices,
feedbackSound: config?.feedbackSound,
feedbackSoundVolume: config?.feedbackSoundVolume,
theme: config?.theme,
},
};
const formData: DefaultValues<SettingsForm> = {};
if (settings.filtering) {
formData.filtering = settings.filtering;
@@ -262,6 +274,19 @@ export function GeneralSettings() {
);
}
if (settings.modelSettings?.ratios) {
formData.ratios = Object.keys(settings.modelSettings?.ratios).reduce(
(curr, key: string) => ({
...curr,
[key]:
(settings.modelSettings?.ratios &&
(settings.modelSettings.ratios as any)[key]) ||
0.0,
}),
{}
);
}
if (settings.tapDetectionSettings) {
formData.tapDetection = {
yawResetEnabled:
@@ -291,6 +316,9 @@ export function GeneralSettings() {
mountingResetTaps:
settings.tapDetectionSettings.mountingResetTaps ||
defaultValues.tapDetection.mountingResetTaps,
numberTrackersOverThreshold:
settings.tapDetectionSettings.numberTrackersOverThreshold ||
defaultValues.tapDetection.numberTrackersOverThreshold,
};
}
@@ -650,7 +678,7 @@ export function GeneralSettings() {
<div className="flex flex-col pt-2 pb-3">
<Typography bold>
{l10n.getString(
'settings-general-fk_settings-skeleton_settings'
'settings-general-fk_settings-skeleton_settings-toggles'
)}
</Typography>
<Typography color="secondary">
@@ -666,7 +694,7 @@ export function GeneralSettings() {
control={control}
name="toggles.extendedSpine"
label={l10n.getString(
'settings-general-fk_settings-skeleton_settings-extended_spine'
'settings-general-fk_settings-skeleton_settings-extended_spine_model'
)}
/>
<CheckBox
@@ -675,7 +703,7 @@ export function GeneralSettings() {
control={control}
name="toggles.extendedPelvis"
label={l10n.getString(
'settings-general-fk_settings-skeleton_settings-extended_pelvis'
'settings-general-fk_settings-skeleton_settings-extended_pelvis_model'
)}
/>
<CheckBox
@@ -684,10 +712,105 @@ export function GeneralSettings() {
control={control}
name="toggles.extendedKnee"
label={l10n.getString(
'settings-general-fk_settings-skeleton_settings-extended_knees'
'settings-general-fk_settings-skeleton_settings-extended_knees_model'
)}
/>
</div>
<div className="flex flex-col pt-2 pb-3">
<div className="flex flex-col pt-2 pb-3">
<Typography bold>
{l10n.getString(
'settings-general-fk_settings-skeleton_settings-ratios'
)}
</Typography>
<Typography color="secondary">
{l10n.getString(
'settings-general-fk_settings-skeleton_settings-ratios-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 gap-3 pb-3">
<NumberSelector
control={control}
name="ratios.imputeWaistFromChestHip"
label={l10n.getString(
'settings-general-fk_settings-skeleton_settings-impute_waist_from_chest_hip'
)}
valueLabelFormat={(value) =>
percentageFormat.format(value)
}
min={0.0}
max={1.0}
step={0.05}
/>
<NumberSelector
control={control}
name="ratios.imputeWaistFromChestLegs"
label={l10n.getString(
'settings-general-fk_settings-skeleton_settings-impute_waist_from_chest_legs'
)}
valueLabelFormat={(value) =>
percentageFormat.format(value)
}
min={0.0}
max={1.0}
step={0.05}
/>
<NumberSelector
control={control}
name="ratios.imputeHipFromChestLegs"
label={l10n.getString(
'settings-general-fk_settings-skeleton_settings-impute_hip_from_chest_legs'
)}
valueLabelFormat={(value) =>
percentageFormat.format(value)
}
min={0.0}
max={1.0}
step={0.05}
/>
<NumberSelector
control={control}
name="ratios.imputeHipFromWaistLegs"
label={l10n.getString(
'settings-general-fk_settings-skeleton_settings-impute_hip_from_waist_legs'
)}
valueLabelFormat={(value) =>
percentageFormat.format(value)
}
min={0.0}
max={1.0}
step={0.05}
/>
<NumberSelector
control={control}
name="ratios.interpHipLegs"
label={l10n.getString(
'settings-general-fk_settings-skeleton_settings-interp_hip_legs'
)}
valueLabelFormat={(value) =>
percentageFormat.format(value)
}
min={0.0}
max={1.0}
step={0.05}
/>
<NumberSelector
control={control}
name="ratios.interpKneeTrackerAnkle"
label={l10n.getString(
'settings-general-fk_settings-skeleton_settings-interp_knee_tracker_ankle'
)}
valueLabelFormat={(value) =>
percentageFormat.format(value)
}
min={0.0}
max={1.0}
step={0.05}
/>
</div>
</div>
<div className="flex flex-col pt-2 pb-3">
<Typography bold>
{l10n.getString(
@@ -711,6 +834,29 @@ export function GeneralSettings() {
)}
/>
</div>
<div className="flex flex-col pt-2 pb-3">
<Typography bold>
{l10n.getString(
'settings-general-fk_settings-self_localization-title'
)}
</Typography>
<Typography color="secondary">
{l10n.getString(
'settings-general-fk_settings-self_localization-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-1 gap3 pb5">
<CheckBox
variant="toggle"
outlined
control={control}
name="toggles.selfLocalization"
label={l10n.getString(
'settings-general-fk_settings-self_localization-title'
)}
/>
</div>
</>
)}
</>
@@ -843,162 +989,35 @@ export function GeneralSettings() {
step={1}
/>
</div>
</>
</SettingsPagePaneLayout>
<SettingsPagePaneLayout
icon={<SquaresIcon></SquaresIcon>}
id="interface"
>
<>
<Typography variant="main-title">
{l10n.getString('settings-general-interface')}
</Typography>
<Typography bold>
{l10n.getString('settings-general-interface-dev_mode')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-general-interface-dev_mode-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="interface.devmode"
label={l10n.getString(
'settings-general-interface-dev_mode-label'
)}
/>
</div>
<Typography bold>
{l10n.getString('settings-general-interface-serial_detection')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-general-interface-serial_detection-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="interface.watchNewDevices"
label={l10n.getString(
'settings-general-interface-serial_detection-label'
)}
/>
</div>
<Typography bold>
{l10n.getString('settings-general-interface-feedback_sound')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-general-interface-feedback_sound-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="interface.feedbackSound"
label={l10n.getString(
'settings-general-interface-feedback_sound-label'
)}
/>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<NumberSelector
control={control}
name="interface.feedbackSoundVolume"
label={l10n.getString(
'settings-general-interface-feedback_sound-volume'
)}
valueLabelFormat={(value) => percentageFormat.format(value)}
min={0.1}
max={1.0}
step={0.1}
/>
</div>
<div className="pb-4">
<Typography bold>
{l10n.getString('settings-general-interface-theme')}
</Typography>
<div className="flex flex-wrap gap-3 pt-2">
<ThemeSelector
{config?.debug && (
<div className="grid sm:grid-cols-1 gap-2 pt-2">
<Typography bold>
{l10n.getString(
'settings-general-gesture_control-numberTrackersOverThreshold'
)}
</Typography>
<Typography color="secondary">
{l10n.getString(
'settings-general-gesture_control-numberTrackersOverThreshold-description'
)}
</Typography>
<NumberSelector
control={control}
name="interface.theme"
value={'slime'}
colors="!bg-slime"
></ThemeSelector>
<ThemeSelector
control={control}
name="interface.theme"
value={'slime-green'}
colors="!bg-slime-green"
></ThemeSelector>
<ThemeSelector
control={control}
name="interface.theme"
value={'slime-yellow'}
colors="!bg-slime-yellow"
></ThemeSelector>
<ThemeSelector
control={control}
name="interface.theme"
value={'slime-orange'}
colors="!bg-slime-orange"
></ThemeSelector>
<ThemeSelector
control={control}
name="interface.theme"
value={'slime-red'}
colors="!bg-slime-red"
></ThemeSelector>
<ThemeSelector
control={control}
name="interface.theme"
value={'dark'}
colors="!bg-dark"
></ThemeSelector>
<ThemeSelector
control={control}
name="interface.theme"
value={'light'}
colors="!bg-light"
></ThemeSelector>
<ThemeSelector
control={control}
name="interface.theme"
value={'trans'}
colors="!bg-trans-flag"
></ThemeSelector>
name="tapDetection.numberTrackersOverThreshold"
valueLabelFormat={(value) =>
l10n.getString(
'settings-general-gesture_control-trackers',
{
amount: Math.round(value),
}
)
}
min={1}
max={20}
step={1}
/>
</div>
</div>
<Typography bold>
{l10n.getString('settings-general-interface-lang')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString('settings-general-interface-lang-description')}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<LangSelector alignment="left" />
</div>
)}
</>
</SettingsPagePaneLayout>
</form>

View File

@@ -0,0 +1,338 @@
import { useLocalization } from '@fluent/react';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { CheckBox } from '../../commons/Checkbox';
import { Typography } from '../../commons/Typography';
import {
SettingsPageLayout,
SettingsPagePaneLayout,
} from '../SettingsPageLayout';
import { defaultConfig, useConfig } from '../../../hooks/config';
import { ThemeSelector } from '../../commons/ThemeSelector';
import { SquaresIcon } from '../../commons/icon/SquaresIcon';
import { NumberSelector } from '../../commons/NumberSelector';
import { useLocaleConfig } from '../../../i18n/config';
import { LangSelector } from '../../commons/LangSelector';
import { BellIcon } from '../../commons/icon/BellIcon';
import { Range } from '../../commons/Range';
import { Dropdown } from '../../commons/Dropdown';
interface InterfaceSettingsForm {
appearance: {
devmode: boolean;
theme: string;
textSize: number;
fonts: string;
};
notifications: {
watchNewDevices: boolean;
feedbackSound: boolean;
feedbackSoundVolume: number;
};
}
export function InterfaceSettings() {
const { currentLocales } = useLocaleConfig();
const { l10n } = useLocalization();
const { config, setConfig } = useConfig();
const { control, watch, handleSubmit, getValues } =
useForm<InterfaceSettingsForm>({
defaultValues: {
appearance: {
devmode: config?.debug ?? defaultConfig.debug,
theme: config?.theme ?? defaultConfig.theme,
textSize: config?.textSize ?? defaultConfig.textSize,
fonts: config?.fonts.join(',') ?? defaultConfig.fonts.join(','),
},
notifications: {
watchNewDevices:
config?.watchNewDevices ?? defaultConfig.watchNewDevices,
feedbackSound: config?.feedbackSound ?? defaultConfig.feedbackSound,
feedbackSoundVolume:
config?.feedbackSoundVolume ?? defaultConfig.feedbackSoundVolume,
},
},
});
const onSubmit = (values: InterfaceSettingsForm) => {
setConfig({
debug: values.appearance.devmode,
watchNewDevices: values.notifications.watchNewDevices,
feedbackSound: values.notifications.feedbackSound,
feedbackSoundVolume: values.notifications.feedbackSoundVolume,
theme: values.appearance.theme,
fonts: values.appearance.fonts.split(','),
textSize: values.appearance.textSize,
});
};
const percentageFormat = Intl.NumberFormat(currentLocales, {
style: 'percent',
maximumFractionDigits: 0,
});
useEffect(() => {
const subscription = watch(() => handleSubmit(onSubmit)());
return () => subscription.unsubscribe();
}, []);
return (
<SettingsPageLayout>
<form
className="flex flex-col gap-2 w-full"
style={
{
'--font-size': '12rem',
'--font-size-standard': '12rem',
'--font-size-vr': '16rem',
'--font-size-title': '25rem',
} as React.CSSProperties
}
>
<SettingsPagePaneLayout icon={<BellIcon></BellIcon>} id="notifications">
<>
<Typography variant="main-title">
{l10n.getString('settings-interface-notifications')}
</Typography>
<Typography bold>
{l10n.getString('settings-general-interface-serial_detection')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-general-interface-serial_detection-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="notifications.watchNewDevices"
label={l10n.getString(
'settings-general-interface-serial_detection-label'
)}
/>
</div>
<Typography bold>
{l10n.getString('settings-general-interface-feedback_sound')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-general-interface-feedback_sound-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="notifications.feedbackSound"
label={l10n.getString(
'settings-general-interface-feedback_sound-label'
)}
/>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<NumberSelector
control={control}
name="notifications.feedbackSoundVolume"
label={l10n.getString(
'settings-general-interface-feedback_sound-volume'
)}
valueLabelFormat={(value) => percentageFormat.format(value)}
min={0.1}
max={1.0}
step={0.1}
/>
</div>
</>
</SettingsPagePaneLayout>
<SettingsPagePaneLayout
icon={<SquaresIcon></SquaresIcon>}
id="appearance"
>
<>
<Typography variant="main-title">
{l10n.getString('settings-interface-appearance')}
</Typography>
<Typography bold>
{l10n.getString('settings-general-interface-dev_mode')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-general-interface-dev_mode-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<CheckBox
variant="toggle"
control={control}
outlined
name="appearance.devmode"
label={l10n.getString(
'settings-general-interface-dev_mode-label'
)}
/>
</div>
<div className="pb-4">
<Typography bold>
{l10n.getString('settings-general-interface-theme')}
</Typography>
<div className="flex flex-wrap gap-3 pt-2">
<ThemeSelector
control={control}
name="appearance.theme"
value={'slime'}
colors="!bg-slime"
></ThemeSelector>
<ThemeSelector
control={control}
name="appearance.theme"
value={'slime-green'}
colors="!bg-slime-green"
></ThemeSelector>
<ThemeSelector
control={control}
name="appearance.theme"
value={'slime-yellow'}
colors="!bg-slime-yellow"
></ThemeSelector>
<ThemeSelector
control={control}
name="appearance.theme"
value={'slime-orange'}
colors="!bg-slime-orange"
></ThemeSelector>
<ThemeSelector
control={control}
name="appearance.theme"
value={'slime-red'}
colors="!bg-slime-red"
></ThemeSelector>
<ThemeSelector
control={control}
name="appearance.theme"
value={'dark'}
colors="!bg-dark"
></ThemeSelector>
<ThemeSelector
control={control}
name="appearance.theme"
value={'light'}
colors="!bg-light"
></ThemeSelector>
<ThemeSelector
control={control}
name="appearance.theme"
value={'trans'}
colors="!bg-trans-flag"
></ThemeSelector>
</div>
</div>
<Typography bold>
{l10n.getString('settings-interface-appearance-font')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-interface-appearance-font-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<Dropdown
control={control}
getValues={getValues}
name="appearance.fonts"
placeholder={l10n.getString(
'settings-interface-appearance-font-placeholder'
)}
/* Supports multiple items by separating them with a comma */
items={[
{
label: l10n.getString(
'settings-interface-appearance-font-slime_font'
),
value: 'poppins',
fontName: 'poppins',
},
{
label: 'OpenDyslexic',
value: 'OpenDyslexic',
fontName: 'OpenDyslexic',
},
{ label: 'Lexend', value: 'Lexend', fontName: 'Lexend' },
{ label: 'Ubuntu', value: 'Ubuntu', fontName: 'Ubuntu' },
{
label: 'Noto Sans (CJK)',
value: 'Noto Sans CJK',
fontName: 'Noto Sans CJK',
},
{
label: l10n.getString(
'settings-interface-appearance-font-os_font'
),
value: 'ui-sans-serif',
},
]}
alignment="left"
/>
</div>
<Typography bold>
{l10n.getString('settings-interface-appearance-font_size')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-interface-appearance-font_size-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<Range
control={control}
name="appearance.textSize"
min={10}
max={15}
step={1}
values={[
{ value: 10, label: '10pt' },
{ value: 11, label: '11pt' },
{ value: 12, label: '12pt', defaultValue: true },
{ value: 13, label: '13pt' },
{ value: 14, label: '14pt' },
{ value: 15, label: '15pt' },
]}
/>
</div>
<Typography bold>
{l10n.getString('settings-general-interface-lang')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString('settings-general-interface-lang-description')}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<LangSelector alignment="left" />
</div>
</>
</SettingsPagePaneLayout>
</form>
</SettingsPageLayout>
);
}

View File

@@ -15,11 +15,12 @@ import { FileInput } from '../../commons/FileInput';
import { VMCIcon } from '../../commons/icon/VMCIcon';
import { Input } from '../../commons/Input';
import { Typography } from '../../commons/Typography';
import { magic } from '../../utils/formatting';
import { magic } from '../../../utils/formatting';
import {
SettingsPageLayout,
SettingsPagePaneLayout,
} from '../SettingsPageLayout';
import { error } from '../../../utils/logging';
interface VMCSettingsForm {
vmc: {
@@ -287,7 +288,7 @@ async function parseVRMFile(vrm: File): Promise<string | null> {
let cursor = 0;
const magicBytes = headerView.getUint32(cursor, true);
if (magicBytes !== magic`glTF`) {
console.error(
error(
`.vrm file starts with ${magicBytes.toString(
16
)} instead of ${magic`glTF`.toString(16)}`
@@ -298,7 +299,7 @@ async function parseVRMFile(vrm: File): Promise<string | null> {
const versionNumber = headerView.getUint32(cursor, true);
if (versionNumber !== 2) {
console.error('unsupported glTF version');
error('unsupported glTF version');
return null;
}
cursor += 4;
@@ -310,7 +311,7 @@ async function parseVRMFile(vrm: File): Promise<string | null> {
cursor += 4;
const jsonMagicBytes = headerView.getUint32(cursor, true);
if (jsonMagicBytes !== magic`JSON`) {
console.error(
error(
`first chunk contains ${jsonMagicBytes.toString(
16
)} instead of ${magic`JSON`.toString(16)}`

View File

@@ -14,8 +14,10 @@ import { useDebouncedEffect } from '../../hooks/timeout';
import { useTrackerFromId } from '../../hooks/tracker';
import { useWebsocketAPI } from '../../hooks/websocket-api';
import {
getYawInDegrees,
MountingOrientationDegreesToQuatT,
QuaternionFromQuatT,
rotationToQuatMap,
similarQuaternions,
} from '../../maths/quaternion';
import { ArrowLink } from '../commons/ArrowLink';
import { BodyPartIcon } from '../commons/BodyPartIcon';
@@ -28,20 +30,18 @@ import { MountingSelectionMenu } from '../onboarding/pages/mounting/MountingSele
import { IMUVisualizerWidget } from '../widgets/IMUVisualizerWidget';
import { SingleTrackerBodyAssignmentMenu } from './SingleTrackerBodyAssignmentMenu';
import { TrackerCard } from './TrackerCard';
import { Quaternion } from 'three';
export const rotationToQuatMap = {
FRONT: 180,
LEFT: 90,
RIGHT: -90,
BACK: 0,
};
const rotationsLabels = {
[rotationToQuatMap.BACK]: 'tracker-rotation-back',
[rotationToQuatMap.FRONT]: 'tracker-rotation-front',
[rotationToQuatMap.LEFT]: 'tracker-rotation-left',
[rotationToQuatMap.RIGHT]: 'tracker-rotation-right',
};
const rotationsLabels: [Quaternion, string][] = [
[rotationToQuatMap.BACK, 'tracker-rotation-back'],
[rotationToQuatMap.FRONT, 'tracker-rotation-front'],
[rotationToQuatMap.LEFT, 'tracker-rotation-left'],
[rotationToQuatMap.RIGHT, 'tracker-rotation-right'],
[rotationToQuatMap.BACK_LEFT, 'tracker-rotation-back_left'],
[rotationToQuatMap.BACK_RIGHT, 'tracker-rotation-back_right'],
[rotationToQuatMap.FRONT_LEFT, 'tracker-rotation-front_left'],
[rotationToQuatMap.FRONT_RIGHT, 'tracker-rotation-front_right'],
];
export function TrackerSettingsPage() {
const { l10n } = useLocalization();
@@ -68,7 +68,7 @@ export function TrackerSettingsPage() {
const tracker = useTrackerFromId(trackernum, deviceid);
const onDirectionSelected = (mountingOrientationDegrees: number) => {
const onDirectionSelected = (mountingOrientationDegrees: Quaternion) => {
if (!tracker) return;
const assignreq = new AssignTrackerRequestT();
@@ -92,14 +92,13 @@ export function TrackerSettingsPage() {
assignreq.trackerId = tracker?.tracker.trackerId;
if (allowDriftCompensation != null)
assignreq.allowDriftCompensation = allowDriftCompensation;
assignreq.accessoryId = 1; // TODO
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
setSelectBodypart(false);
};
const currRotationDegrees = useMemo(() => {
return tracker?.tracker.info?.mountingOrientation
? getYawInDegrees(tracker?.tracker.info?.mountingOrientation)
: rotationToQuatMap.FRONT;
const currRotation = useMemo(() => {
return QuaternionFromQuatT(tracker?.tracker.info?.mountingOrientation);
}, [tracker?.tracker.info?.mountingOrientation]);
const updateTrackerSettings = () => {
@@ -112,8 +111,9 @@ export function TrackerSettingsPage() {
return;
const assignreq = new AssignTrackerRequestT();
assignreq.bodyPosition = tracker?.tracker.info?.bodyPart || BodyPart.NONE;
assignreq.mountingOrientation =
MountingOrientationDegreesToQuatT(currRotationDegrees);
assignreq.mountingOrientation = currRotation
? MountingOrientationDegreesToQuatT(currRotation)
: null;
assignreq.displayName = trackerName;
assignreq.trackerId = tracker?.tracker.trackerId;
@@ -161,6 +161,7 @@ export function TrackerSettingsPage() {
onRoleSelected={onRoleSelected}
></SingleTrackerBodyAssignmentMenu>
<MountingSelectionMenu
bodyPart={tracker?.tracker.info?.bodyPart}
isOpen={selectRotation}
onClose={() => setSelectRotation(false)}
onDirectionSelected={onDirectionSelected}
@@ -169,7 +170,7 @@ export function TrackerSettingsPage() {
<div className="flex flex-col w-full md:max-w-xs gap-2">
{tracker && (
<TrackerCard
bg={'bg-background-70'}
bg="bg-background-70"
device={tracker?.device}
tracker={tracker?.tracker}
shakeHighlight={false}
@@ -333,11 +334,22 @@ 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">
<BodyPartIcon
bodyPart={tracker?.tracker.info?.bodyPart}
></BodyPartIcon>
<BodyPartIcon bodyPart={BodyPart.NONE}></BodyPartIcon>
<Typography>
{l10n.getString(rotationsLabels[currRotationDegrees])}
{l10n.getString(
(rotationsLabels.find((q) =>
similarQuaternions(q[0], currRotation)
) || [])[1] || 'tracker-rotation-custom'
) +
(tracker?.tracker.info?.mountingResetOrientation &&
!similarQuaternions(
QuaternionFromQuatT(
tracker.tracker.info.mountingResetOrientation
),
new Quaternion()
)
? ` ${l10n.getString('tracker-rotation-overriden')}`
: '')}
</Typography>
</div>
<div className="flex">

View File

@@ -12,7 +12,7 @@ import { useConfig } from '../../hooks/config';
import { useTracker } from '../../hooks/tracker';
import { BodyPartIcon } from '../commons/BodyPartIcon';
import { Typography } from '../commons/Typography';
import { formatVector3 } from '../utils/formatting';
import { formatVector3 } from '../../utils/formatting';
import { TrackerBattery } from './TrackerBattery';
import { TrackerStatus } from './TrackerStatus';
import { TrackerWifi } from './TrackerWifi';

View File

@@ -2,7 +2,7 @@ import { useEffect, 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 { formatVector3 } from '../../utils/formatting';
import { Canvas, useLoader } from '@react-three/fiber';
import * as THREE from 'three';
import { PerspectiveCamera } from 'three';

View File

@@ -24,6 +24,7 @@ import { playSoundOnResetStarted } from '../sounds/sounds';
import { useConfig } from './config';
import { useDataFeedConfig } from './datafeed-config';
import { useWebsocketAPI } from './websocket-api';
import { error } from '../utils/logging';
export interface FlatDeviceTracker {
device?: DeviceDataT;
@@ -41,6 +42,7 @@ export interface AppContext {
trackers: FlatDeviceTracker[];
dispatch: Dispatch<AppStateAction>;
bones: BoneT[];
computedTrackers: FlatDeviceTracker[];
}
export function reducer(state: AppState, action: AppStateAction) {
@@ -88,6 +90,11 @@ export function useProvideAppContext(): AppContext {
[state]
);
const computedTrackers: FlatDeviceTracker[] = useMemo(
() => (state.datafeed?.syntheticTrackers || []).map((tracker) => ({ tracker })),
[state]
);
const bones = useMemo(() => state.datafeed?.bones || [], [state]);
useDataFeedPacket(DataFeedMessage.DataFeedUpdate, (packet: DataFeedUpdateT) => {
@@ -103,8 +110,8 @@ export function useProvideAppContext(): AppContext {
break;
}
}
} catch (error) {
console.log(error);
} catch (e) {
error(e);
}
});
@@ -113,6 +120,7 @@ export function useProvideAppContext(): AppContext {
trackers,
dispatch,
bones,
computedTrackers,
};
}

View File

@@ -1,22 +1,30 @@
import { createContext, useContext, useMemo, useState } from 'react';
import {
AutoBoneApplyRequestT,
AutoBoneEpochResponseT,
AutoBoneProcessRequestT,
AutoBoneProcessStatusResponseT,
AutoBoneProcessType,
RpcMessage,
SkeletonBone,
SkeletonConfigRequestT,
SkeletonPartT,
} from 'solarxr-protocol';
import { useWebsocketAPI } from './websocket-api';
import { useLocalization } from '@fluent/react';
import { log } from '../utils/logging';
export enum ProcessStatus {
PENDING,
FULFILLED,
REJECTED,
}
export interface AutoboneContext {
hasRecording: boolean;
hasCalibration: boolean;
hasRecording: ProcessStatus;
hasCalibration: ProcessStatus;
progress: number;
bodyParts: { bone: SkeletonBone; label: string; value: number }[] | null;
eta: number;
startRecording: () => void;
startProcessing: () => void;
applyProcessing: () => void;
@@ -25,9 +33,10 @@ export interface AutoboneContext {
export function useProvideAutobone(): AutoboneContext {
const { l10n } = useLocalization();
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const [hasRecording, setHasRecording] = useState(false);
const [hasCalibration, setHasCalibration] = useState(false);
const [hasRecording, setHasRecording] = useState(ProcessStatus.PENDING);
const [hasCalibration, setHasCalibration] = useState(ProcessStatus.PENDING);
const [progress, setProgress] = useState(0);
const [eta, setEta] = useState(-1);
const [skeletonParts, setSkeletonParts] = useState<SkeletonPartT[] | null>(null);
const bodyParts = useMemo(() => {
@@ -47,6 +56,7 @@ export function useProvideAutobone(): AutoboneContext {
// }
setProgress(0);
setEta(-1);
const processRequest = new AutoBoneProcessRequestT();
processRequest.processType = processType;
@@ -55,19 +65,19 @@ export function useProvideAutobone(): AutoboneContext {
};
const startRecording = () => {
setHasCalibration(false);
setHasRecording(false);
setHasCalibration(ProcessStatus.PENDING);
setHasRecording(ProcessStatus.PENDING);
setSkeletonParts(null);
startProcess(AutoBoneProcessType.RECORD);
};
const startProcessing = () => {
setHasCalibration(false);
setHasCalibration(ProcessStatus.PENDING);
startProcess(AutoBoneProcessType.PROCESS);
};
const applyProcessing = () => {
startProcess(AutoBoneProcessType.APPLY);
sendRPCPacket(RpcMessage.AutoBoneApplyRequest, new AutoBoneApplyRequestT());
};
useRPCPacket(
@@ -78,39 +88,36 @@ export function useProvideAutobone(): AutoboneContext {
}
if (data.processType) {
if (data.message) {
console.log(AutoBoneProcessType[data.processType], ': ', data.message);
}
if (data.total > 0 && data.current >= 0) {
setProgress(data.current / data.total);
}
setEta(data.eta);
if (data.completed) {
console.log(
'Process ',
AutoBoneProcessType[data.processType],
' has completed'
);
log(`Process ${AutoBoneProcessType[data.processType]} has completed`);
switch (data.processType) {
case AutoBoneProcessType.RECORD:
setHasRecording(data.success);
setHasRecording(
data.success ? ProcessStatus.FULFILLED : ProcessStatus.REJECTED
);
startProcessing();
break;
case AutoBoneProcessType.PROCESS:
setHasCalibration(data.success);
break;
case AutoBoneProcessType.APPLY:
// Update skeleton config when applied
sendRPCPacket(
RpcMessage.SkeletonConfigRequest,
new SkeletonConfigRequestT()
setHasCalibration(
data.success ? ProcessStatus.FULFILLED : ProcessStatus.REJECTED
);
break;
// case AutoBoneProcessType.APPLY:
// // Update skeleton config when applied
// sendRPCPacket(
// RpcMessage.SkeletonConfigRequest,
// new SkeletonConfigRequestT()
// );
// break;
}
}
}
@@ -121,7 +128,7 @@ export function useProvideAutobone(): AutoboneContext {
setProgress(data.currentEpoch / data.totalEpochs);
// Probably not necessary to show to the user
console.log(
log(
'Epoch ',
data.currentEpoch,
'/',
@@ -138,6 +145,7 @@ export function useProvideAutobone(): AutoboneContext {
hasCalibration,
hasRecording,
progress,
eta,
bodyParts,
startProcessing,
startRecording,

View File

@@ -2,6 +2,7 @@ import { BaseDirectory, readTextFile } from '@tauri-apps/api/fs';
import { createContext, useContext, useRef, useState } from 'react';
import { DeveloperModeWidgetForm } from '../components/widgets/DeveloperModeWidget';
import { error } from '../utils/logging';
export interface WindowConfig {
width: number;
@@ -19,6 +20,8 @@ export interface Config {
feedbackSound: boolean;
feedbackSoundVolume: number;
theme: string;
textSize: number;
fonts: string[];
}
export interface ConfigContext {
@@ -28,13 +31,16 @@ export interface ConfigContext {
loadConfig: () => Promise<Config | null>;
}
const defaultConfig: Partial<Config> = {
export const defaultConfig = {
lang: 'en',
debug: false,
doneOnboarding: false,
watchNewDevices: true,
feedbackSound: true,
feedbackSoundVolume: 0.5,
theme: 'slime',
textSize: 12,
fonts: ['poppins'],
};
function fallbackToDefaults(loadedConfig: any): Config {
@@ -54,10 +60,24 @@ export function useConfigProvider(): ConfigContext {
}
: null;
set(newConfig as Config);
if ('theme' in config) {
if (config.theme !== undefined) {
document.documentElement.dataset.theme = config.theme;
}
if (config.fonts !== undefined) {
document.documentElement.style.setProperty(
'--font-name',
config.fonts.map((x) => `"${x}"`).join(',')
);
}
if (config.textSize !== undefined) {
document.documentElement.style.setProperty(
'--font-size',
`${config.textSize}rem`
);
}
if (!debounceTimer.current) {
debounceTimer.current = setTimeout(async () => {
localStorage.setItem('config.json', JSON.stringify(newConfig));
@@ -91,10 +111,18 @@ export function useConfigProvider(): ConfigContext {
const loadedConfig = fallbackToDefaults(JSON.parse(json));
set(loadedConfig);
document.documentElement.dataset.theme = loadedConfig.theme;
document.documentElement.style.setProperty(
'--font-size',
`${loadedConfig.textSize}rem`
);
document.documentElement.style.setProperty(
'--font-name',
loadedConfig.fonts.map((x) => `"${x}"`).join(',')
);
setLoading(false);
return loadedConfig;
} catch (e) {
console.log(e);
error(e);
setConfig(defaultConfig);
setLoading(false);
return null;

View File

@@ -22,3 +22,14 @@ export const useDebouncedEffect = (effect: () => void, deps: any[], delay: numbe
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...(deps || []), delay]);
};
export const debounce = <F extends (...args: any) => any>(func: F, waitFor: number) => {
let timeout = 0;
const debounced = (...args: any) => {
window.clearTimeout(timeout);
timeout = window.setTimeout(() => func(...args), waitFor);
};
return debounced as (...args: Parameters<F>) => ReturnType<F>;
};

View File

@@ -24,11 +24,12 @@ export function useTrackers() {
trackers.filter(({ tracker }) => tracker.info?.bodyPart === BodyPart.NONE),
[trackers]
),
useConnectedTrackers: () =>
useConnectedIMUTrackers: () =>
useMemo(
() =>
trackers.filter(
({ tracker }) => tracker.status !== TrackerStatus.DISCONNECTED
({ tracker }) =>
tracker.status !== TrackerStatus.DISCONNECTED && tracker.info?.isImu
),
[trackers]
),

View File

@@ -13,6 +13,7 @@ import {
import { Builder, ByteBuffer } from 'flatbuffers';
import { useInterval } from './timeout';
import { log } from '../utils/logging';
export interface WebSocketApi {
isConnected: boolean;
@@ -48,7 +49,7 @@ export function useProvideWebsocketApi(): WebSocketApi {
useInterval(() => {
if (webSocketRef.current && !isConnected) {
console.log('Attempting to reconnect');
log('Attempting to reconnect');
reconnect();
}
}, 3000);

View File

@@ -10,6 +10,7 @@ import {
useContext,
} from 'react';
import { exists, readTextFile, BaseDirectory } from '@tauri-apps/api/fs';
import { error } from '../utils/logging';
export const defaultNS = 'translation';
export const DEFAULT_LOCALE = 'en';
@@ -136,7 +137,7 @@ function verifyLocale(locale: string | null): string | null {
new Intl.Locale(locale);
return locale;
} catch (e) {
console.error(e);
error(e);
return null;
}
}
@@ -178,6 +179,7 @@ export function AppLocalizationProvider(props: AppLocalizationProviderProps) {
const bundles = lazilyParsedBundles(fetchedMessages);
localStorage.setItem('i18nextLng', currentLocale);
document.documentElement.lang = currentLocale;
setL10n(new ReactLocalization(bundles));
}

View File

@@ -4,7 +4,8 @@
body {
font-variant-numeric: tabular-nums;
font-family: 'poppins', sans-serif, 'Twemoji Chromium', emoji;
font-family: var(--font-name), 'Noto Sans CJK', sans-serif, 'Twemoji Chromium',
emoji;
width: 100vw;
height: 100vh;
user-select: none;
@@ -19,26 +20,71 @@ body {
@media (-webkit-animation) {
body {
font-family: 'poppins', sans-serif, 'Twemoji Webkit', emoji;
font-family: var(--font-name), 'Noto Sans CJK', sans-serif, 'Twemoji Webkit',
emoji;
}
body.linux {
font-family: 'poppins', sans-serif, emoji;
font-family: var(--font-name), 'Noto Sans CJK', sans-serif, emoji;
}
}
@font-face {
font-family: 'Twemoji Webkit';
src: url('/fonts/twemoji-picosvg.ttf') format('truetype');
src: url('/fonts/twemoji-picosvg.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'Twemoji Chromium';
src: url('/fonts/twemoji-glyf_colr_1.ttf') format('truetype');
src: url('/fonts/twemoji-glyf_colr_1.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'OpenDyslexic';
src: url('/fonts/OpenDyslexic-Regular.woff') format('woff');
font-style: normal;
}
@font-face {
font-family: 'OpenDyslexic';
src: url('/fonts/OpenDyslexic-Italic.woff') format('woff');
font-style: italic;
}
@font-face {
font-family: 'OpenDyslexic';
src: url('/fonts/OpenDyslexic-Bold.woff') format('woff');
font-weight: bold;
}
@font-face {
font-family: 'OpenDyslexic';
src: url('/fonts/OpenDyslexic-Bold-Italic.woff') format('woff');
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'Lexend';
src: url('/fonts/Lexend[HEXP,wght].woff2') format('woff2-variations');
font-weight: 125 950;
}
@font-face {
font-family: 'Noto Sans CJK';
src: url('/fonts/NotoSansCJK-VF.otf.woff2') format('woff2-variations');
font-weight: 125 950;
font-stretch: 75% 125%;
font-style: oblique 0deg 20deg;
}
@font-face {
font-family: 'Ubuntu';
src: url('/fonts/Ubuntu-R.woff2') format('woff2');
}
:root {
overflow: hidden;
background: theme('colors.background.20');
@@ -69,6 +115,16 @@ body {
--window-icon-stroke: 192, 161, 216;
--default-color: 255, 255, 255;
/* Default font size */
--font-size: 12rem;
--font-size-standard: var(--font-size);
/* 16% bigger */
--font-size-vr: calc(var(--font-size) * 1.16);
/* 100.08% bigger */
--font-size-title: calc(var(--font-size) * 2.08);
--font-name: 'poppins';
}
:root[data-theme='trans'] {
@@ -264,8 +320,10 @@ body {
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
::-webkit-scrollbar {

View File

@@ -1,6 +1,16 @@
import { Euler, Quaternion } from 'three';
import { QuatT } from 'solarxr-protocol';
import { DEG_TO_RAD } from './angle';
export const rotationToQuatMap = {
FRONT: new Quaternion(0, 1, 0, 0),
FRONT_LEFT: new Quaternion(0, 0.924, 0, 0.383),
LEFT: new Quaternion(0, 0.707, 0, 0.707),
BACK_LEFT: new Quaternion(0, 0.383, 0, 0.924),
FRONT_RIGHT: new Quaternion(0, -0.924, 0, 0.383),
RIGHT: new Quaternion(0, -0.707, 0, 0.707),
BACK_RIGHT: new Quaternion(0, -0.383, 0, 0.924),
BACK: new Quaternion(0, 0, 0, 1),
};
export type QuatObject = { x: number; y: number; z: number; w: number };
@@ -17,12 +27,8 @@ export function QuaternionToQuatT(q: QuatObject) {
return quat;
}
export function MountingOrientationDegreesToQuatT(mountingOrientationDegrees: number) {
return QuaternionToQuatT(
new Quaternion().setFromEuler(
new Euler(0, +mountingOrientationDegrees * DEG_TO_RAD, 0)
)
);
export function MountingOrientationDegreesToQuatT(orientation: Quaternion) {
return QuaternionToQuatT(orientation);
}
const RAD_TO_DEG = 180 / Math.PI;
@@ -54,3 +60,15 @@ export function compareQuatT(a: QuatT | null, b: QuatT | null): boolean {
if (!a || !b) return false;
return a.w === b.w && a.x === b.x && a.y === b.y && a.z === b.z;
}
export function similarQuaternions(
a: Quaternion | null,
b: Quaternion | null,
tolerance = 1e-5
): boolean {
if (!a || !b) return false;
const len = new Quaternion(b.x - a.x, b.y - a.y, b.z - a.z, b.w - a.w).lengthSq();
const squareSum = a.lengthSq() + b.lengthSq();
return len <= tolerance ** 2 * squareSum;
}

Some files were not shown because too many files have changed in this diff Show More