react-native-nitro-pose-exercises 1.1.16 → 1.1.18
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/android/build.gradle +1 -0
- package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +87 -61
- package/ios/NitroPoseExercises.swift +41 -33
- package/lib/module/config/pushup.js +6 -21
- package/lib/module/config/pushup.js.map +1 -1
- package/lib/typescript/src/config/pushup.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/config/pushup.ts +4 -19
package/android/build.gradle
CHANGED
|
@@ -122,5 +122,6 @@ dependencies {
|
|
|
122
122
|
implementation project(":react-native-nitro-modules")
|
|
123
123
|
implementation 'com.google.mlkit:pose-detection:18.0.0-beta5'
|
|
124
124
|
implementation project(":react-native-vision-camera")
|
|
125
|
+
implementation "androidx.camera:camera-core:1.3.4"
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -4,6 +4,7 @@ import com.margelo.nitro.camera.HybridFrameSpec
|
|
|
4
4
|
import com.margelo.nitro.camera.public.NativeFrame
|
|
5
5
|
import com.google.android.gms.tasks.Tasks
|
|
6
6
|
import java.util.concurrent.TimeUnit
|
|
7
|
+
import androidx.camera.core.ImageProxy
|
|
7
8
|
|
|
8
9
|
// import android.graphics.Matrix
|
|
9
10
|
import androidx.annotation.Keep
|
|
@@ -130,7 +131,7 @@ override var onPostureRegained: (() -> Unit)? = null
|
|
|
130
131
|
|
|
131
132
|
// ─── Posture Gate ──────────────────────────────────────────
|
|
132
133
|
private var consecutivePostureFailures: Int = 0
|
|
133
|
-
private val postureFailureThreshold: Int =
|
|
134
|
+
private val postureFailureThreshold: Int = 30 // ~3s — tolerant of pushup occlusion
|
|
134
135
|
private var postureWasLost = false
|
|
135
136
|
|
|
136
137
|
// ─── Hold Tracking ──────────────────────────────────────────
|
|
@@ -214,10 +215,6 @@ override fun isReady(): Boolean {
|
|
|
214
215
|
// Frame Processing (ML Kit — async with cached results)
|
|
215
216
|
// ═══════════════════════════════════════════════════════════
|
|
216
217
|
|
|
217
|
-
// Reusable scratch — allocated once, never GC'd per frame
|
|
218
|
-
@Volatile private var lastProcessTime: Long = 0L
|
|
219
|
-
private val minIntervalMs: Long = 66L // ~15fps cap; bump down to 33 for ~30fps
|
|
220
|
-
|
|
221
218
|
// Time-based throttle — more reliable than frame-count under variable FPS
|
|
222
219
|
@Volatile private var lastProcessTime: Long = 0L
|
|
223
220
|
private val minIntervalMs: Long = 66L // ~15fps; lower to 33 for ~30fps once release build is fast enough
|
|
@@ -304,23 +301,31 @@ private fun processExerciseLogic() {
|
|
|
304
301
|
val config = exerciseConfig ?: return
|
|
305
302
|
if (_landmarks.isEmpty()) return
|
|
306
303
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
304
|
+
// Hold exercises get 3x more tolerance — stationary poses suffer from
|
|
305
|
+
// visibility flicker more than active reps.
|
|
306
|
+
val failureThreshold = if (config.type == ExerciseType.HOLD) {
|
|
307
|
+
postureFailureThreshold * 3
|
|
308
|
+
} else {
|
|
309
|
+
postureFailureThreshold
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Posture gate with hysteresis
|
|
313
|
+
if (!isPostureValid(config.postureFamily, config.visibilityThreshold)) {
|
|
314
|
+
consecutivePostureFailures += 1
|
|
315
|
+
if (consecutivePostureFailures >= failureThreshold) {
|
|
316
|
+
if (!postureWasLost) {
|
|
317
|
+
postureWasLost = true
|
|
318
|
+
onPostureLost?.invoke()
|
|
319
|
+
}
|
|
320
|
+
// Only nuke phase history after EXTENDED loss (3x threshold).
|
|
321
|
+
// Brief occlusions during a pushup shouldn't wipe an in-progress rep.
|
|
322
|
+
if (consecutivePostureFailures >= failureThreshold * 3) {
|
|
323
|
+
_currentPhase = ExercisePhase.UNKNOWN
|
|
324
|
+
phaseHistory = mutableListOf()
|
|
325
|
+
}
|
|
318
326
|
}
|
|
319
|
-
|
|
320
|
-
phaseHistory.clear()
|
|
327
|
+
return
|
|
321
328
|
}
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
329
|
|
|
325
330
|
consecutivePostureFailures = 0
|
|
326
331
|
if (postureWasLost) {
|
|
@@ -328,40 +333,52 @@ if (!isPostureValid(config.postureFamily, config.visibilityThreshold)) {
|
|
|
328
333
|
onPostureRegained?.invoke()
|
|
329
334
|
}
|
|
330
335
|
|
|
331
|
-
|
|
332
|
-
|
|
336
|
+
// Angle calculation
|
|
337
|
+
val visThreshold = config.visibilityThreshold
|
|
338
|
+
val currentAngles = mutableMapOf<String, Double>()
|
|
339
|
+
val angleSnapshots = mutableListOf<AngleSnapshot>()
|
|
333
340
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
341
|
+
for (angleDef in config.angles) {
|
|
342
|
+
val a = angleDef.landmarkA.toInt()
|
|
343
|
+
val b = angleDef.landmarkB.toInt()
|
|
344
|
+
val c = angleDef.landmarkC.toInt()
|
|
338
345
|
|
|
339
|
-
|
|
346
|
+
if (a >= _landmarks.size || b >= _landmarks.size || c >= _landmarks.size) continue
|
|
340
347
|
|
|
341
|
-
|
|
342
|
-
|
|
348
|
+
if (_landmarks[a].visibility <= visThreshold ||
|
|
349
|
+
_landmarks[b].visibility <= visThreshold ||
|
|
350
|
+
_landmarks[c].visibility <= visThreshold) continue
|
|
343
351
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
352
|
+
val angle = calculateAngle(
|
|
353
|
+
pointA = _landmarks[a],
|
|
354
|
+
vertex = _landmarks[b],
|
|
355
|
+
pointC = _landmarks[c]
|
|
356
|
+
)
|
|
348
357
|
|
|
349
|
-
|
|
358
|
+
currentAngles[angleDef.name] = angle
|
|
359
|
+
angleSnapshots.add(AngleSnapshot(name = angleDef.name, value = angle))
|
|
360
|
+
}
|
|
350
361
|
|
|
351
|
-
|
|
362
|
+
repAngleSnapshots = angleSnapshots
|
|
352
363
|
|
|
353
|
-
|
|
354
|
-
_currentPhase = detectedPhase
|
|
355
|
-
onPhaseChange?.invoke(detectedPhase)
|
|
356
|
-
handlePhaseTransition(detectedPhase, config)
|
|
357
|
-
}
|
|
364
|
+
val detectedPhase = determinePhase(currentAngles, config)
|
|
358
365
|
|
|
359
|
-
|
|
366
|
+
// Debug log — remove once reps count reliably
|
|
367
|
+
println("[Pose] angles=$currentAngles current=$_currentPhase detected=$detectedPhase history=$phaseHistory reps=$_repCount")
|
|
360
368
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
369
|
+
if (detectedPhase != _currentPhase && detectedPhase != ExercisePhase.UNKNOWN) {
|
|
370
|
+
val previousPhase = _currentPhase
|
|
371
|
+
_currentPhase = detectedPhase
|
|
372
|
+
onPhaseChange?.invoke(detectedPhase)
|
|
373
|
+
handlePhaseTransition(previousPhase, detectedPhase, config)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
checkFormRules(currentAngles, config)
|
|
377
|
+
|
|
378
|
+
if (config.type == ExerciseType.HOLD) {
|
|
379
|
+
handleHoldProgress(currentAngles, config)
|
|
364
380
|
}
|
|
381
|
+
}
|
|
365
382
|
|
|
366
383
|
// ═══════════════════════════════════════════════════════════
|
|
367
384
|
// Angle Calculation
|
|
@@ -635,6 +652,7 @@ private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
|
|
|
635
652
|
val lh = _landmarks[23]; val rh = _landmarks[24]
|
|
636
653
|
val lk = _landmarks[25]; val rk = _landmarks[26]
|
|
637
654
|
val la = _landmarks[27]; val ra = _landmarks[28]
|
|
655
|
+
val lw = _landmarks[15]; val rw = _landmarks[16]
|
|
638
656
|
|
|
639
657
|
// Shoulders mandatory; everything below is optional for close-range framing
|
|
640
658
|
val shouldersVisible = ls.visibility > threshold && rs.visibility > threshold
|
|
@@ -643,6 +661,8 @@ private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
|
|
|
643
661
|
val hipsVisible = lh.visibility > threshold && rh.visibility > threshold
|
|
644
662
|
val kneesVisible = lk.visibility > threshold && rk.visibility > threshold
|
|
645
663
|
val anklesVisible = la.visibility > threshold && ra.visibility > threshold
|
|
664
|
+
val wristsVisible = lw.visibility > threshold && rw.visibility > threshold
|
|
665
|
+
val oneWristVisible = lw.visibility > threshold || rw.visibility > threshold
|
|
646
666
|
|
|
647
667
|
val shoulderY = (ls.y + rs.y) / 2
|
|
648
668
|
val shoulderX = (ls.x + rs.x) / 2
|
|
@@ -656,25 +676,31 @@ private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
|
|
|
656
676
|
|
|
657
677
|
return when (family) {
|
|
658
678
|
PostureFamily.HORIZONTALPRONE, PostureFamily.SUPINE -> {
|
|
659
|
-
|
|
679
|
+
// Case A: side view — full body in horizontal band
|
|
680
|
+
if (hipsVisible) {
|
|
681
|
+
val ys = if (anklesVisible)
|
|
682
|
+
listOf(shoulderY, hipY, ankleY)
|
|
683
|
+
else
|
|
684
|
+
listOf(shoulderY, hipY)
|
|
685
|
+
if ((ys.max() - ys.min()) < 0.25) return true
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Case B: front-facing prone — minimum signature is shoulders + at least
|
|
689
|
+
// one wrist. Hips often occluded by the body in pushup framings.
|
|
690
|
+
if (!oneWristVisible) return false
|
|
660
691
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
listOf(shoulderY, hipY, ankleY)
|
|
692
|
+
val wristY = if (wristsVisible)
|
|
693
|
+
(lw.y + rw.y) / 2
|
|
664
694
|
else
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
val
|
|
671
|
-
val
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
if (!upperBodyVisible) return false
|
|
675
|
-
|
|
676
|
-
val wristY = (lw.y + rw.y) / 2
|
|
677
|
-
wristY > shoulderY + 0.03 && shoulderWidth > 0.10
|
|
695
|
+
kotlin.math.max(lw.y, rw.y)
|
|
696
|
+
|
|
697
|
+
// Geometry: wrists at or below shoulder line, torso reasonably wide.
|
|
698
|
+
// Tolerance loosened for deep DOWN phase where shoulders drop close
|
|
699
|
+
// to wrist level.
|
|
700
|
+
val handsLowerOrLevel = wristY > shoulderY - 0.08
|
|
701
|
+
val torsoFacing = shoulderWidth > 0.06
|
|
702
|
+
|
|
703
|
+
handsLowerOrLevel && torsoFacing
|
|
678
704
|
}
|
|
679
705
|
|
|
680
706
|
PostureFamily.STANDINGUPRIGHT -> {
|
|
@@ -72,7 +72,7 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
|
|
|
72
72
|
|
|
73
73
|
// ─── Posture Gate ──────────────────────────────────────────
|
|
74
74
|
private var consecutivePostureFailures: Int = 0
|
|
75
|
-
private let postureFailureThreshold: Int =
|
|
75
|
+
private let postureFailureThreshold: Int = 30 // ~3s at 30fps with throttle=3 — tolerant of pushup occlusion
|
|
76
76
|
private var postureWasLost = false
|
|
77
77
|
|
|
78
78
|
// ─── Pose Tracking ──────────────────────────────────────────
|
|
@@ -287,8 +287,12 @@ private func processExerciseLogic() {
|
|
|
287
287
|
postureWasLost = true
|
|
288
288
|
onPostureLost?()
|
|
289
289
|
}
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
// Only nuke phase history after EXTENDED loss (3x threshold).
|
|
291
|
+
// Brief occlusions during a pushup shouldn't wipe an in-progress rep.
|
|
292
|
+
if consecutivePostureFailures >= failureThreshold * 3 {
|
|
293
|
+
_currentPhase = .unknown
|
|
294
|
+
phaseHistory = []
|
|
295
|
+
}
|
|
292
296
|
}
|
|
293
297
|
return
|
|
294
298
|
}
|
|
@@ -329,6 +333,9 @@ private func processExerciseLogic() {
|
|
|
329
333
|
|
|
330
334
|
let detectedPhase = determinePhase(from: currentAngles, config: config)
|
|
331
335
|
|
|
336
|
+
// Debug log — remove once reps count reliably
|
|
337
|
+
print("[Pose] angles=\(currentAngles) current=\(_currentPhase) detected=\(detectedPhase) history=\(phaseHistory) reps=\(_repCount)")
|
|
338
|
+
|
|
332
339
|
if detectedPhase != _currentPhase && detectedPhase != .unknown {
|
|
333
340
|
let previousPhase = _currentPhase
|
|
334
341
|
_currentPhase = detectedPhase
|
|
@@ -354,14 +361,17 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
|
|
|
354
361
|
let lh = _landmarks[23], rh = _landmarks[24]
|
|
355
362
|
let lk = _landmarks[25], rk = _landmarks[26]
|
|
356
363
|
let la = _landmarks[27], ra = _landmarks[28]
|
|
364
|
+
let lw = _landmarks[15], rw = _landmarks[16]
|
|
357
365
|
|
|
358
|
-
// Shoulders
|
|
366
|
+
// Shoulders mandatory; everything below is optional for close-range framing
|
|
359
367
|
let shouldersVisible = ls.visibility > threshold && rs.visibility > threshold
|
|
360
368
|
guard shouldersVisible else { return false }
|
|
361
369
|
|
|
362
370
|
let hipsVisible = lh.visibility > threshold && rh.visibility > threshold
|
|
363
371
|
let kneesVisible = lk.visibility > threshold && rk.visibility > threshold
|
|
364
372
|
let anklesVisible = la.visibility > threshold && ra.visibility > threshold
|
|
373
|
+
let wristsVisible = lw.visibility > threshold && rw.visibility > threshold
|
|
374
|
+
let oneWristVisible = lw.visibility > threshold || rw.visibility > threshold
|
|
365
375
|
|
|
366
376
|
let shoulderY = (ls.y + rs.y) / 2
|
|
367
377
|
let shoulderX = (ls.x + rs.x) / 2
|
|
@@ -370,40 +380,40 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
|
|
|
370
380
|
let kneeY = kneesVisible ? (lk.y + rk.y) / 2 : hipY
|
|
371
381
|
let ankleY = anklesVisible ? (la.y + ra.y) / 2 : kneeY
|
|
372
382
|
|
|
373
|
-
//
|
|
374
|
-
// whether the front camera mirror has been applied or not.
|
|
383
|
+
// Mirror-invariant — works for front and back camera identically
|
|
375
384
|
let shoulderWidth = (ls.x - rs.x).magnitude
|
|
376
385
|
|
|
377
386
|
switch family {
|
|
378
387
|
case .horizontalprone, .supine:
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return true
|
|
388
|
+
// Case A: side view — full body in horizontal band
|
|
389
|
+
if hipsVisible {
|
|
390
|
+
let ys: [Double] = anklesVisible
|
|
391
|
+
? [shoulderY, hipY, ankleY]
|
|
392
|
+
: [shoulderY, hipY]
|
|
393
|
+
let ySpread = (ys.max() ?? 0) - (ys.min() ?? 0)
|
|
394
|
+
if ySpread < 0.25 {
|
|
395
|
+
return true
|
|
396
|
+
}
|
|
389
397
|
}
|
|
390
398
|
|
|
391
|
-
// Case B: front-facing prone
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
399
|
+
// Case B: front-facing prone — minimum signature is shoulders + at least
|
|
400
|
+
// one wrist. We do NOT require hips because in pushup framings hips are
|
|
401
|
+
// often occluded by the body itself.
|
|
402
|
+
guard oneWristVisible else { return false }
|
|
403
|
+
|
|
404
|
+
let wristY = wristsVisible
|
|
405
|
+
? (lw.y + rw.y) / 2
|
|
406
|
+
: max(lw.y, rw.y) // whichever wrist we can see
|
|
407
|
+
|
|
408
|
+
// Geometry: wrists at or below shoulder line, torso reasonably wide.
|
|
409
|
+
// Tolerance loosened to handle deep DOWN phase where shoulders drop
|
|
410
|
+
// close to wrist level.
|
|
411
|
+
let handsLowerOrLevel = wristY > shoulderY - 0.08
|
|
412
|
+
let torsoFacing = shoulderWidth > 0.06
|
|
413
|
+
|
|
414
|
+
return handsLowerOrLevel && torsoFacing
|
|
403
415
|
|
|
404
416
|
case .standingupright:
|
|
405
|
-
// Front-facing standing: user often crops hips/knees at close range.
|
|
406
|
-
// Use a tiered check based on what's visible.
|
|
407
417
|
if hipsVisible {
|
|
408
418
|
if kneesVisible {
|
|
409
419
|
return shoulderY < hipY - 0.05
|
|
@@ -412,8 +422,7 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
|
|
|
412
422
|
}
|
|
413
423
|
return shoulderY < hipY - 0.05
|
|
414
424
|
}
|
|
415
|
-
// Hips
|
|
416
|
-
// which means the person is upright and facing (or backing) the camera.
|
|
425
|
+
// Hips cropped (close-range front-facing) — accept based on shoulder span
|
|
417
426
|
return shoulderWidth > 0.08
|
|
418
427
|
|
|
419
428
|
case .seated:
|
|
@@ -423,7 +432,6 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
|
|
|
423
432
|
if hipsVisible {
|
|
424
433
|
return shoulderY < hipY - 0.05
|
|
425
434
|
}
|
|
426
|
-
// Seated with hips out of frame is rare; fall back to upright shoulder span
|
|
427
435
|
return shoulderWidth > 0.08
|
|
428
436
|
|
|
429
437
|
case .sideplank:
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
export const PUSHUP_CONFIG = {
|
|
10
10
|
name: 'Push-Up',
|
|
11
11
|
type: 'rep',
|
|
12
|
-
postureFamily: '
|
|
13
|
-
visibilityThreshold: 0.
|
|
12
|
+
postureFamily: 'horizontalProne',
|
|
13
|
+
visibilityThreshold: 0.1,
|
|
14
14
|
cameraAngle: 'front',
|
|
15
15
|
angles: [{
|
|
16
16
|
name: 'leftElbow',
|
|
@@ -23,10 +23,7 @@ export const PUSHUP_CONFIG = {
|
|
|
23
23
|
landmarkB: 14,
|
|
24
24
|
landmarkC: 16
|
|
25
25
|
}],
|
|
26
|
-
phases: [
|
|
27
|
-
// Calibrated for 2D-projected angles in front-facing portrait filming.
|
|
28
|
-
// Real anatomical 180° appears as ~140-150° due to perspective foreshortening.
|
|
29
|
-
{
|
|
26
|
+
phases: [{
|
|
30
27
|
phase: 'up',
|
|
31
28
|
angleName: 'leftElbow',
|
|
32
29
|
minAngle: 130,
|
|
@@ -34,23 +31,11 @@ export const PUSHUP_CONFIG = {
|
|
|
34
31
|
}, {
|
|
35
32
|
phase: 'down',
|
|
36
33
|
angleName: 'leftElbow',
|
|
37
|
-
minAngle:
|
|
38
|
-
maxAngle:
|
|
34
|
+
minAngle: 0,
|
|
35
|
+
maxAngle: 120
|
|
39
36
|
}],
|
|
40
37
|
repSequence: ['up', 'down', 'up'],
|
|
41
|
-
formRules: [
|
|
42
|
-
// Trigger when descending but stalled above true-down threshold
|
|
43
|
-
{
|
|
44
|
-
name: 'shallowRep',
|
|
45
|
-
message: 'Go lower',
|
|
46
|
-
severity: 'info',
|
|
47
|
-
angleName: 'leftElbow',
|
|
48
|
-
minAngle: 80,
|
|
49
|
-
maxAngle: 130 // "limbo zone" — too low to be up, too high to be down
|
|
50
|
-
// Note: ideally this only fires when angle has stalled, not on the way down.
|
|
51
|
-
// If your formRule engine doesn't support velocity/stall detection,
|
|
52
|
-
// accept that it'll fire briefly during transitions too.
|
|
53
|
-
}],
|
|
38
|
+
formRules: [],
|
|
54
39
|
holdDurationMs: 0
|
|
55
40
|
};
|
|
56
41
|
//# sourceMappingURL=pushup.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["PUSHUP_CONFIG","name","type","postureFamily","visibilityThreshold","cameraAngle","angles","landmarkA","landmarkB","landmarkC","phases","phase","angleName","minAngle","maxAngle","repSequence","formRules","
|
|
1
|
+
{"version":3,"names":["PUSHUP_CONFIG","name","type","postureFamily","visibilityThreshold","cameraAngle","angles","landmarkA","landmarkB","landmarkC","phases","phase","angleName","minAngle","maxAngle","repSequence","formRules","holdDurationMs"],"sourceRoot":"../../../src","sources":["config/pushup.ts"],"mappings":";;AAEA;AACA;AACA;AACA;AACA;;AAEA,OAAO,MAAMA,aAA6B,GAAG;EAC3CC,IAAI,EAAE,SAAS;EACfC,IAAI,EAAE,KAAK;EACXC,aAAa,EAAE,iBAAiB;EAChCC,mBAAmB,EAAE,GAAG;EACxBC,WAAW,EAAE,OAAO;EACpBC,MAAM,EAAE,CACN;IAAEL,IAAI,EAAE,WAAW;IAAEM,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE;EAAG,CAAC,EAClE;IAAER,IAAI,EAAE,YAAY;IAAEM,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE;EAAG,CAAC,CACpE;EACDC,MAAM,EAAE,CACN;IAAEC,KAAK,EAAE,IAAI;IAAEC,SAAS,EAAE,WAAW;IAAEC,QAAQ,EAAE,GAAG;IAAEC,QAAQ,EAAE;EAAI,CAAC,EACrE;IAAEH,KAAK,EAAE,MAAM;IAAEC,SAAS,EAAE,WAAW;IAAEC,QAAQ,EAAE,CAAC;IAAEC,QAAQ,EAAE;EAAI,CAAC,CACtE;EACDC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC;EACjCC,SAAS,EAAE,EAAE;EACbC,cAAc,EAAE;AAClB,CAAC","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pushup.d.ts","sourceRoot":"","sources":["../../../../src/config/pushup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAQlE,eAAO,MAAM,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"pushup.d.ts","sourceRoot":"","sources":["../../../../src/config/pushup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAQlE,eAAO,MAAM,aAAa,EAAE,cAiB3B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-pose-exercises",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.18",
|
|
4
4
|
"description": "Real-time on-device exercise tracking for React Native. Rep counting, form validation, and skeleton overlay powered by Apple Vision (iOS) and Google ML Kit (Android) with VisionCamera v5 via Nitro Modules.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
package/src/config/pushup.ts
CHANGED
|
@@ -9,33 +9,18 @@ import type { ExerciseConfig } from '../NitroPoseExercises.nitro';
|
|
|
9
9
|
export const PUSHUP_CONFIG: ExerciseConfig = {
|
|
10
10
|
name: 'Push-Up',
|
|
11
11
|
type: 'rep',
|
|
12
|
-
postureFamily: '
|
|
13
|
-
visibilityThreshold: 0.
|
|
12
|
+
postureFamily: 'horizontalProne',
|
|
13
|
+
visibilityThreshold: 0.1,
|
|
14
14
|
cameraAngle: 'front',
|
|
15
15
|
angles: [
|
|
16
16
|
{ name: 'leftElbow', landmarkA: 11, landmarkB: 13, landmarkC: 15 },
|
|
17
17
|
{ name: 'rightElbow', landmarkA: 12, landmarkB: 14, landmarkC: 16 },
|
|
18
18
|
],
|
|
19
19
|
phases: [
|
|
20
|
-
// Calibrated for 2D-projected angles in front-facing portrait filming.
|
|
21
|
-
// Real anatomical 180° appears as ~140-150° due to perspective foreshortening.
|
|
22
20
|
{ phase: 'up', angleName: 'leftElbow', minAngle: 130, maxAngle: 180 },
|
|
23
|
-
{ phase: 'down', angleName: 'leftElbow', minAngle:
|
|
21
|
+
{ phase: 'down', angleName: 'leftElbow', minAngle: 0, maxAngle: 120 },
|
|
24
22
|
],
|
|
25
23
|
repSequence: ['up', 'down', 'up'],
|
|
26
|
-
formRules: [
|
|
27
|
-
// Trigger when descending but stalled above true-down threshold
|
|
28
|
-
{
|
|
29
|
-
name: 'shallowRep',
|
|
30
|
-
message: 'Go lower',
|
|
31
|
-
severity: 'info',
|
|
32
|
-
angleName: 'leftElbow',
|
|
33
|
-
minAngle: 80,
|
|
34
|
-
maxAngle: 130, // "limbo zone" — too low to be up, too high to be down
|
|
35
|
-
// Note: ideally this only fires when angle has stalled, not on the way down.
|
|
36
|
-
// If your formRule engine doesn't support velocity/stall detection,
|
|
37
|
-
// accept that it'll fire briefly during transitions too.
|
|
38
|
-
},
|
|
39
|
-
],
|
|
24
|
+
formRules: [],
|
|
40
25
|
holdDurationMs: 0,
|
|
41
26
|
};
|