Add a ratio mode for manual offsets (#615)

This commit is contained in:
Uriel
2023-03-13 22:10:06 +01:00
committed by GitHub
parent 7c1fd5b6ab
commit 5f8eaf432b
30 changed files with 640 additions and 167 deletions

6
Cargo.lock generated
View File

@@ -3133,16 +3133,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.25.0"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
dependencies = [
"autocfg",
"bytes",
"memchr",
"num_cpus",
"pin-project-lite",
"windows-sys 0.42.0",
"windows-sys 0.45.0",
]
[[package]]

8
flake.lock generated
View File

@@ -32,16 +32,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1675410244,
"narHash": "sha256-ODj6egMoH/HgAF/0wIy0EfRBeUx5FMuLl6uAdUW3kCI=",
"lastModified": 1677407201,
"narHash": "sha256-3blwdI9o1BAprkvlByHvtEm5HAIRn/XPjtcfiunpY7s=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f7543a7539a007e9562e4d8d24e17a4bcf369b68",
"rev": "7f5639fa3b68054ca0b062866dc62b22c3f11505",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.11",
"ref": "nixos-unstable",
"type": "indirect"
}
},

View File

@@ -1,7 +1,7 @@
{
description = "Server app for SlimeVR ecosystem";
inputs.nixpkgs.url = "nixpkgs/nixos-22.11";
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.rust-overlay.url = "github:oxalica/rust-overlay";
@@ -57,7 +57,7 @@
exa
fd
jdk # JDK17
jdk17 # JDK17
nodejs
gradle
];

View File

@@ -81,6 +81,7 @@
"postcss": "^8.4.12",
"prettier": "^2.7.1",
"pretty-quick": "^3.1.3",
"tailwind-gradient-mask-image": "^1.0.0",
"tailwindcss": "^3.0.23",
"vite": "^4.0.3"
}

View File

@@ -40,12 +40,14 @@ body_part-LEFT_FOOT = Left foot
skeleton_bone-NONE = None
skeleton_bone-HEAD = Head Shift
skeleton_bone-NECK = Neck Length
skeleton_bone-torso_group = Torso length
skeleton_bone-CHEST = Chest Length
skeleton_bone-CHEST_OFFSET = Chest Offset
skeleton_bone-WAIST = Waist Length
skeleton_bone-HIP = Hip Length
skeleton_bone-HIP_OFFSET = Hip Offset
skeleton_bone-HIPS_WIDTH = Hips Width
skeleton_bone-leg_group = Leg length
skeleton_bone-UPPER_LEG = Upper Leg Length
skeleton_bone-LOWER_LEG = Lower Leg Length
skeleton_bone-FOOT_LENGTH = Foot Length
@@ -53,6 +55,7 @@ skeleton_bone-FOOT_SHIFT = Foot Shift
skeleton_bone-SKELETON_OFFSET = Skeleton Offset
skeleton_bone-SHOULDERS_DISTANCE = Shoulders Distance
skeleton_bone-SHOULDERS_WIDTH = Shoulders Width
skeleton_bone-arm_group = Arm length
skeleton_bone-UPPER_ARM = Upper Arm Length
skeleton_bone-LOWER_ARM = Lower Arm Length
skeleton_bone-HAND_Y = Hand Distance Y
@@ -615,6 +618,7 @@ onboarding-manual_proportions-back = Go Back to Reset tutorial
onboarding-manual_proportions-title = Manual Body Proportions
onboarding-manual_proportions-precision = Precision adjust
onboarding-manual_proportions-auto = Automatic calibration
onboarding-manual_proportions-ratio = Adjust by ratio groups
## Tracker automatic proportions setup
onboarding-automatic_proportions-back = Go Back to Reset tutorial

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-miterlimit="10" clip-rule="evenodd" version="1.1" viewBox="0 0 380 380" xml:space="preserve"><rect id="bg" width="380" height="380" fill="#663499" stroke-width="1"/><g id="logo" fill="none" stroke="#fff"><path id="left" stroke-width="13.62" d="m72.867 191.74 37-39 39 36"/><path id="right" stroke-width="13.62" d="m208.87 187.74 38-35 36 38"/><path id="outer" stroke-linecap="square" stroke-width="17" d="m56.867 253.74s130.61-31.182 248 5c13.45 4.146 20.244 2.975 20-8s1.909-126.06-46-131"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-miterlimit="10" clip-rule="evenodd" version="1.1" viewBox="0 0 380 380" xml:space="preserve"><rect id="bg" width="380" height="380" fill="#663499" stroke-width="1"/><g id="logo" fill="none" stroke="#fff"><path id="left" stroke-width="13.62" d="m72.867 191.74 37-39 39 36"/><path id="right" stroke-width="13.62" d="m208.87 187.74 38-35 36 38"/><path id="outer" stroke-linecap="square" stroke-width="17" d="m56.867 253.74s130.61-31.182 248 5c13.45 4.146 20.244 2.975 20-8s1.909-126.06-46-131"/></g></svg>

