diff --git a/gui/package.json b/gui/package.json index 138723e1b..0f5d98470 100644 --- a/gui/package.json +++ b/gui/package.json @@ -22,6 +22,7 @@ "@tauri-apps/plugin-os": "^2.0.0", "@tauri-apps/plugin-shell": "^2.0.0", "@tauri-apps/plugin-store": "^2.0.0", + "@tweenjs/tween.js": "^25.0.0", "@twemoji/svg": "^15.0.0", "browser-fs-access": "^0.35.0", "classnames": "^2.5.1", diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 3f4f27c53..2e8ee6de1 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -555,7 +555,7 @@ settings-stay_aligned-relaxed_poses-description = Stay Aligned uses your relaxed settings-stay_aligned-relaxed_poses-standing = Adjust trackers while standing settings-stay_aligned-relaxed_poses-sitting = Adjust trackers while sitting in a chair settings-stay_aligned-relaxed_poses-flat = Adjust trackers while sitting on the floor, or lying on your back -settings-stay_aligned-relaxed_poses-detect_pose = Detect pose +settings-stay_aligned-relaxed_poses-save_pose = Save pose settings-stay_aligned-relaxed_poses-reset_pose = Reset pose settings-stay_aligned-debug-label = Debugging settings-stay_aligned-debug-description = Please include your settings when reporting problems about Stay Aligned. @@ -1111,8 +1111,9 @@ onboarding-automatic_mounting-mounting_reset-title = Mounting Reset onboarding-automatic_mounting-mounting_reset-step-0 = 1. Squat in a "skiing" pose with your legs bent, your upper body tilted forwards, and your arms bent. onboarding-automatic_mounting-mounting_reset-step-1 = 2. Press the "Reset Mounting" button and wait for 3 seconds before the trackers' mounting orientations will reset. onboarding-automatic_mounting-preparation-title = Preparation -onboarding-automatic_mounting-preparation-step-0 = 1. Stand upright with your arms to your sides. -onboarding-automatic_mounting-preparation-step-1 = 2. Press the "Full Reset" button and wait for 3 seconds before the trackers will reset. +onboarding-automatic_mounting-preparation-v2-step-0 = 1. Press the "Full Reset" button. +onboarding-automatic_mounting-preparation-v2-step-1 = 2. Stand upright with your arms to your sides. Make sure to look forward. +onboarding-automatic_mounting-preparation-v2-step-2 = 3. Hold the position until the 3s timer ends. onboarding-automatic_mounting-put_trackers_on-title = Put on your trackers onboarding-automatic_mounting-put_trackers_on-description = To calibrate mounting orientations, 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_mounting-put_trackers_on-next = I have all my trackers on @@ -1248,26 +1249,31 @@ onboarding-scaled_proportions-done-description = Your body proportions should no ## Stay Aligned setup onboarding-stay_aligned-title = Stay Aligned onboarding-stay_aligned-description = Configure Stay Aligned to keep your trackers aligned. +onboarding-stay_aligned-put_trackers_on-title = Put on your trackers +onboarding-stay_aligned-put_trackers_on-description = To save your resting poses, we'll use the trackers you just assigned. Put on all your trackers, you can see which are which in the figure to the right. +onboarding-stay_aligned-put_trackers_on-trackers_warning = You have fewer than 5 trackers currently connected and assigned! This is the minimum amount of trackers required for Stay Aligned to function properly. +onboarding-stay_aligned-put_trackers_on-next = I have all my trackers on onboarding-stay_aligned-verify_mounting-title = Check your Mounting onboarding-stay_aligned-verify_mounting-step-0 = Stay Aligned requires good mounting. Otherwise, you won't get a good experience with Stay Aligned. onboarding-stay_aligned-verify_mounting-step-1 = 1. Move around while standing. onboarding-stay_aligned-verify_mounting-step-2 = 2. Sit down and move your legs and feet. -onboarding-stay_aligned-verify_mounting-step-3 = 3. If your trackers aren't in the right place, restart the process. +onboarding-stay_aligned-verify_mounting-step-3 = 3. If your trackers aren't in the right place, press "Redo Mounting Calibration" +onboarding-stay_aligned-verify_mounting-redo_mounting = Redo Mounting calibration +onboarding-stay_aligned-preparation-title = Preparation +onboarding-stay_aligned-preparation-tip = Make sure to stand upright. You must be looking forward and your arms must be down to your sides. onboarding-stay_aligned-relaxed_poses-standing-title = Relaxed Standing Pose onboarding-stay_aligned-relaxed_poses-standing-step-0 = 1. Stand in a comfortable position. Relax! -onboarding-stay_aligned-relaxed_poses-standing-step-1 = 2. Check that your trackers match your body. If it does not match, you need to restart this flow. onboarding-stay_aligned-relaxed_poses-standing-step-2 = 3. Press the "Detect pose" button. onboarding-stay_aligned-relaxed_poses-sitting-title = Relaxed Sitting in Chair Pose onboarding-stay_aligned-relaxed_poses-sitting-step-0 = 1. Sit in a comfortable position. Relax! -onboarding-stay_aligned-relaxed_poses-sitting-step-1 = 2. Check that your trackers match your body. If it does not match, you need to restart this flow. onboarding-stay_aligned-relaxed_poses-sitting-step-2 = 3. Press the "Detect pose" button. onboarding-stay_aligned-relaxed_poses-flat-title = Relaxed Sitting on Floor Pose onboarding-stay_aligned-relaxed_poses-flat-step-0 = 1. Sit on the floor with your legs in front. Relax! -onboarding-stay_aligned-relaxed_poses-flat-step-1 = 2. Check that your trackers match your body. If it does not match, you need to restart this flow. onboarding-stay_aligned-relaxed_poses-flat-step-2 = 3. Press the "Detect pose" button. onboarding-stay_aligned-relaxed_poses-skip_step = Skip onboarding-stay_aligned-done-title = Stay Aligned enabled! onboarding-stay_aligned-done-description = Your Stay Aligned setup is complete! +onboarding-stay_aligned-done-description-2 = Setup is complete! You may restart the process if you want to re-calibrate the poses onboarding-stay_aligned-previous_step = Previous onboarding-stay_aligned-next_step = Next onboarding-stay_aligned-restart = Restart diff --git a/gui/public/images/reset/FullResetPose.webp b/gui/public/images/reset/FullResetPose.webp new file mode 100644 index 000000000..1aada469c Binary files /dev/null and b/gui/public/images/reset/FullResetPose.webp differ diff --git a/gui/public/images/reset/FullResetPoseSide.webp b/gui/public/images/reset/FullResetPoseSide.webp new file mode 100644 index 000000000..772aab67a Binary files /dev/null and b/gui/public/images/reset/FullResetPoseSide.webp differ diff --git a/gui/public/images/reset/FullResetPoseWrong.webp b/gui/public/images/reset/FullResetPoseWrong.webp new file mode 100644 index 000000000..433b8b880 Binary files /dev/null and b/gui/public/images/reset/FullResetPoseWrong.webp differ diff --git a/gui/public/images/stay-aligned/StayAlignedFloor.webp b/gui/public/images/stay-aligned/StayAlignedFloor.webp new file mode 100644 index 000000000..b1cd3b70f Binary files /dev/null and b/gui/public/images/stay-aligned/StayAlignedFloor.webp differ diff --git a/gui/public/images/stay-aligned/StayAlignedSitting.webp b/gui/public/images/stay-aligned/StayAlignedSitting.webp new file mode 100644 index 000000000..9e222ad49 Binary files /dev/null and b/gui/public/images/stay-aligned/StayAlignedSitting.webp differ diff --git a/gui/public/images/stay-aligned/StayAlignedStanding.webp b/gui/public/images/stay-aligned/StayAlignedStanding.webp new file mode 100644 index 000000000..285c3ca7e Binary files /dev/null and b/gui/public/images/stay-aligned/StayAlignedStanding.webp differ diff --git a/gui/src/components/commons/Button.tsx b/gui/src/components/commons/Button.tsx index eb66eaa7c..c85b579f8 100644 --- a/gui/src/components/commons/Button.tsx +++ b/gui/src/components/commons/Button.tsx @@ -36,6 +36,16 @@ function ButtonContent({ ); } +export type ButtonProps = { + children?: ReactNode; + icon?: ReactNode; + variant: 'primary' | 'secondary' | 'tertiary' | 'quaternary'; + to?: string; + loading?: boolean; + rounded?: boolean; + state?: any; +} & React.ButtonHTMLAttributes; + export function Button({ children, variant, @@ -46,15 +56,7 @@ export function Button({ icon, rounded = false, ...props -}: { - children?: ReactNode; - icon?: ReactNode; - variant: 'primary' | 'secondary' | 'tertiary' | 'quaternary'; - to?: string; - loading?: boolean; - rounded?: boolean; - state?: any; -} & React.ButtonHTMLAttributes) { +}: ButtonProps) { const classes = useMemo(() => { const variantsMap = { primary: classNames({ diff --git a/gui/src/components/commons/VerticalStepper.tsx b/gui/src/components/commons/VerticalStepper.tsx index 33899d710..98ceeea41 100644 --- a/gui/src/components/commons/VerticalStepper.tsx +++ b/gui/src/components/commons/VerticalStepper.tsx @@ -37,7 +37,7 @@ export function VerticalStep({ setTimeout(() => { if (!refTop.current) return; refTop.current.scrollIntoView({ behavior: 'smooth' }); - }, 500); + }, 300); }, [isSelected]); useLayoutEffect(() => { @@ -78,7 +78,7 @@ export function VerticalStep({
{children}
@@ -88,12 +88,13 @@ export function VerticalStep({ ); } -type VerticalStepComponentType = FC<{ +export type VerticalStepComponentProps = { nextStep: () => void; prevStep: () => void; goTo: (id: string) => void; isActive: boolean; -}>; +}; +type VerticalStepComponentType = FC; export type VerticalStep = { title: string; @@ -101,7 +102,13 @@ export type VerticalStep = { component: VerticalStepComponentType; }; -export default function VerticalStepper({ steps }: { steps: VerticalStep[] }) { +export default function VerticalStepper({ + steps, + onStepChange, +}: { + steps: VerticalStep[]; + onStepChange?: (index: number, id?: string) => void; +}) { const [currStep, setStep] = useState(0); const nextStep = () => { @@ -121,6 +128,10 @@ export default function VerticalStepper({ steps }: { steps: VerticalStep[] }) { setStep(step); }; + useEffect(() => { + onStepChange?.(currStep, steps[currStep].id); + }, [currStep]); + return (
    {steps.map(({ title, component: StepComponent }, index) => ( diff --git a/gui/src/components/commons/icon/CheckIcon.tsx b/gui/src/components/commons/icon/CheckIcon.tsx index 840cd3220..72b460c09 100644 --- a/gui/src/components/commons/icon/CheckIcon.tsx +++ b/gui/src/components/commons/icon/CheckIcon.tsx @@ -1,9 +1,16 @@ -export function CheckIcon({ size = 9 }: { size?: number }) { +export function CheckIcon({ + size = 9, + className, +}: { + size?: number; + className?: string; +}) { return ( diff --git a/gui/src/components/commons/icon/CrossIcon.tsx b/gui/src/components/commons/icon/CrossIcon.tsx index 4980d685c..7e63b8153 100644 --- a/gui/src/components/commons/icon/CrossIcon.tsx +++ b/gui/src/components/commons/icon/CrossIcon.tsx @@ -1,9 +1,16 @@ -export function CrossIcon({ size = 20 }: { size: number }) { +export function CrossIcon({ + size = 20, + className, +}: { + size?: number; + className?: string; +}) { return ( )}
- +
+ +
); } diff --git a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Preparation.tsx b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Preparation.tsx index 6e921fa25..9f4a1f45f 100644 --- a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Preparation.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/Preparation.tsx @@ -2,8 +2,9 @@ import { ResetType } from 'solarxr-protocol'; import { Button } from '@/components/commons/Button'; import { Typography } from '@/components/commons/Typography'; import { ResetButton } from '@/components/home/ResetButton'; -import { useLocalization } from '@fluent/react'; -import { useBreakpoint } from '@/hooks/breakpoint'; +import { Localized, useLocalization } from '@fluent/react'; +import { CrossIcon } from '@/components/commons/icon/CrossIcon'; +import { CheckIcon } from '@/components/commons/icon/CheckIcon'; export function PreparationStep({ nextStep, @@ -14,7 +15,6 @@ export function PreparationStep({ prevStep: () => void; variant: 'onboarding' | 'alone'; }) { - const { isMobile } = useBreakpoint('mobile'); const { l10n } = useLocalization(); return ( @@ -25,27 +25,43 @@ export function PreparationStep({ {l10n.getString('onboarding-automatic_mounting-preparation-title')}
- - {l10n.getString( - 'onboarding-automatic_mounting-preparation-step-0' - )} - - - {l10n.getString( - 'onboarding-automatic_mounting-preparation-step-1' - )} - + + + + + + + + +
- {isMobile && ( -
+
+
+ Reset position
- )} +
+ + Reset position side +
+
+ + Reset position wrong +
+
- {!isMobile && ( -
- Reset position -
- )} ); } diff --git a/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx b/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx index 4873091e2..76fe44546 100644 --- a/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx +++ b/gui/src/components/onboarding/pages/mounting/mounting-steps/Preparation.tsx @@ -2,8 +2,9 @@ import { ResetType } from 'solarxr-protocol'; import { Button } from '@/components/commons/Button'; import { Typography } from '@/components/commons/Typography'; import { ResetButton } from '@/components/home/ResetButton'; -import { useLocalization } from '@fluent/react'; -import { useBreakpoint } from '@/hooks/breakpoint'; +import { Localized, useLocalization } from '@fluent/react'; +import { CheckIcon } from '@/components/commons/icon/CheckIcon'; +import { CrossIcon } from '@/components/commons/icon/CrossIcon'; export function PreparationStep({ nextStep, @@ -14,7 +15,6 @@ export function PreparationStep({ prevStep: () => void; variant: 'onboarding' | 'alone'; }) { - const { isMobile } = useBreakpoint('mobile'); const { l10n } = useLocalization(); return ( @@ -25,27 +25,43 @@ export function PreparationStep({ {l10n.getString('onboarding-automatic_mounting-preparation-title')}
- - {l10n.getString( - 'onboarding-automatic_mounting-preparation-step-0' - )} - - - {l10n.getString( - 'onboarding-automatic_mounting-preparation-step-1' - )} - + + + + + + + + +
- {isMobile && ( -
+
+
+ Reset position
- )} +
+ + Reset position side +
+
+ + Reset position wrong +
+
- {!isMobile && ( -
- Reset position -
- )} ); } diff --git a/gui/src/components/onboarding/pages/stay-aligned/StayAlignedSetup.tsx b/gui/src/components/onboarding/pages/stay-aligned/StayAlignedSetup.tsx index 2639d1b0c..ff43af791 100644 --- a/gui/src/components/onboarding/pages/stay-aligned/StayAlignedSetup.tsx +++ b/gui/src/components/onboarding/pages/stay-aligned/StayAlignedSetup.tsx @@ -1,9 +1,6 @@ -import { useOnboarding } from '@/hooks/onboarding'; import { Typography } from '@/components/commons/Typography'; -import { Step, StepperSlider } from '@/components/onboarding/StepperSlider'; import { DoneStep } from './stay-aligned-steps/Done'; import { useLocalization } from '@fluent/react'; -import { autoMountingSteps } from '@/components/onboarding/pages/mounting/AutomaticMounting'; import { FlatRelaxedPoseStep, SittingRelaxedPoseStep, @@ -11,8 +8,18 @@ import { } from './stay-aligned-steps/RelaxedPoseSteps'; import { EnableStayAlignedRequestT, RpcMessage } from 'solarxr-protocol'; import { RPCPacketType, useWebsocketAPI } from '@/hooks/websocket-api'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; +import VerticalStepper from '@/components/commons/VerticalStepper'; +import { useBreakpoint } from '@/hooks/breakpoint'; +import { + SkeletonPreviewView, + SkeletonVisualizerWidget, +} from '@/components/widgets/SkeletonVisualizerWidget'; +import { Vector3 } from 'three'; +import { Easing } from '@tweenjs/tween.js'; import { VerifyMountingStep } from './stay-aligned-steps/VerifyMounting'; +import { PutTrackersOnStep } from './stay-aligned-steps/PutTrackersOnStep'; +import { PreparationStep } from './stay-aligned-steps/PreparationStep'; export function enableStayAligned( enable: boolean, @@ -23,28 +30,108 @@ export function enableStayAligned( sendRPCPacket(RpcMessage.EnableStayAlignedRequest, req); } -const steps: Step[] = [ - ...autoMountingSteps, - { type: 'numbered', component: VerifyMountingStep }, - { type: 'numbered', component: StandingRelaxedPoseStep }, - { type: 'numbered', component: SittingRelaxedPoseStep }, - { type: 'numbered', component: FlatRelaxedPoseStep }, - { type: 'fullsize', component: DoneStep }, -]; export function StayAlignedSetup() { const { l10n } = useLocalization(); - const { state } = useOnboarding(); + const { isMobile } = useBreakpoint('mobile'); const { sendRPCPacket } = useWebsocketAPI(); + const viewsRef = useRef<{ + cam1?: SkeletonPreviewView; + cam2?: SkeletonPreviewView; + }>({}); + useEffect(() => { // Disable Stay Aligned as soon as we enter the setup flow so that we don't // adjust the trackers while trying to set up the feature enableStayAligned(false, sendRPCPacket); }, []); + const updateCamSizes = () => { + const views = viewsRef.current; + if (!views.cam1 || !views.cam2) return; + if (!views.cam2.hidden) { + if (isMobile) { + views.cam1.height = 1; + views.cam2.height = 1; + views.cam2.width = 0.5; + views.cam1.width = 0.5; + views.cam1.bottom = 0; + views.cam2.bottom = 0; + views.cam2.left = 0.5; + } else { + views.cam1.height = 0.5; + views.cam2.height = 0.5; + views.cam2.width = 1; + views.cam1.width = 1; + views.cam1.bottom = 0; + views.cam2.bottom = 0.5; + views.cam2.left = 0; + } + } else { + views.cam1.height = 1; + views.cam2.height = 1; + views.cam2.width = 1; + views.cam1.width = 1; + views.cam1.bottom = 0; + views.cam2.bottom = 0; + views.cam2.left = 0; + } + }; + + const onStepChange = (index: number, id?: string) => { + if (id === 'start') { + enableStayAligned(false, sendRPCPacket); + } + + const views = viewsRef.current; + if (!views.cam1 || !views.cam2) return; + switch (id) { + case 'standing': { + views.cam2.hidden = true; + views.cam1.tween + .stop() + .to(new Vector3(0, 1, -6), 500) + .easing(Easing.Quadratic.InOut) + .startFromCurrentValues(); + break; + } + case 'flat': + case 'sitting': { + views.cam2.hidden = false; + views.cam1.tween + .stop() + .to(new Vector3(-5, 1, -0), 500) + .easing(Easing.Quadratic.InOut) + .startFromCurrentValues(); + + views.cam2.tween + .stop() + .to(new Vector3(0, 4, -0.2), 500) + .easing(Easing.Quadratic.InOut) + .startFromCurrentValues(); + break; + } + default: { + views.cam2.hidden = true; + views.cam1.tween + .stop() + .to(new Vector3(3, 2.5, -3), 1000) + .easing(Easing.Quadratic.InOut) + .startFromCurrentValues(); + break; + } + } + + updateCamSizes(); + }; + + useEffect(() => { + updateCamSizes(); + }, [isMobile]); + return ( -
-
+
+
{l10n.getString('onboarding-stay_aligned-title')} @@ -53,13 +140,90 @@ export function StayAlignedSetup() { {l10n.getString('onboarding-stay_aligned-description')}
-
- +
+
+
+ { + viewsRef.current.cam1 = context.addView({ + left: 0, + bottom: 0, + width: 1, + height: 1, + position: new Vector3(3, 2.5, -3), + onHeightChange(v, newHeight) { + v.controls.target.set(0, newHeight / 2, 0); + const scale = Math.max(1.8, newHeight) / 1.8; + v.camera.zoom = 1 / scale; + }, + }); + + viewsRef.current.cam2 = context.addView({ + left: 0, + bottom: 0.5, + width: 1, + height: 0.5, + hidden: true, + position: new Vector3(3, 2.5, -3), + onHeightChange(v, newHeight) { + v.controls.target.set(0, newHeight / 2, 0); + const scale = Math.max(1.8, newHeight) / 1.8; + v.camera.zoom = 1 / scale; + }, + }); + }} + > +
); } diff --git a/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/Done.tsx b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/Done.tsx index 634fc5ae9..d612603bc 100644 --- a/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/Done.tsx +++ b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/Done.tsx @@ -1,45 +1,31 @@ import { Button } from '@/components/commons/Button'; import { Typography } from '@/components/commons/Typography'; -import { SkeletonVisualizerWidget } from '@/components/widgets/SkeletonVisualizerWidget'; -import { useLocalization } from '@fluent/react'; - -export function DoneStep({ - resetSteps, - variant, -}: { - resetSteps: () => void; - variant: 'onboarding' | 'alone'; -}) { - const { l10n } = useLocalization(); +import { VerticalStepComponentProps } from '@/components/commons/VerticalStepper'; +import { Localized } from '@fluent/react'; +export function DoneStep({ goTo }: VerticalStepComponentProps) { return ( -
-
- - {l10n.getString('onboarding-stay_aligned-done-title')} - - - {l10n.getString('onboarding-stay_aligned-done-description')} - +
+
+ + + + + +
- -
- - +
+ + + + + +
- -
); } diff --git a/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/PreparationStep.tsx b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/PreparationStep.tsx new file mode 100644 index 000000000..66b1c52c4 --- /dev/null +++ b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/PreparationStep.tsx @@ -0,0 +1,64 @@ +import { Button } from '@/components/commons/Button'; +import { CheckIcon } from '@/components/commons/icon/CheckIcon'; +import { CrossIcon } from '@/components/commons/icon/CrossIcon'; +import { TipBox } from '@/components/commons/TipBox'; +import { Typography } from '@/components/commons/Typography'; +import { VerticalStepComponentProps } from '@/components/commons/VerticalStepper'; +import { ResetButton } from '@/components/home/ResetButton'; +import { Localized } from '@fluent/react'; +import { ResetType } from 'solarxr-protocol'; + +export function PreparationStep({ + nextStep, + prevStep, +}: VerticalStepComponentProps) { + return ( +
+
+ + + + + + + + + +
+ + TIP + +
+
+ + Reset position +
+
+ + Reset position side +
+
+ + Reset position wrong +
+
+
+ + + + + +
+
+ ); +} diff --git a/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/PutTrackersOnStep.tsx b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/PutTrackersOnStep.tsx new file mode 100644 index 000000000..6ee645fa0 --- /dev/null +++ b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/PutTrackersOnStep.tsx @@ -0,0 +1,61 @@ +import { BodyDisplay } from '@/components/commons/BodyDisplay'; +import { Button } from '@/components/commons/Button'; +import { TipBox, WarningBox } from '@/components/commons/TipBox'; +import { Typography } from '@/components/commons/Typography'; +import { VerticalStepComponentProps } from '@/components/commons/VerticalStepper'; +import { assignedTrackersAtom } from '@/store/app-store'; +import { Localized } from '@fluent/react'; +import { useAtomValue } from 'jotai'; + +export function PutTrackersOnStep({ nextStep }: VerticalStepComponentProps) { + const assignedTrackers = useAtomValue(assignedTrackersAtom); + + // Keep the button while in dev + const canContinue = assignedTrackers.length >= 5 || import.meta.env.DEV; + + return ( +
+
+
+
+ + + +
+
+ + Tip + +
+ {assignedTrackers.length < 5 && ( +
+ + Warning + +
+ )} +
+
+ +
+
+
+ + + +
+
+
+
+ ); +} diff --git a/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/RelaxedPoseSteps.tsx b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/RelaxedPoseSteps.tsx index 3d36e0066..205a707fa 100644 --- a/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/RelaxedPoseSteps.tsx +++ b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/RelaxedPoseSteps.tsx @@ -8,110 +8,122 @@ import { useLocalization } from '@fluent/react'; import { StayAlignedRelaxedPose } from 'solarxr-protocol'; import { enableStayAligned } from '@/components/onboarding/pages/stay-aligned/StayAlignedSetup'; import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { VerticalStepComponentProps } from '@/components/commons/VerticalStepper'; +import { ReactNode } from 'react'; -function makeRelaxedPoseStep( - titleKey: string, - descriptionKeys: string[], - imageUrl: string, - relaxedPose: StayAlignedRelaxedPose, - lastStep: boolean -) { - return ({ - nextStep, - prevStep, - variant, - }: { - nextStep: () => void; - prevStep: () => void; - variant: 'onboarding' | 'alone'; - }) => { - const { l10n } = useLocalization(); - const { sendRPCPacket } = useWebsocketAPI(); - - return ( -
-
- - {l10n.getString(titleKey)} +function PosePage({ + nextStep, + prevStep, + descriptionKeys, + children, + relaxedPose, + lastStep = false, +}: VerticalStepComponentProps & { + descriptionKeys: string[]; + children: ReactNode; + relaxedPose: StayAlignedRelaxedPose; + lastStep?: boolean; +}) { + const { l10n } = useLocalization(); + const { sendRPCPacket } = useWebsocketAPI(); + return ( +
+
+ {descriptionKeys.map((descriptionKey) => ( + + {l10n.getString(descriptionKey)} -
- {descriptionKeys.map((descriptionKey) => ( - - {l10n.getString(descriptionKey)} - - ))} -
-
- - { - if (lastStep) { - enableStayAligned(true, sendRPCPacket); - } - nextStep(); - }} - pose={relaxedPose} - /> - { - if (lastStep) { - enableStayAligned(true, sendRPCPacket); - } - nextStep(); - }} - pose={relaxedPose} - > - {l10n.getString( - 'onboarding-stay_aligned-relaxed_poses-skip_step' - )} - -
-
-
- Reset position + ))} +
+
+ {children} +
+
+ +
+ { + if (lastStep) { + enableStayAligned(true, sendRPCPacket); + } + nextStep(); + }} + pose={relaxedPose} + > + {l10n.getString('onboarding-stay_aligned-relaxed_poses-skip_step')} + + { + if (lastStep) { + enableStayAligned(true, sendRPCPacket); + } + nextStep(); + }} + pose={relaxedPose} + />
- ); - }; +
+ ); } -export const StandingRelaxedPoseStep = makeRelaxedPoseStep( - 'onboarding-stay_aligned-relaxed_poses-standing-title', - [ - 'onboarding-stay_aligned-relaxed_poses-standing-step-0', - 'onboarding-stay_aligned-relaxed_poses-standing-step-1', - 'onboarding-stay_aligned-relaxed_poses-standing-step-2', - ], - '/images/relaxed_pose_standing.webp', - StayAlignedRelaxedPose.STANDING, - false +export const StandingRelaxedPoseStep = ( + verticalStepProps: VerticalStepComponentProps +) => ( + + Reset position + ); -export const SittingRelaxedPoseStep = makeRelaxedPoseStep( - 'onboarding-stay_aligned-relaxed_poses-sitting-title', - [ - 'onboarding-stay_aligned-relaxed_poses-sitting-step-0', - 'onboarding-stay_aligned-relaxed_poses-sitting-step-1', - 'onboarding-stay_aligned-relaxed_poses-sitting-step-2', - ], - '/images/relaxed_pose_sitting.webp', - StayAlignedRelaxedPose.SITTING, - false +export const SittingRelaxedPoseStep = ( + verticalStepProps: VerticalStepComponentProps +) => ( + + Reset position + ); -export const FlatRelaxedPoseStep = makeRelaxedPoseStep( - 'onboarding-stay_aligned-relaxed_poses-flat-title', - [ - 'onboarding-stay_aligned-relaxed_poses-flat-step-0', - 'onboarding-stay_aligned-relaxed_poses-flat-step-1', - 'onboarding-stay_aligned-relaxed_poses-flat-step-2', - ], - '/images/relaxed_pose_flat.webp', - StayAlignedRelaxedPose.FLAT, - true +export const FlatRelaxedPoseStep = ( + verticalStepProps: VerticalStepComponentProps +) => ( + + Reset position + ); diff --git a/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/VerifyMounting.tsx b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/VerifyMounting.tsx index 481af2e0f..6e17f90b1 100644 --- a/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/VerifyMounting.tsx +++ b/gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/VerifyMounting.tsx @@ -1,48 +1,43 @@ import { Button } from '@/components/commons/Button'; import { Typography } from '@/components/commons/Typography'; -import { useLocalization } from '@fluent/react'; +import { VerticalStepComponentProps } from '@/components/commons/VerticalStepper'; +import { Localized } from '@fluent/react'; export function VerifyMountingStep({ nextStep, - resetSteps, - variant, -}: { - nextStep: () => void; - resetSteps: () => void; - variant: 'onboarding' | 'alone'; -}) { - const { l10n } = useLocalization(); - + prevStep, +}: VerticalStepComponentProps) { return ( -
-
- - {l10n.getString('onboarding-stay_aligned-verify_mounting-title')} - -
- - {l10n.getString('onboarding-stay_aligned-verify_mounting-step-0')} - - - {l10n.getString('onboarding-stay_aligned-verify_mounting-step-1')} - - - {l10n.getString('onboarding-stay_aligned-verify_mounting-step-2')} - - - {l10n.getString('onboarding-stay_aligned-verify_mounting-step-3')} - -
-
- - +
+
+ + + + + + + + + + + + +
+
+ + + + +
+ + + + + +
diff --git a/gui/src/components/settings/DriftCompensationModal.tsx b/gui/src/components/settings/DriftCompensationModal.tsx deleted file mode 100644 index 1ca4511f6..000000000 --- a/gui/src/components/settings/DriftCompensationModal.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Button } from '@/components/commons/Button'; -import { WarningBox } from '@/components/commons/TipBox'; -import { Localized, useLocalization } from '@fluent/react'; -import { BaseModal } from '@/components/commons/BaseModal'; -import ReactModal from 'react-modal'; - -export function DriftCompensationModal({ - isOpen = true, - onClose, - accept, - ...props -}: { - /** - * Is the parent/sibling component opened? - */ - isOpen: boolean; - /** - * Function to trigger when the warning hasn't been accepted - */ - onClose: () => void; - /** - * Function when you press `I understand` - */ - accept: () => void; -} & ReactModal.Props) { - const { l10n } = useLocalization(); - - return ( - -
-
- }} - > - - Warning: Drift compensation should only be used if you find - you need to reset very often (~5-10 minutes). -
- Some IMUs prone to frequent resets include: Joy-Cons, owoTrack, - and MPUs (without recent firmware). -
-
- -
- - -
-
-
-
- ); -} diff --git a/gui/src/components/settings/pages/GeneralSettings.tsx b/gui/src/components/settings/pages/GeneralSettings.tsx index 267b9c7da..e128074a4 100644 --- a/gui/src/components/settings/pages/GeneralSettings.tsx +++ b/gui/src/components/settings/pages/GeneralSettings.tsx @@ -3,7 +3,6 @@ import { useEffect, useState } from 'react'; import { DefaultValues, useForm } from 'react-hook-form'; import { ChangeSettingsRequestT, - DriftCompensationSettingsT, FilteringSettingsT, FilteringType, LegTweaksSettingsT, @@ -32,7 +31,6 @@ import { } from '@/components/settings/SettingsPageLayout'; import { HandsWarningModal } from '@/components/settings/HandsWarningModal'; import { MagnetometerToggleSetting } from './MagnetometerToggleSetting'; -import { DriftCompensationModal } from '@/components/settings/DriftCompensationModal'; import { defaultStayAlignedSettings, StayAlignedSettings, @@ -59,12 +57,6 @@ export type SettingsForm = { type: number; amount: number; }; - driftCompensation: { - enabled: boolean; - prediction: boolean; - amount: number; - maxResets: number; - }; toggles: { extendedSpine: boolean; extendedPelvis: boolean; @@ -151,12 +143,6 @@ const defaultValues: SettingsForm = { interpKneeAnkle: 0.2, }, filtering: { amount: 0.1, type: FilteringType.NONE }, - driftCompensation: { - enabled: false, - prediction: false, - amount: 0.1, - maxResets: 1, - }, tapDetection: { mountingResetEnabled: false, yawResetEnabled: false, @@ -299,13 +285,6 @@ export function GeneralSettings() { filtering.amount = values.filtering.amount; settings.filtering = filtering; - const driftCompensation = new DriftCompensationSettingsT(); - driftCompensation.enabled = values.driftCompensation.enabled; - driftCompensation.prediction = values.driftCompensation.prediction; - driftCompensation.amount = values.driftCompensation.amount; - driftCompensation.maxResets = values.driftCompensation.maxResets; - settings.driftCompensation = driftCompensation; - settings.stayAligned = serializeStayAlignedSettings(values.stayAligned); if (values.resetsSettings) { @@ -344,10 +323,6 @@ export function GeneralSettings() { formData.filtering = settings.filtering; } - if (settings.driftCompensation) { - formData.driftCompensation = settings.driftCompensation; - } - if (settings.steamVrTrackers) { formData.trackers = settings.steamVrTrackers; if ( @@ -463,8 +438,6 @@ export function GeneralSettings() { // } // }, [state]); - const [showDriftCompWarning, setShowDriftCompWarning] = useState(false); - return (
-
- - {l10n.getString( - 'settings-general-tracker_mechanics-drift_compensation' - )} - -
- {l10n - .getString( - 'settings-general-tracker_mechanics-drift_compensation-description' - ) - .split('\n') - .map((line, i) => ( - - {line} - - ))} -
- { - if (getValues('driftCompensation.enabled')) { - return; - } - - setShowDriftCompWarning(true); - }} - /> -
- - {l10n.getString( - 'settings-general-tracker_mechanics-drift_compensation-prediction' - )} - -
- {l10n - .getString( - 'settings-general-tracker_mechanics-drift_compensation-prediction-description' - ) - .split('\n') - .map((line, i) => ( - - {line} - - ))} -
- - { - setShowDriftCompWarning(false); - }} - onClose={() => { - setShowDriftCompWarning(false); - setValue('driftCompensation.enabled', false); - }} - isOpen={showDriftCompWarning} - > -
- percentageFormat.format(value)} - min={0.1} - max={1.0} - step={0.1} - /> -
-
- -
{ OTHER ===== -Drift compensation: ${values.driftCompensation.enabled ? 'true <------------ WTF' : 'false'} Filtering: type=${values.filtering.type} amount=${numberFormat.format(values.filtering.amount)} Enforce constraints: ${boolify(values.toggles.enforceConstraints)} Skating correction: ${boolify(values.toggles.skatingCorrection)} @@ -190,14 +189,7 @@ export function StayAlignedSettings({ {l10n.getString('settings-stay_aligned-general-label')} - {values.stayAligned.enabled && values.driftCompensation.enabled && ( -
- {l10n.getString( - 'settings-stay_aligned-warnings-drift_compensation' - )} -
- )} -
+
- {l10n.getString('settings-stay_aligned-relaxed_poses-detect_pose')} + {l10n.getString('settings-stay_aligned-relaxed_poses-save_pose')} ); } @@ -43,9 +43,11 @@ export function DetectRelaxedPoseButton({ */ export function ResetRelaxedPoseButton({ pose, + variant = 'primary', onClick, children, }: { + variant: ButtonProps['variant']; pose: StayAlignedRelaxedPose; onClick?: MouseEventHandler; } & React.PropsWithChildren) { @@ -54,7 +56,7 @@ export function ResetRelaxedPoseButton({ return (