From 4ad9d5cfca2de12669e2f5b266403e51ac76857c Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Thu, 23 Jan 2025 11:21:38 -0500 Subject: [PATCH] Expand skeleton height config (#1156) Co-authored-by: Uriel --- gui/public/i18n/en/translation.ftl | 66 +++++- gui/src/App.tsx | 5 + gui/src/components/commons/NumberSelector.tsx | 54 ++++- .../components/onboarding/StepperSlider.tsx | 30 ++- .../body-proportions/AutomaticProportions.tsx | 99 +++------ .../body-proportions/ProportionsChoose.tsx | 199 ++++++++++++----- .../ProportionsResetModal.tsx | 26 ++- .../body-proportions/ScaledProportions.tsx | 75 +++++++ .../autobone-steps/CheckFloorHeight.tsx | 207 ++++++++++++++++++ .../autobone-steps/CheckHeight.tsx | 194 ++++++++-------- .../autobone-steps/Preparation.tsx | 2 +- .../autobone-steps/TooSmolModal.tsx | 73 ++++++ .../body-proportions/scaled-steps/Done.tsx | 30 +++ .../scaled-steps/ManualHeightStep.tsx | 158 +++++++++++++ .../scaled-steps/ResetProportions.tsx | 62 ++++++ gui/src/hooks/height.ts | 58 +++++ .../java/dev/slimevr/autobone/AutoBone.kt | 22 +- .../java/dev/slimevr/autobone/AutoBoneStep.kt | 4 - .../autobone/errors/BodyProportionError.kt | 73 ++---- .../java/dev/slimevr/config/AutoBoneConfig.kt | 2 - .../config/CurrentVRConfigConverter.java | 9 + .../dev/slimevr/config/SkeletonConfig.java | 25 +++ .../dev/slimevr/protocol/rpc/RPCHandler.kt | 25 ++- .../rpc/settings/RPCSettingsBuilder.java | 34 +-- .../rpc/settings/RPCSettingsHandler.kt | 5 + .../processor/config/SkeletonConfigManager.kt | 15 +- solarxr-protocol | 2 +- 27 files changed, 1215 insertions(+), 339 deletions(-) create mode 100644 gui/src/components/onboarding/pages/body-proportions/ScaledProportions.tsx create mode 100644 gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckFloorHeight.tsx create mode 100644 gui/src/components/onboarding/pages/body-proportions/autobone-steps/TooSmolModal.tsx create mode 100644 gui/src/components/onboarding/pages/body-proportions/scaled-steps/Done.tsx create mode 100644 gui/src/components/onboarding/pages/body-proportions/scaled-steps/ManualHeightStep.tsx create mode 100644 gui/src/components/onboarding/pages/body-proportions/scaled-steps/ResetProportions.tsx create mode 100644 gui/src/hooks/height.ts diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index b9a1816a34..c86ac7e403 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -122,6 +122,10 @@ reset-reset_all_warning = Are you sure you want to do this? reset-reset_all_warning-reset = Reset proportions reset-reset_all_warning-cancel = Cancel +reset-reset_all_warning_default = + Warning: You currently don't have your height defined, which + will make the proportions be based on a default height. + Are you sure you want to do this? reset-full = Full Reset reset-mounting = Reset Mounting @@ -962,6 +966,15 @@ onboarding-choose_proportions-manual_proportions = Manual proportions # Italicized text onboarding-choose_proportions-manual_proportions-subtitle = For small touches onboarding-choose_proportions-manual_proportions-description = This will let you adjust your proportions manually by modifying them directly +onboarding-choose_proportions-scaled_proportions = Scaled proportions +# Italized text +onboarding-choose_proportions-scaled_proportions-subtitle = Recommended for new users +# Multiline string +onboarding-choose_proportions-scaled_proportions-description = + This will scale the proportions of an average human body based on your height, this will help for basic full-body tracking. + + This requires having your headset (HMD) connected to SlimeVR and on your head! +onboarding-choose_proportions-scaled_proportions-button = Scaled proportions onboarding-choose_proportions-export = Export proportions onboarding-choose_proportions-import = Import proportions onboarding-choose_proportions-import-success = Imported @@ -981,9 +994,11 @@ onboarding-automatic_proportions-title = Measure your body onboarding-automatic_proportions-description = For SlimeVR trackers to work, we need to know the length of your bones. This short calibration will measure it for you. onboarding-automatic_proportions-manual = Manual proportions onboarding-automatic_proportions-prev_step = Previous step + onboarding-automatic_proportions-put_trackers_on-title = Put on your trackers onboarding-automatic_proportions-put_trackers_on-description = To calibrate your proportions, we're gonna use the trackers you just assigned. Put on all your trackers, you can see which are which in the figure to the right. onboarding-automatic_proportions-put_trackers_on-next = I have all my trackers on + onboarding-automatic_proportions-requirements-title = Requirements # Each line of text is a different list item onboarding-automatic_proportions-requirements-descriptionv2 = @@ -993,23 +1008,38 @@ onboarding-automatic_proportions-requirements-descriptionv2 = 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). Your tracking is working and is accurately representing your movements (ex. you have performed a full reset and they move the right direction when kicking, bending over, sitting, etc). 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 headset's (HMD) height as an approximation of your actual height, but it's better to check if they are right yourself! + +onboarding-automatic_proportions-check_height-title-v2 = Measure your height +onboarding-automatic_proportions-check_height-description-v2 = Your headset (HMD) height should be slightly less than your full height, as headsets measure your eye height. This measurement will be used as a baseline for your body proportions. # All the text is in bold! -onboarding-automatic_proportions-check_height-calculation_warning = Please press the button while standing upright to calculate your height. You have 3 seconds after you press the button! +onboarding-automatic_proportions-check_height-calculation_warning-v2 = Start measuring while standing upright to calculate your height. Be careful to not raise your hands higher than your headset, as they may affect the measurement! onboarding-automatic_proportions-check_height-guardian_tip = If you are using a standalone VR headset, make sure to have your guardian / boundary turned on so that your height is correct! -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-hmd_height1 = Your HMD height is +onboarding-automatic_proportions-check_height-hmd_height2 = Your headset height is: +onboarding-automatic_proportions-check_height-measure-start = Start measuring +onboarding-automatic_proportions-check_height-measure-stop = Stop measuring +onboarding-automatic_proportions-check_height-measure-reset = Retry measuring +onboarding-automatic_proportions-check_height-next_step = Use headset height + +onboarding-automatic_proportions-check_floor_height-title = Measure your floor height (optional) +onboarding-automatic_proportions-check_floor_height-description = In some cases, your floor height may not be set correctly by your headset, causing the headset height to be measured as higher than it should be. You can measure the "height" of your floor to correct your headset height. +# All the text is in bold! +onboarding-automatic_proportions-check_floor_height-calculation_warning = If you are sure that your floor height is correct, you can skip this step. # Shows an element below it -onboarding-automatic_proportions-check_height-height1 = so your actual height is -onboarding-automatic_proportions-check_height-next_step = They are fine +onboarding-automatic_proportions-check_floor_height-floor_height = Your floor height is: +onboarding-automatic_proportions-check_floor_height-measure-start = Start measuring +onboarding-automatic_proportions-check_floor_height-measure-stop = Stop measuring +onboarding-automatic_proportions-check_floor_height-measure-reset = Retry measuring +onboarding-automatic_proportions-check_floor_height-skip_step = Skip step and save +onboarding-automatic_proportions-check_floor_height-next_step = Use floor height and save + 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 + onboarding-automatic_proportions-recording-title = REC onboarding-automatic_proportions-recording-description-p0 = Recording in progress... onboarding-automatic_proportions-recording-description-p1 = Make the moves shown below: @@ -1027,12 +1057,14 @@ onboarding-automatic_proportions-recording-timer = { $time -> [one] 1 second left *[other] { $time } seconds left } + onboarding-automatic_proportions-verify_results-title = Verify results onboarding-automatic_proportions-verify_results-description = Check the results below, do they look correct? onboarding-automatic_proportions-verify_results-results = Recording results onboarding-automatic_proportions-verify_results-processing = Processing the result 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-v2 = @@ -1041,6 +1073,26 @@ onboarding-automatic_proportions-error_modal-v2 = Please check the docs or join our Discord for help ^_^ onboarding-automatic_proportions-error_modal-confirm = Understood! +onboarding-automatic_proportions-smol_warning = + Your configured height of { $height } is smaller than the minimum accepted height of { $minHeight }. + Please redo the measurements and ensure they are correct. +onboarding-automatic_proportions-smol_warning-cancel = Go back + +## Tracker scaled proportions setup +onboarding-scaled_proportions-title = Scaled proportions +onboarding-scaled_proportions-description = For SlimeVR trackers to work, we need to know the length of your bones. This will use an average proportion and scale it based on your height. +onboarding-scaled_proportions-manual_height-title = Configure your height +onboarding-scaled_proportions-manual_height-description = Your headset (HMD) height should be slightly less than your full height, as headsets measure your eye height. This height will be used as a baseline for your body proportions. +onboarding-scaled_proportions-manual_height-missing_steamvr = SteamVR is not currently connected to SlimeVR, so measurements can't be based on your headset. Proceed at your own risk or check the docs! +onboarding-scaled_proportions-manual_height-height = Your headset height is +onboarding-scaled_proportions-manual_height-next_step = Continue and save + +## Tracker scaled proportions reset +onboarding-scaled_proportions-reset_proportion-title = Reset your body proportions +onboarding-scaled_proportions-reset_proportion-description = To set your body proportions based on your height, you need to now reset all of your proportions. This will clear any proportions you have configured and provide a baseline configuration. +onboarding-scaled_proportions-done-title = Body proportions set +onboarding-scaled_proportions-done-description = Your body proportions should now be configured based on your height. + ## Home home-no_trackers = No trackers detected or assigned diff --git a/gui/src/App.tsx b/gui/src/App.tsx index e2bb4c4a6f..0281f35fac 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -56,6 +56,7 @@ import { AppLayout } from './AppLayout'; import { Preload } from './components/Preload'; import { UnknownDeviceModal } from './components/UnknownDeviceModal'; import { useDiscordPresence } from './hooks/discord-presence'; +import { ScaledProportionsPage } from './components/onboarding/pages/body-proportions/ScaledProportions'; import { EmptyLayout } from './components/EmptyLayout'; import { AdvancedSettings } from './components/settings/pages/AdvancedSettings'; import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate'; @@ -162,6 +163,10 @@ function Layout() { path="body-proportions/manual" element={} /> + } + /> } /> }> diff --git a/gui/src/components/commons/NumberSelector.tsx b/gui/src/components/commons/NumberSelector.tsx index d2a2da7891..a21d35634a 100644 --- a/gui/src/components/commons/NumberSelector.tsx +++ b/gui/src/components/commons/NumberSelector.tsx @@ -1,6 +1,8 @@ import { Control, Controller } from 'react-hook-form'; import { Button } from './Button'; import { Typography } from './Typography'; +import { useCallback, useMemo } from 'react'; +import { useLocaleConfig } from '@/i18n/config'; export function NumberSelector({ label, @@ -10,7 +12,9 @@ export function NumberSelector({ min, max, step, + doubleStep, disabled = false, + showButtonWithNumber = false, }: { label?: string; valueLabelFormat?: (value: number) => string; @@ -19,14 +23,36 @@ export function NumberSelector({ min: number; max: number; step: number | ((value: number, add: boolean) => number); + doubleStep?: number; disabled?: boolean; + showButtonWithNumber?: boolean; }) { + const { currentLocales } = useLocaleConfig(); + const stepFn = typeof step === 'function' ? step : (value: number, add: boolean) => +(add ? value + step : value - step).toFixed(2); + const doubleStepFn = useCallback( + (value: number, add: boolean) => + doubleStep === undefined + ? 0 + : +(add ? value + doubleStep : value - doubleStep).toFixed(2), + [doubleStep] + ); + + const decimalFormat = useMemo( + () => + new Intl.NumberFormat(currentLocales, { + style: 'decimal', + maximumFractionDigits: 2, + signDisplay: 'exceptZero', + }), + [currentLocales] + ); + return ( {label}
-
+
+ {doubleStep !== undefined && ( + + )}
-
+
+ {doubleStep !== undefined && ( + + )}
diff --git a/gui/src/components/onboarding/StepperSlider.tsx b/gui/src/components/onboarding/StepperSlider.tsx index 8778bfc58b..3f5815af88 100644 --- a/gui/src/components/onboarding/StepperSlider.tsx +++ b/gui/src/components/onboarding/StepperSlider.tsx @@ -94,30 +94,42 @@ export function StepDot({ export function StepperSlider({ variant, steps, + back, + forward, }: { variant: 'alone' | 'onboarding'; steps: Step[]; + /** + * Ran when step is 0 and `prevStep` is executed + */ + back?: () => void; + /** + * Ran when step is `steps.length - 1` and nextStep is executed + */ + forward?: () => void; }) { const ref = useRef(null); const { width } = useElemSize(ref); - const [stepsContainers, setSteps] = useState(0); const [shouldAnimate, setShouldAnimate] = useState(true); const [step, setStep] = useState(0); useEffect(() => { - if (!ref.current) return; - const stepsContainers = - ref.current.getElementsByClassName('step-container'); - setSteps(stepsContainers.length); - }, [ref]); + setStep((x) => Math.min(x, steps.length - 1)); + }, [steps.length]); const nextStep = () => { - if (step + 1 === stepsContainers) return; + if (step + 1 === steps.length) { + forward?.(); + return; + } setStep(step + 1); }; const prevStep = () => { - if (step - 1 < 0) return; + if (step - 1 < 0) { + back?.(); + return; + } setStep(step - 1); }; @@ -168,7 +180,7 @@ export function StepperSlider({
- {Array.from({ length: stepsContainers }).map((_, index) => ( + {Array.from({ length: steps.length }).map((_, index) => (
{index !== 0 && (
diff --git a/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx index b843ba1352..f0527ed82b 100644 --- a/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx @@ -1,9 +1,6 @@ import { useLocalization } from '@fluent/react'; -import { RpcMessage, SkeletonResetAllRequestT } from 'solarxr-protocol'; import { AutoboneContextC, useProvideAutobone } from '@/hooks/autobone'; import { useOnboarding } from '@/hooks/onboarding'; -import { useWebsocketAPI } from '@/hooks/websocket-api'; -import { Button } from '@/components/commons/Button'; import { Typography } from '@/components/commons/Typography'; import { StepperSlider } from '@/components/onboarding/StepperSlider'; import { DoneStep } from './autobone-steps/Done'; @@ -12,83 +9,55 @@ import { PutTrackersOnStep } from './autobone-steps/PutTrackersOn'; 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/CheckHeight'; +import { CheckHeightStep } from './autobone-steps/CheckHeight'; import { PreparationStep } from './autobone-steps/Preparation'; -import { useState } from 'react'; -import { ProportionsResetModal } from './ProportionsResetModal'; +import { HeightContextC, useProvideHeightContext } from '@/hooks/height'; +import { CheckFloorHeightStep } from './autobone-steps/CheckFloorHeight'; export function AutomaticProportionsPage() { const { l10n } = useLocalization(); const { applyProgress, state } = useOnboarding(); - const { sendRPCPacket } = useWebsocketAPI(); const context = useProvideAutobone(); - const { isCounting, startCountdown, timer } = useCountdown({ - onCountdownEnd: () => { - sendRPCPacket( - RpcMessage.SkeletonResetAllRequest, - new SkeletonResetAllRequestT() - ); - }, - }); - - const [showWarning, setShowWarning] = useState(false); + const heightContext = useProvideHeightContext(); applyProgress(0.9); return ( -
-
-
- - {l10n.getString('onboarding-automatic_proportions-title')} - -
- - {l10n.getString('onboarding-automatic_proportions-description')} + +
+
+
+ + {l10n.getString('onboarding-automatic_proportions-title')} -
-
-
- -
-
-
- +
+ +
+
- { - startCountdown(); - setShowWarning(false); - }} - onClose={() => setShowWarning(false)} - isOpen={showWarning} - > -
+ ); } diff --git a/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx b/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx index d142cb78ba..4563903720 100644 --- a/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx @@ -1,6 +1,6 @@ import { useOnboarding } from '@/hooks/onboarding'; import { Localized, useLocalization } from '@fluent/react'; -import { useMemo, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import { Typography } from '@/components/commons/Typography'; import { Button } from '@/components/commons/Button'; @@ -9,6 +9,7 @@ import { SkeletonConfigRequestT, SkeletonBone, ChangeSkeletonConfigRequestT, + SkeletonResetAllRequestT, } from 'solarxr-protocol'; import { useWebsocketAPI } from '@/hooks/websocket-api'; import { save } from '@tauri-apps/plugin-dialog'; @@ -18,6 +19,7 @@ import { useAppContext } from '@/hooks/app'; import { error } from '@/utils/logging'; import { fileOpen, fileSave } from 'browser-fs-access'; import { useDebouncedEffect } from '@/hooks/timeout'; +import { ProportionsResetModal } from './ProportionsResetModal'; export const MIN_HEIGHT = 0.4; export const MAX_HEIGHT = 4; @@ -36,7 +38,9 @@ export function ProportionsChoose() { const { applyProgress, state } = useOnboarding(); const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); const [animated, setAnimated] = useState(false); + const [showProportionWarning, setShowProportionWarning] = useState(false); const [importState, setImportState] = useState(ImportStatus.OK); + const exporting = useRef(false); const { computedTrackers } = useAppContext(); useDebouncedEffect( @@ -78,6 +82,8 @@ export function ProportionsChoose() { useRPCPacket( RpcMessage.SkeletonConfigResponse, (data: SkeletonConfigExport) => { + if (!exporting.current) return; + exporting.current = false; // Convert the skeleton part enums into a string data.skeletonParts.forEach((x) => { if (typeof x.bone === 'number') @@ -151,6 +157,13 @@ export function ProportionsChoose() { setImportState(ImportStatus.SUCCESS); }; + const resetAll = () => { + sendRPCPacket( + RpcMessage.SkeletonResetAllRequest, + new SkeletonResetAllRequestT() + ); + }; + return ( <>
@@ -217,62 +230,122 @@ export function ProportionsChoose() {
-
-
-
- setAnimated(() => true)} - onAnimationEnd={() => setAnimated(() => false)} - src="/images/slimetower.webp" - className={classNames( - 'absolute w-[100px] -right-2 -top-24', - animated && 'animate-[bounce_1s_1]' - )} - > -
- - {l10n.getString( - 'onboarding-choose_proportions-auto_proportions' - )} - - - {l10n.getString( - 'onboarding-choose_proportions-auto_proportions-subtitle' + {state.alonePage && ( +
+
+
+ setAnimated(() => true)} + onAnimationEnd={() => setAnimated(() => false)} + src="/images/slimetower.webp" + className={classNames( + 'absolute w-[100px] -right-2 -top-24', + animated && 'animate-[bounce_1s_1]' )} - -
-
- }} - > - +
+ + {l10n.getString( + 'onboarding-choose_proportions-auto_proportions' + )} + + + {l10n.getString( + 'onboarding-choose_proportions-auto_proportions-subtitle' + )} + +
+
+ }} > - Description for autobone + + Description for autobone + + +
+
+ +
+
+ )} + {!state.alonePage && ( +
+
+
+ setAnimated(() => true)} + onAnimationEnd={() => setAnimated(() => false)} + src="/images/slimetower.webp" + className={classNames( + 'absolute w-[100px] -right-2 -top-24', + animated && 'animate-[bounce_1s_1]' + )} + > +
+ + {l10n.getString( + 'onboarding-choose_proportions-scaled_proportions' + )} - + + {l10n.getString( + 'onboarding-choose_proportions-scaled_proportions-subtitle' + )} + +
+
+ }} + > + + Description for scaled proportions + + +
+
-
-
+ )}
{!state.alonePage && ( @@ -280,15 +353,33 @@ export function ProportionsChoose() { {l10n.getString('onboarding-previous_step')} )} + {state.alonePage && ( + + )} + { + resetAll(); + setShowProportionWarning(false); + }} + onClose={() => setShowProportionWarning(false)} + isOpen={showProportionWarning} + > diff --git a/gui/src/components/onboarding/pages/body-proportions/ProportionsResetModal.tsx b/gui/src/components/onboarding/pages/body-proportions/ProportionsResetModal.tsx index 62839c2a5e..233bdfb147 100644 --- a/gui/src/components/onboarding/pages/body-proportions/ProportionsResetModal.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/ProportionsResetModal.tsx @@ -3,6 +3,13 @@ import { WarningBox } from '@/components/commons/TipBox'; import { Localized, useLocalization } from '@fluent/react'; import { BaseModal } from '@/components/commons/BaseModal'; import ReactModal from 'react-modal'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { useEffect, useState } from 'react'; +import { + RpcMessage, + SettingsRequestT, + SettingsResponseT, +} from 'solarxr-protocol'; export function ProportionsResetModal({ isOpen = true, @@ -24,6 +31,16 @@ export function ProportionsResetModal({ accept: () => void; } & ReactModal.Props) { const { l10n } = useLocalization(); + const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); + const [usingDefaultHeight, setUsingDefaultHeight] = useState(true); + + useEffect( + () => sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT()), + [] + ); + useRPCPacket(RpcMessage.SettingsResponse, (res: SettingsResponseT) => + setUsingDefaultHeight(!res.modelSettings?.skeletonHeight?.hmdHeight) + ); return (
- }}> + }} + > Warning: This will reset your proportions to being just based on your height. diff --git a/gui/src/components/onboarding/pages/body-proportions/ScaledProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/ScaledProportions.tsx new file mode 100644 index 0000000000..d9110a25f4 --- /dev/null +++ b/gui/src/components/onboarding/pages/body-proportions/ScaledProportions.tsx @@ -0,0 +1,75 @@ +import { useLocalization } from '@fluent/react'; +import { useOnboarding } from '@/hooks/onboarding'; +import { Typography } from '@/components/commons/Typography'; +import { StepperSlider } from '@/components/onboarding/StepperSlider'; +import { CheckHeightStep } from './autobone-steps/CheckHeight'; +import { HeightContextC, useProvideHeightContext } from '@/hooks/height'; +import { CheckFloorHeightStep } from './autobone-steps/CheckFloorHeight'; +import { ResetProportionsStep } from './scaled-steps/ResetProportions'; +import { DoneStep } from './scaled-steps/Done'; +import { useNavigate } from 'react-router-dom'; +import { useMemo } from 'react'; +import { ManualHeightStep } from './scaled-steps/ManualHeightStep'; +import { useTrackers } from '@/hooks/tracker'; +import { BodyPart } from 'solarxr-protocol'; + +export function ScaledProportionsPage() { + const { l10n } = useLocalization(); + const { applyProgress, state } = useOnboarding(); + const heightContext = useProvideHeightContext(); + const navigate = useNavigate(); + const { trackers } = useTrackers(); + + const hmdTracker = useMemo( + () => + trackers.some( + (tracker) => + tracker.tracker.info?.bodyPart === BodyPart.HEAD && + (tracker.tracker.info.isHmd || tracker.tracker.position?.y) + ), + [trackers] + ); + + applyProgress(0.9); + + return ( + +
+
+
+ + {l10n.getString('onboarding-scaled_proportions-title')} + +
+ + {l10n.getString('onboarding-scaled_proportions-description')} + +
+
+
+ + navigate('/onboarding/body-proportions/choose', { state }) + } + > +
+
+
+
+ ); +} diff --git a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckFloorHeight.tsx b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckFloorHeight.tsx new file mode 100644 index 0000000000..6469e6bb8c --- /dev/null +++ b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckFloorHeight.tsx @@ -0,0 +1,207 @@ +import { + ChangeSettingsRequestT, + HeightRequestT, + HeightResponseT, + ModelSettingsT, + RpcMessage, + SkeletonHeightT, +} from 'solarxr-protocol'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { Button } from '@/components/commons/Button'; +import { Typography } from '@/components/commons/Typography'; +import { Localized, useLocalization } from '@fluent/react'; +import { useEffect, useMemo, useState } from 'react'; +import { useLocaleConfig } from '@/i18n/config'; +import { useHeightContext } from '@/hooks/height'; +import { useInterval } from '@/hooks/timeout'; +import { TooSmolModal } from './TooSmolModal'; + +export function CheckFloorHeightStep({ + nextStep, + prevStep, + variant, +}: { + nextStep: () => void; + prevStep: () => void; + variant: 'onboarding' | 'alone'; +}) { + const { l10n } = useLocalization(); + const { floorHeight, hmdHeight, setFloorHeight, validateHeight } = + useHeightContext(); + const [fetchHeight, setFetchHeight] = useState(false); + const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + const [isOpen, setOpen] = useState(false); + const { currentLocales } = useLocaleConfig(); + + useEffect(() => setFloorHeight(0), []); + + useInterval(() => { + if (fetchHeight) { + sendRPCPacket(RpcMessage.HeightRequest, new HeightRequestT()); + } + }, 100); + + const mFormat = useMemo( + () => + new Intl.NumberFormat(currentLocales, { + style: 'unit', + unit: 'meter', + maximumFractionDigits: 2, + }), + [currentLocales] + ); + + useRPCPacket(RpcMessage.HeightResponse, ({ minHeight }: HeightResponseT) => { + if (fetchHeight) { + setFloorHeight((val) => + val === null ? minHeight : Math.min(minHeight, val) + ); + } + }); + return ( + <> +
+
+
+ + {l10n.getString( + 'onboarding-automatic_proportions-check_floor_height-title' + )} + +
+ + {l10n.getString( + 'onboarding-automatic_proportions-check_floor_height-description' + )} + + }} + > + + Press the button to get your height! + + +
+
+
+ {!fetchHeight && ( + + )} + {fetchHeight && ( + + )} + + {l10n.getString( + 'onboarding-automatic_proportions-check_floor_height-floor_height' + )} + + + {floorHeight === null + ? l10n.getString( + 'onboarding-automatic_proportions-check_height-unknown' + ) + : mFormat.format(floorHeight)} + +
+
+
+ {/* TODO: Get image of person putting controller in floor */} + {/*
+ Reset position +
*/} +
+ +
+ + + +
+
+ setOpen(false)} /> + + ); +} diff --git a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckHeight.tsx b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckHeight.tsx index 80943a4265..fcf900de2d 100644 --- a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckHeight.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckHeight.tsx @@ -1,27 +1,15 @@ -import { - AutoBoneSettingsT, - ChangeSettingsRequestT, - HeightRequestT, - HeightResponseT, - RpcMessage, -} from 'solarxr-protocol'; +import { HeightRequestT, HeightResponseT, RpcMessage } from 'solarxr-protocol'; import { useWebsocketAPI } from '@/hooks/websocket-api'; import { Button } from '@/components/commons/Button'; import { Typography } from '@/components/commons/Typography'; import { Localized, useLocalization } from '@fluent/react'; -import { useForm } from 'react-hook-form'; import { useMemo, useState } from 'react'; -import { NumberSelector } from '@/components/commons/NumberSelector'; -import { MIN_HEIGHT } from '@/components/onboarding/pages/body-proportions/ProportionsChoose'; import { useLocaleConfig } from '@/i18n/config'; -import { useCountdown } from '@/hooks/countdown'; import { TipBox } from '@/components/commons/TipBox'; +import { useHeightContext } from '@/hooks/height'; +import { useInterval } from '@/hooks/timeout'; -interface HeightForm { - hmdHeight: number; -} - -export function CheckHeight({ +export function CheckHeightStep({ nextStep, prevStep, variant, @@ -31,18 +19,17 @@ export function CheckHeight({ variant: 'onboarding' | 'alone'; }) { const { l10n } = useLocalization(); - const { control, handleSubmit, setValue } = useForm(); - const [fetchedHeight, setFetchedHeight] = useState(false); + const { hmdHeight, setHmdHeight } = useHeightContext(); + const [fetchHeight, setFetchHeight] = useState(false); const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); - const { timer, isCounting, startCountdown } = useCountdown({ - duration: 3, - onCountdownEnd: () => { - setFetchedHeight(true); - sendRPCPacket(RpcMessage.HeightRequest, new HeightRequestT()); - }, - }); const { currentLocales } = useLocaleConfig(); + useInterval(() => { + if (fetchHeight) { + sendRPCPacket(RpcMessage.HeightRequest, new HeightRequestT()); + } + }, 100); + const mFormat = useMemo( () => new Intl.NumberFormat(currentLocales, { @@ -53,88 +40,103 @@ export function CheckHeight({ [currentLocales] ); - const sFormat = useMemo( - () => new Intl.RelativeTimeFormat(currentLocales, { style: 'short' }), - [currentLocales] - ); - - useRPCPacket(RpcMessage.HeightResponse, ({ hmdHeight }: HeightResponseT) => { - setValue('hmdHeight', hmdHeight); + useRPCPacket(RpcMessage.HeightResponse, ({ maxHeight }: HeightResponseT) => { + if (fetchHeight) { + setHmdHeight((val) => + val === null ? maxHeight : Math.max(maxHeight, val) + ); + } }); - - const onSubmit = (values: HeightForm) => { - const changeSettings = new ChangeSettingsRequestT(); - const autobone = new AutoBoneSettingsT(); - autobone.targetHmdHeight = values.hmdHeight; - changeSettings.autoBoneSettings = autobone; - - sendRPCPacket(RpcMessage.ChangeSettingsRequest, changeSettings); - nextStep(); - }; - return ( <>
-
- - {l10n.getString( - 'onboarding-automatic_proportions-check_height-title' - )} - -
- +
+
+ {l10n.getString( - 'onboarding-automatic_proportions-check_height-description' + 'onboarding-automatic_proportions-check_height-title-v2' )} - }} - > - - Press the button to get your height! +
+ + {l10n.getString( + 'onboarding-automatic_proportions-check_height-description-v2' + )} - - -
- - - {l10n.getString( - 'onboarding-automatic_proportions-check_height-guardian_tip' + + Press the button to get your height! + + + +
+ + {l10n.getString( + 'onboarding-automatic_proportions-check_height-guardian_tip' + )} + +
+
+
+
+ {!fetchHeight && ( + + )} + {fetchHeight && ( + )} - + + {l10n.getString( + 'onboarding-automatic_proportions-check_height-hmd_height2' + )} + + + {hmdHeight === null + ? l10n.getString( + 'onboarding-automatic_proportions-check_height-unknown' + ) + : mFormat.format(hmdHeight)} + +
-
- - isNaN(value) - ? l10n.getString( - 'onboarding-automatic_proportions-check_height-unknown' - ) - : mFormat.format(value) - } - min={MIN_HEIGHT} - max={4} - step={0.01} - disabled={true} +
+ Reset position - +
@@ -146,8 +148,8 @@ export function CheckHeight({ +
+
+
+ + ); +} diff --git a/gui/src/components/onboarding/pages/body-proportions/scaled-steps/Done.tsx b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/Done.tsx new file mode 100644 index 0000000000..a782d8401c --- /dev/null +++ b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/Done.tsx @@ -0,0 +1,30 @@ +import { Typography } from '@/components/commons/Typography'; +import { useLocalization } from '@fluent/react'; +import { Button } from '@/components/commons/Button'; +import { SkeletonVisualizerWidget } from '@/components/widgets/SkeletonVisualizerWidget'; + +export function DoneStep({ variant }: { variant: 'onboarding' | 'alone' }) { + const { l10n } = useLocalization(); + + return ( +
+
+ + {l10n.getString('onboarding-scaled_proportions-done-title')} + + + {l10n.getString('onboarding-scaled_proportions-done-description')} + +
+ +
+ {variant === 'onboarding' && ( + + )} +
+ +
+ ); +} diff --git a/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ManualHeightStep.tsx b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ManualHeightStep.tsx new file mode 100644 index 0000000000..6e548293bf --- /dev/null +++ b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ManualHeightStep.tsx @@ -0,0 +1,158 @@ +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { Button } from '@/components/commons/Button'; +import { Typography } from '@/components/commons/Typography'; +import { Localized, useLocalization } from '@fluent/react'; +import { useMemo } from 'react'; +import { useLocaleConfig } from '@/i18n/config'; +import { useHeightContext } from '@/hooks/height'; +import { useForm } from 'react-hook-form'; +import { + ChangeSettingsRequestT, + ModelSettingsT, + RpcMessage, + SkeletonHeightT, + StatusData, + StatusSteamVRDisconnectedT, +} from 'solarxr-protocol'; +import { NumberSelector } from '@/components/commons/NumberSelector'; +import { MIN_HEIGHT } from '@/components/onboarding/pages/body-proportions/ProportionsChoose'; +import { WarningBox } from '@/components/commons/TipBox'; +import { useStatusContext } from '@/hooks/status-system'; + +interface HeightForm { + height: number; +} + +export function ManualHeightStep({ + nextStep, + prevStep, + variant, +}: { + nextStep: () => void; + prevStep: () => void; + variant: 'onboarding' | 'alone'; +}) { + const { l10n } = useLocalization(); + const { hmdHeight, setHmdHeight } = useHeightContext(); + const { control, handleSubmit } = useForm({ + defaultValues: { height: 1.5 }, + }); + const { sendRPCPacket } = useWebsocketAPI(); + const { currentLocales } = useLocaleConfig(); + const { statuses } = useStatusContext(); + + const missingSteamConnection = useMemo( + () => + Object.values(statuses).some( + (x) => + x.dataType === StatusData.StatusSteamVRDisconnected && + (x.data as StatusSteamVRDisconnectedT).bridgeSettingsName === + 'steamvr' + ), + [statuses] + ); + + const mFormat = useMemo( + () => + new Intl.NumberFormat(currentLocales, { + style: 'unit', + unit: 'meter', + maximumFractionDigits: 2, + }), + [currentLocales] + ); + + handleSubmit((values) => { + setHmdHeight(values.height); + }); + + return ( + <> +
+
+
+ + {l10n.getString( + 'onboarding-scaled_proportions-manual_height-title' + )} + +
+ + {l10n.getString( + 'onboarding-scaled_proportions-manual_height-description' + )} + + {/* }} + > + + Input your height manually! + + */} + {missingSteamConnection && ( +
+ }} + // TODO: Add link to docs! + > + You don't have SteamVR connected! + +
+ )} +
+
+ + isNaN(value) + ? l10n.getString( + 'onboarding-scaled_proportions-manual_height-unknown' + ) + : mFormat.format(value) + } + min={MIN_HEIGHT} + max={4} + step={0.01} + showButtonWithNumber + doubleStep={0.1} + /> + +
+
+ +
+ + +
+
+ + ); +} diff --git a/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ResetProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ResetProportions.tsx new file mode 100644 index 0000000000..ac8fdc3d19 --- /dev/null +++ b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ResetProportions.tsx @@ -0,0 +1,62 @@ +import { RpcMessage, SkeletonResetAllRequestT } from 'solarxr-protocol'; +import { Button } from '@/components/commons/Button'; +import { Typography } from '@/components/commons/Typography'; +import { useLocalization } from '@fluent/react'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; + +export function ResetProportionsStep({ + nextStep, + prevStep, + variant, +}: { + nextStep: () => void; + prevStep: () => void; + variant: 'onboarding' | 'alone'; +}) { + const { l10n } = useLocalization(); + const { sendRPCPacket } = useWebsocketAPI(); + + return ( + <> +
+
+ + {l10n.getString( + 'onboarding-scaled_proportions-reset_proportion-title' + )} + +
+ + {l10n.getString( + 'onboarding-scaled_proportions-reset_proportion-description' + )} + +
+
+ +
+
+ + +
+
+
+ + ); +} diff --git a/gui/src/hooks/height.ts b/gui/src/hooks/height.ts new file mode 100644 index 0000000000..5dffb530d9 --- /dev/null +++ b/gui/src/hooks/height.ts @@ -0,0 +1,58 @@ +import { createContext, useContext, useEffect, useState } from 'react'; +import { useWebsocketAPI } from './websocket-api'; +import { RpcMessage, SettingsRequestT, SettingsResponseT } from 'solarxr-protocol'; +import { MIN_HEIGHT } from '@/components/onboarding/pages/body-proportions/ProportionsChoose'; + +export interface HeightContext { + hmdHeight: number | null; + setHmdHeight: React.Dispatch>; + floorHeight: number | null; + setFloorHeight: React.Dispatch>; + validateHeight: ( + hmdHeight: number | null | undefined, + floorHeight: number | null | undefined + ) => boolean; +} + +export function useProvideHeightContext(): HeightContext { + const [hmdHeight, setHmdHeight] = useState(null); + const [floorHeight, setFloorHeight] = useState(null); + const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + + function validateHeight( + hmdHeight: number | null | undefined, + floorHeight: number | null | undefined + ) { + return ( + hmdHeight !== undefined && + hmdHeight !== null && + hmdHeight - (floorHeight ?? 0) > MIN_HEIGHT + ); + } + + useEffect( + () => sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT()), + [] + ); + useRPCPacket(RpcMessage.SettingsResponse, (res: SettingsResponseT) => { + const hmd = res.modelSettings?.skeletonHeight?.hmdHeight; + const floor = res.modelSettings?.skeletonHeight?.floorHeight; + + if (validateHeight(hmd, floor)) { + setHmdHeight(hmd ?? null); + setFloorHeight(floor ?? null); + } + }); + + return { hmdHeight, setHmdHeight, floorHeight, setFloorHeight, validateHeight }; +} + +export const HeightContextC = createContext(undefined as never); + +export function useHeightContext() { + const context = useContext(HeightContextC); + if (!context) { + throw new Error('useHeightContext must be within a HeightContext Provider'); + } + return context; +} diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt index faf714f2d2..bf7e606302 100644 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt +++ b/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt @@ -4,6 +4,7 @@ import dev.slimevr.SLIMEVR_IDENTIFIER import dev.slimevr.VRServer import dev.slimevr.autobone.errors.* import dev.slimevr.config.AutoBoneConfig +import dev.slimevr.config.SkeletonConfig import dev.slimevr.poseframeformat.PoseFrameIO import dev.slimevr.poseframeformat.PoseFrames import dev.slimevr.tracking.processor.BoneType @@ -23,7 +24,7 @@ import java.util.function.Consumer import java.util.function.Function import kotlin.math.* -class AutoBone(server: VRServer) { +class AutoBone(private val server: VRServer) { // This is filled by loadConfigValues() val offsets = EnumMap( SkeletonConfigOffsets::class.java, @@ -49,8 +50,6 @@ class AutoBone(server: VRServer) { // The total height of the normalized adjusted offsets var adjustedHeightNormalized: Float = 1f - private val server: VRServer - // #region Error functions var slideError = SlideError() var offsetSlideError = OffsetSlideError() @@ -63,11 +62,10 @@ class AutoBone(server: VRServer) { private val rand = Random() - val globalConfig: AutoBoneConfig + val globalConfig: AutoBoneConfig = server.configManager.vrConfig.autoBone + val globalSkeletonConfig: SkeletonConfig = server.configManager.vrConfig.skeleton init { - globalConfig = server.configManager.vrConfig.autoBone - this.server = server loadConfigValues() } @@ -191,6 +189,7 @@ class AutoBone(server: VRServer) { // Get the current skeleton from the server val humanPoseManager = server.humanPoseManager // Still compensate for a null skeleton, as it may not be initialized yet + @Suppress("SENSELESS_COMPARISON") if (config.useSkeletonHeight && humanPoseManager != null) { // If there is a skeleton available, calculate the target height // from its configs @@ -229,6 +228,7 @@ class AutoBone(server: VRServer) { fun processFrames( frames: PoseFrames, config: AutoBoneConfig = globalConfig, + skeletonConfig: SkeletonConfig = globalSkeletonConfig, epochCallback: Consumer? = null, ): AutoBoneResults { check(frames.frameHolders.isNotEmpty()) { "Recording has no trackers." } @@ -238,16 +238,11 @@ class AutoBone(server: VRServer) { loadConfigValues() // Set the target heights either from config or calculate them - val targetHmdHeight = if (config.targetHmdHeight > 0f) { - config.targetHmdHeight + val targetHmdHeight = if (skeletonConfig.userHeight > MIN_HEIGHT) { + skeletonConfig.userHeight } else { calcTargetHmdHeight(frames, config) } - val targetFullHeight = if (config.targetFullHeight > 0f) { - config.targetFullHeight - } else { - targetHmdHeight / BodyProportionError.eyeHeightToHeightRatio - } check(targetHmdHeight > MIN_HEIGHT) { "Configured height ($targetHmdHeight) is too small (<= $MIN_HEIGHT)." } // Set up the current state, making all required players and setting up the @@ -255,7 +250,6 @@ class AutoBone(server: VRServer) { val trainingStep = AutoBoneStep( config = config, targetHmdHeight = targetHmdHeight, - targetFullHeight = targetFullHeight, frames = frames, epochCallback = epochCallback, serverConfig = server.configManager, diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt index a0b802f679..112121606b 100644 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt +++ b/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt @@ -10,7 +10,6 @@ import java.util.function.Consumer class AutoBoneStep( val config: AutoBoneConfig, val targetHmdHeight: Float, - val targetFullHeight: Float, val frames: PoseFrames, val epochCallback: Consumer?, serverConfig: ConfigManager, @@ -20,9 +19,6 @@ class AutoBoneStep( var cursor2: Int = 0, var currentHmdHeight: Float = 0f, ) { - - val eyeHeightToHeightRatio: Float = targetHmdHeight / targetFullHeight - var maxFrameCount = frames.maxFrameCount val framePlayer1 = TrackerFramesPlayer(frames) diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt index 51d2649854..c8f0912c56 100644 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt +++ b/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt @@ -4,6 +4,7 @@ import dev.slimevr.autobone.AutoBoneStep import dev.slimevr.autobone.errors.proportions.ProportionLimiter import dev.slimevr.autobone.errors.proportions.RangeProportionLimiter import dev.slimevr.tracking.processor.HumanPoseManager +import dev.slimevr.tracking.processor.config.SkeletonConfigManager import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets import kotlin.math.* @@ -32,84 +33,58 @@ class BodyProportionError : IAutoBoneError { @JvmField var eyeHeightToHeightRatio = 0.936f - // Default config - // Height: 1.58 - // Full Height: 1.58 / 0.936 = 1.688034 - // Neck: 0.1 / 1.688034 = 0.059241 - // Torso: 0.56 / 1.688034 = 0.331747 - // Upper Chest: 0.16 / 1.688034 = 0.094784 - // Chest: 0.16 / 1.688034 = 0.094784 - // Waist: (0.56 - 0.32 - 0.04) / 1.688034 = 0.118481 - // Hip: 0.04 / 1.688034 = 0.023696 - // Hip Width: 0.26 / 1.688034 = 0.154025 - // Upper Leg: (0.92 - 0.50) / 1.688034 = 0.24881 - // Lower Leg: 0.50 / 1.688034 = 0.296203 + private val defaultHeight = SkeletonConfigManager.HEIGHT_OFFSETS.sumOf { it.defaultValue.toDouble() }.toFloat() + private fun makeLimiter(offset: SkeletonConfigOffsets, range: Float): RangeProportionLimiter = RangeProportionLimiter( + offset.defaultValue / defaultHeight, + offset, + range, + ) + // "Expected" are values from Drillis and Contini (1966) - // "Experimental" are values from experimentation by the SlimeVR community + // Default are values from experimentation by the SlimeVR community + /** + * Proportions are based off the headset height (or eye height), not the total height of the user. + * To use the total height of the user, multiply it by [eyeHeightToHeightRatio] and use that in the limiters. + */ val proportionLimits = arrayOf( - // Head - // Experimental: 0.059 - RangeProportionLimiter( - 0.059f, + makeLimiter( SkeletonConfigOffsets.HEAD, 0.01f, ), - // Neck // Expected: 0.052 - // Experimental: 0.059 - RangeProportionLimiter( - 0.054f, + makeLimiter( SkeletonConfigOffsets.NECK, - 0.0015f, + 0.002f, ), - // Upper Chest - // Experimental: 0.0945 - RangeProportionLimiter( - 0.0945f, + makeLimiter( SkeletonConfigOffsets.UPPER_CHEST, 0.01f, ), - // Chest - // Experimental: 0.0945 - RangeProportionLimiter( - 0.0945f, + makeLimiter( SkeletonConfigOffsets.CHEST, 0.01f, ), - // Waist - // Experimental: 0.118 - RangeProportionLimiter( - 0.118f, + makeLimiter( SkeletonConfigOffsets.WAIST, 0.05f, ), - // Hip - // Experimental: 0.0237 - RangeProportionLimiter( - 0.0237f, + makeLimiter( SkeletonConfigOffsets.HIP, 0.01f, ), - // Hip Width // Expected: 0.191 - // Experimental: 0.154 - RangeProportionLimiter( - 0.184f, + makeLimiter( SkeletonConfigOffsets.HIPS_WIDTH, 0.04f, ), - // Upper Leg // Expected: 0.245 - RangeProportionLimiter( - 0.245f, + makeLimiter( SkeletonConfigOffsets.UPPER_LEG, - 0.015f, + 0.02f, ), - // Lower Leg // Expected: 0.246 (0.285 including below ankle, could use a separate // offset?) - RangeProportionLimiter( - 0.285f, + makeLimiter( SkeletonConfigOffsets.LOWER_LEG, 0.02f, ), diff --git a/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt b/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt index 3c7cf20d94..3d3c62f22a 100644 --- a/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt @@ -16,8 +16,6 @@ class AutoBoneConfig { var positionErrorFactor = 0.0f var positionOffsetErrorFactor = 0.0f var calcInitError = false - var targetHmdHeight = -1f - var targetFullHeight = -1f var randomizeFrameOrder = true var scaleEachStep = true var sampleCount = 1500 diff --git a/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java b/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java index ce6e436b2d..76dc56d261 100644 --- a/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java +++ b/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java @@ -309,6 +309,15 @@ public ObjectNode convert( // Update AutoBone defaults ObjectNode autoBoneNode = (ObjectNode) modelData.get("autoBone"); if (autoBoneNode != null) { + // Move HMD height to skeleton + ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton"); + if (skeletonNode != null) { + JsonNode targetHmdHeight = autoBoneNode.get("targetHmdHeight"); + if (targetHmdHeight != null) { + skeletonNode.set("hmdHeight", targetHmdHeight); + } + } + JsonNode offsetSlideNode = autoBoneNode.get("offsetSlideErrorFactor"); JsonNode slideNode = autoBoneNode.get("slideErrorFactor"); if ( diff --git a/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java b/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java index 07ec694f3e..27e6b3c3e8 100644 --- a/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java +++ b/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java @@ -1,5 +1,6 @@ package dev.slimevr.config; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.StdKeySerializers; @@ -24,6 +25,9 @@ public class SkeletonConfig { @JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer.class) public Map offsets = new HashMap<>(); + private float hmdHeight = 0f; + private float floorHeight = 0f; + public Map getToggles() { return toggles; } @@ -35,4 +39,25 @@ public Map getOffsets() { public Map getValues() { return values; } + + public float getHmdHeight() { + return hmdHeight; + } + + public void setHmdHeight(float hmdHeight) { + this.hmdHeight = hmdHeight; + } + + public float getFloorHeight() { + return floorHeight; + } + + public void setFloorHeight(float hmdHeight) { + this.floorHeight = hmdHeight; + } + + @JsonIgnore + public float getUserHeight() { + return hmdHeight - floorHeight; + } } diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt index 850644ea1e..19165b7c81 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt @@ -1,7 +1,6 @@ package dev.slimevr.protocol.rpc import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.autobone.errors.BodyProportionError import dev.slimevr.config.config import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI @@ -20,6 +19,7 @@ import dev.slimevr.protocol.rpc.status.RPCStatusHandler import dev.slimevr.protocol.rpc.trackingpause.RPCTrackingPause import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets import dev.slimevr.tracking.trackers.TrackerPosition.Companion.getByBodyPart +import dev.slimevr.tracking.trackers.TrackerStatus import dev.slimevr.tracking.trackers.TrackerUtils.getTrackerForSkeleton import io.eiren.util.logging.LogManager import io.github.axisangles.ktmath.Quaternion @@ -464,13 +464,22 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler { - val height = ( - humanPoseManager!!.hmdHeight / - BodyProportionError.eyeHeightToHeightRatio - ) - if (height > 0.5f) { // Reset only if floor level seems right, + val height = humanPoseManager?.server?.configManager?.vrConfig?.skeleton?.userHeight ?: -1f + if (height > AutoBone.MIN_HEIGHT) { // Reset only if floor level seems right, val proportionLimiter = proportionLimitMap[config] if (proportionLimiter != null) { setOffset( diff --git a/solarxr-protocol b/solarxr-protocol index aa69fb6e56..796c58e147 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit aa69fb6e56b88a206010d1cb75bfa0edde5b4582 +Subproject commit 796c58e147c03faa1c662fd9c1734fb9536d1d4b