react-native-nitro-pose-exercises 1.0.2

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 (124) hide show
  1. package/LICENSE +20 -0
  2. package/NitroPoseExercises.podspec +32 -0
  3. package/README.md +538 -0
  4. package/android/CMakeLists.txt +31 -0
  5. package/android/build.gradle +121 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +535 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercisesPackage.kt +22 -0
  10. package/ios/NitroPoseExercises.swift +527 -0
  11. package/lib/module/NitroPoseExercises.nitro.js +17 -0
  12. package/lib/module/NitroPoseExercises.nitro.js.map +1 -0
  13. package/lib/module/config/pushup.js +71 -0
  14. package/lib/module/config/pushup.js.map +1 -0
  15. package/lib/module/index.js +7 -0
  16. package/lib/module/index.js.map +1 -0
  17. package/lib/module/package.json +1 -0
  18. package/lib/typescript/package.json +1 -0
  19. package/lib/typescript/src/NitroPoseExercises.nitro.d.ts +97 -0
  20. package/lib/typescript/src/NitroPoseExercises.nitro.d.ts.map +1 -0
  21. package/lib/typescript/src/config/pushup.d.ts +3 -0
  22. package/lib/typescript/src/config/pushup.d.ts.map +1 -0
  23. package/lib/typescript/src/index.d.ts +6 -0
  24. package/lib/typescript/src/index.d.ts.map +1 -0
  25. package/nitro.json +23 -0
  26. package/nitrogen/generated/android/c++/JAngleDefinition.hpp +69 -0
  27. package/nitrogen/generated/android/c++/JAngleSnapshot.hpp +61 -0
  28. package/nitrogen/generated/android/c++/JExerciseConfig.hpp +166 -0
  29. package/nitrogen/generated/android/c++/JExercisePhase.hpp +67 -0
  30. package/nitrogen/generated/android/c++/JExerciseType.hpp +61 -0
  31. package/nitrogen/generated/android/c++/JFormFeedback.hpp +67 -0
  32. package/nitrogen/generated/android/c++/JFormRule.hpp +79 -0
  33. package/nitrogen/generated/android/c++/JFormSeverity.hpp +61 -0
  34. package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
  35. package/nitrogen/generated/android/c++/JFunc_void_ExercisePhase.hpp +77 -0
  36. package/nitrogen/generated/android/c++/JFunc_void_FormFeedback.hpp +80 -0
  37. package/nitrogen/generated/android/c++/JFunc_void_HoldProgress.hpp +77 -0
  38. package/nitrogen/generated/android/c++/JFunc_void_RepData.hpp +81 -0
  39. package/nitrogen/generated/android/c++/JFunc_void_SessionResult.hpp +85 -0
  40. package/nitrogen/generated/android/c++/JHoldProgress.hpp +65 -0
  41. package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.cpp +311 -0
  42. package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.hpp +87 -0
  43. package/nitrogen/generated/android/c++/JLandmark.hpp +69 -0
  44. package/nitrogen/generated/android/c++/JPhaseThreshold.hpp +71 -0
  45. package/nitrogen/generated/android/c++/JRepData.hpp +90 -0
  46. package/nitrogen/generated/android/c++/JSessionResult.hpp +120 -0
  47. package/nitrogen/generated/android/c++/JSessionStatus.hpp +67 -0
  48. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/AngleDefinition.kt +66 -0
  49. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/AngleSnapshot.kt +56 -0
  50. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseConfig.kt +81 -0
  51. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExercisePhase.kt +26 -0
  52. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseType.kt +24 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormFeedback.kt +61 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormRule.kt +76 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/FormSeverity.kt +24 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void.kt +80 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_ExercisePhase.kt +80 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_FormFeedback.kt +80 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_HoldProgress.kt +80 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_RepData.kt +80 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Func_void_SessionResult.kt +80 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HoldProgress.kt +61 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HybridNitroPoseExercisesSpec.kt +196 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/Landmark.kt +66 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/PhaseThreshold.kt +66 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/RepData.kt +66 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/SessionResult.kt +76 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/SessionStatus.kt +26 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/nitroposeexercisesOnLoad.kt +35 -0
  70. package/nitrogen/generated/android/nitroposeexercises+autolinking.cmake +81 -0
  71. package/nitrogen/generated/android/nitroposeexercises+autolinking.gradle +27 -0
  72. package/nitrogen/generated/android/nitroposeexercisesOnLoad.cpp +66 -0
  73. package/nitrogen/generated/android/nitroposeexercisesOnLoad.hpp +34 -0
  74. package/nitrogen/generated/ios/NitroPoseExercises+autolinking.rb +62 -0
  75. package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.cpp +100 -0
  76. package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.hpp +449 -0
  77. package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Umbrella.hpp +95 -0
  78. package/nitrogen/generated/ios/NitroPoseExercisesAutolinking.mm +33 -0
  79. package/nitrogen/generated/ios/NitroPoseExercisesAutolinking.swift +26 -0
  80. package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.cpp +11 -0
  81. package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.hpp +236 -0
  82. package/nitrogen/generated/ios/swift/AngleDefinition.swift +44 -0
  83. package/nitrogen/generated/ios/swift/AngleSnapshot.swift +34 -0
  84. package/nitrogen/generated/ios/swift/ExerciseConfig.swift +83 -0
  85. package/nitrogen/generated/ios/swift/ExercisePhase.swift +52 -0
  86. package/nitrogen/generated/ios/swift/ExerciseType.swift +44 -0
  87. package/nitrogen/generated/ios/swift/FormFeedback.swift +39 -0
  88. package/nitrogen/generated/ios/swift/FormRule.swift +54 -0
  89. package/nitrogen/generated/ios/swift/FormSeverity.swift +44 -0
  90. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  91. package/nitrogen/generated/ios/swift/Func_void_ExercisePhase.swift +46 -0
  92. package/nitrogen/generated/ios/swift/Func_void_FormFeedback.swift +46 -0
  93. package/nitrogen/generated/ios/swift/Func_void_HoldProgress.swift +46 -0
  94. package/nitrogen/generated/ios/swift/Func_void_RepData.swift +46 -0
  95. package/nitrogen/generated/ios/swift/Func_void_SessionResult.swift +46 -0
  96. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  97. package/nitrogen/generated/ios/swift/HoldProgress.swift +39 -0
  98. package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec.swift +73 -0
  99. package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec_cxx.swift +483 -0
  100. package/nitrogen/generated/ios/swift/Landmark.swift +44 -0
  101. package/nitrogen/generated/ios/swift/PhaseThreshold.swift +44 -0
  102. package/nitrogen/generated/ios/swift/RepData.swift +50 -0
  103. package/nitrogen/generated/ios/swift/SessionResult.swift +66 -0
  104. package/nitrogen/generated/ios/swift/SessionStatus.swift +52 -0
  105. package/nitrogen/generated/shared/c++/AngleDefinition.hpp +95 -0
  106. package/nitrogen/generated/shared/c++/AngleSnapshot.hpp +87 -0
  107. package/nitrogen/generated/shared/c++/ExerciseConfig.hpp +122 -0
  108. package/nitrogen/generated/shared/c++/ExercisePhase.hpp +88 -0
  109. package/nitrogen/generated/shared/c++/ExerciseType.hpp +80 -0
  110. package/nitrogen/generated/shared/c++/FormFeedback.hpp +93 -0
  111. package/nitrogen/generated/shared/c++/FormRule.hpp +105 -0
  112. package/nitrogen/generated/shared/c++/FormSeverity.hpp +80 -0
  113. package/nitrogen/generated/shared/c++/HoldProgress.hpp +91 -0
  114. package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.cpp +46 -0
  115. package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.hpp +117 -0
  116. package/nitrogen/generated/shared/c++/Landmark.hpp +95 -0
  117. package/nitrogen/generated/shared/c++/PhaseThreshold.hpp +97 -0
  118. package/nitrogen/generated/shared/c++/RepData.hpp +97 -0
  119. package/nitrogen/generated/shared/c++/SessionResult.hpp +108 -0
  120. package/nitrogen/generated/shared/c++/SessionStatus.hpp +88 -0
  121. package/package.json +187 -0
  122. package/src/NitroPoseExercises.nitro.ts +155 -0
  123. package/src/config/pushup.ts +62 -0
  124. package/src/index.tsx +28 -0
