Fix backward-bending joints in the five legacy motions
Abdominal's pinned hands used elbow -40 as the IK plane hint, drawing the arms hyperextended (user-reported). Flipping the hint bends the elbows the natural way while the hands stay on the handles. Same class of fix for the milder cases: Arm Curl and Shoulder Press elbows and Calfs knees clamp to -8, Side Plank's raised arm to the -70 ROM cap. The whole library now passes render.py --strict with zero warnings, making it a valid verification gate. Fixtures regenerated; 48 tests green. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
@@ -1,5 +1,7 @@
|
|||||||
**July 2026**
|
**July 2026**
|
||||||
|
|
||||||
|
The Abdominal figure's arms no longer bend backward at the elbow, and a few other form guides lose their subtly hyperextended joints.
|
||||||
|
|
||||||
The exercise library more than doubles — deadlifts, squats, lunges, bench presses, dumbbell work, pull-ups, and the cable stations — each with its own animated form guide.
|
The exercise library more than doubles — deadlifts, squats, lunges, bench presses, dumbbell work, pull-ups, and the cable stations — each with its own animated form guide.
|
||||||
|
|
||||||
Machine form guides now slowly circle around the figure too — seats, cables, and pads turn with the body, showing every exercise from all sides.
|
Machine form guides now slowly circle around the figure too — seats, cables, and pads turn with the body, showing every exercise from all sides.
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
"root": {"pos": [128, 126], "pitch": -4},
|
"root": {"pos": [128, 126], "pitch": -4},
|
||||||
"spine": [0, 4],
|
"spine": [0, 4],
|
||||||
"neck": 8,
|
"neck": 8,
|
||||||
"shoulder_r": 78, "elbow_r": -40,
|
"shoulder_r": 78, "elbow_r": 40,
|
||||||
"shoulder_l": 78, "elbow_l": -40,
|
"shoulder_l": 78, "elbow_l": 40,
|
||||||
"hip_r": 66, "knee_r": 70,
|
"hip_r": 66, "knee_r": 70,
|
||||||
"hip_l": 66, "knee_l": 70,
|
"hip_l": 66, "knee_l": 70,
|
||||||
"pins": {"hand_r": [155, 62], "hand_l": [161, 64], "foot_r": [168, 150], "foot_l": [172, 150]}
|
"pins": {"hand_r": [155, 62], "hand_l": [161, 64], "foot_r": [168, 150], "foot_l": [172, 150]}
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
"root": {"pos": [128, 126], "pitch": 10},
|
"root": {"pos": [128, 126], "pitch": 10},
|
||||||
"spine": [0, 40],
|
"spine": [0, 40],
|
||||||
"neck": 18, "head": -26,
|
"neck": 18, "head": -26,
|
||||||
"shoulder_r": 128, "elbow_r": -40,
|
"shoulder_r": 128, "elbow_r": 40,
|
||||||
"shoulder_l": 128, "elbow_l": -40,
|
"shoulder_l": 128, "elbow_l": 40,
|
||||||
"hip_r": 80, "knee_r": 70,
|
"hip_r": 80, "knee_r": 70,
|
||||||
"hip_l": 80, "knee_l": 70,
|
"hip_l": 80, "knee_l": 70,
|
||||||
"pins": {"hand_r": [190, 76], "hand_l": [196, 78], "foot_r": [168, 150], "foot_l": [172, 150]}
|
"pins": {"hand_r": [190, 76], "hand_l": [196, 78], "foot_r": [168, 150], "foot_l": [172, 150]}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
<path d="M 136.0 52.0 L 164.0 70.0 L 164.0 70.0 L 136.0 52.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M 136.0 52.0 L 164.0 70.0 L 164.0 70.0 L 136.0 52.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M 164.0 70.0 L 164.0 128.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M 164.0 70.0 L 164.0 128.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="leg_l" d="M 135.9 124.8 L 179.4 110.0 L 180.0 149.4 L 191.0 149.2" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="leg_l" d="M 135.9 124.8 L 179.4 110.0 L 180.0 149.4 L 191.0 149.2" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="arm_l" d="M 140.3 40.5 L 164.9 57.4 L 180.8 82.5" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="arm_l" d="M 140.3 40.5 L 164.9 57.4 L 186.9 77.6" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="girdle" d="M 140.3 40.5 L 134.4 40.4 L 134.4 42.3" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="girdle" d="M 140.3 40.5 L 134.4 40.4 L 134.4 42.3" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="pelvisBar" d="M 135.9 124.8 L 130.0 124.0 L 130.0 125.2" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="pelvisBar" d="M 135.9 124.8 L 130.0 124.0 L 130.0 125.2" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="spine" d="M 130.0 124.0 Q 130.8 81.1 134.4 40.4" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="spine" d="M 130.0 124.0 Q 130.8 81.1 134.4 40.4" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="leg_r" d="M 130.0 125.2 L 173.8 111.5 L 176.0 150.8 L 187.0 150.2" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="leg_r" d="M 130.0 125.2 L 173.8 111.5 L 176.0 150.8 L 187.0 150.2" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="arm_r" d="M 134.4 42.3 L 159.0 59.3 L 174.9 84.3" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="arm_r" d="M 134.4 42.3 L 159.0 59.3 L 180.9 79.4" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M 177.9 89.4 L 177.9 77.4" stroke="#6b7180" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M 183.9 84.5 L 183.9 72.5" stroke="#6b7180" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<circle id="head" cx="137.2" cy="20.9" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
|
<circle id="head" cx="137.2" cy="20.9" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
|
||||||
<line id="nose" x1="146.7" y1="24.0" x2="153.4" y2="26.1" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
|
<line id="nose" x1="146.7" y1="24.0" x2="153.4" y2="26.1" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
|
||||||
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
|
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -16,8 +16,8 @@
|
|||||||
"root": {"pos": [130, 124], "pitch": 2},
|
"root": {"pos": [130, 124], "pitch": 2},
|
||||||
"spine": [0, 2],
|
"spine": [0, 2],
|
||||||
"neck": 4, "head": 10,
|
"neck": 4, "head": 10,
|
||||||
"shoulder_r": 59, "elbow_r": -23,
|
"shoulder_r": 59, "elbow_r": -8,
|
||||||
"shoulder_l": 59, "elbow_l": -23,
|
"shoulder_l": 59, "elbow_l": -8,
|
||||||
"hip_r": 84, "knee_r": 64,
|
"hip_r": 84, "knee_r": 64,
|
||||||
"hip_l": 84, "knee_l": 64,
|
"hip_l": 84, "knee_l": 64,
|
||||||
"pins": {"foot_r": [176, 150], "foot_l": [180, 151]}
|
"pins": {"foot_r": [176, 150], "foot_l": [180, 151]}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 136 KiB |
@@ -33,8 +33,8 @@
|
|||||||
"neck": 16,
|
"neck": 16,
|
||||||
"shoulder_r": 44, "elbow_r": 25,
|
"shoulder_r": 44, "elbow_r": 25,
|
||||||
"shoulder_l": 44, "elbow_l": 25,
|
"shoulder_l": 44, "elbow_l": 25,
|
||||||
"hip_r": 66, "knee_r": -18, "ankle_r": -18,
|
"hip_r": 66, "knee_r": -8, "ankle_r": -18,
|
||||||
"hip_l": 66, "knee_l": -18, "ankle_l": -18,
|
"hip_l": 66, "knee_l": -8, "ankle_l": -18,
|
||||||
"pins": {"hand_r": [166, 74], "hand_l": [170, 76], "foot_r": [212, 139], "foot_l": [216, 141]}
|
"pins": {"hand_r": [166, 74], "hand_l": [170, 76], "foot_r": [212, 139], "foot_l": [216, 141]}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
<path d="M 126.0 143.0 L 126.0 151.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M 126.0 143.0 L 126.0 151.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M 110.0 151.0 L 142.0 151.0 L 142.0 151.0 L 110.0 151.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M 110.0 151.0 L 142.0 151.0 L 142.0 151.0 L 110.0 151.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="leg_l" d="M 125.9 130.8 L 167.4 111.1 L 168.0 150.5 L 179.0 150.3" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="leg_l" d="M 125.9 130.8 L 167.4 111.1 L 168.0 150.5 L 179.0 150.3" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="arm_l" d="M 101.0 50.1 L 118.7 26.2 L 142.9 8.9" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="arm_l" d="M 101.0 50.1 L 118.7 26.2 L 139.5 5.0" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="girdle" d="M 101.0 50.1 L 95.1 50.1 L 95.1 52.0" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="girdle" d="M 101.0 50.1 L 95.1 50.1 L 95.1 52.0" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="pelvisBar" d="M 125.9 130.8 L 120.0 130.0 L 120.0 131.2" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="pelvisBar" d="M 125.9 130.8 L 120.0 130.0 L 120.0 131.2" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="spine" d="M 120.0 130.0 Q 103.0 90.4 95.1 50.1" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="spine" d="M 120.0 130.0 Q 103.0 90.4 95.1 50.1" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="leg_r" d="M 120.0 131.2 L 161.9 112.6 L 164.0 151.9 L 175.0 151.3" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="leg_r" d="M 120.0 131.2 L 161.9 112.6 L 164.0 151.9 L 175.0 151.3" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="arm_r" d="M 95.1 52.0 L 112.8 28.1 L 137.0 10.7" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="arm_r" d="M 95.1 52.0 L 112.8 28.1 L 133.6 6.8" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M 130.0 9.8 L 150.0 9.8" stroke="#6b7180" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M 126.6 5.9 L 146.6 5.9" stroke="#6b7180" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<circle id="head" cx="98.6" cy="30.7" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
|
<circle id="head" cx="98.6" cy="30.7" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
|
||||||
<line id="nose" x1="108.5" y1="29.3" x2="115.4" y2="28.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
|
<line id="nose" x1="108.5" y1="29.3" x2="115.4" y2="28.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
|
||||||
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
|
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -28,8 +28,8 @@
|
|||||||
"root": {"pos": [120, 130], "pitch": -20},
|
"root": {"pos": [120, 130], "pitch": -20},
|
||||||
"spine": [0, 6],
|
"spine": [0, 6],
|
||||||
"neck": 24, "head": -18,
|
"neck": 24, "head": -18,
|
||||||
"shoulder_r": 130, "elbow_r": -18,
|
"shoulder_r": 130, "elbow_r": -8,
|
||||||
"shoulder_l": 130, "elbow_l": -18,
|
"shoulder_l": 130, "elbow_l": -8,
|
||||||
"hip_r": 45, "knee_r": 47,
|
"hip_r": 45, "knee_r": 47,
|
||||||
"hip_l": 45, "knee_l": 47,
|
"hip_l": 45, "knee_l": 47,
|
||||||
"pins": {"foot_r": [164, 151], "foot_l": [168, 152]}
|
"pins": {"foot_r": [164, 151], "foot_l": [168, 152]}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 148 KiB |
@@ -6,13 +6,13 @@
|
|||||||
<path d="M 126.0 143.0 L 126.0 151.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M 126.0 143.0 L 126.0 151.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M 110.0 151.0 L 142.0 151.0 L 142.0 151.0 L 110.0 151.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M 110.0 151.0 L 142.0 151.0 L 142.0 151.0 L 110.0 151.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="leg_l" d="M 125.9 130.8 L 167.4 111.1 L 168.0 150.5 L 179.0 150.3" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="leg_l" d="M 125.9 130.8 L 167.4 111.1 L 168.0 150.5 L 179.0 150.3" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="arm_l" d="M 101.0 50.1 L 118.7 26.2 L 142.9 8.9" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="arm_l" d="M 101.0 50.1 L 118.7 26.2 L 139.5 5.0" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="girdle" d="M 101.0 50.1 L 95.1 50.1 L 95.1 52.0" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="girdle" d="M 101.0 50.1 L 95.1 50.1 L 95.1 52.0" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="pelvisBar" d="M 125.9 130.8 L 120.0 130.0 L 120.0 131.2" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="pelvisBar" d="M 125.9 130.8 L 120.0 130.0 L 120.0 131.2" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="spine" d="M 120.0 130.0 Q 103.0 90.4 95.1 50.1" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="spine" d="M 120.0 130.0 Q 103.0 90.4 95.1 50.1" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="leg_r" d="M 120.0 131.2 L 161.9 112.6 L 164.0 151.9 L 175.0 151.3" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="leg_r" d="M 120.0 131.2 L 161.9 112.6 L 164.0 151.9 L 175.0 151.3" stroke="#3a3f4b" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="arm_r" d="M 95.1 52.0 L 112.8 28.1 L 137.0 10.7" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="arm_r" d="M 95.1 52.0 L 112.8 28.1 L 133.6 6.8" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M 130.0 9.8 L 150.0 9.8" stroke="#6b7180" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M 126.6 5.9 L 146.6 5.9" stroke="#6b7180" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<circle id="head" cx="98.6" cy="30.7" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
|
<circle id="head" cx="98.6" cy="30.7" r="10" fill="white" stroke="#3a3f4b" stroke-width="6"/>
|
||||||
<line id="nose" x1="108.5" y1="29.3" x2="115.4" y2="28.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
|
<line id="nose" x1="108.5" y1="29.3" x2="115.4" y2="28.3" stroke="#3a3f4b" stroke-width="4" stroke-linecap="round"/>
|
||||||
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
|
<g font-family="-apple-system, Helvetica, sans-serif" font-size="11" fill="#6b7180">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -2,7 +2,7 @@
|
|||||||
<title>Side Plank</title>
|
<title>Side Plank</title>
|
||||||
<path d="M 48.0 160.9 L 277.9 160.9 L 277.9 150.5 L 48.0 150.5 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
<path d="M 48.0 160.9 L 277.9 160.9 L 277.9 150.5 L 48.0 150.5 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||||
<path id="leg_r" d="M 179.9 137.8 L 225.2 145.6 L 264.6 152.5 L 262.7 163.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="leg_r" d="M 179.9 137.8 L 225.2 145.6 L 264.6 152.5 L 262.7 163.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="arm_r" d="M 96.2 122.5 L 101.4 93.4 L 106.6 64.3" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="arm_r" d="M 96.2 122.5 L 111.2 96.9 L 126.2 71.4" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="girdle" d="M 90.3 124.4 L 90.3 122.5 L 96.2 122.5" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="girdle" d="M 90.3 124.4 L 90.3 122.5 L 96.2 122.5" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="pelvisBar" d="M 174.0 138.2 L 174.0 137.0 L 179.9 137.8" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="pelvisBar" d="M 174.0 138.2 L 174.0 137.0 L 179.9 137.8" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="spine" d="M 174.0 137.0 Q 131.2 129.6 90.3 122.5" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="spine" d="M 174.0 137.0 Q 131.2 129.6 90.3 122.5" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -19,7 +19,7 @@
|
|||||||
"root": {"pos": [174, 137], "yaw": 180, "pitch": 80},
|
"root": {"pos": [174, 137], "yaw": 180, "pitch": 80},
|
||||||
"spine": [0, 0],
|
"spine": [0, 0],
|
||||||
"neck": 0, "head": -80,
|
"neck": 0, "head": -80,
|
||||||
"shoulder_r": -90, "elbow_r": 0,
|
"shoulder_r": -70, "elbow_r": 0,
|
||||||
"shoulder_l": 80, "elbow_l": 90,
|
"shoulder_l": 80, "elbow_l": 90,
|
||||||
"hip_r": 0, "knee_r": 0,
|
"hip_r": 0, "knee_r": 0,
|
||||||
"hip_l": 0, "knee_l": 0,
|
"hip_l": 0, "knee_l": 0,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 131 KiB |
@@ -2,7 +2,7 @@
|
|||||||
<title>Side Plank</title>
|
<title>Side Plank</title>
|
||||||
<path d="M 48.0 160.9 L 277.9 160.9 L 277.9 150.5 L 48.0 150.5 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
<path d="M 48.0 160.9 L 277.9 160.9 L 277.9 150.5 L 48.0 150.5 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||||
<path id="leg_r" d="M 179.9 137.8 L 225.2 145.6 L 264.6 152.5 L 262.7 163.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="leg_r" d="M 179.9 137.8 L 225.2 145.6 L 264.6 152.5 L 262.7 163.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="arm_r" d="M 96.2 122.5 L 101.4 93.4 L 106.6 64.3" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="arm_r" d="M 96.2 122.5 L 111.2 96.9 L 126.2 71.4" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="girdle" d="M 90.3 124.4 L 90.3 122.5 L 96.2 122.5" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="girdle" d="M 90.3 124.4 L 90.3 122.5 L 96.2 122.5" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="pelvisBar" d="M 174.0 138.2 L 174.0 137.0 L 179.9 137.8" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="pelvisBar" d="M 174.0 138.2 L 174.0 137.0 L 179.9 137.8" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path id="spine" d="M 174.0 137.0 Q 131.2 129.6 90.3 122.5" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
<path id="spine" d="M 174.0 137.0 Q 131.2 129.6 90.3 122.5" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -18,8 +18,8 @@
|
|||||||
"root": {"pos": [128, 126], "pitch": -4},
|
"root": {"pos": [128, 126], "pitch": -4},
|
||||||
"spine": [0, 4],
|
"spine": [0, 4],
|
||||||
"neck": 8,
|
"neck": 8,
|
||||||
"shoulder_r": 78, "elbow_r": -40,
|
"shoulder_r": 78, "elbow_r": 40,
|
||||||
"shoulder_l": 78, "elbow_l": -40,
|
"shoulder_l": 78, "elbow_l": 40,
|
||||||
"hip_r": 66, "knee_r": 70,
|
"hip_r": 66, "knee_r": 70,
|
||||||
"hip_l": 66, "knee_l": 70,
|
"hip_l": 66, "knee_l": 70,
|
||||||
"pins": {"hand_r": [155, 62], "hand_l": [161, 64], "foot_r": [168, 150], "foot_l": [172, 150]}
|
"pins": {"hand_r": [155, 62], "hand_l": [161, 64], "foot_r": [168, 150], "foot_l": [172, 150]}
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
"root": {"pos": [128, 126], "pitch": 10},
|
"root": {"pos": [128, 126], "pitch": 10},
|
||||||
"spine": [0, 40],
|
"spine": [0, 40],
|
||||||
"neck": 18, "head": -26,
|
"neck": 18, "head": -26,
|
||||||
"shoulder_r": 128, "elbow_r": -40,
|
"shoulder_r": 128, "elbow_r": 40,
|
||||||
"shoulder_l": 128, "elbow_l": -40,
|
"shoulder_l": 128, "elbow_l": 40,
|
||||||
"hip_r": 80, "knee_r": 70,
|
"hip_r": 80, "knee_r": 70,
|
||||||
"hip_l": 80, "knee_l": 70,
|
"hip_l": 80, "knee_l": 70,
|
||||||
"pins": {"hand_r": [190, 76], "hand_l": [196, 78], "foot_r": [168, 150], "foot_l": [172, 150]}
|
"pins": {"hand_r": [190, 76], "hand_l": [196, 78], "foot_r": [168, 150], "foot_l": [172, 150]}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
"root": {"pos": [130, 124], "pitch": 2},
|
"root": {"pos": [130, 124], "pitch": 2},
|
||||||
"spine": [0, 2],
|
"spine": [0, 2],
|
||||||
"neck": 4, "head": 10,
|
"neck": 4, "head": 10,
|
||||||
"shoulder_r": 59, "elbow_r": -23,
|
"shoulder_r": 59, "elbow_r": -8,
|
||||||
"shoulder_l": 59, "elbow_l": -23,
|
"shoulder_l": 59, "elbow_l": -8,
|
||||||
"hip_r": 84, "knee_r": 64,
|
"hip_r": 84, "knee_r": 64,
|
||||||
"hip_l": 84, "knee_l": 64,
|
"hip_l": 84, "knee_l": 64,
|
||||||
"pins": {"foot_r": [176, 150], "foot_l": [180, 151]}
|
"pins": {"foot_r": [176, 150], "foot_l": [180, 151]}
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
"neck": 16,
|
"neck": 16,
|
||||||
"shoulder_r": 44, "elbow_r": 25,
|
"shoulder_r": 44, "elbow_r": 25,
|
||||||
"shoulder_l": 44, "elbow_l": 25,
|
"shoulder_l": 44, "elbow_l": 25,
|
||||||
"hip_r": 66, "knee_r": -18, "ankle_r": -18,
|
"hip_r": 66, "knee_r": -8, "ankle_r": -18,
|
||||||
"hip_l": 66, "knee_l": -18, "ankle_l": -18,
|
"hip_l": 66, "knee_l": -8, "ankle_l": -18,
|
||||||
"pins": {"hand_r": [166, 74], "hand_l": [170, 76], "foot_r": [212, 139], "foot_l": [216, 141]}
|
"pins": {"hand_r": [166, 74], "hand_l": [170, 76], "foot_r": [212, 139], "foot_l": [216, 141]}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
"root": {"pos": [120, 130], "pitch": -20},
|
"root": {"pos": [120, 130], "pitch": -20},
|
||||||
"spine": [0, 6],
|
"spine": [0, 6],
|
||||||
"neck": 24, "head": -18,
|
"neck": 24, "head": -18,
|
||||||
"shoulder_r": 130, "elbow_r": -18,
|
"shoulder_r": 130, "elbow_r": -8,
|
||||||
"shoulder_l": 130, "elbow_l": -18,
|
"shoulder_l": 130, "elbow_l": -8,
|
||||||
"hip_r": 45, "knee_r": 47,
|
"hip_r": 45, "knee_r": 47,
|
||||||
"hip_l": 45, "knee_l": 47,
|
"hip_l": 45, "knee_l": 47,
|
||||||
"pins": {"foot_r": [164, 151], "foot_l": [168, 152]}
|
"pins": {"foot_r": [164, 151], "foot_l": [168, 152]}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"root": {"pos": [174, 137], "yaw": 180, "pitch": 80},
|
"root": {"pos": [174, 137], "yaw": 180, "pitch": 80},
|
||||||
"spine": [0, 0],
|
"spine": [0, 0],
|
||||||
"neck": 0, "head": -80,
|
"neck": 0, "head": -80,
|
||||||
"shoulder_r": -90, "elbow_r": 0,
|
"shoulder_r": -70, "elbow_r": 0,
|
||||||
"shoulder_l": 80, "elbow_l": 90,
|
"shoulder_l": 80, "elbow_l": 90,
|
||||||
"hip_r": 0, "knee_r": 0,
|
"hip_r": 0, "knee_r": 0,
|
||||||
"hip_l": 0, "knee_l": 0,
|
"hip_l": 0, "knee_l": 0,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ struct ExerciseLibraryView: View {
|
|||||||
ExerciseLibraryDetailView(exerciseName: name)
|
ExerciseLibraryDetailView(exerciseName: name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Exercises")
|
.navigationTitle("\(ExerciseMotionLibrary.exerciseNames.count) Exercises")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,12 +27,21 @@ struct ExerciseLibraryView: View {
|
|||||||
/// reference content (summary, targets, setup/execution/cues…), and the weight
|
/// reference content (summary, targets, setup/execution/cues…), and the weight
|
||||||
/// progression chart; the looping form-guide figure holds the bottom.
|
/// progression chart; the looping form-guide figure holds the bottom.
|
||||||
struct ExerciseLibraryDetailView: View {
|
struct ExerciseLibraryDetailView: View {
|
||||||
|
@Environment(SyncEngine.self) private var sync
|
||||||
|
|
||||||
let exerciseName: String
|
let exerciseName: String
|
||||||
|
|
||||||
/// This exercise wherever it appears in the user's splits — the source of the
|
/// This exercise wherever it appears in the user's splits — the source of the
|
||||||
/// recorded machine settings.
|
/// recorded machine settings.
|
||||||
@Query private var exercises: [Exercise]
|
@Query private var exercises: [Exercise]
|
||||||
|
|
||||||
|
/// Presents the settings editor; carries the splits the edit writes back to.
|
||||||
|
@State private var settingsEdit: SettingsEditRoute?
|
||||||
|
|
||||||
|
/// True while content extends below the scroll area — drives the bottom fade
|
||||||
|
/// that makes the reference copy read as scrollable.
|
||||||
|
@State private var moreBelow = false
|
||||||
|
|
||||||
/// One completed weighted log is enough to justify the progression chart;
|
/// One completed weighted log is enough to justify the progression chart;
|
||||||
/// bodyweight and timed exercises never accrue one, so they skip the chart
|
/// bodyweight and timed exercises never accrue one, so they skip the chart
|
||||||
/// instead of plotting a flat zero line.
|
/// instead of plotting a flat zero line.
|
||||||
@@ -53,18 +62,33 @@ struct ExerciseLibraryDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Distinct recorded machine-settings lists for this exercise. Usually one; when
|
/// Distinct recorded machine-settings lists for this exercise. Usually one; when
|
||||||
/// splits disagree, each distinct list keeps its split's name as a label.
|
/// splits disagree, each distinct list keeps its split's name as a label. Each
|
||||||
private var settingsGroups: [(label: String?, settings: [MachineSetting])] {
|
/// group carries every split showing that list, so an edit updates them all and
|
||||||
var groups: [(label: String?, settings: [MachineSetting])] = []
|
/// they stay deduped.
|
||||||
|
private var settingsGroups: [SettingsGroup] {
|
||||||
|
var groups: [SettingsGroup] = []
|
||||||
for exercise in exercises {
|
for exercise in exercises {
|
||||||
guard let settings = exercise.machineSettings, !settings.isEmpty else { continue }
|
guard let settings = exercise.machineSettings, !settings.isEmpty else { continue }
|
||||||
guard !groups.contains(where: { $0.settings == settings }) else { continue }
|
if let i = groups.firstIndex(where: { $0.settings == settings }) {
|
||||||
groups.append((exercise.split?.name, settings))
|
if let id = exercise.split?.id { groups[i].splitIDs.append(id) }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
groups.append(SettingsGroup(
|
||||||
|
label: exercise.split?.name,
|
||||||
|
settings: settings,
|
||||||
|
splitIDs: exercise.split.map { [$0.id] } ?? []
|
||||||
|
))
|
||||||
}
|
}
|
||||||
if groups.count == 1 { groups[0].label = nil }
|
if groups.count == 1 { groups[0].label = nil }
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every split containing this exercise — the write targets when settings are
|
||||||
|
/// recorded here for the first time.
|
||||||
|
private var containingSplitIDs: [String] {
|
||||||
|
exercises.compactMap { $0.split?.id }
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -73,9 +97,17 @@ struct ExerciseLibraryDetailView: View {
|
|||||||
// exercise the authored info identifies as machine-based, as an
|
// exercise the authored info identifies as machine-based, as an
|
||||||
// empty state, so the feature is discoverable before first use.
|
// empty state, so the feature is discoverable before first use.
|
||||||
if !settingsGroups.isEmpty {
|
if !settingsGroups.isEmpty {
|
||||||
MachineSettingsCard(groups: settingsGroups)
|
MachineSettingsCard(groups: settingsGroups, onEdit: edit(group:))
|
||||||
} else if info?.isMachineBased == true {
|
} else if info?.isMachineBased == true {
|
||||||
MachineSettingsCard(groups: [])
|
MachineSettingsCard(
|
||||||
|
groups: [],
|
||||||
|
onEdit: edit(group:),
|
||||||
|
// Recording needs a split exercise to write to — without
|
||||||
|
// one the card explains instead of offering a dead button.
|
||||||
|
onAdd: containingSplitIDs.isEmpty ? nil : {
|
||||||
|
settingsEdit = SettingsEditRoute(initial: [], targetSplitIDs: containingSplitIDs)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if let info {
|
if let info {
|
||||||
ExerciseInfoContent(info: info)
|
ExerciseInfoContent(info: info)
|
||||||
@@ -88,36 +120,105 @@ struct ExerciseLibraryDetailView: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
// Fade the last lines while more content lies below, so the reference
|
||||||
|
// copy visibly continues past the figure boundary; gone at scroll end.
|
||||||
|
.onScrollGeometryChange(for: Bool.self) { geo in
|
||||||
|
geo.contentOffset.y + geo.containerSize.height < geo.contentSize.height - 12
|
||||||
|
} action: { _, more in
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) { moreBelow = more }
|
||||||
|
}
|
||||||
|
.overlay(alignment: .bottom) {
|
||||||
|
if moreBelow {
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color(.systemBackground).opacity(0), Color(.systemBackground)],
|
||||||
|
startPoint: .top, endPoint: .bottom
|
||||||
|
)
|
||||||
|
.frame(height: 28)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ExerciseFigureSlot(exerciseName: exerciseName)
|
ExerciseFigureSlot(exerciseName: exerciseName)
|
||||||
}
|
}
|
||||||
.navigationTitle(exerciseName)
|
.navigationTitle(exerciseName)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.sheet(item: $settingsEdit) { route in
|
||||||
|
MachineSettingsSheet(exerciseName: exerciseName, initialSettings: route.initial) { updated in
|
||||||
|
let targets = route.targetSplitIDs
|
||||||
|
Task {
|
||||||
|
for splitID in targets {
|
||||||
|
await sync.writeBackMachineSettings(updated, exerciseName: exerciseName, splitID: splitID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func edit(group: SettingsGroup) {
|
||||||
|
settingsEdit = SettingsEditRoute(initial: group.settings, targetSplitIDs: group.splitIDs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One distinct recorded settings list and the splits it came from (the edit targets).
|
||||||
|
fileprivate struct SettingsGroup {
|
||||||
|
var label: String?
|
||||||
|
var settings: [MachineSetting]
|
||||||
|
var splitIDs: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sheet route: the settings to seed the editor with and the splits a save writes to.
|
||||||
|
private struct SettingsEditRoute: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
var initial: [MachineSetting]
|
||||||
|
var targetSplitIDs: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The user's recorded machine comfort settings, set apart from the reference text
|
/// The user's recorded machine comfort settings, set apart from the reference text
|
||||||
/// as a card: name–value rows, grouped per split when the recorded lists differ.
|
/// as a card: name–value rows, grouped per split when the recorded lists differ.
|
||||||
/// Empty `groups` renders the not-yet-recorded state (shown for machine-based
|
/// Empty `groups` renders the not-yet-recorded state (shown for machine-based
|
||||||
/// exercises so the feature is visible before first use).
|
/// exercises so the feature is visible before first use), with an Add button when
|
||||||
|
/// the exercise lives in at least one split (`onAdd` non-nil). A single group edits
|
||||||
|
/// from the header; disagreeing groups edit per group.
|
||||||
private struct MachineSettingsCard: View {
|
private struct MachineSettingsCard: View {
|
||||||
let groups: [(label: String?, settings: [MachineSetting])]
|
let groups: [SettingsGroup]
|
||||||
|
var onEdit: (SettingsGroup) -> Void
|
||||||
|
var onAdd: (() -> Void)? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Text("Your Machine Settings")
|
HStack {
|
||||||
.font(.headline)
|
Text("Your Machine Settings")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
if groups.count == 1 {
|
||||||
|
Button("Edit") { onEdit(groups[0]) }
|
||||||
|
.font(.subheadline)
|
||||||
|
} else if groups.isEmpty, let onAdd {
|
||||||
|
Button("Add") { onAdd() }
|
||||||
|
.font(.subheadline)
|
||||||
|
}
|
||||||
|
}
|
||||||
if groups.isEmpty {
|
if groups.isEmpty {
|
||||||
Text("None recorded yet. Mark the exercise as machine-based in its editor, or use the settings button on the workout screen.")
|
Text(onAdd != nil
|
||||||
|
? "None recorded yet — save the seat, pad, and pin positions once and they'll be here next time."
|
||||||
|
: "None recorded yet. Add this exercise to a split, then record its settings here or from the workout screen.")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
ForEach(Array(groups.enumerated()), id: \.offset) { _, group in
|
ForEach(Array(groups.enumerated()), id: \.offset) { _, group in
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
if let label = group.label {
|
if let label = group.label {
|
||||||
Text(label)
|
HStack {
|
||||||
.font(.caption)
|
Text(label)
|
||||||
.foregroundStyle(.secondary)
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
if groups.count > 1 {
|
||||||
|
Button("Edit") { onEdit(group) }
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ForEach(Array(group.settings.enumerated()), id: \.offset) { _, setting in
|
ForEach(Array(group.settings.enumerated()), id: \.offset) { _, setting in
|
||||||
HStack(alignment: .firstTextBaseline) {
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
|||||||
@@ -79,9 +79,12 @@ struct WorkoutLogListView: View {
|
|||||||
Section {
|
Section {
|
||||||
LabeledContent("Started", value: doc.start.formattedRelativeDateTime())
|
LabeledContent("Started", value: doc.start.formattedRelativeDateTime())
|
||||||
LabeledContent("Status") {
|
LabeledContent("Status") {
|
||||||
|
// Emphasize the states that mean the workout amounted to
|
||||||
|
// something — a running or finished one — over notStarted.
|
||||||
|
let emphasized = statusEnum == .inProgress || statusEnum == .completed
|
||||||
Text(statusEnum.displayName)
|
Text(statusEnum.displayName)
|
||||||
.fontWeight(statusEnum == .inProgress ? .bold : nil)
|
.fontWeight(emphasized ? .bold : nil)
|
||||||
.foregroundStyle(statusEnum == .inProgress ? Color.accentColor : Color.secondary)
|
.foregroundStyle(emphasized ? Color.accentColor : Color.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +404,17 @@ private struct WorkoutLogRow: View {
|
|||||||
|
|
||||||
private var showsWeight: Bool { loadType == .weight }
|
private var showsWeight: Bool { loadType == .weight }
|
||||||
|
|
||||||
|
/// Compact glanceable summary of the recorded machine settings ("Seat 4 · Back 3")
|
||||||
|
/// so the values are readable while setting up the machine, without opening the
|
||||||
|
/// sheet. Nil when nothing is recorded yet — the button then reads "Settings".
|
||||||
|
private var settingsSummary: String? {
|
||||||
|
guard let settings = log.machineSettings, !settings.isEmpty else { return nil }
|
||||||
|
let parts = settings
|
||||||
|
.map { "\($0.name) \($0.value)".trimmingCharacters(in: .whitespaces) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
return parts.isEmpty ? nil : parts.joined(separator: " · ")
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Button {
|
Button {
|
||||||
@@ -443,9 +457,11 @@ private struct WorkoutLogRow: View {
|
|||||||
if log.machineSettings != nil
|
if log.machineSettings != nil
|
||||||
|| ExerciseInfoLibrary.info(for: log.exerciseName)?.isMachineBased == true {
|
|| ExerciseInfoLibrary.info(for: log.exerciseName)?.isMachineBased == true {
|
||||||
Button(action: onSettingsTap) {
|
Button(action: onSettingsTap) {
|
||||||
Label("Settings", systemImage: "slider.horizontal.3")
|
Label(settingsSummary ?? "Settings", systemImage: "slider.horizontal.3")
|
||||||
.font(.footnote.weight(.medium))
|
.font(.footnote.weight(.medium))
|
||||||
.foregroundStyle(Color.accentColor)
|
.foregroundStyle(Color.accentColor)
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
.lineLimit(2)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|||||||