Compare commits

...

8 Commits

Author SHA1 Message Date
Uriel
ca358b6394 Merge branch 'main' into support-inches 2025-07-09 17:47:33 -04:00
Uriel
66ec595c09 keep underlying representation in meters 2025-07-02 21:25:45 -04:00
Uriel
be8c000b36 lint 2025-06-22 06:02:29 -04:00
Uriel
b2f74640f7 change manual proportion's height estimate 2025-06-22 05:50:20 -04:00
Uriel
8b67ee1a36 Merge branch 'main' into support-inches 2025-06-20 21:02:08 -04:00
Uriel
6293cf4c83 Merge branch 'main' into support-inches 2025-06-14 03:24:57 -04:00
Uriel
8a05572715 format 2025-05-24 17:24:15 -04:00
Uriel
8232a9342a start adding support for inches 2025-05-15 23:40:05 -04:00
15 changed files with 255 additions and 72 deletions

View File

@@ -26,6 +26,7 @@
"@twemoji/svg": "^15.0.0",
"browser-fs-access": "^0.35.0",
"classnames": "^2.5.1",
"convert": "^5.10.0",
"flatbuffers": "22.10.26",
"intl-pluralrules": "^2.0.1",
"ip-num": "^1.5.1",

View File

@@ -676,6 +676,11 @@ settings-interface-appearance-font_size-description = This affects the font size
settings-interface-appearance-decorations = Use the system native decorations
settings-interface-appearance-decorations-description = This will not render the top bar of the interface and will use the operating system's instead.
settings-interface-appearance-decorations-label = Use native decorations
settings-interface-appearance-unit_system = Select unit system
settings-interface-appearance-unit_system-description = Change if you want to use the metric system or the imperial system for body measurements.
settings-interface-appearance-unit_system-placeholder = Select the unit system to use
settings-interface-appearance-unit_system-metric = Metric units
settings-interface-appearance-unit_system-imperial = Imperial units
## Notification settings
settings-interface-notifications = Notifications

View File

