mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
add manual mounting to stay alligned setup (#1692)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,7 +10,7 @@ export function TipBox({
|
||||
whitespace = false,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
hideIcon?: boolean;
|
||||
whitespace?: boolean;
|
||||
className?: string;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user