Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gui/app/public/i18n/en/translation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,8 @@ onboarding-user_height-title = What is your height?
onboarding-user_height-description = We need your height to calculate your body proportions and accurately represent your movements. You can either let SlimeVR calculate it, or input your height manually.
onboarding-user_height-need_head_tracker = A headset and controllers with positional tracking are required to perform the calibration.
onboarding-user_height-calculate = Calculate my height automatically
onboarding-user_height-error_bounds = Input too high or too low
onboarding-user_height-error_format = Input wrong format
onboarding-user_height-next_step = Continue and save
onboarding-user_height-prev_step = Back
onboarding-user_height-manual-proportions = Manual Proportions
Expand Down
33 changes: 29 additions & 4 deletions gui/app/src/components/commons/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export const InputInside = forwardRef<
label?: string;
error?: FieldError;
autocomplete?: boolean | string;
className?: string;
errorClassName?: string;
onBlur?: () => void;
} & Partial<React.HTMLProps<HTMLInputElement>>
>(function AppInput(
{
Expand All @@ -37,6 +40,9 @@ export const InputInside = forwardRef<
value,
error,
variant = 'primary',
className,
errorClassName,
onBlur,
},
ref
) {
Expand Down Expand Up @@ -90,16 +96,21 @@ export const InputInside = forwardRef<
<div className="relative w-full">
<input
type={forceText ? 'text' : type}
className={classNames(classes, {
'pr-10 sentry-mask': type === 'password',
})}
className={classNames(
classes,
{
'pr-10 sentry-mask': type === 'password',
},
className
)}
placeholder={placeholder || undefined}
autoComplete={autocomplete ? 'off' : 'on'}
onChange={onChange}
name={name}
value={computedValue} // Do we want that behaviour ?
disabled={disabled}
ref={ref}
onBlur={onBlur}
/>
{type === 'password' && (
<div
Expand All @@ -110,7 +121,12 @@ export const InputInside = forwardRef<
</div>
)}
{error?.message && (
<div className="absolute top-[38px] z-0 pt-1.5 bg-background-70 px-1 w-full rounded-b-md text-status-critical">
<div
className={classNames(
'absolute top-[38px] z-0 pt-1.5 px-1 w-full rounded-b-md bg-dark-background-600 text-status-critical',
errorClassName
)}
>
{error.message}
</div>
)}
Expand All @@ -129,11 +145,17 @@ export const Input = <T extends FieldValues = FieldValues>({
disabled,
variant = 'primary',
rules,
className,
errorClassName,
onBlur,
}: {
rules?: UseControllerProps<T, FieldPath<T>>['rules'];
control: Control<T>;
name: FieldPath<T>;
autocomplete?: boolean | string;
className?: string;
errorClassName?: string;
onBlur?: () => void;
} & Omit<InputProps, 'name'> &
Partial<React.HTMLProps<HTMLInputElement>>) => {
return (
Expand All @@ -157,6 +179,9 @@ export const Input = <T extends FieldValues = FieldValues>({
onChange={onChange}
ref={ref}
name={name}
className={className}
errorClassName={errorClassName}
onBlur={onBlur}
/>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Input } from '@/components/commons/Input';
import { Typography } from '@/components/commons/Typography';
import { useBreakpoint } from '@/hooks/breakpoint';
import { EYE_HEIGHT_TO_HEIGHT_RATIO } from '@/hooks/height';
import { useLocaleConfig } from '@/i18n/config';
import classNames from 'classnames';
import convert from 'convert';
import { useMemo, useState } from 'react';
import { useMemo, useState, useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { useLocalization } from '@fluent/react';
import { yupResolver } from '@hookform/resolvers/yup';
import { string, object } from 'yup';

function IncrementButton({
value,
Expand Down Expand Up @@ -121,6 +126,9 @@ export function HeightSelectionInput({
const { isXs } = useBreakpoint('xs');
const [unit, setUnit] = useState<'meter' | 'foot'>('meter');
const { currentLocales } = useLocaleConfig();
const { l10n } = useLocalization();
const isSubmitting = useRef(false);
const footRegex = /^(\d+)(?:[′'.,\s]+(\d+(?:\.\d+)?)?[″"”]?)?$/;

const formattedHeight = useMemo(() => {
if (!hmdHeight) return '--';
Expand All @@ -140,6 +148,95 @@ export function HeightSelectionInput({
return formatInFoot(displayHeight, currentLocales);
}, [hmdHeight, unit]);

const defaultValues: { height: string } = {
height: formattedHeight,
};

const { reset, control, watch, trigger, handleSubmit } = useForm<{
height: string;
}>({
defaultValues,
mode: 'onChange',
reValidateMode: 'onChange',

resolver: yupResolver(
object({
height: string()
.defined()
.test(
'format',
l10n.getString('onboarding-user_height-error_format'),
function (value) {
if (unit === 'meter') {
return !isNaN(Number(value.replace(/[ m]/g, '')));
} else {
return footRegex.test(value);
}
}
)
.test(
'bounds',
l10n.getString('onboarding-user_height-error_bounds'),
function (value) {
if (unit === 'meter') {
const newNum = Number(value.replace(/[ m]/g, ''));
return newNum >= 0.97 && newNum <= 2.56;
} else {
const match = value.match(footRegex);
if (!match) return;
const feet = Number(match[1]);
const inches = Number(match[2] || 0);
return (
!(feet > 8 || (feet === 8 && inches > 4)) &&
!(feet < 3 || (feet === 3 && inches < 2))
);
}
}
),
})
),
});

const onSubmit = async (values: { height: string }) => {
if (!(await trigger('height'))) return;
isSubmitting.current = true;

if (unit === 'meter') {
const newFullHeight = Number(values.height.replace(/[ m]/g, ''));

setHmdHeight(round4Digit(newFullHeight * EYE_HEIGHT_TO_HEIGHT_RATIO));
} else {
const match = values.height.match(footRegex);
if (!match) return;

const feet = Number(match[1]);
const inches = Number(match[2] || 0);

const newFullHeight = convert(feet + inches / 12, 'foot').to('meter');

setHmdHeight(round4Digit(newFullHeight * EYE_HEIGHT_TO_HEIGHT_RATIO));
}
};

useEffect(() => {
const subscription = watch((value, { type }) => {
if (type === 'change') {
handleSubmit(onSubmit)();
}
});
return () => subscription.unsubscribe();
}, []);

useEffect(() => {
if (isSubmitting.current) return;

reset({ height: defaultValues.height });
}, [defaultValues.height]);

useEffect(() => {
reset({ height: formattedHeight });
}, [unit]);

const incrementMath = (unit: 'inch' | 'cm' | 'foot', value: number) => {
const incrementInMeters = convert(value, unit).to('meter');
const oldFull = hmdHeight / EYE_HEIGHT_TO_HEIGHT_RATIO;
Expand Down Expand Up @@ -182,6 +279,7 @@ export function HeightSelectionInput({
const newEyeHeight = round4Digit(
snappedHeight * EYE_HEIGHT_TO_HEIGHT_RATIO
);

setHmdHeight(newEyeHeight);
setUnit(newUnit);
};
Expand Down Expand Up @@ -222,11 +320,19 @@ export function HeightSelectionInput({
</>
)}
</div>
<div className="flex w-full xs:w-auto xs:flex-grow bg-background-50 rounded-md px-2 py-2 h-full">
<div className="h-full flex items-center flex-grow justify-center min-w-24">
<Typography variant="main-title">{formattedHeight}</Typography>
</div>
<div className="w-[60px] xs:w-20 h-full gap-2 grid p-1">
<div className="flex w-full xs:w-auto xs:flex-grow bg-background-50 rounded-md px-2 py-2 h-full gap-1 items-center">
<Input
name="height"
control={control}
variant="secondary"
className="text-center !text-3xl !font-bold !w-[210px]"
errorClassName="text-center top-[47px] "
onBlur={() => {
reset({ height: formattedHeight });
isSubmitting.current = false;
}}
/>
<div className="w-[70px] xs:w-20 h-full gap-2 grid p-1">
<UnitSelector
active={unit === 'meter'}
name={isXs ? 'unit-meter' : 'unit-cm'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import solarxr_protocol.rpc.SkeletonProportionsRequest
import solarxr_protocol.rpc.SkeletonProportionsResetAllRequest
import solarxr_protocol.rpc.SkeletonProportionsResponse

private const val MIN_HEIGHT = 1.0f
private const val MIN_HEIGHT = 0.9f

class SkeletonProportionsBehaviour(
private val userConfig: UserConfig,
Expand Down
Loading