add manual mounting to stay alligned setup (#1692)

This commit is contained in:
Aed
2026-01-08 01:16:08 +00:00
committed by GitHub
parent 255b8b2865
commit 690a8b5c6e
8 changed files with 181 additions and 41 deletions

View File

@@ -1352,6 +1352,7 @@ onboarding-stay_aligned-previous_step = Previous
onboarding-stay_aligned-next_step = Next
onboarding-stay_aligned-restart = Restart
onboarding-stay_aligned-done = Done
onboarding-stay_aligned-manual_mounting-done = Done
## Home
home-no_trackers = No trackers detected or assigned

View File

@@ -156,9 +156,9 @@ export function TopBar({
<>
<div className="flex gap-0 flex-col">
<div className="h-[3px]" />
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-50">
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-49">
<div
className="flex px-2 py-2 justify-around z-50"
className="flex px-2 py-2 justify-around z-49"
data-tauri-drag-region
>
<div className="flex gap-2" data-tauri-drag-region>

View File

@@ -10,7 +10,7 @@ export function TipBox({
whitespace = false,
className,
}: {
children: ReactNode;
children?: ReactNode;
hideIcon?: boolean;
whitespace?: boolean;
className?: string;

View File

@@ -28,18 +28,23 @@ export function ResetButtonIcon(options: UseResetOptions) {
}
export function ResetButton({
onClick,
className,
onReseted,
children,
onFailed,
...options
}: {
onClick?: () => void;
className?: string;
children?: ReactNode;
onReseted?: () => void;
onFailed?: () => void;
} & UseResetOptions) {
const { triggerReset, status, timer, disabled, name, error } = useReset(
options,
onReseted
onReseted,
onFailed
);
return (
@@ -60,7 +65,10 @@ export function ResetButton({
>
<Button
icon={<ResetButtonIcon {...options} />}
onClick={triggerReset}
onClick={() => {
if (onClick) onClick();
triggerReset();
}}
className={classNames(
'border-2 py-[5px]',
status === 'finished'

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { AssignTrackerRequestT, BodyPart, RpcMessage } from 'solarxr-protocol';
import { useOnboarding } from '@/hooks/onboarding';
import { useWebsocketAPI } from '@/hooks/websocket-api';
@@ -12,7 +12,7 @@ import { TipBox } from '@/components/commons/TipBox';
import { Typography } from '@/components/commons/Typography';
import { BodyAssignment } from '@/components/onboarding/BodyAssignment';
import { MountingSelectionMenu } from './MountingSelectionMenu';
import { useLocalization } from '@fluent/react';
import { Localized } from '@fluent/react';
import { useBreakpoint } from '@/hooks/breakpoint';
import { Quaternion } from 'three';
import { AssignMode, defaultConfig, useConfig } from '@/hooks/config';
@@ -22,7 +22,6 @@ import * as Sentry from '@sentry/react';
export function ManualMountingPage() {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const { applyProgress, state } = useOnboarding();
const { sendRPCPacket } = useWebsocketAPI();
const { config } = useConfig();
@@ -103,28 +102,26 @@ export function ManualMountingPage() {
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto">
<div className="flex xs:flex-row mobile:flex-col h-full px-8 xs:w-full xs:justify-center mobile:px-4 items-center">
<div className="flex flex-col w-full xs:max-w-sm gap-3">
<Typography variant="main-title">
{l10n.getString('onboarding-manual_mounting')}
</Typography>
<Typography>
{l10n.getString('onboarding-manual_mounting-description')}
</Typography>
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>
<Typography variant="main-title" id="onboarding-manual_mounting" />
<Typography id="onboarding-manual_mounting-description" />
<Typography id="tips-find_tracker" />
<Localized id="tips-find_tracker">
<TipBox />
</Localized>
<div className="flex flex-row gap-3 mt-auto">
<Button
variant="secondary"
to="/onboarding/mounting/choose"
state={state}
>
{l10n.getString('onboarding-previous_step')}
</Button>
id="onboarding-previous_step"
/>
{!state.alonePage && (
<Button
variant="primary"
to="/onboarding/body-proportions/scaled"
>
{l10n.getString('onboarding-manual_mounting-next')}
</Button>
id="onboarding-manual_mounting-next"
/>
)}
</div>
</div>
@@ -142,3 +139,109 @@ export function ManualMountingPage() {
</>
);
}
export function ManualMountingPageStayAligned({
children,
}: {
children: ReactNode;
}) {
const { isMobile } = useBreakpoint('mobile');
const { sendRPCPacket } = useWebsocketAPI();
const { config } = useConfig();
const [selectedRole, setSelectRole] = useState<BodyPart>(BodyPart.NONE);
const assignedTrackers = useAtomValue(assignedTrackersAtom);
const trackerPartGrouped = useMemo(
() =>
assignedTrackers.reduce<{ [key: number]: FlatDeviceTracker[] }>(
(curr, td) => {
const key = td.tracker.info?.bodyPart || BodyPart.NONE;
return {
...curr,
[key]: [...(curr[key] || []), td],
};
},
{}
),
[assignedTrackers]
);
const onDirectionSelected = (mountingOrientationDegrees: Quaternion) => {
(trackerPartGrouped[selectedRole] || []).forEach((td) => {
const assignreq = new AssignTrackerRequestT();
assignreq.bodyPosition = td.tracker.info?.bodyPart || BodyPart.NONE;
assignreq.mountingOrientation = MountingOrientationDegreesToQuatT(
mountingOrientationDegrees
);
assignreq.trackerId = td.tracker.trackerId;
assignreq.allowDriftCompensation = false;
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
Sentry.metrics.count('manual_mounting_set', 1, {
attributes: {
part: BodyPart[assignreq.bodyPosition],
direction: assignreq.mountingOrientation,
},
});
});
setSelectRole(BodyPart.NONE);
};
const getCurrRotation = useCallback(
(role: BodyPart) => {
if (role === BodyPart.NONE) return undefined;
const trackers = trackerPartGrouped[role] || [];
const [mountingOrientation, ...orientation] = trackers
.map((td) => td.tracker.info?.mountingOrientation)
.filter((orientation) => !!orientation)
.map((orientation) => QuaternionFromQuatT(orientation));
const identicalOrientations =
mountingOrientation !== undefined &&
orientation.every((quat) =>
similarQuaternions(quat, mountingOrientation)
);
return identicalOrientations ? mountingOrientation : undefined;
},
[trackerPartGrouped]
);
return (
<>
<MountingSelectionMenu
bodyPart={selectedRole}
currRotation={getCurrRotation(selectedRole)}
isOpen={selectedRole !== BodyPart.NONE}
onClose={() => setSelectRole(BodyPart.NONE)}
onDirectionSelected={onDirectionSelected}
/>
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto">
<div className="flex xs:flex-row mobile:flex-col h-full px-8 xs:w-full xs:justify-center mobile:px-4 items-center">
<div className="flex flex-col w-full xs:max-w-sm gap-3">
<Typography variant="main-title" id="onboarding-manual_mounting" />
<Typography id="onboarding-manual_mounting-description" />
<Typography id="tips-find_tracker" />
<Localized id="tips-find_tracker">
<TipBox />
</Localized>
{children}
</div>
<div className="flex flex-row justify-center">
<BodyAssignment
width={isMobile ? 160 : undefined}
mirror={config?.mirrorView ?? defaultConfig.mirrorView}
onlyAssigned={true}
assignMode={AssignMode.All}
onRoleSelected={setSelectRole}
/>
</div>
</div>
</div>
</>
);
}

View File

@@ -238,7 +238,7 @@ export function MountingSelectionMenu({
shouldCloseOnEsc
onRequestClose={onClose}
overlayClassName={classNames(
'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full bg-background-90 bg-opacity-90 z-20'
'fixed top-0 right-0 left-0 bottom-0 flex flex-col items-center w-full h-full bg-background-90 bg-opacity-90 z-50'
)}
className={classNames(
'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-20 z-10'

View File

@@ -1,32 +1,33 @@
import { useState } from 'react';
import { Button } from '@/components/commons/Button';
import { Typography } from '@/components/commons/Typography';
import { ResetType } from 'solarxr-protocol';
import { ResetButton } from '@/components/home/ResetButton';
import { useLocalization } from '@fluent/react';
import { useBreakpoint } from '@/hooks/breakpoint';
import { VerticalStepComponentProps } from '@/components/commons/VerticalStepper';
import { BaseModal } from '@/components/commons/BaseModal';
import { ManualMountingPageStayAligned } from '@/components/onboarding/pages/mounting/ManualMounting';
export function VerifyMountingStep({
nextStep,
prevStep,
}: VerticalStepComponentProps) {
const { isMobile } = useBreakpoint('mobile');
const { l10n } = useLocalization();
const [isOpen, setOpen] = useState(false);
const [disableMounting, setDisableMounting] = useState(false);
const goNextStep = () => {
setDisableMounting(false);
setOpen(false);
nextStep();
};
return (
<div className="flex flex-col flex-grow justify-between py-2 gap-2">
<div className="flex flex-col flex-grow">
<div className="flex flex-grow flex-col gap-4 max-w-sm">
<div className="flex flex-col gap-2">
<Typography>
{l10n.getString(
'onboarding-automatic_mounting-mounting_reset-step-0'
)}
</Typography>
<Typography>
{l10n.getString(
'onboarding-automatic_mounting-mounting_reset-step-1'
)}
</Typography>
<Typography id="onboarding-automatic_mounting-mounting_reset-step-0" />
<Typography id="onboarding-automatic_mounting-mounting_reset-step-1" />
</div>
</div>
@@ -50,13 +51,36 @@ export function VerifyMountingStep({
</div>
)}
<div className="flex gap-3 justify-between">
<Button variant={'secondary'} onClick={prevStep}>
{l10n.getString('onboarding-automatic_mounting-prev_step')}
</Button>
<Button
variant={'secondary'}
onClick={prevStep}
id="onboarding-automatic_mounting-prev_step"
/>
<Button
disabled={disableMounting}
variant={'secondary'}
className="self-start mt-auto"
onClick={() => setOpen(true)}
id="onboarding-automatic_mounting-manual_mounting"
/>
<BaseModal isOpen={isOpen} onRequestClose={() => setOpen(false)}>
<ManualMountingPageStayAligned>
<div className="flex flex-row gap-3 mt-auto">
<Button
variant="primary"
onClick={goNextStep}
id="onboarding-stay_aligned-manual_mounting-done"
/>
</div>
</ManualMountingPageStayAligned>
</BaseModal>
<ResetButton
onClick={() => setDisableMounting(true)}
type={ResetType.Mounting}
group="default"
onReseted={nextStep}
onReseted={goNextStep}
onFailed={() => setDisableMounting(false)}
/>
</div>
</div>

View File

@@ -27,13 +27,16 @@ export const BODY_PARTS_GROUPS: Record<MountingResetGroup, BodyPart[]> = {
fingers: FINGER_BODY_PARTS,
};
export function useReset(options: UseResetOptions, onReseted?: () => void) {
export function useReset(
options: UseResetOptions,
onReseted?: () => void,
onFailed?: () => void
) {
if (options.type === ResetType.Mounting && !options.group) options.group = 'default';
const serverGuards = useAtomValue(serverGuardsAtom);
const { currentLocales } = useLocaleConfig();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const finishedTimeoutRef = useRef<NodeJS.Timeout>();
const [status, setStatus] = useState<ResetBtnStatus>('idle');
const [progress, setProgress] = useState(0);
@@ -62,6 +65,7 @@ export function useReset(options: UseResetOptions, onReseted?: () => void) {
const onResetCanceled = () => {
if (status !== 'finished') setStatus('idle');
if (onFailed) onFailed();
};
useEffect(() => {