react-native-nitro-pose-exercises 1.1.12 → 1.1.14

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 (24) 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 +148 -124
  4. package/ios/NitroPoseExercises.swift +51 -15
  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 +8 -2
  12. package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.hpp +2 -1
  13. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HybridNitroPoseExercisesSpec.kt +6 -1
  14. package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Umbrella.hpp +1 -0
  15. package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.hpp +12 -2
  16. package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec.swift +2 -1
  17. package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec_cxx.swift +13 -2
  18. package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.cpp +2 -1
  19. package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.hpp +3 -1
  20. package/package.json +25 -4
  21. package/src/NitroPoseExercises.nitro.ts +10 -2
  22. package/src/config/pushup.ts +19 -43
  23. package/android/src/main/cpp/frame_helper.cpp +0 -37
  24. 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,29 @@
1
1
  package com.margelo.nitro.nitroposeexercises
2
2
 
3
- import android.graphics.Bitmap
4
3
  import android.graphics.Matrix
5
- import android.media.Image
6
4
  import androidx.annotation.Keep
7
5
  import com.facebook.proguard.annotations.DoNotStrip
8
- import com.google.mlkit.vision.common.InputImage
9
6
  import com.google.mlkit.vision.pose.PoseDetection
10
7
  import com.google.mlkit.vision.pose.PoseDetector
11
8
  import com.google.mlkit.vision.pose.PoseLandmark
12
9
  import com.google.mlkit.vision.pose.defaults.PoseDetectorOptions
13
10
  import com.margelo.nitro.NitroModules
14
11
  import com.margelo.nitro.core.Promise
15
- import com.margelo.nitro.camera.HybridFrameSpec
16
12
  import kotlin.math.acos
17
13
  import kotlin.math.max
18
14
  import kotlin.math.min
19
15
  import kotlin.math.sqrt
20
16
 
17
+ import android.media.Image
18
+ import android.graphics.Bitmap
19
+ import com.google.mlkit.vision.common.InputImage
20
+ import com.margelo.nitro.core.ArrayBuffer
21
+ import java.nio.ByteBuffer
22
+
23
+ import java.nio.ByteOrder
24
+
25
+ import com.margelo.nitro.camera.HybridFrameSpec
26
+
21
27
  @Keep
22
28
  @DoNotStrip
