From 4e698b693cf291f6e2d3251212319b5742809292 Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Thu, 2 Jan 2025 16:19:10 -0500 Subject: [PATCH 01/16] Don't filter non-temporal trackers (#1255) --- .../trackerdata/TrackerFrames.kt | 1 + .../dev/slimevr/tracking/trackers/Tracker.kt | 39 ++++++++++++------- .../dev/slimevr/unit/MountingResetTests.kt | 2 + 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrames.kt b/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrames.kt index 2aae21d4d1..d81c2dc09e 100644 --- a/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrames.kt +++ b/server/core/src/main/java/dev/slimevr/poseframeformat/trackerdata/TrackerFrames.kt @@ -41,6 +41,7 @@ data class TrackerFrames(val name: String = "", val frames: FastList Date: Thu, 2 Jan 2025 16:24:56 -0500 Subject: [PATCH 02/16] Fix over 180 degrees rotations in HumanSkeleton (#1277) --- .../processor/skeleton/HumanSkeleton.kt | 47 +------------------ 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt index adbe0cfced..c500ea2632 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt @@ -785,11 +785,6 @@ class HumanSkeleton( var hipRot = it.getRotation() var chestRot = chest.getRotation() - // Get the rotation relative to where we expect the hip to be - if (chestRot.times(FORWARD_QUATERNION).dot(hipRot) < 0.0f) { - hipRot = hipRot.unaryMinus() - } - // Interpolate between the chest and the hip chestRot = chestRot.interpQ(hipRot, waistFromChestHipAveraging) @@ -802,15 +797,6 @@ class HumanSkeleton( var rightLegRot = rightUpperLegTracker?.getRotation() ?: IDENTITY var chestRot = chest.getRotation() - // Get the rotation relative to where we expect the upper legs to be - val expectedUpperLegsRot = chestRot.times(FORWARD_QUATERNION) - if (expectedUpperLegsRot.dot(leftLegRot) < 0.0f) { - leftLegRot = leftLegRot.unaryMinus() - } - if (expectedUpperLegsRot.dot(rightLegRot) < 0.0f) { - rightLegRot = rightLegRot.unaryMinus() - } - // Interpolate between the pelvis, averaged from the legs, and the chest chestRot = chestRot.interpQ(leftLegRot.lerpQ(rightLegRot, 0.5f), waistFromChestLegsAveraging).unit() @@ -827,15 +813,6 @@ class HumanSkeleton( var rightLegRot = rightUpperLegTracker?.getRotation() ?: IDENTITY var waistRot = it.getRotation() - // Get the rotation relative to where we expect the upper legs to be - val expectedUpperLegsRot = waistRot.times(FORWARD_QUATERNION) - if (expectedUpperLegsRot.dot(leftLegRot) < 0.0f) { - leftLegRot = leftLegRot.unaryMinus() - } - if (expectedUpperLegsRot.dot(rightLegRot) < 0.0f) { - rightLegRot = rightLegRot.unaryMinus() - } - // Interpolate between the pelvis, averaged from the legs, and the chest waistRot = waistRot.interpQ(leftLegRot.lerpQ(rightLegRot, 0.5f), hipFromWaistLegsAveraging).unit() @@ -849,15 +826,6 @@ class HumanSkeleton( var rightLegRot = rightUpperLegTracker?.getRotation() ?: IDENTITY var chestRot = it.getRotation() - // Get the rotation relative to where we expect the upper legs to be - val expectedUpperLegsRot = chestRot.times(FORWARD_QUATERNION) - if (expectedUpperLegsRot.dot(leftLegRot) < 0.0f) { - leftLegRot = leftLegRot.unaryMinus() - } - if (expectedUpperLegsRot.dot(rightLegRot) < 0.0f) { - rightLegRot = rightLegRot.unaryMinus() - } - // Interpolate between the pelvis, averaged from the legs, and the chest chestRot = chestRot.interpQ(leftLegRot.lerpQ(rightLegRot, 0.5f), hipFromChestLegsAveraging).unit() @@ -1110,24 +1078,11 @@ class HumanSkeleton( rightKnee: Quaternion, hip: Quaternion, ): Quaternion { - // Get the knees' rotation relative to where we expect them to be. - // The angle between your knees and hip can be over 180 degrees... - var leftKneeRot = leftKnee - var rightKneeRot = rightKnee - - val kneeRot = hip.times(FORWARD_QUATERNION) - if (kneeRot.dot(leftKneeRot) < 0.0f) { - leftKneeRot = leftKneeRot.unaryMinus() - } - if (kneeRot.dot(rightKneeRot) < 0.0f) { - rightKneeRot = rightKneeRot.unaryMinus() - } - // R = InverseHip * (LeftLeft + RightLeg) // C = Quaternion(R.w, -R.x, 0, 0) // Pelvis = Hip * R * C // normalize(Pelvis) - val r = hip.inv() * (leftKneeRot + rightKneeRot) + val r = hip.inv() * (leftKnee + rightKnee) val c = Quaternion(r.w, -r.x, 0f, 0f) return (hip * r * c).unit() } From dfeb4e79a41eb0a57d3aa8eb4df1647bc0d92665 Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Thu, 2 Jan 2025 17:28:31 -0500 Subject: [PATCH 03/16] AutoBone bone contribution fix & cleanup (#1249) --- .../java/dev/slimevr/autobone/AutoBone.kt | 131 ++++++++++++------ .../slimevr/autobone/errors/PositionError.kt | 10 +- .../autobone/errors/PositionOffsetError.kt | 16 ++- .../java/dev/slimevr/config/AutoBoneConfig.kt | 8 +- .../config/CurrentVRConfigConverter.java | 26 ++++ .../main/java/dev/slimevr/config/VRConfig.kt | 4 +- 6 files changed, 137 insertions(+), 58 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt index d8f4f2868b..faf714f2d2 100644 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt +++ b/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt @@ -6,6 +6,7 @@ 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 @@ -94,38 +95,73 @@ class AutoBone(server: VRServer) { } } - fun getBoneDirection( + /** + * Computes the local tail position of the bone after rotation. + */ + fun getBoneLocalTail( skeleton: HumanPoseManager, - configOffset: SkeletonConfigOffsets, - rightSide: Boolean, + boneType: BoneType, ): Vector3 { - // 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] - } - return skeleton.getBone(boneType).getGlobalRotation().toRotationVector() + val bone = skeleton.getBone(boneType) + return bone.getTailPosition() - bone.getPosition() + } + + /** + * Computes the direction of the bone tail's movement between skeletons 1 and 2. + */ + fun getBoneLocalTailDir( + skeleton1: HumanPoseManager, + skeleton2: HumanPoseManager, + boneType: BoneType, + ): Vector3? { + val boneOff = getBoneLocalTail(skeleton2, boneType) - getBoneLocalTail(skeleton1, boneType) + val boneOffLen = boneOff.len() + return if (boneOffLen > MIN_SLIDE_DIST) boneOff / boneOffLen else null } - fun getDotProductDiff( + /** + * Predicts how much the provided config should be affecting the slide offsets + * of the left and right ankles. + */ + fun getSlideDot( skeleton1: HumanPoseManager, skeleton2: HumanPoseManager, - configOffset: SkeletonConfigOffsets, - rightSide: Boolean, - offset: Vector3, + config: SkeletonConfigOffsets, + slideL: Vector3?, + slideR: Vector3?, ): Float { - val normalizedOffset = offset.unit() - val dot1 = normalizedOffset.dot(getBoneDirection(skeleton1, configOffset, rightSide)) - val dot2 = normalizedOffset.dot(getBoneDirection(skeleton2, configOffset, rightSide)) - return dot2 - dot1 + var slideDot = 0f + // Used for right offset if not a symmetric bone + var boneOffL: Vector3? = null + + if (slideL != null) { + boneOffL = getBoneLocalTailDir(skeleton1, skeleton2, config.affectedOffsets[0]) + + if (boneOffL != null) { + slideDot += slideL.dot(boneOffL) + } + } + + if (slideR != null) { + // 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 boneOffR = if (SYMM_CONFIGS.contains(config)) { + getBoneLocalTailDir(skeleton1, skeleton2, config.affectedOffsets[1]) + } else if (slideL != null) { + // Use cached offset if slideL was used + boneOffL + } else { + // Compute offset if missing because of slideL + getBoneLocalTailDir(skeleton1, skeleton2, config.affectedOffsets[0]) + } + + if (boneOffR != null) { + slideDot += slideR.dot(boneOffR) + } + } + + return slideDot / 2f } fun applyConfig( @@ -488,13 +524,15 @@ class AutoBone(server: VRServer) { return } - val slideLeft = skeleton2 - .getComputedTracker(TrackerRole.LEFT_FOOT).position - + val slideL = skeleton2.getComputedTracker(TrackerRole.LEFT_FOOT).position - skeleton1.getComputedTracker(TrackerRole.LEFT_FOOT).position + val slideLLen = slideL.len() + val slideLUnit: Vector3? = if (slideLLen > MIN_SLIDE_DIST) slideL / slideLLen else null - val slideRight = skeleton2 - .getComputedTracker(TrackerRole.RIGHT_FOOT).position - + val slideR = skeleton2.getComputedTracker(TrackerRole.RIGHT_FOOT).position - skeleton1.getComputedTracker(TrackerRole.RIGHT_FOOT).position + val slideRLen = slideR.len() + val slideRUnit: Vector3? = if (slideRLen > MIN_SLIDE_DIST) slideR / slideRLen else null val intermediateOffsets = EnumMap(offsets) for (entry in intermediateOffsets.entries) { @@ -505,28 +543,23 @@ class AutoBone(server: VRServer) { } val originalLength = entry.value - val leftDotProduct = getDotProductDiff( - skeleton1, - skeleton2, - entry.key, - false, - slideLeft, - ) - val rightDotProduct = getDotProductDiff( + // Calculate the total effect of the bone based on change in rotation + val slideDot = getSlideDot( skeleton1, skeleton2, entry.key, - true, - slideRight, + slideLUnit, + slideRUnit, ) - - // Calculate the total effect of the bone based on change in rotation - val dotLength = originalLength * ((leftDotProduct + rightDotProduct) / 2f) + val dotLength = originalLength * slideDot // Scale by the total effect of the bone val curAdjustVal = adjustVal * -dotLength - val newLength = originalLength + curAdjustVal + if (curAdjustVal == 0f) { + continue + } + val newLength = originalLength + curAdjustVal // No small or negative numbers!!! Bad algorithm! if (newLength < 0.01f) { continue @@ -754,6 +787,7 @@ class AutoBone(server: VRServer) { companion object { const val MIN_HEIGHT = 0.4f + const val MIN_SLIDE_DIST = 0.002f const val AUTOBONE_FOLDER = "AutoBone Recordings" const val LOADAUTOBONE_FOLDER = "Load AutoBone Recordings" @@ -773,5 +807,16 @@ class AutoBone(server: VRServer) { private fun errorFunc(errorDeriv: Float): Float = 0.5f * (errorDeriv * errorDeriv) private fun decayFunc(initialAdjustRate: Float, adjustRateDecay: Float, epoch: Int): Float = if (epoch >= 0) initialAdjustRate / (1 + (adjustRateDecay * epoch)) else 0.0f + + private val SYMM_CONFIGS = arrayOf( + SkeletonConfigOffsets.HIPS_WIDTH, + SkeletonConfigOffsets.SHOULDERS_WIDTH, + SkeletonConfigOffsets.SHOULDERS_DISTANCE, + SkeletonConfigOffsets.UPPER_ARM, + SkeletonConfigOffsets.LOWER_ARM, + SkeletonConfigOffsets.UPPER_LEG, + SkeletonConfigOffsets.LOWER_LEG, + SkeletonConfigOffsets.FOOT_LENGTH, + ) } } diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/PositionError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/PositionError.kt index 498379875f..f438f1bf2e 100644 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/PositionError.kt +++ b/server/core/src/main/java/dev/slimevr/autobone/errors/PositionError.kt @@ -39,10 +39,14 @@ class PositionError : IAutoBoneError { val position = trackerFrame.tryGetPosition() ?: continue val trackerRole = trackerFrame.tryGetTrackerPosition()?.trackerRole ?: continue - val computedTracker = skeleton.getComputedTracker(trackerRole) ?: continue + try { + val computedTracker = skeleton.getComputedTracker(trackerRole) - offset += (position - computedTracker.position).len() - offsetCount++ + offset += (position - computedTracker.position).len() + offsetCount++ + } catch (_: Exception) { + // Ignore unsupported positions + } } return if (offsetCount > 0) offset / offsetCount else 0f } diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/PositionOffsetError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/PositionOffsetError.kt index d514b517ee..953c022c81 100644 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/PositionOffsetError.kt +++ b/server/core/src/main/java/dev/slimevr/autobone/errors/PositionOffsetError.kt @@ -37,13 +37,17 @@ class PositionOffsetError : IAutoBoneError { val position2 = trackerFrame2.tryGetPosition() ?: continue val trackerRole2 = trackerFrame2.tryGetTrackerPosition()?.trackerRole ?: continue - val computedTracker1 = skeleton1.getComputedTracker(trackerRole1) ?: continue - val computedTracker2 = skeleton2.getComputedTracker(trackerRole2) ?: continue + try { + val computedTracker1 = skeleton1.getComputedTracker(trackerRole1) + val computedTracker2 = skeleton2.getComputedTracker(trackerRole2) - val dist1 = (position1 - computedTracker1.position).len() - val dist2 = (position2 - computedTracker2.position).len() - offset += abs(dist2 - dist1) - offsetCount++ + val dist1 = (position1 - computedTracker1.position).len() + val dist2 = (position2 - computedTracker2.position).len() + offset += abs(dist2 - dist1) + offsetCount++ + } catch (_: Exception) { + // Ignore unsupported positions + } } return if (offsetCount > 0) offset / offsetCount else 0f } diff --git a/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt b/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt index 7c6c136720..3c7cf20d94 100644 --- a/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt @@ -4,14 +4,14 @@ class AutoBoneConfig { var cursorIncrement = 2 var minDataDistance = 1 var maxDataDistance = 1 - var numEpochs = 100 + var numEpochs = 50 var printEveryNumEpochs = 25 var initialAdjustRate = 10.0f var adjustRateDecay = 1.0f - var slideErrorFactor = 0.0f - var offsetSlideErrorFactor = 1.0f + var slideErrorFactor = 1.0f + var offsetSlideErrorFactor = 0.0f var footHeightOffsetErrorFactor = 0.0f - var bodyProportionErrorFactor = 0.25f + var bodyProportionErrorFactor = 0.05f var heightErrorFactor = 0.0f var positionErrorFactor = 0.0f var positionOffsetErrorFactor = 0.0f diff --git a/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java b/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java index 479df1f6da..ce6e436b2d 100644 --- a/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java +++ b/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java @@ -304,6 +304,32 @@ public ObjectNode convert( } } } + + if (version < 14) { + // Update AutoBone defaults + ObjectNode autoBoneNode = (ObjectNode) modelData.get("autoBone"); + if (autoBoneNode != null) { + JsonNode offsetSlideNode = autoBoneNode.get("offsetSlideErrorFactor"); + JsonNode slideNode = autoBoneNode.get("slideErrorFactor"); + if ( + offsetSlideNode != null + && slideNode != null + && offsetSlideNode.floatValue() == 1.0f + && slideNode.floatValue() == 0.0f + ) { + autoBoneNode.set("offsetSlideErrorFactor", new FloatNode(0.0f)); + autoBoneNode.set("slideErrorFactor", new FloatNode(1.0f)); + } + JsonNode bodyProportionsNode = autoBoneNode.get("bodyProportionErrorFactor"); + if (bodyProportionsNode != null && bodyProportionsNode.floatValue() == 0.25f) { + autoBoneNode.set("bodyProportionErrorFactor", new FloatNode(0.05f)); + } + JsonNode numEpochsNode = autoBoneNode.get("numEpochs"); + if (numEpochsNode != null && numEpochsNode.intValue() == 100) { + autoBoneNode.set("numEpochs", new IntNode(50)); + } + } + } } catch (Exception e) { LogManager.severe("Error during config migration: " + e); } diff --git a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt index 873ce37e61..65487f07de 100644 --- a/server/core/src/main/java/dev/slimevr/config/VRConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/VRConfig.kt @@ -10,8 +10,8 @@ import dev.slimevr.tracking.trackers.Tracker import dev.slimevr.tracking.trackers.TrackerRole @JsonVersionedModel( - currentVersion = "13", - defaultDeserializeToVersion = "13", + currentVersion = "14", + defaultDeserializeToVersion = "14", toCurrentConverterClass = CurrentVRConfigConverter::class, ) class VRConfig { From 3614612ac2ba65d9e4f776e005fdea3f0d8cfb8a Mon Sep 17 00:00:00 2001 From: Uriel Date: Thu, 2 Jan 2025 23:29:21 +0100 Subject: [PATCH 04/16] Fix CI bugs that appeared recently (#1272) --- .github/workflows/gradle.yaml | 2 +- flake.nix | 2 +- gui/.gitignore | 1 + gui/vite.config.ts | 4 +++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gradle.yaml b/.github/workflows/gradle.yaml index 3e90c7d18e..9d1b711612 100644 --- a/.github/workflows/gradle.yaml +++ b/.github/workflows/gradle.yaml @@ -285,7 +285,7 @@ jobs: ./bundle_dmg.sh --volname SlimeVR --icon slimevr 180 170 --app-drop-link 480 170 \ --window-size 660 400 --hide-extension ../macos/SlimeVR.app \ --volicon ../macos/SlimeVR.app/Contents/Resources/icon.icns --skip-jenkins \ - --eula ../../../../LICENSE-MIT slimevr.dmg ../macos/SlimeVR.app + --eula ../../../../../LICENSE-MIT slimevr.dmg ../macos/SlimeVR.app - uses: actions/upload-artifact@v4 with: diff --git a/flake.nix b/flake.nix index 809182a6c7..2bdecbac9d 100644 --- a/flake.nix +++ b/flake.nix @@ -92,7 +92,7 @@ harfbuzz libffi libsoup_3 - openssl + openssl.dev pango pkg-config treefmt diff --git a/gui/.gitignore b/gui/.gitignore index a1ac58f664..90069ef5a6 100644 --- a/gui/.gitignore +++ b/gui/.gitignore @@ -28,6 +28,7 @@ yarn-error.log* # vite /dist /stats.html +vite.config.ts.timestamp* # eslint .eslintcache diff --git a/gui/vite.config.ts b/gui/vite.config.ts index 0d17119671..a61b5476e1 100644 --- a/gui/vite.config.ts +++ b/gui/vite.config.ts @@ -10,7 +10,9 @@ const versionTag = execSync('git --no-pager tag --sort -taggerdate --points-at H .split('\n')[0] .trim(); // If not empty then it's not clean -const gitClean = execSync('git status --porcelain').toString() ? false : true; +const gitCleanString = execSync('git status --porcelain').toString(); +const gitClean = gitCleanString ? false : true; +if (!gitClean) console.log('Git is dirty because of:\n' + gitCleanString); console.log(`version is ${versionTag || commitHash}${gitClean ? '' : '-dirty'}`); From 2ff6e99385bb76290e52dfc34d90fb1bde104f9f Mon Sep 17 00:00:00 2001 From: JovannMC Date: Mon, 6 Jan 2025 17:16:26 +0300 Subject: [PATCH 05/16] Fix "trackers still on" when closing via tray (#1265) Co-authored-by: Eiren Rain Co-authored-by: lucas lelievre --- dev.slimevr.SlimeVR.metainfo.xml | 1 + gui/src/components/TopBar.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dev.slimevr.SlimeVR.metainfo.xml b/dev.slimevr.SlimeVR.metainfo.xml index 141ea2040c..688c848ad4 100644 --- a/dev.slimevr.SlimeVR.metainfo.xml +++ b/dev.slimevr.SlimeVR.metainfo.xml @@ -65,6 +65,7 @@ work. If not, see . + https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.2 https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1 https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1-rc.3 https://github.com/SlimeVR/SlimeVR-Server/releases/tag/v0.13.1-rc.2 diff --git a/gui/src/components/TopBar.tsx b/gui/src/components/TopBar.tsx index 63973af56a..4e5f76f44d 100644 --- a/gui/src/components/TopBar.tsx +++ b/gui/src/components/TopBar.tsx @@ -290,7 +290,9 @@ export function TopBar({ await invoke('update_tray_text'); } else if ( config?.connectedTrackersWarning && - connectedIMUTrackers.length > 0 + connectedIMUTrackers.filter( + (t) => t.tracker.status !== TrackerStatus.TIMED_OUT + ).length > 0 ) { setConnectedTrackerWarning(true); } else { From 394c1dd43833e6ffc1f65ee4e9efa764f027391c Mon Sep 17 00:00:00 2001 From: Yao Wei Date: Wed, 8 Jan 2025 01:12:34 +0800 Subject: [PATCH 06/16] i18n: improve SlimeVR tracker flashing instruction (#1276) --- gui/public/i18n/en/translation.ftl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index 492a40833b..b9a1816a34 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -1152,9 +1152,7 @@ firmware_tool-flash_method_step-serial = firmware_tool-flashbtn_step = Press the boot btn firmware_tool-flashbtn_step-description = Before going into the next step there is a few things you need to do -firmware_tool-flashbtn_step-board_SLIMEVR = Press the flash button on the pcb before inserting turning on the tracker. - If the tracker was already on, simply turn it off and back on while pressing the button or shorting the flash pads. - Here are a few pictures on how to do it according to the different revisions of the SlimeVR tracker +firmware_tool-flashbtn_step-board_SLIMEVR = Turn off the tracker, remove the case (if any), connect a USB cable to this computer, then do one of the following steps according to your SlimeVR board revision: firmware_tool-flashbtn_step-board_SLIMEVR-r11 = Turn on the tracker while shorting the second rectangular FLASH pad from the edge on the top side of the board, and the metal shield of the microcontroller firmware_tool-flashbtn_step-board_SLIMEVR-r12 = Turn on the tracker while shorting the circular FLASH pad on the top side of the board, and the metal shield of the microcontroller firmware_tool-flashbtn_step-board_SLIMEVR-r14 = Turn on the tracker while pushing in the FLASH button on the top side of the board From 181ba089c27e77f1c1496936d14977c5b2242eee Mon Sep 17 00:00:00 2001 From: Erimel Date: Mon, 13 Jan 2025 17:24:22 -0500 Subject: [PATCH 07/16] Attempt to fix moving average quaternions not resetting properly (#1278) --- .../filtering/QuaternionMovingAverage.kt | 19 +++++++----- .../dev/slimevr/tracking/trackers/Tracker.kt | 10 ++----- .../trackers/TrackerFilteringHandler.kt | 29 +++++++------------ 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt b/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt index f86c4525d8..09517e692b 100644 --- a/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt +++ b/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt @@ -23,7 +23,7 @@ class QuaternionMovingAverage( ) { private var smoothFactor = 0f private var predictFactor = 0f - private lateinit var rotBuffer: CircularArrayList + private var rotBuffer: CircularArrayList? = null private var latestQuaternion = IDENTITY private var smoothingQuaternion = IDENTITY private val fpsTimer = if (VRServer.instanceInitialized) VRServer.instance.fpsTimer else NanoTimer() @@ -57,11 +57,11 @@ class QuaternionMovingAverage( @Synchronized fun update() { if (type == TrackerFilters.PREDICTION) { - if (rotBuffer.size > 0) { + if (rotBuffer!!.size > 0) { var quatBuf = latestQuaternion // Applies the past rotations to the current rotation - rotBuffer.forEach { quatBuf *= it } + rotBuffer?.forEach { quatBuf *= it } // Calculate how much to slerp val amt = predictFactor * fpsTimer.timePerFrame @@ -98,12 +98,12 @@ class QuaternionMovingAverage( @Synchronized fun addQuaternion(q: Quaternion) { if (type == TrackerFilters.PREDICTION) { - if (rotBuffer.size == rotBuffer.capacity()) { - rotBuffer.removeLast() + if (rotBuffer!!.size == rotBuffer!!.capacity()) { + rotBuffer?.removeLast() } // Gets and stores the rotation between the last 2 quaternions - rotBuffer.add(latestQuaternion.inv().times(q)) + rotBuffer?.add(latestQuaternion.inv().times(q)) } else if (type == TrackerFilters.SMOOTHING) { frameCounter = 0 lastAmt = 0f @@ -116,8 +116,11 @@ class QuaternionMovingAverage( } fun resetQuats(q: Quaternion) { + if (type == TrackerFilters.PREDICTION) { + rotBuffer?.clear() + latestQuaternion = q + } filteredQuaternion = q - latestQuaternion = q - smoothingQuaternion = q + addQuaternion(q) } } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt index d50c091b83..16ff5193dc 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/Tracker.kt @@ -329,13 +329,7 @@ class Tracker @JvmOverloads constructor( } private fun getFilteredRotation(): Quaternion = if (trackRotDirection) { - if (filteringHandler.filteringEnabled) { - // Get filtered rotation - filteringHandler.getFilteredRotation() - } else { - // Get unfiltered rotation - filteringHandler.getTrackedRotation() - } + filteringHandler.getFilteredRotation() } else { // Get raw rotation _rotation @@ -433,6 +427,6 @@ class Tracker @JvmOverloads constructor( * Call when doing a full reset to reset the tracking of rotations >180 degrees */ fun resetFilteringQuats() { - filteringHandler.resetQuats(_rotation) + filteringHandler.resetMovingAverage(_rotation) } } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt index e00f06b8a4..87a6e0a805 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt @@ -11,9 +11,8 @@ import io.github.axisangles.ktmath.Quaternion * See QuaternionMovingAverage.kt for the quaternion math. */ class TrackerFilteringHandler { - - private var filteringMovingAverage: QuaternionMovingAverage? = null - private var trackingMovingAverage = QuaternionMovingAverage(TrackerFilters.NONE) + // Instantiated by default in case config doesn't get read (if tracker doesn't support filtering) + private var movingAverage = QuaternionMovingAverage(TrackerFilters.NONE) var filteringEnabled = false /** @@ -22,14 +21,14 @@ class TrackerFilteringHandler { fun readFilteringConfig(config: FiltersConfig, currentRawRotation: Quaternion) { val type = TrackerFilters.getByConfigkey(config.type) if (type == TrackerFilters.SMOOTHING || type == TrackerFilters.PREDICTION) { - filteringMovingAverage = QuaternionMovingAverage( + movingAverage = QuaternionMovingAverage( type, config.amount, currentRawRotation, ) filteringEnabled = true } else { - filteringMovingAverage = null + movingAverage = QuaternionMovingAverage(TrackerFilters.NONE) filteringEnabled = false } } @@ -38,33 +37,25 @@ class TrackerFilteringHandler { * Update the moving average to make it smooth */ fun update() { - trackingMovingAverage.update() - filteringMovingAverage?.update() + movingAverage.update() } /** * Updates the latest rotation */ fun dataTick(currentRawRotation: Quaternion) { - trackingMovingAverage.addQuaternion(currentRawRotation) - filteringMovingAverage?.addQuaternion(currentRawRotation) + movingAverage.addQuaternion(currentRawRotation) } /** * Call when doing a full reset to reset the tracking of rotations >180 degrees */ - fun resetQuats(currentRawRotation: Quaternion) { - trackingMovingAverage.resetQuats(currentRawRotation) - filteringMovingAverage?.resetQuats(currentRawRotation) + fun resetMovingAverage(currentRawRotation: Quaternion) { + movingAverage.resetQuats(currentRawRotation) } /** - * Gets the tracked rotation from the moving average (allows >180 degrees) - */ - fun getTrackedRotation() = trackingMovingAverage.filteredQuaternion - - /** - * Get the filtered rotation from the moving average + * Get the filtered rotation from the moving average (either prediction/smoothing or just >180 degs) */ - fun getFilteredRotation() = filteringMovingAverage?.filteredQuaternion ?: Quaternion.IDENTITY + fun getFilteredRotation() = movingAverage.filteredQuaternion } From b4df1d1444e14fe6c2f0536149439b5d6dcc5c65 Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Wed, 22 Jan 2025 12:30:28 -0500 Subject: [PATCH 08/16] Improve log file behaviour (#1262) --- .../io/eiren/util/logging/FileLogHandler.java | 265 ++++++++++++++++++ .../io/eiren/util/logging/LogManager.java | 16 +- 2 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 server/core/src/main/java/io/eiren/util/logging/FileLogHandler.java diff --git a/server/core/src/main/java/io/eiren/util/logging/FileLogHandler.java b/server/core/src/main/java/io/eiren/util/logging/FileLogHandler.java new file mode 100644 index 0000000000..eee14f4e28 --- /dev/null +++ b/server/core/src/main/java/io/eiren/util/logging/FileLogHandler.java @@ -0,0 +1,265 @@ +package io.eiren.util.logging; + +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.logging.ErrorManager; +import java.util.logging.LogRecord; +import java.util.logging.StreamHandler; + + +public class FileLogHandler extends StreamHandler { + + protected class DatedLogFile implements Comparable { + public final File file; + public final LocalDateTime dateTime; + public final int count; + + protected DatedLogFile(File file, LocalDateTime dateTime, int count) { + this.file = file; + this.dateTime = dateTime; + this.count = count; + } + + @Override + public int compareTo(@NotNull DatedLogFile o) { + int dtCompare = dateTime.compareTo(o.dateTime); + return dtCompare != 0 ? dtCompare : Integer.compare(count, o.count); + } + } + + private final char sectionSeparator = '_'; + private final String logSuffix = ".log"; + + private final ArrayList logFiles; + + private final Path path; + private final String logTag; + private final DateTimeFormatter dateFormat; + private final LocalDateTime dateTime; + private final String date; + private final int limit; + private final int maxCount; + private final long collectiveLimit; + + private DataOutputStream curStream; + private int fileCount = 0; + private long collectiveSize = 0; + + public FileLogHandler( + @NotNull Path path, + @NotNull String logTag, + @NotNull DateTimeFormatter dateFormat, + int limit, + int count + ) { + this(path, logTag, dateFormat, limit, count, -1); + } + + public FileLogHandler( + @NotNull Path path, + @NotNull String logTag, + @NotNull DateTimeFormatter dateFormat, + int limit, + int count, + long collectiveLimit + ) { + this.path = path; + this.logTag = logTag; + + this.dateFormat = dateFormat; + this.dateTime = LocalDateTime.now(); + this.date = dateTime.format(dateFormat); + + this.limit = limit; + this.maxCount = count; + this.collectiveLimit = collectiveLimit; + + // Find old logs to manage + logFiles = findLogs(path); + if (collectiveLimit > 0) { + collectiveSize = sumFileSizes(logFiles); + } + + // Create new log and delete over the count + newFile(); + } + + private DatedLogFile parseFileName(File file) { + String name = file.getName(); + + // Log name should have at least two separators, one integer, and at + // least one char for the datetime (4 chars) + if ( + !name.startsWith(logTag) + || !name.endsWith(logSuffix) + || name.length() < (logTag.length() + logSuffix.length() + 4) + ) { + // Ignore non-matching files + return null; + } + + int dateEnd = name.lastIndexOf(sectionSeparator); + if (dateEnd < 0) { + // Ignore non-matching files + return null; + } + + try { + // Move past the tag, then between the two separators + String dateTimeStr = name.substring(logTag.length() + 1, dateEnd); + LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr, dateFormat); + + // Move past the date separator and behind the suffix + int logNum = Integer + .parseInt(name, dateEnd + 1, name.length() - logSuffix.length(), 10); + + return new DatedLogFile(file, dateTime, logNum); + } catch (Exception e) { + // Unable to parse log file, probably not valid + return null; + } + } + + private ArrayList findLogs(Path path) { + ArrayList logFiles = new ArrayList<>(); + + File[] files = path.toFile().listFiles(); + if (files == null) + return logFiles; + + // Find all parseable log files + for (File log : files) { + DatedLogFile parsedFile = parseFileName(log); + if (parsedFile != null) { + logFiles.add(parsedFile); + } + } + + return logFiles; + } + + private long sumFileSizes(ArrayList logFiles) { + long size = 0; + for (DatedLogFile log : logFiles) { + size += log.file.length(); + } + return size; + } + + private void deleteFile(File file) { + if (!file.delete()) { + file.deleteOnExit(); + reportError( + "Failed to delete file, deleting on exit.", + null, + ErrorManager.GENERIC_FAILURE + ); + } + } + + private DatedLogFile getEarliestFile(ArrayList logFiles) { + DatedLogFile earliest = null; + + for (DatedLogFile log : logFiles) { + if (earliest == null || log.compareTo(earliest) < 0) { + earliest = log; + } + } + + return earliest; + } + + private synchronized void deleteEarliestFile() { + DatedLogFile earliest = getEarliestFile(logFiles); + if (earliest != null) { + // If we have a collective limit, update the current size and clamp + if (collectiveLimit > 0) { + collectiveSize -= earliest.file.length(); + if (collectiveSize < 0) + collectiveSize = 0; + } + + logFiles.remove(earliest); + deleteFile(earliest.file); + } + } + + private synchronized void newFile() { + // Clear the last log file + if (curStream != null) { + collectiveSize += curStream.size(); + close(); + } + + if (maxCount > 0) { + // Delete files over the count + while (logFiles.size() >= maxCount) { + deleteEarliestFile(); + } + } + + if (collectiveLimit > 0) { + // Delete files over the collective size limit + while (!logFiles.isEmpty() && collectiveSize >= collectiveLimit) { + deleteEarliestFile(); + } + } + + try { + Path logPath = path + .resolve( + logTag + + sectionSeparator + + date + + sectionSeparator + + fileCount + + logSuffix + ); + File newFile = logPath.toFile(); + + // Use DataOutputStream to count bytes written + curStream = new DataOutputStream( + new BufferedOutputStream(new FileOutputStream(newFile)) + ); + // Closes the last stream automatically if not already done + setOutputStream(curStream); + + // Add log to the tracking list to be deleted if needed + logFiles.add(new DatedLogFile(newFile, dateTime, fileCount)); + fileCount += 1; + } catch (FileNotFoundException e) { + reportError(null, e, ErrorManager.OPEN_FAILURE); + } + } + + @Override + public synchronized void publish(LogRecord record) { + if (!isLoggable(record)) { + return; + } + + super.publish(record); + flush(); + + if (collectiveLimit > 0) { + // Delete files over the collective size limit + while (!logFiles.isEmpty() && collectiveSize + curStream.size() >= collectiveLimit) { + deleteEarliestFile(); + } + } + + // If written above the log limit, make a new file + if (limit > 0 && curStream.size() >= limit) { + newFile(); + } + } +} diff --git a/server/core/src/main/java/io/eiren/util/logging/LogManager.java b/server/core/src/main/java/io/eiren/util/logging/LogManager.java index 948e21130d..237dd4a4e3 100644 --- a/server/core/src/main/java/io/eiren/util/logging/LogManager.java +++ b/server/core/src/main/java/io/eiren/util/logging/LogManager.java @@ -3,10 +3,9 @@ import java.io.File; import java.io.IOException; import java.io.PrintStream; -import java.nio.file.Paths; +import java.time.format.DateTimeFormatter; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.ConsoleHandler; -import java.util.logging.FileHandler; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; @@ -30,10 +29,15 @@ public static void initialize(File mainLogDir) if (!mainLogDir.exists()) mainLogDir.mkdirs(); - String lastLogPattern = Paths.get(mainLogDir.getPath(), "log_last_%g.log").toString(); - FileHandler filehandler = new FileHandler(lastLogPattern, 25 * 1000000, 2); - filehandler.setFormatter(loc); - global.addHandler(filehandler); + FileLogHandler fileHandler = new FileLogHandler( + mainLogDir.toPath(), + "slimevr-server", + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"), + 25 * 1000000, + 2 + ); + fileHandler.setFormatter(loc); + global.addHandler(fileHandler); } } From fcd82324a968e749cd1db9d1adab6797a3dc0c73 Mon Sep 17 00:00:00 2001 From: rcelyte Date: Wed, 22 Jan 2025 17:53:48 +0000 Subject: [PATCH 09/16] SolarXR IPC Socket (#1247) --- .../src/main/java/dev/slimevr/VRServer.kt | 26 +--- .../websocketapi/WebsocketConnection.java | 2 +- .../src/main/java/dev/slimevr/desktop/Main.kt | 147 +++++++++--------- .../platform/linux/UnixSocketConnection.java | 114 ++++++++++++++ .../platform/linux/UnixSocketRpcBridge.java | 139 +++++++++++++++++ 5 files changed, 334 insertions(+), 94 deletions(-) create mode 100644 server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.java create mode 100644 server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.java diff --git a/server/core/src/main/java/dev/slimevr/VRServer.kt b/server/core/src/main/java/dev/slimevr/VRServer.kt index ab43a62987..23b35939ff 100644 --- a/server/core/src/main/java/dev/slimevr/VRServer.kt +++ b/server/core/src/main/java/dev/slimevr/VRServer.kt @@ -38,16 +38,15 @@ import java.util.concurrent.atomic.AtomicInteger import java.util.function.Consumer import kotlin.concurrent.schedule -typealias SteamBridgeProvider = ( +typealias BridgeProvider = ( server: VRServer, computedTrackers: List, -) -> ISteamVRBridge? +) -> Sequence const val SLIMEVR_IDENTIFIER = "dev.slimevr.SlimeVR" class VRServer @JvmOverloads constructor( - driverBridgeProvider: SteamBridgeProvider = { _, _ -> null }, - feederBridgeProvider: (VRServer) -> ISteamVRBridge? = { _ -> null }, + bridgeProvider: BridgeProvider = { _, _ -> sequence {} }, serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() }, flashingHandlerProvider: (VRServer) -> SerialFlashingHandler? = { _ -> null }, acquireMulticastLock: () -> Any? = { null }, @@ -135,22 +134,11 @@ class VRServer @JvmOverloads constructor( "Sensors UDP server", ) { tracker: Tracker -> registerTracker(tracker) } - // Start bridges for SteamVR and Feeder - val driverBridge = driverBridgeProvider(this, computedTrackers) - if (driverBridge != null) { - tasks.add(Runnable { driverBridge.startBridge() }) - bridges.add(driverBridge) + // Start bridges and WebSocket server + for (bridge in bridgeProvider(this, computedTrackers) + sequenceOf(WebSocketVRBridge(computedTrackers, this))) { + tasks.add(Runnable { bridge.startBridge() }) + bridges.add(bridge) } - val feederBridge = feederBridgeProvider(this) - if (feederBridge != null) { - tasks.add(Runnable { feederBridge.startBridge() }) - bridges.add(feederBridge) - } - - // Create WebSocket server - val wsBridge = WebSocketVRBridge(computedTrackers, this) - tasks.add(Runnable { wsBridge.startBridge() }) - bridges.add(wsBridge) // Initialize OSC handlers vrcOSCHandler = VRCOSCHandler( diff --git a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java b/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java index 47a297bdc9..1c0adae841 100644 --- a/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java +++ b/server/core/src/main/java/dev/slimevr/websocketapi/WebsocketConnection.java @@ -28,7 +28,7 @@ public ConnectionContext getContext() { @Override public void send(ByteBuffer bytes) { if (this.conn.isOpen()) - this.conn.send(bytes); + this.conn.send(bytes.slice()); } @Override diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt index e1719a53d0..675336e616 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/Main.kt @@ -5,10 +5,11 @@ package dev.slimevr.desktop import dev.slimevr.Keybinding import dev.slimevr.SLIMEVR_IDENTIFIER import dev.slimevr.VRServer -import dev.slimevr.bridge.ISteamVRBridge +import dev.slimevr.bridge.Bridge import dev.slimevr.desktop.firmware.DesktopSerialFlashingHandler import dev.slimevr.desktop.platform.SteamVRBridge import dev.slimevr.desktop.platform.linux.UnixSocketBridge +import dev.slimevr.desktop.platform.linux.UnixSocketRpcBridge import dev.slimevr.desktop.platform.windows.WindowsNamedPipeBridge import dev.slimevr.desktop.serial.DesktopSerialHandler import dev.slimevr.desktop.tracking.trackers.hid.TrackersHID @@ -119,8 +120,7 @@ fun main(args: Array) { val configDir = resolveConfig() LogManager.info("Using config dir: $configDir") val vrServer = VRServer( - ::provideSteamVRBridge, - ::provideFeederBridge, + ::provideBridges, { _ -> DesktopSerialHandler() }, { _ -> DesktopSerialFlashingHandler() }, configPath = configDir, @@ -151,90 +151,89 @@ fun main(args: Array) { } } -fun provideSteamVRBridge( +fun provideBridges( server: VRServer, computedTrackers: List, -): ISteamVRBridge? { - val driverBridge: SteamVRBridge? - if (OperatingSystem.currentPlatform == OperatingSystem.WINDOWS) { - // Create named pipe bridge for SteamVR driver - driverBridge = WindowsNamedPipeBridge( - server, - "steamvr", - "SteamVR Driver Bridge", - """\\.\pipe\SlimeVRDriver""", - computedTrackers, - ) - } else if (OperatingSystem.currentPlatform == OperatingSystem.LINUX) { - var linuxBridge: SteamVRBridge? = null - try { - linuxBridge = UnixSocketBridge( - server, - "steamvr", - "SteamVR Driver Bridge", - Paths.get(OperatingSystem.socketDirectory, "SlimeVRDriver") - .toString(), - computedTrackers, - ) - } catch (ex: Exception) { - LogManager.severe( - "Failed to initiate Unix socket, disabling driver bridge...", - ex, - ) - } - driverBridge = linuxBridge - if (driverBridge != null) { - // Close the named socket on shutdown, or otherwise it's not going to get removed - Runtime.getRuntime().addShutdownHook( - Thread { - try { - (driverBridge as? UnixSocketBridge)?.close() - } catch (e: Exception) { - throw RuntimeException(e) - } - }, - ) - } - } else { - driverBridge = null - } - - return driverBridge -} - -fun provideFeederBridge( - server: VRServer, -): ISteamVRBridge? { - val feederBridge: SteamVRBridge? +): Sequence = sequence { when (OperatingSystem.currentPlatform) { OperatingSystem.WINDOWS -> { + // Create named pipe bridge for SteamVR driver + yield( + WindowsNamedPipeBridge( + server, + "steamvr", + "SteamVR Driver Bridge", + """\\.\pipe\SlimeVRDriver""", + computedTrackers, + ), + ) + // Create named pipe bridge for SteamVR input - feederBridge = WindowsNamedPipeBridge( - server, - "steamvr_feeder", - "SteamVR Feeder Bridge", - """\\.\pipe\SlimeVRInput""", - FastList(), + yield( + WindowsNamedPipeBridge( + server, + "steamvr_feeder", + "SteamVR Feeder Bridge", + """\\.\pipe\SlimeVRInput""", + FastList(), + ), ) } OperatingSystem.LINUX -> { - feederBridge = UnixSocketBridge( - server, - "steamvr_feeder", - "SteamVR Feeder Bridge", - Paths.get(OperatingSystem.socketDirectory, "SlimeVRInput") - .toString(), - FastList(), + var linuxBridge: SteamVRBridge? = null + try { + linuxBridge = UnixSocketBridge( + server, + "steamvr", + "SteamVR Driver Bridge", + Paths.get(OperatingSystem.socketDirectory, "SlimeVRDriver") + .toString(), + computedTrackers, + ) + } catch (ex: Exception) { + LogManager.severe( + "Failed to initiate Unix socket, disabling driver bridge...", + ex, + ) + } + if (linuxBridge != null) { + // Close the named socket on shutdown, or otherwise it's not going to get removed + Runtime.getRuntime().addShutdownHook( + Thread { + try { + (linuxBridge as? UnixSocketBridge)?.close() + } catch (e: Exception) { + throw RuntimeException(e) + } + }, + ) + yield(linuxBridge) + } + + yield( + UnixSocketBridge( + server, + "steamvr_feeder", + "SteamVR Feeder Bridge", + Paths.get(OperatingSystem.socketDirectory, "SlimeVRInput") + .toString(), + FastList(), + ), ) - } - else -> { - feederBridge = null + yield( + UnixSocketRpcBridge( + server, + Paths.get(OperatingSystem.socketDirectory, "SlimeVRRpc") + .toString(), + computedTrackers, + ), + ) } - } - return feederBridge + else -> {} + } } const val CONFIG_FILENAME = "vrconfig.yml" diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.java new file mode 100644 index 0000000000..45ccb81b0b --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketConnection.java @@ -0,0 +1,114 @@ +package dev.slimevr.desktop.platform.linux; + +import dev.slimevr.protocol.ConnectionContext; +import dev.slimevr.protocol.GenericConnection; +import io.eiren.util.logging.LogManager; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.SocketChannel; +import java.util.UUID; + + +public class UnixSocketConnection implements GenericConnection { + public final UUID id; + public final ConnectionContext context; + private final ByteBuffer dst = ByteBuffer.allocate(2048).order(ByteOrder.LITTLE_ENDIAN); + private final SocketChannel channel; + private int remainingBytes; + + public UnixSocketConnection(SocketChannel channel) { + this.id = UUID.randomUUID(); + this.context = new ConnectionContext(); + this.channel = channel; + } + + @Override + public UUID getConnectionId() { + return id; + } + + @Override + public ConnectionContext getContext() { + return this.context; + } + + public boolean isConnected() { + return this.channel.isConnected(); + } + + private void resetChannel() { + try { + this.channel.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void send(ByteBuffer bytes) { + if (!this.channel.isConnected()) + return; + try { + ByteBuffer[] src = new ByteBuffer[] { + ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN), + bytes.slice(), + }; + src[0].putInt(src[1].remaining() + 4); + src[0].flip(); + synchronized (this) { + while (src[1].hasRemaining()) { + this.channel.write(src); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public ByteBuffer read() { + if (dst.position() < 4 || dst.position() < dst.getInt(0)) { + if (!this.channel.isConnected()) + return null; + try { + int result = this.channel.read(dst); + if (result == -1) { + LogManager.info("[SolarXR Bridge] Reached end-of-stream on connection"); + this.resetChannel(); + return null; + } + if (dst.position() < 4) { + return null; + } + } catch (IOException e) { + e.printStackTrace(); + this.resetChannel(); + return null; + } + } + int messageLength = dst.getInt(0); + if (messageLength > 1024) { + LogManager + .severe( + "[SolarXR Bridge] Buffer overflow on socket. Message length: " + messageLength + ); + this.resetChannel(); + return null; + } + if (dst.position() < messageLength) { + return null; + } + remainingBytes = dst.position() - messageLength; + dst.position(4); + dst.limit(messageLength); + return dst; + } + + public void next() { + dst.position(dst.limit()); + dst.limit(dst.limit() + remainingBytes); + dst.compact(); + dst.limit(dst.capacity()); + } +} diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.java b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.java new file mode 100644 index 0000000000..82f2901fa9 --- /dev/null +++ b/server/desktop/src/main/java/dev/slimevr/desktop/platform/linux/UnixSocketRpcBridge.java @@ -0,0 +1,139 @@ +package dev.slimevr.desktop.platform.linux; + +import dev.slimevr.bridge.BridgeThread; +import dev.slimevr.protocol.GenericConnection; +import dev.slimevr.protocol.ProtocolAPI; +import dev.slimevr.tracking.trackers.Tracker; +import dev.slimevr.util.ann.VRServerThread; +import dev.slimevr.VRServer; +import io.eiren.util.logging.LogManager; + +import java.io.File; +import java.io.IOException; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.List; + + +public class UnixSocketRpcBridge implements dev.slimevr.bridge.Bridge, + dev.slimevr.protocol.ProtocolAPIServer, Runnable, AutoCloseable { + private final Thread runnerThread = new Thread(this, "Named socket thread"); + private final String socketPath; + private final ProtocolAPI protocolAPI; + private final ServerSocketChannel socket; + private final Selector selector; + + public UnixSocketRpcBridge( + VRServer server, + String socketPath, + List shareableTrackers + ) { + this.socketPath = socketPath; + this.protocolAPI = server.protocolAPI; + File socketFile = new File(socketPath); + if (socketFile.exists()) + throw new RuntimeException(socketPath + " socket already exists."); + socketFile.deleteOnExit(); + try { + socket = ServerSocketChannel.open(StandardProtocolFamily.UNIX); + selector = Selector.open(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Socket open failed."); + } + + server.protocolAPI.registerAPIServer(this); + } + + @VRServerThread + private void disconnected() { + } + + @Override + @VRServerThread + public void dataRead() { + } + + @Override + @VRServerThread + public void dataWrite() { + } + + @Override + @VRServerThread + public void addSharedTracker(Tracker tracker) { + } + + @Override + @VRServerThread + public void removeSharedTracker(Tracker tracker) { + } + + @Override + @VRServerThread + public void startBridge() { + this.runnerThread.start(); + } + + @Override + @BridgeThread + public void run() { + try { + this.socket.bind(UnixDomainSocketAddress.of(this.socketPath)); + this.socket.configureBlocking(false); + this.socket.register(this.selector, SelectionKey.OP_ACCEPT); + LogManager.info("[SolarXR Bridge] Socket " + this.socketPath + " created"); + while (this.socket.isOpen()) { + this.selector.select(0); + for (SelectionKey key : this.selector.selectedKeys()) { + UnixSocketConnection conn = (UnixSocketConnection) key.attachment(); + if (conn != null) { + for (ByteBuffer message; (message = conn.read()) != null; conn.next()) + this.protocolAPI.onMessage(conn, message); + } else + for (SocketChannel channel; (channel = socket.accept()) != null;) { + channel.configureBlocking(false); + channel + .register( + this.selector, + SelectionKey.OP_READ, + new UnixSocketConnection(channel) + ); + LogManager + .info( + "[SolarXR Bridge] Connected to " + + channel.getRemoteAddress().toString() + ); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void close() throws Exception { + this.socket.close(); + this.selector.close(); + } + + @Override + public boolean isConnected() { + return this.selector.keys().stream().anyMatch(key -> key.attachment() != null); + } + + @Override + public java.util.stream.Stream getAPIConnections() { + return this.selector + .keys() + .stream() + .map(key -> (GenericConnection) key.attachment()) + .filter(conn -> conn != null); + } +} From 2c49d1dc650087dc0bb616c323a31b11419b5bae Mon Sep 17 00:00:00 2001 From: Uriel Date: Thu, 23 Jan 2025 17:06:44 +0100 Subject: [PATCH 10/16] Update apt cache action to fix CI error (#1287) --- .github/workflows/build-gui.yml | 2 +- .github/workflows/gradle.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-gui.yml b/.github/workflows/build-gui.yml index 11aa25aed0..9b8ba45738 100644 --- a/.github/workflows/build-gui.yml +++ b/.github/workflows/build-gui.yml @@ -57,7 +57,7 @@ jobs: - if: matrix.os == 'ubuntu-22.04' name: Set up Linux dependencies - uses: awalsh128/cache-apt-pkgs-action@latest + uses: awalsh128/cache-apt-pkgs-action@v1.4.3 with: packages: libgtk-3-dev webkit2gtk-4.1 libappindicator3-dev librsvg2-dev patchelf # Increment to invalidate the cache diff --git a/.github/workflows/gradle.yaml b/.github/workflows/gradle.yaml index 9d1b711612..3c393d3c6f 100644 --- a/.github/workflows/gradle.yaml +++ b/.github/workflows/gradle.yaml @@ -158,7 +158,7 @@ jobs: path: server/desktop/build/libs/ - name: Set up Linux dependencies - uses: awalsh128/cache-apt-pkgs-action@latest + uses: awalsh128/cache-apt-pkgs-action@v1.4.3 with: packages: | build-essential curl wget file libssl-dev libgtk-3-dev libappindicator3-dev librsvg2-dev From 4ad9d5cfca2de12669e2f5b266403e51ac76857c Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Thu, 23 Jan 2025 11:21:38 -0500 Subject: [PATCH 11/16] Expand skeleton height config (#1156) Co-authored-by: Uriel --- gui/public/i18n/en/translation.ftl | 66 +++++- gui/src/App.tsx | 5 + gui/src/components/commons/NumberSelector.tsx | 54 ++++- .../components/onboarding/StepperSlider.tsx | 30 ++- .../body-proportions/AutomaticProportions.tsx | 99 +++------ .../body-proportions/ProportionsChoose.tsx | 199 ++++++++++++----- .../ProportionsResetModal.tsx | 26 ++- .../body-proportions/ScaledProportions.tsx | 75 +++++++ .../autobone-steps/CheckFloorHeight.tsx | 207 ++++++++++++++++++ .../autobone-steps/CheckHeight.tsx | 194 ++++++++-------- .../autobone-steps/Preparation.tsx | 2 +- .../autobone-steps/TooSmolModal.tsx | 73 ++++++ .../body-proportions/scaled-steps/Done.tsx | 30 +++ .../scaled-steps/ManualHeightStep.tsx | 158 +++++++++++++ .../scaled-steps/ResetProportions.tsx | 62 ++++++ gui/src/hooks/height.ts | 58 +++++ .../java/dev/slimevr/autobone/AutoBone.kt | 22 +- .../java/dev/slimevr/autobone/AutoBoneStep.kt | 4 - .../autobone/errors/BodyProportionError.kt | 73 ++---- .../java/dev/slimevr/config/AutoBoneConfig.kt | 2 - .../config/CurrentVRConfigConverter.java | 9 + .../dev/slimevr/config/SkeletonConfig.java | 25 +++ .../dev/slimevr/protocol/rpc/RPCHandler.kt | 25 ++- .../rpc/settings/RPCSettingsBuilder.java | 34 +-- .../rpc/settings/RPCSettingsHandler.kt | 5 + .../processor/config/SkeletonConfigManager.kt | 15 +- solarxr-protocol | 2 +- 27 files changed, 1215 insertions(+), 339 deletions(-) create mode 100644 gui/src/components/onboarding/pages/body-proportions/ScaledProportions.tsx create mode 100644 gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckFloorHeight.tsx create mode 100644 gui/src/components/onboarding/pages/body-proportions/autobone-steps/TooSmolModal.tsx create mode 100644 gui/src/components/onboarding/pages/body-proportions/scaled-steps/Done.tsx create mode 100644 gui/src/components/onboarding/pages/body-proportions/scaled-steps/ManualHeightStep.tsx create mode 100644 gui/src/components/onboarding/pages/body-proportions/scaled-steps/ResetProportions.tsx create mode 100644 gui/src/hooks/height.ts diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index b9a1816a34..c86ac7e403 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -122,6 +122,10 @@ reset-reset_all_warning = Are you sure you want to do this? reset-reset_all_warning-reset = Reset proportions reset-reset_all_warning-cancel = Cancel +reset-reset_all_warning_default = + Warning: You currently don't have your height defined, which + will make the proportions be based on a default height. + Are you sure you want to do this? reset-full = Full Reset reset-mounting = Reset Mounting @@ -962,6 +966,15 @@ onboarding-choose_proportions-manual_proportions = Manual proportions # Italicized text 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-scaled_proportions = Scaled proportions +# Italized text +onboarding-choose_proportions-scaled_proportions-subtitle = Recommended for new users +# Multiline string +onboarding-choose_proportions-scaled_proportions-description = + This will scale the proportions of an average human body based on your height, this will help for basic full-body tracking. + + This requires having your headset (HMD) connected to SlimeVR and on your head! +onboarding-choose_proportions-scaled_proportions-button = Scaled proportions onboarding-choose_proportions-export = Export proportions onboarding-choose_proportions-import = Import proportions onboarding-choose_proportions-import-success = Imported @@ -981,9 +994,11 @@ onboarding-automatic_proportions-title = Measure your body onboarding-automatic_proportions-description = For SlimeVR trackers to work, we need to know the length of your bones. This short calibration will measure it for you. onboarding-automatic_proportions-manual = Manual proportions onboarding-automatic_proportions-prev_step = Previous step + onboarding-automatic_proportions-put_trackers_on-title = Put on your trackers onboarding-automatic_proportions-put_trackers_on-description = To calibrate your proportions, we're gonna use the trackers you just assigned. Put on all your trackers, you can see which are which in the figure to the right. onboarding-automatic_proportions-put_trackers_on-next = I have all my trackers on + onboarding-automatic_proportions-requirements-title = Requirements # Each line of text is a different list item onboarding-automatic_proportions-requirements-descriptionv2 = @@ -993,23 +1008,38 @@ onboarding-automatic_proportions-requirements-descriptionv2 = 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). Your tracking is working and is accurately representing your movements (ex. you have performed a full reset and they move the right direction when kicking, bending over, sitting, etc). 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 headset's (HMD) height as an approximation of your actual height, but it's better to check if they are right yourself! + +onboarding-automatic_proportions-check_height-title-v2 = Measure your height +onboarding-automatic_proportions-check_height-description-v2 = Your headset (HMD) height should be slightly less than your full height, as headsets measure your eye height. This measurement will be used as a baseline for your body proportions. # All the text is in bold! -onboarding-automatic_proportions-check_height-calculation_warning = Please press the button while standing upright to calculate your height. You have 3 seconds after you press the button! +onboarding-automatic_proportions-check_height-calculation_warning-v2 = Start measuring while standing upright to calculate your height. Be careful to not raise your hands higher than your headset, as they may affect the measurement! onboarding-automatic_proportions-check_height-guardian_tip = If you are using a standalone VR headset, make sure to have your guardian / boundary turned on so that your height is correct! -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-hmd_height1 = Your HMD height is +onboarding-automatic_proportions-check_height-hmd_height2 = Your headset height is: +onboarding-automatic_proportions-check_height-measure-start = Start measuring +onboarding-automatic_proportions-check_height-measure-stop = Stop measuring +onboarding-automatic_proportions-check_height-measure-reset = Retry measuring +onboarding-automatic_proportions-check_height-next_step = Use headset height + +onboarding-automatic_proportions-check_floor_height-title = Measure your floor height (optional) +onboarding-automatic_proportions-check_floor_height-description = In some cases, your floor height may not be set correctly by your headset, causing the headset height to be measured as higher than it should be. You can measure the "height" of your floor to correct your headset height. +# All the text is in bold! +onboarding-automatic_proportions-check_floor_height-calculation_warning = If you are sure that your floor height is correct, you can skip this step. # Shows an element below it -onboarding-automatic_proportions-check_height-height1 = so your actual height is -onboarding-automatic_proportions-check_height-next_step = They are fine +onboarding-automatic_proportions-check_floor_height-floor_height = Your floor height is: +onboarding-automatic_proportions-check_floor_height-measure-start = Start measuring +onboarding-automatic_proportions-check_floor_height-measure-stop = Stop measuring +onboarding-automatic_proportions-check_floor_height-measure-reset = Retry measuring +onboarding-automatic_proportions-check_floor_height-skip_step = Skip step and save +onboarding-automatic_proportions-check_floor_height-next_step = Use floor height and save + 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 + onboarding-automatic_proportions-recording-title = REC onboarding-automatic_proportions-recording-description-p0 = Recording in progress... onboarding-automatic_proportions-recording-description-p1 = Make the moves shown below: @@ -1027,12 +1057,14 @@ onboarding-automatic_proportions-recording-timer = { $time -> [one] 1 second left *[other] { $time } seconds left } + onboarding-automatic_proportions-verify_results-title = Verify results onboarding-automatic_proportions-verify_results-description = Check the results below, do they look correct? onboarding-automatic_proportions-verify_results-results = Recording results onboarding-automatic_proportions-verify_results-processing = Processing the result 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-v2 = @@ -1041,6 +1073,26 @@ onboarding-automatic_proportions-error_modal-v2 = Please check the docs or join our Discord for help ^_^ onboarding-automatic_proportions-error_modal-confirm = Understood! +onboarding-automatic_proportions-smol_warning = + Your configured height of { $height } is smaller than the minimum accepted height of { $minHeight }. + Please redo the measurements and ensure they are correct. +onboarding-automatic_proportions-smol_warning-cancel = Go back + +## Tracker scaled proportions setup +onboarding-scaled_proportions-title = Scaled proportions +onboarding-scaled_proportions-description = For SlimeVR trackers to work, we need to know the length of your bones. This will use an average proportion and scale it based on your height. +onboarding-scaled_proportions-manual_height-title = Configure your height +onboarding-scaled_proportions-manual_height-description = Your headset (HMD) height should be slightly less than your full height, as headsets measure your eye height. This height will be used as a baseline for your body proportions. +onboarding-scaled_proportions-manual_height-missing_steamvr = SteamVR is not currently connected to SlimeVR, so measurements can't be based on your headset. Proceed at your own risk or check the docs! +onboarding-scaled_proportions-manual_height-height = Your headset height is +onboarding-scaled_proportions-manual_height-next_step = Continue and save + +## Tracker scaled proportions reset +onboarding-scaled_proportions-reset_proportion-title = Reset your body proportions +onboarding-scaled_proportions-reset_proportion-description = To set your body proportions based on your height, you need to now reset all of your proportions. This will clear any proportions you have configured and provide a baseline configuration. +onboarding-scaled_proportions-done-title = Body proportions set +onboarding-scaled_proportions-done-description = Your body proportions should now be configured based on your height. + ## Home home-no_trackers = No trackers detected or assigned diff --git a/gui/src/App.tsx b/gui/src/App.tsx index e2bb4c4a6f..0281f35fac 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -56,6 +56,7 @@ import { AppLayout } from './AppLayout'; import { Preload } from './components/Preload'; import { UnknownDeviceModal } from './components/UnknownDeviceModal'; import { useDiscordPresence } from './hooks/discord-presence'; +import { ScaledProportionsPage } from './components/onboarding/pages/body-proportions/ScaledProportions'; import { EmptyLayout } from './components/EmptyLayout'; import { AdvancedSettings } from './components/settings/pages/AdvancedSettings'; import { FirmwareUpdate } from './components/firmware-update/FirmwareUpdate'; @@ -162,6 +163,10 @@ function Layout() { path="body-proportions/manual" element={} /> + } + /> } /> }> diff --git a/gui/src/components/commons/NumberSelector.tsx b/gui/src/components/commons/NumberSelector.tsx index d2a2da7891..a21d35634a 100644 --- a/gui/src/components/commons/NumberSelector.tsx +++ b/gui/src/components/commons/NumberSelector.tsx @@ -1,6 +1,8 @@ import { Control, Controller } from 'react-hook-form'; import { Button } from './Button'; import { Typography } from './Typography'; +import { useCallback, useMemo } from 'react'; +import { useLocaleConfig } from '@/i18n/config'; export function NumberSelector({ label, @@ -10,7 +12,9 @@ export function NumberSelector({ min, max, step, + doubleStep, disabled = false, + showButtonWithNumber = false, }: { label?: string; valueLabelFormat?: (value: number) => string; @@ -19,14 +23,36 @@ export function NumberSelector({ min: number; max: number; step: number | ((value: number, add: boolean) => number); + doubleStep?: number; disabled?: boolean; + showButtonWithNumber?: boolean; }) { + const { currentLocales } = useLocaleConfig(); + const stepFn = typeof step === 'function' ? step : (value: number, add: boolean) => +(add ? value + step : value - step).toFixed(2); + const doubleStepFn = useCallback( + (value: number, add: boolean) => + doubleStep === undefined + ? 0 + : +(add ? value + doubleStep : value - doubleStep).toFixed(2), + [doubleStep] + ); + + const decimalFormat = useMemo( + () => + new Intl.NumberFormat(currentLocales, { + style: 'decimal', + maximumFractionDigits: 2, + signDisplay: 'exceptZero', + }), + [currentLocales] + ); + return ( {label}
-
+
+ {doubleStep !== undefined && ( + + )}
-
+
+ {doubleStep !== undefined && ( + + )}
diff --git a/gui/src/components/onboarding/StepperSlider.tsx b/gui/src/components/onboarding/StepperSlider.tsx index 8778bfc58b..3f5815af88 100644 --- a/gui/src/components/onboarding/StepperSlider.tsx +++ b/gui/src/components/onboarding/StepperSlider.tsx @@ -94,30 +94,42 @@ export function StepDot({ export function StepperSlider({ variant, steps, + back, + forward, }: { variant: 'alone' | 'onboarding'; steps: Step[]; + /** + * Ran when step is 0 and `prevStep` is executed + */ + back?: () => void; + /** + * Ran when step is `steps.length - 1` and nextStep is executed + */ + forward?: () => void; }) { const ref = useRef(null); const { width } = useElemSize(ref); - const [stepsContainers, setSteps] = useState(0); const [shouldAnimate, setShouldAnimate] = useState(true); const [step, setStep] = useState(0); useEffect(() => { - if (!ref.current) return; - const stepsContainers = - ref.current.getElementsByClassName('step-container'); - setSteps(stepsContainers.length); - }, [ref]); + setStep((x) => Math.min(x, steps.length - 1)); + }, [steps.length]); const nextStep = () => { - if (step + 1 === stepsContainers) return; + if (step + 1 === steps.length) { + forward?.(); + return; + } setStep(step + 1); }; const prevStep = () => { - if (step - 1 < 0) return; + if (step - 1 < 0) { + back?.(); + return; + } setStep(step - 1); }; @@ -168,7 +180,7 @@ export function StepperSlider({
- {Array.from({ length: stepsContainers }).map((_, index) => ( + {Array.from({ length: steps.length }).map((_, index) => (
{index !== 0 && (
diff --git a/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx index b843ba1352..f0527ed82b 100644 --- a/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/AutomaticProportions.tsx @@ -1,9 +1,6 @@ import { useLocalization } from '@fluent/react'; -import { RpcMessage, SkeletonResetAllRequestT } from 'solarxr-protocol'; import { AutoboneContextC, useProvideAutobone } from '@/hooks/autobone'; import { useOnboarding } from '@/hooks/onboarding'; -import { useWebsocketAPI } from '@/hooks/websocket-api'; -import { Button } from '@/components/commons/Button'; import { Typography } from '@/components/commons/Typography'; import { StepperSlider } from '@/components/onboarding/StepperSlider'; import { DoneStep } from './autobone-steps/Done'; @@ -12,83 +9,55 @@ import { PutTrackersOnStep } from './autobone-steps/PutTrackersOn'; 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/CheckHeight'; +import { CheckHeightStep } from './autobone-steps/CheckHeight'; import { PreparationStep } from './autobone-steps/Preparation'; -import { useState } from 'react'; -import { ProportionsResetModal } from './ProportionsResetModal'; +import { HeightContextC, useProvideHeightContext } from '@/hooks/height'; +import { CheckFloorHeightStep } from './autobone-steps/CheckFloorHeight'; export function AutomaticProportionsPage() { const { l10n } = useLocalization(); const { applyProgress, state } = useOnboarding(); - const { sendRPCPacket } = useWebsocketAPI(); const context = useProvideAutobone(); - const { isCounting, startCountdown, timer } = useCountdown({ - onCountdownEnd: () => { - sendRPCPacket( - RpcMessage.SkeletonResetAllRequest, - new SkeletonResetAllRequestT() - ); - }, - }); - - const [showWarning, setShowWarning] = useState(false); + const heightContext = useProvideHeightContext(); applyProgress(0.9); return ( -
-
-
- - {l10n.getString('onboarding-automatic_proportions-title')} - -
- - {l10n.getString('onboarding-automatic_proportions-description')} + +
+
+
+ + {l10n.getString('onboarding-automatic_proportions-title')} -
-
-
- -
-
-
- +
+ +
+
- { - startCountdown(); - setShowWarning(false); - }} - onClose={() => setShowWarning(false)} - isOpen={showWarning} - > -
+ ); } diff --git a/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx b/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx index d142cb78ba..4563903720 100644 --- a/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/ProportionsChoose.tsx @@ -1,6 +1,6 @@ import { useOnboarding } from '@/hooks/onboarding'; import { Localized, useLocalization } from '@fluent/react'; -import { useMemo, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import { Typography } from '@/components/commons/Typography'; import { Button } from '@/components/commons/Button'; @@ -9,6 +9,7 @@ import { SkeletonConfigRequestT, SkeletonBone, ChangeSkeletonConfigRequestT, + SkeletonResetAllRequestT, } from 'solarxr-protocol'; import { useWebsocketAPI } from '@/hooks/websocket-api'; import { save } from '@tauri-apps/plugin-dialog'; @@ -18,6 +19,7 @@ import { useAppContext } from '@/hooks/app'; import { error } from '@/utils/logging'; import { fileOpen, fileSave } from 'browser-fs-access'; import { useDebouncedEffect } from '@/hooks/timeout'; +import { ProportionsResetModal } from './ProportionsResetModal'; export const MIN_HEIGHT = 0.4; export const MAX_HEIGHT = 4; @@ -36,7 +38,9 @@ export function ProportionsChoose() { const { applyProgress, state } = useOnboarding(); const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); const [animated, setAnimated] = useState(false); + const [showProportionWarning, setShowProportionWarning] = useState(false); const [importState, setImportState] = useState(ImportStatus.OK); + const exporting = useRef(false); const { computedTrackers } = useAppContext(); useDebouncedEffect( @@ -78,6 +82,8 @@ export function ProportionsChoose() { useRPCPacket( RpcMessage.SkeletonConfigResponse, (data: SkeletonConfigExport) => { + if (!exporting.current) return; + exporting.current = false; // Convert the skeleton part enums into a string data.skeletonParts.forEach((x) => { if (typeof x.bone === 'number') @@ -151,6 +157,13 @@ export function ProportionsChoose() { setImportState(ImportStatus.SUCCESS); }; + const resetAll = () => { + sendRPCPacket( + RpcMessage.SkeletonResetAllRequest, + new SkeletonResetAllRequestT() + ); + }; + return ( <>
@@ -217,62 +230,122 @@ export function ProportionsChoose() {
-
-
-
- setAnimated(() => true)} - onAnimationEnd={() => setAnimated(() => false)} - src="/images/slimetower.webp" - className={classNames( - 'absolute w-[100px] -right-2 -top-24', - animated && 'animate-[bounce_1s_1]' - )} - > -
- - {l10n.getString( - 'onboarding-choose_proportions-auto_proportions' - )} - - - {l10n.getString( - 'onboarding-choose_proportions-auto_proportions-subtitle' + {state.alonePage && ( +
+
+
+ setAnimated(() => true)} + onAnimationEnd={() => setAnimated(() => false)} + src="/images/slimetower.webp" + className={classNames( + 'absolute w-[100px] -right-2 -top-24', + animated && 'animate-[bounce_1s_1]' )} - -
-
- }} - > - +
+ + {l10n.getString( + 'onboarding-choose_proportions-auto_proportions' + )} + + + {l10n.getString( + 'onboarding-choose_proportions-auto_proportions-subtitle' + )} + +
+
+ }} > - Description for autobone + + Description for autobone + + +
+
+ +
+
+ )} + {!state.alonePage && ( +
+
+
+ setAnimated(() => true)} + onAnimationEnd={() => setAnimated(() => false)} + src="/images/slimetower.webp" + className={classNames( + 'absolute w-[100px] -right-2 -top-24', + animated && 'animate-[bounce_1s_1]' + )} + > +
+ + {l10n.getString( + 'onboarding-choose_proportions-scaled_proportions' + )} - + + {l10n.getString( + 'onboarding-choose_proportions-scaled_proportions-subtitle' + )} + +
+
+ }} + > + + Description for scaled proportions + + +
+
-
-
+ )}
{!state.alonePage && ( @@ -280,15 +353,33 @@ export function ProportionsChoose() { {l10n.getString('onboarding-previous_step')} )} + {state.alonePage && ( + + )} + { + resetAll(); + setShowProportionWarning(false); + }} + onClose={() => setShowProportionWarning(false)} + isOpen={showProportionWarning} + > diff --git a/gui/src/components/onboarding/pages/body-proportions/ProportionsResetModal.tsx b/gui/src/components/onboarding/pages/body-proportions/ProportionsResetModal.tsx index 62839c2a5e..233bdfb147 100644 --- a/gui/src/components/onboarding/pages/body-proportions/ProportionsResetModal.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/ProportionsResetModal.tsx @@ -3,6 +3,13 @@ import { WarningBox } from '@/components/commons/TipBox'; import { Localized, useLocalization } from '@fluent/react'; import { BaseModal } from '@/components/commons/BaseModal'; import ReactModal from 'react-modal'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { useEffect, useState } from 'react'; +import { + RpcMessage, + SettingsRequestT, + SettingsResponseT, +} from 'solarxr-protocol'; export function ProportionsResetModal({ isOpen = true, @@ -24,6 +31,16 @@ export function ProportionsResetModal({ accept: () => void; } & ReactModal.Props) { const { l10n } = useLocalization(); + const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); + const [usingDefaultHeight, setUsingDefaultHeight] = useState(true); + + useEffect( + () => sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT()), + [] + ); + useRPCPacket(RpcMessage.SettingsResponse, (res: SettingsResponseT) => + setUsingDefaultHeight(!res.modelSettings?.skeletonHeight?.hmdHeight) + ); return (
- }}> + }} + > Warning: This will reset your proportions to being just based on your height. diff --git a/gui/src/components/onboarding/pages/body-proportions/ScaledProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/ScaledProportions.tsx new file mode 100644 index 0000000000..d9110a25f4 --- /dev/null +++ b/gui/src/components/onboarding/pages/body-proportions/ScaledProportions.tsx @@ -0,0 +1,75 @@ +import { useLocalization } from '@fluent/react'; +import { useOnboarding } from '@/hooks/onboarding'; +import { Typography } from '@/components/commons/Typography'; +import { StepperSlider } from '@/components/onboarding/StepperSlider'; +import { CheckHeightStep } from './autobone-steps/CheckHeight'; +import { HeightContextC, useProvideHeightContext } from '@/hooks/height'; +import { CheckFloorHeightStep } from './autobone-steps/CheckFloorHeight'; +import { ResetProportionsStep } from './scaled-steps/ResetProportions'; +import { DoneStep } from './scaled-steps/Done'; +import { useNavigate } from 'react-router-dom'; +import { useMemo } from 'react'; +import { ManualHeightStep } from './scaled-steps/ManualHeightStep'; +import { useTrackers } from '@/hooks/tracker'; +import { BodyPart } from 'solarxr-protocol'; + +export function ScaledProportionsPage() { + const { l10n } = useLocalization(); + const { applyProgress, state } = useOnboarding(); + const heightContext = useProvideHeightContext(); + const navigate = useNavigate(); + const { trackers } = useTrackers(); + + const hmdTracker = useMemo( + () => + trackers.some( + (tracker) => + tracker.tracker.info?.bodyPart === BodyPart.HEAD && + (tracker.tracker.info.isHmd || tracker.tracker.position?.y) + ), + [trackers] + ); + + applyProgress(0.9); + + return ( + +
+
+
+ + {l10n.getString('onboarding-scaled_proportions-title')} + +
+ + {l10n.getString('onboarding-scaled_proportions-description')} + +
+
+
+ + navigate('/onboarding/body-proportions/choose', { state }) + } + > +
+
+
+
+ ); +} diff --git a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckFloorHeight.tsx b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckFloorHeight.tsx new file mode 100644 index 0000000000..6469e6bb8c --- /dev/null +++ b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckFloorHeight.tsx @@ -0,0 +1,207 @@ +import { + ChangeSettingsRequestT, + HeightRequestT, + HeightResponseT, + ModelSettingsT, + RpcMessage, + SkeletonHeightT, +} from 'solarxr-protocol'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { Button } from '@/components/commons/Button'; +import { Typography } from '@/components/commons/Typography'; +import { Localized, useLocalization } from '@fluent/react'; +import { useEffect, useMemo, useState } from 'react'; +import { useLocaleConfig } from '@/i18n/config'; +import { useHeightContext } from '@/hooks/height'; +import { useInterval } from '@/hooks/timeout'; +import { TooSmolModal } from './TooSmolModal'; + +export function CheckFloorHeightStep({ + nextStep, + prevStep, + variant, +}: { + nextStep: () => void; + prevStep: () => void; + variant: 'onboarding' | 'alone'; +}) { + const { l10n } = useLocalization(); + const { floorHeight, hmdHeight, setFloorHeight, validateHeight } = + useHeightContext(); + const [fetchHeight, setFetchHeight] = useState(false); + const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + const [isOpen, setOpen] = useState(false); + const { currentLocales } = useLocaleConfig(); + + useEffect(() => setFloorHeight(0), []); + + useInterval(() => { + if (fetchHeight) { + sendRPCPacket(RpcMessage.HeightRequest, new HeightRequestT()); + } + }, 100); + + const mFormat = useMemo( + () => + new Intl.NumberFormat(currentLocales, { + style: 'unit', + unit: 'meter', + maximumFractionDigits: 2, + }), + [currentLocales] + ); + + useRPCPacket(RpcMessage.HeightResponse, ({ minHeight }: HeightResponseT) => { + if (fetchHeight) { + setFloorHeight((val) => + val === null ? minHeight : Math.min(minHeight, val) + ); + } + }); + return ( + <> +
+
+
+ + {l10n.getString( + 'onboarding-automatic_proportions-check_floor_height-title' + )} + +
+ + {l10n.getString( + 'onboarding-automatic_proportions-check_floor_height-description' + )} + + }} + > + + Press the button to get your height! + + +
+
+
+ {!fetchHeight && ( + + )} + {fetchHeight && ( + + )} + + {l10n.getString( + 'onboarding-automatic_proportions-check_floor_height-floor_height' + )} + + + {floorHeight === null + ? l10n.getString( + 'onboarding-automatic_proportions-check_height-unknown' + ) + : mFormat.format(floorHeight)} + +
+
+
+ {/* TODO: Get image of person putting controller in floor */} + {/*
+ Reset position +
*/} +
+ +
+ + + +
+
+ setOpen(false)} /> + + ); +} diff --git a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckHeight.tsx b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckHeight.tsx index 80943a4265..fcf900de2d 100644 --- a/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckHeight.tsx +++ b/gui/src/components/onboarding/pages/body-proportions/autobone-steps/CheckHeight.tsx @@ -1,27 +1,15 @@ -import { - AutoBoneSettingsT, - ChangeSettingsRequestT, - HeightRequestT, - HeightResponseT, - RpcMessage, -} from 'solarxr-protocol'; +import { HeightRequestT, HeightResponseT, RpcMessage } from 'solarxr-protocol'; import { useWebsocketAPI } from '@/hooks/websocket-api'; import { Button } from '@/components/commons/Button'; import { Typography } from '@/components/commons/Typography'; import { Localized, useLocalization } from '@fluent/react'; -import { useForm } from 'react-hook-form'; import { useMemo, useState } from 'react'; -import { NumberSelector } from '@/components/commons/NumberSelector'; -import { MIN_HEIGHT } from '@/components/onboarding/pages/body-proportions/ProportionsChoose'; import { useLocaleConfig } from '@/i18n/config'; -import { useCountdown } from '@/hooks/countdown'; import { TipBox } from '@/components/commons/TipBox'; +import { useHeightContext } from '@/hooks/height'; +import { useInterval } from '@/hooks/timeout'; -interface HeightForm { - hmdHeight: number; -} - -export function CheckHeight({ +export function CheckHeightStep({ nextStep, prevStep, variant, @@ -31,18 +19,17 @@ export function CheckHeight({ variant: 'onboarding' | 'alone'; }) { const { l10n } = useLocalization(); - const { control, handleSubmit, setValue } = useForm(); - const [fetchedHeight, setFetchedHeight] = useState(false); + const { hmdHeight, setHmdHeight } = useHeightContext(); + const [fetchHeight, setFetchHeight] = useState(false); const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); - const { timer, isCounting, startCountdown } = useCountdown({ - duration: 3, - onCountdownEnd: () => { - setFetchedHeight(true); - sendRPCPacket(RpcMessage.HeightRequest, new HeightRequestT()); - }, - }); const { currentLocales } = useLocaleConfig(); + useInterval(() => { + if (fetchHeight) { + sendRPCPacket(RpcMessage.HeightRequest, new HeightRequestT()); + } + }, 100); + const mFormat = useMemo( () => new Intl.NumberFormat(currentLocales, { @@ -53,88 +40,103 @@ export function CheckHeight({ [currentLocales] ); - const sFormat = useMemo( - () => new Intl.RelativeTimeFormat(currentLocales, { style: 'short' }), - [currentLocales] - ); - - useRPCPacket(RpcMessage.HeightResponse, ({ hmdHeight }: HeightResponseT) => { - setValue('hmdHeight', hmdHeight); + useRPCPacket(RpcMessage.HeightResponse, ({ maxHeight }: HeightResponseT) => { + if (fetchHeight) { + setHmdHeight((val) => + val === null ? maxHeight : Math.max(maxHeight, val) + ); + } }); - - const onSubmit = (values: HeightForm) => { - const changeSettings = new ChangeSettingsRequestT(); - const autobone = new AutoBoneSettingsT(); - autobone.targetHmdHeight = values.hmdHeight; - changeSettings.autoBoneSettings = autobone; - - sendRPCPacket(RpcMessage.ChangeSettingsRequest, changeSettings); - nextStep(); - }; - return ( <>
-
- - {l10n.getString( - 'onboarding-automatic_proportions-check_height-title' - )} - -
- +
+
+ {l10n.getString( - 'onboarding-automatic_proportions-check_height-description' + 'onboarding-automatic_proportions-check_height-title-v2' )} - }} - > - - Press the button to get your height! +
+ + {l10n.getString( + 'onboarding-automatic_proportions-check_height-description-v2' + )} - - -
- - - {l10n.getString( - 'onboarding-automatic_proportions-check_height-guardian_tip' + + Press the button to get your height! + + + +
+ + {l10n.getString( + 'onboarding-automatic_proportions-check_height-guardian_tip' + )} + +
+
+
+
+ {!fetchHeight && ( + + )} + {fetchHeight && ( + )} - + + {l10n.getString( + 'onboarding-automatic_proportions-check_height-hmd_height2' + )} + + + {hmdHeight === null + ? l10n.getString( + 'onboarding-automatic_proportions-check_height-unknown' + ) + : mFormat.format(hmdHeight)} + +
-
- - isNaN(value) - ? l10n.getString( - 'onboarding-automatic_proportions-check_height-unknown' - ) - : mFormat.format(value) - } - min={MIN_HEIGHT} - max={4} - step={0.01} - disabled={true} +
+ Reset position - +
@@ -146,8 +148,8 @@ export function CheckHeight({ +
+
+
+ + ); +} diff --git a/gui/src/components/onboarding/pages/body-proportions/scaled-steps/Done.tsx b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/Done.tsx new file mode 100644 index 0000000000..a782d8401c --- /dev/null +++ b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/Done.tsx @@ -0,0 +1,30 @@ +import { Typography } from '@/components/commons/Typography'; +import { useLocalization } from '@fluent/react'; +import { Button } from '@/components/commons/Button'; +import { SkeletonVisualizerWidget } from '@/components/widgets/SkeletonVisualizerWidget'; + +export function DoneStep({ variant }: { variant: 'onboarding' | 'alone' }) { + const { l10n } = useLocalization(); + + return ( +
+
+ + {l10n.getString('onboarding-scaled_proportions-done-title')} + + + {l10n.getString('onboarding-scaled_proportions-done-description')} + +
+ +
+ {variant === 'onboarding' && ( + + )} +
+ +
+ ); +} diff --git a/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ManualHeightStep.tsx b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ManualHeightStep.tsx new file mode 100644 index 0000000000..6e548293bf --- /dev/null +++ b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ManualHeightStep.tsx @@ -0,0 +1,158 @@ +import { useWebsocketAPI } from '@/hooks/websocket-api'; +import { Button } from '@/components/commons/Button'; +import { Typography } from '@/components/commons/Typography'; +import { Localized, useLocalization } from '@fluent/react'; +import { useMemo } from 'react'; +import { useLocaleConfig } from '@/i18n/config'; +import { useHeightContext } from '@/hooks/height'; +import { useForm } from 'react-hook-form'; +import { + ChangeSettingsRequestT, + ModelSettingsT, + RpcMessage, + SkeletonHeightT, + StatusData, + StatusSteamVRDisconnectedT, +} from 'solarxr-protocol'; +import { NumberSelector } from '@/components/commons/NumberSelector'; +import { MIN_HEIGHT } from '@/components/onboarding/pages/body-proportions/ProportionsChoose'; +import { WarningBox } from '@/components/commons/TipBox'; +import { useStatusContext } from '@/hooks/status-system'; + +interface HeightForm { + height: number; +} + +export function ManualHeightStep({ + nextStep, + prevStep, + variant, +}: { + nextStep: () => void; + prevStep: () => void; + variant: 'onboarding' | 'alone'; +}) { + const { l10n } = useLocalization(); + const { hmdHeight, setHmdHeight } = useHeightContext(); + const { control, handleSubmit } = useForm({ + defaultValues: { height: 1.5 }, + }); + const { sendRPCPacket } = useWebsocketAPI(); + const { currentLocales } = useLocaleConfig(); + const { statuses } = useStatusContext(); + + const missingSteamConnection = useMemo( + () => + Object.values(statuses).some( + (x) => + x.dataType === StatusData.StatusSteamVRDisconnected && + (x.data as StatusSteamVRDisconnectedT).bridgeSettingsName === + 'steamvr' + ), + [statuses] + ); + + const mFormat = useMemo( + () => + new Intl.NumberFormat(currentLocales, { + style: 'unit', + unit: 'meter', + maximumFractionDigits: 2, + }), + [currentLocales] + ); + + handleSubmit((values) => { + setHmdHeight(values.height); + }); + + return ( + <> +
+
+
+ + {l10n.getString( + 'onboarding-scaled_proportions-manual_height-title' + )} + +
+ + {l10n.getString( + 'onboarding-scaled_proportions-manual_height-description' + )} + + {/* }} + > + + Input your height manually! + + */} + {missingSteamConnection && ( +
+ }} + // TODO: Add link to docs! + > + You don't have SteamVR connected! + +
+ )} +
+
+ + isNaN(value) + ? l10n.getString( + 'onboarding-scaled_proportions-manual_height-unknown' + ) + : mFormat.format(value) + } + min={MIN_HEIGHT} + max={4} + step={0.01} + showButtonWithNumber + doubleStep={0.1} + /> + +
+
+ +
+ + +
+
+ + ); +} diff --git a/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ResetProportions.tsx b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ResetProportions.tsx new file mode 100644 index 0000000000..ac8fdc3d19 --- /dev/null +++ b/gui/src/components/onboarding/pages/body-proportions/scaled-steps/ResetProportions.tsx @@ -0,0 +1,62 @@ +import { RpcMessage, SkeletonResetAllRequestT } from 'solarxr-protocol'; +import { Button } from '@/components/commons/Button'; +import { Typography } from '@/components/commons/Typography'; +import { useLocalization } from '@fluent/react'; +import { useWebsocketAPI } from '@/hooks/websocket-api'; + +export function ResetProportionsStep({ + nextStep, + prevStep, + variant, +}: { + nextStep: () => void; + prevStep: () => void; + variant: 'onboarding' | 'alone'; +}) { + const { l10n } = useLocalization(); + const { sendRPCPacket } = useWebsocketAPI(); + + return ( + <> +
+
+ + {l10n.getString( + 'onboarding-scaled_proportions-reset_proportion-title' + )} + +
+ + {l10n.getString( + 'onboarding-scaled_proportions-reset_proportion-description' + )} + +
+
+ +
+
+ + +
+
+
+ + ); +} diff --git a/gui/src/hooks/height.ts b/gui/src/hooks/height.ts new file mode 100644 index 0000000000..5dffb530d9 --- /dev/null +++ b/gui/src/hooks/height.ts @@ -0,0 +1,58 @@ +import { createContext, useContext, useEffect, useState } from 'react'; +import { useWebsocketAPI } from './websocket-api'; +import { RpcMessage, SettingsRequestT, SettingsResponseT } from 'solarxr-protocol'; +import { MIN_HEIGHT } from '@/components/onboarding/pages/body-proportions/ProportionsChoose'; + +export interface HeightContext { + hmdHeight: number | null; + setHmdHeight: React.Dispatch>; + floorHeight: number | null; + setFloorHeight: React.Dispatch>; + validateHeight: ( + hmdHeight: number | null | undefined, + floorHeight: number | null | undefined + ) => boolean; +} + +export function useProvideHeightContext(): HeightContext { + const [hmdHeight, setHmdHeight] = useState(null); + const [floorHeight, setFloorHeight] = useState(null); + const { sendRPCPacket, useRPCPacket } = useWebsocketAPI(); + + function validateHeight( + hmdHeight: number | null | undefined, + floorHeight: number | null | undefined + ) { + return ( + hmdHeight !== undefined && + hmdHeight !== null && + hmdHeight - (floorHeight ?? 0) > MIN_HEIGHT + ); + } + + useEffect( + () => sendRPCPacket(RpcMessage.SettingsRequest, new SettingsRequestT()), + [] + ); + useRPCPacket(RpcMessage.SettingsResponse, (res: SettingsResponseT) => { + const hmd = res.modelSettings?.skeletonHeight?.hmdHeight; + const floor = res.modelSettings?.skeletonHeight?.floorHeight; + + if (validateHeight(hmd, floor)) { + setHmdHeight(hmd ?? null); + setFloorHeight(floor ?? null); + } + }); + + return { hmdHeight, setHmdHeight, floorHeight, setFloorHeight, validateHeight }; +} + +export const HeightContextC = createContext(undefined as never); + +export function useHeightContext() { + const context = useContext(HeightContextC); + if (!context) { + throw new Error('useHeightContext must be within a HeightContext Provider'); + } + return context; +} diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt index faf714f2d2..bf7e606302 100644 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt +++ b/server/core/src/main/java/dev/slimevr/autobone/AutoBone.kt @@ -4,6 +4,7 @@ import dev.slimevr.SLIMEVR_IDENTIFIER import dev.slimevr.VRServer import dev.slimevr.autobone.errors.* import dev.slimevr.config.AutoBoneConfig +import dev.slimevr.config.SkeletonConfig import dev.slimevr.poseframeformat.PoseFrameIO import dev.slimevr.poseframeformat.PoseFrames import dev.slimevr.tracking.processor.BoneType @@ -23,7 +24,7 @@ import java.util.function.Consumer import java.util.function.Function import kotlin.math.* -class AutoBone(server: VRServer) { +class AutoBone(private val server: VRServer) { // This is filled by loadConfigValues() val offsets = EnumMap( SkeletonConfigOffsets::class.java, @@ -49,8 +50,6 @@ class AutoBone(server: VRServer) { // The total height of the normalized adjusted offsets var adjustedHeightNormalized: Float = 1f - private val server: VRServer - // #region Error functions var slideError = SlideError() var offsetSlideError = OffsetSlideError() @@ -63,11 +62,10 @@ class AutoBone(server: VRServer) { private val rand = Random() - val globalConfig: AutoBoneConfig + val globalConfig: AutoBoneConfig = server.configManager.vrConfig.autoBone + val globalSkeletonConfig: SkeletonConfig = server.configManager.vrConfig.skeleton init { - globalConfig = server.configManager.vrConfig.autoBone - this.server = server loadConfigValues() } @@ -191,6 +189,7 @@ class AutoBone(server: VRServer) { // Get the current skeleton from the server val humanPoseManager = server.humanPoseManager // Still compensate for a null skeleton, as it may not be initialized yet + @Suppress("SENSELESS_COMPARISON") if (config.useSkeletonHeight && humanPoseManager != null) { // If there is a skeleton available, calculate the target height // from its configs @@ -229,6 +228,7 @@ class AutoBone(server: VRServer) { fun processFrames( frames: PoseFrames, config: AutoBoneConfig = globalConfig, + skeletonConfig: SkeletonConfig = globalSkeletonConfig, epochCallback: Consumer? = null, ): AutoBoneResults { check(frames.frameHolders.isNotEmpty()) { "Recording has no trackers." } @@ -238,16 +238,11 @@ class AutoBone(server: VRServer) { loadConfigValues() // Set the target heights either from config or calculate them - val targetHmdHeight = if (config.targetHmdHeight > 0f) { - config.targetHmdHeight + val targetHmdHeight = if (skeletonConfig.userHeight > MIN_HEIGHT) { + skeletonConfig.userHeight } else { calcTargetHmdHeight(frames, config) } - val targetFullHeight = if (config.targetFullHeight > 0f) { - config.targetFullHeight - } else { - targetHmdHeight / BodyProportionError.eyeHeightToHeightRatio - } check(targetHmdHeight > MIN_HEIGHT) { "Configured height ($targetHmdHeight) is too small (<= $MIN_HEIGHT)." } // Set up the current state, making all required players and setting up the @@ -255,7 +250,6 @@ class AutoBone(server: VRServer) { val trainingStep = AutoBoneStep( config = config, targetHmdHeight = targetHmdHeight, - targetFullHeight = targetFullHeight, frames = frames, epochCallback = epochCallback, serverConfig = server.configManager, diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt index a0b802f679..112121606b 100644 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt +++ b/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt @@ -10,7 +10,6 @@ import java.util.function.Consumer class AutoBoneStep( val config: AutoBoneConfig, val targetHmdHeight: Float, - val targetFullHeight: Float, val frames: PoseFrames, val epochCallback: Consumer?, serverConfig: ConfigManager, @@ -20,9 +19,6 @@ class AutoBoneStep( var cursor2: Int = 0, var currentHmdHeight: Float = 0f, ) { - - val eyeHeightToHeightRatio: Float = targetHmdHeight / targetFullHeight - var maxFrameCount = frames.maxFrameCount val framePlayer1 = TrackerFramesPlayer(frames) diff --git a/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt b/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt index 51d2649854..c8f0912c56 100644 --- a/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt +++ b/server/core/src/main/java/dev/slimevr/autobone/errors/BodyProportionError.kt @@ -4,6 +4,7 @@ import dev.slimevr.autobone.AutoBoneStep import dev.slimevr.autobone.errors.proportions.ProportionLimiter import dev.slimevr.autobone.errors.proportions.RangeProportionLimiter import dev.slimevr.tracking.processor.HumanPoseManager +import dev.slimevr.tracking.processor.config.SkeletonConfigManager import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets import kotlin.math.* @@ -32,84 +33,58 @@ class BodyProportionError : IAutoBoneError { @JvmField var eyeHeightToHeightRatio = 0.936f - // Default config - // Height: 1.58 - // Full Height: 1.58 / 0.936 = 1.688034 - // Neck: 0.1 / 1.688034 = 0.059241 - // Torso: 0.56 / 1.688034 = 0.331747 - // Upper Chest: 0.16 / 1.688034 = 0.094784 - // Chest: 0.16 / 1.688034 = 0.094784 - // Waist: (0.56 - 0.32 - 0.04) / 1.688034 = 0.118481 - // Hip: 0.04 / 1.688034 = 0.023696 - // Hip Width: 0.26 / 1.688034 = 0.154025 - // Upper Leg: (0.92 - 0.50) / 1.688034 = 0.24881 - // Lower Leg: 0.50 / 1.688034 = 0.296203 + private val defaultHeight = SkeletonConfigManager.HEIGHT_OFFSETS.sumOf { it.defaultValue.toDouble() }.toFloat() + private fun makeLimiter(offset: SkeletonConfigOffsets, range: Float): RangeProportionLimiter = RangeProportionLimiter( + offset.defaultValue / defaultHeight, + offset, + range, + ) + // "Expected" are values from Drillis and Contini (1966) - // "Experimental" are values from experimentation by the SlimeVR community + // Default are values from experimentation by the SlimeVR community + /** + * Proportions are based off the headset height (or eye height), not the total height of the user. + * To use the total height of the user, multiply it by [eyeHeightToHeightRatio] and use that in the limiters. + */ val proportionLimits = arrayOf( - // Head - // Experimental: 0.059 - RangeProportionLimiter( - 0.059f, + makeLimiter( SkeletonConfigOffsets.HEAD, 0.01f, ), - // Neck // Expected: 0.052 - // Experimental: 0.059 - RangeProportionLimiter( - 0.054f, + makeLimiter( SkeletonConfigOffsets.NECK, - 0.0015f, + 0.002f, ), - // Upper Chest - // Experimental: 0.0945 - RangeProportionLimiter( - 0.0945f, + makeLimiter( SkeletonConfigOffsets.UPPER_CHEST, 0.01f, ), - // Chest - // Experimental: 0.0945 - RangeProportionLimiter( - 0.0945f, + makeLimiter( SkeletonConfigOffsets.CHEST, 0.01f, ), - // Waist - // Experimental: 0.118 - RangeProportionLimiter( - 0.118f, + makeLimiter( SkeletonConfigOffsets.WAIST, 0.05f, ), - // Hip - // Experimental: 0.0237 - RangeProportionLimiter( - 0.0237f, + makeLimiter( SkeletonConfigOffsets.HIP, 0.01f, ), - // Hip Width // Expected: 0.191 - // Experimental: 0.154 - RangeProportionLimiter( - 0.184f, + makeLimiter( SkeletonConfigOffsets.HIPS_WIDTH, 0.04f, ), - // Upper Leg // Expected: 0.245 - RangeProportionLimiter( - 0.245f, + makeLimiter( SkeletonConfigOffsets.UPPER_LEG, - 0.015f, + 0.02f, ), - // Lower Leg // Expected: 0.246 (0.285 including below ankle, could use a separate // offset?) - RangeProportionLimiter( - 0.285f, + makeLimiter( SkeletonConfigOffsets.LOWER_LEG, 0.02f, ), diff --git a/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt b/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt index 3c7cf20d94..3d3c62f22a 100644 --- a/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt +++ b/server/core/src/main/java/dev/slimevr/config/AutoBoneConfig.kt @@ -16,8 +16,6 @@ class AutoBoneConfig { var positionErrorFactor = 0.0f var positionOffsetErrorFactor = 0.0f var calcInitError = false - var targetHmdHeight = -1f - var targetFullHeight = -1f var randomizeFrameOrder = true var scaleEachStep = true var sampleCount = 1500 diff --git a/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java b/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java index ce6e436b2d..76dc56d261 100644 --- a/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java +++ b/server/core/src/main/java/dev/slimevr/config/CurrentVRConfigConverter.java @@ -309,6 +309,15 @@ public ObjectNode convert( // Update AutoBone defaults ObjectNode autoBoneNode = (ObjectNode) modelData.get("autoBone"); if (autoBoneNode != null) { + // Move HMD height to skeleton + ObjectNode skeletonNode = (ObjectNode) modelData.get("skeleton"); + if (skeletonNode != null) { + JsonNode targetHmdHeight = autoBoneNode.get("targetHmdHeight"); + if (targetHmdHeight != null) { + skeletonNode.set("hmdHeight", targetHmdHeight); + } + } + JsonNode offsetSlideNode = autoBoneNode.get("offsetSlideErrorFactor"); JsonNode slideNode = autoBoneNode.get("slideErrorFactor"); if ( diff --git a/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java b/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java index 07ec694f3e..27e6b3c3e8 100644 --- a/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java +++ b/server/core/src/main/java/dev/slimevr/config/SkeletonConfig.java @@ -1,5 +1,6 @@ package dev.slimevr.config; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.StdKeySerializers; @@ -24,6 +25,9 @@ public class SkeletonConfig { @JsonSerialize(keyUsing = StdKeySerializers.StringKeySerializer.class) public Map offsets = new HashMap<>(); + private float hmdHeight = 0f; + private float floorHeight = 0f; + public Map getToggles() { return toggles; } @@ -35,4 +39,25 @@ public Map getOffsets() { public Map getValues() { return values; } + + public float getHmdHeight() { + return hmdHeight; + } + + public void setHmdHeight(float hmdHeight) { + this.hmdHeight = hmdHeight; + } + + public float getFloorHeight() { + return floorHeight; + } + + public void setFloorHeight(float hmdHeight) { + this.floorHeight = hmdHeight; + } + + @JsonIgnore + public float getUserHeight() { + return hmdHeight - floorHeight; + } } diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt index 850644ea1e..19165b7c81 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/RPCHandler.kt @@ -1,7 +1,6 @@ package dev.slimevr.protocol.rpc import com.google.flatbuffers.FlatBufferBuilder -import dev.slimevr.autobone.errors.BodyProportionError import dev.slimevr.config.config import dev.slimevr.protocol.GenericConnection import dev.slimevr.protocol.ProtocolAPI @@ -20,6 +19,7 @@ import dev.slimevr.protocol.rpc.status.RPCStatusHandler import dev.slimevr.protocol.rpc.trackingpause.RPCTrackingPause import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets import dev.slimevr.tracking.trackers.TrackerPosition.Companion.getByBodyPart +import dev.slimevr.tracking.trackers.TrackerStatus import dev.slimevr.tracking.trackers.TrackerUtils.getTrackerForSkeleton import io.eiren.util.logging.LogManager import io.github.axisangles.ktmath.Quaternion @@ -464,13 +464,22 @@ class RPCHandler(private val api: ProtocolAPI) : ProtocolHandler { - val height = ( - humanPoseManager!!.hmdHeight / - BodyProportionError.eyeHeightToHeightRatio - ) - if (height > 0.5f) { // Reset only if floor level seems right, + val height = humanPoseManager?.server?.configManager?.vrConfig?.skeleton?.userHeight ?: -1f + if (height > AutoBone.MIN_HEIGHT) { // Reset only if floor level seems right, val proportionLimiter = proportionLimitMap[config] if (proportionLimiter != null) { setOffset( diff --git a/solarxr-protocol b/solarxr-protocol index aa69fb6e56..796c58e147 160000 --- a/solarxr-protocol +++ b/solarxr-protocol @@ -1 +1 @@ -Subproject commit aa69fb6e56b88a206010d1cb75bfa0edde5b4582 +Subproject commit 796c58e147c03faa1c662fd9c1734fb9536d1d4b From bfb30c472b1b78fe024e40e7b8e70b40fa74c656 Mon Sep 17 00:00:00 2001 From: Collin Date: Sun, 26 Jan 2025 07:36:10 -0800 Subject: [PATCH 12/16] Skeleton Constraints (#1222) Co-authored-by: Butterscotch! --- gui/public/i18n/en/translation.ftl | 5 + .../settings/pages/GeneralSettings.tsx | 52 +++++ .../java/dev/slimevr/autobone/AutoBoneStep.kt | 2 +- .../filtering/QuaternionMovingAverage.kt | 5 +- .../rpc/settings/RPCSettingsBuilder.java | 4 +- .../rpc/settings/RPCSettingsHandler.kt | 2 + .../dev/slimevr/tracking/processor/Bone.kt | 56 ++++- .../slimevr/tracking/processor/BoneType.java | 2 + .../slimevr/tracking/processor/Constraint.kt | 191 ++++++++++++++++++ .../tracking/processor/TransformNode.kt | 5 + .../processor/config/SkeletonConfigManager.kt | 14 ++ .../config/SkeletonConfigToggles.java | 5 +- .../processor/skeleton/HumanSkeleton.kt | 191 ++++++++++++------ .../trackers/TrackerFilteringHandler.kt | 5 + .../tracking/trackers/TrackerResetsHandler.kt | 16 ++ 15 files changed, 484 insertions(+), 71 deletions(-) create mode 100644 server/core/src/main/java/dev/slimevr/tracking/processor/Constraint.kt diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index c86ac7e403..ac9a714a39 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -450,6 +450,11 @@ settings-general-fk_settings-leg_tweak-foot_plant-description = Foot-plant rotat settings-general-fk_settings-leg_fk = Leg tracking settings-general-fk_settings-leg_fk-reset_mounting_feet-description = Enable feet Mounting Reset by tiptoeing. settings-general-fk_settings-leg_fk-reset_mounting_feet = Feet Mounting Reset +settings-general-fk_settings-enforce_joint_constraints = Skeletal Limits +settings-general-fk_settings-enforce_joint_constraints-enforce_constraints = Enforce constraints +settings-general-fk_settings-enforce_joint_constraints-enforce_constraints-description = Prevents joints from rotating past their limit +settings-general-fk_settings-enforce_joint_constraints-correct_constraints = Correct with constraints +settings-general-fk_settings-enforce_joint_constraints-correct_constraints-description = Correct joint rotations when they push past their limit settings-general-fk_settings-arm_fk = Arm tracking settings-general-fk_settings-arm_fk-description = Force arms to be tracked from the headset (HMD) even if positional hand data is available. settings-general-fk_settings-arm_fk-force_arms = Force arms from HMD diff --git a/gui/src/components/settings/pages/GeneralSettings.tsx b/gui/src/components/settings/pages/GeneralSettings.tsx index d384670e79..3946b551fc 100644 --- a/gui/src/components/settings/pages/GeneralSettings.tsx +++ b/gui/src/components/settings/pages/GeneralSettings.tsx @@ -69,6 +69,9 @@ interface SettingsForm { toeSnap: boolean; footPlant: boolean; selfLocalization: boolean; + usePosition: boolean; + enforceConstraints: boolean; + correctConstraints: boolean; }; ratios: { imputeWaistFromChestHip: number; @@ -128,6 +131,9 @@ const defaultValues: SettingsForm = { toeSnap: false, footPlant: true, selfLocalization: false, + usePosition: true, + enforceConstraints: true, + correctConstraints: true, }, ratios: { imputeWaistFromChestHip: 0.3, @@ -235,6 +241,9 @@ export function GeneralSettings() { toggles.toeSnap = values.toggles.toeSnap; toggles.footPlant = values.toggles.footPlant; toggles.selfLocalization = values.toggles.selfLocalization; + toggles.usePosition = values.toggles.usePosition; + toggles.enforceConstraints = values.toggles.enforceConstraints; + toggles.correctConstraints = values.toggles.correctConstraints; modelSettings.toggles = toggles; } @@ -981,6 +990,7 @@ export function GeneralSettings() { )} />
+ {l10n.getString( 'settings-general-fk_settings-arm_fk-reset_mode-description' @@ -1033,6 +1043,48 @@ export function GeneralSettings() { >
+
+ + {l10n.getString( + 'settings-general-fk_settings-enforce_joint_constraints' + )} + + + {l10n.getString( + 'settings-general-fk_settings-enforce_joint_constraints-enforce_constraints-description' + )} + +
+
+ +
+
+ + {l10n.getString( + 'settings-general-fk_settings-enforce_joint_constraints-correct_constraints-description' + )} + +
+
+ +
+ {config?.debug && ( <>
diff --git a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt b/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt index 112121606b..bbcce83814 100644 --- a/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt +++ b/server/core/src/main/java/dev/slimevr/autobone/AutoBoneStep.kt @@ -36,7 +36,7 @@ class AutoBoneStep( // Load server configs into the skeleton skeleton1.loadFromConfig(serverConfig) skeleton2.loadFromConfig(serverConfig) - // Disable leg tweaks, this will mess with the resulting positions + // Disable leg tweaks and IK solver, these will mess with the resulting positions skeleton1.setLegTweaksEnabled(false) skeleton2.setLegTweaksEnabled(false) } diff --git a/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt b/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt index 09517e692b..479263ba1f 100644 --- a/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt +++ b/server/core/src/main/java/dev/slimevr/filtering/QuaternionMovingAverage.kt @@ -21,6 +21,8 @@ class QuaternionMovingAverage( var amount: Float = 0f, initialRotation: Quaternion = IDENTITY, ) { + var filteredQuaternion = IDENTITY + var filteringImpact = 0f private var smoothFactor = 0f private var predictFactor = 0f private var rotBuffer: CircularArrayList? = null @@ -29,7 +31,6 @@ class QuaternionMovingAverage( private val fpsTimer = if (VRServer.instanceInitialized) VRServer.instance.fpsTimer else NanoTimer() private var frameCounter = 0 private var lastAmt = 0f - var filteredQuaternion = IDENTITY init { // amount should range from 0 to 1. @@ -93,6 +94,8 @@ class QuaternionMovingAverage( // No filtering; just keep track of rotations (for going over 180 degrees) filteredQuaternion = latestQuaternion.twinNearest(smoothingQuaternion) } + + filteringImpact = latestQuaternion.angleToR(filteredQuaternion) } @Synchronized diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.java b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.java index 6e0bf02855..5e38d571ec 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.java +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsBuilder.java @@ -190,8 +190,8 @@ public static int createModelSettings( humanPoseManager.getToggle(SkeletonConfigToggles.FOOT_PLANT), humanPoseManager.getToggle(SkeletonConfigToggles.SELF_LOCALIZATION), false, - true, - true + humanPoseManager.getToggle(SkeletonConfigToggles.ENFORCE_CONSTRAINTS), + humanPoseManager.getToggle(SkeletonConfigToggles.CORRECT_CONSTRAINTS) ); int ratiosOffset = ModelRatios .createModelRatios( diff --git a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt index 13bce643ab..9f69fd4052 100644 --- a/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/protocol/rpc/settings/RPCSettingsHandler.kt @@ -258,6 +258,8 @@ class RPCSettingsHandler(var rpcHandler: RPCHandler, var api: ProtocolAPI) { hpm.setToggle(SkeletonConfigToggles.TOE_SNAP, toggles.toeSnap()) hpm.setToggle(SkeletonConfigToggles.FOOT_PLANT, toggles.footPlant()) hpm.setToggle(SkeletonConfigToggles.SELF_LOCALIZATION, toggles.selfLocalization()) + hpm.setToggle(SkeletonConfigToggles.ENFORCE_CONSTRAINTS, toggles.enforceConstraints()) + hpm.setToggle(SkeletonConfigToggles.CORRECT_CONSTRAINTS, toggles.correctConstraints()) } if (ratios != null) { diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt index 3a291f49a4..ee2ce87d4c 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt @@ -1,19 +1,23 @@ package dev.slimevr.tracking.processor +import dev.slimevr.tracking.processor.Constraint.Companion.ConstraintType +import dev.slimevr.tracking.trackers.Tracker import io.github.axisangles.ktmath.Quaternion import io.github.axisangles.ktmath.Vector3 +import solarxr_protocol.datatypes.BodyPart import java.util.concurrent.CopyOnWriteArrayList /** * Represents a bone composed of 2 joints: headNode and tailNode. */ -class Bone(val boneType: BoneType) { +class Bone(val boneType: BoneType, val rotationConstraint: Constraint) { private val headNode = TransformNode(true) private val tailNode = TransformNode(false) var parent: Bone? = null private set val children: MutableList = CopyOnWriteArrayList() var rotationOffset = Quaternion.IDENTITY + var attachedTracker: Tracker? = null init { headNode.attachChild(tailNode) @@ -58,6 +62,49 @@ class Bone(val boneType: BoneType) { headNode.update() } + /** + * Computes the rotations and positions of + * this bone and all of its children while + * enforcing rotation constraints. + */ + fun updateWithConstraints() { + val initialRot = getGlobalRotation() + val newRot = rotationConstraint.applyConstraint(initialRot, this) + setRotationRaw(newRot) + updateThisNode() + + // Correct tracker if applicable. Do not adjust correction for hinge constraints + // or the upper chest tracker. + if (rotationConstraint.constraintType != ConstraintType.HINGE && + rotationConstraint.constraintType != ConstraintType.LOOSE_HINGE && + boneType.bodyPart != BodyPart.UPPER_CHEST + ) { + val deltaRot = newRot * initialRot.inv() + val angle = deltaRot.angleR() + + if (angle > Constraint.ANGLE_THRESHOLD && + (attachedTracker?.filteringHandler?.getFilteringImpact() ?: 1f) < Constraint.FILTER_IMPACT_THRESHOLD && + (parent?.attachedTracker?.filteringHandler?.getFilteringImpact() ?: 0f) < Constraint.FILTER_IMPACT_THRESHOLD + ) { + attachedTracker?.resetsHandler?.updateConstraintFix(deltaRot) + } + } + + // Recursively apply constraints and update children. + for (child in children) { + child.updateWithConstraints() + } + } + + /** + * Computes the rotations and positions of this bone. + * Only to be used while traversing bones from top to bottom. + */ + private fun updateThisNode() { + headNode.updateThisNode() + tailNode.updateThisNode() + } + /** * Returns the world-aligned rotation of the bone */ @@ -75,6 +122,13 @@ class Bone(val boneType: BoneType) { headNode.localTransform.rotation = rotation * rotationOffset } + /** + * Sets the global rotation of the bone directly + */ + fun setRotationRaw(rotation: Quaternion) { + headNode.localTransform.rotation = rotation + } + /** * Returns the global position of the head of the bone */ diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/BoneType.java b/server/core/src/main/java/dev/slimevr/tracking/processor/BoneType.java index 0e263992bd..b423640103 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/BoneType.java +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/BoneType.java @@ -36,6 +36,8 @@ public enum BoneType { RIGHT_UPPER_ARM(BodyPart.RIGHT_UPPER_ARM), LEFT_SHOULDER(BodyPart.LEFT_SHOULDER), RIGHT_SHOULDER(BodyPart.RIGHT_SHOULDER), + LEFT_UPPER_SHOULDER, + RIGHT_UPPER_SHOULDER, LEFT_HAND(BodyPart.LEFT_HAND), RIGHT_HAND(BodyPart.RIGHT_HAND), LEFT_HAND_TRACKER, diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/Constraint.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/Constraint.kt new file mode 100644 index 0000000000..91c6d34ff1 --- /dev/null +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/Constraint.kt @@ -0,0 +1,191 @@ +package dev.slimevr.tracking.processor + +import com.jme3.math.FastMath +import io.github.axisangles.ktmath.Quaternion +import io.github.axisangles.ktmath.Vector3 +import kotlin.math.* + +/** + * Represents a function that applies a rotational constraint. + */ +typealias ConstraintFunction = (localRotation: Quaternion, limit1: Float, limit2: Float, limit3: Float) -> Quaternion + +/** + * Represents the rotational limits of a Bone relative to its parent, + * twist and swing are the max and min when constraintType is a hinge. + * Twist, swing, allowedDeviation, and maxDeviationFromTracker represent + * an angle in degrees. + */ +class Constraint( + val constraintType: ConstraintType, + twist: Float = 0.0f, + swing: Float = 0.0f, + allowedDeviation: Float = 0f, + maxDeviationFromTracker: Float = 15f, +) { + private val constraintFunction = constraintTypeToFunc(constraintType) + private val twistRad = twist * FastMath.DEG_TO_RAD + private val swingRad = swing * FastMath.DEG_TO_RAD + private val allowedDeviationRad = allowedDeviation * FastMath.DEG_TO_RAD + private val maxDeviationFromTrackerRad = maxDeviationFromTracker * FastMath.DEG_TO_RAD + + /** + * allowModification may be false for reasons other than a tracker being on this bone + * while hasTrackerRotation is only true if this bone has a tracker. These values are + * to be used with an IK solver and are not currently set accurately + */ + var allowModifications = true + var hasTrackerRotation = false + + /** + * The rotation before any IK solve takes place. Again this value is not currently set accurately + */ + var initialRotation = Quaternion.IDENTITY + + /** + * Apply rotational constraints and if applicable force the rotation + * to be unchanged unless it violates the constraints + */ + fun applyConstraint(rotation: Quaternion, thisBone: Bone): Quaternion { + // When constraints are being used during a IK solve the input rotation is not necessarily + // the bones global rotation, thus complete constraints must be specifically handled. + if (constraintType == ConstraintType.COMPLETE) return thisBone.getGlobalRotation() + + // If there is no parent and this is not a complete constraint accept the rotation as is. + if (thisBone.parent == null) return rotation + + val localRotation = getLocalRotation(rotation, thisBone) + val constrainedRotation = constraintFunction(localRotation, swingRad, twistRad, allowedDeviationRad) + return getWorldRotationFromLocal(constrainedRotation, thisBone) + } + + /** + * Force the given rotation to be within allowedDeviation degrees away from + * initialRotation on both the twist and swing axis + */ + fun constrainToInitialRotation(rotation: Quaternion): Quaternion { + val rotationLocal = rotation * initialRotation.inv() + var (swingQ, twistQ) = decompose(rotationLocal, Vector3.NEG_Y) + swingQ = constrain(swingQ, maxDeviationFromTrackerRad) + twistQ = constrain(twistQ, maxDeviationFromTrackerRad) + return initialRotation * (swingQ * twistQ) + } + + companion object { + const val ANGLE_THRESHOLD = 0.004f // == 0.25 degrees + const val FILTER_IMPACT_THRESHOLD = 0.0349f // == 2 degrees + + enum class ConstraintType { + TWIST_SWING, + HINGE, + LOOSE_HINGE, + COMPLETE, + } + + private fun constraintTypeToFunc(type: ConstraintType) = + when (type) { + ConstraintType.COMPLETE -> completeConstraint + ConstraintType.TWIST_SWING -> twistSwingConstraint + ConstraintType.HINGE -> hingeConstraint + ConstraintType.LOOSE_HINGE -> looseHingeConstraint + } + + private fun getLocalRotation(rotation: Quaternion, thisBone: Bone): Quaternion { + val parent = thisBone.parent!! + val localRotationOffset = parent.rotationOffset.inv() * thisBone.rotationOffset + return (parent.getGlobalRotation() * localRotationOffset).inv() * rotation + } + + private fun getWorldRotationFromLocal(rotation: Quaternion, thisBone: Bone): Quaternion { + val parent = thisBone.parent!! + val localRotationOffset = parent.rotationOffset.inv() * thisBone.rotationOffset + return (parent.getGlobalRotation() * localRotationOffset * rotation).unit() + } + + private fun decompose( + rotation: Quaternion, + twistAxis: Vector3, + ): Pair { + val projection = rotation.project(twistAxis).unit() + val twist = Quaternion(sqrt(1.0f - projection.xyz.lenSq()) * if (rotation.w >= 0f) 1f else -1f, projection.xyz).unit() + val swing = (rotation * twist.inv()).unit() + return Pair(swing, twist) + } + + private fun constrain(rotation: Quaternion, angle: Float): Quaternion { + // Use angle to get the maximum magnitude the vector part of rotation can be + // before it has violated a constraint. + // Multiplying by 0.5 uniquely maps angles 0-180 degrees to 0-1 which works + // nicely with unit quaternions. + val magnitude = sin(angle * 0.5f) + val magnitudeSqr = magnitude * magnitude + val sign = if (rotation.w >= 0f) 1f else -1f + var vector = rotation.xyz + var rot = rotation + + if (vector.lenSq() > magnitudeSqr) { + vector = vector.unit() * magnitude + rot = Quaternion(sqrt(1.0f - magnitudeSqr) * sign, vector) + } + + return rot.unit() + } + + private fun constrain(rotation: Quaternion, minAngle: Float, maxAngle: Float, axis: Vector3): Quaternion { + val magnitudeMin = sin(minAngle * 0.5f) + val magnitudeMax = sin(maxAngle * 0.5f) + val magnitudeSqrMin = magnitudeMin * magnitudeMin * if (minAngle >= 0f) 1f else -1f + val magnitudeSqrMax = magnitudeMax * magnitudeMax * if (maxAngle >= 0f) 1f else -1f + var vector = rotation.xyz + var rot = rotation + + val rotMagnitude = vector.lenSq() * if (vector.dot(axis) * sign(rot.w) < 0) -1f else 1f + if (rotMagnitude < magnitudeSqrMin || rotMagnitude > magnitudeSqrMax) { + val distToMin = min(abs(rotMagnitude - magnitudeSqrMin), abs(rotMagnitude + magnitudeSqrMin)) + val distToMax = min(abs(rotMagnitude - magnitudeSqrMax), abs(rotMagnitude + magnitudeSqrMax)) + + val magnitude = if (distToMin < distToMax) magnitudeMin else magnitudeMax + val magnitudeSqr = abs(if (distToMin < distToMax) magnitudeSqrMin else magnitudeSqrMax) + vector = vector.unit() * -magnitude + + rot = Quaternion(sqrt(1.0f - magnitudeSqr), vector) + } + + return rot.unit() + } + + // Constraint function for TwistSwingConstraint + private val twistSwingConstraint: ConstraintFunction = + { rotation: Quaternion, swingRad: Float, twistRad: Float, _: Float -> + var (swingQ, twistQ) = decompose(rotation, Vector3.NEG_Y) + swingQ = constrain(swingQ, swingRad) + twistQ = constrain(twistQ, twistRad) + + swingQ * twistQ + } + + // Constraint function for a hinge constraint with min and max angles + private val hingeConstraint: ConstraintFunction = + { rotation: Quaternion, min: Float, max: Float, _: Float -> + val (_, hingeAxisRot) = decompose(rotation, Vector3.NEG_X) + + constrain(hingeAxisRot, min, max, Vector3.NEG_X) + } + + // Constraint function for a hinge constraint with min and max angles that allows nonHingeDeviation + // rotation on all axis but the hinge + private val looseHingeConstraint: ConstraintFunction = + { rotation: Quaternion, min: Float, max: Float, nonHingeDeviation: Float -> + var (nonHingeRot, hingeAxisRot) = decompose(rotation, Vector3.NEG_X) + hingeAxisRot = constrain(hingeAxisRot, min, max, Vector3.NEG_X) + nonHingeRot = constrain(nonHingeRot, nonHingeDeviation) + + nonHingeRot * hingeAxisRot + } + + // Constraint function for CompleteConstraint + private val completeConstraint: ConstraintFunction = { rotation: Quaternion, _: Float, _: Float, _: Float -> + rotation + } + } +} diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/TransformNode.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/TransformNode.kt index 773e11e282..626cf35cbe 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/TransformNode.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/TransformNode.kt @@ -32,6 +32,11 @@ class TransformNode(val localRotation: Boolean) { } } + @ThreadSafe + fun updateThisNode() { + updateWorldTransforms() + } + @Synchronized private fun updateWorldTransforms() { worldTransform.set(localTransform) diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt index 66fb43ea17..2d26a447b7 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigManager.kt @@ -245,6 +245,20 @@ class SkeletonConfigManager( -getOffset(SkeletonConfigOffsets.FOOT_LENGTH), ) + BoneType.LEFT_UPPER_SHOULDER -> setNodeOffset( + nodeOffset, + 0f, + 0f, + 0f, + ) + + BoneType.RIGHT_UPPER_SHOULDER -> setNodeOffset( + nodeOffset, + 0f, + 0f, + 0f, + ) + BoneType.LEFT_SHOULDER -> setNodeOffset( nodeOffset, -getOffset(SkeletonConfigOffsets.SHOULDERS_WIDTH) / 2f, diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigToggles.java b/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigToggles.java index cffec046b0..d27021c159 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigToggles.java +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/config/SkeletonConfigToggles.java @@ -15,7 +15,10 @@ public enum SkeletonConfigToggles { VIVE_EMULATION(7, "Vive emulation", "viveEmulation", false), TOE_SNAP(8, "Toe Snap", "toeSnap", false), FOOT_PLANT(9, "Foot Plant", "footPlant", true), - SELF_LOCALIZATION(10, "Self Localization", "selfLocalization", false),; + SELF_LOCALIZATION(10, "Self Localization", "selfLocalization", false), + USE_POSITION(11, "Use Position", "usePosition", true), + ENFORCE_CONSTRAINTS(12, "Enforce Constraints", "enforceConstraints", true), + CORRECT_CONSTRAINTS(13, "Correct Constraints", "correctConstraints", true),; public static final SkeletonConfigToggles[] values = values(); private static final Map byStringVal = new HashMap<>(); diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt index c500ea2632..33295f5aa2 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt @@ -4,6 +4,8 @@ import com.jme3.math.FastMath import dev.slimevr.VRServer import dev.slimevr.tracking.processor.Bone import dev.slimevr.tracking.processor.BoneType +import dev.slimevr.tracking.processor.Constraint +import dev.slimevr.tracking.processor.Constraint.Companion.ConstraintType import dev.slimevr.tracking.processor.HumanPoseManager import dev.slimevr.tracking.processor.config.SkeletonConfigToggles import dev.slimevr.tracking.processor.config.SkeletonConfigValues @@ -35,77 +37,79 @@ class HumanSkeleton( val humanPoseManager: HumanPoseManager, ) { // Upper body bones - val headBone = Bone(BoneType.HEAD) - val neckBone = Bone(BoneType.NECK) - val upperChestBone = Bone(BoneType.UPPER_CHEST) - val chestBone = Bone(BoneType.CHEST) - val waistBone = Bone(BoneType.WAIST) - val hipBone = Bone(BoneType.HIP) + val headBone = Bone(BoneType.HEAD, Constraint(ConstraintType.COMPLETE)) + val neckBone = Bone(BoneType.NECK, Constraint(ConstraintType.COMPLETE)) + val upperChestBone = Bone(BoneType.UPPER_CHEST, Constraint(ConstraintType.TWIST_SWING, 90f, 120f)) + val chestBone = Bone(BoneType.CHEST, Constraint(ConstraintType.TWIST_SWING, 60f, 120f)) + val waistBone = Bone(BoneType.WAIST, Constraint(ConstraintType.TWIST_SWING, 60f, 120f)) + val hipBone = Bone(BoneType.HIP, Constraint(ConstraintType.TWIST_SWING, 60f, 120f)) // Lower body bones - val leftHipBone = Bone(BoneType.LEFT_HIP) - val rightHipBone = Bone(BoneType.RIGHT_HIP) - val leftUpperLegBone = Bone(BoneType.LEFT_UPPER_LEG) - val rightUpperLegBone = Bone(BoneType.RIGHT_UPPER_LEG) - val leftLowerLegBone = Bone(BoneType.LEFT_LOWER_LEG) - val rightLowerLegBone = Bone(BoneType.RIGHT_LOWER_LEG) - val leftFootBone = Bone(BoneType.LEFT_FOOT) - val rightFootBone = Bone(BoneType.RIGHT_FOOT) + val leftHipBone = Bone(BoneType.LEFT_HIP, Constraint(ConstraintType.TWIST_SWING, 0f, 15f)) + val rightHipBone = Bone(BoneType.RIGHT_HIP, Constraint(ConstraintType.TWIST_SWING, 0f, 15f)) + val leftUpperLegBone = Bone(BoneType.LEFT_UPPER_LEG, Constraint(ConstraintType.TWIST_SWING, 120f, 180f)) + val rightUpperLegBone = Bone(BoneType.RIGHT_UPPER_LEG, Constraint(ConstraintType.TWIST_SWING, 120f, 180f)) + val leftLowerLegBone = Bone(BoneType.LEFT_LOWER_LEG, Constraint(ConstraintType.LOOSE_HINGE, 180f, 0f, 50f)) + val rightLowerLegBone = Bone(BoneType.RIGHT_LOWER_LEG, Constraint(ConstraintType.LOOSE_HINGE, 180f, 0f, 50f)) + val leftFootBone = Bone(BoneType.LEFT_FOOT, Constraint(ConstraintType.TWIST_SWING, 60f, 60f)) + val rightFootBone = Bone(BoneType.RIGHT_FOOT, Constraint(ConstraintType.TWIST_SWING, 60f, 60f)) // Arm bones - val leftShoulderBone = Bone(BoneType.LEFT_SHOULDER) - val rightShoulderBone = Bone(BoneType.RIGHT_SHOULDER) - val leftUpperArmBone = Bone(BoneType.LEFT_UPPER_ARM) - val rightUpperArmBone = Bone(BoneType.RIGHT_UPPER_ARM) - val leftLowerArmBone = Bone(BoneType.LEFT_LOWER_ARM) - val rightLowerArmBone = Bone(BoneType.RIGHT_LOWER_ARM) - val leftHandBone = Bone(BoneType.LEFT_HAND) - val rightHandBone = Bone(BoneType.RIGHT_HAND) + val leftUpperShoulderBone = Bone(BoneType.LEFT_UPPER_SHOULDER, Constraint(ConstraintType.COMPLETE)) + val rightUpperShoulderBone = Bone(BoneType.RIGHT_UPPER_SHOULDER, Constraint(ConstraintType.COMPLETE)) + val leftShoulderBone = Bone(BoneType.LEFT_SHOULDER, Constraint(ConstraintType.TWIST_SWING, 0f, 10f)) + val rightShoulderBone = Bone(BoneType.RIGHT_SHOULDER, Constraint(ConstraintType.TWIST_SWING, 0f, 10f)) + val leftUpperArmBone = Bone(BoneType.LEFT_UPPER_ARM, Constraint(ConstraintType.TWIST_SWING, 120f, 180f)) + val rightUpperArmBone = Bone(BoneType.RIGHT_UPPER_ARM, Constraint(ConstraintType.TWIST_SWING, 120f, 180f)) + val leftLowerArmBone = Bone(BoneType.LEFT_LOWER_ARM, Constraint(ConstraintType.LOOSE_HINGE, 0f, -180f, 40f)) + val rightLowerArmBone = Bone(BoneType.RIGHT_LOWER_ARM, Constraint(ConstraintType.LOOSE_HINGE, 0f, -180f, 40f)) + val leftHandBone = Bone(BoneType.LEFT_HAND, Constraint(ConstraintType.TWIST_SWING, 120f, 120f)) + val rightHandBone = Bone(BoneType.RIGHT_HAND, Constraint(ConstraintType.TWIST_SWING, 120f, 120f)) // Finger bones - val leftThumbMetacarpalBone = Bone(BoneType.LEFT_THUMB_METACARPAL) - val leftThumbProximalBone = Bone(BoneType.LEFT_THUMB_PROXIMAL) - val leftThumbDistalBone = Bone(BoneType.LEFT_THUMB_DISTAL) - val leftIndexProximalBone = Bone(BoneType.LEFT_INDEX_PROXIMAL) - val leftIndexIntermediateBone = Bone(BoneType.LEFT_INDEX_INTERMEDIATE) - val leftIndexDistalBone = Bone(BoneType.LEFT_INDEX_DISTAL) - val leftMiddleProximalBone = Bone(BoneType.LEFT_MIDDLE_PROXIMAL) - val leftMiddleIntermediateBone = Bone(BoneType.LEFT_MIDDLE_INTERMEDIATE) - val leftMiddleDistalBone = Bone(BoneType.LEFT_MIDDLE_DISTAL) - val leftRingProximalBone = Bone(BoneType.LEFT_RING_PROXIMAL) - val leftRingIntermediateBone = Bone(BoneType.LEFT_RING_INTERMEDIATE) - val leftRingDistalBone = Bone(BoneType.LEFT_RING_DISTAL) - val leftLittleProximalBone = Bone(BoneType.LEFT_LITTLE_PROXIMAL) - val leftLittleIntermediateBone = Bone(BoneType.LEFT_LITTLE_INTERMEDIATE) - val leftLittleDistalBone = Bone(BoneType.LEFT_LITTLE_DISTAL) - val rightThumbMetacarpalBone = Bone(BoneType.RIGHT_THUMB_METACARPAL) - val rightThumbProximalBone = Bone(BoneType.RIGHT_THUMB_PROXIMAL) - val rightThumbDistalBone = Bone(BoneType.RIGHT_THUMB_DISTAL) - val rightIndexProximalBone = Bone(BoneType.RIGHT_INDEX_PROXIMAL) - val rightIndexIntermediateBone = Bone(BoneType.RIGHT_INDEX_INTERMEDIATE) - val rightIndexDistalBone = Bone(BoneType.RIGHT_INDEX_DISTAL) - val rightMiddleProximalBone = Bone(BoneType.RIGHT_MIDDLE_PROXIMAL) - val rightMiddleIntermediateBone = Bone(BoneType.RIGHT_MIDDLE_INTERMEDIATE) - val rightMiddleDistalBone = Bone(BoneType.RIGHT_MIDDLE_DISTAL) - val rightRingProximalBone = Bone(BoneType.RIGHT_RING_PROXIMAL) - val rightRingIntermediateBone = Bone(BoneType.RIGHT_RING_INTERMEDIATE) - val rightRingDistalBone = Bone(BoneType.RIGHT_RING_DISTAL) - val rightLittleProximalBone = Bone(BoneType.RIGHT_LITTLE_PROXIMAL) - val rightLittleIntermediateBone = Bone(BoneType.RIGHT_LITTLE_INTERMEDIATE) - val rightLittleDistalBone = Bone(BoneType.RIGHT_LITTLE_DISTAL) + val leftThumbMetacarpalBone = Bone(BoneType.LEFT_THUMB_METACARPAL, Constraint(ConstraintType.COMPLETE)) + val leftThumbProximalBone = Bone(BoneType.LEFT_THUMB_PROXIMAL, Constraint(ConstraintType.COMPLETE)) + val leftThumbDistalBone = Bone(BoneType.LEFT_THUMB_DISTAL, Constraint(ConstraintType.COMPLETE)) + val leftIndexProximalBone = Bone(BoneType.LEFT_INDEX_PROXIMAL, Constraint(ConstraintType.COMPLETE)) + val leftIndexIntermediateBone = Bone(BoneType.LEFT_INDEX_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) + val leftIndexDistalBone = Bone(BoneType.LEFT_INDEX_DISTAL, Constraint(ConstraintType.COMPLETE)) + val leftMiddleProximalBone = Bone(BoneType.LEFT_MIDDLE_PROXIMAL, Constraint(ConstraintType.COMPLETE)) + val leftMiddleIntermediateBone = Bone(BoneType.LEFT_MIDDLE_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) + val leftMiddleDistalBone = Bone(BoneType.LEFT_MIDDLE_DISTAL, Constraint(ConstraintType.COMPLETE)) + val leftRingProximalBone = Bone(BoneType.LEFT_RING_PROXIMAL, Constraint(ConstraintType.COMPLETE)) + val leftRingIntermediateBone = Bone(BoneType.LEFT_RING_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) + val leftRingDistalBone = Bone(BoneType.LEFT_RING_DISTAL, Constraint(ConstraintType.COMPLETE)) + val leftLittleProximalBone = Bone(BoneType.LEFT_LITTLE_PROXIMAL, Constraint(ConstraintType.COMPLETE)) + val leftLittleIntermediateBone = Bone(BoneType.LEFT_LITTLE_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) + val leftLittleDistalBone = Bone(BoneType.LEFT_LITTLE_DISTAL, Constraint(ConstraintType.COMPLETE)) + val rightThumbMetacarpalBone = Bone(BoneType.RIGHT_THUMB_METACARPAL, Constraint(ConstraintType.COMPLETE)) + val rightThumbProximalBone = Bone(BoneType.RIGHT_THUMB_PROXIMAL, Constraint(ConstraintType.COMPLETE)) + val rightThumbDistalBone = Bone(BoneType.RIGHT_THUMB_DISTAL, Constraint(ConstraintType.COMPLETE)) + val rightIndexProximalBone = Bone(BoneType.RIGHT_INDEX_PROXIMAL, Constraint(ConstraintType.COMPLETE)) + val rightIndexIntermediateBone = Bone(BoneType.RIGHT_INDEX_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) + val rightIndexDistalBone = Bone(BoneType.RIGHT_INDEX_DISTAL, Constraint(ConstraintType.COMPLETE)) + val rightMiddleProximalBone = Bone(BoneType.RIGHT_MIDDLE_PROXIMAL, Constraint(ConstraintType.COMPLETE)) + val rightMiddleIntermediateBone = Bone(BoneType.RIGHT_MIDDLE_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) + val rightMiddleDistalBone = Bone(BoneType.RIGHT_MIDDLE_DISTAL, Constraint(ConstraintType.COMPLETE)) + val rightRingProximalBone = Bone(BoneType.RIGHT_RING_PROXIMAL, Constraint(ConstraintType.COMPLETE)) + val rightRingIntermediateBone = Bone(BoneType.RIGHT_RING_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) + val rightRingDistalBone = Bone(BoneType.RIGHT_RING_DISTAL, Constraint(ConstraintType.COMPLETE)) + val rightLittleProximalBone = Bone(BoneType.RIGHT_LITTLE_PROXIMAL, Constraint(ConstraintType.COMPLETE)) + val rightLittleIntermediateBone = Bone(BoneType.RIGHT_LITTLE_INTERMEDIATE, Constraint(ConstraintType.COMPLETE)) + val rightLittleDistalBone = Bone(BoneType.RIGHT_LITTLE_DISTAL, Constraint(ConstraintType.COMPLETE)) // Tracker bones - val headTrackerBone = Bone(BoneType.HEAD_TRACKER) - val chestTrackerBone = Bone(BoneType.CHEST_TRACKER) - val hipTrackerBone = Bone(BoneType.HIP_TRACKER) - val leftKneeTrackerBone = Bone(BoneType.LEFT_KNEE_TRACKER) - val rightKneeTrackerBone = Bone(BoneType.RIGHT_KNEE_TRACKER) - val leftFootTrackerBone = Bone(BoneType.LEFT_FOOT_TRACKER) - val rightFootTrackerBone = Bone(BoneType.RIGHT_FOOT_TRACKER) - val leftElbowTrackerBone = Bone(BoneType.LEFT_ELBOW_TRACKER) - val rightElbowTrackerBone = Bone(BoneType.RIGHT_ELBOW_TRACKER) - val leftHandTrackerBone = Bone(BoneType.LEFT_HAND_TRACKER) - val rightHandTrackerBone = Bone(BoneType.RIGHT_HAND_TRACKER) + val headTrackerBone = Bone(BoneType.HEAD_TRACKER, Constraint(ConstraintType.COMPLETE)) + val chestTrackerBone = Bone(BoneType.CHEST_TRACKER, Constraint(ConstraintType.COMPLETE)) + val hipTrackerBone = Bone(BoneType.HIP_TRACKER, Constraint(ConstraintType.COMPLETE)) + val leftKneeTrackerBone = Bone(BoneType.LEFT_KNEE_TRACKER, Constraint(ConstraintType.COMPLETE)) + val rightKneeTrackerBone = Bone(BoneType.RIGHT_KNEE_TRACKER, Constraint(ConstraintType.COMPLETE)) + val leftFootTrackerBone = Bone(BoneType.LEFT_FOOT_TRACKER, Constraint(ConstraintType.COMPLETE)) + val rightFootTrackerBone = Bone(BoneType.RIGHT_FOOT_TRACKER, Constraint(ConstraintType.COMPLETE)) + val leftElbowTrackerBone = Bone(BoneType.LEFT_ELBOW_TRACKER, Constraint(ConstraintType.COMPLETE)) + val rightElbowTrackerBone = Bone(BoneType.RIGHT_ELBOW_TRACKER, Constraint(ConstraintType.COMPLETE)) + val leftHandTrackerBone = Bone(BoneType.LEFT_HAND_TRACKER, Constraint(ConstraintType.COMPLETE)) + val rightHandTrackerBone = Bone(BoneType.RIGHT_HAND_TRACKER, Constraint(ConstraintType.COMPLETE)) // Buffers var hasSpineTracker = false @@ -190,6 +194,8 @@ class HumanSkeleton( private var extendedPelvisModel = false private var extendedKneeModel = false private var forceArmsFromHMD = true + private var enforceConstraints = true + private var correctConstraints = true // Ratios private var waistFromChestHipAveraging = 0f @@ -292,8 +298,10 @@ class HumanSkeleton( } // Shoulders - neckBone.attachChild(leftShoulderBone) - neckBone.attachChild(rightShoulderBone) + neckBone.attachChild(leftUpperShoulderBone) + neckBone.attachChild(rightUpperShoulderBone) + leftUpperShoulderBone.attachChild(leftShoulderBone) + rightUpperShoulderBone.attachChild(rightShoulderBone) // Upper arm leftShoulderBone.attachChild(leftUpperArmBone) @@ -442,6 +450,9 @@ class HumanSkeleton( // Update tap detection's trackers tapDetectionManager.updateConfig(trackers) + + // Update bones tracker field + refreshBoneTracker() } /** @@ -500,6 +511,7 @@ class HumanSkeleton( updateTransforms() updateBones() + headBone.updateWithConstraints() updateComputedTrackers() // Don't run post-processing if the tracking is paused @@ -510,6 +522,15 @@ class HumanSkeleton( viveEmulation.update() } + /** + * Refresh the attachedTracker field in each bone + */ + private fun refreshBoneTracker() { + for (bone in allHumanBones) { + bone.attachedTracker = getTrackerForBone(bone.boneType) + } + } + /** * Update all the bones by updating the roots */ @@ -560,6 +581,7 @@ class HumanSkeleton( // Left arm updateArmTransforms( isTrackingLeftArmFromController, + leftUpperShoulderBone, leftShoulderBone, leftUpperArmBone, leftElbowTrackerBone, @@ -575,6 +597,7 @@ class HumanSkeleton( // Right arm updateArmTransforms( isTrackingRightArmFromController, + rightUpperShoulderBone, rightShoulderBone, rightUpperArmBone, rightElbowTrackerBone, @@ -925,6 +948,7 @@ class HumanSkeleton( */ private fun updateArmTransforms( isTrackingFromController: Boolean, + upperShoulderBone: Bone, shoulderBone: Bone, upperArmBone: Bone, elbowTrackerBone: Bone, @@ -957,6 +981,7 @@ class HumanSkeleton( // Get shoulder rotation var armRot = shoulderTracker?.getRotation() ?: upperChestBone.getLocalRotation() // Set shoulder rotation + upperShoulderBone.setRotation(upperChestBone.getLocalRotation()) shoulderBone.setRotation(armRot) if (upperArmTracker != null || lowerArmTracker != null) { @@ -1136,6 +1161,12 @@ class HumanSkeleton( SkeletonConfigToggles.FOOT_PLANT -> legTweaks.footPlantEnabled = newValue SkeletonConfigToggles.SELF_LOCALIZATION -> localizer.setEnabled(newValue) + + SkeletonConfigToggles.USE_POSITION -> newValue + + SkeletonConfigToggles.ENFORCE_CONSTRAINTS -> enforceConstraints = newValue + + SkeletonConfigToggles.CORRECT_CONSTRAINTS -> correctConstraints = newValue } } @@ -1217,6 +1248,8 @@ class HumanSkeleton( BoneType.RIGHT_FOOT -> rightFootBone BoneType.LEFT_FOOT_TRACKER -> leftFootTrackerBone BoneType.RIGHT_FOOT_TRACKER -> rightFootTrackerBone + BoneType.LEFT_UPPER_SHOULDER -> leftUpperShoulderBone + BoneType.RIGHT_UPPER_SHOULDER -> rightUpperShoulderBone BoneType.LEFT_SHOULDER -> leftShoulderBone BoneType.RIGHT_SHOULDER -> rightShoulderBone BoneType.LEFT_UPPER_ARM -> leftUpperArmBone @@ -1261,6 +1294,30 @@ class HumanSkeleton( BoneType.RIGHT_LITTLE_DISTAL -> rightLittleDistalBone } + private fun getTrackerForBone(bone: BoneType?): Tracker? = when (bone) { + BoneType.HEAD -> headTracker + BoneType.NECK -> neckTracker + BoneType.UPPER_CHEST -> upperChestTracker + BoneType.CHEST -> chestTracker + BoneType.WAIST -> waistTracker + BoneType.HIP -> hipTracker + BoneType.LEFT_UPPER_LEG -> leftUpperLegTracker + BoneType.RIGHT_UPPER_LEG -> rightUpperLegTracker + BoneType.LEFT_LOWER_LEG -> leftLowerLegTracker + BoneType.RIGHT_LOWER_LEG -> rightLowerLegTracker + BoneType.LEFT_FOOT -> leftFootTracker + BoneType.RIGHT_FOOT -> rightFootTracker + BoneType.LEFT_SHOULDER -> leftShoulderTracker + BoneType.RIGHT_SHOULDER -> rightShoulderTracker + BoneType.LEFT_UPPER_ARM -> leftUpperArmTracker + BoneType.RIGHT_UPPER_ARM -> rightUpperArmTracker + BoneType.LEFT_LOWER_ARM -> leftLowerArmTracker + BoneType.RIGHT_LOWER_ARM -> rightLowerArmTracker + BoneType.LEFT_HAND -> leftHandTracker + BoneType.RIGHT_HAND -> rightHandTracker + else -> null + } + /** * Returns an array of all the non-tracker bones. */ @@ -1280,6 +1337,8 @@ class HumanSkeleton( rightLowerLegBone, leftFootBone, rightFootBone, + leftUpperShoulderBone, + rightUpperShoulderBone, leftShoulderBone, rightShoulderBone, leftUpperArmBone, @@ -1325,6 +1384,8 @@ class HumanSkeleton( */ private val allArmBones: Array get() = arrayOf( + leftUpperShoulderBone, + rightUpperShoulderBone, leftShoulderBone, rightShoulderBone, leftUpperArmBone, diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt index 87a6e0a805..1ab9c2ecbb 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerFilteringHandler.kt @@ -58,4 +58,9 @@ class TrackerFilteringHandler { * Get the filtered rotation from the moving average (either prediction/smoothing or just >180 degs) */ fun getFilteredRotation() = movingAverage.filteredQuaternion + + /** + * Get the impact filtering has on the rotation + */ + fun getFilteringImpact(): Float = movingAverage.filteringImpact } diff --git a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt index c90414dd65..1e21fbf8f3 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/trackers/TrackerResetsHandler.kt @@ -63,6 +63,7 @@ class TrackerResetsHandler(val tracker: Tracker) { var mountRotFix = Quaternion.IDENTITY private set private var yawFix = Quaternion.IDENTITY + private var constraintFix = Quaternion.IDENTITY // Yaw reset smoothing vars private var yawFixOld = Quaternion.IDENTITY @@ -168,6 +169,7 @@ class TrackerResetsHandler(val tracker: Tracker) { rot = mountRotFix.inv() * (rot * mountRotFix) rot *= tposeDownFix rot = yawFix * rot + rot = constraintFix * rot return rot } @@ -180,6 +182,7 @@ class TrackerResetsHandler(val tracker: Tracker) { rot = gyroFixNoMounting * rot rot *= attachmentFixNoMounting rot = yawFixZeroReference * rot + rot = constraintFix * rot return rot } @@ -214,6 +217,8 @@ class TrackerResetsHandler(val tracker: Tracker) { * 0). This allows the tracker to be strapped to body at any pitch and roll. */ fun resetFull(reference: Quaternion) { + constraintFix = Quaternion.IDENTITY + if (tracker.trackerDataType == TrackerDataType.FLEX_RESISTANCE) { tracker.trackerFlexHandler.resetMin() postProcessResetFull() @@ -305,6 +310,8 @@ class TrackerResetsHandler(val tracker: Tracker) { * position should be corrected in the source. */ fun resetYaw(reference: Quaternion) { + constraintFix = Quaternion.IDENTITY + if (tracker.trackerDataType == TrackerDataType.FLEX_RESISTANCE || tracker.trackerDataType == TrackerDataType.FLEX_ANGLE ) { @@ -355,6 +362,8 @@ class TrackerResetsHandler(val tracker: Tracker) { return } + constraintFix = Quaternion.IDENTITY + // Get the current calibrated rotation var rotBuf = adjustToDrift(tracker.getRawRotation() * mountingOrientation) rotBuf = gyroFix * rotBuf @@ -403,6 +412,13 @@ class TrackerResetsHandler(val tracker: Tracker) { tracker.resetFilteringQuats() } + /** + * Apply a corrective rotation to the gyroFix + */ + fun updateConstraintFix(correctedRotation: Quaternion) { + constraintFix *= correctedRotation + } + fun clearMounting() { mountRotFix = Quaternion.IDENTITY } From d102cb377871a77e49dd6ed48b54bd38f9538f1a Mon Sep 17 00:00:00 2001 From: Uriel Date: Sun, 26 Jan 2025 17:33:52 +0100 Subject: [PATCH 13/16] add university of maryland mirror for Eclipse formatter (#1289) --- gradle.properties | 2 +- server/build.gradle.kts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 613dd48626..da78dfd873 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ android.nonTransitiveRClass=true org.gradle.unsafe.configuration-cache=false kotlinVersion=2.0.20 -spotlessVersion=6.25.0 +spotlessVersion=7.0.2 shadowJarVersion=8.3.2 buildconfigVersion=5.5.0 grgitVersion=5.2.2 diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 11ddef897f..a273d1a986 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -20,7 +20,7 @@ configure { // define the steps to apply to those files trimTrailingWhitespace() endWithNewline() - indentWithTabs() + leadingSpacesToTabs() } // format "yaml", { // target "*.yml", "*.yaml", @@ -61,6 +61,8 @@ configure { removeUnusedImports() // Use eclipse JDT formatter - eclipse().configFile("spotless.xml") + eclipse() + .configFile("spotless.xml") + .withP2Mirrors(mapOf("https://download.eclipse.org/" to "https://mirror.umd.edu/eclipse/")) } } From eb08cb5aa1c5b69a8b35378b2574a23d722d5002 Mon Sep 17 00:00:00 2001 From: Uriel Date: Wed, 29 Jan 2025 14:52:49 +0100 Subject: [PATCH 14/16] Improve close window request checks (#1285) --- flake.nix | 2 +- gui/src-tauri/capabilities/migrated.json | 11 ++--- gui/src-tauri/src/main.rs | 3 +- gui/src/components/TopBar.tsx | 53 +++++++++++++++++------- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/flake.nix b/flake.nix index 2bdecbac9d..d33070ad51 100644 --- a/flake.nix +++ b/flake.nix @@ -140,7 +140,7 @@ enterShell = with pkgs; '' # Export a LD_LIBRARY_PATH without libudev-zero as libgudev not likey export SLIMEVR_RUST_LD_LIBRARY_PATH="$LD_LIBRARY_PATH" - export LD_LIBRARY_PATH="${libudev-zero}/lib:$LD_LIBRARY_PATH" + export LD_LIBRARY_PATH="${libudev-zero}/lib:${libayatana-appindicator}/lib:$LD_LIBRARY_PATH" # GStreamer plugins won't be found without this export GST_PLUGIN_SYSTEM_PATH_1_0="${pkgs.gst_all_1.gstreamer.out}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-base}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-good}/lib/gstreamer-1.0:${pkgs.gst_all_1.gst-plugins-bad}/lib/gstreamer-1.0" ''; diff --git a/gui/src-tauri/capabilities/migrated.json b/gui/src-tauri/capabilities/migrated.json index 5e8916e035..c284614949 100644 --- a/gui/src-tauri/capabilities/migrated.json +++ b/gui/src-tauri/capabilities/migrated.json @@ -6,14 +6,7 @@ "main" ], "permissions": [ - "core:path:default", - "core:event:default", - "core:window:default", - "core:app:default", - "core:resources:default", - "core:menu:default", - "core:tray:default", - "core:webview:default", + "core:default", "core:window:allow-close", "core:window:allow-toggle-maximize", "core:window:allow-minimize", @@ -21,6 +14,8 @@ "core:window:allow-hide", "core:window:allow-show", "core:window:allow-set-focus", + "core:window:allow-destroy", + "core:window:allow-request-user-attention", "core:window:allow-set-decorations", "store:default", "os:allow-os-type", diff --git a/gui/src-tauri/src/main.rs b/gui/src-tauri/src/main.rs index fda7f594a1..f40da30ab0 100644 --- a/gui/src-tauri/src/main.rs +++ b/gui/src-tauri/src/main.rs @@ -13,7 +13,8 @@ use clap::Parser; use color_eyre::Result; use state::WindowState; use tauri::Emitter; -use tauri::{Manager, RunEvent, WindowEvent}; +use tauri::WindowEvent; +use tauri::{Manager, RunEvent}; use tauri_plugin_shell::process::CommandChild; use crate::util::{ diff --git a/gui/src/components/TopBar.tsx b/gui/src/components/TopBar.tsx index 4e5f76f44d..c9495a4d6c 100644 --- a/gui/src/components/TopBar.tsx +++ b/gui/src/components/TopBar.tsx @@ -1,4 +1,3 @@ -import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import { ReactNode, useContext, useEffect, useState } from 'react'; import { NavLink, useMatch } from 'react-router-dom'; import { @@ -25,11 +24,16 @@ import { invoke } from '@tauri-apps/api/core'; import { useTrackers } from '@/hooks/tracker'; import { TrackersStillOnModal } from './TrackersStillOnModal'; import { useConfig } from '@/hooks/config'; -import { listen } from '@tauri-apps/api/event'; +import { listen, TauriEvent } from '@tauri-apps/api/event'; import { TrayOrExitModal } from './TrayOrExitModal'; import { error } from '@/utils/logging'; import { useDoubleTap } from 'use-double-tap'; import { isTrayAvailable } from '@/utils/tauri'; +import { + CloseRequestedEvent, + getCurrentWindow, + UserAttentionType, +} from '@tauri-apps/api/window'; export function VersionTag() { return ( @@ -70,10 +74,11 @@ export function TopBar({ const doesMatchSettings = useMatch({ path: '/settings/*', }); + const closeApp = async () => { await saveConfig(); await invoke('update_window_state'); - await getCurrentWebviewWindow().close(); + await getCurrentWindow().destroy(); }; const tryCloseApp = async (dontTray = false) => { if (isTrayAvailable && config?.useTray === null) { @@ -81,8 +86,8 @@ export function TopBar({ return; } - if (config?.useTray && !dontTray) { - await getCurrentWebviewWindow().hide(); + if (isTrayAvailable && config?.useTray && !dontTray) { + await getCurrentWindow().hide(); await invoke('update_tray_text'); } else if ( config?.connectedTrackersWarning && @@ -95,25 +100,42 @@ export function TopBar({ await closeApp(); } }; + const showVersionBind = useDoubleTap(() => setShowVersionMobile(true)); const unshowVersionBind = useDoubleTap(() => setShowVersionMobile(false)); useEffect(() => { - const unlisten = listen('try-close', async () => { - const window = getCurrentWebviewWindow(); + const unlistenTrayClose = listen('try-close', async () => { + const window = getCurrentWindow(); await window.show(); + await window.requestUserAttention(UserAttentionType.Critical); await window.setFocus(); if (isTrayAvailable) await invoke('update_tray_text'); await tryCloseApp(true); }); + + const unlistenCloseRequested = getCurrentWindow().listen( + TauriEvent.WINDOW_CLOSE_REQUESTED, + async (data) => { + const ev = new CloseRequestedEvent(data); + ev.preventDefault(); + await tryCloseApp(); + } + ); + return () => { - unlisten.then((fn) => fn()); + unlistenTrayClose.then((fn) => fn()); + unlistenCloseRequested.then((fn) => fn()); }; - }, [config?.useTray, config?.connectedTrackersWarning]); + }, [ + config?.useTray, + config?.connectedTrackersWarning, + JSON.stringify(connectedIMUTrackers.map((t) => t.tracker.status)), + ]); useEffect(() => { if (config === null || !isTauri) return; - getCurrentWebviewWindow().setDecorations(config?.decorations).catch(error); + getCurrentWindow().setDecorations(config?.decorations).catch(error); }, [config?.decorations]); useEffect(() => { @@ -252,13 +274,13 @@ export function TopBar({ <>
getCurrentWebviewWindow().minimize()} + onClick={() => getCurrentWindow().minimize()} >
getCurrentWebviewWindow().toggleMaximize()} + onClick={() => getCurrentWindow().toggleMaximize()} >
@@ -286,7 +308,7 @@ export function TopBar({ // Doing this in here just in case config doesn't get updated in time if (useTray) { - await getCurrentWebviewWindow().hide(); + await getCurrentWindow().hide(); await invoke('update_tray_text'); } else if ( config?.connectedTrackersWarning && @@ -304,7 +326,10 @@ export function TopBar({ closeApp()} - cancel={() => setConnectedTrackerWarning(false)} + cancel={() => { + setConnectedTrackerWarning(false); + getCurrentWindow().requestUserAttention(null); + }} > ); From d389b5acecc3ee036cdfb794ce4b4d8d755da71f Mon Sep 17 00:00:00 2001 From: Butterscotch! Date: Wed, 29 Jan 2025 08:56:36 -0500 Subject: [PATCH 15/16] Implement skeleton constraint toggles (#1290) --- .../src/main/java/dev/slimevr/tracking/processor/Bone.kt | 7 ++++--- .../slimevr/tracking/processor/skeleton/HumanSkeleton.kt | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt index ee2ce87d4c..aed9d7768c 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/Bone.kt @@ -67,7 +67,7 @@ class Bone(val boneType: BoneType, val rotationConstraint: Constraint) { * this bone and all of its children while * enforcing rotation constraints. */ - fun updateWithConstraints() { + fun updateWithConstraints(correctConstraints: Boolean) { val initialRot = getGlobalRotation() val newRot = rotationConstraint.applyConstraint(initialRot, this) setRotationRaw(newRot) @@ -82,7 +82,8 @@ class Bone(val boneType: BoneType, val rotationConstraint: Constraint) { val deltaRot = newRot * initialRot.inv() val angle = deltaRot.angleR() - if (angle > Constraint.ANGLE_THRESHOLD && + if (correctConstraints && + angle > Constraint.ANGLE_THRESHOLD && (attachedTracker?.filteringHandler?.getFilteringImpact() ?: 1f) < Constraint.FILTER_IMPACT_THRESHOLD && (parent?.attachedTracker?.filteringHandler?.getFilteringImpact() ?: 0f) < Constraint.FILTER_IMPACT_THRESHOLD ) { @@ -92,7 +93,7 @@ class Bone(val boneType: BoneType, val rotationConstraint: Constraint) { // Recursively apply constraints and update children. for (child in children) { - child.updateWithConstraints() + child.updateWithConstraints(correctConstraints) } } diff --git a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt index 33295f5aa2..728faedf1d 100644 --- a/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt +++ b/server/core/src/main/java/dev/slimevr/tracking/processor/skeleton/HumanSkeleton.kt @@ -511,7 +511,9 @@ class HumanSkeleton( updateTransforms() updateBones() - headBone.updateWithConstraints() + if (enforceConstraints) { + headBone.updateWithConstraints(correctConstraints) + } updateComputedTrackers() // Don't run post-processing if the tracking is paused From 4ea57510edebeb3e04aa00261c404320b570e360 Mon Sep 17 00:00:00 2001 From: Uriel Date: Wed, 29 Jan 2025 15:05:00 +0100 Subject: [PATCH 16/16] Make SlimeVR icon rounded (#1286) Co-authored-by: lucas lelievre --- gui/package.json | 5 +++-- gui/src-tauri/icons/1024x1024.png | Bin 33365 -> 0 bytes gui/src-tauri/icons/128x128.png | Bin 5198 -> 2808 bytes gui/src-tauri/icons/128x128@2x.png | Bin 11263 -> 5884 bytes gui/src-tauri/icons/32x32.png | Bin 1135 -> 747 bytes gui/src-tauri/icons/Square107x107Logo.png | Bin 4255 -> 2336 bytes gui/src-tauri/icons/Square142x142Logo.png | Bin 5876 -> 3177 bytes gui/src-tauri/icons/Square150x150Logo.png | Bin 6317 -> 3321 bytes gui/src-tauri/icons/Square284x284Logo.png | Bin 12502 -> 6460 bytes gui/src-tauri/icons/Square30x30Logo.png | Bin 1072 -> 698 bytes gui/src-tauri/icons/Square310x310Logo.png | Bin 13647 -> 6951 bytes gui/src-tauri/icons/Square44x44Logo.png | Bin 1655 -> 1008 bytes gui/src-tauri/icons/Square71x71Logo.png | Bin 2709 -> 1534 bytes gui/src-tauri/icons/Square89x89Logo.png | Bin 3520 -> 2027 bytes gui/src-tauri/icons/StoreLogo.png | Bin 1862 -> 1163 bytes .../icons/android/mipmap-hdpi/ic_launcher.png | Bin 1731 -> 1190 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 6778 -> 3581 bytes .../android/mipmap-hdpi/ic_launcher_round.png | Bin 1731 -> 1190 bytes .../icons/android/mipmap-mdpi/ic_launcher.png | Bin 1771 -> 1137 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 4247 -> 2448 bytes .../android/mipmap-mdpi/ic_launcher_round.png | Bin 1771 -> 1137 bytes .../android/mipmap-xhdpi/ic_launcher.png | Bin 3825 -> 2112 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 9377 -> 4960 bytes .../mipmap-xhdpi/ic_launcher_round.png | Bin 3825 -> 2112 bytes .../android/mipmap-xxhdpi/ic_launcher.png | Bin 5985 -> 3185 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 14184 -> 7081 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 5985 -> 3185 bytes .../android/mipmap-xxxhdpi/ic_launcher.png | Bin 8110 -> 4236 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 19821 -> 9824 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 8110 -> 4236 bytes gui/src-tauri/icons/icon.icns | Bin 113344 -> 69880 bytes gui/src-tauri/icons/icon.ico | Bin 18968 -> 12393 bytes gui/src-tauri/icons/icon.png | Bin 23908 -> 11880 bytes gui/src-tauri/icons/icon.svg | 2 +- gui/src-tauri/icons/ios/AppIcon-20x20@1x.png | Bin 702 -> 555 bytes .../icons/ios/AppIcon-20x20@2x-1.png | Bin 1475 -> 848 bytes gui/src-tauri/icons/ios/AppIcon-20x20@2x.png | Bin 1475 -> 848 bytes gui/src-tauri/icons/ios/AppIcon-20x20@3x.png | Bin 2241 -> 1310 bytes gui/src-tauri/icons/ios/AppIcon-29x29@1x.png | Bin 1008 -> 645 bytes .../icons/ios/AppIcon-29x29@2x-1.png | Bin 2159 -> 1287 bytes gui/src-tauri/icons/ios/AppIcon-29x29@2x.png | Bin 2159 -> 1287 bytes gui/src-tauri/icons/ios/AppIcon-29x29@3x.png | Bin 3401 -> 1921 bytes gui/src-tauri/icons/ios/AppIcon-40x40@1x.png | Bin 1475 -> 848 bytes .../icons/ios/AppIcon-40x40@2x-1.png | Bin 3046 -> 1742 bytes gui/src-tauri/icons/ios/AppIcon-40x40@2x.png | Bin 3046 -> 1742 bytes gui/src-tauri/icons/ios/AppIcon-40x40@3x.png | Bin 4947 -> 2527 bytes gui/src-tauri/icons/ios/AppIcon-512@2x.png | Bin 31122 -> 26091 bytes gui/src-tauri/icons/ios/AppIcon-60x60@2x.png | Bin 4947 -> 2527 bytes gui/src-tauri/icons/ios/AppIcon-60x60@3x.png | Bin 7484 -> 3857 bytes gui/src-tauri/icons/ios/AppIcon-76x76@1x.png | Bin 2880 -> 1621 bytes gui/src-tauri/icons/ios/AppIcon-76x76@2x.png | Bin 6349 -> 3047 bytes .../icons/ios/AppIcon-83.5x83.5@2x.png | Bin 7049 -> 3510 bytes 52 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 gui/src-tauri/icons/1024x1024.png diff --git a/gui/package.json b/gui/package.json index 00bd7da5ca..00614168b5 100644 --- a/gui/package.json +++ b/gui/package.json @@ -55,7 +55,8 @@ "preview-vite": "vite preview", "javaversion-build": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class", "gen:javaversion": "cd src-tauri/src/ && javac JavaVersion.java && jar cvfe JavaVersion.jar JavaVersion JavaVersion.class", - "gen:firmware-tool": "openapi-codegen gen firmwareTool" + "gen:firmware-tool": "openapi-codegen gen firmwareTool", + "gen:icons": "tauri icon --ios-color '#663499' src-tauri/icons/icon.svg" }, "devDependencies": { "@dword-design/eslint-plugin-import-alias": "^4.0.9", @@ -93,4 +94,4 @@ "vite": "^5.4.8", "typescript-eslint": "^8.8.0" } -} \ No newline at end of file +} diff --git a/gui/src-tauri/icons/1024x1024.png b/gui/src-tauri/icons/1024x1024.png deleted file mode 100644 index 1e96b691c0b3a0bc6bd5120e4a0e5ffc0213bbc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33365 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 DTnRm} literal 5198 zcmcJTlgLgRwR*%d5W`JhLaGl_DV%DVyLtxZp*T1-^B0ba)tR( zS9;s5Ud4O}Wa=jxS>77{mE%dIB>A(y9V_RUz1-X8NG^}>pa(0~tDW7h+wrFl{`VLK zFJYM*_79O=joWE_%@wXT&?H6U5l*@Z+u$mI#{6Y4FRiIC9fPu_RwdBuxp^6+zPBq*7wKxr(<{2<|Z zQt8wd{`Bsg-)9$d&bitp6_@w!rOR{ey!5_&-7?X2{t*Wt6u=fDiq4LmllW5)m;JZT zf8uKHf-m&&TDm+{57Tn~ug;{a1Q0}d{LPiUjEw~fckqYxkF?m?6~QKp!Zaa^X9 z$UItCN+dm**3zNFX#BgRFZJc2ItV;Kqz>)fjd+89-fNJou)&Ktv1NoUg;(F6Rc6U# zx@ThqWN&zi9d#_It}zu3NdVg=IodiXnGo(>-YO{KJ1&}+wc-D7uCO*j@6ET)7mm|T zwvu4%3cHm5mUj-)BIXsyD2uV#`RQ@}Ua`D{NFaRaF9x@zKv^wU6s55BypYTDb=9K8 zCd`YO?W@Sh$G+*8mGmz)x56ZHA8A9zx}&EYASC4f(xHp9lW^(WGr{9(H{DeBWXhAC zFoU@YX_oS|e5e1;D+V3qWEHw}uWJ+iFww9YH&^??D4h)FCtTbIZDG-tgMg2L29U8J zCkSqOgF2r{WfAtYy6F94qr>p|EmLhr;$EyM)uS$#VM> z$x5~@07-*fxK};>VOn+)L5_)d3h0r7$g7xoSzF)wTiwaJnYPC(S|0-i#V69oMFa-3 zB03g7dZqfYh=8iA?5k$KCs`0lcggwYT|^VyoF3fCj%4EzK^rI$JqG(y}xLfZ7nmz2!9d$P7lDg=T2OUs6zdCwPXUQkJr~Tz3F&Ox7HgQvp9oigmYo< zF3-ZzFb$Mlywr3~qx{Sss+XLeI`7|6|Kgj0rf($)F*su9{t}-~g2qYGu9+Trt%rTk z(+CKiW|jRi>QMxxs@(sCANo-+nE89y<_S?nxnQ^P=F6Ay6Wvf3D6_O5G0orac7EXj z$ToUW5$sK(y%A0zUbN_UWiKY&O2W#~Ts)TBONI zw9Ki)q+u+7Lqu~p_6ptS=wf&X%!gRoAnIO9o7}WF|F&xfuCb3u+fBU%CP9QD{)~qu~@OS#OVaEW|DR!T01X8rWX(qY6X!=CMMv|(hKFwo9n@M&i z|EC^nG?+9Tu^-z=HTG&^O2B2rV|XT@&bg_CZpj~-1Oh(B@; zV;LpQMkNhNIqSP;+xj+Joas_*>Clf?*7C4!29nPEkC-8{F?RfrO-at7=^M1{<#%2a*Y{$ zdJTJ@BoL*`Tck(<7f{Meknsf+y3--N&9;U2n}-shnp|ET zx)&G-4e(1DHlXz&T%TBbT(Ew(;4tzqR=j%WYG)@sRpZdDE9`f1g?R&gjx-`)^WsT-cKFKPz#maA#T&`B9kpfbC{sHPJH^&=GgLH+8r0eui^l3@Ya%C%|%#nS2hiaYP{ zDBB>J{|Sfww=I3x{g3nK`>{EDQESo?_T@eL8TW7DzeW;!SK0n{yDuSh_+>t};!J<^ zHsNC&jb%Xb#e*E_guiNbUUU^qEuI(;YiMyk3ymLpu-$Qi%*hN9;DEZZ*eo2Q`z=5oP0Qa=x zdq$u#t{2A{G9BnS?4;%H6X`mlCXG`TXN4XQ+jo7Z5nJ9r_-X%$1;}gNugTapPJ6qN za$iKQ9_o46E10+-xHR&Pwlk0FiGB4xV}4ex_vN)^!&#k?G~Y5%yT? zEj41TC@bm4e_x$>%fBrIzyy6T9zXXFRi9XePT*KFO_29(Kl45R@@9ybEsK8e++S$2 zs(*H0k*OQAy?1dcd2(8L^>*&}aXecgTNC`HB)g zGUVb#pzkhIk_Nh6I5@)?rwS=kV;d}N4kt*IZSSoFxyzm4sU5HbH8D^;`njdIt9TZ^`<3q?{LIa6}D74X)S z*r58aNIL$XzD^=7jTXiaA>lEwX6(CRkFcugHg1W#WWq$hRj=RAN9L+Z>8MpJNWo4ElmRJZmX<>*yrE%tv4!cgpnXSrFHRgm8_~Ai%-+%9>Q^zf*4XmZ zel7Js>*RO%5+T$XJKQzy02^w5Xk@sN8e~F{urMUMv3!3%bhU_^`y`0ZO}Dm0_>vIO zkZjPSp`p!%bSzRWYX-&i#y*=CIzk9|4i+$|n<~a=?>e4I-{#|{Um^>Nox^dmSjV{r zMmOUcgf+dtNo3lae@XT7IL0lXJCeN^(DE^z>&ZJJ{hT>`%y{KXSRM)@{57_KkN{fc&(qx?BDlJw1$?W}@9&6TxviJ6Uwa+ECVXHMJ}JqQ)DN zuLp=u^eFiRS1f@GMWl^mJ_Nd!Wj@70R1J`4Jti*G=fV@| zYmS<#ni+u5;*-N!uzl5A-)0*cBMgWSMZ$NM9F@e2(J>CXWUHGD`e z>J`H*S^{b~1GdQkCnb0R8uyT1_mh3OD(*o52Y_f#-o_M}0g+As;su4ZcRxque_uK& zFu?8fl|hvg=tb+Uya@)A1Fz+7@sHqy#bfcib_=-5OTrfA5gEid(|hdL zH&{-%NL#bq3&p+TFI|?<=1z+R?4sYS0lxN?8p9kuU!OShdqvYiOM-pQ$DZ3wpEj;V zkt|s2*Nb^x;T3`bk#U94@;@ofRON| z=b3fKC$p8TrI+1hMTWeWs@yIL89lsp!XHNpxpTLK>-c|WN)?&4$yL)8O=%Y~K9OAC zzTB3FE<|bY$CwVnI+8n;_yUb_c8d#bSB3)D@et>S76$EYiD9ZiCdF~POK{fem+39n znpwvF$bHA7ecwsJr5gr-*W)K%9I3^40=4J$z8Hf<5vzx{fn+7Cal;!N9uQSfrC zCui2C*KF+UOdfr>22Y*koTQ=rO($+*6P@lIiY{B$$$Nhbyu(o%uS(}i+VuCxjhI}W z!T|qlUGcG*pDAonvoH@gM=CKCPKx`&Y7zKVaJVB4tJ$@&6=)rDGtt5&F&G_kK5-UX zis4)k|JPA$o6Xn9D(U*CGdAfJj99nt_d+K%fvunlJZ%z_X98+S%W-j{CbcFZPERFa zPV491ZSJA!BwnOM%=72~Xw;YtNQIzOS3_C${tnQE}|aJr-E^lEu;? zJEz8virH+)q6JNYNw_v zed_DM{z^F(i+d#DeU}K5ev?1xmmo! zW{!q#9la2)+{c`Hx6dV~&B;D~GejkaNOnozy9)+4D+q!0nrV>3C_x0W#1;S#E0kK@#F2nqVy{&3p7k1xi0ka`oUPax}b^U!NcFAz9Q` z)JS}Y=`V&U@1C3?q!oj;+smq^3D}Y$Tx0K!Se;$>A0jXyn87>2;=?IX)OW`oYm1>{ zx?u9$aa^`}7eerKr&Ea*K$q8_jXmO*jC=O`wweDhNQT>{D z+PB5@6g@_YS{wRn$l~GSJS2}2y4D1{GC;9R+^Eg%bRZwgHk~~tU(FQ^S%+4;;J-5+ z%^_Kgq6is_zX} z*qNS;7zqF+D22(Sb1S3c9eHMlsud0HLC~6XAB|r`n-&6 zAmMFMr{veCAW(G1X(+6p4AN7H9Y97u>XJTSak}hnRMRo2jVe9*bA>I2M-r$2>%2;S zPpYnKBV?+SDLtW^3Sw3TG{g`@843&;d^jM{^u>)y(ac}mNxakuQoOYiHyQ!ptAU(n w_9`1^Bn7-VMUdO(hu43I?Ee#-$zWJnU2T|QN(Gt!aY8^tMO(S{nKkNv0Mibao&W#< diff --git a/gui/src-tauri/icons/128x128@2x.png b/gui/src-tauri/icons/128x128@2x.png index 063fd21a7d2c2f280cc48775640b08a90e77ec76..2c56f71b0854382375af261cc3cabe7106c02040 100644 GIT binary patch literal 5884 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 diff --git a/gui/src-tauri/icons/32x32.png b/gui/src-tauri/icons/32x32.png index 4040b1a4eeb17bbfca612cd67291e1b630bad424..6441457164d617b5c0ec75c33b2984097f86f52c 100644 GIT binary patch delta 724 zcmV;_0xSLR2mpVR7@&UK%Zk zhw4FLDjwE>_~)ebBr?>4H*xAo#e)-+iXb@bb)!A?w6d~ZnjP%eXdEWmJ`nhsec zk&7n7{b%lg%|H=~wwUusgJZ9mJw5&407&Tfc>{hv3=Sd49*KFuL13jhDgfcQ0QXKA z=7j)^-HXxD5q|*)aU=v2?=DawwhLI6Rk(XD4Id`+5Irk`FdW>P0CeGKDq@-Vf49%1 z7eFpl$$M$@0?<(EMFPUo08G3+4-DJYaV~zl`RFutJFfXxL6Y_~{a1(V=V+`TA&us-TBfn@we>2Jj>musf$3{`+7xEU8(sRS%P9PvQX&xDQw z?8fdN5=RBJ)|=nmOZDQkQvwvys<{_dXP!DG08P2w*eH@-?Ty{!$cfyvHzYmUC(^48 zV!LD)IcZLl`0Q%`fbIEEU;K8Ew&(ty?M^t1Il8^QJ^ux-hYOQ8GLMn~0000 delta 1115 zcmV-h1f=`x1@8!uBYy-oNkl;`X^=>8A!@gDoivll%w+Dp zf6qBLlTO;13570D<|o5V?&O^Fec$=cxihMJ%^r;#cnsX&Q-1|e1#lAsP?X#>=%p(D zn{`cUQ%%qHF`CK>fM{)q@j&bApnx(I#` z@bz>%lVvxQ{NjXdEWEt|EACr}<@YYY?ycX`J@c{{uSAZAed{ z%L9cEjHPlgEq?=}!x?<}b}vTGX0Tz)YK)xAV4(LHLQzM^OxprZ!a*zVUxXLm+5j)_ z!yWfZnu>vP&vPXG3%k~1&AMBWN@h_vD~4}BJdFNdkD%+hI}wgM@I8Ne4=~eIGJ~b} z%*XR@K7_W8X7ucPQtszS4P~e)JGb2pbFqzRI32iCFhkMV?9-}yRJM0|$>|h!UNq{+&NPhJW<=tgz)#@~Dp-W{}*>l9k~eH_WTS+?IMG z(15iYmtozemBEstxPoHEdn5p$gFf-v-LdI&k&xwNR|ktpO_(m|v+RHg7x753q&yAc z6Hm)1a*McS{7_m&+qAW<3BBL^jU-Kk6S5}P@nvHetXo5N|HXx&3qPfw1?u8scvSKa zD&P`xCY7I9K5Wcz%&z=2WI2VGucs`Giboe;iXJqi81INkTuqW-r78l@`YL`;;ewK{ he9r2J(kg%|fD8F+*Tq)&+SC95002ovPDHLkV1m5|D!~8% diff --git a/gui/src-tauri/icons/Square107x107Logo.png b/gui/src-tauri/icons/Square107x107Logo.png index de0a69ce5336288245921efb3d448ecdede2910f..ac7f760794e587d9c4b5f3f36c10adf3f16466f9 100644 GIT binary patch delta 2326 zcmV+x3F-EqA)peFBYz0zNklm1fi8E7Y_fZ-=N;lHRno~8T+S%06mpsF!5O~jVOwQLJ`=)%JE zhx9yi&BLC+16(w)s!JP=(mIMQfk(xLy4Gc|yo)t}U?FRz||oQlkFeO&>r>ze`b`K)dKc2*kOEhSoC?A9D}xpTR? z*DAS>&hF)W)g=Ur6&04cSA6a8=?1Q*-bb)l(0}w~>}d{h%?=M+v;Ubh|1oVHM&LFPtzMAy_O4o}@SRg@(r@U{;ByK?~IWX^)f*s^uv!v^0JZ zEDM1Ju8nGr*S}qz{;+VzVcus^FtE#=lkodaeQ-&03>l(T-jxzq7(C|rL#OMR1J?ZU zE`P3Je>L~&`!xx_7Yky&>Ch_gN;)jS0VoKINmhWtlQ0%!cMu){E>U%gRSZ4vvf?v3 z@V%^Lf`UP^bC;@Ic>e6(r#T@U;YN~FW77q7s-D|iS+sn-VuHYnb-=;i+i_oOTJ$bG zI1gs$xIgu~>q49Ql5)E-2-=kcOXlr&BYoc&F-sxGAZY-u6n$VZ{q7mSF(w$k|s6El{I@ zl9vr;#uX1R6ma2%N@2=1^I0s1m47HGwPDmH8(at1GYd=)6yhbkI@Y*4#Ifte)>zvh zfdQ>|m6=#?Sqdu}D5SFS%+)Z=@dI6&b7f#g1*7VLLV^=d{{+s_ONJ1SS6b}qU0ZGi zNI5K&Rn(wh`12?F61t9_7Y_bmJlVh8D^)-vrDq?J`Yqecz(d;B2 z*zq+Ru&gr3YSG~{^^2`*W`AMXlDZS9m1VT-L_5VcD>ST4V)%uQ-b6(mxi%}F*0p#= zo@KMfbXw7juC>UsY?iv@WJUxljYP0gAiWM=g2jOJ_P#vj#)`QF%Vt@*=42Wc7LR=K z@-+#z7@{@Z$U*^iI*O4-ZU||+XiqvU89<&`lJfZGEA*(fM-wLu#+pT=9C!V|1C62T%^B!We-NCb;Sum~23 zV37zG!6LD*tbcGM&`V}uUz4-K?X1YG5X^W>q=7~wfu1a3D&!|qurF|Oiql|E##1ybFtL#;fL;2#w{X2iT za|<`|E>Bg4+bQJ|!%~Mo{M%>BV-EF6;mJUcC%@uFSN7w@nPwnXButMCUcUA1zPU#a z?ym?7C)^tE|B-;D4pi6h@ml5d7Znx(V-r+VSV~~7?EDgnVfG6t`lY|WYE(~sO09|K zLf&;%jeoV?mi8P+Uj8byQ%Z&960-2%xc@?|bJxUs(OjpipkhN^Yh_VgD?zgc63R$J z_qyAnO?!r6DWUoJnifD?o5OERZ<8foh)aR7tL>;5#S6(WETzOXnb%vetr0QJox^)e w+&=sNh)5bq-i5~oFSa;>6aEW{0#fRK*oi(3_zw!sw*UYD07*qoM6N<$f-4Sj$^ZZW literal 4255 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 Mb&R#^HBoW@10>YTb^rhX diff --git a/gui/src-tauri/icons/Square142x142Logo.png b/gui/src-tauri/icons/Square142x142Logo.png index 63bacc8c3fd712bc390819ba286937aa26fcdcc1..28e0d6d27eebbf18e429f05e0bfedd27a808f8f4 100644 GIT binary patch delta 3173 zcmV-r44U)wE$JALBYzAqNklk91~47G$~(NH4U5@_uRw|4g`#Bmn$sR?{v$j?sX*K_H`{I$#ZwQO4U|Jf$%i6U*? zz562l!?BLuV-Q9mo&f@-m!{8M%TA7GS2EL{5GSt6vVZ5{6O1VJL2flO{npJ(ucTCo zY9bDV>5Cpc#ee8hpCAfNX3l;qeQWxhE8-x3NydiHbcN!{dTJ( zTJOB`g?OmqphaW8!&ay-~GhPzH237NPqX2UPk08E6(7a;WNH!8DdIHb6W=rzNX?N zA_F5#hJQ&Zm?emPr1zMiI2c7CEcv&2h#95n%ceM}3L`A}wPA=kn9N3xbdNYa>O7Q> z+l5%5XXm5OZ4n3k7netnav=Z3sQa!s9a|G0LPW$jx~mOw4m|Gr79p0{z2QngTTITR zm{>k%!`V42-0v)#NvIbwT~v+DHoWoE#l;^*FaZ zedrhe_e6^kLl8;geNnYr>e({VGUCelS${QYzU<0zHRBUtyx3r*zz6GhlpEM)O;%E_;InTsv|J`e(4+MO}j9+Mp_i7&d8|IWc1q?mDOeA z0$^JxsExKL4l$JHcQ0KL-v8)VdK`~Bo)-r-idq%9oWSmITLf|*jJ}L;BkFisoPRKe znXeAjtjHys;TH$Bif{i=T*~vEJv}MB&*`Y+*-;fv`q`g;Uae6)puK|*J@%0D_sMzX z{oJY-54Y^OeD_z6D?I}r)qd|y#+3PMIc>C54Xf%zCu%rGZ>sIVfYw&;zWr}U&dHMz z2Mvva7KWF1ZwqSCr`{ru(YXLbAAcF=a{jd`;eEZy-Hx4?=fn{;bg?|LLcP<3H!8Yj z=TfaaSf_*fju9vrI@Miys;e@SMh77nq28@RSEWD*bWiAN&CW$u7mPMB0^Ok#?c}M5 zgULEGX^I*7e$um%2ht##AgYvqvs52>Fv7AcRKtjaK0ncCJQpLXltLa;JPl^L+u(HjFu^&t*KYH@_TG7i0)GP3KFMtS$;kJ=95C-Rg zDFdNAqm|1uJ&`uN9nb4T)Ty~R<)lFv#`@aR&zu*^gU8oe9!P@-Xaq!oFgSjEiR3{y zU8y`Xf`|ip2V;#TjumNibbon7AsNq8ZMd%#6x&47pqtgD(Q!f^w2$bG6^^zyjyOl3 z?h{IL>9=ngCVXfdOz?@4LF7o)g!@Y6$1VgT@Is2ZStsNXjh@P_zx$<25EHedA=N6o~MCC%mwG_@#fbf@OG^de>AP*ivx=uWY6 z&njl}Ao5t12Xj}S`G4Ae!xe%z=a!`I!*Z>wk}Zvu$LjTwaG8Bdb~vDpP((#ar_RI9 zs4PT~{9N?^bq%+oVgQgIwmF$&8k?;`=XOxa8P5Avwod7^^R6 zCC{yogdpZ0$7d`(c6+c{85lAi>7h+TV%f#Diw@`oypw849>|tPN3V}q-ON@EPDHcP z=f)7*j+kz(7=Li}Y8ARX`p-_0@$hnsO60NI#v)K4l{K<*y#R<4tM#>A8XY&HEw{(` zNStt&ys`u@tki&rBD&1UPDY#t#Ek@*5ywZ!oF!+(X+X?o$te!02wC%X6vuDkbjeAO znd1%HTs0VXy}Ew zQo(RhrFvf>P&08LhaM>)2BL(UQnS?jMlLw3E zMt_ZsX8N9~;$;I)l-ETUDJuKs(MNwp^6lEVz_(L^=@(AYySA)0g3C+afai!fxs8LZ zWKHYpa^an?i=j(_(MG)uS&QF5BKeAQKYg$ub1iV;vpdDlC#y}Rbyaxh_Yavj0J;+V zoqztWs7Eb@#MePy#8DHo3$IOWX8c4#o0iCgJf^tZVF#oE;}UnC@}m{&6#XL? zArgc#%af26U{|QjH#Eg*5Ky6qL`aNV)qgoX{H~`6m5vh4;IfJa0VCT<7iZ-u4xu=N z;t+~MC=MYMhfo|saR|jB6o(LsLnscRIE3Pm#-}N_IzOWjVuQte>T+QH_KLjE+atcA zX(6AQ@O>)~TfmvX@_H`q`<5WKfHMI`9AXzZ)0AJ!rd5NyaR~(CdEg41(JqjkUVoZ7 z$0i|G&|T5QNoB8$`Mp7iHFQ@rak49!Y1VT{Ip7Ff*%Sx7ee=>Q{%#ghj$%hP#hFz@ z5R+sg0&dI}r8HYUJ!Wd*!YO|?3n|By%-Ivgzi$x-MqwVpj}1e_z=`y&>2pGHz(jg{ zWc|+SfBe=s#2B~$CrWQ`zV{DTmVZ`nzkegv(R<8SEkq3Y>%V^U&4v8zL|Jh_P91WE zf{{Jpw(ee^v=1>Lm75%&S~!2o=)PvrgVIaW=c1v+BcWh8*4WKMj6hy<@gJkYUdQw^ zIWJxxJ;uVI|G;aG*WT=Jn$7OK^S=-N`_Ef%zP|JRz5R?l{s58YjcY$UvVT@)fnRgE zCxSd`UpK~_r}05fgYF5hdAYxBw)&*Vd6w7Tn?v5y+_e3^25Bpz6o%XwJ&6)wBevQ7 zCu-!0C1b;5U7`43eKr#*23-^CG+1L}=W({h(Md!GM*5;dr`lV>yX&W!hz{nSFpW@J z5q4ahVu5;g9zGFn-F3jj?OsGSz=#R6+_P1#!Kzvuorsz^$5n}99S4pv3V91e7p&aF zA}oD9O|?i9rx@fx(NH|83Z#9L7$|?f^zIez<0a(ixoZu*pk3p3prd=00000 LNkvXXu0mjfLV+vK literal 5876 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 diff --git a/gui/src-tauri/icons/Square150x150Logo.png b/gui/src-tauri/icons/Square150x150Logo.png index 3f2b7c291e644209258dff5dbbe49c770b8e69f2..c9202779ee9f9bb8670b02ec07b9bb955e3b7980 100644 GIT binary patch delta 3318 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 ArT_o{ literal 6317 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 diff --git a/gui/src-tauri/icons/Square284x284Logo.png b/gui/src-tauri/icons/Square284x284Logo.png index ec06bdb994c25f13d30b09dd82f921e662bf69d1..7c35ed105e70ddc03e80b232d9f143fd58c7bbbd 100644 GIT binary patch literal 6460 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 K(kl+IZ9em*O-XC%5d^pwB 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 diff --git a/gui/src-tauri/icons/Square30x30Logo.png b/gui/src-tauri/icons/Square30x30Logo.png index dc064621090f909b35ce38df5626f4bca3588abd..6f55246a2021d93f746bbb17db030b066b921e2d 100644 GIT binary patch delta 675 zcmV;U0$lyD2)YH3BYy&cNklUG^_}k=VfzA zmbTeQ(?6tyCvV^1^ZeiEeM#J)5)(*Fvox2Zsc_8WqQ<}?iGOnK`;VU=p9;;&>)n_G zf4pvLEW_qM)4oUy1}IFR%oi_h|7rhn+66@+16g?QI(_WoJfqkPHi;TJdZN|Y+Hj>$ z&b;wjxSnR@_tH_f-$O^iW})S77?Oqtb`HeKkRK?&$Nq|Q50Vf*MMI@5!24pwaEvP+ z%09FjjKJTb41ZU0=U~L^0^wWZ;8tg5;(cw~RU*S^;O`16FnME~SbrdJb~a9~{Ig6? zI6k|#bXBo91=IJF8o1o*Kr9(G560}%H_yQ1TOXjlz6E!eu0e#M4WXDAj#-#2L4*xx z;Ak@w51IqVQc<|R$U$Ll8ES$ErAJ@j)7v$;{p9k#v42x}jzDotTk1e@ewo10DU{fd zNTccHn4Ng>*Qg3Q=qFHp949vuLtz}NU3{K}xeG7*ZzudiTM`Z9hmGkI9LNEpmb?QC z>-wYT0m7UQlC6NYqp-Pry6#r*(VEbN^=d;?nSQwp!3ddJNGPCD(jfmQuWo8=tSgiG zMm@?H8h_Y{;7*dEJs964KtV|YufAYe+6L-~2UfpsK>1Y_#7(Jd#@+Ta6Auw*u&KB# zyU+5TU2UZ>0kP2{MVMY=D`a#|+XC*U;k^ima(&fqC^DTwbERZAcuo1LaN?(homC@? zSBfP^W|=Rf?0_Q!S$OYIKVSn~Zv1%q4($t{wHz9V_VTJ_{Rg%H4H4V`Rk#2E002ov JPDHLkV1h&WN!I`X delta 1052 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 Tg&8Cs00000NkvXXu0mjfEdTe^ delta 2702 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 IM6N<$g2z2NegFUf diff --git a/gui/src-tauri/icons/Square89x89Logo.png b/gui/src-tauri/icons/Square89x89Logo.png index 9b2c5fbefc84be7e473ed51df48046af02fdf11b..505fadf3ff3686d0ac8d9287a4aec72e7f0f33c4 100644 GIT binary patch delta 2014 zcmV<42O;>t8|x2{BYy|7Nkl(tt zQf;%*;?(oK<94^VckBJz)!y?>mUXvxJ?DPz@BIFpbGKtBDu3m2c>Mn6)tkJoK-f|1 zY;JV8{jPeqpH!*RIWC^ba`Ei!bSjySjmA^sXVTfJxSG$XQF>y)(>2hsyl0oY!569q zczVRmB%+t^o;wtqiX1Zpvj34#f0u9bXVq-2daOK>IQdrmUi7pk$PT-+8E^ycuC3J! z$_T}{$Pi#>|9_rNDTeHE`aruFld%utvoq{Mcwt8E=aVV>%18=CDbt1wq| zzI~;gVK7%nDj4K4h(TtM8Du5~nL%cdnT(aq$`|i+)t99mOIvc!mJZVUS|^!KXGv@< zNun3xB!Bi(l5mr02H7aNy{?BA?I5ABpZ@~zvtNvn^Czwt){dp!g}bG(=oo;%w5OZw zKHO`VJFzrmZZfmrd%?~YGVnpzkalXeuRZeb{|%1u7VRRTb$;rj+w(3&(7EH6$;pEe zU7v4hOP;srG!Jv%tKZVVH(Br=f@7f5%OD%2>3?*N9M~~j1R5Dg#Y5I>33}xVxK#zQ zp}iN#OeUBA8U9(%HJA<=Y0;j;eKd$H%_@3sqxLdeqe$Ag@!X;`Zm# zGJjzzARmcAh1P7^fO^bTnQFS)(P_cDD3=WYh&BsekwDO|pPt6(0J&0JhFy zCu=u6POe36lfSqbL)r(p=U(Y31ia%cxqo@(cXH|b>*T2}A8B$s^EcYr^v6>_7oCd^a>;bwzX9|!FFwg1yDb{hYTDkmzki23 z{rnU8V*rclO=@v+pz*p?+ugnMHxHjF_0^@M+b7EbhV@EQAhGa1tXwSkQ3NctLfhhi z1i%NW4@Pz%CmbJqXQZImSgdU+{MNQdRrgUhWI4c6fD3_^UMz;DT49oX{`KUdfb-T! ziw(FEDQLG$`_j)6Uu)Ui1%{wtNqtl#E_eQwus#BH$u0iscJGV6 zTP$Bt-E;thTm~`73Z$hHWsp@$T9CB1F?V6QHsp+-}*xDg5np=gMW(Ng$MW=RSkU62Ksm~X`d zI4mycZ(@Hv`|z9s#N#V^yPVjhFupux4ANIit+MgO?h^ z1hsTn{WzT~PUy@gbV1&8q)&CVvzRcX#q$D);!4w{R+$G@0&BF7A_08g_W9?P0sdEN z<%x2paA;J?Vj|{g6S1iRX!zYZu;%+kKA?IV^YCC8sL~K0lo*Nld4J`yTQY!2A!BYe zF~RZh02b$znlv|CxQW7zl+Kp%afFV7ARk9u{A;zV=4vfl6=W=tqf_WOfT~?ktOUUT z&~I_yVfg_XYlLUsq>s-^HT>n6sy5m4$ubh2R|asw-S8uX#t*FCNT0`GkSjo}tY(lI zWF`igL1vJd7-R;Ssef3uOpd$7FqkLswWYIM-0~z$6!_Y6vok4%!aRYmZ8AMEYIzbS zDxQj-w#8H9rx^E1?&;Ik^zHqGI@A$C$*2r&z{WO98)3TIw2;7Dz4uqwy zZ9ddk?_OEOoL4jnX&SN!&3mLuh0UVc`Q3h7S62}yO@eDcp&{hKrKA|LpojZb_hTm* wY*gZMxc${^<*-&vS7J5|lRcP=P`hvSf6}^_=^+*&^#A|>07*qoM6N<$f+o`FT>t<8 delta 3519 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 delta 1716 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 literal 6778 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 delta 1716 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 delta 1756 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 Ef)%lfod5s; literal 4247 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 E4_ch?LI3~& diff --git a/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png index f9e1acb687ac09f04bb038d687975f3f7f9b4745..30bd847b19916e7b3b1e783dba06e4186b29cc02 100644 GIT binary patch delta 1117 zcmV-j1fu)v4etBmMx`52i>>isJZM!= zlnRO{iU)I1s|OXut3RLzLB(6~=TNVLs5Br}g!W=DrbHB>DJ?Axjg>C6O-XHP)p^@a zOyefoq=oKkAA}^kGqdk~-`j5{nYEG%>{dsaBnD1-%c>8Fj(>`3o5fK^3xL#Qa!5|b zB0WQqmr=Rx>G+T2P<9(r_5r`U?s`@Eo=XcwI-i(N1MeP$`$IQX1t98s*Pnag5~Nx^ z5zj_E5p8MiYB7ro`n)@_5O(gOVMgEu5+AgFipnIf2ZOvr{A z)~PDrebFf7Ie)+z+$|ec?Wjk!F`592dCAhTiID-T5y^9ayOq`X%|;baY8Tg7^;Yi- zfnXE8R-=;VSQM}*;9n7NZi=?YX}Pn6R(n^FE6q zCJ*uF@9v{@RplhQO6g;3?~L;ksYzy|GV(|w`^9BaGBPh2z=ijmsG>eb8W9R!uPrze zXpesAkAHEtAM~ka;(e$?d9U2!njZ`C3*(~q>zkNYo0QKu220HbyN_>5lShAygE0`4 z>_9}rapm3V7L-iP$wHY?yH_7lDg$00-geQ(fQL41_o!MS`@Y#9tWgNxI{BR0Z&a}g z9|N|`wxn;Xi;XK#_vddpt*V&;+(emC%bZ}6iGNf&iZ#2|lE|W^28UoHkI$LT9{OdD z(QvgnZOP0r`a0xv7IHyzB10-ekHbod>r;sW7lBCSnmu_N{y7`yhu~B&t7MJ=Q9Ou6 z3jmrPh4Y>VsboUdY?BWNPIiDg*s`MTv_Ee@LoDX7Ml!U3Io$&b2~;XA6fFR?xun!a zC4ZY^ED2K)2@0&517*~X#(m`OXQ`WrM zXD#6Ns}p&>_a~Br>F=o9yz~LckjE!xe7Db-Rk7m4f(y)OV0a{h!LPpacy+YJb$YsKIn*>%(8UJPzXXL3Fkdr`J6aTflMK zf_x)eUSlFHNPtnW_^-sGfd5^9IW;*pY*g~#ZkCgM5u*wKcQZDTjVb`#&1}1}(TFwz zXK*)T!x%f@MiT(e;I8ZwW53lc_;yY0zv>^1Z);0WV-TOyJN3Q8A78Q^@POV$fL45S zSHq#npA+9_2*3e&3OnFVho=VnuzDg6&quz#xr4}a@_Jk>T!$?}CF jjZ!)NxJ{$l<2JtmA8s2OaYz|A00000NkvXXu0mjfE_M<( delta 1756 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