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.
@@ -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 = 10
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
- val failureThreshold = if (config.type == ExerciseType.HOLD)
308
- postureFailureThreshold * 3
309
- else
310
- postureFailureThreshold
311
-
312
- if (!isPostureValid(config.postureFamily, config.visibilityThreshold)) {
313
- consecutivePostureFailures++
314
- if (consecutivePostureFailures >= failureThreshold) {
315
- if (!postureWasLost) {
316
- postureWasLost = true
317
- onPostureLost?.invoke()
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
- _currentPhase = ExercisePhase.UNKNOWN
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
- val currentAngles = mutableMapOf<String, Double>()
332
- val angleSnapshots = mutableListOf<AngleSnapshot>()
336
+ // Angle calculation
337
+ val visThreshold = config.visibilityThreshold
338
+ val currentAngles = mutableMapOf<String, Double>()
339
+ val angleSnapshots = mutableListOf<AngleSnapshot>()
333
340
 
334
- for (angleDef in config.angles) {
335
- val a = angleDef.landmarkA.toInt()
336
- val b = angleDef.landmarkB.toInt()
337
- val c = angleDef.landmarkC.toInt()
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
- if (a >= _landmarks.size || b >= _landmarks.size || c >= _landmarks.size) continue
346
+ if (a >= _landmarks.size || b >= _landmarks.size || c >= _landmarks.size) continue
340
347
 
341
- // Only calculate if all three landmarks have reasonable confidence
342
- if (_landmarks[a].visibility < 0.3 || _landmarks[b].visibility < 0.3 || _landmarks[c].visibility < 0.3) continue
348
+ if (_landmarks[a].visibility <= visThreshold ||
349
+ _landmarks[b].visibility <= visThreshold ||
350
+ _landmarks[c].visibility <= visThreshold) continue
343
351
 
344
- val angle = calculateAngle(_landmarks[a], _landmarks[b], _landmarks[c])
345
- currentAngles[angleDef.name] = angle
346
- angleSnapshots.add(AngleSnapshot(name = angleDef.name, value = angle))
347
- }
352
+ val angle = calculateAngle(
353
+ pointA = _landmarks[a],
354
+ vertex = _landmarks[b],
355
+ pointC = _landmarks[c]
356
+ )
348
357
 
349
- repAngleSnapshots = angleSnapshots.toTypedArray()
358
+ currentAngles[angleDef.name] = angle
359
+ angleSnapshots.add(AngleSnapshot(name = angleDef.name, value = angle))
360
+ }
350
361
 
351
- val detectedPhase = determinePhase(currentAngles, config)
362
+ repAngleSnapshots = angleSnapshots
352
363
 
353
- if (detectedPhase != _currentPhase && detectedPhase != ExercisePhase.UNKNOWN) {
354
- _currentPhase = detectedPhase
355
- onPhaseChange?.invoke(detectedPhase)
356
- handlePhaseTransition(detectedPhase, config)
357
- }
364
+ val detectedPhase = determinePhase(currentAngles, config)
358
365
 
359
- checkFormRules(currentAngles, config)
366
+ // Debug log — remove once reps count reliably
367
+ println("[Pose] angles=$currentAngles current=$_currentPhase detected=$detectedPhase history=$phaseHistory reps=$_repCount")
360
368
 
361
- if (config.type == ExerciseType.HOLD) {
362
- handleHoldProgress(currentAngles, config)
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
- if (!hipsVisible) return false
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
- // Case A: side view — shoulders/hips/(ankles) in a horizontal band
662
- val ys = if (anklesVisible)
663
- listOf(shoulderY, hipY, ankleY)
692
+ val wristY = if (wristsVisible)
693
+ (lw.y + rw.y) / 2
664
694
  else
665
- listOf(shoulderY, hipY)
666
- if ((ys.max() - ys.min()) < 0.25) return true
667
-
668
- // Case B: front-facing prone (pushup head-on) large y-spread,
669
- // fall back to upper-body geometry
670
- val le = _landmarks[13]; val re = _landmarks[14]
671
- val lw = _landmarks[15]; val rw = _landmarks[16]
672
- val upperBodyVisible = le.visibility > threshold && re.visibility > threshold &&
673
- lw.visibility > threshold && rw.visibility > threshold
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 = 10 // ~1s at 30fps with throttle=3
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
- _currentPhase = .unknown
291
- phaseHistory = []
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 are mandatory; hips/knees/ankles are optional for close-range framing
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
- // Shoulder width is camera-mirror invariant: |x1 - x2| is the same
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
- // Need hips to make any meaningful judgment for prone/supine
380
- guard hipsVisible else { return false }
381
-
382
- // Case A: side view — shoulders, hips, ankles in a horizontal band
383
- let ys: [Double] = anklesVisible
384
- ? [shoulderY, hipY, ankleY]
385
- : [shoulderY, hipY]
386
- let ySpread = (ys.max() ?? 0) - (ys.min() ?? 0)
387
- if ySpread < 0.25 {
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 (e.g. pushup viewed head-on) body extends
392
- // away along Z, so y-spread is large. Fall back to upper-body geometry.
393
- let le = _landmarks[13], re = _landmarks[14]
394
- let lw = _landmarks[15], rw = _landmarks[16]
395
- let upperBodyVisible = le.visibility > threshold && re.visibility > threshold
396
- && lw.visibility > threshold && rw.visibility > threshold
397
- guard upperBodyVisible else { return false }
398
-
399
- let wristY = (lw.y + rw.y) / 2
400
- guard wristY > shoulderY + 0.03 else { return false }
401
- guard shoulderWidth > 0.10 else { return false }
402
- return true
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 off-frame — accept if shoulders form a reasonable horizontal span,
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: 'none',
13
- visibilityThreshold: 0.3,
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: 40,
38
- maxAngle: 80
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","message","severity","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,MAAM;EACrBC,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;EACN;EACA;EACA;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,EAAE;IAAEC,QAAQ,EAAE;EAAG,CAAC,CACtE;EACDC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC;EACjCC,SAAS,EAAE;EACT;EACA;IACEf,IAAI,EAAE,YAAY;IAClBgB,OAAO,EAAE,UAAU;IACnBC,QAAQ,EAAE,MAAM;IAChBN,SAAS,EAAE,WAAW;IACtBC,QAAQ,EAAE,EAAE;IACZC,QAAQ,EAAE,GAAG,CAAE;IACf;IACA;IACA;EACF,CAAC,CACF;EACDK,cAAc,EAAE;AAClB,CAAC","ignoreList":[]}
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,cAgC3B,CAAC"}
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.16",
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",
@@ -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: 'none',
13
- visibilityThreshold: 0.3,
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: 40, maxAngle: 80 },
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
  };