Compare commits
86 Commits
v0.8.2
...
accessorie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
472f6d8fc0 | ||
|
|
d89a4c2704 | ||
|
|
0ad237a219 | ||
|
|
9c980f06f9 | ||
|
|
f607693d83 | ||
|
|
27faa1908c | ||
|
|
39cd1f9ba9 | ||
|
|
c59f20a79a | ||
|
|
e7de25dfab | ||
|
|
74f1d8ed61 | ||
|
|
ad41c46092 | ||
|
|
f3346bbeee | ||
|
|
ca6f82492d | ||
|
|
02acc6ede1 | ||
|
|
d9c631fcf6 | ||
|
|
ea9df2c31f | ||
|
|
e18bd2d382 | ||
|
|
b6a681b1bb | ||
|
|
793dd374f8 | ||
|
|
97b617bb24 | ||
|
|
1aa32b8264 | ||
|
|
5c2c6749c3 | ||
|
|
e472b12e83 | ||
|
|
75fc1c37d3 | ||
|
|
9c4b9b401e | ||
|
|
4243951214 | ||
|
|
2f61d5b4b8 | ||
|
|
35e6b8b721 | ||
|
|
82db6a1ff5 | ||
|
|
17be65d2a2 | ||
|
|
db2f7fbd49 | ||
|
|
95f1bfd52f | ||
|
|
7511e0098e | ||
|
|
f0f2731387 | ||
|
|
5f3182e2c6 | ||
|
|
8b4a2843a1 | ||
|
|
efd20ee7b2 | ||
|
|
c076ebabb3 | ||
|
|
9e650dad08 | ||
|
|
dc22b503e8 | ||
|
|
0bd6a9002a | ||
|
|
9c7558cae8 | ||
|
|
d89a53ef44 | ||
|
|
525f29f3c5 | ||
|
|
c94d71c24f | ||
|
|
31ff3f4868 | ||
|
|
a6911e072c | ||
|
|
bf062c9b65 | ||
|
|
8945e05354 | ||
|
|
ea3cdb7658 | ||
|
|
ae40121a31 | ||
|
|
f64a45fb2e | ||
|
|
cb19aa17cc | ||
|
|
4564671b38 | ||
|
|
60f74d6d5c | ||
|
|
4450260dd0 | ||
|
|
55f030a145 | ||
|
|
3038de8a5f | ||
|
|
338e153834 | ||
|
|
544efb6efe | ||
|
|
0e64f1241f | ||
|
|
ffe530dc94 | ||
|
|
a8ce510f70 | ||
|
|
1b17fcbec3 | ||
|
|
01f1d2ee56 | ||
|
|
d14a7bb5e7 | ||
|
|
a14a2ea253 | ||
|
|
b67de108e7 | ||
|
|
e4a4f38c15 | ||
|
|
858354eee8 | ||
|
|
fcb736d371 | ||
|
|
f6d8026761 | ||
|
|
cb5e27875c | ||
|
|
72f506822a | ||
|
|
4e942fded5 | ||
|
|
463e558e7f | ||
|
|
a1db52144a | ||
|
|
81b7ea0967 | ||
|
|
a489e32828 | ||
|
|
6eaf04ba64 | ||
|
|
b8ceaa6bc0 | ||
|
|
6b7c47d36c | ||
|
|
c0f8fb1758 | ||
|
|
07600f0133 | ||
|
|
3634bedef1 | ||
|
|
e864487246 |
6
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
25
.github/workflows/gradle.yaml
vendored
@@ -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
@@ -36,3 +36,6 @@ build/
|
||||
|
||||
# direnv has been claimed for Nix usage
|
||||
.direnv/
|
||||
|
||||
# Ignore Android local properties
|
||||
local.properties
|
||||
|
||||
413
Cargo.lock
generated
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
@@ -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
@@ -25,5 +25,6 @@ yarn-error.log*
|
||||
|
||||
*.log
|
||||
|
||||
|
||||
/dist
|
||||
# vite
|
||||
/dist
|
||||
/stats.html
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
gui/public/fonts/Lexend[HEXP,wght].woff2
Normal file
BIN
gui/public/fonts/NotoSansCJK-VF.otf.woff2
Normal file
BIN
gui/public/fonts/OpenDyslexic-Bold-Italic.woff
Normal file
BIN
gui/public/fonts/OpenDyslexic-Bold.woff
Normal file
BIN
gui/public/fonts/OpenDyslexic-Italic.woff
Normal file
BIN
gui/public/fonts/OpenDyslexic-Regular.woff
Normal file
BIN
gui/public/fonts/Ubuntu-R.woff2
Normal file
BIN
gui/public/fonts/twemoji-glyf_colr_1.woff2
Normal file
BIN
gui/public/fonts/twemoji-picosvg.woff2
Normal 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
|
||||
|
||||
|
Before Width: | Height: | Size: 312 KiB |
BIN
gui/public/images/boxslime.webp
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
BIN
gui/public/images/front-standing-pose.webp
Normal file
|
After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
BIN
gui/public/images/mounting-reset-pose.webp
Normal file
|
After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 998 KiB |
BIN
gui/public/images/reset-pose.webp
Normal file
|
After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 758 KiB |
BIN
gui/public/images/slime-girl.webp
Normal file
|
After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 426 KiB |
BIN
gui/public/images/slimes.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 387 KiB |
BIN
gui/public/images/slimetower.webp
Normal file
|
After Width: | Height: | Size: 20 KiB |
@@ -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"
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
25
gui/src/components/ClearMountingButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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]}`}
|
||||
|
||||
14
gui/src/components/commons/A.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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' &&
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
72
gui/src/components/commons/Range.tsx
Normal 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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
gui/src/components/commons/icon/BellIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
26
gui/src/components/commons/icon/SlimeUpIcon.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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]'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
338
gui/src/components/settings/pages/InterfaceSettings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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)}`
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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]
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||