diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 6c33547edc..12485399fc 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -185,6 +185,7 @@ widget-imu_visualizer-rotation_raw = Raw rotation widget-imu_visualizer-rotation_preview = Preview rotation widget-imu_visualizer-acceleration = Acceleration widget-imu_visualizer-position = Position +widget-imu_visualizer-stay_aligned = Stay Aligned ## Widget: Skeleton Visualizer widget-skeleton_visualizer-preview = Skeleton preview @@ -209,6 +210,7 @@ tracker-table-column-temperature = Temp. °C tracker-table-column-linear-acceleration = Accel. X/Y/Z tracker-table-column-rotation = Rotation X/Y/Z tracker-table-column-position = Position X/Y/Z +tracker-table-column-stay_aligned = Stay Aligned tracker-table-column-url = URL ## Tracker rotation @@ -338,6 +340,7 @@ mounting_selection_menu-close = Close settings-sidebar-title = Settings settings-sidebar-general = General settings-sidebar-tracker_mechanics = Tracker mechanics +settings-sidebar-stay_aligned = Stay Aligned settings-sidebar-fk_settings = Tracking settings settings-sidebar-gesture_control = Gesture control settings-sidebar-interface = Interface @@ -428,6 +431,23 @@ settings-general-tracker_mechanics-use_mag_on_all_trackers-description = Can be disabled per tracker in the tracker's settings. Please don't shutdown any of the trackers while toggling this! settings-general-tracker_mechanics-use_mag_on_all_trackers-label = Use magnetometer on trackers +settings-general-stay_aligned = Stay Aligned +settings-general-stay_aligned-description = Keeps your trackers aligned by slowly adjusting the yaw of your trackers. +settings-general-stay_aligned-warnings-drift_compensation = ⚠ Please disable "Drift Compensation". Stay Aligned and Drift Compensation try to solve the same problem and only one should be enabled. +settings-general-stay_aligned-enabled-label = Enabled +settings-general-stay_aligned-amount-label = Maximum yaw adjustment rate +settings-general-stay_aligned-amount-description = Pick a rate depending on how good your IMU is: 0.1 deg/s for ICM45686, LSM6DSV or BNO085; 0.2 deg/s for LSM6DSR or BMI270; and 0.4 deg/s for BMI160. +settings-general-stay_aligned-relaxed_body_angles-label = Relaxed Body Angles +settings-general-stay_aligned-relaxed_body_angles-description = Stay Aligned needs to know your pose when you are relaxed. These angles describe how much you twist your legs and feet outwards. Do a yaw reset, then stand in a relaxed position, and press "Auto detect angles". Repeat while relaxing in a chair, and lying on your back. +settings-general-stay_aligned-relaxed_body_angles-standing-label = Standing +settings-general-stay_aligned-relaxed_body_angles-sitting-label = Sitting in chair +settings-general-stay_aligned-relaxed_body_angles-lying_on_back-label = Lying on back +settings-general-stay_aligned-relaxed_body_angles-upper_leg_angle = Upper leg +settings-general-stay_aligned-relaxed_body_angles-lower_leg_angle = Lower leg +settings-general-stay_aligned-relaxed_body_angles-foot_angle = Foot +settings-general-stay_aligned-relaxed_body_angles-auto_detect = Auto detect angles +settings-general-stay_aligned-relaxed_body_angles-reset = Reset angles + ## FK/Tracking settings settings-general-fk_settings = Tracking settings diff --git a/gui/src/components/settings/SettingsSidebar.tsx b/gui/src/components/settings/SettingsSidebar.tsx index ad6ddb29b6..d84e398eb3 100644 --- a/gui/src/components/settings/SettingsSidebar.tsx +++ b/gui/src/components/settings/SettingsSidebar.tsx @@ -58,6 +58,9 @@ export function SettingsSidebar() { {l10n.getString('settings-sidebar-tracker_mechanics')} + + {l10n.getString('settings-general-stay_aligned')} + {l10n.getString('settings-sidebar-fk_settings')} diff --git a/gui/src/components/settings/pages/GeneralSettings.tsx b/gui/src/components/settings/pages/GeneralSettings.tsx index 3946b551fc..4a4dd693ec 100644 --- a/gui/src/components/settings/pages/GeneralSettings.tsx +++ b/gui/src/components/settings/pages/GeneralSettings.tsx @@ -16,6 +16,7 @@ import { SettingsResponseT, SteamVRTrackersSettingT, TapDetectionSettingsT, + YawCorrectionSettingsT, } from 'solarxr-protocol'; import { useConfig } from '@/hooks/config'; import { useWebsocketAPI } from '@/hooks/websocket-api'; @@ -33,8 +34,9 @@ import { import { HandsWarningModal } from '@/components/settings/HandsWarningModal'; import { MagnetometerToggleSetting } from './MagnetometerToggleSetting'; import { DriftCompensationModal } from '@/components/settings/DriftCompensationModal'; +import { StayAlignedSettings } from './components/StayAlignedSettings'; -interface SettingsForm { +export interface SettingsForm { trackers: { waist: boolean; chest: boolean; @@ -104,6 +106,18 @@ interface SettingsForm { saveMountingReset: boolean; resetHmdPitch: boolean; }; + yawCorrectionSettings: { + enabled: boolean; + amountInDegPerSec: number; + standingUpperLegAngle: number; + standingLowerLegAngle: number; + standingFootAngle: number; + sittingUpperLegAngle: number; + sittingLowerLegAngle: number; + sittingFootAngle: number; + lyingOnBackUpperLegAngle: number; + lyingOnBackLowerLegAngle: number; + }; } const defaultValues: SettingsForm = { @@ -171,6 +185,18 @@ const defaultValues: SettingsForm = { saveMountingReset: false, resetHmdPitch: false, }, + yawCorrectionSettings: { + enabled: true, + amountInDegPerSec: 0.2, + standingUpperLegAngle: 0.0, + standingLowerLegAngle: 0.0, + standingFootAngle: 0.0, + sittingUpperLegAngle: 0.0, + sittingLowerLegAngle: 0.0, + sittingFootAngle: 0.0, + lyingOnBackUpperLegAngle: 0.0, + lyingOnBackLowerLegAngle: 0.0, + }, }; export function GeneralSettings() { @@ -300,6 +326,28 @@ export function GeneralSettings() { driftCompensation.maxResets = values.driftCompensation.maxResets; settings.driftCompensation = driftCompensation; + const yawCorrectionSettings = new YawCorrectionSettingsT(); + yawCorrectionSettings.enabled = values.yawCorrectionSettings.enabled; + yawCorrectionSettings.amountInDegPerSec = + values.yawCorrectionSettings.amountInDegPerSec; + yawCorrectionSettings.standingUpperLegAngle = + values.yawCorrectionSettings.standingUpperLegAngle; + yawCorrectionSettings.standingLowerLegAngle = + values.yawCorrectionSettings.standingLowerLegAngle; + yawCorrectionSettings.standingFootAngle = + values.yawCorrectionSettings.standingFootAngle; + yawCorrectionSettings.sittingUpperLegAngle = + values.yawCorrectionSettings.sittingUpperLegAngle; + yawCorrectionSettings.sittingLowerLegAngle = + values.yawCorrectionSettings.sittingLowerLegAngle; + yawCorrectionSettings.sittingFootAngle = + values.yawCorrectionSettings.sittingFootAngle; + yawCorrectionSettings.lyingOnBackUpperLegAngle = + values.yawCorrectionSettings.lyingOnBackUpperLegAngle; + yawCorrectionSettings.lyingOnBackLowerLegAngle = + values.yawCorrectionSettings.lyingOnBackLowerLegAngle; + settings.yawCorrectionSettings = yawCorrectionSettings; + if (values.resetsSettings) { const resetsSettings = new ResetsSettingsT(); resetsSettings.resetMountingFeet = @@ -423,6 +471,10 @@ export function GeneralSettings() { formData.resetsSettings = settings.resetsSettings; } + if (settings.yawCorrectionSettings) { + formData.yawCorrectionSettings = settings.yawCorrectionSettings; + } + reset({ ...getValues(), ...formData }); }); @@ -824,6 +876,11 @@ export function GeneralSettings() { /> + } id="fksettings" diff --git a/gui/src/components/settings/pages/components/StayAlignedSettings.tsx b/gui/src/components/settings/pages/components/StayAlignedSettings.tsx new file mode 100644 index 0000000000..e9c117bda6 --- /dev/null +++ b/gui/src/components/settings/pages/components/StayAlignedSettings.tsx @@ -0,0 +1,381 @@ +import { FlatDeviceTracker } from '@/hooks/app'; +import { normalizeAngleAroundZero, RAD_TO_DEG } from '@/maths/angle'; +import { QuaternionFromQuatT } from '@/maths/quaternion'; +import { Control, FieldPath, UseFormSetValue } from 'react-hook-form'; +import { BodyPart } from 'solarxr-protocol'; +import { SettingsForm } from '@/components/settings/pages/GeneralSettings'; +import { Button } from '@/components/commons/Button'; +import { CheckBox } from '@/components/commons/Checkbox'; +import { WrenchIcon } from '@/components/commons/icon/WrenchIcons'; +import { NumberSelector } from '@/components/commons/NumberSelector'; +import { Typography } from '@/components/commons/Typography'; +import { SettingsPagePaneLayout } from '@/components/settings/SettingsPageLayout'; +import { useLocalization } from '@fluent/react'; +import { useTrackers } from '@/hooks/tracker'; +import { useLocaleConfig } from '@/i18n/config'; +import { Euler } from 'three'; + +export function StayAlignedSettings({ + getValues, + setValue, + control, +}: { + getValues: () => SettingsForm; + setValue: UseFormSetValue; + control: Control; +}) { + const { l10n } = useLocalization(); + const { currentLocales } = useLocaleConfig(); + const degreePerSecFormat = new Intl.NumberFormat(currentLocales, { + style: 'unit', + unit: 'degree-per-second', + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + const degreeFormat = new Intl.NumberFormat(currentLocales, { + style: 'unit', + unit: 'degree', + maximumFractionDigits: 0, + }); + + const { useConnectedIMUTrackers } = useTrackers(); + const trackers = useConnectedIMUTrackers(); + + const values = getValues(); + + const yawBetweenInDeg = ( + leftTracker: FlatDeviceTracker, + rightTracker: FlatDeviceTracker + ) => { + const leftTrackerYaw = new Euler().setFromQuaternion( + QuaternionFromQuatT(leftTracker.tracker.rotationReferenceAdjusted), + 'YZX' + ).y; + const rightTrackerYaw = new Euler().setFromQuaternion( + QuaternionFromQuatT(rightTracker.tracker.rotationReferenceAdjusted), + 'YZX' + ).y; + const yawDelta = normalizeAngleAroundZero(leftTrackerYaw - rightTrackerYaw); + return yawDelta * RAD_TO_DEG; + }; + + function findTracker(bodyPart: BodyPart): FlatDeviceTracker | undefined { + return trackers.find((t) => t.tracker.info?.bodyPart === bodyPart); + } + + const detectAngles = ( + upperLegKey: FieldPath, + lowerLegKey: FieldPath, + footKey?: FieldPath + ) => { + const leftUpperLegTracker = findTracker(BodyPart.LEFT_UPPER_LEG); + const rightUpperLegTracker = findTracker(BodyPart.RIGHT_UPPER_LEG); + if (leftUpperLegTracker && rightUpperLegTracker) { + const upperLegToBodyAngleInDeg = + yawBetweenInDeg(leftUpperLegTracker, rightUpperLegTracker) / 2.0; + setValue(upperLegKey, Math.round(upperLegToBodyAngleInDeg)); + } + + const leftLowerLegTracker = findTracker(BodyPart.LEFT_LOWER_LEG); + const rightLowerLegTracker = findTracker(BodyPart.RIGHT_LOWER_LEG); + if (leftLowerLegTracker && rightLowerLegTracker) { + const footToBodyAngleInDeg = + yawBetweenInDeg(leftLowerLegTracker, rightLowerLegTracker) / 2.0; + setValue(lowerLegKey, Math.round(footToBodyAngleInDeg)); + } + + if (footKey) { + const leftFootTracker = findTracker(BodyPart.LEFT_FOOT); + const rightFootTracker = findTracker(BodyPart.RIGHT_FOOT); + if (leftFootTracker && rightFootTracker) { + const footToBodyAngleInDeg = + yawBetweenInDeg(leftFootTracker, rightFootTracker) / 2.0; + setValue(footKey, Math.round(footToBodyAngleInDeg)); + } + } + }; + + const resetAngles = ( + upperLegKey: FieldPath, + lowerLegKey: FieldPath, + footKey?: FieldPath + ) => { + setValue(upperLegKey, 0.0); + setValue(lowerLegKey, 0.0); + if (footKey) { + setValue(footKey, 0.0); + } + }; + + return ( + } id="stayaligned"> + + {l10n.getString('settings-general-stay_aligned')} + +
+ {l10n + .getString('settings-general-stay_aligned-description') + .split('\n') + .map((line, i) => ( + + {line} + + ))} + {values.yawCorrectionSettings.enabled && ( + <> + {!!values.driftCompensation.enabled && ( +
+ {l10n.getString( + 'settings-general-stay_aligned-warnings-drift_compensation' + )} +
+ )} + + )} +
+
+ +
+ + {l10n.getString('settings-general-stay_aligned-amount-label')} + + + {l10n.getString('settings-general-stay_aligned-amount-description')} + + degreePerSecFormat.format(value)} + min={0.02} + max={2.0} + step={0.02} + /> +
+
+ + {l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-label' + )} + + + {l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-description' + )} + +
+
+ + {l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-standing-label' + )} + +
+ + `${l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-upper_leg_angle' + )}: ${degreeFormat.format(value)}` + } + min={-90.0} + max={90.0} + step={1.0} + /> + + `${l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-lower_leg_angle' + )}: ${degreeFormat.format(value)}` + } + min={-90.0} + max={90.0} + step={1.0} + /> + + `${l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-foot_angle' + )}: ${degreeFormat.format(value)}` + } + min={-90.0} + max={90.0} + step={1.0} + /> + + +
+
+
+ + {l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-sitting-label' + )} + +
+ + `${l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-upper_leg_angle' + )}: ${degreeFormat.format(value)}` + } + min={-90.0} + max={90.0} + step={1.0} + /> + + `${l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-lower_leg_angle' + )}: ${degreeFormat.format(value)}` + } + min={-90.0} + max={90.0} + step={1.0} + /> + + `${l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-foot_angle' + )}: ${degreeFormat.format(value)}` + } + min={-90.0} + max={90.0} + step={1.0} + /> + + +
+
+
+ + {l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-lying_on_back-label' + )} + +
+ + `${l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-upper_leg_angle' + )}: ${degreeFormat.format(value)}` + } + min={-90.0} + max={90.0} + step={1.0} + /> + + `${l10n.getString( + 'settings-general-stay_aligned-relaxed_body_angles-lower_leg_angle' + )}: ${degreeFormat.format(value)}` + } + min={-90.0} + max={90.0} + step={1.0} + /> +
+ + +
+
+
+
+ ); +} diff --git a/gui/src/components/tracker/StayAlignedInfo.tsx b/gui/src/components/tracker/StayAlignedInfo.tsx new file mode 100644 index 0000000000..9092e5f35d --- /dev/null +++ b/gui/src/components/tracker/StayAlignedInfo.tsx @@ -0,0 +1,50 @@ +import { Typography } from '@/components/commons/Typography'; +import { useLocaleConfig } from '@/i18n/config'; +import { angleIsNearZero } from '@/maths/angle'; +import { TrackerDataT } from 'solarxr-protocol'; + +export function StayAlignedInfo({ + color, + tracker, +}: { + color: 'primary' | 'secondary'; + tracker: TrackerDataT; +}) { + const { currentLocales } = useLocaleConfig(); + const degreeFormat = new Intl.NumberFormat(currentLocales, { + style: 'unit', + unit: 'degree', + minimumFractionDigits: 1, + maximumFractionDigits: 1, + }); + const errorFormat = new Intl.NumberFormat(currentLocales, { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + }); + + const locked = tracker.stayAlignedLocked ? '🔒' : ''; + + const delta = `Δ=${degreeFormat.format(tracker.stayAlignedYawCorrectionInDeg)}`; + + const errors = []; + const maxErrorToShow = 0.1; + if (!angleIsNearZero(tracker.stayAlignedLockedErrorInDeg, maxErrorToShow)) { + errors.push(`L=${errorFormat.format(tracker.stayAlignedLockedErrorInDeg)}`); + } + if (!angleIsNearZero(tracker.stayAlignedCenterErrorInDeg, maxErrorToShow)) { + errors.push(`C=${errorFormat.format(tracker.stayAlignedCenterErrorInDeg)}`); + } + if (!angleIsNearZero(tracker.stayAlignedNeighborErrorInDeg, maxErrorToShow)) { + errors.push( + `N=${errorFormat.format(tracker.stayAlignedNeighborErrorInDeg)}` + ); + } + + const error = errors.length > 0 ? `(${errors.join(', ')})` : ''; + + return ( + + {locked} {delta} {error} + + ); +} diff --git a/gui/src/components/tracker/TrackersTable.tsx b/gui/src/components/tracker/TrackersTable.tsx index cdba255964..f36cbcb236 100644 --- a/gui/src/components/tracker/TrackersTable.tsx +++ b/gui/src/components/tracker/TrackersTable.tsx @@ -17,6 +17,7 @@ import { TrackerBattery } from './TrackerBattery'; import { TrackerStatus } from './TrackerStatus'; import { TrackerWifi } from './TrackerWifi'; import { trackerStatusRelated, useStatusContext } from '@/hooks/status-system'; +import { StayAlignedInfo } from './StayAlignedInfo'; enum DisplayColumn { NAME, @@ -28,6 +29,7 @@ enum DisplayColumn { TEMPERATURE, LINEAR_ACCELERATION, POSITION, + STAY_ALIGNED, URL, } @@ -41,6 +43,7 @@ const displayColumns: { [k: string]: boolean } = { [DisplayColumn.TEMPERATURE]: true, [DisplayColumn.LINEAR_ACCELERATION]: true, [DisplayColumn.POSITION]: true, + [DisplayColumn.STAY_ALIGNED]: true, [DisplayColumn.URL]: true, }; @@ -196,6 +199,7 @@ export function TrackersTable({ displayColumns[DisplayColumn.TEMPERATURE] = hasTemperature || false; displayColumns[DisplayColumn.POSITION] = moreInfo || false; displayColumns[DisplayColumn.LINEAR_ACCELERATION] = moreInfo || false; + displayColumns[DisplayColumn.STAY_ALIGNED] = moreInfo || false; displayColumns[DisplayColumn.URL] = moreInfo || false; const displayColumnsKeys = Object.keys(displayColumns).filter( (k) => displayColumns[k] @@ -362,6 +366,15 @@ export function TrackersTable({ ), })} + {column({ + id: DisplayColumn.STAY_ALIGNED, + label: l10n.getString('tracker-table-column-stay_aligned'), + labelClassName: 'w-36', + row: ({ tracker }) => ( + + ), + })} + {column({ id: DisplayColumn.URL, label: l10n.getString('tracker-table-column-url'), diff --git a/gui/src/components/widgets/IMUVisualizerWidget.tsx b/gui/src/components/widgets/IMUVisualizerWidget.tsx index 5cecbbf9fa..4a130f38cd 100644 --- a/gui/src/components/widgets/IMUVisualizerWidget.tsx +++ b/gui/src/components/widgets/IMUVisualizerWidget.tsx @@ -12,6 +12,7 @@ import { useLocalization } from '@fluent/react'; import { Vector3Object } from '@/maths/vector3'; import { Gltf } from '@react-three/drei'; import { ErrorBoundary } from 'react-error-boundary'; +import { StayAlignedInfo } from '@/components/tracker/StayAlignedInfo'; const groundColor = '#4444aa'; @@ -157,6 +158,15 @@ export function IMUVisualizerWidget({ tracker }: { tracker: TrackerDataT }) { )} + {!!tracker.stayAlignedYawCorrectionInDeg && ( +
+ + {l10n.getString('widget-imu_visualizer-stay_aligned')} + + +
+ )} + {!enabled && (