23
29
  class NitroPoseExercises : HybridNitroPoseExercisesSpec() {
@@ -205,88 +211,97 @@ override fun isReady(): Boolean {
205
211
  // Frame Processing (ML Kit — async with cached results)
206
212
  // ═══════════════════════════════════════════════════════════
207
213
 
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
214
 
263
- bitmap.recycle()
264
- }
265
- .addOnFailureListener { e ->
266
- println("[PoseExercise] ML Kit error: ${e.message}")
267
- bitmap.recycle()
268
- }
269
215
 
270
- // Use cached landmarks from previous frame
271
- val currentLandmarks: Array<Landmark>
272
- synchronized(landmarkLock) {
273
- currentLandmarks = cachedLandmarks.copyOf()
274
- }
216
+ override fun processFrameAndroid(buffer: ArrayBuffer, width: Double, height: Double, rotation: Double) {
217
+ if (_status != SessionStatus.ACTIVE && _status != SessionStatus.COUNTDOWN) return
218
+ if (!isInitialized || poseDetector == null) return
275
219
 
276
- _landmarks = currentLandmarks
220
+ frameCount++
221
+ if (frameCount % processEveryNFrames != 0) return
277
222
 
278
- if (currentLandmarks.isNotEmpty() && _status == SessionStatus.ACTIVE) {
279
- processExerciseLogic()
280
- }
223
+ val w = width.toInt()
224
+ val h = height.toInt()
225
+ val rot = rotation.toInt()
226
+
227
+ try {
228
+ val rgb = buffer.getBuffer(false)
229
+ rgb.rewind()
281
230
 
282
- } catch (e: Exception) {
283
- println("[PoseExercise] Frame processing error: ${e.message}")
231
+ // RGB (3 bytes) -> ARGB_8888 (4 bytes, A=0xFF). Bitmap.Config.ARGB_8888 stores as RGBA in memory.
232
+ val pixelCount = w * h
233
+ val rgba = ByteBuffer.allocateDirect(pixelCount * 4).order(ByteOrder.nativeOrder())
234
+ for (i in 0 until pixelCount) {
235
+ val r = rgb.get()
236
+ val g = rgb.get()
237
+ val b = rgb.get()
238
+ rgba.put(r)
239
+ rgba.put(g)
240
+ rgba.put(b)
241
+ rgba.put(0xFF.toByte())
284
242
  }
243
+ rgba.rewind()
244
+
245
+ val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
246
+ bitmap.copyPixelsFromBuffer(rgba)
247
+
248
+ val inputImage = InputImage.fromBitmap(bitmap, rot)
249
+
250
+ val rotated = rot == 90 || rot == 270
251
+ val imageWidth = (if (rotated) h else w).toDouble()
252
+ val imageHeight = (if (rotated) w else h).toDouble()
253
+
254
+ poseDetector!!.process(inputImage)
255
+ .addOnSuccessListener { pose ->
256
+ val poseLandmarks = pose.allPoseLandmarks
257
+
258
+ if (poseLandmarks.isNotEmpty()) {
259
+ if (poseWasLost) {
260
+ poseWasLost = false
261
+ onPoseRegained?.invoke()
262
+ }
263
+
264
+ val landmarkArray = Array(34) { Landmark(x = 0.0, y = 0.0, z = 0.0, visibility = 0.0) }
265
+
266
+ for (poseLandmark in poseLandmarks) {
267
+ val mediaPipeIndex = mlKitToMediaPipeMap[poseLandmark.landmarkType] ?: continue
268
+ if (mediaPipeIndex >= 34) continue
269
+
270
+ landmarkArray[mediaPipeIndex] = Landmark(
271
+ x = (poseLandmark.position3D.x / imageWidth).coerceIn(0.0, 1.0),
272
+ y = (poseLandmark.position3D.y / imageHeight).coerceIn(0.0, 1.0),
273
+ z = poseLandmark.position3D.z.toDouble(),
274
+ visibility = poseLandmark.inFrameLikelihood.toDouble()
275
+ )
276
+ }
277
+
278
+ synchronized(landmarkLock) {
279
+ cachedLandmarks = landmarkArray
280
+ _landmarks = landmarkArray
281
+ }
282
+
283
+ processExerciseLogic() // your actual method name
284
+ } else {
285
+ if (!poseWasLost) {
286
+ poseWasLost = true
287
+ onPoseLost?.invoke()
288
+ }
289
+ }
290
+ }
291
+ .addOnFailureListener { e ->
292
+ println("[PoseExercise] ML Kit failure: ${e.message}")
293
+ }
294
+ .addOnCompleteListener {
295
+ bitmap.recycle()
296
+ }
297
+ } catch (e: Exception) {
298
+ println("[PoseExercise] processFrameAndroid error: ${e.message}")
285
299
  }
300
+ }
286
301
 
287
- // ═══════════════════════════════════════════════════════════
288
- // Exercise Logic Engine
289
- // ═══════════════════════════════════════════════════════════
302
+ override fun processFrameIOS(frame: HybridFrameSpec) {
303
+ // no-op on Android
304
+ }
290
305
 
291
306
  private fun processExerciseLogic() {
292
307
  val config = exerciseConfig ?: return
@@ -616,53 +631,62 @@ if (!isPostureValid(config.postureFamily, config.visibilityThreshold)) {
616
631
  // Posture Gates
617
632
  // ═══════════════════════════════════════════════════════════
618
633
 
619
- private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
620
- if (_landmarks.size < 33) return false
634
+ private fun isPostureValid(family: PostureFamily, threshold: Double): Boolean {
635
+ if (_landmarks.size < 33) return false
621
636
 
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]
637
+ val ls = _landmarks[11]; val rs = _landmarks[12]
638
+ val lh = _landmarks[23]; val rh = _landmarks[24]
639
+ val lk = _landmarks[25]; val rk = _landmarks[26]
640
+ val la = _landmarks[27]; val ra = _landmarks[28]
626
641
 
627
- val keyVisible = ls.visibility > threshold && rs.visibility > threshold &&
642
+ // Only require torso visible knees and ankles are optional
643
+ val torsoVisible = ls.visibility > threshold && rs.visibility > threshold &&
628
644
  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.HORIZONTALPRONE, PostureFamily.SUPINE -> {
643
- val ys = listOf(shoulderY, hipY, ankleY)
644
- (ys.max() - ys.min()) < 0.25
645
- }
646
- PostureFamily.STANDINGUPRIGHT -> {
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.SIDEPLANK -> {
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
- }
645
+ if (!torsoVisible) return false
646
+
647
+ val shoulderY = (ls.y + rs.y) / 2
648
+ val hipY = (lh.y + rh.y) / 2
649
+ val shoulderX = (ls.x + rs.x) / 2
650
+ val hipX = (lh.x + rh.x) / 2
651
+
652
+ val kneesVisible = lk.visibility > threshold && rk.visibility > threshold
653
+ val anklesVisible = la.visibility > threshold && ra.visibility > threshold
654
+ val kneeY = if (kneesVisible) (lk.y + rk.y) / 2 else hipY
655
+ val ankleY = if (anklesVisible) (la.y + ra.y) / 2 else kneeY
656
+
657
+ return when (family) {
658
+ PostureFamily.HORIZONTALPRONE, PostureFamily.SUPINE -> {
659
+ val ys = if (anklesVisible)
660
+ listOf(shoulderY, hipY, ankleY)
661
+ else
662
+ listOf(shoulderY, hipY)
663
+ (ys.max() - ys.min()) < 0.25
664
+ }
665
+ PostureFamily.STANDINGUPRIGHT -> {
666
+ if (kneesVisible) {
667
+ shoulderY < hipY - 0.08 &&
668
+ hipY < kneeY + 0.05 &&
669
+ (if (anklesVisible) kneeY < ankleY else true)
670
+ } else {
671
+ shoulderY < hipY - 0.08
672
+ }
673
+ }
674
+ PostureFamily.SEATED -> {
675
+ if (kneesVisible) {
676
+ shoulderY < hipY - 0.05 && kotlin.math.abs(hipY - kneeY) < 0.20
677
+ } else {
678
+ shoulderY < hipY - 0.05
679
+ }
680
+ }
681
+ PostureFamily.SIDEPLANK -> {
682
+ val ySpread = kotlin.math.abs(shoulderY - hipY)
683
+ val shoulderHipDx = kotlin.math.abs(shoulderX - hipX)
684
+ ySpread < 0.20 && shoulderHipDx < 0.15
685
+ }
686
+ PostureFamily.INVERTED -> {
687
+ if (!anklesVisible) false
688
+ else hipY < shoulderY && hipY < ankleY
689
+ }
690
+ PostureFamily.NONE -> true
667
691
  }
668
- }
692
+ }}
@@ -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,9 @@ 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
+ // no-op on iOS
266
+ }
266
267
  // ═══════════════════════════════════════════════════════════
267
268
  // MARK: - Exercise Logic Engine
268
269
  // ═══════════════════════════════════════════════════════════
@@ -345,7 +346,7 @@ private func processExerciseLogic() {
345
346
  // MARK: - Posture Gates
346
347
  // ═══════════════════════════════════════════════════════════
347
348
 
348
- private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
349
+ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
349
350
  guard _landmarks.count >= 33 else { return false }
350
351
 
351
352
  let ls = _landmarks[11], rs = _landmarks[12]
@@ -353,38 +354,73 @@ private func isPostureValid(family: PostureFamily, threshold: Double) -> Bool {
353
354
  let lk = _landmarks[25], rk = _landmarks[26]
354
355
  let la = _landmarks[27], ra = _landmarks[28]
355
356
 
356
- let key = [ls, rs, lh, rh]
357
- guard key.allSatisfy({ $0.visibility > 0.3 }) else { return false }
357
+ // Only require torso visible knees and ankles are optional
358
+ let torsoVisible = ls.visibility > threshold && rs.visibility > threshold
359
+ && lh.visibility > threshold && rh.visibility > threshold
360
+ guard torsoVisible else { return false }
358
361
 
359
362
  let shoulderY = (ls.y + rs.y) / 2
360
363
  let hipY = (lh.y + rh.y) / 2
361
364
  let shoulderX = (ls.x + rs.x) / 2
362
365
  let hipX = (lh.x + rh.x) / 2
363
366
 
364
- let kneesVisible = lk.visibility > 0.3 && rk.visibility > 0.3
365
- let anklesVisible = la.visibility > 0.3 && ra.visibility > 0.3
367
+ let kneesVisible = lk.visibility > threshold && rk.visibility > threshold
368
+ let anklesVisible = la.visibility > threshold && ra.visibility > threshold
366
369
  let kneeY = kneesVisible ? (lk.y + rk.y) / 2 : hipY
367
370
  let ankleY = anklesVisible ? (la.y + ra.y) / 2 : kneeY
368
371
 
369
372
  switch family {
370
- case .horizontalprone, .supine:
371
- let ys = [shoulderY, hipY, ankleY]
372
- return ((ys.max() ?? 0) - (ys.min() ?? 0)) < 0.25
373
+ case .horizontalprone, .supine:
374
+ // Case A: side view — shoulders, hips, ankles in horizontal band
375
+ let ys: [Double] = anklesVisible
376
+ ? [shoulderY, hipY, ankleY]
377
+ : [shoulderY, hipY]
378
+ let ySpread = (ys.max() ?? 0) - (ys.min() ?? 0)
379
+ if ySpread < 0.25 {
380
+ return true
381
+ }
382
+
383
+ // Case B: front-facing — body extends away from camera along Z.
384
+ // Y-spread is large (head close, feet far), so check upper-body geometry.
385
+ let le = _landmarks[13], re = _landmarks[14]
386
+ let lw = _landmarks[15], rw = _landmarks[16]
387
+ let upperBodyVisible = le.visibility > threshold && re.visibility > threshold
388
+ && lw.visibility > threshold && rw.visibility > threshold
389
+ guard upperBodyVisible else { return false }
390
+
391
+ let wristY = (lw.y + rw.y) / 2
392
+ let shoulderWidth = abs(ls.x - rs.x)
393
+ guard wristY > shoulderY + 0.03 else { return false }
394
+ guard shoulderWidth > 0.10 else { return false }
395
+
396
+ return true
373
397
 
374
398
  case .standingupright:
375
- guard kneesVisible else { return false }
376
- return shoulderY < hipY - 0.08 && hipY < kneeY + 0.05 && (anklesVisible ? kneeY < ankleY : true)
399
+ // Torso vertical (shoulders above hips). If knees visible, check chain.
400
+ if kneesVisible {
401
+ return shoulderY < hipY - 0.08
402
+ && hipY < kneeY + 0.05
403
+ && (anklesVisible ? kneeY < ankleY : true)
404
+ } else {
405
+ return shoulderY < hipY - 0.08
406
+ }
377
407
 
378
408
  case .seated:
379
- guard kneesVisible else { return false }
380
- return shoulderY < hipY - 0.05 && Swift.abs(hipY - kneeY) < 0.20
409
+ // Forward-leaning torso. If knees visible, also check hip-knee distance.
410
+ if kneesVisible {
411
+ return shoulderY < hipY - 0.05 && Swift.abs(hipY - kneeY) < 0.20
412
+ } else {
413
+ return shoulderY < hipY - 0.05
414
+ }
381
415
 
382
416
  case .sideplank:
417
+ // Body horizontal, shoulders and hips stacked in x
383
418
  let ySpread = Swift.abs(shoulderY - hipY)
384
419
  let shoulderHipDx = Swift.abs(shoulderX - hipX)
385
420
  return ySpread < 0.20 && shoulderHipDx < 0.15
386
421
 
387
422
  case .inverted:
423
+ // Genuinely needs ankles — hips highest of all three
388
424
  guard anklesVisible else { return false }
389
425
  return hipY < shoulderY && hipY < ankleY
390
426
 
@@ -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;;AAoDA,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(buffer: ArrayBuffer, width: number, height: number, rotation: number): 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"}
@@ -91,6 +91,8 @@ namespace margelo::nitro::camera { class HybridFrameSpec; }
91
91
  #include <memory>
92
92
  #include <VisionCamera/HybridFrameSpec.hpp>
93
93
  #include <VisionCamera/JHybridFrameSpec.hpp>
94
+ #include <NitroModules/ArrayBuffer.hpp>
95
+ #include <NitroModules/JArrayBuffer.hpp>
94
96
 
95
97
  namespace margelo::nitro::nitroposeexercises {
96
98
 
@@ -329,10 +331,14 @@ namespace margelo::nitro::nitroposeexercises {
329
331
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JExerciseConfig> /* config */)>("loadExercise");
330
332
  method(_javaPart, JExerciseConfig::fromCpp(config));
331
333
  }
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");
334
+ void JHybridNitroPoseExercisesSpec::processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) {
335
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<margelo::nitro::camera::JHybridFrameSpec::JavaPart> /* frame */)>("processFrameIOS");
334
336
  method(_javaPart, std::dynamic_pointer_cast<margelo::nitro::camera::JHybridFrameSpec>(frame)->getJavaPart());
335
337
  }
338
+ void JHybridNitroPoseExercisesSpec::processFrameAndroid(const std::shared_ptr<ArrayBuffer>& buffer, double width, double height, double rotation) {
339
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JArrayBuffer::javaobject> /* buffer */, double /* width */, double /* height */, double /* rotation */)>("processFrameAndroid");
340
+ method(_javaPart, JArrayBuffer::wrap(buffer), width, height, rotation);
341
+ }
336
342
  void JHybridNitroPoseExercisesSpec::startSession(double targetReps, double countdownSeconds) {
337
343
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(double /* targetReps */, double /* countdownSeconds */)>("startSession");
338
344
  method(_javaPart, targetReps, 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<ArrayBuffer>& buffer, double width, double height, double rotation) override;
82
83
  void startSession(double targetReps, double countdownSeconds) override;
83
84
  void pauseSession() override;
84
85
  void resumeSession() override;
@@ -12,6 +12,7 @@ import com.facebook.jni.HybridData
12
12
  import com.facebook.proguard.annotations.DoNotStrip
13
13
  import com.margelo.nitro.core.Promise
14
14
  import com.margelo.nitro.camera.HybridFrameSpec
15
+ import com.margelo.nitro.core.ArrayBuffer
15
16
  import com.margelo.nitro.core.HybridObject
16
17
 
17
18
  /**
@@ -184,7 +185,11 @@ abstract class HybridNitroPoseExercisesSpec: HybridObject() {
184
185
 
185
186
  @DoNotStrip
186
187
  @Keep
187
- abstract fun processFrame(frame: com.margelo.nitro.camera.HybridFrameSpec): Unit
188
+ abstract fun processFrameIOS(frame: com.margelo.nitro.camera.HybridFrameSpec): Unit
189
+
190
+ @DoNotStrip
191
+ @Keep
192
+ abstract fun processFrameAndroid(buffer: ArrayBuffer, width: Double, height: Double, rotation: Double): Unit
188
193
 
189
194
  @DoNotStrip
190
195
  @Keep
@@ -63,6 +63,7 @@ namespace margelo::nitro::nitroposeexercises { enum class SessionStatus; }
63
63
  #include "RepData.hpp"
64
64
  #include "SessionResult.hpp"
65
65
  #include "SessionStatus.hpp"
66
+ #include <NitroModules/ArrayBuffer.hpp>
66
67
  #include <NitroModules/Promise.hpp>
67
68
  #include <NitroModules/Result.hpp>
68
69
  #include <VisionCamera/HybridFrameSpec.hpp>
@@ -46,6 +46,8 @@ namespace margelo::nitro::nitroposeexercises { enum class PostureFamily; }
46
46
  namespace margelo::nitro::nitroposeexercises { enum class CameraAngleType; }
47
47
  // Forward declaration of `HybridFrameSpec` to properly resolve imports.
48
48
  namespace margelo::nitro::camera { class HybridFrameSpec; }
49
+ // Forward declaration of `ArrayBufferHolder` to properly resolve imports.
50
+ namespace NitroModules { class ArrayBufferHolder; }
49
51
 
50
52
  #include "SessionStatus.hpp"
51
53
  #include "RepData.hpp"
@@ -70,6 +72,8 @@ namespace margelo::nitro::camera { class HybridFrameSpec; }
70
72
  #include "CameraAngleType.hpp"
71
73
  #include <memory>
72
74
  #include <VisionCamera/HybridFrameSpec.hpp>
75
+ #include <NitroModules/ArrayBuffer.hpp>
76
+ #include <NitroModules/ArrayBufferHolder.hpp>
73
77
 
74
78
  #include "NitroPoseExercises-Swift-Cxx-Umbrella.hpp"
75
79
 
@@ -218,8 +222,14 @@ namespace margelo::nitro::nitroposeexercises {
218
222
  std::rethrow_exception(__result.error());
219
223
  }
220
224
  }
221
- inline void processFrame(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override {
222
- auto __result = _swiftPart.processFrame(frame);
225
+ inline void processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) override {
226
+ auto __result = _swiftPart.processFrameIOS(frame);
227
+ if (__result.hasError()) [[unlikely]] {
228
+ std::rethrow_exception(__result.error());
229
+ }
230
+ }
231
+ inline void processFrameAndroid(const std::shared_ptr<ArrayBuffer>& buffer, double width, double height, double rotation) override {
232
+ auto __result = _swiftPart.processFrameAndroid(ArrayBufferHolder(buffer), std::forward<decltype(width)>(width), std::forward<decltype(height)>(height), std::forward<decltype(rotation)>(rotation));
223
233
  if (__result.hasError()) [[unlikely]] {
224
234
  std::rethrow_exception(__result.error());
225
235
  }
@@ -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(buffer: ArrayBuffer, width: Double, height: Double, rotation: Double) 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,9 @@ 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
493
  let __unsafePointer = bridge.get_std__shared_ptr_margelo__nitro__camera__HybridFrameSpec_(frame)
494
494
  let __instance = HybridFrameSpec_cxx.fromUnsafe(__unsafePointer)
495
495
  return __instance.getHybridFrameSpec()
@@ -501,6 +501,17 @@ open class HybridNitroPoseExercisesSpec_cxx {
501
501
  }
502
502
  }
503
503
 
504
+ @inline(__always)
505
+ public final func processFrameAndroid(buffer: ArrayBuffer, width: Double, height: Double, rotation: Double) -> bridge.Result_void_ {
506
+ do {
507
+ try self.__implementation.processFrameAndroid(buffer: buffer, width: width, height: height, rotation: rotation)
508
+ return bridge.create_Result_void_()
509
+ } catch (let __error) {
510
+ let __exceptionPtr = __error.toCpp()
511
+ return bridge.create_Result_void_(__exceptionPtr)
512
+ }
513
+ }
514
+
504
515
  @inline(__always)
505
516
  public final func startSession(targetReps: Double, countdownSeconds: Double) -> bridge.Result_void_ {
506
517
  do {
@@ -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);
@@ -47,6 +47,7 @@ namespace margelo::nitro::camera { class HybridFrameSpec; }
47
47
  #include "ExerciseConfig.hpp"
48
48
  #include <memory>
49
49
  #include <VisionCamera/HybridFrameSpec.hpp>
50
+ #include <NitroModules/ArrayBuffer.hpp>
50
51
 
51
52
  namespace margelo::nitro::nitroposeexercises {
52
53
 
@@ -103,7 +104,8 @@ namespace margelo::nitro::nitroposeexercises {
103
104
  virtual std::shared_ptr<Promise<void>> initialize(const std::string& modelPath) = 0;
104
105
  virtual void release() = 0;
105
106
  virtual void loadExercise(const ExerciseConfig& config) = 0;
106
- virtual void processFrame(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) = 0;
107
+ virtual void processFrameIOS(const std::shared_ptr<margelo::nitro::camera::HybridFrameSpec>& frame) = 0;
108
+ virtual void processFrameAndroid(const std::shared_ptr<ArrayBuffer>& buffer, double width, double height, double rotation) = 0;
107
109
  virtual void startSession(double targetReps, double countdownSeconds) = 0;
108
110
  virtual void pauseSession() = 0;
109
111
  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.12",
3
+ "version": "1.1.14",
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",
@@ -73,6 +76,7 @@
73
76
  "@react-native/eslint-config": "0.85.3",
74
77
  "@react-native/jest-preset": "0.85.3",
75
78
  "@release-it/conventional-changelog": "^10.0.6",
79
+ "@shopify/react-native-skia": "^2.6.4",
76
80
  "@types/react": "^19.2.0",
77
81
  "commitlint": "^20.5.0",
78
82
  "del-cli": "^7.0.0",
@@ -88,8 +92,10 @@
88
92
  "react-native": "0.85.3",
89
93
  "react-native-builder-bob": "^0.41.0",
90
94
  "react-native-nitro-modules": "^0.35.9",
95
+ "react-native-reanimated": "4.4.0",
91
96
  "react-native-svg": "^15.15.5",
92
97
  "react-native-vision-camera": "^5.0.11",
98
+ "react-native-vision-camera-resizer": "^5.0.11",
93
99
  "react-native-vision-camera-worklets": "^5.0.11",
94
100
  "react-native-worklets": "^0.9.1",
95
101
  "release-it": "^19.2.4",
@@ -102,12 +108,27 @@
102
108
  "react-native-nitro-modules": "*",
103
109
  "react-native-reanimated": "*",
104
110
  "react-native-vision-camera": "*",
111
+ "react-native-vision-camera-resizer": "*",
105
112
  "react-native-vision-camera-worklets": "*",
106
113
  "react-native-worklets": "*"
107
114
  },
108
- "workspaces": [
109
- "example"
110
- ],
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
+ },
111
132
  "packageManager": "yarn@4.11.0",
112
133
  "react-native-builder-bob": {
113
134
  "source": "src",
@@ -121,8 +121,16 @@ 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: pre-resized RGBA buffer from VisionCamera Resizer (Vulkan-accelerated)
128
+ processFrameAndroid(
129
+ buffer: ArrayBuffer,
130
+ width: number,
131
+ height: number,
132
+ rotation: number
133
+ ): void;
126
134
 
127
135
  // Callbacks (set from JS side)
128
136
  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
- }