Improvements on the Autobone GUI (#776)

Co-authored-by: Butterscotch! <bscotchvanilla@gmail.com>
This commit is contained in:
Uriel
2023-07-20 05:19:43 -03:00
committed by GitHub
parent d9c631fcf6
commit 02acc6ede1
32 changed files with 1523 additions and 625 deletions

View File

@@ -29,10 +29,11 @@
"react-modal": "3.15.1",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.2.2",
"semver": "^7.5.0",
"semver": "^7.5.3",
"solarxr-protocol": "file:../solarxr-protocol",
"three": "^0.148.0",
"typescript": "^4.6.3"
"ts-pattern": "^5.0.1",
"typescript": "^5.1.6"
},
"scripts": {
"start": "vite --force",
@@ -64,30 +65,30 @@
]
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.0",
"@tailwindcss/forms": "^0.5.3",
"@tauri-apps/cli": "^1.4.0",
"@types/file-saver": "^2.0.5",
"@types/react": "18.0.25",
"@types/react-dom": "^18.0.5",
"@types/react-modal": "3.13.1",
"@types/three": "^0.148.0",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.0",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"autoprefixer": "^10.4.4",
"cross-env": "^7.0.3",
"eslint": "^8.18.0",
"eslint": "^8.44.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-import-resolver-typescript": "^3.1.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-react": "^7.30.1",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.12",
"prettier": "^2.7.1",
"postcss": "^8.4.24",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"rollup-plugin-visualizer": "^5.9.2",
"tailwind-gradient-mask-image": "^1.0.0",
"tailwindcss": "^3.3.1",
"vite": "^4.0.3"
"tailwindcss": "^3.3.2",
"vite": "^4.3.9"
}
}

View File