@@ -0,0 +1,527 @@
1
+ import Foundation
2
+ import NitroModules
3
+ import MediaPipeTasksVision
4
+ import VisionCamera
5
+ import AVFoundation
6
+
7
+ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
8
+
9
+ // ─── MediaPipe ──────────────────────────────────────────────
10
+ private var poseLandmarker: PoseLandmarker?
11
+ private var isInitialized = false
12
+
13
+ // ─── Exercise Config ────────────────────────────────────────
14
+ private var exerciseConfig: ExerciseConfig?
15
+
16
+ // ─── Session State ──────────────────────────────────────────
17
+ private var _status: SessionStatus = .idle
18
+ var status: SessionStatus { _status }
19
+
20
+ private var _currentPhase: ExercisePhase = .unknown
21
+ var currentPhase: ExercisePhase { _currentPhase }
22
+
23
+ private var _repCount: Double = 0
24
+ var repCount: Double { _repCount }
25
+
26
+ private var _landmarks: [Landmark] = []
27
+ var landmarks: [Landmark] { _landmarks }
28
+
29
+ // ─── State Machine ──────────────────────────────────────────
30
+ private var phaseHistory: [ExercisePhase] = []
31
+ private var repStartTime: Date = Date()
32
+ private var sessionStartTime: Date = Date()
33
+ private var targetReps: Double = 0
34
+ private var countdownSeconds: Double = 0
35
+ private var countdownTimer: Timer?
36
+
37
+ private var frameCount: Int = 0
38
+ private let processEveryNFrames: Int = 3 // Only process every 3rd frame
39
+
40
+ // ─── Form Tracking ──────────────────────────────────────────
41
+ private var lastFormFeedbackTime: [String: Date] = [:]
42
+ private var sessionFormViolations: [FormFeedback] = []
43
+ private var repFormScore: Double = 100.0
44
+ private var repAngleSnapshots: [AngleSnapshot] = []
45
+ private var allRepDurations: [Double] = []
46
+ private var allRepFormScores: [Double] = []
47
+
48
+ // ─── Pose Tracking ──────────────────────────────────────────
49
+ private var poseWasLost = false
50
+
51
+ // ─── Callbacks ──────────────────────────────────────────────
52
+ var onRepComplete: ((_ data: RepData) -> Void)?
53
+ var onPhaseChange: ((_ phase: ExercisePhase) -> Void)?
54
+ var onFormFeedback: ((_ feedback: FormFeedback) -> Void)?
55
+ var onHoldProgress: ((_ progress: HoldProgress) -> Void)?
56
+ var onPoseLost: (() -> Void)?
57
+ var onPoseRegained: (() -> Void)?
58
+ var onSessionComplete: ((_ result: SessionResult) -> Void)?
59
+
60
+ // ─── Hold Tracking ──────────────────────────────────────────
61
+ private var holdStartTime: Date?
62
+
63
+ // ═══════════════════════════════════════════════════════════
64
+ // MARK: - Lifecycle
65
+ // ═══════════════════════════════════════════════════════════
66
+
67
+ func initialize(modelPath: String) throws -> Promise<Void> {
68
+ print("[PoseExercise] initialize called with path: \(modelPath)")
69
+
70
+ return Promise.async { [weak self] in
71
+ guard let self = self else { return }
72
+
73
+ let options = PoseLandmarkerOptions()
74
+ options.baseOptions.modelAssetPath = modelPath
75
+ options.runningMode = .image
76
+ options.numPoses = 1
77
+ options.minPoseDetectionConfidence = 0.5
78
+ options.minPosePresenceConfidence = 0.5
79
+ options.minTrackingConfidence = 0.5
80
+
81
+ self.poseLandmarker = try PoseLandmarker(options: options)
82
+ self.isInitialized = true
83
+
84
+ print("[PoseExercise] MediaPipe initialized successfully")
85
+ }
86
+ }
87
+
88
+ func release() throws {
89
+ poseLandmarker = nil
90
+ isInitialized = false
91
+ _status = .idle
92
+ resetSession()
93
+ }
94
+
95
+ // ═══════════════════════════════════════════════════════════
96
+ // MARK: - Exercise Setup
97
+ // ═══════════════════════════════════════════════════════════
98
+
99
+ func loadExercise(config: ExerciseConfig) throws {
100
+ self.exerciseConfig = config
101
+ resetSession()
102
+ }
103
+
104
+ // ═══════════════════════════════════════════════════════════
105
+ // MARK: - Session Control
106
+ // ═══════════════════════════════════════════════════════════
107
+
108
+ func startSession(targetReps: Double, countdownSeconds: Double) throws {
109
+ print("[PoseExercise] startSession called - target: \(targetReps), countdown: \(countdownSeconds)")
110
+
111
+ resetSession()
112
+ self.targetReps = targetReps
113
+ self.countdownSeconds = countdownSeconds
114
+
115
+ if countdownSeconds > 0 {
116
+ _status = .countdown
117
+ startCountdown()
118
+ } else {
119
+ _status = .active
120
+ sessionStartTime = Date()
121
+ repStartTime = Date()
122
+ }
123
+ }
124
+
125
+ func pauseSession() throws {
126
+ guard _status == .active else { return }
127
+ _status = .paused
128
+ }
129
+
130
+ func resumeSession() throws {
131
+ guard _status == .paused else { return }
132
+ _status = .active
133
+ }
134
+
135
+ func stopSession() throws {
136
+ guard _status == .active || _status == .paused else { return }
137
+ completeSession()
138
+ }
139
+
140
+ // ═══════════════════════════════════════════════════════════
141
+ // MARK: - Frame Processing
142
+ // ═══════════════════════════════════════════════════════════
143
+
144
+ func processFrame(frame: any HybridFrameSpec) throws {
145
+ print("[PoseExercise] processFrame called, status: \(_status)")
146
+
147
+ frameCount += 1
148
+ if frameCount % processEveryNFrames != 0 { return }
149
+
150
+ guard _status == .active || _status == .countdown else {
151
+ print("[PoseExercise] Skipping - status is \(_status)")
152
+ return
153
+ }
154
+ guard isInitialized, let landmarker = poseLandmarker else {
155
+ print("[PoseExercise] Skipping - not initialized: \(isInitialized)")
156
+ return
157
+ }
158
+
159
+ guard let nativeFrame = frame as? any NativeFrame else {
160
+ print("[PoseExercise] Failed to cast to NativeFrame")
161
+ return
162
+ }
163
+ guard let sampleBuffer = nativeFrame.sampleBuffer else {
164
+ print("[PoseExercise] sampleBuffer is nil")
165
+ return
166
+ }
167
+
168
+ print("[PoseExercise] Got sampleBuffer, running detection...")
169
+
170
+ do {
171
+ let mpImage = try MPImage(sampleBuffer: sampleBuffer)
172
+ let result = try landmarker.detect(image: mpImage)
173
+
174
+ print("[PoseExercise] Detection done, landmarks count: \(result.landmarks.count)")
175
+
176
+ if let poseLandmarks = result.landmarks.first {
177
+ if poseWasLost {
178
+ poseWasLost = false
179
+ onPoseRegained?()
180
+ }
181
+
182
+ _landmarks = poseLandmarks.map { lm in
183
+ Landmark(
184
+ x: Double(lm.x),
185
+ y: Double(lm.y),
186
+ z: Double(lm.z),
187
+ visibility: Double(lm.visibility ?? 0)
188
+ )
189
+ }
190
+
191
+ print("[PoseExercise] Landmarks detected: \(_landmarks.count)")
192
+
193
+ if _status == .active {
194
+ processExerciseLogic()
195
+ }
196
+
197
+ } else {
198
+ print("[PoseExercise] No pose detected in frame")
199
+ if !poseWasLost {
200
+ poseWasLost = true
201
+ onPoseLost?()
202
+ }
203
+ _landmarks = []
204
+ }
205
+
206
+ } catch {
207
+ print("[PoseExercise] MediaPipe error: \(error.localizedDescription)")
208
+ }
209
+ }
210
+
211
+ // ═══════════════════════════════════════════════════════════
212
+ // MARK: - Exercise Logic Engine
213
+ // ═══════════════════════════════════════════════════════════
214
+
215
+ private func processExerciseLogic() {
216
+ guard let config = exerciseConfig else { return }
217
+ guard !_landmarks.isEmpty else { return }
218
+
219
+ // 1. Calculate all angles defined in the config
220
+ var currentAngles: [String: Double] = [:]
221
+ var angleSnapshots: [AngleSnapshot] = []
222
+
223
+ for angleDef in config.angles {
224
+ let a = Int(angleDef.landmarkA)
225
+ let b = Int(angleDef.landmarkB)
226
+ let c = Int(angleDef.landmarkC)
227
+
228
+ guard a < _landmarks.count, b < _landmarks.count, c < _landmarks.count else { continue }
229
+
230
+ let angle = calculateAngle(
231
+ pointA: _landmarks[a],
232
+ vertex: _landmarks[b],
233
+ pointC: _landmarks[c]
234
+ )
235
+
236
+ currentAngles[angleDef.name] = angle
237
+ angleSnapshots.append(AngleSnapshot(name: angleDef.name, value: angle))
238
+ }
239
+
240
+ repAngleSnapshots = angleSnapshots
241
+
242
+ // 2. Determine current phase from angle thresholds
243
+ let detectedPhase = determinePhase(from: currentAngles, config: config)
244
+
245
+ if detectedPhase != _currentPhase && detectedPhase != .unknown {
246
+ let previousPhase = _currentPhase
247
+ _currentPhase = detectedPhase
248
+ onPhaseChange?(detectedPhase)
249
+
250
+ // 3. Update phase history for rep counting
251
+ handlePhaseTransition(from: previousPhase, to: detectedPhase, config: config)
252
+ }
253
+
254
+ // 4. Check form rules
255
+ checkFormRules(currentAngles: currentAngles, config: config)
256
+
257
+ // 5. Handle hold-based exercises
258
+ if config.type == .hold {
259
+ handleHoldProgress(currentAngles: currentAngles, config: config)
260
+ }
261
+
262
+ // Temporary debug logging — remove after testing
263
+ for (name, angle) in currentAngles {
264
+ print("[PoseExercise] Angle \(name): \(String(format: "%.1f", angle))°")
265
+ }
266
+ print("[PoseExercise] Detected phase: \(detectedPhase), Current phase: \(_currentPhase)")
267
+ }
268
+
269
+ // ═══════════════════════════════════════════════════════════
270
+ // MARK: - Angle Calculation
271
+ // ═══════════════════════════════════════════════════════════
272
+
273
+ private func calculateAngle(pointA: Landmark, vertex: Landmark, pointC: Landmark) -> Double {
274
+ let vaX = pointA.x - vertex.x
275
+ let vaY = pointA.y - vertex.y
276
+ let vcX = pointC.x - vertex.x
277
+ let vcY = pointC.y - vertex.y
278
+
279
+ let dot = vaX * vcX + vaY * vcY
280
+ let magA = sqrt(vaX * vaX + vaY * vaY)
281
+ let magC = sqrt(vcX * vcX + vcY * vcY)
282
+
283
+ guard magA > 0, magC > 0 else { return 0 }
284
+
285
+ let cosAngle = max(-1.0, min(1.0, dot / (magA * magC)))
286
+ let angleRad = acos(cosAngle)
287
+ let angleDeg = angleRad * (180.0 / .pi)
288
+
289
+ return angleDeg
290
+ }
291
+
292
+ // ═══════════════════════════════════════════════════════════
293
+ // MARK: - Phase Detection
294
+ // ═══════════════════════════════════════════════════════════
295
+
296
+ private func determinePhase(from angles: [String: Double], config: ExerciseConfig) -> ExercisePhase {
297
+ for phaseThreshold in config.phases {
298
+ guard let angle = angles[phaseThreshold.angleName] else { continue }
299
+
300
+ if angle >= phaseThreshold.minAngle && angle <= phaseThreshold.maxAngle {
301
+ return phaseThreshold.phase
302
+ }
303
+ }
304
+ return .unknown
305
+ }
306
+
307
+ // ═══════════════════════════════════════════════════════════
308
+ // MARK: - Rep Counting State Machine
309
+ // ═══════════════════════════════════════════════════════════
310
+
311
+ private func handlePhaseTransition(from previousPhase: ExercisePhase, to newPhase: ExercisePhase, config: ExerciseConfig) {
312
+ guard config.type == .rep else { return }
313
+
314
+ phaseHistory.append(newPhase)
315
+
316
+ let repSeq = config.repSequence
317
+ if phaseHistory.count >= repSeq.count {
318
+ let tail = Array(phaseHistory.suffix(repSeq.count))
319
+
320
+ if tail == repSeq {
321
+ let now = Date()
322
+ let repDuration = now.timeIntervalSince(repStartTime) * 1000
323
+
324
+ // Minimum 800ms per rep — prevents false counts from noise
325
+ guard repDuration > 800 else {
326
+ phaseHistory = [newPhase]
327
+ return
328
+ }
329
+
330
+ // Don't count rep if form is terrible
331
+ guard repFormScore > 30 else {
332
+ onFormFeedback?(FormFeedback(
333
+ ruleName: "poorForm",
334
+ message: "Fix your form before continuing",
335
+ severity: .error
336
+ ))
337
+ repFormScore = 100.0
338
+ phaseHistory = [newPhase]
339
+ return
340
+ }
341
+
342
+ _repCount += 1
343
+
344
+ let repData = RepData(
345
+ repNumber: _repCount,
346
+ durationMs: repDuration,
347
+ formScore: repFormScore,
348
+ angles: repAngleSnapshots
349
+ )
350
+
351
+ allRepDurations.append(repDuration)
352
+ allRepFormScores.append(repFormScore)
353
+
354
+ onRepComplete?(repData)
355
+
356
+ repStartTime = now
357
+ repFormScore = 100.0
358
+ phaseHistory = [newPhase]
359
+
360
+ if targetReps > 0 && _repCount >= targetReps {
361
+ completeSession()
362
+ }
363
+ }
364
+ }
365
+
366
+ let maxHistory = config.repSequence.count * 2
367
+ if phaseHistory.count > maxHistory {
368
+ phaseHistory = Array(phaseHistory.suffix(maxHistory))
369
+ }
370
+ }
371
+
372
+ // ═══════════════════════════════════════════════════════════
373
+ // MARK: - Form Validation
374
+ // ═══════════════════════════════════════════════════════════
375
+
376
+ private func checkFormRules(currentAngles: [String: Double], config: ExerciseConfig) {
377
+ let now = Date()
378
+ let throttleInterval: TimeInterval = 3.0
379
+
380
+ for rule in config.formRules {
381
+ guard let angle = currentAngles[rule.angleName] else { continue }
382
+
383
+ let isViolating = angle < rule.minAngle || angle > rule.maxAngle
384
+
385
+ if isViolating {
386
+ if let lastTime = lastFormFeedbackTime[rule.name],
387
+ now.timeIntervalSince(lastTime) < throttleInterval {
388
+ continue
389
+ }
390
+
391
+ let feedback = FormFeedback(
392
+ ruleName: rule.name,
393
+ message: rule.message,
394
+ severity: rule.severity
395
+ )
396
+
397
+ switch rule.severity {
398
+ case .warning:
399
+ repFormScore = max(0, repFormScore - 5)
400
+ case .error:
401
+ repFormScore = max(0, repFormScore - 15)
402
+ case .info:
403
+ break
404
+ }
405
+
406
+ sessionFormViolations.append(feedback)
407
+ lastFormFeedbackTime[rule.name] = now
408
+ onFormFeedback?(feedback)
409
+ }
410
+ }
411
+ }
412
+
413
+ // ═══════════════════════════════════════════════════════════
414
+ // MARK: - Hold Progress
415
+ // ═══════════════════════════════════════════════════════════
416
+
417
+ private func handleHoldProgress(currentAngles: [String: Double], config: ExerciseConfig) {
418
+ guard config.holdDurationMs > 0 else { return }
419
+
420
+ var inPosition = true
421
+ for phaseThreshold in config.phases {
422
+ guard let angle = currentAngles[phaseThreshold.angleName] else {
423
+ inPosition = false
424
+ break
425
+ }
426
+ if angle < phaseThreshold.minAngle || angle > phaseThreshold.maxAngle {
427
+ inPosition = false
428
+ break
429
+ }
430
+ }
431
+
432
+ if inPosition {
433
+ if holdStartTime == nil {
434
+ holdStartTime = Date()
435
+ }
436
+
437
+ let elapsed = Date().timeIntervalSince(holdStartTime!) * 1000
438
+ let stability = min(100.0, max(0.0, repFormScore))
439
+
440
+ let progress = HoldProgress(
441
+ elapsedMs: elapsed,
442
+ targetMs: config.holdDurationMs,
443
+ stability: stability
444
+ )
445
+
446
+ onHoldProgress?(progress)
447
+
448
+ if elapsed >= config.holdDurationMs {
449
+ completeSession()
450
+ }
451
+ } else {
452
+ holdStartTime = nil
453
+ }
454
+ }
455
+
456
+ // ═══════════════════════════════════════════════════════════
457
+ // MARK: - Session Completion
458
+ // ═══════════════════════════════════════════════════════════
459
+
460
+ private func completeSession() {
461
+ _status = .completed
462
+
463
+ let totalDuration = Date().timeIntervalSince(sessionStartTime) * 1000
464
+ let avgRepDuration = allRepDurations.isEmpty ? 0 : allRepDurations.reduce(0, +) / Double(allRepDurations.count)
465
+ let avgFormScore = allRepFormScores.isEmpty ? 100.0 : allRepFormScores.reduce(0, +) / Double(allRepFormScores.count)
466
+
467
+ let result = SessionResult(
468
+ totalReps: _repCount,
469
+ totalDurationMs: totalDuration,
470
+ averageRepDurationMs: avgRepDuration,
471
+ averageFormScore: avgFormScore,
472
+ formViolations: sessionFormViolations,
473
+ angleHistory: repAngleSnapshots
474
+ )
475
+
476
+ onSessionComplete?(result)
477
+ }
478
+
479
+ // ═══════════════════════════════════════════════════════════
480
+ // MARK: - Countdown
481
+ // ═══════════════════════════════════════════════════════════
482
+
483
+ private func startCountdown() {
484
+ var remaining = countdownSeconds
485
+
486
+ countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
487
+ guard let self = self else {
488
+ timer.invalidate()
489
+ return
490
+ }
491
+
492
+ remaining -= 1
493
+
494
+ if remaining <= 0 {
495
+ timer.invalidate()
496
+ self.countdownTimer = nil
497
+ self._status = .active
498
+ self.sessionStartTime = Date()
499
+ self.repStartTime = Date()
500
+ }
501
+ }
502
+ }
503
+
504
+ // ═══════════════════════════════════════════════════════════
505
+ // MARK: - Reset
506
+ // ═══════════════════════════════════════════════════════════
507
+
508
+ private func resetSession() {
509
+ _status = .idle
510
+ _currentPhase = .unknown
511
+ _repCount = 0
512
+ _landmarks = []
513
+ phaseHistory = []
514
+ repFormScore = 100.0
515
+ repAngleSnapshots = []
516
+ allRepDurations = []
517
+ allRepFormScores = []
518
+ sessionFormViolations = []
519
+ lastFormFeedbackTime = [:]
520
+ holdStartTime = nil
521
+ poseWasLost = false
522
+ targetReps = 0
523
+ countdownSeconds = 0
524
+ countdownTimer?.invalidate()
525
+ countdownTimer = nil
526
+ }
527
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ import { NitroModules } from 'react-native-nitro-modules';
4
+
5
+ // ─── Enums & Types ───────────────────────────────────────────
6
+
7
+ // ─── Landmark ────────────────────────────────────────────────
8
+
9
+ // ─── Exercise Config (passed from JS) ────────────────────────
10
+
11
+ // ─── Callback Payloads ───────────────────────────────────────
12
+
13
+ // ─── The HybridObject ────────────────────────────────────────
14
+
15
+ const nitroPoseExercises = NitroModules.createHybridObject('PoseExercise');
16
+ export { nitroPoseExercises };
17
+ //# sourceMappingURL=NitroPoseExercises.nitro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["NitroModules","nitroPoseExercises","createHybridObject"],"sourceRoot":"../../src","sources":["NitroPoseExercises.nitro.ts"],"mappings":";;AAAA,SAA4BA,YAAY,QAAQ,4BAA4B;;AAI5E;;AAUA;;AASA;;AAmCA;;AAmCA;;AAwCA,MAAMC,kBAAkB,GACtBD,YAAY,CAACE,kBAAkB,CAAqB,cAAc,CAAC;AAErE,SAASD,kBAAkB","ignoreList":[]}
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ // MediaPipe Pose Landmark indices
4
+ // 11 = left shoulder, 13 = left elbow, 15 = left wrist
5
+ // 12 = right shoulder, 14 = right elbow, 16 = right wrist
6
+ // 23 = left hip, 25 = left knee, 27 = left ankle
7
+ // 24 = right hip, 26 = right knee, 28 = right ankle
8
+
9
+ export const PUSHUP_CONFIG = {
10
+ name: 'Push-Up',
11
+ type: 'rep',
12
+ angles: [{
13
+ name: 'leftElbow',
14
+ landmarkA: 11,
15
+ // left shoulder
16
+ landmarkB: 13,
17
+ // left elbow (vertex)
18
+ landmarkC: 15 // left wrist
19
+ }, {
20
+ name: 'rightElbow',
21
+ landmarkA: 12,
22
+ // right shoulder
23
+ landmarkB: 14,
24
+ // right elbow (vertex)
25
+ landmarkC: 16 // right wrist
26
+ }, {
27
+ name: 'leftHip',
28
+ landmarkA: 11,
29
+ // left shoulder
30
+ landmarkB: 23,
31
+ // left hip (vertex)
32
+ landmarkC: 27 // left ankle
33
+ }, {
34
+ name: 'rightHip',
35
+ landmarkA: 12,
36
+ // right shoulder
37
+ landmarkB: 24,
38
+ // right hip (vertex)
39
+ landmarkC: 28 // right ankle
40
+ }],
41
+ phases: [{
42
+ phase: 'up',
43
+ angleName: 'leftElbow',
44
+ minAngle: 150,
45
+ maxAngle: 180
46
+ }, {
47
+ phase: 'down',
48
+ angleName: 'leftElbow',
49
+ minAngle: 30,
50
+ maxAngle: 90
51
+ }],
52
+ repSequence: ['up', 'down', 'up'],
53
+ formRules: [{
54
+ name: 'hipSag',
55
+ message: 'Keep your hips up — your body should be a straight line',
56
+ severity: 'warning',
57
+ angleName: 'leftHip',
58
+ minAngle: 160,
59
+ // body should be mostly straight
60
+ maxAngle: 180
61
+ }, {
62
+ name: 'hipPike',
63
+ message: "Lower your hips — you're piking up",
64
+ severity: 'warning',
65
+ angleName: 'leftHip',
66
+ minAngle: 160,
67
+ maxAngle: 180
68
+ }],
69
+ holdDurationMs: 0 // not a hold exercise
70
+ };
71
+ //# sourceMappingURL=pushup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["PUSHUP_CONFIG","name","type","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,MAAM,EAAE,CACN;IACEF,IAAI,EAAE,WAAW;IACjBG,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE,CAAE;EACjB,CAAC,EACD;IACEL,IAAI,EAAE,YAAY;IAClBG,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE,CAAE;EACjB,CAAC,EACD;IACEL,IAAI,EAAE,SAAS;IACfG,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE,CAAE;EACjB,CAAC,EACD;IACEL,IAAI,EAAE,UAAU;IAChBG,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;IACEZ,IAAI,EAAE,QAAQ;IACda,OAAO,EAAE,yDAAyD;IAClEC,QAAQ,EAAE,SAAS;IACnBN,SAAS,EAAE,SAAS;IACpBC,QAAQ,EAAE,GAAG;IAAE;IACfC,QAAQ,EAAE;EACZ,CAAC,EACD;IACEV,IAAI,EAAE,SAAS;IACfa,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":[]}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ import { NitroModules } from 'react-native-nitro-modules';
4
+ const nitroPoseExercises = NitroModules.createHybridObject('NitroPoseExercises');
5
+ export { nitroPoseExercises };
6
+ export { PUSHUP_CONFIG } from "./config/pushup.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["NitroModules","nitroPoseExercises","createHybridObject","PUSHUP_CONFIG"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AAIzD,MAAMC,kBAAkB,GACtBD,YAAY,CAACE,kBAAkB,CAAqB,oBAAoB,CAAC;AAE3E,SAASD,kBAAkB;AAoB3B,SAASE,aAAa,QAAQ,oBAAiB","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1 @@
1
+ {"type":"module"}