mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
stay aligned wizard (#1442)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
gui/public/images/reset/FullResetPose.webp
Normal file
BIN
gui/public/images/reset/FullResetPose.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
BIN
gui/public/images/reset/FullResetPoseSide.webp
Normal file
BIN
gui/public/images/reset/FullResetPoseSide.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
gui/public/images/reset/FullResetPoseWrong.webp
Normal file
BIN
gui/public/images/reset/FullResetPoseWrong.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
BIN
gui/public/images/stay-aligned/StayAlignedFloor.webp
Normal file
BIN
gui/public/images/stay-aligned/StayAlignedFloor.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
BIN
gui/public/images/stay-aligned/StayAlignedSitting.webp
Normal file
BIN
gui/public/images/stay-aligned/StayAlignedSitting.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
BIN
gui/public/images/stay-aligned/StayAlignedStanding.webp
Normal file
BIN
gui/public/images/stay-aligned/StayAlignedStanding.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
@@ -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<HTMLButtonElement>;
|
||||
|
||||
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<HTMLButtonElement>) {
|
||||
}: ButtonProps) {
|
||||
const classes = useMemo(() => {
|
||||
const variantsMap = {
|
||||
primary: classNames({
|
||||
|
||||
@@ -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({
|
||||
<div
|
||||
style={{ height: !isSelected ? 0 : height }}
|
||||
className={classNames('overflow-clip px-1', {
|
||||
'duration-500 transition-[height]': shouldAnimate,
|
||||
'duration-300 transition-[height]': shouldAnimate,
|
||||
})}
|
||||
>
|
||||
<div ref={ref}>{children}</div>
|
||||
@@ -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<VerticalStepComponentProps>;
|
||||
|
||||
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 (
|
||||
<ol className="relative border-l border-gray-700 text-gray-400">
|
||||
{steps.map(({ title, component: StepComponent }, index) => (
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
export function CheckIcon({ size = 9 }: { size?: number }) {
|
||||
export function CheckIcon({
|
||||
size = 9,
|
||||
className,
|
||||
}: {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 9 7"
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_548_761)">
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
export function CrossIcon({ size = 20 }: { size: number }) {
|
||||
export function CrossIcon({
|
||||
size = 20,
|
||||
className,
|
||||
}: {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
width={size}
|
||||
height={20}
|
||||
height={size}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -24,7 +24,9 @@ export function DoneStep({ variant }: { variant: 'onboarding' | 'alone' }) {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<SkeletonVisualizerWidget />
|
||||
<div className="flex flex-grow max-h-[450px]">
|
||||
<SkeletonVisualizerWidget />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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')}
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-automatic_mounting-preparation-step-0'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-automatic_mounting-preparation-step-1'
|
||||
)}
|
||||
</Typography>
|
||||
<Localized id="onboarding-automatic_mounting-preparation-v2-step-0">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
<Localized id="onboarding-automatic_mounting-preparation-v2-step-1">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
<Localized id="onboarding-automatic_mounting-preparation-v2-step-2">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
{isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<div className="grid grid-cols-3 py-4 gap-2">
|
||||
<div className="flex flex-col bg-background-70 rounded-md relative max-h-64">
|
||||
<CheckIcon className="md:w-14 sm:w-8 w-6 h-auto absolute top-2 right-2 fill-status-success"></CheckIcon>
|
||||
<img
|
||||
src="/images/reset-pose.webp"
|
||||
width={50}
|
||||
src="/images/reset/FullResetPose.webp"
|
||||
className="h-full object-contain"
|
||||
alt="Reset position"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col bg-background-70 rounded-md relative max-h-64">
|
||||
<CheckIcon className="md:w-14 sm:w-8 w-6 h-auto absolute top-2 right-2 fill-status-success"></CheckIcon>
|
||||
<img
|
||||
src="/images/reset/FullResetPoseSide.webp"
|
||||
className="h-full object-contain"
|
||||
alt="Reset position side"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-70 rounded-md relative max-h-64">
|
||||
<CrossIcon className="md:w-14 sm:w-8 w-6 h-auto absolute top-2 right-2 fill-status-critical"></CrossIcon>
|
||||
<img
|
||||
src="/images/reset/FullResetPoseWrong.webp"
|
||||
className="h-full object-contain"
|
||||
alt="Reset position wrong"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 mobile:justify-between">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
@@ -60,11 +76,6 @@ export function PreparationStep({
|
||||
></ResetButton>
|
||||
</div>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<img src="/images/reset-pose.webp" width={90} alt="Reset position" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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')}
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-automatic_mounting-preparation-step-0'
|
||||
)}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(
|
||||
'onboarding-automatic_mounting-preparation-step-1'
|
||||
)}
|
||||
</Typography>
|
||||
<Localized id="onboarding-automatic_mounting-preparation-v2-step-0">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
<Localized id="onboarding-automatic_mounting-preparation-v2-step-1">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
<Localized id="onboarding-automatic_mounting-preparation-v2-step-2">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
{isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<div className="grid grid-cols-3 py-4 gap-2">
|
||||
<div className="flex flex-col bg-background-70 rounded-md relative max-h-64">
|
||||
<CheckIcon className="md:w-14 sm:w-8 w-6 h-auto absolute top-2 right-2 fill-status-success"></CheckIcon>
|
||||
<img
|
||||
src="/images/reset-pose.webp"
|
||||
width={100}
|
||||
src="/images/reset/FullResetPose.webp"
|
||||
className="h-full object-contain"
|
||||
alt="Reset position"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col bg-background-70 rounded-md relative max-h-64">
|
||||
<CheckIcon className="md:w-14 sm:w-8 w-6 h-auto absolute top-2 right-2 fill-status-success"></CheckIcon>
|
||||
<img
|
||||
src="/images/reset/FullResetPoseSide.webp"
|
||||
className="h-full object-contain"
|
||||
alt="Reset position side"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-70 rounded-md relative max-h-64">
|
||||
<CrossIcon className="md:w-14 sm:w-8 w-6 h-auto absolute top-2 right-2 fill-status-critical"></CrossIcon>
|
||||
<img
|
||||
src="/images/reset/FullResetPoseWrong.webp"
|
||||
className="h-full object-contain"
|
||||
alt="Reset position wrong"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 mobile:justify-between">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
@@ -60,11 +76,6 @@ export function PreparationStep({
|
||||
></ResetButton>
|
||||
</div>
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<img src="/images/reset-pose.webp" width={90} alt="Reset position" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col gap-2 h-full items-center w-full xs:justify-center relative overflow-y-auto overflow-x-hidden px-4 pb-4">
|
||||
<div className="flex flex-col w-full h-full xs:justify-center xs:max-w-3xl gap-5">
|
||||
<div className="h-full w-full flex gap-2 mobile:flex-col bg-background-80">
|
||||
<div className="bg-background-70 rounded-md flex-grow p-4 overflow-y-auto">
|
||||
<div className="flex flex-col xs:max-w-lg gap-3">
|
||||
<Typography variant="main-title">
|
||||
{l10n.getString('onboarding-stay_aligned-title')}
|
||||
@@ -53,13 +140,90 @@ export function StayAlignedSetup() {
|
||||
{l10n.getString('onboarding-stay_aligned-description')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex pb-4">
|
||||
<StepperSlider
|
||||
variant={state.alonePage ? 'alone' : 'onboarding'}
|
||||
steps={steps}
|
||||
/>
|
||||
<div className="flex pl-4 pt-4">
|
||||
<VerticalStepper
|
||||
onStepChange={onStepChange}
|
||||
steps={[
|
||||
{
|
||||
title: l10n.getString(
|
||||
'onboarding-stay_aligned-put_trackers_on-title'
|
||||
),
|
||||
component: PutTrackersOnStep,
|
||||
id: 'start',
|
||||
},
|
||||
{
|
||||
title: l10n.getString(
|
||||
'onboarding-stay_aligned-preparation-title'
|
||||
),
|
||||
component: PreparationStep,
|
||||
},
|
||||
{
|
||||
title: l10n.getString(
|
||||
'onboarding-stay_aligned-verify_mounting-title'
|
||||
),
|
||||
component: VerifyMountingStep,
|
||||
},
|
||||
{
|
||||
title: l10n.getString(
|
||||
'onboarding-stay_aligned-relaxed_poses-standing-title'
|
||||
),
|
||||
component: StandingRelaxedPoseStep,
|
||||
id: 'standing',
|
||||
},
|
||||
{
|
||||
title: l10n.getString(
|
||||
'onboarding-stay_aligned-relaxed_poses-sitting-title'
|
||||
),
|
||||
component: SittingRelaxedPoseStep,
|
||||
id: 'sitting',
|
||||
},
|
||||
{
|
||||
title: l10n.getString(
|
||||
'onboarding-stay_aligned-relaxed_poses-flat-title'
|
||||
),
|
||||
component: FlatRelaxedPoseStep,
|
||||
id: 'flat',
|
||||
},
|
||||
{
|
||||
title: l10n.getString('onboarding-stay_aligned-done-title'),
|
||||
component: DoneStep,
|
||||
},
|
||||
]}
|
||||
></VerticalStepper>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-background-70 rounded-md xs:max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg w-full mobile:h-[30%]">
|
||||
<SkeletonVisualizerWidget
|
||||
onInit={(context) => {
|
||||
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;
|
||||
},
|
||||
});
|
||||
}}
|
||||
></SkeletonVisualizerWidget>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col items-center w-full justify-center gap-5">
|
||||
<div className="flex gap-1 flex-col justify-center items-center">
|
||||
<Typography variant="section-title">
|
||||
{l10n.getString('onboarding-stay_aligned-done-title')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-stay_aligned-done-description')}
|
||||
</Typography>
|
||||
<div className="flex flex-col items-center w-full justify-center gap-5 pt-2">
|
||||
<div className="flex gap-1 flex-col justify-center items-center pt-10">
|
||||
<Localized id="onboarding-stay_aligned-done-description">
|
||||
<Typography variant="main-title"></Typography>
|
||||
</Localized>
|
||||
<Localized id="onboarding-stay_aligned-done-description-2">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
onClick={resetSteps}
|
||||
>
|
||||
{l10n.getString('onboarding-stay_aligned-restart')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/settings/trackers"
|
||||
state={{ scrollTo: 'stayaligned' }}
|
||||
>
|
||||
{l10n.getString('onboarding-stay_aligned-done')}
|
||||
</Button>
|
||||
<div className="flex gap-3 justify-between">
|
||||
<Localized id="onboarding-stay_aligned-restart">
|
||||
<Button variant={'secondary'} onClick={() => goTo('start')}></Button>
|
||||
</Localized>
|
||||
<Localized id="onboarding-stay_aligned-done">
|
||||
<Button
|
||||
variant="primary"
|
||||
to="/settings/trackers"
|
||||
state={{ scrollTo: 'stayaligned' }}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
|
||||
<SkeletonVisualizerWidget />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col flex-grow justify-between pt-2 gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Localized id="onboarding-automatic_mounting-preparation-v2-step-0">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
<Localized id="onboarding-automatic_mounting-preparation-v2-step-1">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
<Localized id="onboarding-automatic_mounting-preparation-v2-step-2">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
<Localized id="onboarding-stay_aligned-preparation-tip">
|
||||
<TipBox>TIP</TipBox>
|
||||
</Localized>
|
||||
<div className="grid grid-cols-3 py-4 gap-2">
|
||||
<div className="flex flex-col bg-background-60 rounded-md relative">
|
||||
<CheckIcon className="md:w-20 sm:w-10 w-6 h-auto absolute top-2 right-2 fill-status-success"></CheckIcon>
|
||||
<img src="/images/reset/FullResetPose.webp" alt="Reset position" />
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-60 rounded-md relative">
|
||||
<CheckIcon className="md:w-20 sm:w-10 w-6 h-auto absolute top-2 right-2 fill-status-success"></CheckIcon>
|
||||
<img
|
||||
src="/images/reset/FullResetPoseSide.webp"
|
||||
alt="Reset position side"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col bg-background-60 rounded-md relative">
|
||||
<CrossIcon className="md:w-20 sm:w-10 w-6 h-auto absolute top-2 right-2 fill-status-critical"></CrossIcon>
|
||||
<img
|
||||
src="/images/reset/FullResetPoseWrong.webp"
|
||||
alt="Reset position wrong"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-between">
|
||||
<Localized id="onboarding-stay_aligned-previous_step">
|
||||
<Button variant={'secondary'} onClick={prevStep}></Button>
|
||||
</Localized>
|
||||
|
||||
<ResetButton
|
||||
size="small"
|
||||
type={ResetType.Full}
|
||||
onReseted={nextStep}
|
||||
></ResetButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex flex-col w-full pt-2">
|
||||
<div className="flex flex-col flex-grow gap-2">
|
||||
<div className="flex flex-grow flex-col gap-4">
|
||||
<div>
|
||||
<Localized id="onboarding-stay_aligned-put_trackers_on-description">
|
||||
<Typography color="secondary"></Typography>
|
||||
</Localized>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Localized id="tips-find_tracker">
|
||||
<TipBox>Tip</TipBox>
|
||||
</Localized>
|
||||
</div>
|
||||
{assignedTrackers.length < 5 && (
|
||||
<div className="flex">
|
||||
<Localized id="onboarding-stay_aligned-put_trackers_on-trackers_warning">
|
||||
<WarningBox>Warning</WarningBox>
|
||||
</Localized>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-16">
|
||||
<BodyDisplay
|
||||
trackers={assignedTrackers}
|
||||
width={150}
|
||||
dotsSize={15}
|
||||
variant="dots"
|
||||
hideUnassigned={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-3 justify-end">
|
||||
<Localized id="onboarding-stay_aligned-put_trackers_on-next">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={nextStep}
|
||||
disabled={!canContinue}
|
||||
></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex mobile:flex-col">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<Typography variant="main-title" bold>
|
||||
{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 (
|
||||
<div className="flex flex-col pt-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
{descriptionKeys.map((descriptionKey) => (
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(descriptionKey)}
|
||||
</Typography>
|
||||
<div className="flex flex-col gap-2">
|
||||
{descriptionKeys.map((descriptionKey) => (
|
||||
<Typography color="secondary">
|
||||
{l10n.getString(descriptionKey)}
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-3 mobile:justify-between">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
onClick={prevStep}
|
||||
>
|
||||
{l10n.getString('onboarding-stay_aligned-previous_step')}
|
||||
</Button>
|
||||
<DetectRelaxedPoseButton
|
||||
onClick={() => {
|
||||
if (lastStep) {
|
||||
enableStayAligned(true, sendRPCPacket);
|
||||
}
|
||||
nextStep();
|
||||
}}
|
||||
pose={relaxedPose}
|
||||
/>
|
||||
<ResetRelaxedPoseButton
|
||||
onClick={() => {
|
||||
if (lastStep) {
|
||||
enableStayAligned(true, sendRPCPacket);
|
||||
}
|
||||
nextStep();
|
||||
}}
|
||||
pose={relaxedPose}
|
||||
>
|
||||
{l10n.getString(
|
||||
'onboarding-stay_aligned-relaxed_poses-skip_step'
|
||||
)}
|
||||
</ResetRelaxedPoseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col pt-1 items-center fill-background-50 justify-center px-12">
|
||||
<img src={imageUrl} width={200} alt="Reset position" />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex pt-1 items-center fill-background-50 justify-center px-12">
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<Button variant={'secondary'} onClick={prevStep}>
|
||||
{l10n.getString('onboarding-stay_aligned-previous_step')}
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<ResetRelaxedPoseButton
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
if (lastStep) {
|
||||
enableStayAligned(true, sendRPCPacket);
|
||||
}
|
||||
nextStep();
|
||||
}}
|
||||
pose={relaxedPose}
|
||||
>
|
||||
{l10n.getString('onboarding-stay_aligned-relaxed_poses-skip_step')}
|
||||
</ResetRelaxedPoseButton>
|
||||
<DetectRelaxedPoseButton
|
||||
onClick={() => {
|
||||
if (lastStep) {
|
||||
enableStayAligned(true, sendRPCPacket);
|
||||
}
|
||||
nextStep();
|
||||
}}
|
||||
pose={relaxedPose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
) => (
|
||||
<PosePage
|
||||
{...verticalStepProps}
|
||||
descriptionKeys={[
|
||||
'onboarding-stay_aligned-relaxed_poses-standing-step-0',
|
||||
'onboarding-stay_aligned-relaxed_poses-standing-step-2',
|
||||
]}
|
||||
relaxedPose={StayAlignedRelaxedPose.STANDING}
|
||||
>
|
||||
<img
|
||||
src={'/images/stay-aligned/StayAlignedStanding.webp'}
|
||||
width={300}
|
||||
alt="Reset position"
|
||||
/>
|
||||
</PosePage>
|
||||
);
|
||||
|
||||
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
|
||||
) => (
|
||||
<PosePage
|
||||
{...verticalStepProps}
|
||||
descriptionKeys={[
|
||||
'onboarding-stay_aligned-relaxed_poses-sitting-step-0',
|
||||
'onboarding-stay_aligned-relaxed_poses-sitting-step-2',
|
||||
]}
|
||||
relaxedPose={StayAlignedRelaxedPose.SITTING}
|
||||
>
|
||||
<img
|
||||
src={'/images/stay-aligned/StayAlignedSitting.webp'}
|
||||
width={300}
|
||||
alt="Reset position"
|
||||
/>
|
||||
</PosePage>
|
||||
);
|
||||
|
||||
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
|
||||
) => (
|
||||
<PosePage
|
||||
{...verticalStepProps}
|
||||
descriptionKeys={[
|
||||
'onboarding-stay_aligned-relaxed_poses-flat-step-0',
|
||||
'onboarding-stay_aligned-relaxed_poses-flat-step-2',
|
||||
]}
|
||||
relaxedPose={StayAlignedRelaxedPose.FLAT}
|
||||
lastStep
|
||||
>
|
||||
<img
|
||||
src={'/images/stay-aligned/StayAlignedFloor.webp'}
|
||||
width={600}
|
||||
alt="Reset position"
|
||||
/>
|
||||
</PosePage>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex mobile:flex-col">
|
||||
<div className="flex flex-grow flex-col gap-4 max-w-sm">
|
||||
<Typography variant="main-title" bold>
|
||||
{l10n.getString('onboarding-stay_aligned-verify_mounting-title')}
|
||||
</Typography>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-stay_aligned-verify_mounting-step-0')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-stay_aligned-verify_mounting-step-1')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-stay_aligned-verify_mounting-step-2')}
|
||||
</Typography>
|
||||
<Typography color="secondary">
|
||||
{l10n.getString('onboarding-stay_aligned-verify_mounting-step-3')}
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="flex gap-3 mobile:justify-between">
|
||||
<Button
|
||||
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
|
||||
onClick={resetSteps}
|
||||
>
|
||||
{l10n.getString('onboarding-stay_aligned-restart')}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={nextStep}>
|
||||
{l10n.getString('onboarding-stay_aligned-next_step')}
|
||||
</Button>
|
||||
<div className="flex flex-grow flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Localized id="onboarding-stay_aligned-verify_mounting-step-0">
|
||||
<Typography color="secondary" />
|
||||
</Localized>
|
||||
<Localized id="onboarding-stay_aligned-verify_mounting-step-1">
|
||||
<Typography color="secondary" />
|
||||
</Localized>
|
||||
<Localized id="onboarding-stay_aligned-verify_mounting-step-2">
|
||||
<Typography color="secondary" />
|
||||
</Localized>
|
||||
<Localized id="onboarding-stay_aligned-verify_mounting-step-3">
|
||||
<Typography color="secondary" />
|
||||
</Localized>
|
||||
</div>
|
||||
<div className="flex gap-3 justify-between">
|
||||
<Localized id="onboarding-stay_aligned-previous_step">
|
||||
<Button variant="secondary" onClick={prevStep}></Button>
|
||||
</Localized>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Localized id="onboarding-stay_aligned-verify_mounting-redo_mounting">
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
to="/onboarding/mounting/choose"
|
||||
></Button>
|
||||
</Localized>
|
||||
<Localized id="onboarding-stay_aligned-next_step">
|
||||
<Button variant="primary" onClick={nextStep}></Button>
|
||||
</Localized>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<BaseModal
|
||||
isOpen={isOpen}
|
||||
shouldCloseOnOverlayClick
|
||||
onRequestClose={onClose}
|
||||
className={props.className}
|
||||
overlayClassName={props.overlayClassName}
|
||||
>
|
||||
<div className="flex w-full h-full flex-col ">
|
||||
<div className="flex flex-col flex-grow items-center gap-3">
|
||||
<Localized
|
||||
id="settings-general-tracker_mechanics-drift_compensation_warning"
|
||||
elems={{ b: <b></b> }}
|
||||
>
|
||||
<WarningBox>
|
||||
<b>Warning:</b> Drift compensation should only be used if you find
|
||||
you need to reset very often (~5-10 minutes).
|
||||
<br />
|
||||
Some IMUs prone to frequent resets include: Joy-Cons, owoTrack,
|
||||
and MPUs (without recent firmware).
|
||||
</WarningBox>
|
||||
</Localized>
|
||||
|
||||
<div className="flex flex-row gap-3 pt-5 place-content-center">
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
{l10n.getString(
|
||||
'settings-general-tracker_mechanics-drift_compensation_warning-cancel'
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
accept();
|
||||
}}
|
||||
>
|
||||
{l10n.getString(
|
||||
'settings-general-tracker_mechanics-drift_compensation_warning-done'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<SettingsPageLayout>
|
||||
<HandsWarningModal
|
||||
@@ -703,102 +676,6 @@ export function GeneralSettings() {
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col pt-4 pb-4"></div>
|
||||
<Typography bold>
|
||||
{l10n.getString(
|
||||
'settings-general-tracker_mechanics-drift_compensation'
|
||||
)}
|
||||
</Typography>
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
{l10n
|
||||
.getString(
|
||||
'settings-general-tracker_mechanics-drift_compensation-description'
|
||||
)
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="driftCompensation.enabled"
|
||||
label={l10n.getString(
|
||||
'settings-general-tracker_mechanics-drift_compensation-enabled-label'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (getValues('driftCompensation.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShowDriftCompWarning(true);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col pt-2 pb-4"></div>
|
||||
<Typography bold>
|
||||
{l10n.getString(
|
||||
'settings-general-tracker_mechanics-drift_compensation-prediction'
|
||||
)}
|
||||
</Typography>
|
||||
<div className="flex flex-col pt-2 pb-4">
|
||||
{l10n
|
||||
.getString(
|
||||
'settings-general-tracker_mechanics-drift_compensation-prediction-description'
|
||||
)
|
||||
.split('\n')
|
||||
.map((line, i) => (
|
||||
<Typography color="secondary" key={i}>
|
||||
{line}
|
||||
</Typography>
|
||||
))}
|
||||
</div>
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
control={control}
|
||||
name="driftCompensation.prediction"
|
||||
label={l10n.getString(
|
||||
'settings-general-tracker_mechanics-drift_compensation-prediction-label'
|
||||
)}
|
||||
/>
|
||||
<DriftCompensationModal
|
||||
accept={() => {
|
||||
setShowDriftCompWarning(false);
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowDriftCompWarning(false);
|
||||
setValue('driftCompensation.enabled', false);
|
||||
}}
|
||||
isOpen={showDriftCompWarning}
|
||||
></DriftCompensationModal>
|
||||
<div className="flex gap-5 pt-5 md:flex-row flex-col">
|
||||
<NumberSelector
|
||||
control={control}
|
||||
name="driftCompensation.amount"
|
||||
label={l10n.getString(
|
||||
'settings-general-tracker_mechanics-drift_compensation-amount-label'
|
||||
)}
|
||||
valueLabelFormat={(value) => percentageFormat.format(value)}
|
||||
min={0.1}
|
||||
max={1.0}
|
||||
step={0.1}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-5 pt-5 md:flex-row flex-col">
|
||||
<NumberSelector
|
||||
control={control}
|
||||
name="driftCompensation.maxResets"
|
||||
label={l10n.getString(
|
||||
'settings-general-tracker_mechanics-drift_compensation-max_resets-label'
|
||||
)}
|
||||
min={1}
|
||||
max={25}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-5 pt-5 md:flex-row flex-col">
|
||||
<NumberSelector
|
||||
control={control}
|
||||
|
||||
@@ -122,7 +122,6 @@ ${trackers.map((t) => {
|
||||
|
||||
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({
|
||||
<Typography bold>
|
||||
{l10n.getString('settings-stay_aligned-general-label')}
|
||||
</Typography>
|
||||
{values.stayAligned.enabled && values.driftCompensation.enabled && (
|
||||
<div className="mt-2">
|
||||
{l10n.getString(
|
||||
'settings-stay_aligned-warnings-drift_compensation'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid sm:grid-cols-1 gap-3 mt-2">
|
||||
<div className="grid sm:grid-cols-2 gap-3 mt-2">
|
||||
<CheckBox
|
||||
variant="toggle"
|
||||
outlined
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from '@/hooks/websocket-api';
|
||||
import { MouseEventHandler } from 'react';
|
||||
import { Button } from '@/components/commons/Button';
|
||||
import { Button, ButtonProps } from '@/components/commons/Button';
|
||||
|
||||
/**
|
||||
* Tells the server to set a relaxed pose to the current pose's angles.
|
||||
@@ -33,7 +33,7 @@ export function DetectRelaxedPoseButton({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{l10n.getString('settings-stay_aligned-relaxed_poses-detect_pose')}
|
||||
{l10n.getString('settings-stay_aligned-relaxed_poses-save_pose')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -43,9 +43,11 @@ export function DetectRelaxedPoseButton({
|
||||
*/
|
||||
export function ResetRelaxedPoseButton({
|
||||
pose,
|
||||
variant = 'primary',
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
variant: ButtonProps['variant'];
|
||||
pose: StayAlignedRelaxedPose;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
} & React.PropsWithChildren) {
|
||||
@@ -54,7 +56,7 @@ export function ResetRelaxedPoseButton({
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
variant={variant}
|
||||
onClick={(e) => {
|
||||
const req = new DetectStayAlignedRelaxedPoseRequestT();
|
||||
req.pose = pose;
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Canvas, Object3DNode, extend, useThree } from '@react-three/fiber';
|
||||
import { Bone } from 'three';
|
||||
import { useMemo, useEffect, useState } from 'react';
|
||||
import {
|
||||
OrbitControls,
|
||||
OrthographicCamera,
|
||||
PerspectiveCamera,
|
||||
} from '@react-three/drei';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||
|
||||
import { useMemo, useEffect, useState, useRef, useLayoutEffect } from 'react';
|
||||
import {
|
||||
BoneKind,
|
||||
createChildren,
|
||||
@@ -21,53 +16,10 @@ import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Typography } from '@/components/commons/Typography';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { bonesAtom } from '@/store/app-store';
|
||||
|
||||
extend({ BasedSkeletonHelper });
|
||||
|
||||
declare module '@react-three/fiber' {
|
||||
interface ThreeElements {
|
||||
basedSkeletonHelper: Object3DNode<
|
||||
BasedSkeletonHelper,
|
||||
typeof BasedSkeletonHelper
|
||||
>;
|
||||
}
|
||||
}
|
||||
import { useConfig } from '@/hooks/config';
|
||||
import { Tween } from '@tweenjs/tween.js';
|
||||
|
||||
const GROUND_COLOR = '#4444aa';
|
||||
const FRUSTUM_SIZE = 10;
|
||||
const FACTOR = 2;
|
||||
// Not currently used but nice to have
|
||||
export function OrthographicCameraWrapper() {
|
||||
const { size } = useThree();
|
||||
const aspect = useMemo(() => size.width / size.height, [size]);
|
||||
|
||||
return (
|
||||
<OrthographicCamera
|
||||
makeDefault
|
||||
zoom={200}
|
||||
top={FRUSTUM_SIZE / FACTOR}
|
||||
bottom={FRUSTUM_SIZE / -FACTOR}
|
||||
left={(0.5 * FRUSTUM_SIZE * aspect) / -FACTOR}
|
||||
right={(0.5 * FRUSTUM_SIZE * aspect) / FACTOR}
|
||||
near={0.1}
|
||||
far={1000}
|
||||
position={[25, 75, 50]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkeletonHelper({ object }: { object: Bone }) {
|
||||
const { size } = useThree();
|
||||
const res = useMemo(() => new THREE.Vector2(size.width, size.height), [size]);
|
||||
|
||||
return (
|
||||
<basedSkeletonHelper
|
||||
frustumCulled={false}
|
||||
resolution={res}
|
||||
args={[object]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Just need to know the length of the total body, so don't need right legs
|
||||
const Y_PARTS = [
|
||||
@@ -135,45 +87,65 @@ export function ToggleableSkeletonVisualizerWidget({
|
||||
);
|
||||
}
|
||||
|
||||
export function SkeletonVisualizerWidget() {
|
||||
const _bones = useAtomValue(bonesAtom);
|
||||
export type SkeletonPreviewView = {
|
||||
left: number;
|
||||
bottom: number;
|
||||
width: number;
|
||||
height: number;
|
||||
camera: THREE.PerspectiveCamera;
|
||||
controls: OrbitControls;
|
||||
hidden: boolean;
|
||||
tween: Tween<THREE.Vector3>;
|
||||
onHeightChange: (view: SkeletonPreviewView, newHeight: number) => void;
|
||||
};
|
||||
|
||||
const { l10n } = useLocalization();
|
||||
const bones = useMemo(() => {
|
||||
return new Map(_bones.map((b) => [b.bodyPart, b]));
|
||||
}, [_bones]);
|
||||
function initializePreview(
|
||||
canvas: HTMLCanvasElement,
|
||||
skeleton: (BoneKind | THREE.Bone)[]
|
||||
) {
|
||||
let lastRenderTimeRef = 0;
|
||||
let frameInterval = 0;
|
||||
|
||||
const skeleton = useMemo(
|
||||
() => createChildren(bones, BoneKind.root),
|
||||
[bones.size]
|
||||
);
|
||||
const views: SkeletonPreviewView[] = [];
|
||||
|
||||
useEffect(() => {
|
||||
skeleton.forEach(
|
||||
(bone) => bone instanceof BoneKind && bone.updateData(bones)
|
||||
);
|
||||
}, [bones]);
|
||||
const resolution = new THREE.Vector2(canvas.clientWidth, canvas.clientHeight);
|
||||
const scene = new THREE.Scene();
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas,
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
});
|
||||
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
|
||||
|
||||
const heightOffset = useMemo(() => {
|
||||
const hmd = bones.get(BodyPart.HEAD);
|
||||
// If I know the head position, don't use an offset
|
||||
if (hmd?.headPositionG?.y !== undefined && hmd.headPositionG?.y > 0) {
|
||||
return 0;
|
||||
}
|
||||
const yLength = Y_PARTS.map((x) => bones.get(x));
|
||||
if (yLength.some((x) => x === undefined)) return 0;
|
||||
return (yLength as BoneT[]).reduce((prev, cur) => prev + cur.boneLength, 0);
|
||||
}, [bones]);
|
||||
const grid = new THREE.GridHelper(10, 50, GROUND_COLOR, GROUND_COLOR);
|
||||
grid.position.set(0, 0, 0);
|
||||
scene.add(grid);
|
||||
|
||||
const targetCamera = useMemo(() => {
|
||||
const hmd = bones.get(BodyPart.HEAD);
|
||||
if (hmd?.headPositionG?.y && hmd.headPositionG.y > 0) {
|
||||
return hmd.headPositionG.y / 2;
|
||||
}
|
||||
return heightOffset / 2;
|
||||
}, [bones]);
|
||||
const skeletonGroup = new THREE.Group();
|
||||
let skeletonHelper = new BasedSkeletonHelper(skeleton[0]);
|
||||
skeletonHelper.resolution.copy(resolution);
|
||||
skeletonGroup.add(skeletonHelper);
|
||||
|
||||
scene.add(skeletonGroup);
|
||||
scene.add(skeleton[0]);
|
||||
|
||||
let heightOffset = 0;
|
||||
|
||||
const rebuildSkeleton = (
|
||||
newSkeleton: (BoneKind | THREE.Bone)[],
|
||||
bones: Map<BodyPart, BoneT>
|
||||
) => {
|
||||
skeletonGroup.remove(skeletonHelper);
|
||||
skeletonHelper.dispose();
|
||||
scene.remove(skeleton[0]);
|
||||
|
||||
skeleton = newSkeleton;
|
||||
|
||||
skeletonHelper = new BasedSkeletonHelper(newSkeleton[0]);
|
||||
skeletonHelper.resolution.copy(resolution);
|
||||
skeletonGroup.add(skeletonHelper);
|
||||
scene.add(newSkeleton[0]);
|
||||
|
||||
const yawReset = useMemo(() => {
|
||||
const hmd = bones.get(BodyPart.HEAD);
|
||||
const chest = bones.get(BodyPart.UPPER_CHEST);
|
||||
// Check if HMD is identity, if it's then use upper chest's rotation
|
||||
@@ -186,15 +158,277 @@ export function SkeletonVisualizerWidget() {
|
||||
const vec = VEC_Y.multiplyScalar(
|
||||
new THREE.Vector3(quat.x, quat.y, quat.z).dot(VEC_Y) / VEC_Y.lengthSq()
|
||||
);
|
||||
return new THREE.Quaternion(vec.x, vec.y, vec.z, quat.w).normalize();
|
||||
const yawReset = new THREE.Quaternion(
|
||||
vec.x,
|
||||
vec.y,
|
||||
vec.z,
|
||||
quat.w
|
||||
).normalize();
|
||||
|
||||
skeletonGroup.rotation.setFromQuaternion(yawReset);
|
||||
};
|
||||
|
||||
const computeHeight = (bones: Map<BodyPart, BoneT>) => {
|
||||
const hmd = bones.get(BodyPart.HEAD);
|
||||
// If I know the head position, don't use an offset
|
||||
if (hmd?.headPositionG?.y !== undefined && hmd.headPositionG?.y > 0) {
|
||||
return 0;
|
||||
}
|
||||
const yLength = Y_PARTS.map((x) => bones.get(x));
|
||||
if (yLength.some((x) => x === undefined)) return 0;
|
||||
return (yLength as BoneT[]).reduce((prev, cur) => prev + cur.boneLength, 0);
|
||||
};
|
||||
|
||||
const render = (delta: number) => {
|
||||
views.forEach((v) => {
|
||||
v.controls.update(delta);
|
||||
|
||||
const left = Math.floor(resolution.x * v.left);
|
||||
const bottom = Math.floor(resolution.y * v.bottom);
|
||||
const width = Math.floor(resolution.x * v.width);
|
||||
const height = Math.floor(resolution.y * v.height);
|
||||
|
||||
renderer.setViewport(left, bottom, width, height);
|
||||
renderer.setScissor(left, bottom, width, height);
|
||||
renderer.setScissorTest(true);
|
||||
|
||||
v.tween.update();
|
||||
|
||||
v.camera.aspect = width / height;
|
||||
v.camera.updateProjectionMatrix();
|
||||
|
||||
if (v.hidden) return;
|
||||
renderer.render(scene, v.camera);
|
||||
});
|
||||
};
|
||||
|
||||
let animationFrameId: number;
|
||||
const animate = (currentTime: number) => {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
if (currentTime - lastRenderTimeRef > frameInterval) {
|
||||
lastRenderTimeRef = currentTime;
|
||||
render(currentTime);
|
||||
}
|
||||
};
|
||||
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
|
||||
// Make sure orbit controls works only on the current view
|
||||
canvas.addEventListener('pointermove', (event) => {
|
||||
const x = event.offsetX / resolution.x;
|
||||
const y = 1 - event.offsetY / resolution.y;
|
||||
views.forEach((v) => {
|
||||
if (
|
||||
x >= v.left &&
|
||||
x <= v.left + v.width &&
|
||||
y >= v.bottom &&
|
||||
y <= v.bottom + v.height
|
||||
) {
|
||||
v.controls.enabled = true;
|
||||
} else {
|
||||
v.controls.enabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
resize: (width: number, height: number) => {
|
||||
resolution.set(width, height);
|
||||
skeletonHelper.resolution.copy(resolution);
|
||||
renderer.setSize(width, height);
|
||||
},
|
||||
setFrameInterval: (interval: number) => {
|
||||
frameInterval = interval;
|
||||
},
|
||||
rebuildSkeleton,
|
||||
updatesBones: (bones: Map<BodyPart, BoneT>) => {
|
||||
skeleton.forEach(
|
||||
(bone) => bone instanceof BoneKind && bone.updateData(bones)
|
||||
);
|
||||
const newHeight = computeHeight(bones);
|
||||
if (newHeight !== heightOffset) {
|
||||
heightOffset = newHeight;
|
||||
skeletonGroup.position.set(0, heightOffset, 0);
|
||||
views.forEach((v) => {
|
||||
v.onHeightChange(v, heightOffset);
|
||||
});
|
||||
}
|
||||
},
|
||||
destroy: () => {
|
||||
skeletonHelper.dispose();
|
||||
renderer.dispose();
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
},
|
||||
addView: ({
|
||||
left,
|
||||
bottom,
|
||||
width,
|
||||
height,
|
||||
position,
|
||||
hidden = false,
|
||||
onHeightChange,
|
||||
}: {
|
||||
left: number;
|
||||
bottom: number;
|
||||
width: number;
|
||||
height: number;
|
||||
position: THREE.Vector3;
|
||||
hidden?: boolean;
|
||||
onHeightChange: (view: SkeletonPreviewView, newHeight: number) => void;
|
||||
}) => {
|
||||
const camera = new THREE.PerspectiveCamera(
|
||||
20,
|
||||
resolution.width / resolution.height,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.maxDistance = 20;
|
||||
controls.dampingFactor = 0.2;
|
||||
controls.enableDamping = true;
|
||||
|
||||
const tween = new Tween(position)
|
||||
.onUpdate(() => {
|
||||
camera.position.copy(position);
|
||||
})
|
||||
.onStart(() => (frameInterval = 0))
|
||||
.onComplete(() => (frameInterval = 1000 / 30));
|
||||
|
||||
camera.position.copy(position);
|
||||
|
||||
const view: SkeletonPreviewView = {
|
||||
camera,
|
||||
left,
|
||||
bottom,
|
||||
width,
|
||||
height,
|
||||
controls,
|
||||
tween,
|
||||
hidden,
|
||||
onHeightChange,
|
||||
};
|
||||
|
||||
views.push(view);
|
||||
|
||||
return view;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const BASE_FRAMERATE = 60;
|
||||
const LOW_FRAMERATE = 60;
|
||||
|
||||
type PreviewContext = ReturnType<typeof initializePreview>;
|
||||
|
||||
function SkeletonVisualizer({
|
||||
onInit,
|
||||
}: {
|
||||
onInit: (context: PreviewContext) => void;
|
||||
}) {
|
||||
const { config } = useConfig();
|
||||
|
||||
const previewContext = useRef<PreviewContext | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const resizeObserver = useRef(new ResizeObserver(([e]) => onResize(e)));
|
||||
const _bones = useAtomValue(bonesAtom);
|
||||
|
||||
const bones = useMemo(() => {
|
||||
return new Map(_bones.map((b) => [b.bodyPart, b]));
|
||||
}, [_bones]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bones.size === 0) return;
|
||||
const context = previewContext.current;
|
||||
if (!context) return;
|
||||
context.rebuildSkeleton(createChildren(bones, BoneKind.root), bones);
|
||||
}, [bones.size]);
|
||||
|
||||
const scale = useMemo(
|
||||
() => Math.max(1.8, heightOffset) / 1.8,
|
||||
[heightOffset]
|
||||
);
|
||||
useEffect(() => {
|
||||
const context = previewContext.current;
|
||||
if (!context) return;
|
||||
context.updatesBones(bones);
|
||||
}, [bones]);
|
||||
|
||||
const onResize = (e: ResizeObserverEntry) => {
|
||||
const context = previewContext.current;
|
||||
if (!context || !containerRef.current || !canvasRef.current) return;
|
||||
context.resize(e.contentRect.width, e.contentRect.height);
|
||||
};
|
||||
|
||||
const onEnter = () => {
|
||||
if (config?.devSettings.fastDataFeed) return;
|
||||
const context = previewContext.current;
|
||||
if (!context) return;
|
||||
context.setFrameInterval(1000 / BASE_FRAMERATE);
|
||||
};
|
||||
|
||||
const onLeave = () => {
|
||||
if (config?.devSettings.fastDataFeed) return;
|
||||
const context = previewContext.current;
|
||||
if (!context) return;
|
||||
context.setFrameInterval(1000 / LOW_FRAMERATE);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!canvasRef.current || !containerRef.current)
|
||||
throw 'invalid state - no canvas or container';
|
||||
resizeObserver.current.observe(containerRef.current);
|
||||
|
||||
previewContext.current = initializePreview(
|
||||
canvasRef.current,
|
||||
createChildren(bones, BoneKind.root)
|
||||
);
|
||||
if (!config?.devSettings.fastDataFeed)
|
||||
previewContext.current.setFrameInterval(1000 / LOW_FRAMERATE);
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
previewContext.current.resize(rect.width, rect.height);
|
||||
|
||||
containerRef.current.addEventListener('mouseenter', onEnter);
|
||||
containerRef.current.addEventListener('mouseleave', onLeave);
|
||||
|
||||
onInit(previewContext.current);
|
||||
|
||||
return () => {
|
||||
if (!previewContext.current || !containerRef.current) return;
|
||||
resizeObserver.current.unobserve(containerRef.current);
|
||||
previewContext.current.destroy();
|
||||
|
||||
containerRef.current.removeEventListener('mouseenter', onEnter);
|
||||
containerRef.current.removeEventListener('mouseleave', onLeave);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={classNames('w-full h-full')}>
|
||||
<canvas ref={canvasRef} className="w-full h-full"></canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkeletonVisualizerWidget({
|
||||
onInit = (context) => {
|
||||
context.addView({
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
position: new THREE.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;
|
||||
},
|
||||
});
|
||||
},
|
||||
}: {
|
||||
onInit?: (context: PreviewContext) => void;
|
||||
}) {
|
||||
const { l10n } = useLocalization();
|
||||
|
||||
if (!skeleton) return <></>;
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
@@ -203,24 +437,7 @@ export function SkeletonVisualizerWidget() {
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Canvas className={classNames('container mx-auto')}>
|
||||
<gridHelper args={[10, 50, GROUND_COLOR, GROUND_COLOR]} />
|
||||
<group position={[0, heightOffset, 0]} quaternion={yawReset}>
|
||||
<SkeletonHelper object={skeleton[0]}></SkeletonHelper>
|
||||
</group>
|
||||
<primitive object={skeleton[0]} />
|
||||
<PerspectiveCamera
|
||||
makeDefault
|
||||
position={[3, 2.5, -3]}
|
||||
fov={20}
|
||||
zoom={1 / scale}
|
||||
/>
|
||||
<OrbitControls
|
||||
target={[0, targetCamera, 0]}
|
||||
maxDistance={20}
|
||||
maxPolarAngle={Math.PI / 2}
|
||||
/>
|
||||
</Canvas>
|
||||
<SkeletonVisualizer onInit={onInit}></SkeletonVisualizer>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
} from 'solarxr-protocol';
|
||||
import { playSoundOnResetEnded, playSoundOnResetStarted } from '@/sounds/sounds';
|
||||
import { useConfig } from './config';
|
||||
import { useDataFeedConfig } from './datafeed-config';
|
||||
import { useBonesDataFeedConfig, useDataFeedConfig } from './datafeed-config';
|
||||
import { useWebsocketAPI } from './websocket-api';
|
||||
import { error } from '@/utils/logging';
|
||||
import { cacheWrap } from './cache';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { datafeedAtom, devicesAtom } from '@/store/app-store';
|
||||
import { bonesAtom, datafeedAtom, devicesAtom } from '@/store/app-store';
|
||||
import { updateSentryContext } from '@/utils/sentry';
|
||||
|
||||
export interface FirmwareRelease {
|
||||
@@ -34,7 +34,9 @@ export function useProvideAppContext(): AppContext {
|
||||
useWebsocketAPI();
|
||||
const { config } = useConfig();
|
||||
const { dataFeedConfig } = useDataFeedConfig();
|
||||
const bonesDataFeedConfig = useBonesDataFeedConfig();
|
||||
const setDatafeed = useSetAtom(datafeedAtom);
|
||||
const setBones = useSetAtom(bonesAtom);
|
||||
const devices = useAtomValue(devicesAtom);
|
||||
|
||||
const [currentFirmwareRelease, setCurrentFirmwareRelease] =
|
||||
@@ -43,13 +45,17 @@ export function useProvideAppContext(): AppContext {
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
const startDataFeed = new StartDataFeedT();
|
||||
startDataFeed.dataFeeds = [dataFeedConfig];
|
||||
startDataFeed.dataFeeds = [dataFeedConfig, bonesDataFeedConfig];
|
||||
sendDataFeedPacket(DataFeedMessage.StartDataFeed, startDataFeed);
|
||||
}
|
||||
}, [isConnected]);
|
||||
|
||||
useDataFeedPacket(DataFeedMessage.DataFeedUpdate, (packet: DataFeedUpdateT) => {
|
||||
setDatafeed(packet);
|
||||
if (packet.index === 0) {
|
||||
setDatafeed(packet);
|
||||
} else if (packet.index === 1) {
|
||||
setBones(packet.bones);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -26,7 +26,7 @@ export function useDataFeedConfig() {
|
||||
|
||||
const dataFeedConfig = new DataFeedConfigT();
|
||||
dataFeedConfig.dataMask = dataMask;
|
||||
dataFeedConfig.boneMask = true;
|
||||
dataFeedConfig.boneMask = false;
|
||||
dataFeedConfig.minimumTimeSinceLast = 1000 / feedMaxTps;
|
||||
dataFeedConfig.syntheticTrackersMask = trackerData;
|
||||
dataFeedConfig.stayAlignedPoseMask = true;
|
||||
@@ -36,3 +36,10 @@ export function useDataFeedConfig() {
|
||||
feedMaxTps,
|
||||
};
|
||||
}
|
||||
|
||||
export function useBonesDataFeedConfig() {
|
||||
const dataFeedConfig = new DataFeedConfigT();
|
||||
dataFeedConfig.boneMask = true;
|
||||
dataFeedConfig.minimumTimeSinceLast = 1000 / 40;
|
||||
return dataFeedConfig;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { atom } from 'jotai';
|
||||
import {
|
||||
BodyPart,
|
||||
BoneT,
|
||||
DataFeedUpdateT,
|
||||
DeviceDataT,
|
||||
TrackerDataT,
|
||||
@@ -18,6 +19,8 @@ export const ignoredTrackersAtom = atom(new Set<string>());
|
||||
|
||||
export const datafeedAtom = atom(new DataFeedUpdateT());
|
||||
|
||||
export const bonesAtom = atom<BoneT[]>([]);
|
||||
|
||||
export const devicesAtom = selectAtom(
|
||||
datafeedAtom,
|
||||
(datafeed) => datafeed.devices,
|
||||
@@ -56,12 +59,6 @@ export const computedTrackersAtom = selectAtom(
|
||||
isEqual
|
||||
);
|
||||
|
||||
export const bonesAtom = selectAtom(
|
||||
datafeedAtom,
|
||||
(datafeed) => datafeed.bones,
|
||||
isEqual
|
||||
);
|
||||
|
||||
export const hasHMDTrackerAtom = atom((get) => {
|
||||
const trackers = get(flatTrackersAtom);
|
||||
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -71,6 +71,9 @@ importers:
|
||||
'@tauri-apps/plugin-store':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@tweenjs/tween.js':
|
||||
specifier: ^25.0.0
|
||||
version: 25.0.0
|
||||
'@twemoji/svg':
|
||||
specifier: ^15.0.0
|
||||
version: 15.0.0
|
||||
@@ -831,6 +834,7 @@ packages:
|
||||
'@react-hookz/deep-equal@3.0.3':
|
||||
resolution: {integrity: sha512-SLy+NmiDpncqc2d9TR4Y4R7f8lUFOQK9WbnIq02A6wDxy+dTHfA2Np0dPvj0SFp6i1nqERLmEUe9MxPLuO/IqA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
deprecated: Package is deprecated and will be deleted soon. Use @ver0/deep-equal instead.
|
||||
|
||||
'@react-spring/animated@9.6.1':
|
||||
resolution: {integrity: sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==}
|
||||
@@ -1258,6 +1262,9 @@ packages:
|
||||
'@tweenjs/tween.js@23.1.2':
|
||||
resolution: {integrity: sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==}
|
||||
|
||||
'@tweenjs/tween.js@25.0.0':
|
||||
resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==}
|
||||
|
||||
'@twemoji/svg@15.0.0':
|
||||
resolution: {integrity: sha512-ZSPef2B6nBaYnfgdTbAy4jgW95o7pi2xPGwGCU+WMTxo7J6B1lMPTWwSq/wTuiMq+N0khQ90CcvYp1wFoQpo/w==}
|
||||
|
||||
@@ -5463,6 +5470,8 @@ snapshots:
|
||||
|
||||
'@tweenjs/tween.js@23.1.2': {}
|
||||
|
||||
'@tweenjs/tween.js@25.0.0': {}
|
||||
|
||||
'@twemoji/svg@15.0.0': {}
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
|
||||
@@ -54,7 +54,7 @@ public class DataFeedHandler extends ProtocolHandler<DataFeedMessageHeader> {
|
||||
|
||||
FlatBufferBuilder fbb = new FlatBufferBuilder(300);
|
||||
|
||||
int messageOffset = this.buildDatafeed(fbb, req.config().unpack());
|
||||
int messageOffset = this.buildDatafeed(fbb, req.config().unpack(), 0);
|
||||
|
||||
DataFeedMessageHeader.startDataFeedMessageHeader(fbb);
|
||||
DataFeedMessageHeader.addMessage(fbb, messageOffset);
|
||||
@@ -70,7 +70,7 @@ public class DataFeedHandler extends ProtocolHandler<DataFeedMessageHeader> {
|
||||
conn.send(fbb.dataBuffer());
|
||||
}
|
||||
|
||||
public int buildDatafeed(FlatBufferBuilder fbb, DataFeedConfigT config) {
|
||||
public int buildDatafeed(FlatBufferBuilder fbb, DataFeedConfigT config, int index) {
|
||||
int devicesOffset = DataFeedBuilder
|
||||
.createDevicesData(
|
||||
fbb,
|
||||
@@ -110,7 +110,8 @@ public class DataFeedHandler extends ProtocolHandler<DataFeedMessageHeader> {
|
||||
devicesOffset,
|
||||
trackersOffset,
|
||||
bonesOffset,
|
||||
stayAlignedPoseOffset
|
||||
stayAlignedPoseOffset,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,7 +134,7 @@ public class DataFeedHandler extends ProtocolHandler<DataFeedMessageHeader> {
|
||||
fbb = new FlatBufferBuilder(300);
|
||||
}
|
||||
|
||||
int messageOffset = this.buildDatafeed(fbb, configT);
|
||||
int messageOffset = this.buildDatafeed(fbb, configT, index);
|
||||
|
||||
DataFeedMessageHeader.startDataFeedMessageHeader(fbb);
|
||||
DataFeedMessageHeader.addMessage(fbb, messageOffset);
|
||||
|
||||
@@ -131,7 +131,7 @@ class TrackerResetsHandler(val tracker: Tracker) {
|
||||
* Reads/loads drift compensation settings from given config
|
||||
*/
|
||||
fun readDriftCompensationConfig(config: DriftCompensationConfig) {
|
||||
compensateDrift = config.enabled
|
||||
compensateDrift = false
|
||||
driftPrediction = config.prediction
|
||||
driftAmount = config.amount
|
||||
val maxResets = config.maxResets
|
||||
|
||||
Submodule solarxr-protocol updated: bc125ae49a...737d007b4f
Reference in New Issue
Block a user