react-native-nitro-pose-exercises 1.1.7 → 1.1.9
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >=
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
+
// Angle calculation
|
|
301
|
+
let visThreshold = config.visibilityThreshold
|
|
302
|
+
var currentAngles: [String: Double] = [:]
|
|
303
|
+
var angleSnapshots: [AngleSnapshot] = []
|
|
300
304
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
+
for angleDef in config.angles {
|
|
306
|
+
let a = Int(angleDef.landmarkA)
|
|
307
|
+
let b = Int(angleDef.landmarkB)
|
|
308
|
+
let c = Int(angleDef.landmarkC)
|
|
305
309
|
|
|
306
|
-
|
|
310
|
+
guard a < _landmarks.count, b < _landmarks.count, c < _landmarks.count else { continue }
|
|
307
311
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
312
|
+
guard _landmarks[a].visibility > visThreshold,
|
|
313
|
+
_landmarks[b].visibility > visThreshold,
|
|
314
|
+
_landmarks[c].visibility > visThreshold else { continue }
|
|
311
315
|
|
|
316
|
+
let angle = calculateAngle(
|
|
317
|
+
pointA: _landmarks[a],
|
|
318
|
+
vertex: _landmarks[b],
|
|
319
|
+
pointC: _landmarks[c]
|
|
320
|
+
)
|
|
312
321
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
pointC: _landmarks[c]
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
currentAngles[angleDef.name] = angle
|
|
320
|
-
angleSnapshots.append(AngleSnapshot(name: angleDef.name, value: angle))
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
repAngleSnapshots = angleSnapshots
|
|
322
|
+
currentAngles[angleDef.name] = angle
|
|
323
|
+
angleSnapshots.append(AngleSnapshot(name: angleDef.name, value: angle))
|
|
324
|
+
}
|
|
324
325
|
|
|
325
|
-
|
|
326
|
-
// for (name, angle) in currentAngles {
|
|
327
|
-
// print("[PoseExercise] Angle \(name): \(String(format: "%.1f", angle))°")
|
|
328
|
-
// }
|
|
326
|
+
repAngleSnapshots = angleSnapshots
|
|
329
327
|
|
|
330
|
-
|
|
328
|
+
let detectedPhase = determinePhase(from: currentAngles, config: config)
|
|
331
329
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
337
|
+
checkFormRules(currentAngles: currentAngles, config: config)
|
|
340
338
|
|
|
341
|
-
|
|
342
|
-
|
|
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(
|
|
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]
|
|
@@ -369,41 +367,28 @@ private func isPostureValid(_ family: String) -> Bool {
|
|
|
369
367
|
let ankleY = anklesVisible ? (la.y + ra.y) / 2 : kneeY
|
|
370
368
|
|
|
371
369
|
switch family {
|
|
372
|
-
case
|
|
370
|
+
case .horizontalprone, .supine:
|
|
373
371
|
let ys = [shoulderY, hipY, ankleY]
|
|
374
|
-
|
|
375
|
-
return spread < 0.25
|
|
372
|
+
return ((ys.max() ?? 0) - (ys.min() ?? 0)) < 0.25
|
|
376
373
|
|
|
377
|
-
case
|
|
374
|
+
case .standingupright:
|
|
378
375
|
guard kneesVisible else { return false }
|
|
379
|
-
return shoulderY < hipY - 0.08
|
|
380
|
-
&& hipY < kneeY + 0.05
|
|
381
|
-
&& (anklesVisible ? kneeY < ankleY : true)
|
|
376
|
+
return shoulderY < hipY - 0.08 && hipY < kneeY + 0.05 && (anklesVisible ? kneeY < ankleY : true)
|
|
382
377
|
|
|
383
|
-
case
|
|
378
|
+
case .seated:
|
|
384
379
|
guard kneesVisible else { return false }
|
|
385
|
-
return shoulderY < hipY - 0.05
|
|
386
|
-
&& abs(hipY - kneeY) < 0.20
|
|
380
|
+
return shoulderY < hipY - 0.05 && Swift.abs(hipY - kneeY) < 0.20
|
|
387
381
|
|
|
388
|
-
case
|
|
389
|
-
let
|
|
390
|
-
let
|
|
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)
|
|
382
|
+
case .sideplank:
|
|
383
|
+
let ySpread = Swift.abs(shoulderY - hipY)
|
|
384
|
+
let shoulderHipDx = Swift.abs(shoulderX - hipX)
|
|
397
385
|
return ySpread < 0.20 && shoulderHipDx < 0.15
|
|
398
386
|
|
|
399
|
-
case
|
|
387
|
+
case .inverted:
|
|
400
388
|
guard anklesVisible else { return false }
|
|
401
389
|
return hipY < shoulderY && hipY < ankleY
|
|
402
390
|
|
|
403
|
-
case
|
|
404
|
-
return true
|
|
405
|
-
|
|
406
|
-
default:
|
|
391
|
+
case .none:
|
|
407
392
|
return true
|
|
408
393
|
}
|
|
409
394
|
}
|
|
@@ -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.
|
|
3
|
+
"version": "1.1.9",
|
|
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",
|