react-native-nitro-pose-exercises 1.1.13 → 1.1.15

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.
Files changed (23) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/gradle.properties +1 -1
  3. package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +146 -101
  4. package/ios/NitroPoseExercises.swift +61 -30
  5. package/lib/module/NitroPoseExercises.nitro.js.map +1 -1
  6. package/lib/module/config/pushup.js +24 -43
  7. package/lib/module/config/pushup.js.map +1 -1
  8. package/lib/typescript/src/NitroPoseExercises.nitro.d.ts +2 -1
  9. package/lib/typescript/src/NitroPoseExercises.nitro.d.ts.map +1 -1
  10. package/lib/typescript/src/config/pushup.d.ts.map +1 -1
  11. package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.cpp +6 -2
  12. package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.hpp +2 -1
  13. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HybridNitroPoseExercisesSpec.kt +5 -1
  14. package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.hpp +8 -2
  15. package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec.swift +2 -1
  16. package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec_cxx.swift +17 -2
  17. package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.cpp +2 -1
  18. package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.hpp +2 -1
  19. package/package.json +24 -4
  20. package/src/NitroPoseExercises.nitro.ts +13 -2
  21. package/src/config/pushup.ts +19 -43
  22. package/android/src/main/cpp/frame_helper.cpp +0 -37
  23. package/android/src/main/java/com/margelo/nitro/nitroposeexercises/FrameHelper.kt +0 -12
@@ -40,7 +40,7 @@ apply plugin: "com.facebook.react"
40
40
  android {
41
41
  namespace "com.margelo.nitro.nitroposeexercises"
42
42
 
43
- compileSdkVersion getExtOrDefault("compileSdkVersion")
43
+ compileSdkVersion 36
44
44
 
45
45
  defaultConfig {
46
46
  minSdkVersion 26
@@ -1,5 +1,5 @@
1
1
  NitroPoseExercises_kotlinVersion=2.0.21
2
2
  NitroPoseExercises_minSdkVersion=26
3
3
  NitroPoseExercises_targetSdkVersion=34
4
- NitroPoseExercises_compileSdkVersion=35
4
+ NitroPoseExercises_compileSdkVersion=36
5
5
  NitroPoseExercises_ndkVersion=27.1.12297006
@@ -1,23 +1,32 @@
1
1
  package com.margelo.nitro.nitroposeexercises
2
2
 
3
- import android.graphics.Bitmap
4
- import android.graphics.Matrix
5
- import android.media.Image
3
+ import com.margelo.nitro.camera.HybridFrameSpec
4
+ import com.margelo.nitro.camera.public.NativeFrame
5
+ import com.google.android.gms.tasks.Tasks
6
+ import java.util.concurrent.TimeUnit
7
+
8
+ // import android.graphics.Matrix
6
9
  import androidx.annotation.Keep
7
10
  import com.facebook.proguard.annotations.DoNotStrip
8
- import com.google.mlkit.vision.common.InputImage
9
11
  import com.google.mlkit.vision.pose.PoseDetection
10
12
  import com.google.mlkit.vision.pose.PoseDetector
11
13
  import com.google.mlkit.vision.pose.PoseLandmark
12
14
  import com.google.mlkit.vision.pose.defaults.PoseDetectorOptions
13
15
  import com.margelo.nitro.NitroModules
14
16
  import com.margelo.nitro.core.Promise
15
- import com.margelo.nitro.camera.HybridFrameSpec
16
17
  import kotlin.math.acos
17
18
  import kotlin.math.max
18
19
  import kotlin.math.min
19
20
  import kotlin.math.sqrt
20
21
 
22
+ // import android.media.Image
23
+ // import android.graphics.Bitmap
24
+ import com.google.mlkit.vision.common.InputImage
25
+ // import com.margelo.nitro.core.ArrayBuffer
26
+ // import java.nio.ByteBuffer
27
+
28
+ // import java.nio.ByteOrder
29
+
21
30
  @Keep
22
31
  @DoNotStrip
23
32
  class NitroPoseExercises : HybridNitroPoseExercisesSpec() {
@@ -205,88 +214,91 @@ override fun isReady(): Boolean {
205
214
  // Frame Processing (ML Kit — async with cached results)
206
215
  // ═══════════════════════════════════════════════════════════
207
216
 
208
- override fun processFrame(frame: HybridFrameSpec) {
209
- if (_status != SessionStatus.ACTIVE && _status != SessionStatus.COUNTDOWN) return
210
- if (!isInitialized || poseDetector == null) return
211
-
212
- frameCount++
213
- if (frameCount % processEveryNFrames != 0) return
214
-
215
- try {
216
- val nativeBuffer = frame.getNativeBuffer()
217
- val bitmap = FrameHelper.hardwareBufferToBitmap(nativeBuffer.pointer) ?: return
218
-
219
-
220
- val rotation = rotationDegreesFromFrame(frame)
221
- val inputImage = InputImage.fromBitmap(bitmap, rotation)
222
-
223
- val imageWidth = bitmap.width.toDouble()
224
- val imageHeight = bitmap.height.toDouble()
225
-
226
- poseDetector!!.process(inputImage)
227
- .addOnSuccessListener { pose ->
228
- val poseLandmarks = pose.allPoseLandmarks
229
-
230
- if (poseLandmarks.isNotEmpty()) {
231
- if (poseWasLost) {
232
- poseWasLost = false
233
- onPoseRegained?.invoke()
234
- }
235
-
236
- val landmarkArray = Array(34) { Landmark(x = 0.0, y = 0.0, z = 0.0, visibility = 0.0) }
237
-
238
- for (poseLandmark in poseLandmarks) {
239
- val mediaPipeIndex = mlKitToMediaPipeMap[poseLandmark.landmarkType] ?: continue
240
- if (mediaPipeIndex >= 34) continue
241
-
242
- landmarkArray[mediaPipeIndex] = Landmark(
243
- x = poseLandmark.position.x.toDouble() / imageWidth,
244
- y = poseLandmark.position.y.toDouble() / imageHeight,
245
- z = poseLandmark.position3D.z.toDouble(),
246
- visibility = poseLandmark.inFrameLikelihood.toDouble()
247
- )
248
- }
249
-
250
- synchronized(landmarkLock) {
251
- cachedLandmarks = landmarkArray
252
- }
253
- } else {
254
- if (!poseWasLost) {
255
- poseWasLost = true
256
- onPoseLost?.invoke()
257
- }
258
- synchronized(landmarkLock) {
259
- cachedLandmarks = emptyArray()
260
- }
261
- }
262
-
263
- bitmap.recycle()
264
- }
265
- .addOnFailureListener { e ->
266
- println("[PoseExercise] ML Kit error: ${e.message}")
267
- bitmap.recycle()
268
- }
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
269
220
 
270
- // Use cached landmarks from previous frame
271
- val currentLandmarks: Array<Landmark>
272
- synchronized(landmarkLock) {
273
- currentLandmarks = cachedLandmarks.copyOf()
221
+ // Time-based throttle more reliable than frame-count under variable FPS
222
+ @Volatile private var lastProcessTime: Long = 0L
223
+ private val minIntervalMs: Long = 66L // ~15fps; lower to 33 for ~30fps once release build is fast enough
224
+
225
+ override fun processFrameAndroid(frame: HybridFrameSpec) {
226
+ if (_status != SessionStatus.ACTIVE && _status != SessionStatus.COUNTDOWN) return
227
+ if (!isInitialized || poseDetector == null) return
228
+
229
+ val now = System.currentTimeMillis()
230
+ if (now - lastProcessTime < minIntervalMs) return
231
+ lastProcessTime = now
232
+
233
+ val nativeFrame = frame as? NativeFrame ?: return
234
+ val imageProxy = nativeFrame.image ?: return
235
+ val mediaImage = imageProxy.image ?: return
236
+
237
+ try {
238
+ val rotation = imageProxy.imageInfo.rotationDegrees
239
+ val inputImage = InputImage.fromMediaImage(mediaImage, rotation)
240
+
241
+ val rotated = rotation == 90 || rotation == 270
242
+ val imageWidth = (if (rotated) mediaImage.height else mediaImage.width).toDouble()
243
+ val imageHeight = (if (rotated) mediaImage.width else mediaImage.height).toDouble()
244
+
245
+ // SYNC inference — the frame's underlying ImageProxy is only valid until
246
+ // VisionCamera disposes the frame after this method returns. Tasks.await
247
+ // blocks the worklet thread which is exactly what we want here.
248
+ val pose = try {
249
+ Tasks.await(poseDetector!!.process(inputImage), 200, TimeUnit.MILLISECONDS)
250
+ } catch (e: Exception) {
251
+ // Timeout or failure — skip this frame quietly
252
+ null
253
+ }
254
+
255
+ if (pose == null) return
256
+
257
+ val poseLandmarks = pose.allPoseLandmarks
258
+
259
+ if (poseLandmarks.isNotEmpty()) {
260
+ if (poseWasLost) {
261
+ poseWasLost = false
262
+ onPoseRegained?.invoke()
274
263
  }
275
264
 
276
- _landmarks = currentLandmarks
265
+ val landmarkArray = Array(34) { Landmark(x = 0.0, y = 0.0, z = 0.0, visibility = 0.0) }
266
+
267
+ for (poseLandmark in poseLandmarks) {
268
+ val mediaPipeIndex = mlKitToMediaPipeMap[poseLandmark.landmarkType] ?: continue
269
+ if (mediaPipeIndex >= 34) continue
277
270
 
278
- if (currentLandmarks.isNotEmpty() && _status == SessionStatus.ACTIVE) {
279
- processExerciseLogic()
271
+ landmarkArray[mediaPipeIndex] = Landmark(
272
+ x = (poseLandmark.position3D.x / imageWidth).coerceIn(0.0, 1.0),
273
+ y = (poseLandmark.position3D.y / imageHeight).coerceIn(0.0, 1.0),
274
+ z = poseLandmark.position3D.z.toDouble(),
275
+ visibility = poseLandmark.inFrameLikelihood.toDouble()
276
+ )
280
277
  }
281
278
 
282
- } catch (e: Exception) {
283
- println("[PoseExercise] Frame processing error: ${e.message}")
279
+ synchronized(landmarkLock) {
280
+ cachedLandmarks = landmarkArray
281
+ _landmarks = landmarkArray
282
+ }
283
+
284
+ processExerciseLogic()
285
+ } else {
286
+ if (!poseWasLost) {
287
+ poseWasLost = true
288
+ onPoseLost?.invoke()
289
+ }
284
290
  }
291
+ } catch (e: Exception) {
292
+ println("[PoseExercise] processFrameAndroid error: ${e.message}")
285
293
  }
294
+ // NOTE: we do NOT close the ImageProxy ourselves.
295
+ // VisionCamera owns the frame lifecycle and will release it when
296
+ // frame.dispose() runs in the JS worklet after we return.
297
+ }
286
298
 
287
- // ═══════════════════════════════════════════════════════════
288
- // Exercise Logic Engine
289
- // ═══════════════════════════════════════════════════════════
299
+ override fun processFrameIOS(frame: HybridFrameSpec) {
300
+ // no-op on Android
301
+ }
290
302
 
291
303
  private fun processExerciseLogic() {
292
304
  val config = exerciseConfig ?: return
@@ -624,54 +636,87 @@ private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
624
636
  val lk = _landmarks[25]; val rk = _landmarks[26]
625
637
  val la = _landmarks[27]; val ra = _landmarks[28]
626
638
 
627
- // Only require torso visible knees and ankles are optional
628
- val torsoVisible = ls.visibility > threshold && rs.visibility > threshold &&
629
- lh.visibility > threshold && rh.visibility > threshold
630
- if (!torsoVisible) return false
631
-
632
- val shoulderY = (ls.y + rs.y) / 2
633
- val hipY = (lh.y + rh.y) / 2
634
- val shoulderX = (ls.x + rs.x) / 2
635
- val hipX = (lh.x + rh.x) / 2
639
+ // Shoulders mandatory; everything below is optional for close-range framing
640
+ val shouldersVisible = ls.visibility > threshold && rs.visibility > threshold
641
+ if (!shouldersVisible) return false
636
642
 
643
+ val hipsVisible = lh.visibility > threshold && rh.visibility > threshold
637
644
  val kneesVisible = lk.visibility > threshold && rk.visibility > threshold
638
645
  val anklesVisible = la.visibility > threshold && ra.visibility > threshold
646
+
647
+ val shoulderY = (ls.y + rs.y) / 2
648
+ val shoulderX = (ls.x + rs.x) / 2
649
+ val hipY = if (hipsVisible) (lh.y + rh.y) / 2 else shoulderY
650
+ val hipX = if (hipsVisible) (lh.x + rh.x) / 2 else shoulderX
639
651
  val kneeY = if (kneesVisible) (lk.y + rk.y) / 2 else hipY
640
652
  val ankleY = if (anklesVisible) (la.y + ra.y) / 2 else kneeY
641
653
 
654
+ // Mirror-invariant — works for front and back camera identically
655
+ val shoulderWidth = kotlin.math.abs(ls.x - rs.x)
656
+
642
657
  return when (family) {
643
658
  PostureFamily.HORIZONTALPRONE, PostureFamily.SUPINE -> {
659
+ if (!hipsVisible) return false
660
+
661
+ // Case A: side view — shoulders/hips/(ankles) in a horizontal band
644
662
  val ys = if (anklesVisible)
645
663
  listOf(shoulderY, hipY, ankleY)
646
664
  else
647
665
  listOf(shoulderY, hipY)
648
- (ys.max() - ys.min()) < 0.25
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
649
678
  }
679
+
650
680
  PostureFamily.STANDINGUPRIGHT -> {
651
- if (kneesVisible) {
652
- shoulderY < hipY - 0.08 &&
653
- hipY < kneeY + 0.05 &&
654
- (if (anklesVisible) kneeY < ankleY else true)
681
+ if (hipsVisible) {
682
+ if (kneesVisible) {
683
+ shoulderY < hipY - 0.05 &&
684
+ hipY < kneeY + 0.05 &&
685
+ (if (anklesVisible) kneeY < ankleY else true)
686
+ } else {
687
+ shoulderY < hipY - 0.05
688
+ }
655
689
  } else {
656
- shoulderY < hipY - 0.08
690
+ // Hips cropped (close-range front-facing) — accept based on shoulder span
691
+ shoulderWidth > 0.08
657
692
  }
658
693
  }
694
+
659
695
  PostureFamily.SEATED -> {
660
- if (kneesVisible) {
661
- shoulderY < hipY - 0.05 && kotlin.math.abs(hipY - kneeY) < 0.20
662
- } else {
663
- shoulderY < hipY - 0.05
696
+ when {
697
+ hipsVisible && kneesVisible -> shoulderY < hipY - 0.05 && kotlin.math.abs(hipY - kneeY) < 0.20
698
+ hipsVisible -> shoulderY < hipY - 0.05
699
+ else -> shoulderWidth > 0.08
664
700
  }
665
701
  }
702
+
666
703
  PostureFamily.SIDEPLANK -> {
667
- val ySpread = kotlin.math.abs(shoulderY - hipY)
668
- val shoulderHipDx = kotlin.math.abs(shoulderX - hipX)
669
- ySpread < 0.20 && shoulderHipDx < 0.15
704
+ if (!hipsVisible) false
705
+ else {
706
+ val ySpread = kotlin.math.abs(shoulderY - hipY)
707
+ val shoulderHipDx = kotlin.math.abs(shoulderX - hipX)
708
+ ySpread < 0.20 && shoulderHipDx < 0.15
709
+ }
670
710
  }
711
+
671
712
  PostureFamily.INVERTED -> {
672
- if (!anklesVisible) false
713
+ if (!hipsVisible || !anklesVisible) false
673
714
  else hipY < shoulderY && hipY < ankleY
674
715
  }
716
+
675
717
  PostureFamily.NONE -> true
676
718
  }
719
+ }
720
+
721
+
677
722
  }
@@ -178,7 +178,7 @@ private static func cgOrientation(orientation: CameraOrientation, isMirrored: Bo
178
178
  // MARK: - Frame Processing (Apple Vision)
179
179
  // ═══════════════════════════════════════════════════════════
180
180
 
181
- func processFrame(frame: any HybridFrameSpec) throws {
181
+ func processFrameIOS(frame: any HybridFrameSpec) throws {
182
182
  guard _status == .active || _status == .countdown else { return }
183
183
 
184
184
  guard isInitialized else { return }
@@ -243,8 +243,6 @@ private static func cgOrientation(orientation: CameraOrientation, isMirrored: Bo
243
243
  visibility: confidence
244
244
  )
245
245
 
246
- // Debug logging — uncomment to verify mapping
247
- // print("[PoseExercise] \(jointName.rawValue.rawValue) → index \(mediaPipeIndex): x=\(String(format: "%.3f", point.location.x)) y=\(String(format: "%.3f", 1.0 - Double(point.location.y))) conf=\(String(format: "%.2f", confidence))")
248
246
 
249
247
  } catch {
250
248
  // Joint not detected — leave as zero visibility
@@ -263,6 +261,10 @@ private static func cgOrientation(orientation: CameraOrientation, isMirrored: Bo
263
261
  }
264
262
  }
265
263
 
264
+ // func processFrameAndroid(buffer: ArrayBuffer, width: Double, height: Double, rotation: Double) {
265
+ func processFrameAndroid(frame: any HybridFrameSpec) {
266
+ // no-op on iOS
267
+ }
266
268
  // ═══════════════════════════════════════════════════════════
267
269
  // MARK: - Exercise Logic Engine
268
270
  // ═══════════════════════════════════════════════════════════
@@ -353,56 +355,85 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
353
355
  let lk = _landmarks[25], rk = _landmarks[26]
354
356
  let la = _landmarks[27], ra = _landmarks[28]
355
357
 
356
- // Only require torso visible — knees and ankles are optional
357
- let torsoVisible = ls.visibility > threshold && rs.visibility > threshold
358
- && lh.visibility > threshold && rh.visibility > threshold
359
- guard torsoVisible else { return false }
360
-
361
- let shoulderY = (ls.y + rs.y) / 2
362
- let hipY = (lh.y + rh.y) / 2
363
- let shoulderX = (ls.x + rs.x) / 2
364
- let hipX = (lh.x + rh.x) / 2
358
+ // Shoulders are mandatory; hips/knees/ankles are optional for close-range framing
359
+ let shouldersVisible = ls.visibility > threshold && rs.visibility > threshold
360
+ guard shouldersVisible else { return false }
365
361
 
362
+ let hipsVisible = lh.visibility > threshold && rh.visibility > threshold
366
363
  let kneesVisible = lk.visibility > threshold && rk.visibility > threshold
367
364
  let anklesVisible = la.visibility > threshold && ra.visibility > threshold
365
+
366
+ let shoulderY = (ls.y + rs.y) / 2
367
+ let shoulderX = (ls.x + rs.x) / 2
368
+ let hipY = hipsVisible ? (lh.y + rh.y) / 2 : shoulderY
369
+ let hipX = hipsVisible ? (lh.x + rh.x) / 2 : shoulderX
368
370
  let kneeY = kneesVisible ? (lk.y + rk.y) / 2 : hipY
369
371
  let ankleY = anklesVisible ? (la.y + ra.y) / 2 : kneeY
370
372
 
373
+ // Shoulder width is camera-mirror invariant: |x1 - x2| is the same
374
+ // whether the front camera mirror has been applied or not.
375
+ let shoulderWidth = (ls.x - rs.x).magnitude
376
+
371
377
  switch family {
372
378
  case .horizontalprone, .supine:
373
- // Horizontal body torso, plus ankles if visible
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
374
383
  let ys: [Double] = anklesVisible
375
384
  ? [shoulderY, hipY, ankleY]
376
385
  : [shoulderY, hipY]
377
- return ((ys.max() ?? 0) - (ys.min() ?? 0)) < 0.25
386
+ let ySpread = (ys.max() ?? 0) - (ys.min() ?? 0)
387
+ if ySpread < 0.25 {
388
+ return true
389
+ }
390
+
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
378
403
 
379
404
  case .standingupright:
380
- // Torso vertical (shoulders above hips). If knees visible, check chain.
381
- if kneesVisible {
382
- return shoulderY < hipY - 0.08
383
- && hipY < kneeY + 0.05
384
- && (anklesVisible ? kneeY < ankleY : true)
385
- } else {
386
- return shoulderY < hipY - 0.08
405
+ // Front-facing standing: user often crops hips/knees at close range.
406
+ // Use a tiered check based on what's visible.
407
+ if hipsVisible {
408
+ if kneesVisible {
409
+ return shoulderY < hipY - 0.05
410
+ && hipY < kneeY + 0.05
411
+ && (anklesVisible ? kneeY < ankleY : true)
412
+ }
413
+ return shoulderY < hipY - 0.05
387
414
  }
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.
417
+ return shoulderWidth > 0.08
388
418
 
389
419
  case .seated:
390
- // Forward-leaning torso. If knees visible, also check hip-knee distance.
391
- if kneesVisible {
392
- return shoulderY < hipY - 0.05 && Swift.abs(hipY - kneeY) < 0.20
393
- } else {
420
+ if hipsVisible && kneesVisible {
421
+ return shoulderY < hipY - 0.05 && (hipY - kneeY).magnitude < 0.20
422
+ }
423
+ if hipsVisible {
394
424
  return shoulderY < hipY - 0.05
395
425
  }
426
+ // Seated with hips out of frame is rare; fall back to upright shoulder span
427
+ return shoulderWidth > 0.08
396
428
 
397
429
  case .sideplank:
398
- // Body horizontal, shoulders and hips stacked in x
399
- let ySpread = Swift.abs(shoulderY - hipY)
400
- let shoulderHipDx = Swift.abs(shoulderX - hipX)
430
+ guard hipsVisible else { return false }
431
+ let ySpread = (shoulderY - hipY).magnitude
432
+ let shoulderHipDx = (shoulderX - hipX).magnitude
401
433
  return ySpread < 0.20 && shoulderHipDx < 0.15
402
434
 
403
435
  case .inverted:
404
- // Genuinely needs ankles hips highest of all three
405
- guard anklesVisible else { return false }
436
+ guard hipsVisible, anklesVisible else { return false }
406
437
  return hipY < shoulderY && hipY < ankleY
407
438
 
408
439
  case .none:
@@ -1 +1 @@
1
- {"version":3,"names":["NitroModules","nitroPoseExercises","createHybridObject"],"sourceRoot":"../../src","sources":["NitroPoseExercises.nitro.ts"],"mappings":";;AAAA,SAA4BA,YAAY,QAAQ,4BAA4B;;AAI5E;;AAqBA;;AASA;;AAsCA;;AAmCA;;AA4CA,MAAMC,kBAAkB,GACtBD,YAAY,CAACE,kBAAkB,CAAqB,cAAc,CAAC;AAErE,SAASD,kBAAkB","ignoreList":[]}
1
+ {"version":3,"names":["NitroModules","nitroPoseExercises","createHybridObject"],"sourceRoot":"../../src","sources":["NitroPoseExercises.nitro.ts"],"mappings":";;AAAA,SAA4BA,YAAY,QAAQ,4BAA4B;;AAI5E;;AAqBA;;AASA;;AAsCA;;AAmCA;;AAuDA,MAAMC,kBAAkB,GACtBD,YAAY,CAACE,kBAAkB,CAAqB,cAAc,CAAC;AAErE,SAASD,kBAAkB","ignoreList":[]}
@@ -9,67 +9,48 @@
9
9
  export const PUSHUP_CONFIG = {
10
10
  name: 'Push-Up',
11
11
  type: 'rep',
12
- postureFamily: 'horizontalProne',
13
- visibilityThreshold: 0.2,
14
- // ← add
12
+ postureFamily: 'none',
13
+ visibilityThreshold: 0.3,
15
14
  cameraAngle: 'front',
16
15
  angles: [{
17
16
  name: 'leftElbow',
18
17
  landmarkA: 11,
19
- // left shoulder
20
18
  landmarkB: 13,
21
- // left elbow (vertex)
22
- landmarkC: 15 // left wrist
19
+ landmarkC: 15
23
20
  }, {
24
21
  name: 'rightElbow',
25
22
  landmarkA: 12,
26
- // right shoulder
27
23
  landmarkB: 14,
28
- // right elbow (vertex)
29
- landmarkC: 16 // right wrist
30
- }, {
31
- name: 'leftHip',
32
- landmarkA: 11,
33
- // left shoulder
34
- landmarkB: 23,
35
- // left hip (vertex)
36
- landmarkC: 27 // left ankle
37
- }, {
38
- name: 'rightHip',
39
- landmarkA: 12,
40
- // right shoulder
41
- landmarkB: 24,
42
- // right hip (vertex)
43
- landmarkC: 28 // right ankle
24
+ landmarkC: 16
44
25
  }],
45
- phases: [{
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
+ {
46
30
  phase: 'up',
47
31
  angleName: 'leftElbow',
48
- minAngle: 150,
32
+ minAngle: 130,
49
33
  maxAngle: 180
50
34
  }, {
51
35
  phase: 'down',
52
36
  angleName: 'leftElbow',
53
- minAngle: 30,
54
- maxAngle: 90
37
+ minAngle: 40,
38
+ maxAngle: 80
55
39
  }],
56
40
  repSequence: ['up', 'down', 'up'],
57
- formRules: [{
58
- name: 'hipSag',
59
- message: 'Keep your hips up — your body should be a straight line',
60
- severity: 'warning',
61
- angleName: 'leftHip',
62
- minAngle: 160,
63
- // body should be mostly straight
64
- maxAngle: 180
65
- }, {
66
- name: 'hipPike',
67
- message: "Lower your hips you're piking up",
68
- severity: 'warning',
69
- angleName: 'leftHip',
70
- minAngle: 160,
71
- maxAngle: 180
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.
72
53
  }],
73
- holdDurationMs: 0 // not a hold exercise
54
+ holdDurationMs: 0
74
55
  };
75
56
  //# 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,iBAAiB;EAChCC,mBAAmB,EAAE,GAAG;EAAE;EAC1BC,WAAW,EAAE,OAAO;EACpBC,MAAM,EAAE,CACN;IACEL,IAAI,EAAE,WAAW;IACjBM,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE,CAAE;EACjB,CAAC,EACD;IACER,IAAI,EAAE,YAAY;IAClBM,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE,CAAE;EACjB,CAAC,EACD;IACER,IAAI,EAAE,SAAS;IACfM,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE,CAAE;EACjB,CAAC,EACD;IACER,IAAI,EAAE,UAAU;IAChBM,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE,CAAE;EACjB,CAAC,CACF;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,EAAE;IAAEC,QAAQ,EAAE;EAAG,CAAC,CACtE;EACDC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC;EACjCC,SAAS,EAAE,CACT;IACEf,IAAI,EAAE,QAAQ;IACdgB,OAAO,EAAE,yDAAyD;IAClEC,QAAQ,EAAE,SAAS;IACnBN,SAAS,EAAE,SAAS;IACpBC,QAAQ,EAAE,GAAG;IAAE;IACfC,QAAQ,EAAE;EACZ,CAAC,EACD;IACEb,IAAI,EAAE,SAAS;IACfgB,OAAO,EAAE,oCAAoC;IAC7CC,QAAQ,EAAE,SAAS;IACnBN,SAAS,EAAE,SAAS;IACpBC,QAAQ,EAAE,GAAG;IACbC,QAAQ,EAAE;EACZ,CAAC,CACF;EACDK,cAAc,EAAE,CAAC,CAAE;AACrB,CAAC","ignoreList":[]}
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":[]}
@@ -80,7 +80,8 @@ interface NitroPoseExercises extends HybridObject<{
80
80
  release(): void;
81
81
  loadExercise(config: ExerciseConfig): void;
82
82
  readonly status: SessionStatus;
83
- processFrame(frame: Frame): void;
83
+ processFrameIOS(frame: Frame): void;
84
+ processFrameAndroid(frame: Frame): void;
84
85
  onRepComplete: ((data: RepData) => void) | undefined;
85
86
  onPhaseChange: ((phase: ExercisePhase) => void) | undefined;
86
87
  onFormFeedback: ((feedback: FormFeedback) => void) | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"NitroPoseExercises.nitro.d.ts","sourceRoot":"","sources":["../../../src/NitroPoseExercises.nitro.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"NitroPoseExercises.nitro.d.ts","sourceRoot":"","sources":["../../../src/NitroPoseExercises.nitro.ts"],"names":[],"mappings":"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"}
@@ -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,cAwD3B,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,cAgC3B,CAAC"}
@@ -329,8 +329,12 @@ namespace margelo::nitro::nitroposeexercises {
329
329
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JExerciseConfig> /* config */)>("loadExercise");
330
330
  method(_javaPart, JExerciseConfig::fromCpp(config));
331
331
  }
332
- void JHybridNitroPoseExercisesSpec::processFrame(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) {
333
- static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<margelo::nitro::camera::JHybridFrameSpec::JavaPart> /* frame */)>("processFrame");
332
+ void JHybridNitroPoseExercisesSpec::processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) {
333
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<margelo::nitro::camera::JHybridFrameSpec::JavaPart> /* frame */)>("processFrameIOS");
334
+ method(_javaPart, std::dynamic_pointer_cast<margelo::nitro::camera::JHybridFrameSpec>(frame)->getJavaPart());
335
+ }
336
+ void JHybridNitroPoseExercisesSpec::processFrameAndroid(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) {
337
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<margelo::nitro::camera::JHybridFrameSpec::JavaPart> /* frame */)>("processFrameAndroid");
334
338
  method(_javaPart, std::dynamic_pointer_cast<margelo::nitro::camera::JHybridFrameSpec>(frame)->getJavaPart());
335
339
  }
336
340
  void JHybridNitroPoseExercisesSpec::startSession(double targetReps, double countdownSeconds) {
@@ -78,7 +78,8 @@ namespace margelo::nitro::nitroposeexercises {
78
78
  std::shared_ptr<Promise<void>> initialize(const std::string& modelPath) override;
79
79
  void release() override;
80
80
  void loadExercise(const ExerciseConfig& config) override;
81
- void processFrame(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override;
81
+ void processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override;
82
+ void processFrameAndroid(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override;
82
83
  void startSession(double targetReps, double countdownSeconds) override;
83
84
  void pauseSession() override;
84
85
  void resumeSession() override;
@@ -184,7 +184,11 @@ abstract class HybridNitroPoseExercisesSpec: HybridObject() {
184
184
 
185
185
  @DoNotStrip
186
186
  @Keep
187
- abstract fun processFrame(frame: com.margelo.nitro.camera.HybridFrameSpec): Unit
187
+ abstract fun processFrameIOS(frame: com.margelo.nitro.camera.HybridFrameSpec): Unit
188
+
189
+ @DoNotStrip
190
+ @Keep
191
+ abstract fun processFrameAndroid(frame: com.margelo.nitro.camera.HybridFrameSpec): Unit
188
192
 
189
193
  @DoNotStrip
190
194
  @Keep
@@ -218,8 +218,14 @@ namespace margelo::nitro::nitroposeexercises {
218
218
  std::rethrow_exception(__result.error());
219
219
  }
220
220
  }
221
- inline void processFrame(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override {
222
- auto __result = _swiftPart.processFrame(frame);
221
+ inline void processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override {
222
+ auto __result = _swiftPart.processFrameIOS(frame);
223
+ if (__result.hasError()) [[unlikely]] {
224
+ std::rethrow_exception(__result.error());
225
+ }
226
+ }
227
+ inline void processFrameAndroid(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override {
228
+ auto __result = _swiftPart.processFrameAndroid(frame);
223
229
  if (__result.hasError()) [[unlikely]] {
224
230
  std::rethrow_exception(__result.error());
225
231
  }
@@ -29,7 +29,8 @@ public protocol HybridNitroPoseExercisesSpec_protocol: HybridObject {
29
29
  func initialize(modelPath: String) throws -> Promise<Void>
30
30
  func release() throws -> Void
31
31
  func loadExercise(config: ExerciseConfig) throws -> Void
32
- func processFrame(frame: (any HybridFrameSpec)) throws -> Void
32
+ func processFrameIOS(frame: (any HybridFrameSpec)) throws -> Void
33
+ func processFrameAndroid(frame: (any HybridFrameSpec)) throws -> Void
33
34
  func startSession(targetReps: Double, countdownSeconds: Double) throws -> Void
34
35
  func pauseSession() throws -> Void
35
36
  func resumeSession() throws -> Void
@@ -487,9 +487,24 @@ open class HybridNitroPoseExercisesSpec_cxx {
487
487
  }
488
488
 
489
489
  @inline(__always)
490
- public final func processFrame(frame: bridge.std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_) -> bridge.Result_void_ {
490
+ public final func processFrameIOS(frame: bridge.std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_) -> bridge.Result_void_ {
491
491
  do {
492
- try self.__implementation.processFrame(frame: { () -> any HybridFrameSpec in
492
+ try self.__implementation.processFrameIOS(frame: { () -> any HybridFrameSpec in
493
+ let __unsafePointer = bridge.get_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(frame)
494
+ let __instance = HybridFrameSpec_cxx.fromUnsafe(__unsafePointer)
495
+ return __instance.getHybridFrameSpec()
496
+ }())
497
+ return bridge.create_Result_void_()
498
+ } catch (let __error) {
499
+ let __exceptionPtr = __error.toCpp()
500
+ return bridge.create_Result_void_(__exceptionPtr)
501
+ }
502
+ }
503
+
504
+ @inline(__always)
505
+ public final func processFrameAndroid(frame: bridge.std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_) -> bridge.Result_void_ {
506
+ do {
507
+ try self.__implementation.processFrameAndroid(frame: { () -> any HybridFrameSpec in
493
508
  let __unsafePointer = bridge.get_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(frame)
494
509
  let __instance = HybridFrameSpec_cxx.fromUnsafe(__unsafePointer)
495
510
  return __instance.getHybridFrameSpec()
@@ -39,7 +39,8 @@ namespace margelo::nitro::nitroposeexercises {
39
39
  prototype.registerHybridMethod("initialize", &HybridNitroPoseExercisesSpec::initialize);
40
40
  prototype.registerHybridMethod("release", &HybridNitroPoseExercisesSpec::release);
41
41
  prototype.registerHybridMethod("loadExercise", &HybridNitroPoseExercisesSpec::loadExercise);
42
- prototype.registerHybridMethod("processFrame", &HybridNitroPoseExercisesSpec::processFrame);
42
+ prototype.registerHybridMethod("processFrameIOS", &HybridNitroPoseExercisesSpec::processFrameIOS);
43
+ prototype.registerHybridMethod("processFrameAndroid", &HybridNitroPoseExercisesSpec::processFrameAndroid);
43
44
  prototype.registerHybridMethod("startSession", &HybridNitroPoseExercisesSpec::startSession);
44
45
  prototype.registerHybridMethod("pauseSession", &HybridNitroPoseExercisesSpec::pauseSession);
45
46
  prototype.registerHybridMethod("resumeSession", &HybridNitroPoseExercisesSpec::resumeSession);
@@ -103,7 +103,8 @@ namespace margelo::nitro::nitroposeexercises {
103
103
  virtual std::shared_ptr<Promise<void>> initialize(const std::string& modelPath) = 0;
104
104
  virtual void release() = 0;
105
105
  virtual void loadExercise(const ExerciseConfig& config) = 0;
106
- virtual void processFrame(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) = 0;
106
+ virtual void processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) = 0;
107
+ virtual void processFrameAndroid(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) = 0;
107
108
  virtual void startSession(double targetReps, double countdownSeconds) = 0;
108
109
  virtual void pauseSession() = 0;
109
110
  virtual void resumeSession() = 0;
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "react-native-nitro-pose-exercises",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
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",
7
+ "installConfig": {
8
+ "hoistingLimits": "workspaces"
9
+ },
7
10
  "exports": {
8
11
  ".": {
9
12
  "source": "./src/index.tsx",
@@ -89,8 +92,10 @@
89
92
  "react-native": "0.85.3",
90
93
  "react-native-builder-bob": "^0.41.0",
91
94
  "react-native-nitro-modules": "^0.35.9",
95
+ "react-native-reanimated": "4.4.0",
92
96
  "react-native-svg": "^15.15.5",
93
97
  "react-native-vision-camera": "^5.0.11",
98
+ "react-native-vision-camera-resizer": "^5.0.11",
94
99
  "react-native-vision-camera-worklets": "^5.0.11",
95
100
  "react-native-worklets": "^0.9.1",
96
101
  "release-it": "^19.2.4",
@@ -103,12 +108,27 @@
103
108
  "react-native-nitro-modules": "*",
104
109
  "react-native-reanimated": "*",
105
110
  "react-native-vision-camera": "*",
111
+ "react-native-vision-camera-resizer": "*",
106
112
  "react-native-vision-camera-worklets": "*",
107
113
  "react-native-worklets": "*"
108
114
  },
109
- "workspaces": [
110
- "example"
111
- ],
115
+ "workspaces": {
116
+ "packages": [
117
+ "example"
118
+ ],
119
+ "nohoist": [
120
+ "**/react-native",
121
+ "**/react-native/**",
122
+ "**/react-native-vision-camera",
123
+ "**/react-native-vision-camera/**",
124
+ "**/react-native-vision-camera-resizer",
125
+ "**/react-native-vision-camera-resizer/**",
126
+ "**/react-native-vision-camera-worklets",
127
+ "**/react-native-vision-camera-worklets/**",
128
+ "**/react-native-nitro-modules",
129
+ "**/react-native-nitro-modules/**"
130
+ ]
131
+ },
112
132
  "packageManager": "yarn@4.11.0",
113
133
  "react-native-builder-bob": {
114
134
  "source": "src",
@@ -121,8 +121,19 @@ interface NitroPoseExercises extends HybridObject<{
121
121
  // Session control
122
122
  readonly status: SessionStatus;
123
123
 
124
- // Process a VisionCamera frame — called from frame processor worklet
125
- processFrame(frame: Frame): void;
124
+ // iOS: zero-copy Vision path
125
+ processFrameIOS(frame: Frame): void;
126
+
127
+ // Android
128
+ processFrameAndroid(frame: Frame): void;
129
+
130
+ // // Android: pre-resized RGBA buffer from VisionCamera Resizer (Vulkan-accelerated)
131
+ // processFrameAndroid(
132
+ // buffer: ArrayBuffer,
133
+ // width: number,
134
+ // height: number,
135
+ // rotation: number
136
+ // ): void;
126
137
 
127
138
  // Callbacks (set from JS side)
128
139
  onRepComplete: ((data: RepData) => void) | undefined;
@@ -9,57 +9,33 @@ import type { ExerciseConfig } from '../NitroPoseExercises.nitro';
9
9
  export const PUSHUP_CONFIG: ExerciseConfig = {
10
10
  name: 'Push-Up',
11
11
  type: 'rep',
12
- postureFamily: 'horizontalProne',
13
- visibilityThreshold: 0.2, // ← add
12
+ postureFamily: 'none',
13
+ visibilityThreshold: 0.3,
14
14
  cameraAngle: 'front',
15
15
  angles: [
16
- {
17
- name: 'leftElbow',
18
- landmarkA: 11, // left shoulder
19
- landmarkB: 13, // left elbow (vertex)
20
- landmarkC: 15, // left wrist
21
- },
22
- {
23
- name: 'rightElbow',
24
- landmarkA: 12, // right shoulder
25
- landmarkB: 14, // right elbow (vertex)
26
- landmarkC: 16, // right wrist
27
- },
28
- {
29
- name: 'leftHip',
30
- landmarkA: 11, // left shoulder
31
- landmarkB: 23, // left hip (vertex)
32
- landmarkC: 27, // left ankle
33
- },
34
- {
35
- name: 'rightHip',
36
- landmarkA: 12, // right shoulder
37
- landmarkB: 24, // right hip (vertex)
38
- landmarkC: 28, // right ankle
39
- },
16
+ { name: 'leftElbow', landmarkA: 11, landmarkB: 13, landmarkC: 15 },
17
+ { name: 'rightElbow', landmarkA: 12, landmarkB: 14, landmarkC: 16 },
40
18
  ],
41
19
  phases: [
42
- { phase: 'up', angleName: 'leftElbow', minAngle: 150, maxAngle: 180 },
43
- { phase: 'down', angleName: 'leftElbow', minAngle: 30, maxAngle: 90 },
20
+ // Calibrated for 2D-projected angles in front-facing portrait filming.
21
+ // Real anatomical 180° appears as ~140-150° due to perspective foreshortening.
22
+ { phase: 'up', angleName: 'leftElbow', minAngle: 130, maxAngle: 180 },
23
+ { phase: 'down', angleName: 'leftElbow', minAngle: 40, maxAngle: 80 },
44
24
  ],
45
25
  repSequence: ['up', 'down', 'up'],
46
26
  formRules: [
27
+ // Trigger when descending but stalled above true-down threshold
47
28
  {
48
- name: 'hipSag',
49
- message: 'Keep your hips up — your body should be a straight line',
50
- severity: 'warning',
51
- angleName: 'leftHip',
52
- minAngle: 160, // body should be mostly straight
53
- maxAngle: 180,
54
- },
55
- {
56
- name: 'hipPike',
57
- message: "Lower your hips — you're piking up",
58
- severity: 'warning',
59
- angleName: 'leftHip',
60
- minAngle: 160,
61
- maxAngle: 180,
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.
62
38
  },
63
39
  ],
64
- holdDurationMs: 0, // not a hold exercise
40
+ holdDurationMs: 0,
65
41
  };
@@ -1,37 +0,0 @@
1
- #include <jni.h>
2
- #include <android/hardware_buffer_jni.h>
3
-
4
- extern "C"
5
- JNIEXPORT jobject JNICALL
6
- Java_com_margelo_nitro_nitroposeexercises_FrameHelper_hardwareBufferToBitmap(
7
- JNIEnv *env,
8
- jclass clazz,
9
- jlong pointer
10
- ) {
11
- AHardwareBuffer *buffer = reinterpret_cast<AHardwareBuffer *>(pointer);
12
- if (!buffer) return nullptr;
13
-
14
- // Convert AHardwareBuffer* to Java HardwareBuffer
15
- jobject hardwareBuffer = AHardwareBuffer_toHardwareBuffer(env, buffer);
16
- if (!hardwareBuffer) return nullptr;
17
-
18
- // Call Bitmap.wrapHardwareBuffer(hardwareBuffer, null) to create a hardware-backed Bitmap
19
- jclass bitmapClass = env->FindClass("android/graphics/Bitmap");
20
- jmethodID wrapMethod = env->GetStaticMethodID(
21
- bitmapClass,
22
- "wrapHardwareBuffer",
23
- "(Landroid/hardware/HardwareBuffer;Landroid/graphics/ColorSpace;)Landroid/graphics/Bitmap;"
24
- );
25
- jobject hwBitmap = env->CallStaticObjectMethod(bitmapClass, wrapMethod, hardwareBuffer, nullptr);
26
- if (!hwBitmap) return nullptr;
27
-
28
- // Copy to a software ARGB_8888 Bitmap (ML Kit needs software bitmap)
29
- jclass configClass = env->FindClass("android/graphics/Bitmap$Config");
30
- jfieldID argbField = env->GetStaticFieldID(configClass, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
31
- jobject argbConfig = env->GetStaticObjectField(configClass, argbField);
32
-
33
- jmethodID copyMethod = env->GetMethodID(bitmapClass, "copy", "(Landroid/graphics/Bitmap$Config;Z)Landroid/graphics/Bitmap;");
34
- jobject softBitmap = env->CallObjectMethod(hwBitmap, copyMethod, argbConfig, JNI_FALSE);
35
-
36
- return softBitmap;
37
- }
@@ -1,12 +0,0 @@
1
- package com.margelo.nitro.nitroposeexercises
2
-
3
- import android.graphics.Bitmap
4
-
5
- object FrameHelper {
6
- init {
7
- System.loadLibrary("nitroposeexercises")
8
- }
9
-
10
- @JvmStatic
11
- external fun hardwareBufferToBitmap(pointer: Long): Bitmap?
12
- }