mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
add import proportions button (#811)
This commit is contained in:
@@ -16,10 +16,10 @@
|
||||
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
|
||||
"@tauri-apps/plugin-window": "github:tauri-apps/tauri-plugin-window#v2",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"browser-fs-access": "^0.34.1",
|
||||
"browserslist": "^4.18.1",
|
||||
"classnames": "^2.3.1",
|
||||
"eslint-config-react-app": "^7.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"flatbuffers": "^22.10.26",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"intl-pluralrules": "^1.3.1",
|
||||
|
||||
@@ -747,6 +747,9 @@ onboarding-choose_proportions-manual_proportions = Manual proportions
|
||||
onboarding-choose_proportions-manual_proportions-subtitle = For small touches
|
||||
onboarding-choose_proportions-manual_proportions-description = This will let you adjust your proportions manually by modifying them directly
|
||||
onboarding-choose_proportions-export = Export proportions
|
||||
onboarding-choose_proportions-import = Import proportions
|
||||
onboarding-choose_proportions-import-success = Imported
|
||||
onboarding-choose_proportions-import-failed = Failed
|
||||
onboarding-choose_proportions-file_type = Body proportions file
|
||||
|
||||
## Tracker manual proportions setup
|
||||
|
||||
@@ -5,21 +5,30 @@ import classNames from 'classnames';
|
||||
import { Typography } from '../../../commons/Typography';
|
||||
import { Button } from '../../../commons/Button';
|
||||
import {
|
||||
SkeletonConfigResponseT,
|
||||
RpcMessage,
|
||||
SkeletonConfigRequestT,
|
||||
SkeletonBone,
|
||||
ChangeSkeletonConfigRequestT,
|
||||
} from 'solarxr-protocol';
|
||||
import { useWebsocketAPI } from '../../../../hooks/websocket-api';
|
||||
import saveAs from 'file-saver';
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
import { writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { useIsTauri } from '../../../../hooks/breakpoint';
|
||||
import { useAppContext } from '../../../../hooks/app';
|
||||
import { error } from '../../../../utils/logging';
|
||||
import { fileOpen, fileSave } from 'browser-fs-access';
|
||||
import { useDebouncedEffect } from '../../../../hooks/timeout';
|
||||
|
||||
export const MIN_HEIGHT = 0.4;
|
||||
export const MAX_HEIGHT = 4;
|
||||
export const DEFAULT_HEIGHT = 1.5;
|
||||
export const CURRENT_EXPORT_VERSION = 1;
|
||||
|
||||
enum ImportStatus {
|
||||
FAILED,
|
||||
SUCCESS,
|
||||
OK,
|
||||
}
|
||||
|
||||
export function ProportionsChoose() {
|
||||
const isTauri = useIsTauri();
|
||||
@@ -27,8 +36,15 @@ export function ProportionsChoose() {
|
||||
const { applyProgress, state } = useOnboarding();
|
||||
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
|
||||
const [animated, setAnimated] = useState(false);
|
||||
const [importState, setImportState] = useState(ImportStatus.OK);
|
||||
const { computedTrackers } = useAppContext();
|
||||
|
||||
useDebouncedEffect(
|
||||
() => setImportState(ImportStatus.OK),
|
||||
[importState],
|
||||
2000
|
||||
);
|
||||
|
||||
const hmdTracker = useMemo(
|
||||
() =>
|
||||
computedTrackers.find(
|
||||
@@ -48,9 +64,27 @@ export function ProportionsChoose() {
|
||||
[hmdTracker?.tracker.position?.y]
|
||||
);
|
||||
|
||||
const importStatusKey = useMemo(() => {
|
||||
switch (importState) {
|
||||
case ImportStatus.FAILED:
|
||||
return 'onboarding-choose_proportions-import-failed';
|
||||
case ImportStatus.SUCCESS:
|
||||
return 'onboarding-choose_proportions-import-success';
|
||||
case ImportStatus.OK:
|
||||
return 'onboarding-choose_proportions-import';
|
||||
}
|
||||
}, [importState]);
|
||||
|
||||
useRPCPacket(
|
||||
RpcMessage.SkeletonConfigResponse,
|
||||
(data: SkeletonConfigResponseT) => {
|
||||
(data: SkeletonConfigExport) => {
|
||||
// Convert the skeleton part enums into a string
|
||||
data.skeletonParts.forEach((x) => {
|
||||
if (typeof x.bone === 'number')
|
||||
x.bone = SkeletonBone[x.bone] as SkeletonBoneKey;
|
||||
});
|
||||
data.version = CURRENT_EXPORT_VERSION;
|
||||
|
||||
const blob = new Blob([JSON.stringify(data)], {
|
||||
type: 'application/json',
|
||||
});
|
||||
@@ -71,13 +105,52 @@ export function ProportionsChoose() {
|
||||
error(err);
|
||||
});
|
||||
} else {
|
||||
saveAs(blob, 'body-proportions.json');
|
||||
fileSave(blob, {
|
||||
fileName: 'body-proportions.json',
|
||||
extensions: ['.json'],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
applyProgress(0.85);
|
||||
|
||||
const onImport = async () => {
|
||||
const file = await fileOpen({
|
||||
mimeTypes: ['application/json'],
|
||||
});
|
||||
|
||||
const text = await file.text();
|
||||
const config = JSON.parse(text) as SkeletonConfigExport;
|
||||
if (
|
||||
!config?.skeletonParts?.length ||
|
||||
!Array.isArray(config.skeletonParts)
|
||||
) {
|
||||
error(
|
||||
'failed to import body proportions because skeletonParts is not an array/empty'
|
||||
);
|
||||
return setImportState(ImportStatus.FAILED);
|
||||
}
|
||||
|
||||
for (const bone of [...config.skeletonParts]) {
|
||||
if (
|
||||
(typeof bone.bone === 'string' && !(bone.bone in SkeletonBone)) ||
|
||||
(typeof bone.bone === 'number' &&
|
||||
typeof SkeletonBone[bone.bone] !== 'string')
|
||||
) {
|
||||
error(
|
||||
`failed to import body proportions because ${bone.bone} is not a valid bone`
|
||||
);
|
||||
return setImportState(ImportStatus.FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
parseConfigImport(config).forEach((req) =>
|
||||
sendRPCPacket(RpcMessage.ChangeSkeletonConfigRequest, req)
|
||||
);
|
||||
setImportState(ImportStatus.SUCCESS);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-5 h-full items-center w-full xs:justify-center mobile:overflow-y-auto relative px-4 pb-4">
|
||||
@@ -196,7 +269,7 @@ export function ProportionsChoose() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex flex-row gap-3">
|
||||
{!state.alonePage && (
|
||||
<Button variant="secondary" to="/onboarding/reset-tutorial">
|
||||
{l10n.getString('onboarding-previous_step')}
|
||||
@@ -214,9 +287,46 @@ export function ProportionsChoose() {
|
||||
>
|
||||
{l10n.getString('onboarding-choose_proportions-export')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={!state.alonePage ? 'secondary' : 'tertiary'}
|
||||
className={classNames(
|
||||
'transition-colors',
|
||||
importState === ImportStatus.FAILED && 'bg-status-critical',
|
||||
importState === ImportStatus.SUCCESS && 'bg-status-success'
|
||||
)}
|
||||
onClick={onImport}
|
||||
>
|
||||
{l10n.getString(importStatusKey)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function parseConfigImport(
|
||||
config: SkeletonConfigExport
|
||||
): ChangeSkeletonConfigRequestT[] {
|
||||
if (!config.version) config.version = 1;
|
||||
if (config.version < 1) {
|
||||
// Add config migration stuff here, this one is just an example.
|
||||
}
|
||||
|
||||
return config.skeletonParts.map((part) => {
|
||||
const bone =
|
||||
typeof part.bone === 'string' ? SkeletonBone[part.bone] : part.bone;
|
||||
|
||||
return new ChangeSkeletonConfigRequestT(bone, part.value);
|
||||
});
|
||||
}
|
||||
|
||||
type SkeletonBoneKey = keyof typeof SkeletonBone;
|
||||
|
||||
interface SkeletonConfigExport {
|
||||
version?: number;
|
||||
skeletonParts: {
|
||||
bone: SkeletonBoneKey | SkeletonBone;
|
||||
value: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -33,10 +33,10 @@
|
||||
"@tauri-apps/plugin-shell": "github:tauri-apps/tauri-plugin-shell#v2",
|
||||
"@tauri-apps/plugin-window": "github:tauri-apps/tauri-plugin-window#v2",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"browser-fs-access": "^0.34.1",
|
||||
"browserslist": "^4.18.1",
|
||||
"classnames": "^2.3.1",
|
||||
"eslint-config-react-app": "^7.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"flatbuffers": "^22.10.26",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"intl-pluralrules": "^1.3.1",
|
||||
@@ -4246,6 +4246,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browser-fs-access": {
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.34.1.tgz",
|
||||
"integrity": "sha512-HPaRf2yimp8kWSuWJXc8Mi78dPbDzfduA+Gyq14H4jlMvd6XNfIRm36Y2yRLaa4x0gwcGuepj4zf14oiTlxrxQ=="
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.21.10",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
|
||||
@@ -5794,11 +5799,6 @@
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/file-saver": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||
},
|
||||
"node_modules/filesize": {
|
||||
"version": "8.0.7",
|
||||
"resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz",
|
||||
|
||||
Reference in New Issue
Block a user