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.
Files changed (124) hide show
  1. package/LICENSE +20 -0
  2. package/NitroPoseExercises.podspec +32 -0
  3. package/README.md +538 -0
  4. package/android/CMakeLists.txt +31 -0
  5. package/android/build.gradle +121 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +535 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercisesPackage.kt +22 -0
  10. package/ios/NitroPoseExercises.swift +527 -0
  11. package/lib/module/NitroPoseExercises.nitro.js +17 -0
  12. package/lib/module/NitroPoseExercises.nitro.js.map +1 -0
  13. package/lib/module/config/pushup.js +71 -0
  14. package/lib/module/config/pushup.js.map +1 -0
  15. package/lib/module/index.js +7 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/typescript/package.json +1 -0
  19. package/lib/typescript/src/NitroPoseExercises.nitro.d.ts +97 -0
  20. package/lib/typescript/src/NitroPoseExercises.nitro.d.ts.map +1 -0
  21. package/lib/typescript/src/config/pushup.d.ts +3 -0
  22. package/lib/typescript/src/config/pushup.d.ts.map +1 -0
  23. package/lib/typescript/src/index.d.ts +6 -0
  24. package/lib/typescript/src/index.d.ts.map +1 -0
  25. package/nitro.json +23 -0
  26. package/nitrogen/generated/android/c++/JAngleDefinition.hpp +69 -0
  27. package/nitrogen/generated/android/c++/JAngleSnapshot.hpp +61 -0
  28. package/nitrogen/generated/android/c++/JExerciseConfig.hpp +166 -0
  29. package/nitrogen/generated/android/c++/JExercisePhase.hpp +67 -0
  30. package/nitrogen/generated/android/c++/JExerciseType.hpp +61 -0
  31. package/nitrogen/generated/android/c++/JFormFeedback.hpp +67 -0
  32. package/nitrogen/generated/android/c++/JFormRule.hpp +79 -0
  33. package/nitrogen/generated/android/c++/JFormSeverity.hpp +61 -0
  34. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  35. package/nitrogen/generated/android/c++/JFunc_void_ExercisePhase.hpp +77 -0
  36. package/nitrogen/generated/android/c++/JFunc_void_FormFeedback.hpp +80 -0
  37. package/nitrogen/generated/android/c++/JFunc_void_HoldProgress.hpp +77 -0
  38. package/nitrogen/generated/android/c++/JFunc_void_RepData.hpp +81 -0
  39. package/nitrogen/generated/android/c++/JFunc_void_SessionResult.hpp +85 -0
  40. package/nitrogen/generated/android/c++/JHoldProgress.hpp +65 -0
  41. package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.cpp +311 -0
  42. package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.hpp +87 -0
  43. package/nitrogen/generated/android/c++/JLandmark.hpp +69 -0
  44. package/nitrogen/generated/android/c++/JPhaseThreshold.hpp +71 -0
  45. package/nitrogen/generated/android/c++/JRepData.hpp +90 -0
  46. package/nitrogen/generated/android/c++/JSessionResult.hpp +120 -0
  47. package/nitrogen/generated/android/c++/JSessionStatus.hpp +67 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/AngleDefinition.kt +66 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/AngleSnapshot.kt +56 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseConfig.kt +81 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExercisePhase.kt +26 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseType.kt +24 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormFeedback.kt +61 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormRule.kt +76 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormSeverity.kt +24 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void.kt +80 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_ExercisePhase.kt +80 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_FormFeedback.kt +80 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_HoldProgress.kt +80 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_RepData.kt +80 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_SessionResult.kt +80 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HoldProgress.kt +61 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HybridNitroPoseExercisesSpec.kt +196 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Landmark.kt +66 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/PhaseThreshold.kt +66 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/RepData.kt +66 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/SessionResult.kt +76 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/SessionStatus.kt +26 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/nitroposeexercisesOnLoad.kt +35 -0
  70. package/nitrogen/generated/android/nitroposeexercises+autolinking.cmake +81 -0
  71. package/nitrogen/generated/android/nitroposeexercises+autolinking.gradle +27 -0
  72. package/nitrogen/generated/android/nitroposeexercisesOnLoad.cpp +66 -0
  73. package/nitrogen/generated/android/nitroposeexercisesOnLoad.hpp +34 -0
  74. package/nitrogen/generated/ios/NitroPoseExercises+autolinking.rb +62 -0
  75. package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.cpp +100 -0
  76. package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.hpp +449 -0
  77. package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Umbrella.hpp +95 -0
  78. package/nitrogen/generated/ios/NitroPoseExercisesAutolinking.mm +33 -0
  79. package/nitrogen/generated/ios/NitroPoseExercisesAutolinking.swift +26 -0
  80. package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.cpp +11 -0
  81. package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.hpp +236 -0
  82. package/nitrogen/generated/ios/swift/AngleDefinition.swift +44 -0
  83. package/nitrogen/generated/ios/swift/AngleSnapshot.swift +34 -0
  84. package/nitrogen/generated/ios/swift/ExerciseConfig.swift +83 -0
  85. package/nitrogen/generated/ios/swift/ExercisePhase.swift +52 -0
  86. package/nitrogen/generated/ios/swift/ExerciseType.swift +44 -0
  87. package/nitrogen/generated/ios/swift/FormFeedback.swift +39 -0
  88. package/nitrogen/generated/ios/swift/FormRule.swift +54 -0
  89. package/nitrogen/generated/ios/swift/FormSeverity.swift +44 -0
  90. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  91. package/nitrogen/generated/ios/swift/Func_void_ExercisePhase.swift +46 -0
  92. package/nitrogen/generated/ios/swift/Func_void_FormFeedback.swift +46 -0
  93. package/nitrogen/generated/ios/swift/Func_void_HoldProgress.swift +46 -0
  94. package/nitrogen/generated/ios/swift/Func_void_RepData.swift +46 -0
  95. package/nitrogen/generated/ios/swift/Func_void_SessionResult.swift +46 -0
  96. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  97. package/nitrogen/generated/ios/swift/HoldProgress.swift +39 -0
  98. package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec.swift +73 -0
  99. package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec_cxx.swift +483 -0
  100. package/nitrogen/generated/ios/swift/Landmark.swift +44 -0
  101. package/nitrogen/generated/ios/swift/PhaseThreshold.swift +44 -0
  102. package/nitrogen/generated/ios/swift/RepData.swift +50 -0
  103. package/nitrogen/generated/ios/swift/SessionResult.swift +66 -0
  104. package/nitrogen/generated/ios/swift/SessionStatus.swift +52 -0
  105. package/nitrogen/generated/shared/c++/AngleDefinition.hpp +95 -0
  106. package/nitrogen/generated/shared/c++/AngleSnapshot.hpp +87 -0
  107. package/nitrogen/generated/shared/c++/ExerciseConfig.hpp +122 -0
  108. package/nitrogen/generated/shared/c++/ExercisePhase.hpp +88 -0
  109. package/nitrogen/generated/shared/c++/ExerciseType.hpp +80 -0
  110. package/nitrogen/generated/shared/c++/FormFeedback.hpp +93 -0
  111. package/nitrogen/generated/shared/c++/FormRule.hpp +105 -0
  112. package/nitrogen/generated/shared/c++/FormSeverity.hpp +80 -0
  113. package/nitrogen/generated/shared/c++/HoldProgress.hpp +91 -0
  114. package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.cpp +46 -0
  115. package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.hpp +117 -0
  116. package/nitrogen/generated/shared/c++/Landmark.hpp +95 -0
  117. package/nitrogen/generated/shared/c++/PhaseThreshold.hpp +97 -0
  118. package/nitrogen/generated/shared/c++/RepData.hpp +97 -0
  119. package/nitrogen/generated/shared/c++/SessionResult.hpp +108 -0
  120. package/nitrogen/generated/shared/c++/SessionStatus.hpp +88 -0
  121. package/package.json +187 -0
  122. package/src/NitroPoseExercises.nitro.ts +155 -0
  123. package/src/config/pushup.ts +62 -0
  124. 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,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -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
+ }
@@ -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
+ }