diff --git a/Cargo.lock b/Cargo.lock index c9445a505..aa8ad9cda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/flake.lock b/flake.lock index 9a147acc6..afbf7a157 100644 --- a/flake.lock +++ b/flake.lock @@ -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" } }, diff --git a/flake.nix b/flake.nix index 0d448cd0e..eeaaadc90 100644 --- a/flake.nix +++ b/flake.nix @@ -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 ]; diff --git a/gui/package.json b/gui/package.json index a7a0ce8ed..80a78d5d3 100644 --- a/gui/package.json +++ b/gui/package.json @@ -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" } diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index eea8bd9fd..caa9e14d1 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -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 diff --git a/gui/src-tauri/icons/1024x1024.png b/gui/src-tauri/icons/1024x1024.png new file mode 100644 index 000000000..1e96b691c Binary files /dev/null and b/gui/src-tauri/icons/1024x1024.png differ diff --git a/gui/src-tauri/icons/128x128.png b/gui/src-tauri/icons/128x128.png new file mode 100644 index 000000000..998e8fee3 Binary files /dev/null and b/gui/src-tauri/icons/128x128.png differ diff --git a/gui/src-tauri/icons/128x128@2x.png b/gui/src-tauri/icons/128x128@2x.png new file mode 100644 index 000000000..063fd21a7 Binary files /dev/null and b/gui/src-tauri/icons/128x128@2x.png differ diff --git a/gui/src-tauri/icons/32x32.png b/gui/src-tauri/icons/32x32.png new file mode 100644 index 000000000..4040b1a4e Binary files /dev/null and b/gui/src-tauri/icons/32x32.png differ diff --git a/gui/src-tauri/icons/Square107x107Logo.png b/gui/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 000000000..de0a69ce5 Binary files /dev/null and b/gui/src-tauri/icons/Square107x107Logo.png differ diff --git a/gui/src-tauri/icons/Square142x142Logo.png b/gui/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 000000000..63bacc8c3 Binary files /dev/null and b/gui/src-tauri/icons/Square142x142Logo.png differ diff --git a/gui/src-tauri/icons/Square150x150Logo.png b/gui/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 000000000..3f2b7c291 Binary files /dev/null and b/gui/src-tauri/icons/Square150x150Logo.png differ diff --git a/gui/src-tauri/icons/Square284x284Logo.png b/gui/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 000000000..ec06bdb99 Binary files /dev/null and b/gui/src-tauri/icons/Square284x284Logo.png differ diff --git a/gui/src-tauri/icons/Square30x30Logo.png b/gui/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 000000000..dc0646210 Binary files /dev/null and b/gui/src-tauri/icons/Square30x30Logo.png differ diff --git a/gui/src-tauri/icons/Square310x310Logo.png b/gui/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 000000000..d91e8646e Binary files /dev/null and b/gui/src-tauri/icons/Square310x310Logo.png differ diff --git a/gui/src-tauri/icons/Square44x44Logo.png b/gui/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 000000000..58c4a9dd3 Binary files /dev/null and b/gui/src-tauri/icons/Square44x44Logo.png differ diff --git a/gui/src-tauri/icons/Square71x71Logo.png b/gui/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 000000000..35f5c2329 Binary files /dev/null and b/gui/src-tauri/icons/Square71x71Logo.png differ diff --git a/gui/src-tauri/icons/Square89x89Logo.png b/gui/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 000000000..9b2c5fbef Binary files /dev/null and b/gui/src-tauri/icons/Square89x89Logo.png differ diff --git a/gui/src-tauri/icons/StoreLogo.png b/gui/src-tauri/icons/StoreLogo.png new file mode 100644 index 000000000..1666958da Binary files /dev/null and b/gui/src-tauri/icons/StoreLogo.png differ diff --git a/gui/src-tauri/icons/icon.icns b/gui/src-tauri/icons/icon.icns new file mode 100644 index 000000000..e6127498e Binary files /dev/null and b/gui/src-tauri/icons/icon.icns differ diff --git a/gui/src-tauri/icons/icon.ico b/gui/src-tauri/icons/icon.ico index a68d9acc7..8083c3fe4 100644 Binary files a/gui/src-tauri/icons/icon.ico and b/gui/src-tauri/icons/icon.ico differ diff --git a/gui/src-tauri/icons/icon.png b/gui/src-tauri/icons/icon.png index c8502e595..4ffce3d38 100644 Binary files a/gui/src-tauri/icons/icon.png and b/gui/src-tauri/icons/icon.png differ diff --git a/gui/src-tauri/icons/icon.svg b/gui/src-tauri/icons/icon.svg index 2359e9177..9259ed766 100644 --- a/gui/src-tauri/icons/icon.svg +++ b/gui/src-tauri/icons/icon.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/gui/src-tauri/src/main.rs b/gui/src-tauri/src/main.rs index cb31ddb1d..0a62dea73 100644 --- a/gui/src-tauri/src/main.rs +++ b/gui/src-tauri/src/main.rs @@ -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)] diff --git a/gui/src-tauri/tauri.conf.json b/gui/src-tauri/tauri.conf.json index 1a7c8e804..5316f9028 100644 --- a/gui/src-tauri/tauri.conf.json +++ b/gui/src-tauri/tauri.conf.json @@ -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": "", diff --git a/gui/src/components/onboarding/pages/body-proportions/BodyProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/BodyProportions.tsx index 58d2d2c3d..740923704 100644 --- a/gui/src/components/onboarding/pages/body-proportions/BodyProportions.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/BodyProportions.tsx @@ -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 | 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 | 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 (
-
- {bodyParts.map(({ label, bone, value }) => ( -
-
- {!precise && ( - decrement(value, 5)}> - -5 - - )} - decrement(value, 1)}> - -1 - - {precise && ( - decrement(value, 0.5)}> - -0.5 - - )} -
-
setSelectedBone(bone)} - > -
- - {label} - - - {Number(value * 100) - .toFixed(1) - .replace(/[.,]0$/, '')}{' '} - CM - -
-
-
- {precise && ( - increment(value, 0.5)}> - +0.5 - - )} - increment(value, 1)}> - +1 - - {!precise && ( - increment(value, 5)}> - +5 - - )} -
-
- ))} -
+
+ <> + {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; -
-
+ 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 ( +
+
+ {!precise && ( + + type === LabelType.GroupPart + ? dispatch({ + type: ProportionChangeType.Ratio, + value: -0.05, + }) + : dispatch({ + type: ProportionChangeType.Linear, + value: -5, + }) + } + > + {configFormat.format(-5)} + + )} + + type === LabelType.GroupPart + ? dispatch({ + type: ProportionChangeType.Ratio, + value: -0.01, + }) + : dispatch({ + type: ProportionChangeType.Linear, + value: -1, + }) + } + > + {configFormat.format(-1)} + + {precise && ( + + type === LabelType.GroupPart + ? dispatch({ + type: ProportionChangeType.Ratio, + value: -0.005, + }) + : dispatch({ + type: ProportionChangeType.Linear, + value: -0.5, + }) + } + > + {configFormat.format(-0.5)} + + )} +
+
+
+ + {l10n.getString(label)} + + + {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 && ( +

{`(${cmFormat.format( + value * originalValue * 100 + )})`}

+ )} +
+
+
+
+ {precise && ( + + type === LabelType.GroupPart + ? dispatch({ + type: ProportionChangeType.Ratio, + value: 0.005, + }) + : dispatch({ + type: ProportionChangeType.Linear, + value: 0.5, + }) + } + > + {configFormat.format(+0.5)} + + )} + + type === LabelType.GroupPart + ? dispatch({ + type: ProportionChangeType.Ratio, + value: 0.01, + }) + : dispatch({ + type: ProportionChangeType.Linear, + value: 1, + }) + } + > + {configFormat.format(+1)} + + {!precise && ( + + type === LabelType.GroupPart + ? dispatch({ + type: ProportionChangeType.Ratio, + value: 0.05, + }) + : dispatch({ + type: ProportionChangeType.Linear, + value: 5, + }) + } + > + {configFormat.format(+5)} + + )} +
+
+ ); + })} +
); diff --git a/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx index 62f33b583..64d395055 100644 --- a/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/ManualProportions.tsx @@ -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() { {l10n.getString('onboarding-manual_proportions-title')} +
diff --git a/gui/src/hooks/manual-proportions.ts b/gui/src/hooks/manual-proportions.ts new file mode 100644 index 000000000..a1d5e6534 --- /dev/null +++ b/gui/src/hooks/manual-proportions.ts @@ -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 = 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 | 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 | 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; + } +} diff --git a/gui/tailwind.config.cjs b/gui/tailwind.config.cjs index 587f9d3a4..c4a82521f 100644 --- a/gui/tailwind.config.cjs +++ b/gui/tailwind.config.cjs @@ -70,6 +70,7 @@ module.exports = { }, plugins: [ require('@tailwindcss/forms'), + require('tailwind-gradient-mask-image'), plugin(function ({ addUtilities, theme }) { const textConfig = (fontSize, fontWeight) => ({ fontSize, diff --git a/package-lock.json b/package-lock.json index 05d62ef96..0bd711c9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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",