react-native-nitro-pose-exercises 1.1.7 → 1.1.8

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.
@@ -292,19 +292,23 @@ private fun processExerciseLogic() {
292
292
  val config = exerciseConfig ?: return
293
293
  if (_landmarks.isEmpty()) return
294
294
 
295
- // Posture gate with hysteresis
296
- if (!isPostureValid(config.postureFamily)) {
297
- consecutivePostureFailures++
298
- if (consecutivePostureFailures >= postureFailureThreshold) {
299
- if (!postureWasLost) {
300
- postureWasLost = true
301
- onPostureLost?.invoke()
302
- }
303
- _currentPhase = ExercisePhase.UNKNOWN
304
- phaseHistory.clear()
295
+ val failureThreshold = if (config.type == ExerciseType.HOLD)
296
+ postureFailureThreshold * 3
297
+ else
298
+ postureFailureThreshold
299
+
300
+ if (!isPostureValid(config.postureFamily, config.visibilityThreshold)) {
301
+ consecutivePostureFailures++
302
+ if (consecutivePostureFailures >= failureThreshold) {
303
+ if (!postureWasLost) {
304
+ postureWasLost = true
305
+ onPostureLost?.invoke()
305
306
  }
306
- return
307
+ _currentPhase = ExercisePhase.UNKNOWN
308
+ phaseHistory.clear()
307
309
  }
310
+ return
311
+ }
308
312
 
309
313
  consecutivePostureFailures = 0
310
314
  if (postureWasLost) {
@@ -549,83 +553,6 @@ private fun processExerciseLogic() {
549
553
  onSessionComplete?.invoke(result)
550
554
  }
551
555
 
552
- // ═══════════════════════════════════════════════════════════
553
- // Orientation Helpers
554
- // ═══════════════════════════════════════════════════════════
555
-
556
- private fun rotationDegreesFromFrame(frame: HybridFrameSpec): Int {
557
- return when (frame.orientation.name.lowercase()) {
558
- "up" -> 0
559
- "right" -> 90
560
- "down" -> 180
561
- "left" -> 270
562
- else -> 0
563
- }
564
- }
565
-
566
- // ═══════════════════════════════════════════════════════════
567
- // Posture Gates
568
- // ═══════════════════════════════════════════════════════════
569
-
570
- private func isPostureValid(_ family: String) -> Bool {
571
- guard _landmarks.count >= 33 else { return false }
572
-
573
- let visThreshold = exerciseConfig?.visibilityThreshold ?? 0.3
574
-
575
- if (_landmarks.size < 33) return false
576
-
577
- val ls = _landmarks[11]; val rs = _landmarks[12]
578
- val lh = _landmarks[23]; val rh = _landmarks[24]
579
- val lk = _landmarks[25]; val rk = _landmarks[26]
580
- val la = _landmarks[27]; val ra = _landmarks[28]
581
-
582
- val keyVisible = ls.visibility > 0.3 && rs.visibility > 0.3 &&
583
- lh.visibility > 0.3 && rh.visibility > 0.3
584
- if (!keyVisible) return false
585
-
586
- val shoulderY = (ls.y + rs.y) / 2
587
- val hipY = (lh.y + rh.y) / 2
588
- val shoulderX = (ls.x + rs.x) / 2
589
- val hipX = (lh.x + rh.x) / 2
590
-
591
- val kneesVisible = lk.visibility > 0.3 && rk.visibility > 0.3
592
- val anklesVisible = la.visibility > 0.3 && ra.visibility > 0.3
593
- val kneeY = if (kneesVisible) (lk.y + rk.y) / 2 else hipY
594
- val ankleY = if (anklesVisible) (la.y + ra.y) / 2 else kneeY
595
-
596
- return when (family) {
597
- "horizontalProne" -> {
598
- val ys = listOf(shoulderY, hipY, ankleY)
599
- (ys.max() - ys.min()) < 0.25
600
- }
601
- "standingUpright" -> {
602
- if (!kneesVisible) false
603
- else shoulderY < hipY - 0.08 &&
604
- hipY < kneeY + 0.05 &&
605
- (if (anklesVisible) kneeY < ankleY else true)
606
- }
607
- "seated" -> {
608
- if (!kneesVisible) false
609
- else shoulderY < hipY - 0.05 && kotlin.math.abs(hipY - kneeY) < 0.20
610
- }
611
- "supine" -> {
612
- val ys = listOf(shoulderY, hipY, ankleY)
613
- (ys.max() - ys.min()) < 0.25
614
- }
615
- "sidePlank" -> {
616
- val ySpread = kotlin.math.abs(shoulderY - hipY)
617
- val shoulderHipDx = kotlin.math.abs(shoulderX - hipX)
618
- ySpread < 0.20 && shoulderHipDx < 0.15
619
- }
620
- "inverted" -> {
621
- if (!anklesVisible) false
622
- else hipY < shoulderY && hipY < ankleY
623
- }
624
- "none" -> true
625
- else -> true
626
- }
627
- }
628
-
629
556
  // ═══════════════════════════════════════════════════════════
630
557
  // Countdown
631
558
  // ═══════════════════════════════════════════════════════════
@@ -665,9 +592,77 @@ private func isPostureValid(_ family: String) -> Bool {
665
592
  countdownSeconds = 0.0
666
593
  frameCount = 0
667
594
  consecutivePostureFailures = 0
668
- postureWasLost = false
595
+ postureWasLost = false
669
596
  synchronized(landmarkLock) {
670
597
  cachedLandmarks = emptyArray()
671
598
  }
672
599
  }
600
+
601
+ // ═══════════════════════════════════════════════════════════
602
+ // Orientation Helpers
603
+ // ═══════════════════════════════════════════════════════════
604
+
605
+ private fun rotationDegreesFromFrame(frame: HybridFrameSpec): Int {
606
+ return when (frame.orientation.name.lowercase()) {
607
+ "up" -> 0
608
+ "right" -> 90
609
+ "down" -> 180
610
+ "left" -> 270
611
+ else -> 0
612
+ }
613
+ }
614
+
615
+ // ═══════════════════════════════════════════════════════════
616
+ // Posture Gates
617
+ // ═══════════════════════════════════════════════════════════
618
+
619
+ private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
620
+ if (_landmarks.size < 33) return false
621
+
622
+ val ls = _landmarks[11]; val rs = _landmarks[12]
623
+ val lh = _landmarks[23]; val rh = _landmarks[24]
624
+ val lk = _landmarks[25]; val rk = _landmarks[26]
625
+ val la = _landmarks[27]; val ra = _landmarks[28]
626
+
627
+ val keyVisible = ls.visibility > threshold && rs.visibility > threshold &&
628
+ lh.visibility > threshold && rh.visibility > threshold
629
+ if (!keyVisible) return false
630
+
631
+ val shoulderY = (ls.y + rs.y) / 2
632
+ val hipY = (lh.y + rh.y) / 2
633
+ val shoulderX = (ls.x + rs.x) / 2
634
+ val hipX = (lh.x + rh.x) / 2
635
+
636
+ val kneesVisible = lk.visibility > threshold && rk.visibility > threshold
637
+ val anklesVisible = la.visibility > threshold && ra.visibility > threshold
638
+ val kneeY = if (kneesVisible) (lk.y + rk.y) / 2 else hipY
639
+ val ankleY = if (anklesVisible) (la.y + ra.y) / 2 else kneeY
640
+
641
+ return when (family) {
642
+ PostureFamily.HORIZONTAL_PRONE, PostureFamily.SUPINE -> {
643
+ val ys = listOf(shoulderY, hipY, ankleY)
644
+ (ys.max() - ys.min()) < 0.25
645
+ }
646
+ PostureFamily.STANDING_UPRIGHT -> {
647
+ if (!kneesVisible) false
648
+ else shoulderY < hipY - 0.08 &&
649
+ hipY < kneeY + 0.05 &&
650
+ (if (anklesVisible) kneeY < ankleY else true)
651
+ }
652
+ PostureFamily.SEATED -> {
653
+ if (!kneesVisible) false
654
+ else shoulderY < hipY - 0.05 && kotlin.math.abs(hipY - kneeY) < 0.20
655
+ }
656
+ PostureFamily.SIDE_PLANK -> {
657
+ val ySpread = kotlin.math.abs(shoulderY - hipY)
658
+ val shoulderHipDx = kotlin.math.abs(shoulderX - hipX)
659
+ ySpread < 0.20 && shoulderHipDx < 0.15
660
+ }
661
+ PostureFamily.INVERTED -> {
662
+ if (!anklesVisible) false
663
+ else hipY < shoulderY && hipY < ankleY
664
+ }
665
+ PostureFamily.NONE -> true
666
+ }
667
+ }
673
668
  }
@@ -118,7 +118,7 @@ private var postureWasLost = false
118
118
  func isReady() throws -> Bool {
119
119
  guard let config = exerciseConfig else { return false }
120
120
  guard !_landmarks.isEmpty else { return false }
121
- return isPostureValid(config.postureFamily)
121
+ return isPostureValid(family: config.postureFamily, threshold: config.visibilityThreshold)
122
122
  }
123
123
 
124
124
  // ═══════════════════════════════════════════════════════════
@@ -268,14 +268,19 @@ private static func cgOrientation(orientation: CameraOrientation, isMirrored: Bo
268
268
  // ═══════════════════════════════════════════════════════════
269
269
 
270
270
  private func processExerciseLogic() {
271
-
272
271
  guard let config = exerciseConfig else { return }
273
272
  guard !_landmarks.isEmpty else { return }
274
273
 
274
+ // Hold exercises get 3x more tolerance — stationary poses suffer from
275
+ // visibility flicker more than active reps.
276
+ let failureThreshold = config.type == .hold
277
+ ? postureFailureThreshold * 3
278
+ : postureFailureThreshold
279
+
275
280
  // Posture gate with hysteresis
276
281
  if !isPostureValid(family: config.postureFamily, threshold: config.visibilityThreshold) {
277
282
  consecutivePostureFailures += 1
278
- if consecutivePostureFailures >= postureFailureThreshold {
283
+ if consecutivePostureFailures >= failureThreshold {
279
284
  if !postureWasLost {
280
285
  postureWasLost = true
281
286
  onPostureLost?()
@@ -287,67 +292,60 @@ private func processExerciseLogic() {
287
292
  }
288
293
 
289
294
  consecutivePostureFailures = 0
290
-
291
295
  if postureWasLost {
292
296
  postureWasLost = false
293
297
  onPostureRegained?()
294
298
  }
295
299
 
296
- var currentAngles: [String: Double] = [:]
297
- var angleSnapshots: [AngleSnapshot] = []
298
-
299
- let visThreshold = config.visibilityThreshold
300
-
301
- for angleDef in config.angles {
302
- let a = Int(angleDef.landmarkA)
303
- let b = Int(angleDef.landmarkB)
304
- let c = Int(angleDef.landmarkC)
305
-
306
- guard a < _landmarks.count, b < _landmarks.count, c < _landmarks.count else { continue }
300
+ // Angle calculation
301
+ let visThreshold = config.visibilityThreshold
302
+ var currentAngles: [String: Double] = [:]
303
+ var angleSnapshots: [AngleSnapshot] = []
307
304
 
308
- guard _landmarks[a].visibility > visThreshold,
309
- _landmarks[b].visibility > visThreshold,
310
- _landmarks[c].visibility > visThreshold else { continue }
305
+ for angleDef in config.angles {
306
+ let a = Int(angleDef.landmarkA)
307
+ let b = Int(angleDef.landmarkB)
308
+ let c = Int(angleDef.landmarkC)
311
309
 
310
+ guard a < _landmarks.count, b < _landmarks.count, c < _landmarks.count else { continue }
312
311
 
313
- let angle = calculateAngle(
314
- pointA: _landmarks[a],
315
- vertex: _landmarks[b],
316
- pointC: _landmarks[c]
317
- )
312
+ guard _landmarks[a].visibility > visThreshold,
313
+ _landmarks[b].visibility > visThreshold,
314
+ _landmarks[c].visibility > visThreshold else { continue }
318
315
 
319
- currentAngles[angleDef.name] = angle
320
- angleSnapshots.append(AngleSnapshot(name: angleDef.name, value: angle))
321
- }
316
+ let angle = calculateAngle(
317
+ pointA: _landmarks[a],
318
+ vertex: _landmarks[b],
319
+ pointC: _landmarks[c]
320
+ )
322
321
 
323
- repAngleSnapshots = angleSnapshots
322
+ currentAngles[angleDef.name] = angle
323
+ angleSnapshots.append(AngleSnapshot(name: angleDef.name, value: angle))
324
+ }
324
325
 
325
- // Debug logging — uncomment to see angles
326
- // for (name, angle) in currentAngles {
327
- // print("[PoseExercise] Angle \(name): \(String(format: "%.1f", angle))°")
328
- // }
326
+ repAngleSnapshots = angleSnapshots
329
327
 
330
- let detectedPhase = determinePhase(from: currentAngles, config: config)
328
+ let detectedPhase = determinePhase(from: currentAngles, config: config)
331
329
 
332
- if detectedPhase != _currentPhase && detectedPhase != .unknown {
333
- let previousPhase = _currentPhase
334
- _currentPhase = detectedPhase
335
- onPhaseChange?(detectedPhase)
336
- handlePhaseTransition(from: previousPhase, to: detectedPhase, config: config)
337
- }
330
+ if detectedPhase != _currentPhase && detectedPhase != .unknown {
331
+ let previousPhase = _currentPhase
332
+ _currentPhase = detectedPhase
333
+ onPhaseChange?(detectedPhase)
334
+ handlePhaseTransition(from: previousPhase, to: detectedPhase, config: config)
335
+ }
338
336
 
339
- checkFormRules(currentAngles: currentAngles, config: config)
337
+ checkFormRules(currentAngles: currentAngles, config: config)
340
338
 
341
- if config.type == .hold {
342
- handleHoldProgress(currentAngles: currentAngles, config: config)
343
- }
339
+ if config.type == .hold {
340
+ handleHoldProgress(currentAngles: currentAngles, config: config)
344
341
  }
342
+ }
345
343
 
346
344
  // ═══════════════════════════════════════════════════════════
347
345
  // MARK: - Posture Gates
348
346
  // ═══════════════════════════════════════════════════════════
349
347
 
350
- private func isPostureValid(_ family: String) -> Bool {
348
+ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
351
349
  guard _landmarks.count >= 33 else { return false }
352
350
 
353
351
  let ls = _landmarks[11], rs = _landmarks[12]
@@ -368,44 +366,31 @@ private func isPostureValid(_ family: String) -> Bool {
368
366
  let kneeY = kneesVisible ? (lk.y + rk.y) / 2 : hipY
369
367
  let ankleY = anklesVisible ? (la.y + ra.y) / 2 : kneeY
370
368
 
371
- switch family {
372
- case "horizontalProne":
373
- let ys = [shoulderY, hipY, ankleY]
374
- let spread = (ys.max() ?? 0) - (ys.min() ?? 0)
375
- return spread < 0.25
376
-
377
- case "standingUpright":
378
- guard kneesVisible else { return false }
379
- return shoulderY < hipY - 0.08
380
- && hipY < kneeY + 0.05
381
- && (anklesVisible ? kneeY < ankleY : true)
382
-
383
- case "seated":
384
- guard kneesVisible else { return false }
385
- return shoulderY < hipY - 0.05
386
- && abs(hipY - kneeY) < 0.20
387
-
388
- case "supine":
389
- let ys = [shoulderY, hipY, ankleY]
390
- let spread = (ys.max() ?? 0) - (ys.min() ?? 0)
391
- return spread < 0.25
392
-
393
- case "sidePlank":
394
- let ys = [shoulderY, hipY]
395
- let ySpread = (ys.max() ?? 0) - (ys.min() ?? 0)
396
- let shoulderHipDx = abs(shoulderX - hipX)
397
- return ySpread < 0.20 && shoulderHipDx < 0.15
398
-
399
- case "inverted":
400
- guard anklesVisible else { return false }
401
- return hipY < shoulderY && hipY < ankleY
402
-
403
- case "none":
404
- return true
405
-
406
- default:
407
- return true
408
- }
369
+ switch family {
370
+ case .horizontalProne, .supine:
371
+ let ys = [shoulderY, hipY, ankleY]
372
+ return ((ys.max() ?? 0) - (ys.min() ?? 0)) < 0.25
373
+
374
+ case .standingUpright:
375
+ guard kneesVisible else { return false }
376
+ return shoulderY < hipY - 0.08 && hipY < kneeY + 0.05 && (anklesVisible ? kneeY < ankleY : true)
377
+
378
+ case .seated:
379
+ guard kneesVisible else { return false }
380
+ return shoulderY < hipY - 0.05 && abs(hipY - kneeY) < 0.20
381
+
382
+ case .sidePlank:
383
+ let ySpread = abs(shoulderY - hipY)
384
+ let shoulderHipDx = abs(shoulderX - hipX)
385
+ return ySpread < 0.20 && shoulderHipDx < 0.15
386
+
387
+ case .inverted:
388
+ guard anklesVisible else { return false }
389
+ return hipY < shoulderY && hipY < ankleY
390
+
391
+ case .none:
392
+ return true
393
+ }
409
394
  }
410
395
 
411
396
  // ═══════════════════════════════════════════════════════════
@@ -662,7 +647,7 @@ private func isPostureValid(_ family: String) -> Bool {
662
647
  countdownSeconds = 0
663
648
  countdownTimer?.invalidate()
664
649
  countdownTimer = nil
665
- frameCount = 0,
650
+ frameCount = 0
666
651
  consecutivePostureFailures = 0
667
652
  postureWasLost = false
668
653
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-pose-exercises",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
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",