Skip to content

Commit 690a8b5

Browse files
authored
add manual mounting to stay alligned setup (#1692)
1 parent 255b8b2 commit 690a8b5

File tree

8 files changed

+181
-41
lines changed

8 files changed

+181
-41
lines changed

gui/public/i18n/en/translation.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,7 @@ onboarding-stay_aligned-previous_step = Previous
13521352
onboarding-stay_aligned-next_step = Next
13531353
onboarding-stay_aligned-restart = Restart
13541354
onboarding-stay_aligned-done = Done
1355+
onboarding-stay_aligned-manual_mounting-done = Done
13551356
13561357
## Home
13571358
home-no_trackers = No trackers detected or assigned

gui/src/components/TopBar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ export function TopBar({
156156
<>
157157
<div className="flex gap-0 flex-col">
158158
<div className="h-[3px]" />
159-
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-50">
159+
<div data-tauri-drag-region className="flex gap-2 h-[38px] z-49">
160160
<div
161-
className="flex px-2 py-2 justify-around z-50"
161+
className="flex px-2 py-2 justify-around z-49"
162162
data-tauri-drag-region
163163
>
164164
<div className="flex gap-2" data-tauri-drag-region>

gui/src/components/commons/TipBox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function TipBox({
1010
whitespace = false,
1111
className,
1212
}: {
13-
children: ReactNode;
13+
children?: ReactNode;
1414
hideIcon?: boolean;
1515
whitespace?: boolean;
1616
className?: string;

gui/src/components/home/ResetButton.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,23 @@ export function ResetButtonIcon(options: UseResetOptions) {
2828
}
2929

3030
export function ResetButton({
31+
onClick,
3132
className,
3233
onReseted,
3334
children,
35+
onFailed,
3436
...options
3537
}: {
38+
onClick?: () => void;
3639
className?: string;
3740
children?: ReactNode;
3841
onReseted?: () => void;
42+
onFailed?: () => void;
3943
} & UseResetOptions) {
4044
const { triggerReset, status, timer, disabled, name, error } = useReset(
4145
options,
42-
onReseted
46+
onReseted,
47+
onFailed
4348
);
4449

4550
return (
@@ -60,7 +65,10 @@ export function ResetButton({
6065
>
6166
<Button
6267
icon={<ResetButtonIcon {...options} />}
63-
onClick={triggerReset}
68+
onClick={() => {
69+
if (onClick) onClick();
70+
triggerReset();
71+
}}
6472
className={classNames(
6573
'border-2 py-[5px]',
6674
status === 'finished'

gui/src/components/onboarding/pages/mounting/ManualMounting.tsx

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useMemo, useState } from 'react';
1+
import { ReactNode, useCallback, useMemo, useState } from 'react';
22
import { AssignTrackerRequestT, BodyPart, RpcMessage } from 'solarxr-protocol';
33
import { useOnboarding } from '@/hooks/onboarding';
44
import { useWebsocketAPI } from '@/hooks/websocket-api';
@@ -12,7 +12,7 @@ import { TipBox } from '@/components/commons/TipBox';
1212
import { Typography } from '@/components/commons/Typography';
1313
import { BodyAssignment } from '@/components/onboarding/BodyAssignment';
1414
import { MountingSelectionMenu } from './MountingSelectionMenu';
15-
import { useLocalization } from '@fluent/react';
15+
import { Localized } from '@fluent/react';
1616
import { useBreakpoint } from '@/hooks/breakpoint';
1717
import { Quaternion } from 'three';
1818
import { AssignMode, defaultConfig, useConfig } from '@/hooks/config';
@@ -22,7 +22,6 @@ import * as Sentry from '@sentry/react';
2222

2323
export function ManualMountingPage() {
2424
const { isMobile } = useBreakpoint('mobile');
25-
const { l10n } = useLocalization();
2625
const { applyProgress, state } = useOnboarding();
2726
const { sendRPCPacket } = useWebsocketAPI();
2827
const { config } = useConfig();
@@ -103,28 +102,26 @@ export function ManualMountingPage() {
103102
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto">
104103
<div className="flex xs:flex-row mobile:flex-col h-full px-8 xs:w-full xs:justify-center mobile:px-4 items-center">
105104
<div className="flex flex-col w-full xs:max-w-sm gap-3">
106-
<Typography variant="main-title">
107-
{l10n.getString('onboarding-manual_mounting')}
108-
</Typography>
109-
<Typography>
110-
{l10n.getString('onboarding-manual_mounting-description')}
111-
</Typography>
112-
<TipBox>{l10n.getString('tips-find_tracker')}</TipBox>
105+
<Typography variant="main-title" id="onboarding-manual_mounting" />
106+
<Typography id="onboarding-manual_mounting-description" />
107+
<Typography id="tips-find_tracker" />
108+
<Localized id="tips-find_tracker">
109+
<TipBox />
110+
</Localized>
111+
113112
<div className="flex flex-row gap-3 mt-auto">
114113
<Button
115114
variant="secondary"
116115
to="/onboarding/mounting/choose"
117116
state={state}
118-
>
119-
{l10n.getString('onboarding-previous_step')}
120-
</Button>
117+
id="onboarding-previous_step"
118+
/>
121119
{!state.alonePage && (
122120
<Button
123121
variant="primary"
124122
to="/onboarding/body-proportions/scaled"
125-
>
126-
{l10n.getString('onboarding-manual_mounting-next')}
127-
</Button>
123+
id="onboarding-manual_mounting-next"
124+
/>
128125
)}
129126
</div>
130127
</div>
@@ -142,3 +139,109 @@ export function ManualMountingPage() {
142139
</>
143140
);
144141
}
142+
143+
export function ManualMountingPageStayAligned({
144+
children,
145+
}: {
146+
children: ReactNode;
147+
}) {
148+
const { isMobile } = useBreakpoint('mobile');
149+
const { sendRPCPacket } = useWebsocketAPI();
150+
const { config } = useConfig();
151+
152+
const [selectedRole, setSelectRole] = useState<BodyPart>(BodyPart.NONE);
153+
154+
const assignedTrackers = useAtomValue(assignedTrackersAtom);
155+
156+
const trackerPartGrouped = useMemo(
157+
() =>
158+
assignedTrackers.reduce<{ [key: number]: FlatDeviceTracker[] }>(
159+
(curr, td) => {
160+
const key = td.tracker.info?.bodyPart || BodyPart.NONE;
161+
return {
162+
...curr,
163+
[key]: [...(curr[key] || []), td],
164+
};
165+
},
166+
{}
167+
),
168+
[assignedTrackers]
169+
);
170+
171+
const onDirectionSelected = (mountingOrientationDegrees: Quaternion) => {
172+
(trackerPartGrouped[selectedRole] || []).forEach((td) => {
173+
const assignreq = new AssignTrackerRequestT();
174+
175+
assignreq.bodyPosition = td.tracker.info?.bodyPart || BodyPart.NONE;
176+
assignreq.mountingOrientation = MountingOrientationDegreesToQuatT(
177+
mountingOrientationDegrees
178+
);
179+
assignreq.trackerId = td.tracker.trackerId;
180+
assignreq.allowDriftCompensation = false;
181+
182+
sendRPCPacket(RpcMessage.AssignTrackerRequest, assignreq);
183+
Sentry.metrics.count('manual_mounting_set', 1, {
184+
attributes: {
185+
part: BodyPart[assignreq.bodyPosition],
186+
direction: assignreq.mountingOrientation,
187+
},
188+
});
189+
});
190+
191+
setSelectRole(BodyPart.NONE);
192+
};
193+
194+
const getCurrRotation = useCallback(
195+
(role: BodyPart) => {
196+
if (role === BodyPart.NONE) return undefined;
197+
198+
const trackers = trackerPartGrouped[role] || [];
199+
const [mountingOrientation, ...orientation] = trackers
200+
.map((td) => td.tracker.info?.mountingOrientation)
201+
.filter((orientation) => !!orientation)
202+
.map((orientation) => QuaternionFromQuatT(orientation));
203+
204+
const identicalOrientations =
205+
mountingOrientation !== undefined &&
206+
orientation.every((quat) =>
207+
similarQuaternions(quat, mountingOrientation)
208+
);
209+
return identicalOrientations ? mountingOrientation : undefined;
210+
},
211+
[trackerPartGrouped]
212+
);
213+
214+
return (
215+
<>
216+
<MountingSelectionMenu
217+
bodyPart={selectedRole}
218+
currRotation={getCurrRotation(selectedRole)}
219+
isOpen={selectedRole !== BodyPart.NONE}
220+
onClose={() => setSelectRole(BodyPart.NONE)}
221+
onDirectionSelected={onDirectionSelected}
222+
/>
223+
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center relative overflow-y-auto">
224+
<div className="flex xs:flex-row mobile:flex-col h-full px-8 xs:w-full xs:justify-center mobile:px-4 items-center">
225+
<div className="flex flex-col w-full xs:max-w-sm gap-3">
226+
<Typography variant="main-title" id="onboarding-manual_mounting" />
227+
<Typography id="onboarding-manual_mounting-description" />
228+
<Typography id="tips-find_tracker" />
229+
<Localized id="tips-find_tracker">
230+
<TipBox />
231+
</Localized>
232+
{children}
233+
</div>
234+
<div className="flex flex-row justify-center">
235+
<BodyAssignment
236+
width={isMobile ? 160 : undefined}
237+
mirror={config?.mirrorView ?? defaultConfig.mirrorView}
238+
onlyAssigned={true}
239+
assignMode={AssignMode.All}
240+
onRoleSelected={setSelectRole}
241+
/>
242+
</div>
243+
</div>
244+
</div>
245+
</>
246+
);
247+
}

gui/src/components/onboarding/pages/mounting/MountingSelectionMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export function MountingSelectionMenu({
238238
shouldCloseOnEsc
239239
onRequestClose={onClose}
240240
overlayClassName={classNames(
241-
'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'
241+
'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'
242242
)}
243243
className={classNames(
244244
'focus:ring-transparent focus:ring-offset-transparent focus:outline-transparent outline-none mt-20 z-10'

gui/src/components/onboarding/pages/stay-aligned/stay-aligned-steps/VerifyMounting.tsx

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
1+
import { useState } from 'react';
12
import { Button } from '@/components/commons/Button';
23
import { Typography } from '@/components/commons/Typography';
34
import { ResetType } from 'solarxr-protocol';
45
import { ResetButton } from '@/components/home/ResetButton';
5-
import { useLocalization } from '@fluent/react';
66
import { useBreakpoint } from '@/hooks/breakpoint';
77
import { VerticalStepComponentProps } from '@/components/commons/VerticalStepper';
8-
8+
import { BaseModal } from '@/components/commons/BaseModal';
9+
import { ManualMountingPageStayAligned } from '@/components/onboarding/pages/mounting/ManualMounting';
910
export function VerifyMountingStep({
1011
nextStep,
1112
prevStep,
1213
}: VerticalStepComponentProps) {
1314
const { isMobile } = useBreakpoint('mobile');
14-
const { l10n } = useLocalization();
15+
const [isOpen, setOpen] = useState(false);
16+
const [disableMounting, setDisableMounting] = useState(false);
17+
18+
const goNextStep = () => {
19+
setDisableMounting(false);
20+
setOpen(false);
21+
nextStep();
22+
};
23+
1524
return (
1625
<div className="flex flex-col flex-grow justify-between py-2 gap-2">
1726
<div className="flex flex-col flex-grow">
1827
<div className="flex flex-grow flex-col gap-4 max-w-sm">
1928
<div className="flex flex-col gap-2">
20-
<Typography>
21-
{l10n.getString(
22-
'onboarding-automatic_mounting-mounting_reset-step-0'
23-
)}
24-
</Typography>
25-
<Typography>
26-
{l10n.getString(
27-
'onboarding-automatic_mounting-mounting_reset-step-1'
28-
)}
29-
</Typography>
29+
<Typography id="onboarding-automatic_mounting-mounting_reset-step-0" />
30+
<Typography id="onboarding-automatic_mounting-mounting_reset-step-1" />
3031
</div>
3132
</div>
3233

@@ -50,13 +51,36 @@ export function VerifyMountingStep({
5051
</div>
5152
)}
5253
<div className="flex gap-3 justify-between">
53-
<Button variant={'secondary'} onClick={prevStep}>
54-
{l10n.getString('onboarding-automatic_mounting-prev_step')}
55-
</Button>
54+
<Button
55+
variant={'secondary'}
56+
onClick={prevStep}
57+
id="onboarding-automatic_mounting-prev_step"
58+
/>
59+
<Button
60+
disabled={disableMounting}
61+
variant={'secondary'}
62+
className="self-start mt-auto"
63+
onClick={() => setOpen(true)}
64+
id="onboarding-automatic_mounting-manual_mounting"
65+
/>
66+
<BaseModal isOpen={isOpen} onRequestClose={() => setOpen(false)}>
67+
<ManualMountingPageStayAligned>
68+
<div className="flex flex-row gap-3 mt-auto">
69+
<Button
70+
variant="primary"
71+
onClick={goNextStep}
72+
id="onboarding-stay_aligned-manual_mounting-done"
73+
/>
74+
</div>
75+
</ManualMountingPageStayAligned>
76+
</BaseModal>
77+
5678
<ResetButton
79+
onClick={() => setDisableMounting(true)}
5780
type={ResetType.Mounting}
5881
group="default"
59-
onReseted={nextStep}
82+
onReseted={goNextStep}
83+
onFailed={() => setDisableMounting(false)}
6084
/>
6185
</div>
6286
</div>

gui/src/hooks/reset.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ export const BODY_PARTS_GROUPS: Record<MountingResetGroup, BodyPart[]> = {
2727
fingers: FINGER_BODY_PARTS,
2828
};
2929

30-
export function useReset(options: UseResetOptions, onReseted?: () => void) {
30+
export function useReset(
31+
options: UseResetOptions,
32+
onReseted?: () => void,
33+
onFailed?: () => void
34+
) {
3135
if (options.type === ResetType.Mounting && !options.group) options.group = 'default';
3236

3337
const serverGuards = useAtomValue(serverGuardsAtom);
3438
const { currentLocales } = useLocaleConfig();
3539
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
36-
3740
const finishedTimeoutRef = useRef<NodeJS.Timeout>();
3841
const [status, setStatus] = useState<ResetBtnStatus>('idle');
3942
const [progress, setProgress] = useState(0);
@@ -62,6 +65,7 @@ export function useReset(options: UseResetOptions, onReseted?: () => void) {
6265

6366
const onResetCanceled = () => {
6467
if (status !== 'finished') setStatus('idle');
68+
if (onFailed) onFailed();
6569
};
6670

6771
useEffect(() => {

0 commit comments

Comments
 (0)