Before

Width:  |  Height:  |  Size: 569 B

After

Width:  |  Height:  |  Size: 570 B

View File

@@ -13,7 +13,9 @@ use const_format::concatcp;
use rand::{seq::SliceRandom, thread_rng};
use shadow_rs::shadow;
use tauri::api::process::Command;
use tauri::{Manager, WindowEvent};
use tauri::Manager;
#[cfg(windows)]
use tauri::WindowEvent;
use tempfile::Builder;
#[cfg(windows)]

View File

@@ -14,7 +14,13 @@
"active": true,
"targets": "all",
"identifier": "dev.slimevr.SlimeVR",
"icon": ["icons/icon.ico", "icons/icon.png"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [],
"externalBin": [],
"copyright": "",

View File

@@ -1,20 +1,12 @@
import { useLocalization } from '@fluent/react';
import classNames from 'classnames';
import { MouseEventHandler, ReactNode, useEffect } from 'react';
import {
MouseEventHandler,
ReactNode,
useEffect,
useMemo,
useState,
} from 'react';
import {
ChangeSkeletonConfigRequestT,
RpcMessage,
SkeletonBone,
SkeletonConfigRequestT,
SkeletonConfigResponseT,
} from 'solarxr-protocol';
import { useWebsocketAPI } from '../../../../hooks/websocket-api';
LabelType,
ProportionChangeType,
useManualProportions,
} from '../../../../hooks/manual-proportions';
import { useLocaleConfig } from '../../../../i18n/config';
import { Typography } from '../../../commons/Typography';
function IncrementButton({
@@ -40,156 +32,239 @@ function IncrementButton({
export function BodyProportions({
precise,
variant = 'onboarding',
type,
variant: _variant = 'onboarding',
}: {
precise: boolean;
type: 'linear' | 'ratio';
variant: 'onboarding' | 'alone';
}) {
const [bodyParts, _ratioMode, currentSelection, dispatch, setRatioMode] =
useManualProportions();
const { currentLocales } = useLocaleConfig();
const { l10n } = useLocalization();
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const [config, setConfig] = useState<Omit<
SkeletonConfigResponseT,
'pack'
> | null>(null);
const [selectedBone, setSelectedBone] = useState(SkeletonBone.HEAD);
const bodyParts = useMemo(() => {
return (
config?.skeletonParts.map(({ bone, value }) => ({
bone,
label: l10n.getString('skeleton_bone-' + SkeletonBone[bone]),
value,
})) || []
);
}, [config]);
useRPCPacket(
RpcMessage.SkeletonConfigResponse,
(data: SkeletonConfigResponseT) => {
setConfig(data);
}
);
const cmFormat = Intl.NumberFormat(currentLocales, {
style: 'unit',
unit: 'centimeter',
maximumFractionDigits: 1,
});
const configFormat = Intl.NumberFormat(currentLocales, {
signDisplay: 'always',
maximumFractionDigits: 1,
});
const percentageFormat = Intl.NumberFormat(currentLocales, {
style: 'percent',
maximumFractionDigits: 1,
});
useEffect(() => {
sendRPCPacket(
RpcMessage.SkeletonConfigRequest,
new SkeletonConfigRequestT()
);
}, []);
const roundedStep = (value: number, step: number, add: boolean) => {
if (!add) {
return (Math.round(value * 200) - step * 2) / 200;
if (type === 'linear') {
setRatioMode(false);
} else {
return (Math.round(value * 200) + step * 2) / 200;
setRatioMode(true);
}
};
const updateConfigValue = (configChange: ChangeSkeletonConfigRequestT) => {
sendRPCPacket(RpcMessage.ChangeSkeletonConfigRequest, configChange);
const conf = { ...config } as Omit<SkeletonConfigResponseT, 'pack'> | null;
const b = conf?.skeletonParts?.find(({ bone }) => bone == selectedBone);
if (!b || !conf) return;
b.value = configChange.value;
setConfig(conf);
};
const increment = async (value: number, v: number) => {
const configChange = new ChangeSkeletonConfigRequestT();
configChange.bone = selectedBone;
configChange.value = roundedStep(value, v, true);
updateConfigValue(configChange);
};
const decrement = (value: number, v: number) => {
const configChange = new ChangeSkeletonConfigRequestT();
configChange.bone = selectedBone;
configChange.value = value - v / 100;
updateConfigValue(configChange);
};
}, [type]);
return (
<div className="relative w-full">
<div className="flex flex-col overflow-y-scroll overflow-x-hidden max-h-[450px] w-full px-1 gap-3 pb-16">
{bodyParts.map(({ label, bone, value }) => (
<div className="flex" key={bone}>
<div
className={classNames(
'flex gap-2 transition-opacity duration-300',
selectedBone != bone && 'opacity-0 pointer-events-none'
)}
>
{!precise && (
<IncrementButton onClick={() => decrement(value, 5)}>
-5
</IncrementButton>
)}
<IncrementButton onClick={() => decrement(value, 1)}>
-1
</IncrementButton>
{precise && (
<IncrementButton onClick={() => decrement(value, 0.5)}>
-0.5
</IncrementButton>
)}
</div>
<div
className="flex flex-grow flex-col px-2"
onClick={() => setSelectedBone(bone)}
>
<div
key={bone}
className={classNames(
'p-3 rounded-lg h-16 flex w-full items-center justify-between px-6 transition-colors duration-300 bg-background-60',
(selectedBone == bone && 'opacity-100') || 'opacity-50'
)}
>
<Typography variant="section-title" bold>
{label}
</Typography>
<Typography variant="main-title" bold>
{Number(value * 100)
.toFixed(1)
.replace(/[.,]0$/, '')}{' '}
CM
</Typography>
</div>
</div>
<div
className={classNames(
'flex gap-2 transition-opacity duration-300',
selectedBone != bone && 'opacity-0 pointer-events-none'
)}
>
{precise && (
<IncrementButton onClick={() => increment(value, 0.5)}>
+0.5
</IncrementButton>
)}
<IncrementButton onClick={() => increment(value, 1)}>
+1
</IncrementButton>
{!precise && (
<IncrementButton onClick={() => increment(value, 5)}>
+5
</IncrementButton>
)}
</div>
</div>
))}
</div>
<div
className={classNames(
'flex flex-col overflow-y-scroll overflow-x-hidden max-h-[450px] h-[54vh]',
'w-full px-1 gap-3 gradient-mask-b-90'
)}
>
<>
{bodyParts.map(({ label, type, value: originalValue, ...props }) => {
const value =
'index' in props && props.index !== undefined
? props.bones[props.index].value
: originalValue;
const selected = currentSelection.label === label;
<div className="absolute bottom-0 h-20 w-full pointer-events-none">
<div
className={classNames(
'w-full h-full bg-gradient-to-b from-transparent opacity-100',
variant === 'onboarding' && 'to-background-80',
variant === 'alone' && 'to-background-70'
)}
></div>
const selectNew = () => {
switch (type) {
case LabelType.Bone: {
if (!('bone' in props)) throw 'unreachable';
dispatch({
...props,
label,
value,
type: ProportionChangeType.Bone,
});
break;
}
case LabelType.Group: {
if (!('bones' in props)) throw 'unreachable';
dispatch({
...props,
label,
value,
type: ProportionChangeType.Group,
index: undefined,
parentLabel: label,
});
break;
}
case LabelType.GroupPart: {
if (!('index' in props)) throw 'unreachable';
dispatch({
...props,
label,
// If this isn't done, we are replacing total
// with percentage value
value: originalValue,
type: ProportionChangeType.Group,
index: props.index,
});
}
}
};
return (
<div className="flex" key={label}>
<div
className={classNames(
'flex gap-2 transition-opacity duration-300',
!selected && 'opacity-0 pointer-events-none'
)}
>
{!precise && (
<IncrementButton
onClick={() =>
type === LabelType.GroupPart
? dispatch({
type: ProportionChangeType.Ratio,
value: -0.05,
})
: dispatch({
type: ProportionChangeType.Linear,
value: -5,
})
}
>
{configFormat.format(-5)}
</IncrementButton>
)}
<IncrementButton
onClick={() =>
type === LabelType.GroupPart
? dispatch({
type: ProportionChangeType.Ratio,
value: -0.01,
})
: dispatch({
type: ProportionChangeType.Linear,
value: -1,
})
}
>
{configFormat.format(-1)}
</IncrementButton>
{precise && (
<IncrementButton
onClick={() =>
type === LabelType.GroupPart
? dispatch({
type: ProportionChangeType.Ratio,
value: -0.005,
})
: dispatch({
type: ProportionChangeType.Linear,
value: -0.5,
})
}
>
{configFormat.format(-0.5)}
</IncrementButton>
)}
</div>
<div
className="flex flex-grow flex-col px-2"
onClick={selectNew}
>
<div
key={label}
className={classNames(
'p-3 rounded-lg h-16 flex w-full items-center justify-between px-6 transition-colors duration-300 bg-background-60',
(selected && 'opacity-100') || 'opacity-50'
)}
>
<Typography variant="section-title" bold>
{l10n.getString(label)}
</Typography>
<Typography variant="main-title" bold>
{type === LabelType.GroupPart
? /* Make number rounding so it's based on .5 decimals */
percentageFormat.format(Math.round(value * 200) / 200)
: cmFormat.format(value * 100)}
{type === LabelType.GroupPart && (
<p className="text-standard">{`(${cmFormat.format(
value * originalValue * 100
)})`}</p>
)}
</Typography>
</div>
</div>
<div
className={classNames(
'flex gap-2 transition-opacity duration-300',
!selected && 'opacity-0 pointer-events-none'
)}
>
{precise && (
<IncrementButton
onClick={() =>
type === LabelType.GroupPart
? dispatch({
type: ProportionChangeType.Ratio,
value: 0.005,
})
: dispatch({
type: ProportionChangeType.Linear,
value: 0.5,
})
}
>
{configFormat.format(+0.5)}
</IncrementButton>
)}
<IncrementButton
onClick={() =>
type === LabelType.GroupPart
? dispatch({
type: ProportionChangeType.Ratio,
value: 0.01,
})
: dispatch({
type: ProportionChangeType.Linear,
value: 1,
})
}
>
{configFormat.format(+1)}
</IncrementButton>
{!precise && (
<IncrementButton
onClick={() =>
type === LabelType.GroupPart
? dispatch({
type: ProportionChangeType.Ratio,
value: 0.05,
})
: dispatch({
type: ProportionChangeType.Linear,
value: 5,
})
}
>
{configFormat.format(+5)}
</IncrementButton>
)}
</div>
</div>
);
})}
</>
</div>
</div>
);

View File

@@ -9,7 +9,7 @@ import { PersonFrontIcon } from '../../../commons/PersonFrontIcon';
import { Typography } from '../../../commons/Typography';
import { BodyProportions } from './BodyProportions';
import { useLocalization } from '@fluent/react';
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { useBodyProportions } from '../../../../hooks/body-proportions';
export function ManualProportionsPage() {
@@ -20,10 +20,12 @@ export function ManualProportionsPage() {
applyProgress(0.9);
const { control, watch } = useForm<{ precise: boolean }>({
defaultValues: { precise: false },
const savedValue = useMemo(() => localStorage.getItem('ratioMode'), []);
const { control, watch } = useForm<{ precise: boolean; ratio: boolean }>({
defaultValues: { precise: false, ratio: savedValue !== 'false' },
});
const { precise } = watch();
const { precise, ratio } = watch();
const resetAll = () => {
sendRPCPacket(
@@ -32,6 +34,10 @@ export function ManualProportionsPage() {
);
};
useEffect(() => {
localStorage.setItem('ratioMode', ratio.toString());
}, [ratio]);
useEffect(() => {
onPageOpened();
}, []);
@@ -51,6 +57,12 @@ export function ManualProportionsPage() {
<Typography variant="main-title">
{l10n.getString('onboarding-manual_proportions-title')}
</Typography>
<CheckBox
control={control}
label={l10n.getString('onboarding-manual_proportions-ratio')}
name="ratio"
variant="toggle"
></CheckBox>
<CheckBox
control={control}
label={l10n.getString(
@@ -62,6 +74,7 @@ export function ManualProportionsPage() {
</div>
<BodyProportions
precise={precise}
type={ratio ? 'ratio' : 'linear'}
variant={state.alonePage ? 'alone' : 'onboarding'}
></BodyProportions>
</div>

View File

@@ -0,0 +1,357 @@
import {
SkeletonConfigResponseT,
SkeletonBone,
RpcMessage,
SkeletonConfigRequestT,
ChangeSkeletonConfigRequestT,
} from 'solarxr-protocol';
import { useWebsocketAPI } from './websocket-api';
import { useReducer, useEffect, useMemo, useState, useLayoutEffect } from 'react';
export type ProportionChange = LinearChange | RatioChange | BoneChange | GroupChange;
export enum ProportionChangeType {
Linear,
Ratio,
Bone,
Group,
}
export interface LinearChange {
type: ProportionChangeType.Linear;
value: number;
}
export interface RatioChange {
type: ProportionChangeType.Ratio;
/**
* This is a number between -1 and 1 [-1; 1]
*/
value: number;
}
export interface BoneChange {
type: ProportionChangeType.Bone;
bone: SkeletonBone;
value: number;
label: string;
}
export interface GroupChange {
type: ProportionChangeType.Group;
bones: {
bone: SkeletonBone;
/**
* This is a number between 0 and 1 [0; 1]
*/
value: number;
label: string;
}[];
value: number;
label: string;
index?: number;
parentLabel: string;
}
export type ProportionState = BoneState | GroupState;
export enum BoneType {
Single,
Group,
}
export interface BoneState {
type: BoneType.Single;
bone: SkeletonBone;
value: number;
label: string;
}
export interface GroupState {
type: BoneType.Group;
bones: {
bone: SkeletonBone;
/**
* This is a number between 0 and 1 [0; 1]
*/
value: number;
}[];
value: number;
label: string;
index?: number;
parentLabel: string;
}
function reducer(state: ProportionState, action: ProportionChange): ProportionState {
switch (action.type) {
case ProportionChangeType.Bone: {
return {
...action,
type: BoneType.Single,
};
}
case ProportionChangeType.Group: {
return {
...action,
type: BoneType.Group,
};
}
case ProportionChangeType.Linear: {
if (action.value > 0) {
return {
...state,
value: roundedStep(state.value, action.value, true),
};
}
return {
...state,
value: state.value + action.value / 100,
};
}
case ProportionChangeType.Ratio: {
if (state.type === BoneType.Single || state.index === undefined) {
throw new Error(`Unexpected increase of bone ${state.label}`);
}
const newState: GroupState = JSON.parse(JSON.stringify(state));
if (newState.index === undefined) throw 'unreachable';
newState.bones[newState.index].value += action.value;
if (newState.bones[newState.index].value <= 0) return state;
const filtered = newState.bones.filter((_it, index) => newState.index !== index);
const total = filtered.reduce((acc, cur) => acc + cur.value, 0);
for (const part of filtered) {
part.value += (part.value / total) * action.value * -1;
if (part.value <= 0) return state;
}
return newState;
}
}
}
export type Label = BoneLabel | GroupLabel | GroupPartLabel;
export enum LabelType {
Bone,
Group,
GroupPart,
}
export interface BoneLabel {
type: LabelType.Bone;
bone: SkeletonBone;
value: number;
label: string;
}
export interface GroupLabel {
type: LabelType.Group;
bones: {
bone: SkeletonBone;
/**
* This is a number between 0 and 1 [0; 1]
*/
value: number;
label: string;
}[];
value: number;
label: string;
}
export interface GroupPartLabel {
type: LabelType.GroupPart;
bones: {
bone: SkeletonBone;
/**
* This is a number between 0 and 1 [0; 1]
*/
value: number;
label: string;
}[];
value: number;
label: string;
parentLabel: string;
index: number;
}
const BONE_MAPPING: Map<string, SkeletonBone[]> = new Map([
[
'skeleton_bone-torso_group',
[SkeletonBone.CHEST, SkeletonBone.HIP, SkeletonBone.WAIST],
],
['skeleton_bone-leg_group', [SkeletonBone.UPPER_LEG, SkeletonBone.LOWER_LEG]],
['skeleton_bone-arm_group', [SkeletonBone.UPPER_ARM, SkeletonBone.LOWER_ARM]],
]);
export const INVALID_BONE: BoneState = {
type: BoneType.Single,
bone: SkeletonBone.NONE,
value: 0,
label: 'invalid-bone',
};
export function useManualProportions(): [
Label[],
boolean,
ProportionState,
(change: ProportionChange) => void,
(ratio: boolean) => void
] {
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const [config, setConfig] = useState<Omit<SkeletonConfigResponseT, 'pack'> | null>(
null
);
const [ratio, setRatio] = useState(false);
const [state, dispatch] = useReducer(reducer, INVALID_BONE);
const bodyParts: Label[] = useMemo(() => {
if (!config) return [];
if (ratio) {
const groups: GroupPartLabel[] = [];
for (const [label, related] of BONE_MAPPING) {
const children = config.skeletonParts.filter((it) => related.includes(it.bone));
const total = children.reduce((acc, cur) => cur.value + acc, 0);
const group: GroupPartLabel = {
parentLabel: label,
label,
type: LabelType.GroupPart,
value: total,
bones: children.map((it) => ({
label: 'skeleton_bone-' + SkeletonBone[it.bone],
value: it.value / total,
bone: it.bone,
})),
index: 0,
};
groups.push(
...children.map((_it, index) => ({
...group,
index,
label: group.bones[index].label,
}))
);
}
return config.skeletonParts.flatMap(({ bone, value }) => {
const part = groups.find((it) => it.bones[it.index].bone === bone);
if (part === undefined) {
return {
type: LabelType.Bone,
bone,
label: 'skeleton_bone-' + SkeletonBone[bone],
value,
};
}
if (part.index === 0) {
return [
// For some reason, Typescript can't handle this being a GroupPart
// when specifically inside an array. If I directly return it without part,
// it will work. Surely some typing in flatMap's definition is wrong
{
...part,
type: LabelType.Group,
label: part.parentLabel,
index: undefined,
} as unknown as GroupPartLabel,
part,
];
}
return part;
});
}
return config.skeletonParts.map(({ bone, value }) => ({
type: LabelType.Bone,
bone,
label: 'skeleton_bone-' + SkeletonBone[bone],
value,
}));
}, [config, ratio]);
useLayoutEffect(() => {
dispatch({
...INVALID_BONE,
type: ProportionChangeType.Bone,
});
}, [ratio]);
useRPCPacket(RpcMessage.SkeletonConfigResponse, (data: SkeletonConfigResponseT) => {
setConfig(data);
});
useEffect(() => {
sendRPCPacket(RpcMessage.SkeletonConfigRequest, new SkeletonConfigRequestT());
}, []);
useEffect(() => {
const conf = { ...config } as Omit<SkeletonConfigResponseT, 'pack'> | null;
if (state.type === BoneType.Single) {
// Just ignore if bone is none (because initial state value)
// and check if we actually changed of value
if (
state.bone === SkeletonBone.NONE ||
bodyParts.find((it) => it.type === LabelType.Bone && it.bone === state.bone)
?.value === state.value
) {
return;
}
sendRPCPacket(
RpcMessage.ChangeSkeletonConfigRequest,
new ChangeSkeletonConfigRequestT(state.bone, state.value)
);
const b = conf?.skeletonParts?.find(({ bone }) => bone === state.bone);
if (!b || !conf) return;
b.value = state.value;
} else {
const part = bodyParts.find(
(it) =>
it.type === LabelType.Group &&
(it.label === state.label || it.label === state.parentLabel)
) as GroupLabel | undefined;
// Check if we found the group we were looking for
// and check if it even changed of value
// we only need to check one child because changing one
// value propagates to the other children
if (
!part ||
(part.value === state.value && part.bones[0].value === state.bones[0].value)
) {
return;
}
for (const child of state.bones) {
sendRPCPacket(
RpcMessage.ChangeSkeletonConfigRequest,
new ChangeSkeletonConfigRequestT(child.bone, state.value * child.value)
);
const b = conf?.skeletonParts?.find(({ bone }) => bone === child.bone);
if (!b || !conf) return;
b.value = state.value * child.value;
}
}
setConfig(conf);
}, [state]);
return [bodyParts, ratio, state, dispatch, setRatio];
}
function roundedStep(value: number, step: number, add: boolean): number {
if (!add) {
return (Math.round(value * 200) - step * 2) / 200;
} else {
return (Math.round(value * 200) + step * 2) / 200;
}
}

View File

@@ -70,6 +70,7 @@ module.exports = {
},
plugins: [
require('@tailwindcss/forms'),
require('tailwind-gradient-mask-image'),
plugin(function ({ addUtilities, theme }) {
const textConfig = (fontSize, fontWeight) => ({
fontSize,

14
package-lock.json generated
View File

@@ -70,6 +70,7 @@
"postcss": "^8.4.12",
"prettier": "^2.7.1",
"pretty-quick": "^3.1.3",
"tailwind-gradient-mask-image": "^1.0.0",
"tailwindcss": "^3.0.23",
"vite": "^4.0.3"
}
@@ -8874,6 +8875,12 @@
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==",
"dev": true
},
"node_modules/tailwind-gradient-mask-image": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tailwind-gradient-mask-image/-/tailwind-gradient-mask-image-1.0.0.tgz",
"integrity": "sha512-eZJhn6wHZ0Irfpq5sm0ErewH6IC82gqjfVsDQ7MYrJcfTgepOoTI6EryLppNPoZds4EeD6/H0WrysT8HE90H5g==",
"dev": true
},
"node_modules/tailwindcss": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz",
@@ -15422,6 +15429,7 @@
"react-modal": "3.15.1",
"react-router-dom": "^6.2.2",
"solarxr-protocol": "file:../solarxr-protocol",
"tailwind-gradient-mask-image": "^1.0.0",
"tailwindcss": "^3.0.23",
"three": "^0.148.0",
"typescript": "^4.6.3",
@@ -15583,6 +15591,12 @@
}
}
},
"tailwind-gradient-mask-image": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tailwind-gradient-mask-image/-/tailwind-gradient-mask-image-1.0.0.tgz",
"integrity": "sha512-eZJhn6wHZ0Irfpq5sm0ErewH6IC82gqjfVsDQ7MYrJcfTgepOoTI6EryLppNPoZds4EeD6/H0WrysT8HE90H5g==",
"dev": true
},
"tailwindcss": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz",