react-native-nitro-pose-exercises 1.1.6 → 1.1.7
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.
- package/README.md +78 -126
- package/android/src/main/java/com/margelo/nitro/nitroposeexercises/NitroPoseExercises.kt +121 -4
- package/ios/NitroPoseExercises.swift +128 -11
- package/lib/module/NitroPoseExercises.nitro.js.map +1 -1
- package/lib/module/config/bicep-curl.js +3 -0
- package/lib/module/config/bicep-curl.js.map +1 -1
- package/lib/module/config/boat-pose.js +3 -0
- package/lib/module/config/boat-pose.js.map +1 -1
- package/lib/module/config/bow-pose.js +3 -0
- package/lib/module/config/bow-pose.js.map +1 -1
- package/lib/module/config/bridge-pose.js +3 -0
- package/lib/module/config/bridge-pose.js.map +1 -1
- package/lib/module/config/calf-raise.js +3 -0
- package/lib/module/config/calf-raise.js.map +1 -1
- package/lib/module/config/camel-pose.js +3 -0
- package/lib/module/config/camel-pose.js.map +1 -1
- package/lib/module/config/chair-pose.js +3 -0
- package/lib/module/config/chair-pose.js.map +1 -1
- package/lib/module/config/childs-pose.js +3 -0
- package/lib/module/config/childs-pose.js.map +1 -1
- package/lib/module/config/cobra-pose.js +3 -0
- package/lib/module/config/cobra-pose.js.map +1 -1
- package/lib/module/config/cobra-wings.js +3 -0
- package/lib/module/config/cobra-wings.js.map +1 -1
- package/lib/module/config/dead-lift.js +3 -0
- package/lib/module/config/dead-lift.js.map +1 -1
- package/lib/module/config/downward-dog.js +3 -0
- package/lib/module/config/downward-dog.js.map +1 -1
- package/lib/module/config/extended-side-angle.js +3 -0
- package/lib/module/config/extended-side-angle.js.map +1 -1
- package/lib/module/config/fish-pose.js +3 -0
- package/lib/module/config/fish-pose.js.map +1 -1
- package/lib/module/config/front-raise.js +3 -0
- package/lib/module/config/front-raise.js.map +1 -1
- package/lib/module/config/glute-bridge.js +3 -0
- package/lib/module/config/glute-bridge.js.map +1 -1
- package/lib/module/config/hip-abduction.js +3 -0
- package/lib/module/config/hip-abduction.js.map +1 -1
- package/lib/module/config/knee-raise.js +3 -0
- package/lib/module/config/knee-raise.js.map +1 -1
- package/lib/module/config/lateral-raise.js +3 -0
- package/lib/module/config/lateral-raise.js.map +1 -1
- package/lib/module/config/leg-raise.js +3 -0
- package/lib/module/config/leg-raise.js.map +1 -1
- package/lib/module/config/lunge.js +3 -0
- package/lib/module/config/lunge.js.map +1 -1
- package/lib/module/config/mountain-pose.js +3 -0
- package/lib/module/config/mountain-pose.js.map +1 -1
- package/lib/module/config/overarm-reach.js +3 -0
- package/lib/module/config/overarm-reach.js.map +1 -1
- package/lib/module/config/plank.js +3 -0
- package/lib/module/config/plank.js.map +1 -1
- package/lib/module/config/pull-up.js +3 -0
- package/lib/module/config/pull-up.js.map +1 -1
- package/lib/module/config/pushup.js +4 -0
- package/lib/module/config/pushup.js.map +1 -1
- package/lib/module/config/reverse-warrior.js +3 -0
- package/lib/module/config/reverse-warrior.js.map +1 -1
- package/lib/module/config/shoulder-press.js +3 -0
- package/lib/module/config/shoulder-press.js.map +1 -1
- package/lib/module/config/side-lung.js +3 -0
- package/lib/module/config/side-lung.js.map +1 -1
- package/lib/module/config/side-plank.js +3 -0
- package/lib/module/config/side-plank.js.map +1 -1
- package/lib/module/config/situp.js +3 -0
- package/lib/module/config/situp.js.map +1 -1
- package/lib/module/config/squat.js +3 -0
- package/lib/module/config/squat.js.map +1 -1
- package/lib/module/config/sumo-squat.js +3 -0
- package/lib/module/config/sumo-squat.js.map +1 -1
- package/lib/module/config/tree-pose.js +3 -0
- package/lib/module/config/tree-pose.js.map +1 -1
- package/lib/module/config/triangle-pose.js +3 -0
- package/lib/module/config/triangle-pose.js.map +1 -1
- package/lib/module/config/tricep-dip.js +4 -0
- package/lib/module/config/tricep-dip.js.map +1 -1
- package/lib/module/config/v-up.js +3 -0
- package/lib/module/config/v-up.js.map +1 -1
- package/lib/module/config/wall-sit.js +3 -0
- package/lib/module/config/wall-sit.js.map +1 -1
- package/lib/module/config/warrior-i.js +3 -0
- package/lib/module/config/warrior-i.js.map +1 -1
- package/lib/module/config/warrior-ii.js +3 -0
- package/lib/module/config/warrior-ii.js.map +1 -1
- package/lib/module/config/warrior-iii.js +3 -0
- package/lib/module/config/warrior-iii.js.map +1 -1
- package/lib/typescript/src/NitroPoseExercises.nitro.d.ts +9 -1
- package/lib/typescript/src/NitroPoseExercises.nitro.d.ts.map +1 -1
- package/lib/typescript/src/config/bicep-curl.d.ts.map +1 -1
- package/lib/typescript/src/config/boat-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/bow-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/bridge-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/calf-raise.d.ts.map +1 -1
- package/lib/typescript/src/config/camel-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/chair-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/childs-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/cobra-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/cobra-wings.d.ts.map +1 -1
- package/lib/typescript/src/config/dead-lift.d.ts.map +1 -1
- package/lib/typescript/src/config/downward-dog.d.ts.map +1 -1
- package/lib/typescript/src/config/extended-side-angle.d.ts.map +1 -1
- package/lib/typescript/src/config/fish-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/front-raise.d.ts.map +1 -1
- package/lib/typescript/src/config/glute-bridge.d.ts.map +1 -1
- package/lib/typescript/src/config/hip-abduction.d.ts.map +1 -1
- package/lib/typescript/src/config/knee-raise.d.ts.map +1 -1
- package/lib/typescript/src/config/lateral-raise.d.ts.map +1 -1
- package/lib/typescript/src/config/leg-raise.d.ts.map +1 -1
- package/lib/typescript/src/config/lunge.d.ts.map +1 -1
- package/lib/typescript/src/config/mountain-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/overarm-reach.d.ts.map +1 -1
- package/lib/typescript/src/config/plank.d.ts.map +1 -1
- package/lib/typescript/src/config/pull-up.d.ts.map +1 -1
- package/lib/typescript/src/config/pushup.d.ts.map +1 -1
- package/lib/typescript/src/config/reverse-warrior.d.ts.map +1 -1
- package/lib/typescript/src/config/shoulder-press.d.ts.map +1 -1
- package/lib/typescript/src/config/side-lung.d.ts.map +1 -1
- package/lib/typescript/src/config/side-plank.d.ts.map +1 -1
- package/lib/typescript/src/config/situp.d.ts.map +1 -1
- package/lib/typescript/src/config/squat.d.ts.map +1 -1
- package/lib/typescript/src/config/sumo-squat.d.ts.map +1 -1
- package/lib/typescript/src/config/tree-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/triangle-pose.d.ts.map +1 -1
- package/lib/typescript/src/config/tricep-dip.d.ts.map +1 -1
- package/lib/typescript/src/config/v-up.d.ts.map +1 -1
- package/lib/typescript/src/config/wall-sit.d.ts.map +1 -1
- package/lib/typescript/src/config/warrior-i.d.ts.map +1 -1
- package/lib/typescript/src/config/warrior-ii.d.ts.map +1 -1
- package/lib/typescript/src/config/warrior-iii.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JCameraAngleType.hpp +58 -0
- package/nitrogen/generated/android/c++/JExerciseConfig.hpp +19 -3
- package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.cpp +47 -0
- package/nitrogen/generated/android/c++/JHybridNitroPoseExercisesSpec.hpp +5 -0
- package/nitrogen/generated/android/c++/JPostureFamily.hpp +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/CameraAngleType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/ExerciseConfig.kt +19 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/HybridNitroPoseExercisesSpec.kt +32 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroposeexercises/PostureFamily.kt +28 -0
- package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Bridge.hpp +9 -0
- package/nitrogen/generated/ios/NitroPoseExercises-Swift-Cxx-Umbrella.hpp +6 -0
- package/nitrogen/generated/ios/c++/HybridNitroPoseExercisesSpecSwift.hpp +28 -0
- package/nitrogen/generated/ios/swift/CameraAngleType.swift +40 -0
- package/nitrogen/generated/ios/swift/ExerciseConfig.swift +17 -2
- package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec.swift +3 -0
- package/nitrogen/generated/ios/swift/HybridNitroPoseExercisesSpec_cxx.swift +76 -0
- package/nitrogen/generated/ios/swift/PostureFamily.swift +60 -0
- package/nitrogen/generated/shared/c++/CameraAngleType.hpp +76 -0
- package/nitrogen/generated/shared/c++/ExerciseConfig.hpp +20 -2
- package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.cpp +5 -0
- package/nitrogen/generated/shared/c++/HybridNitroPoseExercisesSpec.hpp +5 -0
- package/nitrogen/generated/shared/c++/PostureFamily.hpp +96 -0
- package/package.json +1 -1
- package/src/NitroPoseExercises.nitro.ts +19 -0
- package/src/config/bicep-curl.ts +3 -0
- package/src/config/boat-pose.ts +3 -0
- package/src/config/bow-pose.ts +3 -0
- package/src/config/bridge-pose.ts +3 -0
- package/src/config/calf-raise.ts +3 -0
- package/src/config/camel-pose.ts +3 -0
- package/src/config/chair-pose.ts +3 -0
- package/src/config/childs-pose.ts +3 -0
- package/src/config/cobra-pose.ts +3 -0
- package/src/config/cobra-wings.ts +3 -0
- package/src/config/dead-lift.ts +3 -0
- package/src/config/downward-dog.ts +3 -0
- package/src/config/extended-side-angle.ts +3 -0
- package/src/config/fish-pose.ts +3 -0
- package/src/config/front-raise.ts +4 -0
- package/src/config/glute-bridge.ts +3 -0
- package/src/config/hip-abduction.ts +3 -0
- package/src/config/knee-raise.ts +3 -0
- package/src/config/lateral-raise.ts +3 -0
- package/src/config/leg-raise.ts +3 -0
- package/src/config/lunge.ts +3 -0
- package/src/config/mountain-pose.ts +3 -0
- package/src/config/overarm-reach.ts +3 -0
- package/src/config/plank.ts +3 -0
- package/src/config/pull-up.ts +3 -0
- package/src/config/pushup.ts +3 -0
- package/src/config/reverse-warrior.ts +3 -0
- package/src/config/shoulder-press.ts +3 -0
- package/src/config/side-lung.ts +3 -0
- package/src/config/side-plank.ts +3 -0
- package/src/config/situp.ts +3 -0
- package/src/config/squat.ts +3 -0
- package/src/config/sumo-squat.ts +4 -0
- package/src/config/tree-pose.ts +3 -0
- package/src/config/triangle-pose.ts +3 -0
- package/src/config/tricep-dip.ts +3 -0
- package/src/config/v-up.ts +4 -0
- package/src/config/wall-sit.ts +3 -0
- package/src/config/warrior-i.ts +3 -0
- package/src/config/warrior-ii.ts +3 -0
- package/src/config/warrior-iii.ts +3 -0
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A **React Native Nitro Module** for real-time, on-device exercise tracking using
|
|
|
12
12
|
* 🔄 **Rep Counting** — Automatic rep detection with configurable state machines
|
|
13
13
|
* 🧘 **Hold Tracking** — Duration and stability tracking for planks, yoga poses, and isometric holds
|
|
14
14
|
* 📐 **Form Validation** — Real-time form feedback with angle-based rules
|
|
15
|
+
* 🚦 **Posture Gating** — Refuses to count reps unless the user is in valid posture; "Get in position" feedback before sessions start
|
|
15
16
|
* 💀 **Skeleton Overlay** — Skia-powered skeleton with glow effects and live angle badges
|
|
16
17
|
* ⚡ **Fully Native** — OS-level pose detection via Nitro Modules, zero JS bridge overhead
|
|
17
18
|
* 📦 **Zero Model Bundling** — No ML model files to download or ship with your app
|
|
@@ -79,6 +80,7 @@ cd ios && pod install
|
|
|
79
80
|
| --- | --- |
|
|
80
81
|
| **Rep-Based Exercises** | Cyclic state machine (UP → DOWN → UP = 1 rep). Push-ups, squats, curls, and more. |
|
|
81
82
|
| **Hold-Based Exercises** | Single target pose with duration + stability tracking. Planks, wall sits, yoga poses. |
|
|
83
|
+
| **Posture Gate** | Family-based posture validation. Refuses to start or count reps until user is in correct position (e.g. horizontal for pushups, upright for squats). |
|
|
82
84
|
| **Form Feedback** | Angle-based rules with throttled real-time callbacks. Bad form blocks rep counting. |
|
|
83
85
|
| **Skeleton Overlay** | Glow-effect bones, color-coded joints, and live angle badges drawn over camera via Skia. |
|
|
84
86
|
| **Bilateral Tracking** | Left and right side angles tracked independently. |
|
|
@@ -236,132 +238,6 @@ const styles = StyleSheet.create({
|
|
|
236
238
|
});
|
|
237
239
|
```
|
|
238
240
|
|
|
239
|
-
### Skeleton Overlay with Angle Badges — SkiaCamera
|
|
240
|
-
|
|
241
|
-
> **Critical:** Create all `Skia.Paint()` and `Skia.Font()` objects **outside** the `onFrame` callback. Creating them inside causes memory leaks and crashes within seconds.
|
|
242
|
-
|
|
243
|
-
```tsx
|
|
244
|
-
import { Skia } from '@shopify/react-native-skia';
|
|
245
|
-
import { SkiaCamera } from 'react-native-vision-camera-skia';
|
|
246
|
-
import { nitroPoseExercises } from 'react-native-nitro-pose-exercises';
|
|
247
|
-
|
|
248
|
-
// Create paints ONCE at module level — NEVER inside onFrame
|
|
249
|
-
const GLOW_PAINT = Skia.Paint();
|
|
250
|
-
GLOW_PAINT.setColor(Skia.Color('rgba(0, 255, 102, 0.3)'));
|
|
251
|
-
GLOW_PAINT.setStrokeWidth(14);
|
|
252
|
-
GLOW_PAINT.setStyle(1);
|
|
253
|
-
GLOW_PAINT.setStrokeCap(1);
|
|
254
|
-
GLOW_PAINT.setAntiAlias(true);
|
|
255
|
-
|
|
256
|
-
const BONE_PAINT = Skia.Paint();
|
|
257
|
-
BONE_PAINT.setColor(Skia.Color('#00FF66'));
|
|
258
|
-
BONE_PAINT.setStrokeWidth(6);
|
|
259
|
-
BONE_PAINT.setStyle(1);
|
|
260
|
-
BONE_PAINT.setStrokeCap(1);
|
|
261
|
-
BONE_PAINT.setAntiAlias(true);
|
|
262
|
-
|
|
263
|
-
const JOINT_PAINT = Skia.Paint();
|
|
264
|
-
JOINT_PAINT.setColor(Skia.Color('#FF3366'));
|
|
265
|
-
JOINT_PAINT.setStyle(0);
|
|
266
|
-
JOINT_PAINT.setAntiAlias(true);
|
|
267
|
-
|
|
268
|
-
const KEY_JOINT_PAINT = Skia.Paint();
|
|
269
|
-
KEY_JOINT_PAINT.setColor(Skia.Color('#00FFFF'));
|
|
270
|
-
KEY_JOINT_PAINT.setStyle(0);
|
|
271
|
-
KEY_JOINT_PAINT.setAntiAlias(true);
|
|
272
|
-
|
|
273
|
-
const ANGLE_BG_PAINT = Skia.Paint();
|
|
274
|
-
ANGLE_BG_PAINT.setColor(Skia.Color('rgba(0, 0, 0, 0.7)'));
|
|
275
|
-
ANGLE_BG_PAINT.setStyle(0);
|
|
276
|
-
|
|
277
|
-
const ANGLE_TEXT_FONT = Skia.Font(null, 14);
|
|
278
|
-
const ANGLE_TEXT_PAINT = Skia.Paint();
|
|
279
|
-
ANGLE_TEXT_PAINT.setColor(Skia.Color('#FFFFFF'));
|
|
280
|
-
|
|
281
|
-
const SKELETON_CONNECTIONS = [
|
|
282
|
-
[11, 12], [11, 23], [12, 24], [23, 24],
|
|
283
|
-
[11, 13], [13, 15], [12, 14], [14, 16],
|
|
284
|
-
[23, 25], [25, 27], [24, 26], [26, 28],
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
const KEY_LANDMARKS = [11, 12, 13, 14, 15, 16, 23, 24, 25, 26, 27, 28];
|
|
288
|
-
|
|
289
|
-
<SkiaCamera
|
|
290
|
-
style={StyleSheet.absoluteFill}
|
|
291
|
-
isActive={true}
|
|
292
|
-
device="back"
|
|
293
|
-
pixelFormat="rgb"
|
|
294
|
-
onFrame={(frame, render) => {
|
|
295
|
-
'worklet';
|
|
296
|
-
try {
|
|
297
|
-
nitroPoseExercises.processFrame(frame);
|
|
298
|
-
const landmarks = nitroPoseExercises.landmarks;
|
|
299
|
-
|
|
300
|
-
render(({ frameTexture, canvas }) => {
|
|
301
|
-
canvas.drawImage(frameTexture, 0, 0);
|
|
302
|
-
|
|
303
|
-
if (landmarks && landmarks.length > 0) {
|
|
304
|
-
const w = frame.width;
|
|
305
|
-
const h = frame.height;
|
|
306
|
-
|
|
307
|
-
// Glow layer (thick translucent)
|
|
308
|
-
for (const [i, j] of SKELETON_CONNECTIONS) {
|
|
309
|
-
if (i < landmarks.length && j < landmarks.length) {
|
|
310
|
-
const a = landmarks[i];
|
|
311
|
-
const b = landmarks[j];
|
|
312
|
-
if (a.visibility > 0.5 && b.visibility > 0.5) {
|
|
313
|
-
canvas.drawLine(a.x * w, a.y * h, b.x * w, b.y * h, GLOW_PAINT);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Solid bones on top
|
|
319
|
-
for (const [i, j] of SKELETON_CONNECTIONS) {
|
|
320
|
-
if (i < landmarks.length && j < landmarks.length) {
|
|
321
|
-
const a = landmarks[i];
|
|
322
|
-
const b = landmarks[j];
|
|
323
|
-
if (a.visibility > 0.5 && b.visibility > 0.5) {
|
|
324
|
-
canvas.drawLine(a.x * w, a.y * h, b.x * w, b.y * h, BONE_PAINT);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Joints with glow rings
|
|
330
|
-
for (let idx = 0; idx < landmarks.length; idx++) {
|
|
331
|
-
const lm = landmarks[idx];
|
|
332
|
-
if (lm && lm.visibility > 0.5) {
|
|
333
|
-
const isKey = KEY_LANDMARKS.includes(idx);
|
|
334
|
-
if (isKey) canvas.drawCircle(lm.x * w, lm.y * h, 12, GLOW_PAINT);
|
|
335
|
-
canvas.drawCircle(lm.x * w, lm.y * h, isKey ? 8 : 4, isKey ? KEY_JOINT_PAINT : JOINT_PAINT);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Elbow angle badges
|
|
340
|
-
for (const [shoulderIdx, elbowIdx, wristIdx] of [[11, 13, 15], [12, 14, 16]]) {
|
|
341
|
-
const s = landmarks[shoulderIdx];
|
|
342
|
-
const e = landmarks[elbowIdx];
|
|
343
|
-
const wr = landmarks[wristIdx];
|
|
344
|
-
if (s?.visibility > 0.5 && e?.visibility > 0.5 && wr?.visibility > 0.5) {
|
|
345
|
-
const vaX = s.x - e.x, vaY = s.y - e.y;
|
|
346
|
-
const vcX = wr.x - e.x, vcY = wr.y - e.y;
|
|
347
|
-
const dot = vaX * vcX + vaY * vcY;
|
|
348
|
-
const mag = Math.sqrt(vaX * vaX + vaY * vaY) * Math.sqrt(vcX * vcX + vcY * vcY);
|
|
349
|
-
const angle = Math.round(Math.acos(Math.max(-1, Math.min(1, dot / mag))) * (180 / Math.PI));
|
|
350
|
-
const tx = e.x * w + 15;
|
|
351
|
-
const ty = e.y * h - 10;
|
|
352
|
-
canvas.drawRoundRect({ x: tx - 4, y: ty - 14, width: 48, height: 20 }, 6, 6, ANGLE_BG_PAINT);
|
|
353
|
-
canvas.drawText(`${angle}°`, tx, ty, ANGLE_TEXT_PAINT, ANGLE_TEXT_FONT);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
} finally {
|
|
359
|
-
frame.dispose();
|
|
360
|
-
}
|
|
361
|
-
}}
|
|
362
|
-
/>
|
|
363
|
-
```
|
|
364
|
-
|
|
365
241
|
---
|
|
366
242
|
|
|
367
243
|
## 🧩 API Reference
|
|
@@ -389,6 +265,9 @@ startSession(targetReps: number, countdownSeconds: number): void
|
|
|
389
265
|
pauseSession(): void
|
|
390
266
|
resumeSession(): void
|
|
391
267
|
stopSession(): void
|
|
268
|
+
// Returns true if the user is currently in valid posture for the loaded exercise.
|
|
269
|
+
// Poll this before starting a session, e.g. show "Get in position" until ready.
|
|
270
|
+
isReady(): boolean
|
|
392
271
|
```
|
|
393
272
|
|
|
394
273
|
### Frame Processing
|
|
@@ -416,6 +295,8 @@ onFormFeedback: ((feedback: FormFeedback) => void) | undefined
|
|
|
416
295
|
onHoldProgress: ((progress: HoldProgress) => void) | undefined
|
|
417
296
|
onPoseLost: (() => void) | undefined
|
|
418
297
|
onPoseRegained: (() => void) | undefined
|
|
298
|
+
onPostureLost: (() => void) | undefined
|
|
299
|
+
onPostureRegained: (() => void) | undefined
|
|
419
300
|
onSessionComplete: ((result: SessionResult) => void) | undefined
|
|
420
301
|
```
|
|
421
302
|
|
|
@@ -454,6 +335,74 @@ onSessionComplete: ((result: SessionResult) => void) | undefined
|
|
|
454
335
|
angleHistory: AngleSnapshot[]
|
|
455
336
|
}
|
|
456
337
|
```
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## 🚦 Posture Gating
|
|
341
|
+
|
|
342
|
+
Each exercise config declares a **posture family** that defines what body position is required before reps are counted. This prevents false counts — e.g. waving your arm while standing won't count as a push-up.
|
|
343
|
+
|
|
344
|
+
### Posture Families
|
|
345
|
+
|
|
346
|
+
| Family | Description | Used For |
|
|
347
|
+
| --- | --- | --- |
|
|
348
|
+
| `horizontalProne` | Body horizontal, face down. Shoulders, hips, ankles in a horizontal band. | Push-ups, planks, cobra, mountain climbers |
|
|
349
|
+
| `standingUpright` | Standing, shoulders above hips above knees. | Squats, lunges, curls, presses, most yoga poses |
|
|
350
|
+
| `seated` | Hips near knees, shoulders above hips. | Boat pose, seated yoga, child's pose |
|
|
351
|
+
| `supine` | Body horizontal, face up. | Sit-ups, glute bridge, leg raises |
|
|
352
|
+
| `sidePlank` | Body horizontal, rotated to one side. | Side plank, side leg raises |
|
|
353
|
+
| `inverted` | Hips higher than shoulders and ankles. | Downward dog, handstand |
|
|
354
|
+
| `none` | No posture gating. | Custom or unconstrained exercises |
|
|
355
|
+
|
|
356
|
+
### Flow
|
|
357
|
+
|
|
358
|
+
loadExercise(config) → poll isReady() → user gets in position →
|
|
359
|
+
isReady() returns true → startSession() → reps counted normally
|
|
360
|
+
↓
|
|
361
|
+
if posture breaks mid-session
|
|
362
|
+
→ onPostureLost fires
|
|
363
|
+
→ phase detection pauses
|
|
364
|
+
→ in-progress rep discarded
|
|
365
|
+
↓
|
|
366
|
+
user re-enters position
|
|
367
|
+
→ onPostureRegained fires
|
|
368
|
+
→ counting resumes
|
|
369
|
+
|
|
370
|
+
### Example: Wait for Position Before Starting
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
const [isInPosition, setIsInPosition] = useState(false);
|
|
374
|
+
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
// Wait for the user to get into position before starting
|
|
377
|
+
const checkInterval = setInterval(() => {
|
|
378
|
+
if (nitroPoseExercises.isReady()) {
|
|
379
|
+
clearInterval(checkInterval);
|
|
380
|
+
nitroPoseExercises.startSession(10, 3);
|
|
381
|
+
}
|
|
382
|
+
}, 300);
|
|
383
|
+
return () => clearInterval(interval);
|
|
384
|
+
}, []);
|
|
385
|
+
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
nitroPoseExercises.onPostureLost = () => {
|
|
388
|
+
setMessage('Get back into position');
|
|
389
|
+
};
|
|
390
|
+
nitroPoseExercises.onPostureRegained = () => {
|
|
391
|
+
setMessage('');
|
|
392
|
+
};
|
|
393
|
+
}, []);
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<>
|
|
397
|
+
{!isInPosition && <Text>Get into push-up position</Text>}
|
|
398
|
+
{isInPosition && <Text>Hold still — starting...</Text>}
|
|
399
|
+
</>
|
|
400
|
+
);
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Tuning
|
|
404
|
+
|
|
405
|
+
Posture gates use a **10-frame hysteresis** (about 1 second at 30fps with frame throttling) — single-frame failures don't pause the session. This prevents flicker from momentary occlusion or visibility drops.
|
|
457
406
|
|
|
458
407
|
---
|
|
459
408
|
|
|
@@ -528,6 +477,7 @@ import type { ExerciseConfig } from 'react-native-nitro-pose-exercises';
|
|
|
528
477
|
const MY_EXERCISE: ExerciseConfig = {
|
|
529
478
|
name: 'Custom Exercise',
|
|
530
479
|
type: 'rep', // 'rep' | 'hold'
|
|
480
|
+
postureFamily: 'standingUpright', // ← required: see table above
|
|
531
481
|
angles: [
|
|
532
482
|
{ name: 'myAngle', landmarkA: 11, landmarkB: 13, landmarkC: 15 },
|
|
533
483
|
],
|
|
@@ -597,6 +547,8 @@ Each exercise config includes a `cameraAngle` recommendation (`'side'` or `'fron
|
|
|
597
547
|
| **Pose lost detection** | `onPoseLost` / `onPoseRegained` callbacks when user exits/enters frame |
|
|
598
548
|
| **Frame throttle** | Processes every 3rd frame to reduce CPU load without losing accuracy |
|
|
599
549
|
| **Visibility filter** | Landmarks with confidence below 0.3 are excluded from angle calculations |
|
|
550
|
+
| **Posture entry gate** | Sessions don't start counting until `isReady()` returns true |
|
|
551
|
+
| **Posture hysteresis** | 10 consecutive failed frames required to fire `onPostureLost` — prevents flicker |
|
|
600
552
|
|
|
601
553
|
---
|
|
602
554
|
|
|
@@ -26,6 +26,7 @@ class NitroPoseExercises : HybridNitroPoseExercisesSpec() {
|
|
|
26
26
|
private var poseDetector: PoseDetector? = null
|
|
27
27
|
private var isInitialized = false
|
|
28
28
|
|
|
29
|
+
|
|
29
30
|
// ─── Cached Landmarks (ML Kit is async, we cache last result) ──
|
|
30
31
|
private var cachedLandmarks: Array<Landmark> = emptyArray()
|
|
31
32
|
private val landmarkLock = Any()
|
|
@@ -115,6 +116,14 @@ class NitroPoseExercises : HybridNitroPoseExercisesSpec() {
|
|
|
115
116
|
override var onPoseRegained: (() -> Unit)? = null
|
|
116
117
|
override var onSessionComplete: ((result: SessionResult) -> Unit)? = null
|
|
117
118
|
|
|
119
|
+
override var onPostureLost: (() -> Unit)? = null
|
|
120
|
+
override var onPostureRegained: (() -> Unit)? = null
|
|
121
|
+
|
|
122
|
+
// ─── Posture Gate ──────────────────────────────────────────
|
|
123
|
+
private var consecutivePostureFailures: Int = 0
|
|
124
|
+
private val postureFailureThreshold: Int = 10
|
|
125
|
+
private var postureWasLost = false
|
|
126
|
+
|
|
118
127
|
// ─── Hold Tracking ──────────────────────────────────────────
|
|
119
128
|
private var holdStartTime: Long? = null
|
|
120
129
|
|
|
@@ -143,6 +152,12 @@ class NitroPoseExercises : HybridNitroPoseExercisesSpec() {
|
|
|
143
152
|
resetSession()
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
override fun isReady(): Boolean {
|
|
156
|
+
val config = exerciseConfig ?: return false
|
|
157
|
+
if (_landmarks.isEmpty()) return false
|
|
158
|
+
return isPostureValid(config.postureFamily)
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
// ═══════════════════════════════════════════════════════════
|
|
147
162
|
// Exercise Setup
|
|
148
163
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -201,7 +216,10 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
201
216
|
val nativeBuffer = frame.getNativeBuffer()
|
|
202
217
|
val bitmap = FrameHelper.hardwareBufferToBitmap(nativeBuffer.pointer) ?: return
|
|
203
218
|
|
|
204
|
-
|
|
219
|
+
|
|
220
|
+
val rotation = rotationDegreesFromFrame(frame)
|
|
221
|
+
val inputImage = InputImage.fromBitmap(bitmap, rotation)
|
|
222
|
+
|
|
205
223
|
val imageWidth = bitmap.width.toDouble()
|
|
206
224
|
val imageHeight = bitmap.height.toDouble()
|
|
207
225
|
|
|
@@ -270,9 +288,29 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
270
288
|
// Exercise Logic Engine
|
|
271
289
|
// ═══════════════════════════════════════════════════════════
|
|
272
290
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
291
|
+
private fun processExerciseLogic() {
|
|
292
|
+
val config = exerciseConfig ?: return
|
|
293
|
+
if (_landmarks.isEmpty()) return
|
|
294
|
+
|
|
295
|
+
// Posture gate with hysteresis
|
|
296
|
+
if (!isPostureValid(config.postureFamily)) {
|
|
297
|
+
consecutivePostureFailures++
|
|
298
|
+
if (consecutivePostureFailures >= postureFailureThreshold) {
|
|
299
|
+
if (!postureWasLost) {
|
|
300
|
+
postureWasLost = true
|
|
301
|
+
onPostureLost?.invoke()
|
|
302
|
+
}
|
|
303
|
+
_currentPhase = ExercisePhase.UNKNOWN
|
|
304
|
+
phaseHistory.clear()
|
|
305
|
+
}
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
consecutivePostureFailures = 0
|
|
310
|
+
if (postureWasLost) {
|
|
311
|
+
postureWasLost = false
|
|
312
|
+
onPostureRegained?.invoke()
|
|
313
|
+
}
|
|
276
314
|
|
|
277
315
|
val currentAngles = mutableMapOf<String, Double>()
|
|
278
316
|
val angleSnapshots = mutableListOf<AngleSnapshot>()
|
|
@@ -511,6 +549,83 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
511
549
|
onSessionComplete?.invoke(result)
|
|
512
550
|
}
|
|
513
551
|
|
|
552
|
+
// ═══════════════════════════════════════════════════════════
|
|
553
|
+
// Orientation Helpers
|
|
554
|
+
// ═══════════════════════════════════════════════════════════
|
|
555
|
+
|
|
556
|
+
private fun rotationDegreesFromFrame(frame: HybridFrameSpec): Int {
|
|
557
|
+
return when (frame.orientation.name.lowercase()) {
|
|
558
|
+
"up" -> 0
|
|
559
|
+
"right" -> 90
|
|
560
|
+
"down" -> 180
|
|
561
|
+
"left" -> 270
|
|
562
|
+
else -> 0
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ═══════════════════════════════════════════════════════════
|
|
567
|
+
// Posture Gates
|
|
568
|
+
// ═══════════════════════════════════════════════════════════
|
|
569
|
+
|
|
570
|
+
private func isPostureValid(_ family: String) -> Bool {
|
|
571
|
+
guard _landmarks.count >= 33 else { return false }
|
|
572
|
+
|
|
573
|
+
let visThreshold = exerciseConfig?.visibilityThreshold ?? 0.3
|
|
574
|
+
|
|
575
|
+
if (_landmarks.size < 33) return false
|
|
576
|
+
|
|
577
|
+
val ls = _landmarks[11]; val rs = _landmarks[12]
|
|
578
|
+
val lh = _landmarks[23]; val rh = _landmarks[24]
|
|
579
|
+
val lk = _landmarks[25]; val rk = _landmarks[26]
|
|
580
|
+
val la = _landmarks[27]; val ra = _landmarks[28]
|
|
581
|
+
|
|
582
|
+
val keyVisible = ls.visibility > 0.3 && rs.visibility > 0.3 &&
|
|
583
|
+
lh.visibility > 0.3 && rh.visibility > 0.3
|
|
584
|
+
if (!keyVisible) return false
|
|
585
|
+
|
|
586
|
+
val shoulderY = (ls.y + rs.y) / 2
|
|
587
|
+
val hipY = (lh.y + rh.y) / 2
|
|
588
|
+
val shoulderX = (ls.x + rs.x) / 2
|
|
589
|
+
val hipX = (lh.x + rh.x) / 2
|
|
590
|
+
|
|
591
|
+
val kneesVisible = lk.visibility > 0.3 && rk.visibility > 0.3
|
|
592
|
+
val anklesVisible = la.visibility > 0.3 && ra.visibility > 0.3
|
|
593
|
+
val kneeY = if (kneesVisible) (lk.y + rk.y) / 2 else hipY
|
|
594
|
+
val ankleY = if (anklesVisible) (la.y + ra.y) / 2 else kneeY
|
|
595
|
+
|
|
596
|
+
return when (family) {
|
|
597
|
+
"horizontalProne" -> {
|
|
598
|
+
val ys = listOf(shoulderY, hipY, ankleY)
|
|
599
|
+
(ys.max() - ys.min()) < 0.25
|
|
600
|
+
}
|
|
601
|
+
"standingUpright" -> {
|
|
602
|
+
if (!kneesVisible) false
|
|
603
|
+
else shoulderY < hipY - 0.08 &&
|
|
604
|
+
hipY < kneeY + 0.05 &&
|
|
605
|
+
(if (anklesVisible) kneeY < ankleY else true)
|
|
606
|
+
}
|
|
607
|
+
"seated" -> {
|
|
608
|
+
if (!kneesVisible) false
|
|
609
|
+
else shoulderY < hipY - 0.05 && kotlin.math.abs(hipY - kneeY) < 0.20
|
|
610
|
+
}
|
|
611
|
+
"supine" -> {
|
|
612
|
+
val ys = listOf(shoulderY, hipY, ankleY)
|
|
613
|
+
(ys.max() - ys.min()) < 0.25
|
|
614
|
+
}
|
|
615
|
+
"sidePlank" -> {
|
|
616
|
+
val ySpread = kotlin.math.abs(shoulderY - hipY)
|
|
617
|
+
val shoulderHipDx = kotlin.math.abs(shoulderX - hipX)
|
|
618
|
+
ySpread < 0.20 && shoulderHipDx < 0.15
|
|
619
|
+
}
|
|
620
|
+
"inverted" -> {
|
|
621
|
+
if (!anklesVisible) false
|
|
622
|
+
else hipY < shoulderY && hipY < ankleY
|
|
623
|
+
}
|
|
624
|
+
"none" -> true
|
|
625
|
+
else -> true
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
514
629
|
// ═══════════════════════════════════════════════════════════
|
|
515
630
|
// Countdown
|
|
516
631
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -549,6 +664,8 @@ override fun processFrame(frame: HybridFrameSpec) {
|
|
|
549
664
|
targetReps = 0.0
|
|
550
665
|
countdownSeconds = 0.0
|
|
551
666
|
frameCount = 0
|
|
667
|
+
consecutivePostureFailures = 0
|
|
668
|
+
postureWasLost = false
|
|
552
669
|
synchronized(landmarkLock) {
|
|
553
670
|
cachedLandmarks = emptyArray()
|
|
554
671
|
}
|
|
@@ -70,6 +70,11 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
|
|
|
70
70
|
private var allRepDurations: [Double] = []
|
|
71
71
|
private var allRepFormScores: [Double] = []
|
|
72
72
|
|
|
73
|
+
// ─── Posture Gate ──────────────────────────────────────────
|
|
74
|
+
private var consecutivePostureFailures: Int = 0
|
|
75
|
+
private let postureFailureThreshold: Int = 10 // ~1s at 30fps with throttle=3
|
|
76
|
+
private var postureWasLost = false
|
|
77
|
+
|
|
73
78
|
// ─── Pose Tracking ──────────────────────────────────────────
|
|
74
79
|
private var poseWasLost = false
|
|
75
80
|
|
|
@@ -85,6 +90,8 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
|
|
|
85
90
|
var onPoseLost: (() -> Void)?
|
|
86
91
|
var onPoseRegained: (() -> Void)?
|
|
87
92
|
var onSessionComplete: ((_ result: SessionResult) -> Void)?
|
|
93
|
+
var onPostureLost: (() -> Void)?
|
|
94
|
+
var onPostureRegained: (() -> Void)?
|
|
88
95
|
|
|
89
96
|
// ─── Hold Tracking ──────────────────────────────────────────
|
|
90
97
|
private var holdStartTime: Date?
|
|
@@ -108,6 +115,12 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
|
|
|
108
115
|
resetSession()
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
func isReady() throws -> Bool {
|
|
119
|
+
guard let config = exerciseConfig else { return false }
|
|
120
|
+
guard !_landmarks.isEmpty else { return false }
|
|
121
|
+
return isPostureValid(config.postureFamily)
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
// ═══════════════════════════════════════════════════════════
|
|
112
125
|
// MARK: - Exercise Setup
|
|
113
126
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -151,12 +164,23 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
|
|
|
151
164
|
completeSession()
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
private static func cgOrientation(orientation: CameraOrientation, isMirrored: Bool) -> CGImagePropertyOrientation {
|
|
168
|
+
switch orientation {
|
|
169
|
+
case .up: return isMirrored ? .upMirrored : .up
|
|
170
|
+
case .down: return isMirrored ? .downMirrored : .down
|
|
171
|
+
case .left: return isMirrored ? .leftMirrored : .left
|
|
172
|
+
case .right: return isMirrored ? .rightMirrored : .right
|
|
173
|
+
@unknown default: return .up
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
154
177
|
// ═══════════════════════════════════════════════════════════
|
|
155
178
|
// MARK: - Frame Processing (Apple Vision)
|
|
156
179
|
// ═══════════════════════════════════════════════════════════
|
|
157
180
|
|
|
158
181
|
func processFrame(frame: any HybridFrameSpec) throws {
|
|
159
182
|
guard _status == .active || _status == .countdown else { return }
|
|
183
|
+
|
|
160
184
|
guard isInitialized else { return }
|
|
161
185
|
|
|
162
186
|
// Frame throttle
|
|
@@ -173,9 +197,11 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
|
|
|
173
197
|
// Create Vision request
|
|
174
198
|
let request = VNDetectHumanBodyPoseRequest()
|
|
175
199
|
|
|
176
|
-
|
|
177
|
-
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
|
|
200
|
+
print("[PoseExercise] orientation=\(frame.orientation) isMirrored=\(frame.isMirrored) type=\(type(of: frame.orientation))")
|
|
178
201
|
|
|
202
|
+
let cgOrient = Self.cgOrientation(orientation: frame.orientation, isMirrored: frame.isMirrored)
|
|
203
|
+
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: cgOrient, options: [:])
|
|
204
|
+
|
|
179
205
|
do {
|
|
180
206
|
try handler.perform([request])
|
|
181
207
|
|
|
@@ -241,13 +267,37 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
|
|
|
241
267
|
// MARK: - Exercise Logic Engine
|
|
242
268
|
// ═══════════════════════════════════════════════════════════
|
|
243
269
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
270
|
+
private func processExerciseLogic() {
|
|
271
|
+
|
|
272
|
+
guard let config = exerciseConfig else { return }
|
|
273
|
+
guard !_landmarks.isEmpty else { return }
|
|
274
|
+
|
|
275
|
+
// Posture gate with hysteresis
|
|
276
|
+
if !isPostureValid(family: config.postureFamily, threshold: config.visibilityThreshold) {
|
|
277
|
+
consecutivePostureFailures += 1
|
|
278
|
+
if consecutivePostureFailures >= postureFailureThreshold {
|
|
279
|
+
if !postureWasLost {
|
|
280
|
+
postureWasLost = true
|
|
281
|
+
onPostureLost?()
|
|
282
|
+
}
|
|
283
|
+
_currentPhase = .unknown
|
|
284
|
+
phaseHistory = []
|
|
285
|
+
}
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
consecutivePostureFailures = 0
|
|
290
|
+
|
|
291
|
+
if postureWasLost {
|
|
292
|
+
postureWasLost = false
|
|
293
|
+
onPostureRegained?()
|
|
294
|
+
}
|
|
247
295
|
|
|
248
296
|
var currentAngles: [String: Double] = [:]
|
|
249
297
|
var angleSnapshots: [AngleSnapshot] = []
|
|
250
298
|
|
|
299
|
+
let visThreshold = config.visibilityThreshold
|
|
300
|
+
|
|
251
301
|
for angleDef in config.angles {
|
|
252
302
|
let a = Int(angleDef.landmarkA)
|
|
253
303
|
let b = Int(angleDef.landmarkB)
|
|
@@ -255,10 +305,10 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
|
|
|
255
305
|
|
|
256
306
|
guard a < _landmarks.count, b < _landmarks.count, c < _landmarks.count else { continue }
|
|
257
307
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
_landmarks[
|
|
261
|
-
|
|
308
|
+
guard _landmarks[a].visibility > visThreshold,
|
|
309
|
+
_landmarks[b].visibility > visThreshold,
|
|
310
|
+
_landmarks[c].visibility > visThreshold else { continue }
|
|
311
|
+
|
|
262
312
|
|
|
263
313
|
let angle = calculateAngle(
|
|
264
314
|
pointA: _landmarks[a],
|
|
@@ -293,6 +343,71 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
|
|
|
293
343
|
}
|
|
294
344
|
}
|
|
295
345
|
|
|
346
|
+
// ═══════════════════════════════════════════════════════════
|
|
347
|
+
// MARK: - Posture Gates
|
|
348
|
+
// ═══════════════════════════════════════════════════════════
|
|
349
|
+
|
|
350
|
+
private func isPostureValid(_ family: String) -> Bool {
|
|
351
|
+
guard _landmarks.count >= 33 else { return false }
|
|
352
|
+
|
|
353
|
+
let ls = _landmarks[11], rs = _landmarks[12]
|
|
354
|
+
let lh = _landmarks[23], rh = _landmarks[24]
|
|
355
|
+
let lk = _landmarks[25], rk = _landmarks[26]
|
|
356
|
+
let la = _landmarks[27], ra = _landmarks[28]
|
|
357
|
+
|
|
358
|
+
let key = [ls, rs, lh, rh]
|
|
359
|
+
guard key.allSatisfy({ $0.visibility > 0.3 }) 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
|
|
365
|
+
|
|
366
|
+
let kneesVisible = lk.visibility > 0.3 && rk.visibility > 0.3
|
|
367
|
+
let anklesVisible = la.visibility > 0.3 && ra.visibility > 0.3
|
|
368
|
+
let kneeY = kneesVisible ? (lk.y + rk.y) / 2 : hipY
|
|
369
|
+
let ankleY = anklesVisible ? (la.y + ra.y) / 2 : kneeY
|
|
370
|
+
|
|
371
|
+
switch family {
|
|
372
|
+
case "horizontalProne":
|
|
373
|
+
let ys = [shoulderY, hipY, ankleY]
|
|
374
|
+
let spread = (ys.max() ?? 0) - (ys.min() ?? 0)
|
|
375
|
+
return spread < 0.25
|
|
376
|
+
|
|
377
|
+
case "standingUpright":
|
|
378
|
+
guard kneesVisible else { return false }
|
|
379
|
+
return shoulderY < hipY - 0.08
|
|
380
|
+
&& hipY < kneeY + 0.05
|
|
381
|
+
&& (anklesVisible ? kneeY < ankleY : true)
|
|
382
|
+
|
|
383
|
+
case "seated":
|
|
384
|
+
guard kneesVisible else { return false }
|
|
385
|
+
return shoulderY < hipY - 0.05
|
|
386
|
+
&& abs(hipY - kneeY) < 0.20
|
|
387
|
+
|
|
388
|
+
case "supine":
|
|
389
|
+
let ys = [shoulderY, hipY, ankleY]
|
|
390
|
+
let spread = (ys.max() ?? 0) - (ys.min() ?? 0)
|
|
391
|
+
return spread < 0.25
|
|
392
|
+
|
|
393
|
+
case "sidePlank":
|
|
394
|
+
let ys = [shoulderY, hipY]
|
|
395
|
+
let ySpread = (ys.max() ?? 0) - (ys.min() ?? 0)
|
|
396
|
+
let shoulderHipDx = abs(shoulderX - hipX)
|
|
397
|
+
return ySpread < 0.20 && shoulderHipDx < 0.15
|
|
398
|
+
|
|
399
|
+
case "inverted":
|
|
400
|
+
guard anklesVisible else { return false }
|
|
401
|
+
return hipY < shoulderY && hipY < ankleY
|
|
402
|
+
|
|
403
|
+
case "none":
|
|
404
|
+
return true
|
|
405
|
+
|
|
406
|
+
default:
|
|
407
|
+
return true
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
296
411
|
// ═══════════════════════════════════════════════════════════
|
|
297
412
|
// MARK: - Angle Calculation
|
|
298
413
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -547,6 +662,8 @@ class NitroPoseExercises: HybridNitroPoseExercisesSpec {
|
|
|
547
662
|
countdownSeconds = 0
|
|
548
663
|
countdownTimer?.invalidate()
|
|
549
664
|
countdownTimer = nil
|
|
550
|
-
frameCount = 0
|
|
665
|
+
frameCount = 0,
|
|
666
|
+
consecutivePostureFailures = 0
|
|
667
|
+
postureWasLost = false
|
|
551
668
|
}
|
|
552
|
-
}
|
|
669
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["NitroModules","nitroPoseExercises","createHybridObject"],"sourceRoot":"../../src","sources":["NitroPoseExercises.nitro.ts"],"mappings":";;AAAA,SAA4BA,YAAY,QAAQ,4BAA4B;;AAI5E;;
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"names":["BICEP_CURL_CONFIG","name","type","angles","landmarkA","landmarkB","landmarkC","phases","phase","angleName","minAngle","maxAngle","repSequence","formRules","message","severity","holdDurationMs"],"sourceRoot":"../../../src","sources":["config/bicep-curl.ts"],"mappings":";;AAEA;AACA;AACA;;AAEA,OAAO,MAAMA,iBAAiC,GAAG;EAC/CC,IAAI,EAAE,YAAY;EAClBC,IAAI,EAAE,KAAK;EACXC,MAAM,EAAE,CACN;
|
|
1
|
+
{"version":3,"names":["BICEP_CURL_CONFIG","name","type","postureFamily","visibilityThreshold","cameraAngle","angles","landmarkA","landmarkB","landmarkC","phases","phase","angleName","minAngle","maxAngle","repSequence","formRules","message","severity","holdDurationMs"],"sourceRoot":"../../../src","sources":["config/bicep-curl.ts"],"mappings":";;AAEA;AACA;AACA;;AAEA,OAAO,MAAMA,iBAAiC,GAAG;EAC/CC,IAAI,EAAE,YAAY;EAClBC,IAAI,EAAE,KAAK;EACXC,aAAa,EAAE,iBAAiB;EAChCC,mBAAmB,EAAE,GAAG;EACxBC,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,cAAc;IACpBM,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE;IAAE;IACfC,SAAS,EAAE,EAAE,CAAE;EACjB,CAAC,CACF;EACDC,MAAM,EAAE,CACN;IACEC,KAAK,EAAE,MAAM;IACbC,SAAS,EAAE,WAAW;IACtBC,QAAQ,EAAE,GAAG;IACbC,QAAQ,EAAE;EACZ,CAAC,EACD;IACEH,KAAK,EAAE,IAAI;IACXC,SAAS,EAAE,WAAW;IACtBC,QAAQ,EAAE,EAAE;IACZC,QAAQ,EAAE;EACZ,CAAC,CACF;EACDC,WAAW,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC;EACnCC,SAAS,EAAE,CACT;IACEf,IAAI,EAAE,YAAY;IAClBgB,OAAO,EAAE,uCAAuC;IAChDC,QAAQ,EAAE,SAAS;IACnBN,SAAS,EAAE,cAAc;IACzBC,QAAQ,EAAE,CAAC;IACXC,QAAQ,EAAE;EACZ,CAAC,EACD;IACEb,IAAI,EAAE,UAAU;IAChBgB,OAAO,EAAE,oCAAoC;IAC7CC,QAAQ,EAAE,OAAO;IACjBN,SAAS,EAAE,cAAc;IACzBC,QAAQ,EAAE,CAAC;IACXC,QAAQ,EAAE;EACZ,CAAC,CACF;EACDK,cAAc,EAAE;AAClB,CAAC","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["BOAT_POSE_CONFIG","name","type","angles","landmarkA","landmarkB","landmarkC","phases","phase","angleName","minAngle","maxAngle","repSequence","formRules","message","severity","holdDurationMs"],"sourceRoot":"../../../src","sources":["config/boat-pose.ts"],"mappings":";;AAEA,OAAO,MAAMA,gBAAgC,GAAG;EAC9CC,IAAI,EAAE,sBAAsB;EAC5BC,IAAI,EAAE,MAAM;EACZC,MAAM,EAAE,CACN;
|
|
1
|
+
{"version":3,"names":["BOAT_POSE_CONFIG","name","type","postureFamily","visibilityThreshold","cameraAngle","angles","landmarkA","landmarkB","landmarkC","phases","phase","angleName","minAngle","maxAngle","repSequence","formRules","message","severity","holdDurationMs"],"sourceRoot":"../../../src","sources":["config/boat-pose.ts"],"mappings":";;AAEA,OAAO,MAAMA,gBAAgC,GAAG;EAC9CC,IAAI,EAAE,sBAAsB;EAC5BC,IAAI,EAAE,MAAM;EACZC,aAAa,EAAE,QAAQ;EACvBC,mBAAmB,EAAE,GAAG;EACxBC,WAAW,EAAE,MAAM;EACnBC,MAAM,EAAE,CACN;IAAEL,IAAI,EAAE,YAAY;IAAEM,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE;EAAG,CAAC,EACnE;IAAER,IAAI,EAAE,WAAW;IAAEM,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE,EAAE;IAAEC,SAAS,EAAE;EAAG,CAAC,CACnE;EACDC,MAAM,EAAE,CACN;IAAEC,KAAK,EAAE,MAAM;IAAEC,SAAS,EAAE,YAAY;IAAEC,QAAQ,EAAE,EAAE;IAAEC,QAAQ,EAAE;EAAI,CAAC,CACxE;EACDC,WAAW,EAAE,EAAE;EACfC,SAAS,EAAE,CACT;IACEf,IAAI,EAAE,cAAc;IACpBgB,OAAO,EAAE,6CAA6C;IACtDC,QAAQ,EAAE,SAAS;IACnBN,SAAS,EAAE,YAAY;IACvBC,QAAQ,EAAE,EAAE;IACZC,QAAQ,EAAE;EACZ,CAAC,EACD;IACEb,IAAI,EAAE,cAAc;IACpBgB,OAAO,EAAE,uBAAuB;IAChCC,QAAQ,EAAE,SAAS;IACnBN,SAAS,EAAE,WAAW;IACtBC,QAAQ,EAAE,GAAG;IACbC,QAAQ,EAAE;EACZ,CAAC,CACF;EACDK,cAAc,EAAE;AAClB,CAAC","ignoreList":[]}
|