Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add import proportions button #811

Merged
merged 9 commits into from
Sep 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions gui/public/i18n/en/translation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,46 @@ 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();
const { l10n } = useLocalization();
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(
Expand All @@ -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',
});
Expand All @@ -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">
Expand Down Expand Up @@ -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')}
Expand All @@ -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 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.