@@ -730,7 +730,10 @@ onboarding-choose_proportions-description = Body proportions are used to know th
onboarding-choose_proportions-auto_proportions = Automatic proportions
# Italized text
onboarding-choose_proportions-auto_proportions-subtitle = Recommended
onboarding-choose_proportions-auto_proportions-description = This will guess your proportions by recording a sample of your movements and passing it through an algorithm
onboarding-choose_proportions-auto_proportions-descriptionv2 =
This will guess your proportions by recording a sample of your movements and passing it through an algorithm.
<b>It requires having your HMD connected to SlimeVR!</b>
onboarding-choose_proportions-manual_proportions = Manual proportions
# Italized text
onboarding-choose_proportions-manual_proportions-subtitle = For small touches
@@ -764,6 +767,18 @@ onboarding-automatic_proportions-requirements-description =
Your trackers and headset are working properly within the SlimeVR server.
Your headset is reporting positional data to the SlimeVR server (this generally means having SteamVR running and connected to SlimeVR using SlimeVR's SteamVR driver).
onboarding-automatic_proportions-requirements-next = I have read the requirements
onboarding-automatic_proportions-check_height-title = Check your height
onboarding-automatic_proportions-check_height-description = We use your height as a basis of our measurements by using the HMD's height as an approximation of your actual height, but it's better to check if they are right yourself!
# All the text is in bold!
onboarding-automatic_proportions-check_height-calculation_warning = Please press the button while standing <u>upright</u> to calculate your height. You have 3 seconds after you press the button!
onboarding-automatic_proportions-check_height-fetch_height = I'm standing!
# Context is that the height is unknown
onboarding-automatic_proportions-check_height-unknown = Unknown
# Shows an element below it
onboarding-automatic_proportions-check_height-height = Your height is
# Shows an element below it
onboarding-automatic_proportions-check_height-hmd_height = And HMD height is
onboarding-automatic_proportions-check_height-next_step = They are fine
onboarding-automatic_proportions-start_recording-title = Get ready to move
onboarding-automatic_proportions-start_recording-description = We're now going to record some specific poses and moves. These will be prompted in the next screen. Be ready to start when the button is pressed!
onboarding-automatic_proportions-start_recording-next = Start Recording
@@ -792,6 +807,10 @@ onboarding-automatic_proportions-verify_results-redo = Redo recording
onboarding-automatic_proportions-verify_results-confirm = They're correct
onboarding-automatic_proportions-done-title = Body measured and saved.
onboarding-automatic_proportions-done-description = Your body proportions' calibration is complete!
onboarding-automatic_proportions-error_modal =
<b>Warning:</b> An error was found while estimating proportions!
Please <docs>check the docs</docs> or join our <discord>Discord</discord> for help ^_^
onboarding-automatic_proportions-error_modal-confirm = Understood!
## Home
home-no_trackers = No trackers detected or assigned

View File

@@ -55,7 +55,8 @@ import { error, log } from './utils/logging';
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
export const VersionContext = createContext('');
export const DOCS_SITE = 'https://docs.slimevr.dev/';
export const DOCS_SITE = 'https://docs.slimevr.dev';
export const SLIMEVR_DISCORD = 'https://discord.gg/slimevr';
function Layout() {
const { loading } = useConfig();

View File

@@ -0,0 +1,14 @@
import { open } from '@tauri-apps/api/shell';
import { ReactNode } from 'react';
export function A({ href, children }: { href: string; children?: ReactNode }) {
return (
<a
href="javascript:void(0)"
onClick={() => open(href).catch(() => window.open(href, '_blank'))}
className="underline"
>
{children}
</a>
);
}

View File

@@ -10,6 +10,7 @@ export function NumberSelector({
min,
max,
step,
disabled = false,
}: {
label: string;
valueLabelFormat?: (value: number) => string;
@@ -18,6 +19,7 @@ export function NumberSelector({
min: number;
max: number;
step: number | ((value: number, add: boolean) => number);
disabled?: boolean;
}) {
const stepFn =
typeof step === 'function'
@@ -38,7 +40,7 @@ export function NumberSelector({
variant="tertiary"
rounded
onClick={() => onChange(stepFn(value, false))}
disabled={stepFn(value, false) < min}
disabled={stepFn(value, false) < min || disabled}
>
-
</Button>
@@ -51,7 +53,7 @@ export function NumberSelector({
variant="tertiary"
rounded
onClick={() => onChange(stepFn(value, true))}
disabled={stepFn(value, true) > max}
disabled={stepFn(value, true) > max || disabled}
>
+
</Button>

View File

@@ -16,6 +16,7 @@ import { Recording } from './autobone-steps/Recording';
import { StartRecording } from './autobone-steps/StartRecording';
import { VerifyResultsStep } from './autobone-steps/VerifyResults';
import { useCountdown } from '../../../../hooks/countdown';
import { CheckHeight } from './autobone-steps/СheckHeight';
export function AutomaticProportionsPage() {
const { l10n } = useLocalization();
@@ -53,6 +54,7 @@ export function AutomaticProportionsPage() {
steps={[
{ type: 'numbered', component: PutTrackersOnStep },
{ type: 'numbered', component: RequirementsStep },
{ type: 'numbered', component: CheckHeight },
{ type: 'numbered', component: StartRecording },
{ type: 'fullsize', component: Recording },
{ type: 'numbered', component: VerifyResultsStep },

View File

@@ -60,7 +60,7 @@ export function BodyProportions({
unit: 'centimeter',
maximumFractionDigits: 1,
});
const percentageFormat = Intl.NumberFormat(currentLocales, {
const percentageFormat = new Intl.NumberFormat(currentLocales, {
style: 'percent',
maximumFractionDigits: 1,
});

View File

@@ -1,6 +1,6 @@
import { useOnboarding } from '../../../../hooks/onboarding';
import { useLocalization } from '@fluent/react';
import { useState } from 'react';
import { Localized, useLocalization } from '@fluent/react';
import { useMemo, useState } from 'react';
import classNames from 'classnames';
import { Typography } from '../../../commons/Typography';
import { Button } from '../../../commons/Button';
@@ -14,14 +14,39 @@ import saveAs from 'file-saver';
import { save } from '@tauri-apps/api/dialog';
import { writeTextFile } from '@tauri-apps/api/fs';
import { useIsTauri } from '../../../../hooks/breakpoint';
import { useAppContext } from '../../../../hooks/app';
import { error } from '../../../../utils/logging';
export const MIN_HEIGHT = 0.4;
export const MAX_HEIGHT = 4;
export const DEFAULT_HEIGHT = 1.5;
export function ProportionsChoose() {
const isTauri = useIsTauri();
const { l10n } = useLocalization();
const { applyProgress, state } = useOnboarding();
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const [animated, setAnimated] = useState(false);
const { computedTrackers } = useAppContext();
const hmdTracker = useMemo(
() =>
computedTrackers.find(
(tracker) =>
tracker.tracker.trackerId?.trackerNum === 1 &&
tracker.tracker.trackerId.deviceId?.id === undefined
),
[computedTrackers]
);
const beneathFloor = useMemo(
() =>
!(
hmdTracker?.tracker.position &&
hmdTracker.tracker.position.y >= MIN_HEIGHT
),
[hmdTracker?.tracker.position?.y]
);
useRPCPacket(
RpcMessage.SkeletonConfigResponse,
@@ -145,15 +170,23 @@ export function ProportionsChoose() {
</Typography>
</div>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-choose_proportions-auto_proportions-description'
)}
</Typography>
<Localized
id="onboarding-choose_proportions-auto_proportions-descriptionv2"
elems={{ b: <b></b> }}
>
<Typography
color="secondary"
whitespace="whitespace-pre-line"
>
Description for autobone
</Typography>
</Localized>
</div>
</div>
<Button
variant="primary"
// Check if we are in dev mode and just let it be used
disabled={beneathFloor && import.meta.env.PROD}
to="/onboarding/body-proportions/auto"
className="self-start mt-auto"
state={{ alonePage: state.alonePage }}

View File

@@ -0,0 +1,64 @@
import { Localized, useLocalization } from '@fluent/react';
import ReactModal from 'react-modal';
import { BaseModal } from '../../../../commons/BaseModal';
import { WarningBox } from '../../../../commons/TipBox';
import { Button } from '../../../../commons/Button';
import { A } from '../../../../commons/A';
import { DOCS_SITE, SLIMEVR_DISCORD } from '../../../../../App';
export function AutoboneErrorModal({
isOpen = true,
onClose,
...props
}: {
/**
* Is the parent/sibling component opened?
*/
isOpen: boolean;
/**
* Function to trigger when closed or accepted
*/
onClose: () => void;
} & ReactModal.Props) {
const { l10n } = useLocalization();
return (
<BaseModal
isOpen={isOpen}
shouldCloseOnOverlayClick
shouldCloseOnEsc
onRequestClose={onClose}
className={props.className}
overlayClassName={props.overlayClassName}
>
<div className="flex w-full h-full flex-col ">
<div className="flex w-full flex-col flex-grow items-center gap-3">
<Localized
id="onboarding-automatic_proportions-error_modal"
elems={{
b: <b></b>,
docs: (
<A
href={`${DOCS_SITE}/server/body-config.html#common-issues--debugging`}
></A>
),
discord: <A href={SLIMEVR_DISCORD}></A>,
}}
>
<WarningBox>
<b>Warning:</b> An autobone error happened!
</WarningBox>
</Localized>
<div className="flex flex-row gap-3 pt-5 place-content-center">
<Button variant="primary" onClick={onClose}>
{l10n.getString(
'onboarding-automatic_proportions-error_modal-confirm'
)}
</Button>
</div>
</div>
</div>
</BaseModal>
);
}

View File

@@ -1,22 +1,49 @@
import { useEffect } from 'react';
import { useAutobone } from '../../../../../hooks/autobone';
import { ReactNode, useEffect, useState } from 'react';
import { ProcessStatus, useAutobone } from '../../../../../hooks/autobone';
import { ProgressBar } from '../../../../commons/ProgressBar';
import { TipBox } from '../../../../commons/TipBox';
import { Typography } from '../../../../commons/Typography';
import { useLocalization } from '@fluent/react';
import { P, match } from 'ts-pattern';
import { AutoboneErrorModal } from './AutoboneErrorModal';
export function Recording({ nextStep }: { nextStep: () => void }) {
export function Recording({
nextStep,
resetSteps,
}: {
nextStep: () => void;
resetSteps: () => void;
}) {
const { l10n } = useLocalization();
const { progress, hasCalibration, hasRecording } = useAutobone();
const { progress, hasCalibration, hasRecording, eta } = useAutobone();
const [modalOpen, setModalOpen] = useState(false);
useEffect(() => {
if (progress === 1 && hasCalibration) {
if (
hasRecording === ProcessStatus.REJECTED ||
hasCalibration === ProcessStatus.REJECTED
) {
setModalOpen(true);
}
if (progress !== 1) return;
if (
hasRecording === ProcessStatus.FULFILLED &&
hasCalibration === ProcessStatus.FULFILLED
) {
nextStep();
}
}, [progress, hasCalibration]);
}, [progress, hasCalibration, hasRecording]);
return (
<div className="flex flex-col items-center w-full justify-between">
<AutoboneErrorModal
isOpen={modalOpen}
onClose={() => {
setModalOpen(false);
resetSteps();
}}
></AutoboneErrorModal>
<div className="flex gap-1 flex-col justify-center items-center">
<div className="flex text-status-critical justify-center items-center gap-1">
<div className="w-2 h-2 rounded-lg bg-status-critical"></div>
@@ -51,19 +78,39 @@ export function Recording({ nextStep }: { nextStep: () => void }) {
<TipBox>{l10n.getString('tips-do_not_move_heels')}</TipBox>
</div>
<div className="flex flex-col gap-2 items-center w-full max-w-[150px]">
<ProgressBar progress={progress} height={2}></ProgressBar>
<ProgressBar
progress={progress}
height={2}
colorClass={match([hasCalibration, hasRecording])
.returnType<string | undefined>()
.with(
P.union(
[ProcessStatus.REJECTED, P._],
[P._, ProcessStatus.REJECTED]
),
() => 'bg-status-critical'
)
.with(
[ProcessStatus.FULFILLED, ProcessStatus.FULFILLED],
() => 'bg-status-success'
)
.otherwise(() => undefined)}
></ProgressBar>
<Typography color="secondary">
{!hasCalibration && hasRecording
? l10n.getString(
{match([hasCalibration, hasRecording])
.returnType<ReactNode>()
.with([ProcessStatus.PENDING, ProcessStatus.FULFILLED], () =>
l10n.getString(
'onboarding-automatic_proportions-recording-processing'
)
: l10n.getString(
)
.with([ProcessStatus.PENDING, ProcessStatus.PENDING], () =>
l10n.getString(
'onboarding-automatic_proportions-recording-timer',
{
// TODO: The progress should be communicated by the server in SolarXR
time: Math.round(20 * (1 - progress)),
}
)}
{ time: Math.round(eta) }
)
)
.otherwise(() => '')}
</Typography>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import { useAutobone } from '../../../../../hooks/autobone';
import { ProcessStatus, useAutobone } from '../../../../../hooks/autobone';
import { Button } from '../../../../commons/Button';
import { Typography } from '../../../../commons/Typography';
import { useLocalization } from '@fluent/react';
@@ -69,13 +69,14 @@ export function VerifyResultsStep({
<Typography bold>{(value * 100).toFixed(2)} CM</Typography>
</div>
))}
{!hasCalibration && hasRecording && (
<Typography>
{l10n.getString(
'onboarding-automatic-proportions-verify-results-processing'
)}
</Typography>
)}
{hasCalibration === ProcessStatus.PENDING &&
hasRecording === ProcessStatus.FULFILLED && (
<Typography>
{l10n.getString(
'onboarding-automatic-proportions-verify-results-processing'
)}
</Typography>
)}
</div>
</div>
</div>

View File

@@ -0,0 +1,176 @@
import {
AutoBoneSettingsT,
ChangeSettingsRequestT,
HeightRequestT,
HeightResponseT,
RpcMessage,
} from 'solarxr-protocol';
import { useWebsocketAPI } from '../../../../../hooks/websocket-api';
import { Button } from '../../../../commons/Button';
import { Typography } from '../../../../commons/Typography';
import { Localized, useLocalization } from '@fluent/react';
import { useForm } from 'react-hook-form';
import { useMemo, useState } from 'react';
import { NumberSelector } from '../../../../commons/NumberSelector';
import { DEFAULT_HEIGHT, MIN_HEIGHT } from '../ProportionsChoose';
import { useLocaleConfig } from '../../../../../i18n/config';
import { useCountdown } from '../../../../../hooks/countdown';
interface HeightForm {
height: number;
hmdHeight: number;
}
export function CheckHeight({
nextStep,
prevStep,
variant,
}: {
nextStep: () => void;
prevStep: () => void;
variant: 'onboarding' | 'alone';
}) {
const { l10n } = useLocalization();
const { control, handleSubmit, setValue } = useForm<HeightForm>();
const [fetchedHeight, setFetchedHeight] = useState(false);
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const { timer, isCounting, startCountdown } = useCountdown({
duration: 3,
onCountdownEnd: () => {
setFetchedHeight(true);
sendRPCPacket(RpcMessage.HeightRequest, new HeightRequestT());
},
});
const { currentLocales } = useLocaleConfig();
const mFormat = useMemo(
() =>
new Intl.NumberFormat(currentLocales, {
style: 'unit',
unit: 'meter',
maximumFractionDigits: 2,
}),
[currentLocales]
);
const sFormat = useMemo(
() => new Intl.RelativeTimeFormat(currentLocales, { style: 'short' }),
[currentLocales]
);
useRPCPacket(
RpcMessage.HeightResponse,
({ hmdHeight, estimatedFullHeight }: HeightResponseT) => {
setValue('height', estimatedFullHeight || DEFAULT_HEIGHT);
setValue('hmdHeight', hmdHeight);
}
);
const onSubmit = (values: HeightForm) => {
const changeSettings = new ChangeSettingsRequestT();
const autobone = new AutoBoneSettingsT();
autobone.targetFullHeight = values.height;
autobone.targetHmdHeight = values.hmdHeight;
changeSettings.autoBoneSettings = autobone;
sendRPCPacket(RpcMessage.ChangeSettingsRequest, changeSettings);
nextStep();
};
return (
<>
<div className="flex flex-col flex-grow">
<div className="flex flex-grow flex-col gap-4">
<Typography variant="main-title" bold>
{l10n.getString(
'onboarding-automatic_proportions-check_height-title'
)}
</Typography>
<div>
<Typography color="secondary">
{l10n.getString(
'onboarding-automatic_proportions-check_height-description'
)}
</Typography>
<Localized
id="onboarding-automatic_proportions-check_height-calculation_warning"
elems={{ u: <span className="underline"></span> }}
>
<Typography color="secondary" bold>
Press the button to get your height!
</Typography>
</Localized>
<Button
variant="primary"
className="mt-2"
onClick={startCountdown}
disabled={isCounting}
>
{isCounting
? sFormat.format(timer, 'second')
: l10n.getString(
'onboarding-automatic_proportions-check_height-fetch_height'
)}
</Button>
</div>
<form className="flex flex-col self-center items-center justify-center">
<NumberSelector
control={control}
name="height"
label={l10n.getString(
'onboarding-automatic_proportions-check_height-height'
)}
valueLabelFormat={(value) =>
isNaN(value)
? l10n.getString(
'onboarding-automatic_proportions-check_height-unknown'
)
: mFormat.format(value)
}
min={MIN_HEIGHT}
max={4}
step={0.01}
/>
<NumberSelector
control={control}
name="hmdHeight"
label={l10n.getString(
'onboarding-automatic_proportions-check_height-hmd_height'
)}
valueLabelFormat={(value) =>
isNaN(value)
? l10n.getString(
'onboarding-automatic_proportions-check_height-unknown'
)
: mFormat.format(value)
}
min={MIN_HEIGHT}
max={4}
step={0.01}
disabled={true}
/>
</form>
</div>
<div className="flex gap-3 mobile:justify-between">
<Button
variant={variant === 'onboarding' ? 'secondary' : 'tertiary'}
onClick={prevStep}
>
{l10n.getString('onboarding-automatic_proportions-prev_step')}
</Button>
<Button
variant="primary"
onClick={handleSubmit(onSubmit)}
disabled={!fetchedHeight}
>
{l10n.getString(
'onboarding-automatic_proportions-check_height-next_step'
)}
</Button>
</div>
</div>
</>
);
}

View File

@@ -125,7 +125,7 @@ export function GeneralSettings() {
const { currentLocales } = useLocaleConfig();
// const pageRef = useRef<HTMLFormElement | null>(null);
const percentageFormat = Intl.NumberFormat(currentLocales, {
const percentageFormat = new Intl.NumberFormat(currentLocales, {
style: 'percent',
maximumFractionDigits: 0,
});

View File

@@ -42,6 +42,7 @@ export interface AppContext {
trackers: FlatDeviceTracker[];
dispatch: Dispatch<AppStateAction>;
bones: BoneT[];
computedTrackers: FlatDeviceTracker[];
}
export function reducer(state: AppState, action: AppStateAction) {
@@ -89,6 +90,11 @@ export function useProvideAppContext(): AppContext {
[state]
);
const computedTrackers: FlatDeviceTracker[] = useMemo(
() => (state.datafeed?.syntheticTrackers || []).map((tracker) => ({ tracker })),
[state]
);
const bones = useMemo(() => state.datafeed?.bones || [], [state]);
useDataFeedPacket(DataFeedMessage.DataFeedUpdate, (packet: DataFeedUpdateT) => {
@@ -114,6 +120,7 @@ export function useProvideAppContext(): AppContext {
trackers,
dispatch,
bones,
computedTrackers,
};
}

View File

@@ -1,23 +1,30 @@
import { createContext, useContext, useMemo, useState } from 'react';
import {
AutoBoneApplyRequestT,
AutoBoneEpochResponseT,
AutoBoneProcessRequestT,
AutoBoneProcessStatusResponseT,
AutoBoneProcessType,
RpcMessage,
SkeletonBone,
SkeletonConfigRequestT,
SkeletonPartT,
} from 'solarxr-protocol';
import { useWebsocketAPI } from './websocket-api';
import { useLocalization } from '@fluent/react';
import { log } from '../utils/logging';
export enum ProcessStatus {
PENDING,
FULFILLED,
REJECTED,
}
export interface AutoboneContext {
hasRecording: boolean;
hasCalibration: boolean;
hasRecording: ProcessStatus;
hasCalibration: ProcessStatus;
progress: number;
bodyParts: { bone: SkeletonBone; label: string; value: number }[] | null;
eta: number;
startRecording: () => void;
startProcessing: () => void;
applyProcessing: () => void;
@@ -26,9 +33,10 @@ export interface AutoboneContext {
export function useProvideAutobone(): AutoboneContext {
const { l10n } = useLocalization();
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const [hasRecording, setHasRecording] = useState(false);
const [hasCalibration, setHasCalibration] = useState(false);
const [hasRecording, setHasRecording] = useState(ProcessStatus.PENDING);
const [hasCalibration, setHasCalibration] = useState(ProcessStatus.PENDING);
const [progress, setProgress] = useState(0);
const [eta, setEta] = useState(-1);
const [skeletonParts, setSkeletonParts] = useState<SkeletonPartT[] | null>(null);
const bodyParts = useMemo(() => {
@@ -48,6 +56,7 @@ export function useProvideAutobone(): AutoboneContext {
// }
setProgress(0);
setEta(-1);
const processRequest = new AutoBoneProcessRequestT();
processRequest.processType = processType;
@@ -56,19 +65,19 @@ export function useProvideAutobone(): AutoboneContext {
};
const startRecording = () => {
setHasCalibration(false);
setHasRecording(false);
setHasCalibration(ProcessStatus.PENDING);
setHasRecording(ProcessStatus.PENDING);
setSkeletonParts(null);
startProcess(AutoBoneProcessType.RECORD);
};
const startProcessing = () => {
setHasCalibration(false);
setHasCalibration(ProcessStatus.PENDING);
startProcess(AutoBoneProcessType.PROCESS);
};
const applyProcessing = () => {
startProcess(AutoBoneProcessType.APPLY);
sendRPCPacket(RpcMessage.AutoBoneApplyRequest, new AutoBoneApplyRequestT());
};
useRPCPacket(
@@ -79,35 +88,36 @@ export function useProvideAutobone(): AutoboneContext {
}
if (data.processType) {
if (data.message) {
log(AutoBoneProcessType[data.processType], ': ', data.message);
}
if (data.total > 0 && data.current >= 0) {
setProgress(data.current / data.total);
}
setEta(data.eta);
if (data.completed) {
log('Process ', AutoBoneProcessType[data.processType], ' has completed');
log(`Process ${AutoBoneProcessType[data.processType]} has completed`);
switch (data.processType) {
case AutoBoneProcessType.RECORD:
setHasRecording(data.success);
setHasRecording(
data.success ? ProcessStatus.FULFILLED : ProcessStatus.REJECTED
);
startProcessing();
break;
case AutoBoneProcessType.PROCESS:
setHasCalibration(data.success);
break;
case AutoBoneProcessType.APPLY:
// Update skeleton config when applied
sendRPCPacket(
RpcMessage.SkeletonConfigRequest,
new SkeletonConfigRequestT()
setHasCalibration(
data.success ? ProcessStatus.FULFILLED : ProcessStatus.REJECTED
);
break;
// case AutoBoneProcessType.APPLY:
// // Update skeleton config when applied
// sendRPCPacket(
// RpcMessage.SkeletonConfigRequest,
// new SkeletonConfigRequestT()
// );
// break;
}
}
}
@@ -135,6 +145,7 @@ export function useProvideAutobone(): AutoboneContext {
hasCalibration,
hasRecording,
progress,
eta,
bodyParts,
startProcessing,
startRecording,

714
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,8 +15,6 @@
"prepare": "husky install && npm run update-solarxr"
},
"devDependencies": {
"@typescript-eslint/parser": "^5.59.6",
"husky": "^8.0.2",
"typescript": "^5.0.4"
"husky": "^8.0.3"
}
}

View File

@@ -7,7 +7,6 @@ import dev.slimevr.autobone.errors.*
import dev.slimevr.config.AutoBoneConfig
import dev.slimevr.poseframeformat.PoseFrameIO
import dev.slimevr.poseframeformat.PoseFrames
import dev.slimevr.tracking.processor.BoneType
import dev.slimevr.tracking.processor.HumanPoseManager
import dev.slimevr.tracking.processor.config.SkeletonConfigManager
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
@@ -20,45 +19,43 @@ import io.github.axisangles.ktmath.Vector3
import org.apache.commons.lang3.tuple.Pair
import java.io.File
import java.util.*
import java.util.function.BiConsumer
import java.util.function.Consumer
import java.util.function.Function
class AutoBone(server: VRServer) {
// This is filled by reloadConfigValues()
val offsets = EnumMap<BoneType, Float>(
BoneType::class.java
// This is filled by loadConfigValues()
val offsets = EnumMap<SkeletonConfigOffsets, Float>(
SkeletonConfigOffsets::class.java
)
val adjustOffsets = FastList(
arrayOf(
BoneType.HEAD,
BoneType.NECK,
BoneType.UPPER_CHEST,
BoneType.CHEST,
BoneType.WAIST,
BoneType.HIP, // This now works when using body proportion error! It's not the
SkeletonConfigOffsets.HEAD,
SkeletonConfigOffsets.NECK,
SkeletonConfigOffsets.UPPER_CHEST,
SkeletonConfigOffsets.CHEST,
SkeletonConfigOffsets.WAIST,
SkeletonConfigOffsets.HIP, // This now works when using body proportion error! It's not the
// best still, but it is somewhat functional
BoneType.LEFT_HIP,
BoneType.LEFT_UPPER_LEG,
BoneType.LEFT_LOWER_LEG
SkeletonConfigOffsets.HIPS_WIDTH,
SkeletonConfigOffsets.UPPER_LEG,
SkeletonConfigOffsets.LOWER_LEG
)
)
val heightOffsets = FastList(
arrayOf(
BoneType.NECK,
BoneType.UPPER_CHEST,
BoneType.CHEST,
BoneType.WAIST,
BoneType.HIP,
BoneType.LEFT_UPPER_LEG,
BoneType.RIGHT_UPPER_LEG,
BoneType.LEFT_LOWER_LEG,
BoneType.RIGHT_LOWER_LEG
)
val heightOffsetDefaults = EnumMap<SkeletonConfigOffsets, Float>(
SkeletonConfigOffsets::class.java
)
val legacyConfigs = EnumMap<SkeletonConfigOffsets, Float>(
SkeletonConfigOffsets::class.java
// This is filled by loadConfigValues()
val heightOffsets = FastList(
arrayOf(
SkeletonConfigOffsets.NECK,
SkeletonConfigOffsets.UPPER_CHEST,
SkeletonConfigOffsets.CHEST,
SkeletonConfigOffsets.WAIST,
SkeletonConfigOffsets.HIP,
SkeletonConfigOffsets.UPPER_LEG,
SkeletonConfigOffsets.LOWER_LEG
)
)
private val server: VRServer
@@ -83,34 +80,6 @@ class AutoBone(server: VRServer) {
loadConfigValues()
}
fun computeBoneOffset(
bone: BoneType,
getOffset: Function<SkeletonConfigOffsets, Float>,
): Float {
return when (bone) {
BoneType.HEAD -> getOffset.apply(SkeletonConfigOffsets.HEAD)
BoneType.NECK -> getOffset.apply(SkeletonConfigOffsets.NECK)
BoneType.UPPER_CHEST -> getOffset.apply(SkeletonConfigOffsets.UPPER_CHEST)
BoneType.CHEST -> getOffset.apply(SkeletonConfigOffsets.CHEST)
BoneType.WAIST -> getOffset.apply(SkeletonConfigOffsets.WAIST)
BoneType.HIP -> getOffset.apply(SkeletonConfigOffsets.HIP)
BoneType.LEFT_HIP, BoneType.RIGHT_HIP -> (
getOffset.apply(SkeletonConfigOffsets.HIPS_WIDTH) /
2f
)
BoneType.LEFT_UPPER_LEG, BoneType.RIGHT_UPPER_LEG ->
getOffset
.apply(SkeletonConfigOffsets.UPPER_LEG)
BoneType.LEFT_LOWER_LEG, BoneType.RIGHT_LOWER_LEG ->
getOffset
.apply(SkeletonConfigOffsets.LOWER_LEG)
else -> -1f
}
}
private fun loadConfigValues() {
// Remove all previous values
offsets.clear()
@@ -127,143 +96,83 @@ class AutoBone(server: VRServer) {
}
}
for (bone in adjustOffsets) {
val offset = computeBoneOffset(bone, getOffset)
val offset = getOffset.apply(bone)
if (offset > 0f) {
offsets[bone] = offset
}
}
for (bone in heightOffsets) {
val offset = getOffset.apply(bone)
if (offset > 0f) {
heightOffsetDefaults[bone] = offset
}
}
}
fun getBoneDirection(
skeleton: HumanPoseManager,
node: BoneType,
configOffset: SkeletonConfigOffsets,
rightSide: Boolean,
): Vector3 {
var node = when (node) {
BoneType.LEFT_HIP, BoneType.RIGHT_HIP -> if (rightSide) BoneType.RIGHT_HIP else BoneType.LEFT_HIP
BoneType.LEFT_UPPER_LEG, BoneType.RIGHT_UPPER_LEG ->
if (rightSide) BoneType.RIGHT_UPPER_LEG else BoneType.LEFT_UPPER_LEG
BoneType.LEFT_LOWER_LEG, BoneType.RIGHT_LOWER_LEG ->
if (rightSide) BoneType.RIGHT_LOWER_LEG else BoneType.LEFT_LOWER_LEG
else -> node
// IMPORTANT: This assumption for acquiring BoneType only works if
// SkeletonConfigOffsets is set up to only affect one BoneType, make sure no
// changes to SkeletonConfigOffsets goes against this assumption, please!
val boneType = when (configOffset) {
SkeletonConfigOffsets.HIPS_WIDTH, SkeletonConfigOffsets.SHOULDERS_WIDTH,
SkeletonConfigOffsets.SHOULDERS_DISTANCE, SkeletonConfigOffsets.UPPER_ARM,
SkeletonConfigOffsets.LOWER_ARM, SkeletonConfigOffsets.UPPER_LEG,
SkeletonConfigOffsets.LOWER_LEG, SkeletonConfigOffsets.FOOT_LENGTH,
->
if (rightSide) configOffset.affectedOffsets[1] else configOffset.affectedOffsets[0]
else -> configOffset.affectedOffsets[0]
}
val relevantTransform = skeleton.getTailNodeOfBone(node)
return (relevantTransform.worldTransform.translation - relevantTransform.parent!!.worldTransform.translation).unit()
val relevantTransform = skeleton.getTailNodeOfBone(boneType)
return (
relevantTransform.worldTransform.translation -
relevantTransform.parent!!.worldTransform.translation
).unit()
}
fun getDotProductDiff(
skeleton1: HumanPoseManager,
skeleton2: HumanPoseManager,
node: BoneType,
configOffset: SkeletonConfigOffsets,
rightSide: Boolean,
offset: Vector3,
): Float {
val normalizedOffset = offset.unit()
val dot1 = normalizedOffset.dot(getBoneDirection(skeleton1, node, rightSide))
val dot2 = normalizedOffset.dot(getBoneDirection(skeleton2, node, rightSide))
val dot1 = normalizedOffset.dot(getBoneDirection(skeleton1, configOffset, rightSide))
val dot2 = normalizedOffset.dot(getBoneDirection(skeleton2, configOffset, rightSide))
return dot2 - dot1
}
fun applyConfig(
configConsumer: BiConsumer<SkeletonConfigOffsets, Float>,
offsets: Map<BoneType, Float> = this.offsets,
): Boolean {
return try {
val headOffset = offsets[BoneType.HEAD]
if (headOffset != null) {
configConsumer.accept(SkeletonConfigOffsets.HEAD, headOffset)
}
val neckOffset = offsets[BoneType.NECK]
if (neckOffset != null) {
configConsumer.accept(SkeletonConfigOffsets.NECK, neckOffset)
}
val upperChestOffset = offsets[BoneType.UPPER_CHEST]
val chestOffset = offsets[BoneType.CHEST]
val waistOffset = offsets[BoneType.WAIST]
val hipOffset = offsets[BoneType.HIP]
if (upperChestOffset != null) {
configConsumer
.accept(SkeletonConfigOffsets.UPPER_CHEST, upperChestOffset)
}
if (chestOffset != null) {
configConsumer
.accept(SkeletonConfigOffsets.CHEST, chestOffset)
}
if (waistOffset != null) {
configConsumer.accept(SkeletonConfigOffsets.WAIST, waistOffset)
}
if (hipOffset != null) {
configConsumer.accept(SkeletonConfigOffsets.HIP, hipOffset)
}
var hipWidthOffset = offsets[BoneType.LEFT_HIP]
if (hipWidthOffset == null) {
hipWidthOffset = offsets[BoneType.RIGHT_HIP]
}
if (hipWidthOffset != null) {
configConsumer
.accept(SkeletonConfigOffsets.HIPS_WIDTH, hipWidthOffset * 2f)
}
var upperLegOffset = offsets[BoneType.LEFT_UPPER_LEG]
if (upperLegOffset == null) {
upperLegOffset = offsets[BoneType.RIGHT_UPPER_LEG]
}
var lowerLegOffset = offsets[BoneType.LEFT_LOWER_LEG]
if (lowerLegOffset == null) {
lowerLegOffset = offsets[BoneType.RIGHT_LOWER_LEG]
}
if (upperLegOffset != null) {
configConsumer
.accept(SkeletonConfigOffsets.UPPER_LEG, upperLegOffset)
}
if (lowerLegOffset != null) {
configConsumer.accept(SkeletonConfigOffsets.LOWER_LEG, lowerLegOffset)
}
true
} catch (e: Exception) {
false
}
}
fun applyConfig(
skeletonConfig: MutableMap<SkeletonConfigOffsets, Float>,
offsets: Map<BoneType, Float> = this.offsets,
): Boolean {
return applyConfig({ key: SkeletonConfigOffsets, value: Float -> skeletonConfig[key] = value }, offsets)
}
fun applyConfig(
humanPoseManager: HumanPoseManager,
offsets: Map<BoneType, Float> = this.offsets,
): Boolean {
return applyConfig({ key: SkeletonConfigOffsets?, newLength: Float? ->
humanPoseManager.setOffset(
key,
newLength
)
}, offsets)
offsets: Map<SkeletonConfigOffsets, Float> = this.offsets,
) {
for ((offset, value) in offsets) {
humanPoseManager.setOffset(offset, value)
}
}
@JvmOverloads
fun applyAndSaveConfig(humanPoseManager: HumanPoseManager? = this.server.humanPoseManager): Boolean {
if (humanPoseManager == null) return false
if (!applyConfig(humanPoseManager)) return false
applyConfig(humanPoseManager)
humanPoseManager.saveConfig()
server.configManager.saveConfig()
LogManager.info("[AutoBone] Configured skeleton bone lengths")
return true
}
fun getConfig(config: BoneType): Float? {
return offsets[config]
}
fun <T> sumSelectConfigs(
selection: List<T>,
configs: Function<T, Float?>,
configs: Map<T, Float>,
configsAlt: Map<T, Float>? = null,
): Float {
var sum = 0f
for (config in selection) {
val length = configs.apply(config)
val length = configs[config] ?: configsAlt?.get(config)
if (length != null) {
sum += length
}
@@ -271,27 +180,17 @@ class AutoBone(server: VRServer) {
return sum
}
fun <T> sumSelectConfigs(
selection: List<T>,
configs: Map<T, Float>,
): Float {
return sumSelectConfigs(selection) { key: T -> configs[key] }
fun calcHeight(): Float {
return sumSelectConfigs(heightOffsets, offsets, heightOffsetDefaults)
}
fun sumSelectConfigs(
selection: List<SkeletonConfigOffsets>,
humanPoseManager: HumanPoseManager,
): Float {
return sumSelectConfigs(selection) { key: SkeletonConfigOffsets? -> humanPoseManager.getOffset(key) }
}
fun getLengthSum(configs: Map<BoneType, Float>): Float {
fun getLengthSum(configs: Map<SkeletonConfigOffsets, Float>): Float {
return getLengthSum(configs, null)
}
fun getLengthSum(
configs: Map<BoneType, Float>,
configsAlt: Map<BoneType, Float>?,
configs: Map<SkeletonConfigOffsets, Float>,
configsAlt: Map<SkeletonConfigOffsets, Float>?,
): Float {
var length = 0f
if (configsAlt != null) {
@@ -327,7 +226,7 @@ class AutoBone(server: VRServer) {
// Otherwise if there is no skeleton available, attempt to get the
// max HMD height from the recording
val hmdHeight = frames.maxHmdHeight
if (hmdHeight <= 0.50f) {
if (hmdHeight <= 0.4f) {
LogManager
.warning(
"[AutoBone] Max headset height detected (Value seems too low, did you not stand up straight while measuring?): $hmdHeight"
@@ -346,7 +245,7 @@ class AutoBone(server: VRServer) {
fun processFrames(
frames: PoseFrames,
config: AutoBoneConfig = globalConfig,
epochCallback: Consumer<Epoch?>? = null,
epochCallback: Consumer<Epoch>? = null,
): AutoBoneResults {
// Load current values for adjustable configs
loadConfigValues()
@@ -370,7 +269,6 @@ class AutoBone(server: VRServer) {
targetHmdHeight = targetHmdHeight,
targetFullHeight = targetFullHeight,
frames = frames,
intermediateOffsets = EnumMap(offsets),
epochCallback = epochCallback,
serverConfig = server.configManager
)
@@ -387,7 +285,7 @@ class AutoBone(server: VRServer) {
internalEpoch(trainingStep)
}
val finalHeight = sumSelectConfigs(heightOffsets, offsets)
val finalHeight = calcHeight()
LogManager
.info(
"[AutoBone] Target height: ${trainingStep.targetHmdHeight}, New height: $finalHeight"
@@ -397,7 +295,7 @@ class AutoBone(server: VRServer) {
finalHeight,
trainingStep.targetHmdHeight,
trainingStep.errorStats,
legacyConfigs
offsets
)
}
@@ -475,20 +373,16 @@ class AutoBone(server: VRServer) {
)
}
// Convert current adjusted config to the legacy config format for the epoch
// callback, then call it
applyConfig(legacyConfigs)
trainingStep.epochCallback?.accept(Epoch(epoch + 1, config.numEpochs, errorStats, legacyConfigs))
trainingStep.epochCallback?.accept(Epoch(epoch + 1, config.numEpochs, errorStats, offsets))
}
private fun internalIter(trainingStep: AutoBoneStep) {
// Pull frequently used variables out of trainingStep to reduce call length
val skeleton1 = trainingStep.skeleton1
val skeleton2 = trainingStep.skeleton2
val intermediateOffsets = trainingStep.intermediateOffsets
val totalLength = getLengthSum(offsets)
val curHeight = sumSelectConfigs(heightOffsets, offsets)
val curHeight = calcHeight()
trainingStep.currentHmdHeight = curHeight
val errorDeriv = getErrorDeriv(trainingStep)
@@ -528,13 +422,10 @@ class AutoBone(server: VRServer) {
.getComputedTracker(TrackerRole.RIGHT_FOOT).position -
skeleton1.getComputedTracker(TrackerRole.RIGHT_FOOT).position
// Load the current offsets into the working offsets holder
intermediateOffsets.putAll(offsets)
for (entry in offsets.entries) {
// Skip adjustment if the epoch is before starting (for
// logging only)
if (trainingStep.curEpoch < 0) {
// logging only) or if there are no BoneTypes for this value
if (trainingStep.curEpoch < 0 || entry.key.affectedOffsets.isEmpty()) {
break
}
val originalLength = entry.value
@@ -571,9 +462,8 @@ class AutoBone(server: VRServer) {
}
// Apply new offset length
intermediateOffsets[entry.key] = newLength
applyConfig(skeleton1, intermediateOffsets)
applyConfig(skeleton2, intermediateOffsets)
skeleton1.setOffset(entry.key, newLength)
skeleton2.setOffset(entry.key, newLength)
// Update the skeleton poses for the new offset length
skeleton1.update()
@@ -587,9 +477,8 @@ class AutoBone(server: VRServer) {
// Reset the length to minimize bias in other variables,
// it's applied later
intermediateOffsets[entry.key] = originalLength
applyConfig(skeleton1, intermediateOffsets)
applyConfig(skeleton2, intermediateOffsets)
skeleton1.setOffset(entry.key, originalLength)
skeleton2.setOffset(entry.key, originalLength)
}
if (trainingStep.config.scaleEachStep) {
@@ -599,12 +488,15 @@ class AutoBone(server: VRServer) {
}
private fun scaleToTargetHeight(trainingStep: AutoBoneStep) {
val stepHeight = sumSelectConfigs(heightOffsets, offsets)
// Recalculate the height and update it in the AutoBoneStep
val stepHeight = calcHeight()
trainingStep.currentHmdHeight = stepHeight
if (stepHeight > 0f) {
val stepHeightDiff = trainingStep.targetHmdHeight - stepHeight
for (entry in offsets.entries) {
// Only height variables
if (entry.key == BoneType.NECK ||
if (entry.key == SkeletonConfigOffsets.NECK ||
!heightOffsets.contains(entry.key)
) {
continue
@@ -671,12 +563,12 @@ class AutoBone(server: VRServer) {
val lengthsString: String
get() {
val configInfo = StringBuilder()
offsets.forEach { (key: BoneType, value: Float) ->
offsets.forEach { (key: SkeletonConfigOffsets, value: Float) ->
if (configInfo.isNotEmpty()) {
configInfo.append(", ")
}
configInfo
.append(key.toString())
.append(key.configKey)
.append(": ")
.append(StringUtils.prettyNumber(value * 100f, 2))
}

View File

@@ -3,7 +3,6 @@ package dev.slimevr.autobone
import dev.slimevr.VRServer
import dev.slimevr.autobone.AutoBone.AutoBoneResults
import dev.slimevr.autobone.AutoBone.Companion.loadDir
import dev.slimevr.autobone.AutoBone.Epoch
import dev.slimevr.autobone.errors.AutoBoneException
import dev.slimevr.poseframeformat.PoseFrames
import dev.slimevr.poseframeformat.PoseRecorder
@@ -16,7 +15,6 @@ import io.eiren.util.logging.LogManager
import org.apache.commons.lang3.tuple.Pair
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Consumer
import kotlin.concurrent.thread
import kotlin.concurrent.withLock
@@ -38,44 +36,29 @@ class AutoBoneHandler(private val server: VRServer) {
}
fun removeListener(listener: AutoBoneListener) {
listeners.removeIf { l: AutoBoneListener -> listener === l }
listeners.removeIf { listener == it }
}
private fun announceProcessStatus(
processType: AutoBoneProcessType,
message: String?,
current: Long,
total: Long,
completed: Boolean,
success: Boolean,
) {
listeners
.forEach(
Consumer { listener: AutoBoneListener ->
listener
.onAutoBoneProcessStatus(
processType,
message,
current,
total,
completed,
success
)
}
)
}
private fun announceProcessStatus(
processType: AutoBoneProcessType,
message: String,
message: String? = null,
current: Long = -1L,
total: Long = -1L,
eta: Float = -1f,
completed: Boolean = false,
success: Boolean = true,
) {
announceProcessStatus(processType, message, 0, 0, completed, success)
}
private fun announceProcessStatus(processType: AutoBoneProcessType, current: Long, total: Long) {
announceProcessStatus(processType, null, current, total, completed = false, success = true)
listeners.forEach {
it.onAutoBoneProcessStatus(
processType,
message,
current,
total,
eta,
completed,
success
)
}
}
val lengthsString: String
@@ -84,17 +67,16 @@ class AutoBoneHandler(private val server: VRServer) {
@Throws(AutoBoneException::class)
private fun processFrames(frames: PoseFrames): AutoBoneResults {
return autoBone
.processFrames(
frames
) { epoch: Epoch? -> listeners.forEach(Consumer { listener: AutoBoneListener -> listener.onAutoBoneEpoch(epoch!!) }) }
.processFrames(frames) { epoch ->
listeners.forEach { listener -> listener.onAutoBoneEpoch(epoch) }
}
}
fun startProcessByType(processType: AutoBoneProcessType): Boolean {
fun startProcessByType(processType: AutoBoneProcessType?): Boolean {
when (processType) {
AutoBoneProcessType.RECORD -> startRecording()
AutoBoneProcessType.SAVE -> saveRecording()
AutoBoneProcessType.PROCESS -> processRecording()
AutoBoneProcessType.APPLY -> applyValues()
else -> {
return false
}
@@ -117,9 +99,12 @@ class AutoBoneHandler(private val server: VRServer) {
if (poseRecorder.isReadyToRecord) {
announceProcessStatus(AutoBoneProcessType.RECORD, "Recording...")
// 1000 samples at 20 ms per sample is 20 seconds
// ex. 1000 samples at 20 ms per sample is 20 seconds
val sampleCount = autoBone.globalConfig.sampleCount
val sampleRate = autoBone.globalConfig.sampleRateMs
// Calculate total time in seconds
val totalTime: Float = (sampleCount * sampleRate) / 1000f
val framesFuture = poseRecorder
.startFrameRecording(
sampleCount,
@@ -127,9 +112,9 @@ class AutoBoneHandler(private val server: VRServer) {
) { progress: RecordingProgress ->
announceProcessStatus(
AutoBoneProcessType.RECORD,
progress.frame.toLong(),
progress.totalFrames
.toLong()
current = progress.frame.toLong(),
total = progress.totalFrames.toLong(),
eta = totalTime - (progress.frame * totalTime / progress.totalFrames)
)
}
val frames = framesFuture.get()
@@ -145,7 +130,7 @@ class AutoBoneHandler(private val server: VRServer) {
)
autoBone.saveRecording(frames)
}
listeners.forEach(Consumer { listener: AutoBoneListener -> listener.onAutoBoneRecordingEnd(frames) })
listeners.forEach { listener: AutoBoneListener -> listener.onAutoBoneRecordingEnd(frames) }
announceProcessStatus(
AutoBoneProcessType.RECORD,
"Done recording!",
@@ -175,6 +160,18 @@ class AutoBoneHandler(private val server: VRServer) {
}
}
fun stopRecording() {
if (poseRecorder.isRecording) {
poseRecorder.stopFrameRecording()
}
}
fun cancelRecording() {
if (poseRecorder.isRecording) {
poseRecorder.cancelFrameRecording()
}
}
fun saveRecording() {
saveRecordingLock.withLock {
// Prevent running multiple times
@@ -373,7 +370,7 @@ class AutoBoneHandler(private val server: VRServer) {
")"
)
// #endregion
listeners.forEach(Consumer { listener: AutoBoneListener -> listener.onAutoBoneEnd(autoBone.legacyConfigs) })
listeners.forEach { listener: AutoBoneListener -> listener.onAutoBoneEnd(autoBone.offsets) }
announceProcessStatus(
AutoBoneProcessType.PROCESS,
"Done processing!",
@@ -395,11 +392,5 @@ class AutoBoneHandler(private val server: VRServer) {
fun applyValues() {
autoBone.applyAndSaveConfig()
announceProcessStatus(
AutoBoneProcessType.APPLY,
"Adjusted values applied!",
completed = true,
success = true
)
}
}

View File

@@ -3,7 +3,7 @@ package dev.slimevr.autobone
import dev.slimevr.autobone.AutoBone.Epoch
import dev.slimevr.poseframeformat.PoseFrames
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
import java.util.EnumMap
import java.util.*
interface AutoBoneListener {
fun onAutoBoneProcessStatus(
@@ -11,6 +11,7 @@ interface AutoBoneListener {
message: String?,
current: Long,
total: Long,
eta: Float,
completed: Boolean,
success: Boolean,
)

View File

@@ -1,7 +1,7 @@
package dev.slimevr.autobone
enum class AutoBoneProcessType(val id: Int) {
NONE(0), RECORD(1), SAVE(2), PROCESS(3), APPLY(4);
NONE(0), RECORD(1), SAVE(2), PROCESS(3);
companion object {
fun getById(id: Int): AutoBoneProcessType? = byId[id]

View File

@@ -4,9 +4,7 @@ import dev.slimevr.config.AutoBoneConfig
import dev.slimevr.config.ConfigManager
import dev.slimevr.poseframeformat.PoseFrames
import dev.slimevr.poseframeformat.player.TrackerFramesPlayer
import dev.slimevr.tracking.processor.BoneType
import dev.slimevr.tracking.processor.HumanPoseManager
import java.util.*
import java.util.function.Consumer
class AutoBoneStep(
@@ -14,8 +12,7 @@ class AutoBoneStep(
val targetHmdHeight: Float,
val targetFullHeight: Float,
val frames: PoseFrames,
val intermediateOffsets: EnumMap<BoneType, Float>,
val epochCallback: Consumer<AutoBone.Epoch?>?,
val epochCallback: Consumer<AutoBone.Epoch>?,
serverConfig: ConfigManager,
var curEpoch: Int = 0,
var curAdjustRate: Float = 0f,
@@ -24,6 +21,8 @@ class AutoBoneStep(
var currentHmdHeight: Float = 0f,
) {
val eyeHeightToHeightRatio: Float = targetHmdHeight / targetFullHeight
val maxFrameCount = frames.maxFrameCount
val framePlayer1 = TrackerFramesPlayer(frames)

View File

@@ -13,12 +13,11 @@ class BodyProportionError : IAutoBoneError {
override fun getStepError(trainingStep: AutoBoneStep): Float {
return getBodyProportionError(
trainingStep.skeleton1,
trainingStep.currentHmdHeight
trainingStep.currentHmdHeight / trainingStep.eyeHeightToHeightRatio
)
}
fun getBodyProportionError(humanPoseManager: HumanPoseManager, height: Float): Float {
val fullHeight = height / eyeHeightToHeightRatio
fun getBodyProportionError(humanPoseManager: HumanPoseManager, fullHeight: Float): Float {
var sum = 0f
for (limiter in proportionLimits) {
sum += FastMath.abs(limiter.getProportionError(humanPoseManager, fullHeight))

View File

@@ -20,8 +20,8 @@ class AutoBoneConfig {
var targetFullHeight = -1f
var randomizeFrameOrder = true
var scaleEachStep = true
var sampleCount = 1000
var sampleRateMs: Long = 20
var sampleCount = 1500
var sampleRateMs = 20L
var saveRecordings = false
var useSkeletonHeight = false
var randSeed = 4L

View File

@@ -225,6 +225,16 @@ public class CurrentVRConfigConverter implements VersionedModelConverter {
}
}
if (version < 9) {
// Change default AutoBone recording length from 20 to 30
// seconds
ObjectNode autoBoneNode = (ObjectNode) modelData.get("autoBone");
if (autoBoneNode != null) {
JsonNode sampleCountNode = autoBoneNode.get("sampleCount");
if (sampleCountNode != null && sampleCountNode.intValue() == 1000) {
autoBoneNode.set("sampleCount", new IntNode(1500));
}
}
// split chest into 2 offsets
ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton");
if (skeletonNode != null) {

View File

@@ -4,6 +4,7 @@ import com.google.flatbuffers.FlatBufferBuilder;
import dev.slimevr.protocol.GenericConnection;
import dev.slimevr.protocol.ProtocolAPI;
import dev.slimevr.protocol.ProtocolHandler;
import dev.slimevr.tracking.trackers.Tracker;
import io.eiren.util.logging.LogManager;
import solarxr_protocol.MessageBundle;
import solarxr_protocol.data_feed.*;
@@ -77,6 +78,7 @@ public class DataFeedHandler extends ProtocolHandler<DataFeedMessageHeader> {
this.api.server.deviceManager
.getDevices()
);
// Synthetic tracker is computed tracker apparently
int trackersOffset = DataFeedBuilder
.createSyntheticTrackersData(
fbb,
@@ -84,7 +86,7 @@ public class DataFeedHandler extends ProtocolHandler<DataFeedMessageHeader> {
this.api.server
.getAllTrackers()
.stream()
.filter(tracker -> tracker.getDevice() == null)
.filter(Tracker::isComputed)
.collect(Collectors.toList())
);

View File

@@ -1,14 +1,12 @@
package dev.slimevr.protocol.rpc;
import com.google.flatbuffers.FlatBufferBuilder;
import dev.slimevr.autobone.AutoBone.Epoch;
import dev.slimevr.autobone.AutoBoneListener;
import dev.slimevr.autobone.AutoBoneProcessType;
import dev.slimevr.autobone.errors.BodyProportionError;
import dev.slimevr.config.OverlayConfig;
import dev.slimevr.poseframeformat.PoseFrames;
import dev.slimevr.protocol.GenericConnection;
import dev.slimevr.protocol.ProtocolAPI;
import dev.slimevr.protocol.ProtocolHandler;
import dev.slimevr.protocol.rpc.autobone.RPCAutoBoneHandler;
import dev.slimevr.protocol.rpc.reset.RPCResetHandler;
import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler;
import dev.slimevr.protocol.rpc.serial.RPCSerialHandler;
@@ -26,13 +24,10 @@ import solarxr_protocol.MessageBundle;
import solarxr_protocol.datatypes.TransactionId;
import solarxr_protocol.rpc.*;
import java.util.EnumMap;
import java.util.Map.Entry;
import java.util.function.BiConsumer;
public class RPCHandler extends ProtocolHandler<RpcMessageHeader>
implements AutoBoneListener {
public class RPCHandler extends ProtocolHandler<RpcMessageHeader> {
private static final String resetSourceName = "WebSocketAPI";
@@ -50,6 +45,7 @@ public class RPCHandler extends ProtocolHandler<RpcMessageHeader>
new RPCSettingsHandler(this, api);
new RPCTapSetupHandler(this, api);
new RPCStatusHandler(this, api);
new RPCAutoBoneHandler(this, api);
registerPacketListener(RpcMessage.ResetRequest, this::onResetRequest);
registerPacketListener(
@@ -72,8 +68,6 @@ public class RPCHandler extends ProtocolHandler<RpcMessageHeader>
this::onChangeSkeletonConfigRequest
);
registerPacketListener(RpcMessage.AutoBoneProcessRequest, this::onAutoBoneProcessRequest);
registerPacketListener(
RpcMessage.OverlayDisplayModeChangeRequest,
this::onOverlayDisplayModeChangeRequest
@@ -93,7 +87,7 @@ public class RPCHandler extends ProtocolHandler<RpcMessageHeader>
registerPacketListener(RpcMessage.SetPauseTrackingRequest, this::onSetPauseTrackingRequest);
this.api.server.autoBoneHandler.addListener(this);
registerPacketListener(RpcMessage.HeightRequest, this::onHeightRequest);
}
private void onServerInfosRequest(
@@ -360,122 +354,6 @@ public class RPCHandler extends ProtocolHandler<RpcMessageHeader>
return RpcMessage.names.length;
}
public void onAutoBoneProcessRequest(GenericConnection conn, RpcMessageHeader messageHeader) {
AutoBoneProcessRequest req = (AutoBoneProcessRequest) messageHeader
.message(new AutoBoneProcessRequest());
if (req == null || conn.getContext().useAutoBone())
return;
conn.getContext().setUseAutoBone(true);
this.api.server.autoBoneHandler
.startProcessByType(AutoBoneProcessType.Companion.getById(req.processType()));
}
@Override
public void onAutoBoneProcessStatus(
AutoBoneProcessType processType,
String message,
long current,
long total,
boolean completed,
boolean success
) {
this.api
.getAPIServers()
.forEach(
(server) -> server
.getAPIConnections()
.filter(conn -> conn.getContext().useAutoBone())
.forEach((conn) -> {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
Integer messageOffset = message != null ? fbb.createString(message) : null;
AutoBoneProcessStatusResponse.startAutoBoneProcessStatusResponse(fbb);
AutoBoneProcessStatusResponse.addProcessType(fbb, processType.getId());
if (messageOffset != null)
AutoBoneProcessStatusResponse.addMessage(fbb, messageOffset);
if (total > 0 && current >= 0) {
AutoBoneProcessStatusResponse.addCurrent(fbb, current);
AutoBoneProcessStatusResponse.addTotal(fbb, total);
}
AutoBoneProcessStatusResponse.addCompleted(fbb, completed);
AutoBoneProcessStatusResponse.addSuccess(fbb, success);
int update = AutoBoneProcessStatusResponse
.endAutoBoneProcessStatusResponse(fbb);
int outbound = this
.createRPCMessage(
fbb,
RpcMessage.AutoBoneProcessStatusResponse,
update
);
fbb.finish(outbound);
conn.send(fbb.dataBuffer());
if (completed) {
conn.getContext().setUseAutoBone(false);
}
})
);
}
@Override
public void onAutoBoneRecordingEnd(PoseFrames recording) {
// Do nothing, this is broadcasted by "onAutoBoneProcessStatus" uwu
}
@Override
public void onAutoBoneEpoch(Epoch epoch) {
this.api
.getAPIServers()
.forEach(
(server) -> server
.getAPIConnections()
.filter(conn -> conn.getContext().useAutoBone())
.forEach((conn) -> {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
int[] skeletonPartOffsets = new int[epoch.getConfigValues().size()];
int i = 0;
for (
Entry<SkeletonConfigOffsets, Float> skeletonConfig : epoch
.getConfigValues()
.entrySet()
) {
skeletonPartOffsets[i++] = SkeletonPart
.createSkeletonPart(
fbb,
skeletonConfig.getKey().id,
skeletonConfig.getValue()
);
}
int skeletonPartsOffset = AutoBoneEpochResponse
.createAdjustedSkeletonPartsVector(fbb, skeletonPartOffsets);
int update = AutoBoneEpochResponse
.createAutoBoneEpochResponse(
fbb,
epoch.getEpoch(),
epoch.getTotalEpochs(),
epoch.getEpochError().getMean(),
skeletonPartsOffset
);
int outbound = this
.createRPCMessage(fbb, RpcMessage.AutoBoneEpochResponse, update);
fbb.finish(outbound);
conn.send(fbb.dataBuffer());
})
);
}
@Override
public void onAutoBoneEnd(EnumMap<SkeletonConfigOffsets, Float> configValues) {
// Do nothing, the last epoch from "onAutoBoneEpoch" should be all
// that's needed
}
public void onStatusSystemRequest(GenericConnection conn, RpcMessageHeader messageHeader) {
StatusSystemRequest req = (StatusSystemRequest) messageHeader
.message(new StatusSystemRequest());
@@ -503,4 +381,18 @@ public class RPCHandler extends ProtocolHandler<RpcMessageHeader>
this.api.server.humanPoseManager.setPauseTracking(req.pauseTracking());
}
public void onHeightRequest(GenericConnection conn, RpcMessageHeader messageHeader) {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
float hmdHeight = this.api.server.humanPoseManager.getHmdHeight();
int response = HeightResponse
.createHeightResponse(
fbb,
hmdHeight,
hmdHeight / BodyProportionError.eyeHeightToHeightRatio
);
fbb.finish(createRPCMessage(fbb, RpcMessage.HeightResponse, response));
conn.send(fbb.dataBuffer());
}
}

View File

@@ -0,0 +1,181 @@
package dev.slimevr.protocol.rpc.autobone
import com.google.flatbuffers.FlatBufferBuilder
import dev.slimevr.autobone.AutoBone.Epoch
import dev.slimevr.autobone.AutoBoneListener
import dev.slimevr.autobone.AutoBoneProcessType
import dev.slimevr.autobone.AutoBoneProcessType.Companion.getById
import dev.slimevr.poseframeformat.PoseFrames
import dev.slimevr.protocol.GenericConnection
import dev.slimevr.protocol.ProtocolAPI
import dev.slimevr.protocol.rpc.RPCBuilder
import dev.slimevr.protocol.rpc.RPCHandler
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets
import solarxr_protocol.rpc.AutoBoneEpochResponse
import solarxr_protocol.rpc.AutoBoneProcessRequest
import solarxr_protocol.rpc.AutoBoneProcessStatusResponse
import solarxr_protocol.rpc.RpcMessage
import solarxr_protocol.rpc.RpcMessageHeader
import solarxr_protocol.rpc.SkeletonPart
import java.util.*
class RPCAutoBoneHandler(
private val rpcHandler: RPCHandler,
val api: ProtocolAPI,
) : AutoBoneListener {
init {
rpcHandler.registerPacketListener(
RpcMessage.AutoBoneProcessRequest,
::onAutoBoneProcessRequest
)
rpcHandler.registerPacketListener(
RpcMessage.AutoBoneApplyRequest,
::onAutoBoneApplyRequest
)
rpcHandler.registerPacketListener(
RpcMessage.AutoBoneStopRecordingRequest,
::onAutoBoneStopRecordingRequest
)
rpcHandler.registerPacketListener(
RpcMessage.AutoBoneCancelRecordingRequest,
::onAutoBoneCancelRecordingRequest
)
this.api.server.autoBoneHandler.addListener(this)
}
fun onAutoBoneProcessRequest(
conn: GenericConnection,
messageHeader: RpcMessageHeader,
) {
val req = messageHeader
.message(AutoBoneProcessRequest()) as AutoBoneProcessRequest
if (conn.context.useAutoBone()) return
conn.context.setUseAutoBone(true)
api.server
.autoBoneHandler
.startProcessByType(getById(req.processType()))
}
override fun onAutoBoneProcessStatus(
processType: AutoBoneProcessType,
message: String?,
current: Long,
total: Long,
eta: Float,
completed: Boolean,
success: Boolean,
) {
forAllListeners { conn ->
if (!conn.context.useAutoBone()) {
return@forAllListeners
}
val fbb = FlatBufferBuilder(32)
AutoBoneProcessStatusResponse.startAutoBoneProcessStatusResponse(fbb)
AutoBoneProcessStatusResponse.addProcessType(
fbb,
processType.id
)
AutoBoneProcessStatusResponse.addCurrent(fbb, current)
AutoBoneProcessStatusResponse.addTotal(fbb, total)
AutoBoneProcessStatusResponse.addEta(fbb, eta)
AutoBoneProcessStatusResponse.addCompleted(fbb, completed)
AutoBoneProcessStatusResponse.addSuccess(fbb, success)
val update = AutoBoneProcessStatusResponse
.endAutoBoneProcessStatusResponse(fbb)
val outbound: Int = rpcHandler.createRPCMessage(
fbb,
RpcMessage.AutoBoneProcessStatusResponse,
update
)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
if (completed) {
conn.context.setUseAutoBone(false)
}
}
}
override fun onAutoBoneRecordingEnd(recording: PoseFrames) {
// Do nothing, this is broadcasted by "onAutoBoneProcessStatus" uwu
}
override fun onAutoBoneEpoch(epoch: Epoch) {
forAllListeners { conn ->
if (!conn.context.useAutoBone()) {
return@forAllListeners
}
val fbb = FlatBufferBuilder(32)
val skeletonPartsOffset = AutoBoneEpochResponse
.createAdjustedSkeletonPartsVector(
fbb,
epoch.configValues.map { (key, value) ->
SkeletonPart.createSkeletonPart(fbb, key.id, value)
}.toIntArray()
)
val update = AutoBoneEpochResponse
.createAutoBoneEpochResponse(
fbb,
epoch.epoch.toLong(),
epoch.totalEpochs.toLong(),
epoch.epochError.mean,
skeletonPartsOffset
)
val outbound: Int = rpcHandler.createRPCMessage(
fbb,
RpcMessage.AutoBoneEpochResponse,
update
)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
}
override fun onAutoBoneEnd(configValues: EnumMap<SkeletonConfigOffsets, Float>) {
// Do nothing, the last epoch from "onAutoBoneEpoch" should be all
// that's needed
}
private fun onAutoBoneApplyRequest(
conn: GenericConnection,
messageHeader: RpcMessageHeader,
) {
api.server.autoBoneHandler.applyValues()
// Send the new body proportions, this is to update the listener's state
val fbb = FlatBufferBuilder(300)
val outbound = rpcHandler.createRPCMessage(
fbb,
RpcMessage.SkeletonConfigResponse,
RPCBuilder.createSkeletonConfig(fbb, api.server.humanPoseManager)
)
fbb.finish(outbound)
conn.send(fbb.dataBuffer())
}
private fun onAutoBoneStopRecordingRequest(
conn: GenericConnection,
messageHeader: RpcMessageHeader,
) {
api.server.autoBoneHandler.stopRecording()
}
private fun onAutoBoneCancelRecordingRequest(
conn: GenericConnection,
messageHeader: RpcMessageHeader,
) {
api.server.autoBoneHandler.cancelRecording()
}
private fun forAllListeners(action: (GenericConnection) -> Unit) {
api.apiServers.forEach {
it.apiConnections.forEach(action)
}
}
}

View File

@@ -204,4 +204,126 @@ public class RPCSettingsBuilder {
);
return ModelSettings.createModelSettings(fbb, togglesOffset, ratiosOffset, legTweaksOffset);
}
public static int createAutoBoneSettings(FlatBufferBuilder fbb, AutoBoneConfig autoBoneConfig) {
return AutoBoneSettings
.createAutoBoneSettings(
fbb,
autoBoneConfig.getCursorIncrement(),
autoBoneConfig.getMinDataDistance(),
autoBoneConfig.getMaxDataDistance(),
autoBoneConfig.getNumEpochs(),
autoBoneConfig.getPrintEveryNumEpochs(),
autoBoneConfig.getInitialAdjustRate(),
autoBoneConfig.getAdjustRateDecay(),
autoBoneConfig.getSlideErrorFactor(),
autoBoneConfig.getOffsetSlideErrorFactor(),
autoBoneConfig.getFootHeightOffsetErrorFactor(),
autoBoneConfig.getBodyProportionErrorFactor(),
autoBoneConfig.getHeightErrorFactor(),
autoBoneConfig.getPositionErrorFactor(),
autoBoneConfig.getPositionOffsetErrorFactor(),
autoBoneConfig.getCalcInitError(),
autoBoneConfig.getTargetHmdHeight(),
autoBoneConfig.getTargetFullHeight(),
autoBoneConfig.getRandomizeFrameOrder(),
autoBoneConfig.getScaleEachStep(),
autoBoneConfig.getSampleCount(),
autoBoneConfig.getSampleRateMs(),
autoBoneConfig.getSaveRecordings(),
autoBoneConfig.getUseSkeletonHeight(),
autoBoneConfig.getRandSeed()
);
}
/**
* Writes values from AutoBoneSettings to an AutoBoneConfig.
*
* @param autoBoneSettings The settings to read from.
* @param autoBoneConfig The config to write to.
* @return The autoBoneConfig parameter.
*/
public static AutoBoneConfig readAutoBoneSettings(
AutoBoneSettings autoBoneSettings,
AutoBoneConfig autoBoneConfig
) {
if (autoBoneSettings.hasCursorIncrement()) {
autoBoneConfig.setCursorIncrement(autoBoneSettings.cursorIncrement());
}
if (autoBoneSettings.hasMinDataDistance()) {
autoBoneConfig.setMinDataDistance(autoBoneSettings.minDataDistance());
}
if (autoBoneSettings.hasMaxDataDistance()) {
autoBoneConfig.setMaxDataDistance(autoBoneSettings.maxDataDistance());
}
if (autoBoneSettings.hasNumEpochs()) {
autoBoneConfig.setNumEpochs(autoBoneSettings.numEpochs());
}
if (autoBoneSettings.hasPrintEveryNumEpochs()) {
autoBoneConfig.setPrintEveryNumEpochs(autoBoneSettings.printEveryNumEpochs());
}
if (autoBoneSettings.hasInitialAdjustRate()) {
autoBoneConfig.setInitialAdjustRate(autoBoneSettings.initialAdjustRate());
}
if (autoBoneSettings.hasAdjustRateDecay()) {
autoBoneConfig.setAdjustRateDecay(autoBoneSettings.adjustRateDecay());
}
if (autoBoneSettings.hasSlideErrorFactor()) {
autoBoneConfig.setSlideErrorFactor(autoBoneSettings.slideErrorFactor());
}
if (autoBoneSettings.hasOffsetSlideErrorFactor()) {
autoBoneConfig.setOffsetSlideErrorFactor(autoBoneSettings.offsetSlideErrorFactor());
}
if (autoBoneSettings.hasFootHeightOffsetErrorFactor()) {
autoBoneConfig
.setFootHeightOffsetErrorFactor(autoBoneSettings.footHeightOffsetErrorFactor());
}
if (autoBoneSettings.hasBodyProportionErrorFactor()) {
autoBoneConfig
.setBodyProportionErrorFactor(autoBoneSettings.bodyProportionErrorFactor());
}
if (autoBoneSettings.hasHeightErrorFactor()) {
autoBoneConfig.setHeightErrorFactor(autoBoneSettings.heightErrorFactor());
}
if (autoBoneSettings.hasPositionErrorFactor()) {
autoBoneConfig.setPositionErrorFactor(autoBoneSettings.positionErrorFactor());
}
if (autoBoneSettings.hasPositionOffsetErrorFactor()) {
autoBoneConfig
.setPositionOffsetErrorFactor(autoBoneSettings.positionOffsetErrorFactor());
}
if (autoBoneSettings.hasCalcInitError()) {
autoBoneConfig.setCalcInitError(autoBoneSettings.calcInitError());
}
if (autoBoneSettings.hasTargetHmdHeight()) {
autoBoneConfig.setTargetHmdHeight(autoBoneSettings.targetHmdHeight());
}
if (autoBoneSettings.hasTargetFullHeight()) {
autoBoneConfig.setTargetFullHeight(autoBoneSettings.targetFullHeight());
}
if (autoBoneSettings.hasRandomizeFrameOrder()) {
autoBoneConfig.setRandomizeFrameOrder(autoBoneSettings.randomizeFrameOrder());
}
if (autoBoneSettings.hasScaleEachStep()) {
autoBoneConfig.setScaleEachStep(autoBoneSettings.scaleEachStep());
}
if (autoBoneSettings.hasSampleCount()) {
autoBoneConfig.setSampleCount(autoBoneSettings.sampleCount());
}
if (autoBoneSettings.hasSampleRateMs()) {
autoBoneConfig.setSampleRateMs(autoBoneSettings.sampleRateMs());
}
if (autoBoneSettings.hasSaveRecordings()) {
autoBoneConfig.setSaveRecordings(autoBoneSettings.saveRecordings());
}
if (autoBoneSettings.hasUseSkeletonHeight()) {
autoBoneConfig.setUseSkeletonHeight(autoBoneSettings.useSkeletonHeight());
}
if (autoBoneSettings.hasRandSeed()) {
autoBoneConfig.setRandSeed(autoBoneSettings.randSeed());
}
return autoBoneConfig;
}
}

View File

@@ -80,6 +80,11 @@ public class RPCSettingsHandler {
.createTapDetectionSettings(
fbb,
this.api.server.configManager.getVrConfig().getTapDetection()
),
RPCSettingsBuilder
.createAutoBoneSettings(
fbb,
this.api.server.configManager.getVrConfig().getAutoBone()
)
);
int outbound = rpcHandler.createRPCMessage(fbb, RpcMessage.SettingsResponse, settings);
@@ -358,6 +363,15 @@ public class RPCSettingsHandler {
}
var autoBoneSettings = req.autoBoneSettings();
if (autoBoneSettings != null) {
AutoBoneConfig autoBoneConfig = this.api.server.configManager
.getVrConfig()
.getAutoBone();
RPCSettingsBuilder.readAutoBoneSettings(autoBoneSettings, autoBoneConfig);
}
this.api.server.configManager.saveConfig();
}

View File

@@ -23,9 +23,18 @@ const val TIMEOUT_MS = 2000L
*/
class Tracker @JvmOverloads constructor(
val device: Device?,
val id: Int, // VRServer.nextLocalTrackerId
val name: String, // unique, for config
val displayName: String = "Tracker #$id", // default display GUI name
/**
* VRServer.nextLocalTrackerId
*/
val id: Int,
/**
* unique, for config
*/
val name: String,
/**
* default display GUI name
*/
val displayName: String = "Tracker #$id",
trackerPosition: TrackerPosition?,
/**
* It's like the ID, but it should be local to the device if it has one
@@ -34,11 +43,23 @@ class Tracker @JvmOverloads constructor(
val hasPosition: Boolean = false,
val hasRotation: Boolean = false,
val hasAcceleration: Boolean = false,
val userEditable: Boolean = false, // User can change TrackerPosition, mounting...
val isInternal: Boolean = false, // Is used within SlimeVR (shareable trackers)
val isComputed: Boolean = false, // Has solved position + rotation (Vive trackers)
/**
* User can change TrackerPosition, mounting...
*/
val userEditable: Boolean = false,
/**
* Is used within SlimeVR (shareable trackers)
*/
val isInternal: Boolean = false,
/**
* Has solved position + rotation (Vive trackers)
*/
val isComputed: Boolean = false,
val imuType: IMUType? = null,
val usesTimeout: Boolean = false, // Automatically set the status to DISCONNECTED
/**
* Automatically set the status to DISCONNECTED
*/
val usesTimeout: Boolean = false,
val allowFiltering: Boolean = false,
val needsReset: Boolean = false,
val needsMounting: Boolean = false,