react-native-nitro-pose-exercises 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/NitroPoseExercises.podspec +32 -0
- package/README.md +538 -0
- package/android/CMakeLists.txt +31 -0
- package/android/build.gradle +121 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +535 -0
- package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercisesPackage.kt +22 -0
- package/ios/NitroPoseExercises.swift +527 -0
- package/lib/module/NitroPoseExercises.nitro.js +17 -0
- package/lib/module/NitroPoseExercises.nitro.js.map +1 -0
- package/lib/module/config/pushup.js +71 -0
- package/lib/module/config/pushup.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NitroPoseExercises.nitro.d.ts +97 -0
- package/lib/typescript/src/NitroPoseExercises.nitro.d.ts.map +1 -0
- package/lib/typescript/src/config/pushup.d.ts +3 -0
- package/lib/typescript/src/config/pushup.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JAngleDefinition.hpp +69 -0
- package/nitrogen/generated/android/c++/JAngleSnapshot.hpp +61 -0
- package/nitrogen/generated/android/c++/JExerciseConfig.hpp +166 -0
- package/nitrogen/generated/android/c++/JExercisePhase.hpp +67 -0
- package/nitrogen/generated/android/c++/JExerciseType.hpp +61 -0
- package/nitrogen/generated/android/c++/JFormFeedback.hpp +67 -0
- package/nitrogen/generated/android/c++/JFormRule.hpp +79 -0
- package/nitrogen/generated/android/c++/JFormSeverity.hpp +61 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_ExercisePhase.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_FormFeedback.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_HoldProgress.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_RepData.hpp +81 -0
- package/nitrogen/generated/android/c++/JFunc_void_SessionResult.hpp +85 -0
- package/nitrogen/generated/android/c++/JHoldProgress.hpp +65 -0
- package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.cpp +311 -0
- package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.hpp +87 -0
- package/nitrogen/generated/android/c++/JLandmark.hpp +69 -0
- package/nitrogen/generated/android/c++/JPhaseThreshold.hpp +71 -0
- package/nitrogen/generated/android/c++/JRepData.hpp +90 -0
- package/nitrogen/generated/android/c++/JSessionResult.hpp +120 -0
- package/nitrogen/generated/android/c++/JSessionStatus.hpp +67 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/AngleDefinition.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/AngleSnapshot.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseConfig.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExercisePhase.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormFeedback.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormRule.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormSeverity.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_ExercisePhase.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_FormFeedback.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_HoldProgress.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_RepData.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_SessionResult.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HoldProgress.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HybridNitroPoseExercisesSpec.kt +196 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Landmark.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/PhaseThreshold.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/RepData.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/SessionResult.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/SessionStatus.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/nitroposeexercisesOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitroposeexercises+autolinking.cmake +81 -0
- package/nitrogen/generated/android/nitroposeexercises+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitroposeexercisesOnLoad.cpp +66 -0
- package/nitrogen/generated/android/nitroposeexercisesOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroPoseExercises+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.cpp +100 -0
- package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.hpp +449 -0
- package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Umbrella.hpp +95 -0
- package/nitrogen/generated/ios/NitroPoseExercisesAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroPoseExercisesAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.hpp +236 -0
- package/nitrogen/generated/ios/swift/AngleDefinition.swift +44 -0
- package/nitrogen/generated/ios/swift/AngleSnapshot.swift +34 -0
- package/nitrogen/generated/ios/swift/ExerciseConfig.swift +83 -0
- package/nitrogen/generated/ios/swift/ExercisePhase.swift +52 -0
- package/nitrogen/generated/ios/swift/ExerciseType.swift +44 -0
- package/nitrogen/generated/ios/swift/FormFeedback.swift +39 -0
- package/nitrogen/generated/ios/swift/FormRule.swift +54 -0
- package/nitrogen/generated/ios/swift/FormSeverity.swift +44 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_ExercisePhase.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_FormFeedback.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_HoldProgress.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_RepData.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_SessionResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HoldProgress.swift +39 -0
- package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec.swift +73 -0
- package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec_cxx.swift +483 -0
- package/nitrogen/generated/ios/swift/Landmark.swift +44 -0
- package/nitrogen/generated/ios/swift/PhaseThreshold.swift +44 -0
- package/nitrogen/generated/ios/swift/RepData.swift +50 -0
- package/nitrogen/generated/ios/swift/SessionResult.swift +66 -0
- package/nitrogen/generated/ios/swift/SessionStatus.swift +52 -0
- package/nitrogen/generated/shared/c++/AngleDefinition.hpp +95 -0
- package/nitrogen/generated/shared/c++/AngleSnapshot.hpp +87 -0
- package/nitrogen/generated/shared/c++/ExerciseConfig.hpp +122 -0
- package/nitrogen/generated/shared/c++/ExercisePhase.hpp +88 -0
- package/nitrogen/generated/shared/c++/ExerciseType.hpp +80 -0
- package/nitrogen/generated/shared/c++/FormFeedback.hpp +93 -0
- package/nitrogen/generated/shared/c++/FormRule.hpp +105 -0
- package/nitrogen/generated/shared/c++/FormSeverity.hpp +80 -0
- package/nitrogen/generated/shared/c++/HoldProgress.hpp +91 -0
- package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.cpp +46 -0
- package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.hpp +117 -0
- package/nitrogen/generated/shared/c++/Landmark.hpp +95 -0
- package/nitrogen/generated/shared/c++/PhaseThreshold.hpp +97 -0
- package/nitrogen/generated/shared/c++/RepData.hpp +97 -0
- package/nitrogen/generated/shared/c++/SessionResult.hpp +108 -0
- package/nitrogen/generated/shared/c++/SessionStatus.hpp +88 -0
- package/package.json +187 -0
- package/src/NitroPoseExercises.nitro.ts +155 -0
- package/src/config/pushup.ts +62 -0
- package/src/index.tsx +28 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.NitroPoseExercises = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36,
|
|
6
|
+
targetSdkVersion: 36
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
ext.getExtOrDefault = { prop ->
|
|
10
|
+
if (rootProject.ext.has(prop)) {
|
|
11
|
+
return rootProject.ext.get(prop)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return NitroPoseExercises[prop]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
repositories {
|
|
18
|
+
google()
|
|
19
|
+
mavenCentral()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dependencies {
|
|
23
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
24
|
+
// noinspection DifferentKotlinGradleVersion
|
|
25
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def reactNativeArchitectures() {
|
|
30
|
+
def value = rootProject.getProperties().get("reactNativeArchitectures")
|
|
31
|
+
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
apply plugin: "com.android.library"
|
|
35
|
+
apply plugin: "kotlin-android"
|
|
36
|
+
apply from: '../nitrogen/generated/android/nitroposeexercises+autolinking.gradle'
|
|
37
|
+
|
|
38
|
+
apply plugin: "com.facebook.react"
|
|
39
|
+
|
|
40
|
+
android {
|
|
41
|
+
namespace "com.margelo.nitro.nitroposeexercises"
|
|
42
|
+
|
|
43
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
44
|
+
|
|
45
|
+
defaultConfig {
|
|
46
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
47
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
48
|
+
|
|
49
|
+
externalNativeBuild {
|
|
50
|
+
cmake {
|
|
51
|
+
cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
|
|
52
|
+
arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
53
|
+
abiFilters (*reactNativeArchitectures())
|
|
54
|
+
|
|
55
|
+
buildTypes {
|
|
56
|
+
debug {
|
|
57
|
+
cppFlags "-O1 -g"
|
|
58
|
+
}
|
|
59
|
+
release {
|
|
60
|
+
cppFlags "-O2"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
externalNativeBuild {
|
|
68
|
+
cmake {
|
|
69
|
+
path "CMakeLists.txt"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
packagingOptions {
|
|
74
|
+
excludes = [
|
|
75
|
+
"META-INF",
|
|
76
|
+
"META-INF/**",
|
|
77
|
+
"**/libc++_shared.so",
|
|
78
|
+
"**/libfbjni.so",
|
|
79
|
+
"**/libjsi.so",
|
|
80
|
+
"**/libfolly_json.so",
|
|
81
|
+
"**/libfolly_runtime.so",
|
|
82
|
+
"**/libglog.so",
|
|
83
|
+
"**/libhermes.so",
|
|
84
|
+
"**/libhermes-executor-debug.so",
|
|
85
|
+
"**/libhermes_executor.so",
|
|
86
|
+
"**/libreactnative.so",
|
|
87
|
+
"**/libreactnativejni.so",
|
|
88
|
+
"**/libturbomodulejsijni.so",
|
|
89
|
+
"**/libreact_nativemodule_core.so",
|
|
90
|
+
"**/libjscexecutor.so"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
buildFeatures {
|
|
95
|
+
buildConfig true
|
|
96
|
+
prefab true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
buildTypes {
|
|
100
|
+
release {
|
|
101
|
+
minifyEnabled false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
lint {
|
|
106
|
+
disable "GradleCompatible"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
compileOptions {
|
|
110
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
111
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
dependencies {
|
|
116
|
+
implementation "com.facebook.react:react-android"
|
|
117
|
+
implementation project(":react-native-nitro-modules")
|
|
118
|
+
|
|
119
|
+
implementation 'com.google.mediapipe:tasks-vision:0.10.21'
|
|
120
|
+
implementation project(':react-native-vision-camera')
|
|
121
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#include <jni.h>
|
|
2
|
+
#include "nitroposeexercisesOnLoad.hpp"
|
|
3
|
+
|
|
4
|
+
#include <fbjni/fbjni.h>
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
|
|
8
|
+
return facebook::jni::initialize(vm, []() {
|
|
9
|
+
margelo::nitro::nitroposeexercises::registerAllNatives();
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroposeexercises
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Matrix
|
|
5
|
+
import androidx.annotation.Keep
|
|
6
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
7
|
+
import com.google.mediapipe.framework.image.BitmapImageBuilder
|
|
8
|
+
import com.google.mediapipe.tasks.core.BaseOptions
|
|
9
|
+
import com.google.mediapipe.tasks.vision.core.RunningMode
|
|
10
|
+
import com.google.mediapipe.tasks.vision.poselandmarker.PoseLandmarker
|
|
11
|
+
import com.google.mediapipe.tasks.vision.poselandmarker.PoseLandmarkerOptions
|
|
12
|
+
import com.margelo.nitro.NitroModules
|
|
13
|
+
import com.margelo.nitro.core.Promise
|
|
14
|
+
import com.margelo.nitro.camera.HybridFrameSpec
|
|
15
|
+
import com.margelo.nitro.camera.NativeFrame
|
|
16
|
+
import kotlin.math.acos
|
|
17
|
+
import kotlin.math.max
|
|
18
|
+
import kotlin.math.min
|
|
19
|
+
import kotlin.math.sqrt
|
|
20
|
+
|
|
21
|
+
@Keep
|
|
22
|
+
@DoNotStrip
|
|
23
|
+
class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
|
|
24
|
+
|
|
25
|
+
// ─── MediaPipe ──────────────────────────────────────────────
|
|
26
|
+
private var poseLandmarker: PoseLandmarker? = null
|
|
27
|
+
private var isInitialized = false
|
|
28
|
+
|
|
29
|
+
// ─── Exercise Config ────────────────────────────────────────
|
|
30
|
+
private var exerciseConfig: ExerciseConfig? = null
|
|
31
|
+
|
|
32
|
+
// ─── Session State ──────────────────────────────────────────
|
|
33
|
+
private var _status: SessionStatus = SessionStatus.IDLE
|
|
34
|
+
override val status: SessionStatus get() = _status
|
|
35
|
+
|
|
36
|
+
private var _currentPhase: ExercisePhase = ExercisePhase.UNKNOWN
|
|
37
|
+
override val currentPhase: ExercisePhase get() = _currentPhase
|
|
38
|
+
|
|
39
|
+
private var _repCount: Double = 0.0
|
|
40
|
+
override val repCount: Double get() = _repCount
|
|
41
|
+
|
|
42
|
+
private var _landmarks: Array<Landmark> = emptyArray()
|
|
43
|
+
override val landmarks: Array<Landmark> get() = _landmarks
|
|
44
|
+
|
|
45
|
+
// ─── State Machine ──────────────────────────────────────────
|
|
46
|
+
private var phaseHistory = mutableListOf<ExercisePhase>()
|
|
47
|
+
private var repStartTime: Long = System.currentTimeMillis()
|
|
48
|
+
private var sessionStartTime: Long = System.currentTimeMillis()
|
|
49
|
+
private var targetReps: Double = 0.0
|
|
50
|
+
private var countdownSeconds: Double = 0.0
|
|
51
|
+
private var frameCount: Int = 0
|
|
52
|
+
private val processEveryNFrames: Int = 3
|
|
53
|
+
|
|
54
|
+
// ─── Form Tracking ──────────────────────────────────────────
|
|
55
|
+
private var lastFormFeedbackTime = mutableMapOf<String, Long>()
|
|
56
|
+
private var sessionFormViolations = mutableListOf<FormFeedback>()
|
|
57
|
+
private var repFormScore: Double = 100.0
|
|
58
|
+
private var repAngleSnapshots: Array<AngleSnapshot> = emptyArray()
|
|
59
|
+
private var allRepDurations = mutableListOf<Double>()
|
|
60
|
+
private var allRepFormScores = mutableListOf<Double>()
|
|
61
|
+
|
|
62
|
+
// ─── Pose Tracking ──────────────────────────────────────────
|
|
63
|
+
private var poseWasLost = false
|
|
64
|
+
|
|
65
|
+
// ─── Callbacks ──────────────────────────────────────────────
|
|
66
|
+
override var onRepComplete: ((data: RepData) -> Unit)? = null
|
|
67
|
+
override var onPhaseChange: ((phase: ExercisePhase) -> Unit)? = null
|
|
68
|
+
override var onFormFeedback: ((feedback: FormFeedback) -> Unit)? = null
|
|
69
|
+
override var onHoldProgress: ((progress: HoldProgress) -> Unit)? = null
|
|
70
|
+
override var onPoseLost: (() -> Unit)? = null
|
|
71
|
+
override var onPoseRegained: (() -> Unit)? = null
|
|
72
|
+
override var onSessionComplete: ((result: SessionResult) -> Unit)? = null
|
|
73
|
+
|
|
74
|
+
// ─── Hold Tracking ──────────────────────────────────────────
|
|
75
|
+
private var holdStartTime: Long? = null
|
|
76
|
+
|
|
77
|
+
// ═══════════════════════════════════════════════════════════
|
|
78
|
+
// Lifecycle
|
|
79
|
+
// ═══════════════════════════════════════════════════════════
|
|
80
|
+
|
|
81
|
+
override fun initialize(modelPath: String): Promise<Unit> {
|
|
82
|
+
return Promise.async {
|
|
83
|
+
val context = NitroModules.applicationContext
|
|
84
|
+
?: throw Error("No ApplicationContext set!")
|
|
85
|
+
|
|
86
|
+
val baseOptions = BaseOptions.builder()
|
|
87
|
+
.setModelAssetPath(modelPath)
|
|
88
|
+
.build()
|
|
89
|
+
|
|
90
|
+
val options = PoseLandmarkerOptions.builder()
|
|
91
|
+
.setBaseOptions(baseOptions)
|
|
92
|
+
.setRunningMode(RunningMode.IMAGE)
|
|
93
|
+
.setNumPoses(1)
|
|
94
|
+
.setMinPoseDetectionConfidence(0.5f)
|
|
95
|
+
.setMinPosePresenceConfidence(0.5f)
|
|
96
|
+
.setMinTrackingConfidence(0.5f)
|
|
97
|
+
.build()
|
|
98
|
+
|
|
99
|
+
poseLandmarker = PoseLandmarker.createFromOptions(context, options)
|
|
100
|
+
isInitialized = true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun release() {
|
|
105
|
+
poseLandmarker?.close()
|
|
106
|
+
poseLandmarker = null
|
|
107
|
+
isInitialized = false
|
|
108
|
+
_status = SessionStatus.IDLE
|
|
109
|
+
resetSession()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ═══════════════════════════════════════════════════════════
|
|
113
|
+
// Exercise Setup
|
|
114
|
+
// ═══════════════════════════════════════════════════════════
|
|
115
|
+
|
|
116
|
+
override fun loadExercise(config: ExerciseConfig) {
|
|
117
|
+
this.exerciseConfig = config
|
|
118
|
+
resetSession()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ═══════════════════════════════════════════════════════════
|
|
122
|
+
// Session Control
|
|
123
|
+
// ═══════════════════════════════════════════════════════════
|
|
124
|
+
|
|
125
|
+
override fun startSession(targetReps: Double, countdownSeconds: Double) {
|
|
126
|
+
resetSession()
|
|
127
|
+
this.targetReps = targetReps
|
|
128
|
+
this.countdownSeconds = countdownSeconds
|
|
129
|
+
|
|
130
|
+
if (countdownSeconds > 0) {
|
|
131
|
+
_status = SessionStatus.COUNTDOWN
|
|
132
|
+
startCountdown()
|
|
133
|
+
} else {
|
|
134
|
+
_status = SessionStatus.ACTIVE
|
|
135
|
+
sessionStartTime = System.currentTimeMillis()
|
|
136
|
+
repStartTime = System.currentTimeMillis()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
override fun pauseSession() {
|
|
141
|
+
if (_status != SessionStatus.ACTIVE) return
|
|
142
|
+
_status = SessionStatus.PAUSED
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override fun resumeSession() {
|
|
146
|
+
if (_status != SessionStatus.PAUSED) return
|
|
147
|
+
_status = SessionStatus.ACTIVE
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override fun stopSession() {
|
|
151
|
+
if (_status != SessionStatus.ACTIVE && _status != SessionStatus.PAUSED) return
|
|
152
|
+
completeSession()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ═══════════════════════════════════════════════════════════
|
|
156
|
+
// Frame Processing
|
|
157
|
+
// ═══════════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
override fun processFrame(frame: HybridFrameSpec) {
|
|
160
|
+
if (_status != SessionStatus.ACTIVE && _status != SessionStatus.COUNTDOWN) return
|
|
161
|
+
if (!isInitialized || poseLandmarker == null) return
|
|
162
|
+
|
|
163
|
+
// Cast to NativeFrame to get the ImageProxy
|
|
164
|
+
val nativeFrame = frame as? NativeFrame ?: return
|
|
165
|
+
val imageProxy = nativeFrame.image ?: return
|
|
166
|
+
|
|
167
|
+
frameCount++
|
|
168
|
+
if (frameCount % processEveryNFrames != 0) return
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// Convert ImageProxy to Bitmap
|
|
172
|
+
val bitmap = imageProxyToBitmap(imageProxy)
|
|
173
|
+
val mpImage = BitmapImageBuilder(bitmap).build()
|
|
174
|
+
|
|
175
|
+
val result = poseLandmarker!!.detect(mpImage)
|
|
176
|
+
|
|
177
|
+
if (result.landmarks().isNotEmpty()) {
|
|
178
|
+
val poseLandmarks = result.landmarks()[0]
|
|
179
|
+
|
|
180
|
+
if (poseWasLost) {
|
|
181
|
+
poseWasLost = false
|
|
182
|
+
onPoseRegained?.invoke()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_landmarks = poseLandmarks.map { lm ->
|
|
186
|
+
Landmark(
|
|
187
|
+
x = lm.x().toDouble(),
|
|
188
|
+
y = lm.y().toDouble(),
|
|
189
|
+
z = lm.z().toDouble(),
|
|
190
|
+
visibility = (lm.visibility().orElse(0f)).toDouble()
|
|
191
|
+
)
|
|
192
|
+
}.toTypedArray()
|
|
193
|
+
|
|
194
|
+
if (_status == SessionStatus.ACTIVE) {
|
|
195
|
+
processExerciseLogic()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
} else {
|
|
199
|
+
if (!poseWasLost) {
|
|
200
|
+
poseWasLost = true
|
|
201
|
+
onPoseLost?.invoke()
|
|
202
|
+
}
|
|
203
|
+
_landmarks = emptyArray()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
bitmap.recycle()
|
|
207
|
+
|
|
208
|
+
} catch (e: Exception) {
|
|
209
|
+
// MediaPipe detection failed — skip this frame
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ═══════════════════════════════════════════════════════════
|
|
214
|
+
// ImageProxy to Bitmap conversion
|
|
215
|
+
// ═══════════════════════════════════════════════════════════
|
|
216
|
+
|
|
217
|
+
private fun imageProxyToBitmap(imageProxy: androidx.camera.core.ImageProxy): Bitmap {
|
|
218
|
+
val buffer = imageProxy.planes[0].buffer
|
|
219
|
+
val bytes = ByteArray(buffer.remaining())
|
|
220
|
+
buffer.get(bytes)
|
|
221
|
+
|
|
222
|
+
val yuvImage = android.graphics.YuvImage(
|
|
223
|
+
bytes,
|
|
224
|
+
android.graphics.ImageFormat.NV21,
|
|
225
|
+
imageProxy.width,
|
|
226
|
+
imageProxy.height,
|
|
227
|
+
null
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
val out = java.io.ByteArrayOutputStream()
|
|
231
|
+
yuvImage.compressToJpeg(
|
|
232
|
+
android.graphics.Rect(0, 0, imageProxy.width, imageProxy.height),
|
|
233
|
+
100,
|
|
234
|
+
out
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
val jpegBytes = out.toByteArray()
|
|
238
|
+
val bitmap = android.graphics.BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
|
|
239
|
+
|
|
240
|
+
// Apply rotation if needed
|
|
241
|
+
val rotation = imageProxy.imageInfo.rotationDegrees
|
|
242
|
+
return if (rotation != 0) {
|
|
243
|
+
val matrix = Matrix()
|
|
244
|
+
matrix.postRotate(rotation.toFloat())
|
|
245
|
+
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
246
|
+
} else {
|
|
247
|
+
bitmap
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ═══════════════════════════════════════════════════════════
|
|
252
|
+
// Exercise Logic Engine
|
|
253
|
+
// ═══════════════════════════════════════════════════════════
|
|
254
|
+
|
|
255
|
+
private fun processExerciseLogic() {
|
|
256
|
+
val config = exerciseConfig ?: return
|
|
257
|
+
if (_landmarks.isEmpty()) return
|
|
258
|
+
|
|
259
|
+
// 1. Calculate all angles
|
|
260
|
+
val currentAngles = mutableMapOf<String, Double>()
|
|
261
|
+
val angleSnapshots = mutableListOf<AngleSnapshot>()
|
|
262
|
+
|
|
263
|
+
for (angleDef in config.angles) {
|
|
264
|
+
val a = angleDef.landmarkA.toInt()
|
|
265
|
+
val b = angleDef.landmarkB.toInt()
|
|
266
|
+
val c = angleDef.landmarkC.toInt()
|
|
267
|
+
|
|
268
|
+
if (a >= _landmarks.size || b >= _landmarks.size || c >= _landmarks.size) continue
|
|
269
|
+
|
|
270
|
+
val angle = calculateAngle(_landmarks[a], _landmarks[b], _landmarks[c])
|
|
271
|
+
currentAngles[angleDef.name] = angle
|
|
272
|
+
angleSnapshots.add(AngleSnapshot(name = angleDef.name, value = angle))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
repAngleSnapshots = angleSnapshots.toTypedArray()
|
|
276
|
+
|
|
277
|
+
// 2. Determine current phase
|
|
278
|
+
val detectedPhase = determinePhase(currentAngles, config)
|
|
279
|
+
|
|
280
|
+
if (detectedPhase != _currentPhase && detectedPhase != ExercisePhase.UNKNOWN) {
|
|
281
|
+
_currentPhase = detectedPhase
|
|
282
|
+
onPhaseChange?.invoke(detectedPhase)
|
|
283
|
+
handlePhaseTransition(detectedPhase, config)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 3. Check form rules
|
|
287
|
+
checkFormRules(currentAngles, config)
|
|
288
|
+
|
|
289
|
+
// 4. Handle hold-based exercises
|
|
290
|
+
if (config.type == ExerciseType.HOLD) {
|
|
291
|
+
handleHoldProgress(currentAngles, config)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ═══════════════════════════════════════════════════════════
|
|
296
|
+
// Angle Calculation
|
|
297
|
+
// ═══════════════════════════════════════════════════════════
|
|
298
|
+
|
|
299
|
+
private fun calculateAngle(pointA: Landmark, vertex: Landmark, pointC: Landmark): Double {
|
|
300
|
+
val vaX = pointA.x - vertex.x
|
|
301
|
+
val vaY = pointA.y - vertex.y
|
|
302
|
+
val vcX = pointC.x - vertex.x
|
|
303
|
+
val vcY = pointC.y - vertex.y
|
|
304
|
+
|
|
305
|
+
val dot = vaX * vcX + vaY * vcY
|
|
306
|
+
val magA = sqrt(vaX * vaX + vaY * vaY)
|
|
307
|
+
val magC = sqrt(vcX * vcX + vcY * vcY)
|
|
308
|
+
|
|
309
|
+
if (magA == 0.0 || magC == 0.0) return 0.0
|
|
310
|
+
|
|
311
|
+
val cosAngle = max(-1.0, min(1.0, dot / (magA * magC)))
|
|
312
|
+
val angleRad = acos(cosAngle)
|
|
313
|
+
return angleRad * (180.0 / Math.PI)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ═══════════════════════════════════════════════════════════
|
|
317
|
+
// Phase Detection
|
|
318
|
+
// ═══════════════════════════════════════════════════════════
|
|
319
|
+
|
|
320
|
+
private fun determinePhase(angles: Map<String, Double>, config: ExerciseConfig): ExercisePhase {
|
|
321
|
+
for (phaseThreshold in config.phases) {
|
|
322
|
+
val angle = angles[phaseThreshold.angleName] ?: continue
|
|
323
|
+
if (angle >= phaseThreshold.minAngle && angle <= phaseThreshold.maxAngle) {
|
|
324
|
+
return phaseThreshold.phase
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return ExercisePhase.UNKNOWN
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ═══════════════════════════════════════════════════════════
|
|
331
|
+
// Rep Counting State Machine
|
|
332
|
+
// ═══════════════════════════════════════════════════════════
|
|
333
|
+
|
|
334
|
+
private fun handlePhaseTransition(newPhase: ExercisePhase, config: ExerciseConfig) {
|
|
335
|
+
if (config.type != ExerciseType.REP) return
|
|
336
|
+
|
|
337
|
+
phaseHistory.add(newPhase)
|
|
338
|
+
|
|
339
|
+
val repSeq = config.repSequence
|
|
340
|
+
if (phaseHistory.size >= repSeq.size) {
|
|
341
|
+
val tail = phaseHistory.takeLast(repSeq.size)
|
|
342
|
+
|
|
343
|
+
if (tail == repSeq.toList()) {
|
|
344
|
+
val now = System.currentTimeMillis()
|
|
345
|
+
val repDuration = (now - repStartTime).toDouble()
|
|
346
|
+
|
|
347
|
+
// Minimum 800ms per rep
|
|
348
|
+
if (repDuration < 800) {
|
|
349
|
+
phaseHistory.clear()
|
|
350
|
+
phaseHistory.add(newPhase)
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Don't count rep if form is terrible
|
|
355
|
+
if (repFormScore <= 30) {
|
|
356
|
+
onFormFeedback?.invoke(FormFeedback(
|
|
357
|
+
ruleName = "poorForm",
|
|
358
|
+
message = "Fix your form before continuing",
|
|
359
|
+
severity = FormSeverity.ERROR
|
|
360
|
+
))
|
|
361
|
+
repFormScore = 100.0
|
|
362
|
+
phaseHistory.clear()
|
|
363
|
+
phaseHistory.add(newPhase)
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_repCount += 1.0
|
|
368
|
+
|
|
369
|
+
val repData = RepData(
|
|
370
|
+
repNumber = _repCount,
|
|
371
|
+
durationMs = repDuration,
|
|
372
|
+
formScore = repFormScore,
|
|
373
|
+
angles = repAngleSnapshots
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
allRepDurations.add(repDuration)
|
|
377
|
+
allRepFormScores.add(repFormScore)
|
|
378
|
+
|
|
379
|
+
onRepComplete?.invoke(repData)
|
|
380
|
+
|
|
381
|
+
repStartTime = now
|
|
382
|
+
repFormScore = 100.0
|
|
383
|
+
phaseHistory.clear()
|
|
384
|
+
phaseHistory.add(newPhase)
|
|
385
|
+
|
|
386
|
+
if (targetReps > 0 && _repCount >= targetReps) {
|
|
387
|
+
completeSession()
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
val maxHistory = repSeq.size * 2
|
|
393
|
+
if (phaseHistory.size > maxHistory) {
|
|
394
|
+
phaseHistory = phaseHistory.takeLast(maxHistory).toMutableList()
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ═══════════════════════════════════════════════════════════
|
|
399
|
+
// Form Validation
|
|
400
|
+
// ═══════════════════════════════════════════════════════════
|
|
401
|
+
|
|
402
|
+
private fun checkFormRules(currentAngles: Map<String, Double>, config: ExerciseConfig) {
|
|
403
|
+
val now = System.currentTimeMillis()
|
|
404
|
+
val throttleMs = 3000L
|
|
405
|
+
|
|
406
|
+
for (rule in config.formRules) {
|
|
407
|
+
val angle = currentAngles[rule.angleName] ?: continue
|
|
408
|
+
|
|
409
|
+
val isViolating = angle < rule.minAngle || angle > rule.maxAngle
|
|
410
|
+
|
|
411
|
+
if (isViolating) {
|
|
412
|
+
val lastTime = lastFormFeedbackTime[rule.name]
|
|
413
|
+
if (lastTime != null && (now - lastTime) < throttleMs) continue
|
|
414
|
+
|
|
415
|
+
val feedback = FormFeedback(
|
|
416
|
+
ruleName = rule.name,
|
|
417
|
+
message = rule.message,
|
|
418
|
+
severity = rule.severity
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
when (rule.severity) {
|
|
422
|
+
FormSeverity.WARNING -> repFormScore = max(0.0, repFormScore - 5)
|
|
423
|
+
FormSeverity.ERROR -> repFormScore = max(0.0, repFormScore - 15)
|
|
424
|
+
FormSeverity.INFO -> {}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
sessionFormViolations.add(feedback)
|
|
428
|
+
lastFormFeedbackTime[rule.name] = now
|
|
429
|
+
onFormFeedback?.invoke(feedback)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ═══════════════════════════════════════════════════════════
|
|
435
|
+
// Hold Progress
|
|
436
|
+
// ═══════════════════════════════════════════════════════════
|
|
437
|
+
|
|
438
|
+
private fun handleHoldProgress(currentAngles: Map<String, Double>, config: ExerciseConfig) {
|
|
439
|
+
if (config.holdDurationMs <= 0) return
|
|
440
|
+
|
|
441
|
+
var inPosition = true
|
|
442
|
+
for (phaseThreshold in config.phases) {
|
|
443
|
+
val angle = currentAngles[phaseThreshold.angleName]
|
|
444
|
+
if (angle == null || angle < phaseThreshold.minAngle || angle > phaseThreshold.maxAngle) {
|
|
445
|
+
inPosition = false
|
|
446
|
+
break
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (inPosition) {
|
|
451
|
+
if (holdStartTime == null) {
|
|
452
|
+
holdStartTime = System.currentTimeMillis()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
val elapsed = (System.currentTimeMillis() - holdStartTime!!).toDouble()
|
|
456
|
+
val stability = min(100.0, max(0.0, repFormScore))
|
|
457
|
+
|
|
458
|
+
val progress = HoldProgress(
|
|
459
|
+
elapsedMs = elapsed,
|
|
460
|
+
targetMs = config.holdDurationMs,
|
|
461
|
+
stability = stability
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
onHoldProgress?.invoke(progress)
|
|
465
|
+
|
|
466
|
+
if (elapsed >= config.holdDurationMs) {
|
|
467
|
+
completeSession()
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
holdStartTime = null
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ═══════════════════════════════════════════════════════════
|
|
475
|
+
// Session Completion
|
|
476
|
+
// ═══════════════════════════════════════════════════════════
|
|
477
|
+
|
|
478
|
+
private fun completeSession() {
|
|
479
|
+
_status = SessionStatus.COMPLETED
|
|
480
|
+
|
|
481
|
+
val totalDuration = (System.currentTimeMillis() - sessionStartTime).toDouble()
|
|
482
|
+
val avgRepDuration = if (allRepDurations.isEmpty()) 0.0 else allRepDurations.average()
|
|
483
|
+
val avgFormScore = if (allRepFormScores.isEmpty()) 100.0 else allRepFormScores.average()
|
|
484
|
+
|
|
485
|
+
val result = SessionResult(
|
|
486
|
+
totalReps = _repCount,
|
|
487
|
+
totalDurationMs = totalDuration,
|
|
488
|
+
averageRepDurationMs = avgRepDuration,
|
|
489
|
+
averageFormScore = avgFormScore,
|
|
490
|
+
formViolations = sessionFormViolations.toTypedArray(),
|
|
491
|
+
angleHistory = repAngleSnapshots
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
onSessionComplete?.invoke(result)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ═══════════════════════════════════════════════════════════
|
|
498
|
+
// Countdown
|
|
499
|
+
// ═══════════════════════════════════════════════════════════
|
|
500
|
+
|
|
501
|
+
private fun startCountdown() {
|
|
502
|
+
Thread {
|
|
503
|
+
var remaining = countdownSeconds.toInt()
|
|
504
|
+
while (remaining > 0) {
|
|
505
|
+
Thread.sleep(1000)
|
|
506
|
+
remaining--
|
|
507
|
+
}
|
|
508
|
+
_status = SessionStatus.ACTIVE
|
|
509
|
+
sessionStartTime = System.currentTimeMillis()
|
|
510
|
+
repStartTime = System.currentTimeMillis()
|
|
511
|
+
}.start()
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ═══════════════════════════════════════════════════════════
|
|
515
|
+
// Reset
|
|
516
|
+
// ═══════════════════════════════════════════════════════════
|
|
517
|
+
|
|
518
|
+
private fun resetSession() {
|
|
519
|
+
_status = SessionStatus.IDLE
|
|
520
|
+
_currentPhase = ExercisePhase.UNKNOWN
|
|
521
|
+
_repCount = 0.0
|
|
522
|
+
_landmarks = emptyArray()
|
|
523
|
+
phaseHistory.clear()
|
|
524
|
+
repFormScore = 100.0
|
|
525
|
+
repAngleSnapshots = emptyArray()
|
|
526
|
+
allRepDurations.clear()
|
|
527
|
+
allRepFormScores.clear()
|
|
528
|
+
sessionFormViolations.clear()
|
|
529
|
+
lastFormFeedbackTime.clear()
|
|
530
|
+
holdStartTime = null
|
|
531
|
+
poseWasLost = false
|
|
532
|
+
targetReps = 0.0
|
|
533
|
+
countdownSeconds = 0.0
|
|
534
|
+
}
|
|
535
|
+
}
|
package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercisesPackage.kt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroposeexercises
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
|
+
|
|
8
|
+
class NitroPoseExercisesPackage : BaseReactPackage() {
|
|
9
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
14
|
+
return ReactModuleInfoProvider { HashMap() }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
companion object {
|
|
18
|
+
init {
|
|
19
|
+
System.loadLibrary("nitroposeexercises")
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|