react-native-nitro-pose-exercises 1.0.8 → 1.1.1
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/NitroPoseExercises.podspec +2 -1
- package/README.md +137 -92
- package/android/build.gradle +9 -10
- package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +148 -125
- package/ios/NitroPoseExercises.swift +113 -88
- package/lib/module/config/bicep-curl.js +61 -0
- package/lib/module/config/bicep-curl.js.map +1 -0
- package/lib/module/config/chair-pose.js +64 -0
- package/lib/module/config/chair-pose.js.map +1 -0
- package/lib/module/config/cobra-pose.js +57 -0
- package/lib/module/config/cobra-pose.js.map +1 -0
- package/lib/module/config/downward-dog.js +65 -0
- package/lib/module/config/downward-dog.js.map +1 -0
- package/lib/module/config/lunge.js +62 -0
- package/lib/module/config/lunge.js.map +1 -0
- package/lib/module/config/plank.js +57 -0
- package/lib/module/config/plank.js.map +1 -0
- package/lib/module/config/shoulder-press.js +62 -0
- package/lib/module/config/shoulder-press.js.map +1 -0
- package/lib/module/config/situp.js +48 -0
- package/lib/module/config/situp.js.map +1 -0
- package/lib/module/config/squat.js +69 -0
- package/lib/module/config/squat.js.map +1 -0
- package/lib/module/config/tree-pose.js +57 -0
- package/lib/module/config/tree-pose.js.map +1 -0
- package/lib/module/config/tricep-dip.js +55 -0
- package/lib/module/config/tricep-dip.js.map +1 -0
- package/lib/module/config/wall-sit.js +57 -0
- package/lib/module/config/wall-sit.js.map +1 -0
- package/lib/module/config/warrior-i.js +79 -0
- package/lib/module/config/warrior-i.js.map +1 -0
- package/lib/module/config/warrior-ii.js +86 -0
- package/lib/module/config/warrior-ii.js.map +1 -0
- package/lib/module/index.js +19 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/config/bicep-curl.d.ts +3 -0
- package/lib/typescript/src/config/bicep-curl.d.ts.map +1 -0
- package/lib/typescript/src/config/chair-pose.d.ts +3 -0
- package/lib/typescript/src/config/chair-pose.d.ts.map +1 -0
- package/lib/typescript/src/config/cobra-pose.d.ts +3 -0
- package/lib/typescript/src/config/cobra-pose.d.ts.map +1 -0
- package/lib/typescript/src/config/downward-dog.d.ts +3 -0
- package/lib/typescript/src/config/downward-dog.d.ts.map +1 -0
- package/lib/typescript/src/config/lunge.d.ts +3 -0
- package/lib/typescript/src/config/lunge.d.ts.map +1 -0
- package/lib/typescript/src/config/plank.d.ts +3 -0
- package/lib/typescript/src/config/plank.d.ts.map +1 -0
- package/lib/typescript/src/config/shoulder-press.d.ts +3 -0
- package/lib/typescript/src/config/shoulder-press.d.ts.map +1 -0
- package/lib/typescript/src/config/situp.d.ts +3 -0
- package/lib/typescript/src/config/situp.d.ts.map +1 -0
- package/lib/typescript/src/config/squat.d.ts +3 -0
- package/lib/typescript/src/config/squat.d.ts.map +1 -0
- package/lib/typescript/src/config/tree-pose.d.ts +3 -0
- package/lib/typescript/src/config/tree-pose.d.ts.map +1 -0
- package/lib/typescript/src/config/tricep-dip.d.ts +3 -0
- package/lib/typescript/src/config/tricep-dip.d.ts.map +1 -0
- package/lib/typescript/src/config/wall-sit.d.ts +3 -0
- package/lib/typescript/src/config/wall-sit.d.ts.map +1 -0
- package/lib/typescript/src/config/warrior-i.d.ts +3 -0
- package/lib/typescript/src/config/warrior-i.d.ts.map +1 -0
- package/lib/typescript/src/config/warrior-ii.d.ts +3 -0
- package/lib/typescript/src/config/warrior-ii.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +14 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/nitro.json +8 -1
- package/package.json +10 -10
- package/src/config/bicep-curl.ts +64 -0
- package/src/config/chair-pose.ts +67 -0
- package/src/config/cobra-pose.ts +59 -0
- package/src/config/downward-dog.ts +68 -0
- package/src/config/lunge.ts +65 -0
- package/src/config/plank.ts +59 -0
- package/src/config/shoulder-press.ts +63 -0
- package/src/config/situp.ts +51 -0
- package/src/config/squat.ts +71 -0
- package/src/config/tree-pose.ts +59 -0
- package/src/config/tricep-dip.ts +58 -0
- package/src/config/wall-sit.ts +59 -0
- package/src/config/warrior-i.ts +82 -0
- package/src/config/warrior-ii.ts +88 -0
- package/src/index.tsx +19 -0
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
package com.margelo.nitro.nitroposeexercises
|
|
2
2
|
|
|
3
3
|
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Matrix
|
|
5
|
+
import android.media.Image
|
|
4
6
|
import androidx.annotation.Keep
|
|
5
7
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
6
|
-
import com.google.
|
|
7
|
-
import com.google.
|
|
8
|
-
import com.google.
|
|
9
|
-
import com.google.
|
|
10
|
-
import com.google.
|
|
8
|
+
import com.google.mlkit.vision.common.InputImage
|
|
9
|
+
import com.google.mlkit.vision.pose.PoseDetection
|
|
10
|
+
import com.google.mlkit.vision.pose.PoseDetector
|
|
11
|
+
import com.google.mlkit.vision.pose.PoseLandmark
|
|
12
|
+
import com.google.mlkit.vision.pose.defaults.PoseDetectorOptions
|
|
11
13
|
import com.margelo.nitro.NitroModules
|
|
12
14
|
import com.margelo.nitro.core.Promise
|
|
13
15
|
import com.margelo.nitro.camera.HybridFrameSpec
|
|
16
|
+
import com.margelo.nitro.camera.NativeFrame
|
|
14
17
|
import kotlin.math.acos
|
|
15
18
|
import kotlin.math.max
|
|
16
19
|
import kotlin.math.min
|
|
@@ -20,10 +23,52 @@ import kotlin.math.sqrt
|
|
|
20
23
|
@DoNotStrip
|
|
21
24
|
class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
|
|
22
25
|
|
|
23
|
-
// ───
|
|
24
|
-
private var
|
|
26
|
+
// ─── ML Kit ─────────────────────────────────────────────────
|
|
27
|
+
private var poseDetector: PoseDetector? = null
|
|
25
28
|
private var isInitialized = false
|
|
26
29
|
|
|
30
|
+
// ─── Cached Landmarks (ML Kit is async, we cache last result) ──
|
|
31
|
+
private var cachedLandmarks: Array<Landmark> = emptyArray()
|
|
32
|
+
private val landmarkLock = Any()
|
|
33
|
+
|
|
34
|
+
// ─── Landmark Index Mapping ─────────────────────────────────
|
|
35
|
+
// ML Kit PoseLandmark type → MediaPipe index that JS configs expect
|
|
36
|
+
private val mlKitToMediaPipeMap = mapOf(
|
|
37
|
+
PoseLandmark.NOSE to 0,
|
|
38
|
+
PoseLandmark.LEFT_EYE_INNER to 1,
|
|
39
|
+
PoseLandmark.LEFT_EYE to 2,
|
|
40
|
+
PoseLandmark.LEFT_EYE_OUTER to 3,
|
|
41
|
+
PoseLandmark.RIGHT_EYE_INNER to 4,
|
|
42
|
+
PoseLandmark.RIGHT_EYE to 5,
|
|
43
|
+
PoseLandmark.RIGHT_EYE_OUTER to 6,
|
|
44
|
+
PoseLandmark.LEFT_EAR to 7,
|
|
45
|
+
PoseLandmark.RIGHT_EAR to 8,
|
|
46
|
+
PoseLandmark.LEFT_MOUTH to 9,
|
|
47
|
+
PoseLandmark.RIGHT_MOUTH to 10,
|
|
48
|
+
PoseLandmark.LEFT_SHOULDER to 11,
|
|
49
|
+
PoseLandmark.RIGHT_SHOULDER to 12,
|
|
50
|
+
PoseLandmark.LEFT_ELBOW to 13,
|
|
51
|
+
PoseLandmark.RIGHT_ELBOW to 14,
|
|
52
|
+
PoseLandmark.LEFT_WRIST to 15,
|
|
53
|
+
PoseLandmark.RIGHT_WRIST to 16,
|
|
54
|
+
PoseLandmark.LEFT_PINKY to 17,
|
|
55
|
+
PoseLandmark.RIGHT_PINKY to 18,
|
|
56
|
+
PoseLandmark.LEFT_INDEX to 19,
|
|
57
|
+
PoseLandmark.RIGHT_INDEX to 20,
|
|
58
|
+
PoseLandmark.LEFT_THUMB to 21,
|
|
59
|
+
PoseLandmark.RIGHT_THUMB to 22,
|
|
60
|
+
PoseLandmark.LEFT_HIP to 23,
|
|
61
|
+
PoseLandmark.RIGHT_HIP to 24,
|
|
62
|
+
PoseLandmark.LEFT_KNEE to 25,
|
|
63
|
+
PoseLandmark.RIGHT_KNEE to 26,
|
|
64
|
+
PoseLandmark.LEFT_ANKLE to 27,
|
|
65
|
+
PoseLandmark.RIGHT_ANKLE to 28,
|
|
66
|
+
PoseLandmark.LEFT_HEEL to 29,
|
|
67
|
+
PoseLandmark.RIGHT_HEEL to 30,
|
|
68
|
+
PoseLandmark.LEFT_FOOT_INDEX to 31,
|
|
69
|
+
PoseLandmark.RIGHT_FOOT_INDEX to 32,
|
|
70
|
+
)
|
|
71
|
+
|
|
27
72
|
// ─── Exercise Config ────────────────────────────────────────
|
|
28
73
|
private var exerciseConfig: ExerciseConfig? = null
|
|
29
74
|
|
|
@@ -46,8 +91,6 @@ class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
|
|
|
46
91
|
private var sessionStartTime: Long = System.currentTimeMillis()
|
|
47
92
|
private var targetReps: Double = 0.0
|
|
48
93
|
private var countdownSeconds: Double = 0.0
|
|
49
|
-
private var frameCount: Int = 0
|
|
50
|
-
private val processEveryNFrames: Int = 3
|
|
51
94
|
|
|
52
95
|
// ─── Form Tracking ──────────────────────────────────────────
|
|
53
96
|
private var lastFormFeedbackTime = mutableMapOf<String, Long>()
|
|
@@ -60,6 +103,10 @@ class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
|
|
|
60
103
|
// ─── Pose Tracking ──────────────────────────────────────────
|
|
61
104
|
private var poseWasLost = false
|
|
62
105
|
|
|
106
|
+
// ─── Frame Throttle ─────────────────────────────────────────
|
|
107
|
+
private var frameCount: Int = 0
|
|
108
|
+
private val processEveryNFrames: Int = 3
|
|
109
|
+
|
|
63
110
|
// ─── Callbacks ──────────────────────────────────────────────
|
|
64
111
|
override var onRepComplete: ((data: RepData) -> Unit)? = null
|
|
65
112
|
override var onPhaseChange: ((phase: ExercisePhase) -> Unit)? = null
|
|
@@ -77,31 +124,21 @@ class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
|
|
|
77
124
|
// ═══════════════════════════════════════════════════════════
|
|
78
125
|
|
|
79
126
|
override fun initialize(modelPath: String): Promise<Unit> {
|
|
127
|
+
// No model file needed — ML Kit downloads/bundles its own model
|
|
80
128
|
return Promise.async {
|
|
81
|
-
val
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
val baseOptions = BaseOptions.builder()
|
|
85
|
-
.setModelAssetPath(modelPath)
|
|
86
|
-
.build()
|
|
87
|
-
|
|
88
|
-
val options = PoseLandmarkerOptions.builder()
|
|
89
|
-
.setBaseOptions(baseOptions)
|
|
90
|
-
.setRunningMode(RunningMode.IMAGE)
|
|
91
|
-
.setNumPoses(1)
|
|
92
|
-
.setMinPoseDetectionConfidence(0.5f)
|
|
93
|
-
.setMinPosePresenceConfidence(0.5f)
|
|
94
|
-
.setMinTrackingConfidence(0.5f)
|
|
129
|
+
val options = PoseDetectorOptions.Builder()
|
|
130
|
+
.setDetectorMode(PoseDetectorOptions.STREAM_MODE)
|
|
95
131
|
.build()
|
|
96
132
|
|
|
97
|
-
|
|
133
|
+
poseDetector = PoseDetection.getClient(options)
|
|
98
134
|
isInitialized = true
|
|
135
|
+
println("[PoseExercise] Initialized with ML Kit Pose Detection (no model file needed)")
|
|
99
136
|
}
|
|
100
137
|
}
|
|
101
138
|
|
|
102
139
|
override fun release() {
|
|
103
|
-
|
|
104
|
-
|
|
140
|
+
poseDetector?.close()
|
|
141
|
+
poseDetector = null
|
|
105
142
|
isInitialized = false
|
|
106
143
|
_status = SessionStatus.IDLE
|
|
107
144
|
resetSession()
|
|
@@ -151,105 +188,88 @@ class HybridPoseExercise : HybridNitroPoseExercisesSpec() {
|
|
|
151
188
|
}
|
|
152
189
|
|
|
153
190
|
// ═══════════════════════════════════════════════════════════
|
|
154
|
-
// Frame Processing
|
|
191
|
+
// Frame Processing (ML Kit — async with cached results)
|
|
155
192
|
// ═══════════════════════════════════════════════════════════
|
|
156
193
|
|
|
157
|
-
override fun processFrame(frame: HybridFrameSpec) {
|
|
194
|
+
override fun processFrame(frame: HybridFrameSpec) {
|
|
158
195
|
if (_status != SessionStatus.ACTIVE && _status != SessionStatus.COUNTDOWN) return
|
|
159
|
-
if (!isInitialized ||
|
|
196
|
+
if (!isInitialized || poseDetector == null) return
|
|
160
197
|
|
|
198
|
+
// Frame throttle
|
|
161
199
|
frameCount++
|
|
162
200
|
if (frameCount % processEveryNFrames != 0) return
|
|
163
201
|
|
|
202
|
+
// Get the ImageProxy from VisionCamera frame
|
|
203
|
+
val nativeFrame = frame as? NativeFrame ?: return
|
|
204
|
+
val imageProxy = nativeFrame.image ?: return
|
|
205
|
+
|
|
164
206
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
207
|
+
// ML Kit takes InputImage directly from ImageProxy — no color conversion needed
|
|
208
|
+
@androidx.camera.core.ExperimentalGetImage
|
|
209
|
+
val mediaImage: Image = imageProxy.image ?: return
|
|
210
|
+
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
|
168
211
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
?: throw Exception("Failed to wrap HardwareBuffer to Bitmap")
|
|
212
|
+
// ML Kit is async — fire detection, cache result, use cached landmarks for current frame
|
|
213
|
+
poseDetector!!.process(inputImage)
|
|
214
|
+
.addOnSuccessListener { pose ->
|
|
215
|
+
val poseLandmarks = pose.allPoseLandmarks
|
|
174
216
|
|
|
175
|
-
|
|
176
|
-
|
|
217
|
+
if (poseLandmarks.isNotEmpty()) {
|
|
218
|
+
if (poseWasLost) {
|
|
219
|
+
poseWasLost = false
|
|
220
|
+
onPoseRegained?.invoke()
|
|
221
|
+
}
|
|
177
222
|
|
|
178
|
-
|
|
179
|
-
|
|
223
|
+
// Build landmark array mapped to MediaPipe indices (34 slots)
|
|
224
|
+
val landmarkArray = Array(34) { Landmark(x = 0.0, y = 0.0, z = 0.0, visibility = 0.0) }
|
|
180
225
|
|
|
181
|
-
|
|
182
|
-
val
|
|
226
|
+
val imageWidth = inputImage.width.toDouble()
|
|
227
|
+
val imageHeight = inputImage.height.toDouble()
|
|
183
228
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
229
|
+
for (poseLandmark in poseLandmarks) {
|
|
230
|
+
val mediaPipeIndex = mlKitToMediaPipeMap[poseLandmark.landmarkType] ?: continue
|
|
231
|
+
if (mediaPipeIndex >= 34) continue
|
|
232
|
+
|
|
233
|
+
// Normalize coordinates to 0-1 range
|
|
234
|
+
landmarkArray[mediaPipeIndex] = Landmark(
|
|
235
|
+
x = poseLandmark.position.x.toDouble() / imageWidth,
|
|
236
|
+
y = poseLandmark.position.y.toDouble() / imageHeight,
|
|
237
|
+
z = poseLandmark.position3D.z.toDouble(),
|
|
238
|
+
visibility = poseLandmark.inFrameLikelihood.toDouble()
|
|
239
|
+
)
|
|
187
240
|
}
|
|
188
241
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
x = lm.x().toDouble(),
|
|
192
|
-
y = lm.y().toDouble(),
|
|
193
|
-
z = lm.z().toDouble(),
|
|
194
|
-
visibility = (lm.visibility().orElse(0f)).toDouble()
|
|
195
|
-
)
|
|
196
|
-
}.toTypedArray()
|
|
197
|
-
|
|
198
|
-
if (_status == SessionStatus.ACTIVE) {
|
|
199
|
-
processExerciseLogic()
|
|
242
|
+
synchronized(landmarkLock) {
|
|
243
|
+
cachedLandmarks = landmarkArray
|
|
200
244
|
}
|
|
201
|
-
|
|
245
|
+
} else {
|
|
202
246
|
if (!poseWasLost) {
|
|
203
|
-
|
|
204
|
-
|
|
247
|
+
poseWasLost = true
|
|
248
|
+
onPoseLost?.invoke()
|
|
249
|
+
}
|
|
250
|
+
synchronized(landmarkLock) {
|
|
251
|
+
cachedLandmarks = emptyArray()
|
|
205
252
|
}
|
|
206
|
-
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
.addOnFailureListener { e ->
|
|
256
|
+
println("[PoseExercise] ML Kit error: ${e.message}")
|
|
207
257
|
}
|
|
208
258
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// MediaPipe detection failed — skip this frame
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// ═══════════════════════════════════════════════════════════
|
|
219
|
-
// ImageProxy to Bitmap conversion
|
|
220
|
-
// ═══════════════════════════════════════════════════════════
|
|
221
|
-
|
|
222
|
-
private fun imageProxyToBitmap(imageProxy: androidx.camera.core.ImageProxy): Bitmap {
|
|
223
|
-
val buffer = imageProxy.planes[0].buffer
|
|
224
|
-
val bytes = ByteArray(buffer.remaining())
|
|
225
|
-
buffer.get(bytes)
|
|
226
|
-
|
|
227
|
-
val yuvImage = android.graphics.YuvImage(
|
|
228
|
-
bytes,
|
|
229
|
-
android.graphics.ImageFormat.NV21,
|
|
230
|
-
imageProxy.width,
|
|
231
|
-
imageProxy.height,
|
|
232
|
-
null
|
|
233
|
-
)
|
|
259
|
+
// Use cached landmarks for exercise logic (from previous frame's detection)
|
|
260
|
+
val currentLandmarks: Array<Landmark>
|
|
261
|
+
synchronized(landmarkLock) {
|
|
262
|
+
currentLandmarks = cachedLandmarks.copyOf()
|
|
263
|
+
}
|
|
234
264
|
|
|
235
|
-
|
|
236
|
-
yuvImage.compressToJpeg(
|
|
237
|
-
android.graphics.Rect(0, 0, imageProxy.width, imageProxy.height),
|
|
238
|
-
100,
|
|
239
|
-
out
|
|
240
|
-
)
|
|
265
|
+
_landmarks = currentLandmarks
|
|
241
266
|
|
|
242
|
-
|
|
243
|
-
|
|
267
|
+
if (currentLandmarks.isNotEmpty() && _status == SessionStatus.ACTIVE) {
|
|
268
|
+
processExerciseLogic()
|
|
269
|
+
}
|
|
244
270
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return if (rotation != 0) {
|
|
248
|
-
val matrix = Matrix()
|
|
249
|
-
matrix.postRotate(rotation.toFloat())
|
|
250
|
-
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
251
|
-
} else {
|
|
252
|
-
bitmap
|
|
271
|
+
} catch (e: Exception) {
|
|
272
|
+
println("[PoseExercise] Frame processing error: ${e.message}")
|
|
253
273
|
}
|
|
254
274
|
}
|
|
255
275
|
|
|
@@ -261,7 +281,6 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
261
281
|
val config = exerciseConfig ?: return
|
|
262
282
|
if (_landmarks.isEmpty()) return
|
|
263
283
|
|
|
264
|
-
// 1. Calculate all angles
|
|
265
284
|
val currentAngles = mutableMapOf<String, Double>()
|
|
266
285
|
val angleSnapshots = mutableListOf<AngleSnapshot>()
|
|
267
286
|
|
|
@@ -272,6 +291,9 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
272
291
|
|
|
273
292
|
if (a >= _landmarks.size || b >= _landmarks.size || c >= _landmarks.size) continue
|
|
274
293
|
|
|
294
|
+
// Only calculate if all three landmarks have reasonable confidence
|
|
295
|
+
if (_landmarks[a].visibility < 0.3 || _landmarks[b].visibility < 0.3 || _landmarks[c].visibility < 0.3) continue
|
|
296
|
+
|
|
275
297
|
val angle = calculateAngle(_landmarks[a], _landmarks[b], _landmarks[c])
|
|
276
298
|
currentAngles[angleDef.name] = angle
|
|
277
299
|
angleSnapshots.add(AngleSnapshot(name = angleDef.name, value = angle))
|
|
@@ -279,7 +301,6 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
279
301
|
|
|
280
302
|
repAngleSnapshots = angleSnapshots.toTypedArray()
|
|
281
303
|
|
|
282
|
-
// 2. Determine current phase
|
|
283
304
|
val detectedPhase = determinePhase(currentAngles, config)
|
|
284
305
|
|
|
285
306
|
if (detectedPhase != _currentPhase && detectedPhase != ExercisePhase.UNKNOWN) {
|
|
@@ -288,10 +309,8 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
288
309
|
handlePhaseTransition(detectedPhase, config)
|
|
289
310
|
}
|
|
290
311
|
|
|
291
|
-
// 3. Check form rules
|
|
292
312
|
checkFormRules(currentAngles, config)
|
|
293
313
|
|
|
294
|
-
// 4. Handle hold-based exercises
|
|
295
314
|
if (config.type == ExerciseType.HOLD) {
|
|
296
315
|
handleHoldProgress(currentAngles, config)
|
|
297
316
|
}
|
|
@@ -345,37 +364,37 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
345
364
|
if (phaseHistory.size >= repSeq.size) {
|
|
346
365
|
val tail = phaseHistory.takeLast(repSeq.size)
|
|
347
366
|
|
|
348
|
-
|
|
367
|
+
if (tail == repSeq.toList()) {
|
|
349
368
|
val now = System.currentTimeMillis()
|
|
350
369
|
val repDuration = (now - repStartTime).toDouble()
|
|
351
370
|
|
|
352
371
|
// Minimum 800ms per rep
|
|
353
372
|
if (repDuration < 800) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
373
|
+
phaseHistory.clear()
|
|
374
|
+
phaseHistory.add(newPhase)
|
|
375
|
+
return
|
|
357
376
|
}
|
|
358
377
|
|
|
359
378
|
// Don't count rep if form is terrible
|
|
360
379
|
if (repFormScore <= 30) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
380
|
+
onFormFeedback?.invoke(FormFeedback(
|
|
381
|
+
ruleName = "poorForm",
|
|
382
|
+
message = "Fix your form before continuing",
|
|
383
|
+
severity = FormSeverity.ERROR
|
|
384
|
+
))
|
|
385
|
+
repFormScore = 100.0
|
|
386
|
+
phaseHistory.clear()
|
|
387
|
+
phaseHistory.add(newPhase)
|
|
388
|
+
return
|
|
370
389
|
}
|
|
371
390
|
|
|
372
391
|
_repCount += 1.0
|
|
373
392
|
|
|
374
393
|
val repData = RepData(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
394
|
+
repNumber = _repCount,
|
|
395
|
+
durationMs = repDuration,
|
|
396
|
+
formScore = repFormScore,
|
|
397
|
+
angles = repAngleSnapshots
|
|
379
398
|
)
|
|
380
399
|
|
|
381
400
|
allRepDurations.add(repDuration)
|
|
@@ -389,9 +408,9 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
389
408
|
phaseHistory.add(newPhase)
|
|
390
409
|
|
|
391
410
|
if (targetReps > 0 && _repCount >= targetReps) {
|
|
392
|
-
|
|
411
|
+
completeSession()
|
|
393
412
|
}
|
|
394
|
-
|
|
413
|
+
}
|
|
395
414
|
}
|
|
396
415
|
|
|
397
416
|
val maxHistory = repSeq.size * 2
|
|
@@ -536,5 +555,9 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
536
555
|
poseWasLost = false
|
|
537
556
|
targetReps = 0.0
|
|
538
557
|
countdownSeconds = 0.0
|
|
558
|
+
frameCount = 0
|
|
559
|
+
synchronized(landmarkLock) {
|
|
560
|
+
cachedLandmarks = emptyArray()
|
|
561
|
+
}
|
|
539
562
|
}
|
|
540
563
|
}
|