@@ -72,7 +72,7 @@ export function DropdownItems({
<div
ref={ref}
className={classNames(
'z-[1000] fixed rounded shadow',
'z-[1000] fixed rounded shadow w-fit',
'overflow-y-auto dropdown-scroll overflow-x-hidden text-background-10',
variant == 'primary' && 'bg-background-60',
variant == 'secondary' && 'bg-background-70',
@@ -100,13 +100,13 @@ export function DropdownItems({
style={item.fontName ? { fontFamily: item.fontName } : {}}
className={classNames(
'py-2 px-4 min-w-max cursor-pointer first-of-type:*:pointer-events-none',
variant == 'primary' &&
'checked-hover:bg-background-50 text-background-20 ' +
variant === 'primary' &&
'data-checked:bg-background-40 hover:bg-background-50 text-background-20 ' +
'checked-hover:text-background-10',
variant == 'secondary' &&
'checked-hover:bg-background-60 text-background-20 ' +
variant === 'secondary' &&
'data-checked:bg-background-50 hover:bg-background-60 text-background-20 ' +
'checked-hover:text-background-10',
variant == 'tertiary' &&
variant === 'tertiary' &&
'bg-accent-background-30 checked-hover:bg-accent-background-20'
)}
onClick={() => {

View File

@@ -0,0 +1,75 @@
import { defaultConfig, UnitType, useConfig } from '@/hooks/config';
import { useLocaleConfig } from '@/i18n/config';
import convert from 'convert';
import { useMemo } from 'react';
export function HeightDisplay({
height,
/**
* Unit type of only the input height
*/
heightUnit = 'm',
unitDisplay = 'short',
className,
roundInches = false,
}: {
heightUnit?: 'm' | 'cm' | 'in';
height: number;
unitDisplay?: 'narrow' | 'short' | 'long';
className?: string;
roundInches?: boolean;
}) {
const { currentLocales } = useLocaleConfig();
const { config } = useConfig();
const cmFormat = useMemo(
() =>
new Intl.NumberFormat(currentLocales, {
style: 'unit',
unit: 'centimeter',
unitDisplay,
maximumFractionDigits: 2,
}),
[currentLocales, unitDisplay]
);
const [footFormat, inchesFormat] = useMemo(
() => [
new Intl.NumberFormat(currentLocales, {
style: 'unit',
unit: 'foot',
unitDisplay: 'narrow',
maximumFractionDigits: 0,
roundingMode: 'trunc',
}),
new Intl.NumberFormat(currentLocales, {
style: 'unit',
unit: 'inch',
unitDisplay: 'narrow',
maximumFractionDigits: 1,
roundingMode: 'trunc',
}),
],
[currentLocales]
);
const value = useMemo(() => {
const unitSystem = config?.unitSystem ?? defaultConfig.unitSystem;
if (unitSystem === UnitType.Metric) {
return cmFormat.format(convert(height, heightUnit).to('cm'));
} else {
let totalInches = convert(height, heightUnit).to('inches');
if (roundInches) totalInches = Math.round(totalInches);
const feet = Math.trunc(totalInches / 12);
const remainingInches = totalInches % 12;
return (
(feet ? footFormat.format(feet) : '') +
inchesFormat.format(remainingInches)
);
}
}, [config?.unitSystem, height, heightUnit]);
return <span className={className}>{value}</span>;
}

View File

@@ -1,7 +1,7 @@
import { useLocalization } from '@fluent/react';
import { useEffect, useMemo, useContext } from 'react';
import { useForm } from 'react-hook-form';
import { useConfig } from '@/hooks/config';
import { defaultConfig, useConfig } from '@/hooks/config';
import { LangContext } from '@/i18n/config';
import { langs } from '@/i18n/names';
import { Dropdown, DropdownDirection } from './Dropdown';
@@ -17,7 +17,7 @@ export function LangSelector({
const { l10n } = useLocalization();
const { config, setConfig } = useConfig();
const { control, watch, handleSubmit } = useForm<{ lang: string }>({
defaultValues: { lang: config?.lang || 'en' },
defaultValues: { lang: config?.lang ?? defaultConfig.lang },
});
const languagesItems = useMemo(

View File

@@ -1,7 +1,7 @@
import { Control, Controller } from 'react-hook-form';
import { Button } from './Button';
import { Typography } from './Typography';
import { useCallback, useMemo } from 'react';
import { ReactNode, useMemo } from 'react';
import { useLocaleConfig } from '@/i18n/config';
export function NumberSelector({
@@ -14,34 +14,33 @@ export function NumberSelector({
step,
doubleStep,
disabled = false,
showButtonWithNumber = false,
showButtonWithNumber,
}: {
label?: string;
valueLabelFormat?: (value: number) => string;
valueLabelFormat?: (value: number) => string | ReactNode;
control: Control<any>;
name: string;
min: number;
max: number;
step: number | ((value: number, add: boolean) => number);
doubleStep?: number;
step: number | ((value: number, sign: number) => number);
doubleStep?: number | ((value: number, sign: number) => number);
disabled?: boolean;
showButtonWithNumber?: boolean;
showButtonWithNumber?: number;
}) {
const { currentLocales } = useLocaleConfig();
const stepFn =
typeof step === 'function'
? step
: (value: number, add: boolean) =>
+(add ? value + step : value - step).toFixed(2);
: (value: number, sign: number) => +(value + step * sign).toFixed(2);
const doubleStepFn = useCallback(
(value: number, add: boolean) =>
doubleStep === undefined
? 0
: +(add ? value + doubleStep : value - doubleStep).toFixed(2),
[doubleStep]
);
const doubleStepFn =
typeof doubleStep === 'function'
? doubleStep
: (value: number, sign: number) =>
doubleStep === undefined
? 0
: +(value + doubleStep * sign).toFixed(2);
const decimalFormat = useMemo(
() =>
@@ -66,19 +65,19 @@ export function NumberSelector({
<Button
variant="tertiary"
rounded
onClick={() => onChange(doubleStepFn(value, false))}
disabled={doubleStepFn(value, false) < min || disabled}
onClick={() => onChange(doubleStepFn(value, -1))}
disabled={doubleStepFn(value, -1) < min || disabled}
>
{showButtonWithNumber
? decimalFormat.format(-doubleStep)
{showButtonWithNumber !== undefined
? decimalFormat.format(-showButtonWithNumber)
: '--'}
</Button>
)}
<Button
variant="tertiary"
rounded
onClick={() => onChange(stepFn(value, false))}
disabled={stepFn(value, false) < min || disabled}
onClick={() => onChange(stepFn(value, -1))}
disabled={stepFn(value, -1) < min || disabled}
>
-
</Button>
@@ -90,8 +89,8 @@ export function NumberSelector({
<Button
variant="tertiary"
rounded
onClick={() => onChange(stepFn(value, true))}
disabled={stepFn(value, true) > max || disabled}
onClick={() => onChange(stepFn(value, 1))}
disabled={stepFn(value, 1) > max || disabled}
>
+
</Button>
@@ -99,11 +98,11 @@ export function NumberSelector({
<Button
variant="tertiary"
rounded
onClick={() => onChange(doubleStepFn(value, true))}
disabled={doubleStepFn(value, true) > max || disabled}
onClick={() => onChange(doubleStepFn(value, 1))}
disabled={doubleStepFn(value, 1) > max || disabled}
>
{showButtonWithNumber
? decimalFormat.format(doubleStep)
{showButtonWithNumber !== undefined
? decimalFormat.format(showButtonWithNumber)
: '++'}
</Button>
)}

View File

@@ -0,0 +1,54 @@
import { defaultConfig, UnitType, useConfig } from '@/hooks/config';
import { Dropdown, DropdownDirection } from './Dropdown';
import { useForm } from 'react-hook-form';
import { useEffect, useMemo } from 'react';
import { useLocalization } from '@fluent/react';
export function UnitSelector({
direction = 'up',
alignment = 'right',
}: {
direction?: DropdownDirection;
alignment?: 'right' | 'left';
}) {
const { l10n } = useLocalization();
const { config, setConfig } = useConfig();
const { control, watch, handleSubmit } = useForm<{ unitSystem: UnitType }>({
defaultValues: {
unitSystem: config?.unitSystem ?? defaultConfig.unitSystem,
},
});
const unitItems = useMemo(
() =>
Object.values(UnitType).map((type) => ({
label: l10n.getString(
`settings-interface-appearance-unit_system-${type}`
),
value: type,
})),
[l10n]
);
useEffect(() => {
const subscription = watch(() => handleSubmit(onSubmit)());
return () => subscription.unsubscribe();
}, []);
const onSubmit = (value: { unitSystem: UnitType }) => {
setConfig({ unitSystem: value.unitSystem });
};
return (
<Dropdown
control={control}
name="unitSystem"
placeholder={l10n.getString(
'settings-interface-appearance-unit_system-placeholder'
)}
items={unitItems}
direction={direction}
alignment={alignment}
></Dropdown>
);
}

View File

@@ -4,6 +4,7 @@ import { Button } from '@/components/commons/Button';
import { SlimeVRIcon } from '@/components/commons/icon/SimevrIcon';
import { LangSelector } from '@/components/commons/LangSelector';
import { Typography } from '@/components/commons/Typography';
import { UnitSelector } from '@/components/commons/UnitSelector';
export function HomePage() {
const { l10n } = useLocalization();
@@ -23,8 +24,9 @@ export function HomePage() {
{l10n.getString('onboarding-home-start')}
</Button>
</div>
<div className="absolute right-4 bottom-4 z-50">
<div className="flex flex-col gap-3 absolute right-4 bottom-4 z-50">
<LangSelector />
<UnitSelector />
</div>
<div
className="absolute bg-accent-background-50 w-full rounded-full"

View File

@@ -32,9 +32,9 @@ import { FullResetIcon } from '@/components/commons/icon/ResetIcon';
import { ImportIcon } from '@/components/commons/icon/ImportIcon';
import { HumanIcon } from '@/components/commons/icon/HumanIcon';
import { Typography } from '@/components/commons/Typography';
import { useLocaleConfig } from '@/i18n/config';
import { useNavigate } from 'react-router-dom';
import { ResetButton } from '@/components/home/ResetButton';
import { HeightDisplay } from '@/components/commons/HeightDisplay';
import { Vector3 } from 'three';
function IconButton({
@@ -406,7 +406,6 @@ function ButtonsControl({ control }: { control: ManualProportionControls }) {
export function ManualProportionsPage() {
const { applyProgress, state } = useOnboarding();
const { useRPCPacket } = useWebsocketAPI();
const { currentLocales } = useLocaleConfig();
const [userHeight, setUserHeight] = useState(0);
@@ -421,15 +420,6 @@ export function ManualProportionsPage() {
});
const { precise, ratio } = watch();
const { cmFormat } = useMemo(() => {
const cmFormat = Intl.NumberFormat(currentLocales, {
style: 'unit',
unit: 'centimeter',
maximumFractionDigits: 1,
});
return { cmFormat };
}, [currentLocales]);
useEffect(() => {
localStorage.setItem('ratioMode', ratio?.toString() ?? 'true');
}, [ratio]);
@@ -491,7 +481,7 @@ export function ManualProportionsPage() {
>
<div className="h-14 bg-background-50 p-4 flex items-center rounded-lg min-w-36 justify-center">
<Typography variant="main-title">
{cmFormat.format((userHeight * 100) / 0.936)}
<HeightDisplay height={userHeight / 0.936} />
</Typography>
</div>
</Tooltip>

View File

@@ -2,8 +2,7 @@ import { useWebsocketAPI } from '@/hooks/websocket-api';
import { Button } from '@/components/commons/Button';
import { Typography } from '@/components/commons/Typography';
import { useLocalization } from '@fluent/react';
import { useEffect, useMemo } from 'react';
import { useLocaleConfig } from '@/i18n/config';
import { useEffect } from 'react';
import {
DEFAULT_FULL_HEIGHT,
EYE_HEIGHT_TO_HEIGHT_RATIO,
@@ -19,11 +18,16 @@ import {
import { NumberSelector } from '@/components/commons/NumberSelector';
import { useOnboarding } from '@/hooks/onboarding';
import { MIN_HEIGHT } from '@/hooks/manual-proportions';
import { HeightDisplay } from '@/components/commons/HeightDisplay';
import { useUnit } from '@/hooks/config';
import convert from 'convert';
interface HeightForm {
height: number;
}
const INCH_IN_METER = convert(1, 'inch').to('m');
export function ManualHeightStep({
nextStep,
prevStep,
@@ -41,8 +45,8 @@ export function ManualHeightStep({
defaultValues: { height: DEFAULT_FULL_HEIGHT },
});
const { sendRPCPacket } = useWebsocketAPI();
const { currentLocales } = useLocaleConfig();
const height = watch('height');
const currentUnit = useUnit();
// Load the last configured height
useEffect(() => {
@@ -53,16 +57,6 @@ export function ManualHeightStep({
});
}, [currentHeight]);
const mFormat = useMemo(
() =>
new Intl.NumberFormat(currentLocales, {
style: 'unit',
unit: 'meter',
maximumFractionDigits: 2,
}),
[currentLocales]
);
const submitFullHeight = (values: HeightForm) => {
const newHeight = values.height * EYE_HEIGHT_TO_HEIGHT_RATIO;
setHmdHeight(newHeight);
@@ -104,17 +98,36 @@ export function ManualHeightStep({
'onboarding-scaled_proportions-manual_height-height-v2'
)}
valueLabelFormat={(value) =>
isNaN(value)
? l10n.getString(
'onboarding-scaled_proportions-manual_height-unknown'
)
: mFormat.format(value)
isNaN(value) ? (
l10n.getString(
'onboarding-scaled_proportions-manual_height-unknown'
)
) : (
<HeightDisplay
height={value}
unitDisplay="narrow"
roundInches
/>
)
}
min={MIN_HEIGHT}
max={4}
step={0.01}
showButtonWithNumber
doubleStep={0.1}
step={
currentUnit === 'm'
? 0.01
: (v, s) =>
Math.round((v + INCH_IN_METER * s) / INCH_IN_METER) *
INCH_IN_METER
}
showButtonWithNumber={currentUnit === 'm' ? 10 : 12}
doubleStep={
currentUnit === 'm'
? 0.1
: (v, s) =>
Math.round(
(v + convert(1, 'ft').to('m') * s) / INCH_IN_METER
) * INCH_IN_METER
}
/>
</div>
<div className="flex flex-col self-center items-center justify-center">
@@ -124,7 +137,7 @@ export function ManualHeightStep({
)}
</Typography>
<Typography>
{mFormat.format(height * EYE_HEIGHT_TO_HEIGHT_RATIO)}
<HeightDisplay height={height * EYE_HEIGHT_TO_HEIGHT_RATIO} />
</Typography>
</div>
</div>

View File

@@ -18,6 +18,7 @@ import { Range } from '@/components/commons/Range';
import { Dropdown } from '@/components/commons/Dropdown';
import { ArrowRightLeftIcon } from '@/components/commons/icon/ArrowIcons';
import { isTrayAvailable } from '@/utils/tauri';
import { UnitSelector } from '@/components/commons/UnitSelector';
import { isTauri } from '@tauri-apps/api/core';
import { TauriFileInput } from '@/components/commons/TauriFileInput';
@@ -552,6 +553,20 @@ export function InterfaceSettings() {
<div className="grid sm:grid-cols-2 pb-4">
<LangSelector alignment="left" />
</div>
<Typography bold>
{l10n.getString('settings-interface-appearance-unit_system')}
</Typography>
<div className="flex flex-col pt-1 pb-2">
<Typography color="secondary">
{l10n.getString(
'settings-interface-appearance-unit_system-description'
)}
</Typography>
</div>
<div className="grid sm:grid-cols-2 pb-4">
<UnitSelector alignment="left" />
</div>
</>
</SettingsPagePaneLayout>
</form>

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useState } from 'react';
import { createContext, useContext, useMemo, useState } from 'react';
import {
defaultValues as defaultDevSettings,
DeveloperModeWidgetForm,
@@ -25,6 +25,11 @@ export enum AssignMode {
All = 'all',
}
export enum UnitType {
Metric = 'metric',
Imperial = 'imperial',
}
export interface Config {
debug: boolean;
lang: string;
@@ -46,6 +51,7 @@ export interface Config {
showNavbarOnboarding: boolean;
vrcMutedWarnings: string[];
bvhDirectory: string | null;
unitSystem: UnitType;
}
export interface ConfigContext {
@@ -73,6 +79,7 @@ export const defaultConfig: Config = {
decorations: false,
showNavbarOnboarding: true,
vrcMutedWarnings: [],
unitSystem: UnitType.Metric,
devSettings: defaultDevSettings,
bvhDirectory: null,
};
@@ -201,3 +208,11 @@ export function useConfig() {
}
return context;
}
export function useUnit() {
const { config } = useConfig();
return useMemo(() => {
const unitSystem = config?.unitSystem ?? defaultConfig.unitSystem;
return unitSystem === UnitType.Metric ? 'm' : 'in';
}, [config?.unitSystem]);
}

View File

@@ -2,6 +2,7 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useWebsocketAPI } from './websocket-api';
import { RpcMessage, SettingsRequestT, SettingsResponseT } from 'solarxr-protocol';
import { MIN_HEIGHT } from './manual-proportions';
import convert from 'convert';
export interface HeightContext {
hmdHeight: number | null;
@@ -73,4 +74,7 @@ export const EYE_HEIGHT_TO_HEIGHT_RATIO = 0.936;
// Based on average human height (1.65m)
// From https://ourworldindata.org/human-height (January 2024)
export const DEFAULT_FULL_HEIGHT = 1.65;
export const DEFAULT_FULL_HEIGHT_INCHES = Math.round(
convert(DEFAULT_FULL_HEIGHT, 'm').to('in')
);
export const DEFAULT_EYE_HEIGHT = DEFAULT_FULL_HEIGHT * EYE_HEIGHT_TO_HEIGHT_RATIO;

View File

@@ -258,10 +258,12 @@ const config = {
addUtilities({
'.text-main-title': textConfig('calc(var(--font-size-title) / 16)', 700),
'.text-section-title': textConfig('calc(var(--font-size-vr) / 16)', 700),
'.text-standard': textConfig('calc(var(--font-size-standard) / 16)', 500),
'.text-standard-bold': textConfig('calc(var(--font-size-standard) / 16)', 700),
'.text-vr-accesible': textConfig('calc(var(--font-size-vr) / 16)', 500),
'.text-vr-accesible-bold': textConfig('calc(var(--font-size-vr) / 16)', 700),
'.text-standard-bold': textConfig('calc(var(--font-size-standard) / 16)', 700),
});
}),
plugin(function ({ addVariant }) {

8
pnpm-lock.yaml generated
View File

@@ -83,6 +83,9 @@ importers:
classnames:
specifier: ^2.5.1
version: 2.5.1
convert:
specifier: ^5.10.0
version: 5.10.0
flatbuffers:
specifier: 22.10.26
version: 22.10.26
@@ -1868,6 +1871,9 @@ packages:
resolution: {integrity: sha512-cj09EBuObp9gZNQCzc7hByQyrs6jVGE+o9kSJmeUoj+GiPiJvi5LYqEH/Hmme4+MTLHM+Ejtq+FChpjjEnsPdQ==}
engines: {node: '>= 4'}
convert@5.10.0:
resolution: {integrity: sha512-1agZ7UULsa0AVUZefVDmfy7hdHDIm7hr/tB8scKwSy8HE8a+5c3EQsCTEQfx6/+8LGNa7Re2+4fir9mKxFsBuw==}
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
@@ -6156,6 +6162,8 @@ snapshots:
convert-to-spaces@1.0.2: {}
convert@5.10.0: {}
create-require@1.1.1: {}
cross-env@7.0.3: