Give machine props world-space 3D form that rotates with the camera
Scene shapes, cable anchors, bar angles, pad perpendiculars, and roller offsets all resolve in the authored view exactly as before, then rotate about the world-vertical axis through the root anchor - the same resolve-then-rotate pattern as the figure's pins and the mat - so at the authored yaw every exercise renders bit-identically to today, and under an orbiting camera the equipment turns with the figure while staying welded to its hands and feet. Scene lines gain an optional depth plane (z) and slab extrusion (depth) so seats, backrests, and platforms keep form edge-on; the rect shape is retired (re-authored as slab lines). All 14 machines' props re-authored with depths and verified at eight orbit angles. The fixture snapshots move into the pipeline as render.py --fixtures and now cover orbit-presentation samples with resolved prop primitives for a spread of prop flavors; the in-app renderer resolves props in MotionSolver (lockstep with resolve_props) and the view just draws primitives. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
@@ -1,11 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Abdominal</title>
|
||||
<path d="M 105.8 160.8 L 213.8 160.8 L 213.8 150.3 L 105.8 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 120.0 128.0 L 114.0 62.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 130.0 L 150.0 130.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 120.0 128.0 L 114.0 62.0 L 114.0 62.0 L 120.0 128.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 130.0 L 150.0 130.0 L 150.0 130.0 L 106.0 130.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 132.0 L 122.0 150.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 102.0 150.0 L 150.0 150.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 156.0 149.0 L 190.0 149.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 102.0 150.0 L 150.0 150.0 L 150.0 150.0 L 102.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 156.0 149.0 L 190.0 149.0 L 190.0 149.0 L 156.0 149.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 133.9 126.8 L 176.4 109.3 L 172.0 148.4 L 182.9 149.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 130.9 42.5 L 159.5 33.6 L 161.0 63.1" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 130.9 42.5 L 125.0 42.4 L 125.0 44.3" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1,11 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Abdominal</title>
|
||||
<path d="M 105.8 160.8 L 213.8 160.8 L 213.8 150.3 L 105.8 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 120.0 128.0 L 114.0 62.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 130.0 L 150.0 130.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 120.0 128.0 L 114.0 62.0 L 114.0 62.0 L 120.0 128.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 130.0 L 150.0 130.0 L 150.0 130.0 L 106.0 130.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 132.0 L 122.0 150.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 102.0 150.0 L 150.0 150.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 156.0 149.0 L 190.0 149.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 102.0 150.0 L 150.0 150.0 L 150.0 150.0 L 102.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 156.0 149.0 L 190.0 149.0 L 190.0 149.0 L 156.0 149.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 133.9 126.8 L 176.4 109.3 L 172.0 148.4 L 182.9 149.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 173.5 57.8 L 201.8 47.9 L 196.0 76.8" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 173.5 57.8 L 167.6 57.7 L 167.6 59.6" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -4,11 +4,11 @@
|
||||
"working": ["spine"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[120, 128], [114, 62]], "w": 8},
|
||||
{"kind": "line", "pts": [[106, 130], [150, 130]], "w": 8},
|
||||
{"kind": "line", "pts": [[120, 128], [114, 62]], "w": 8, "depth": 7},
|
||||
{"kind": "line", "pts": [[106, 130], [150, 130]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[122, 132], [122, 150]], "w": 4},
|
||||
{"kind": "line", "pts": [[102, 150], [150, 150]], "w": 4},
|
||||
{"kind": "line", "pts": [[156, 149], [190, 149]], "w": 5}
|
||||
{"kind": "line", "pts": [[102, 150], [150, 150]], "w": 4, "depth": 5},
|
||||
{"kind": "line", "pts": [[156, 149], [190, 149]], "w": 5, "depth": 6}
|
||||
]},
|
||||
{"type": "pad", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 14, "w": 6}
|
||||
],
|
||||
|
||||
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 177 KiB |
@@ -1,11 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Abdominal</title>
|
||||
<path d="M 105.8 160.8 L 213.8 160.8 L 213.8 150.3 L 105.8 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 120.0 128.0 L 114.0 62.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 130.0 L 150.0 130.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 120.0 128.0 L 114.0 62.0 L 114.0 62.0 L 120.0 128.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 130.0 L 150.0 130.0 L 150.0 130.0 L 106.0 130.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 132.0 L 122.0 150.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 102.0 150.0 L 150.0 150.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 156.0 149.0 L 190.0 149.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 102.0 150.0 L 150.0 150.0 L 150.0 150.0 L 102.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 156.0 149.0 L 190.0 149.0 L 190.0 149.0 L 156.0 149.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 133.9 126.8 L 176.4 109.3 L 172.0 148.4 L 182.9 149.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 173.5 57.8 L 201.8 47.9 L 196.0 76.8" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 173.5 57.8 L 167.6 57.7 L 167.6 59.6" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Abductor</title>
|
||||
<path d="M 130.0 145.6 L 130.0 165.2 L 190.0 165.2 L 190.0 145.6 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<rect x="152" y="56" width="16" height="72" rx="5" fill="#c5cad4"/>
|
||||
<rect x="130" y="126" width="60" height="10" rx="3" fill="#c5cad4"/>
|
||||
<path d="M 160.0 64.0 L 160.0 120.0 L 160.0 120.0 L 160.0 64.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 135.0 131.0 L 185.0 131.0 L 185.0 131.0 L 135.0 131.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 136.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 171.0 32.3 L 160.0 32.3 L 149.0 32.3" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="pelvisBar" d="M 167.0 116.0 L 160.0 116.0 L 153.0 116.0" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Abductor</title>
|
||||
<path d="M 130.0 145.6 L 130.0 165.2 L 190.0 165.2 L 190.0 145.6 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<rect x="152" y="56" width="16" height="72" rx="5" fill="#c5cad4"/>
|
||||
<rect x="130" y="126" width="60" height="10" rx="3" fill="#c5cad4"/>
|
||||
<path d="M 160.0 64.0 L 160.0 120.0 L 160.0 120.0 L 160.0 64.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 135.0 131.0 L 185.0 131.0 L 185.0 131.0 L 135.0 131.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 136.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 171.0 32.3 L 160.0 32.3 L 149.0 32.3" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="pelvisBar" d="M 167.0 116.0 L 160.0 116.0 L 153.0 116.0" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -5,8 +5,8 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "rect", "x": 152, "y": 56, "w": 16, "h": 72, "r": 5},
|
||||
{"kind": "rect", "x": 130, "y": 126, "w": 60, "h": 10, "r": 3},
|
||||
{"kind": "line", "pts": [[160, 64], [160, 120]], "w": 16, "z": -14, "depth": 6},
|
||||
{"kind": "line", "pts": [[135, 131], [185, 131]], "w": 10, "depth": 12},
|
||||
{"kind": "line", "pts": [[160, 136], [160, 150]], "w": 6}
|
||||
]},
|
||||
{"type": "pad", "at": ["knee_r"], "angle": 90, "halfLen": 13, "w": 6},
|
||||
|
||||
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 176 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Abductor</title>
|
||||
<path d="M 130.0 145.6 L 130.0 165.2 L 190.0 165.2 L 190.0 145.6 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<rect x="152" y="56" width="16" height="72" rx="5" fill="#c5cad4"/>
|
||||
<rect x="130" y="126" width="60" height="10" rx="3" fill="#c5cad4"/>
|
||||
<path d="M 160.0 64.0 L 160.0 120.0 L 160.0 120.0 L 160.0 64.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 135.0 131.0 L 185.0 131.0 L 185.0 131.0 L 135.0 131.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 136.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 171.0 32.3 L 160.0 32.3 L 149.0 32.3" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="pelvisBar" d="M 167.0 116.0 L 160.0 116.0 L 153.0 116.0" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Adductor</title>
|
||||
<path d="M 130.0 145.6 L 130.0 165.2 L 190.0 165.2 L 190.0 145.6 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<rect x="152" y="56" width="16" height="72" rx="5" fill="#c5cad4"/>
|
||||
<rect x="130" y="126" width="60" height="10" rx="3" fill="#c5cad4"/>
|
||||
<path d="M 160.0 64.0 L 160.0 120.0 L 160.0 120.0 L 160.0 64.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 135.0 131.0 L 185.0 131.0 L 185.0 131.0 L 135.0 131.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 136.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 171.0 32.3 L 160.0 32.3 L 149.0 32.3" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="pelvisBar" d="M 167.0 116.0 L 160.0 116.0 L 153.0 116.0" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Adductor</title>
|
||||
<path d="M 130.0 145.6 L 130.0 165.2 L 190.0 165.2 L 190.0 145.6 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<rect x="152" y="56" width="16" height="72" rx="5" fill="#c5cad4"/>
|
||||
<rect x="130" y="126" width="60" height="10" rx="3" fill="#c5cad4"/>
|
||||
<path d="M 160.0 64.0 L 160.0 120.0 L 160.0 120.0 L 160.0 64.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 135.0 131.0 L 185.0 131.0 L 185.0 131.0 L 135.0 131.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 136.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 171.0 32.3 L 160.0 32.3 L 149.0 32.3" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="pelvisBar" d="M 167.0 116.0 L 160.0 116.0 L 153.0 116.0" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -5,8 +5,8 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "rect", "x": 152, "y": 56, "w": 16, "h": 72, "r": 5},
|
||||
{"kind": "rect", "x": 130, "y": 126, "w": 60, "h": 10, "r": 3},
|
||||
{"kind": "line", "pts": [[160, 64], [160, 120]], "w": 16, "z": -14, "depth": 6},
|
||||
{"kind": "line", "pts": [[135, 131], [185, 131]], "w": 10, "depth": 12},
|
||||
{"kind": "line", "pts": [[160, 136], [160, 150]], "w": 6}
|
||||
]},
|
||||
{"type": "pad", "at": ["knee_r"], "angle": 90, "halfLen": 13, "w": 6},
|
||||
|
||||
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 176 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Adductor</title>
|
||||
<path d="M 130.0 145.6 L 130.0 165.2 L 190.0 165.2 L 190.0 145.6 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<rect x="152" y="56" width="16" height="72" rx="5" fill="#c5cad4"/>
|
||||
<rect x="130" y="126" width="60" height="10" rx="3" fill="#c5cad4"/>
|
||||
<path d="M 160.0 64.0 L 160.0 120.0 L 160.0 120.0 L 160.0 64.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 135.0 131.0 L 185.0 131.0 L 185.0 131.0 L 135.0 131.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 136.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 171.0 32.3 L 160.0 32.3 L 149.0 32.3" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="pelvisBar" d="M 167.0 116.0 L 160.0 116.0 L 153.0 116.0" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Arm Curl</title>
|
||||
<path d="M 115.2 160.7 L 203.0 160.7 L 203.0 150.3 L 115.2 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 150.0 134.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 136.0 52.0 L 164.0 70.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 150.0 134.0 L 150.0 134.0 L 116.0 134.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 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"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Arm Curl</title>
|
||||
<path d="M 115.2 160.7 L 203.0 160.7 L 203.0 150.3 L 115.2 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 150.0 134.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 136.0 52.0 L 164.0 70.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 150.0 134.0 L 150.0 134.0 L 116.0 134.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 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 184.2 34.8" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -4,8 +4,8 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[116, 134], [150, 134]], "w": 8},
|
||||
{"kind": "line", "pts": [[136, 52], [164, 70]], "w": 8},
|
||||
{"kind": "line", "pts": [[116, 134], [150, 134]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[136, 52], [164, 70]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[164, 70], [164, 128]], "w": 5}
|
||||
]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 6}
|
||||
|
||||
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 145 KiB |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Arm Curl</title>
|
||||
<path d="M 115.2 160.7 L 203.0 160.7 L 203.0 150.3 L 115.2 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 150.0 134.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 136.0 52.0 L 164.0 70.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 150.0 134.0 L 150.0 134.0 L 116.0 134.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 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 184.2 34.8" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Calfs</title>
|
||||
<path d="M 109.6 160.6 L 238.3 160.6 L 238.3 150.2 L 109.6 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 124.0 126.0 L 158.0 124.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 124.0 126.0 L 158.0 124.0 L 158.0 124.0 L 124.0 126.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 128.0 L 138.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 120.0 150.0 L 156.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="198" y="147" width="34" height="6" rx="1" fill="#c5cad4"/>
|
||||
<path d="M 120.0 150.0 L 156.0 150.0 L 156.0 150.0 L 120.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 201.0 150.0 L 229.0 150.0 L 229.0 150.0 L 201.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 183.0 62.0 L 183.0 122.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 164.0 76.0 L 183.0 76.0" stroke="#6b7180" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 145.9 118.8 L 191.9 116.9 L 216.0 148.3 L 223.2 140.1" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Calfs</title>
|
||||
<path d="M 109.6 160.6 L 238.3 160.6 L 238.3 150.2 L 109.6 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 124.0 126.0 L 158.0 124.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 124.0 126.0 L 158.0 124.0 L 158.0 124.0 L 124.0 126.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 128.0 L 138.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 120.0 150.0 L 156.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="198" y="147" width="34" height="6" rx="1" fill="#c5cad4"/>
|
||||
<path d="M 120.0 150.0 L 156.0 150.0 L 156.0 150.0 L 120.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 201.0 150.0 L 229.0 150.0 L 229.0 150.0 L 201.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 183.0 62.0 L 183.0 122.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 164.0 76.0 L 183.0 76.0" stroke="#6b7180" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 145.9 118.8 L 190.8 108.9 L 216.0 139.5 L 226.3 135.6" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -4,10 +4,10 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[124, 126], [158, 124]], "w": 9},
|
||||
{"kind": "line", "pts": [[124, 126], [158, 124]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[138, 128], [138, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[120, 150], [156, 150]], "w": 5},
|
||||
{"kind": "rect", "x": 198, "y": 147, "w": 34, "h": 6, "r": 1},
|
||||
{"kind": "line", "pts": [[120, 150], [156, 150]], "w": 5, "depth": 5},
|
||||
{"kind": "line", "pts": [[201, 150], [229, 150]], "w": 6, "depth": 7},
|
||||
{"kind": "line", "pts": [[183, 62], [183, 122]], "w": 4},
|
||||
{"kind": "line", "pts": [[164, 76], [183, 76]], "w": 3, "color": "prop"}
|
||||
]},
|
||||
|
||||
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 154 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Calfs</title>
|
||||
<path d="M 109.6 160.6 L 238.3 160.6 L 238.3 150.2 L 109.6 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 124.0 126.0 L 158.0 124.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 124.0 126.0 L 158.0 124.0 L 158.0 124.0 L 124.0 126.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 128.0 L 138.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 120.0 150.0 L 156.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="198" y="147" width="34" height="6" rx="1" fill="#c5cad4"/>
|
||||
<path d="M 120.0 150.0 L 156.0 150.0 L 156.0 150.0 L 120.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 201.0 150.0 L 229.0 150.0 L 229.0 150.0 L 201.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 183.0 62.0 L 183.0 122.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 164.0 76.0 L 183.0 76.0" stroke="#6b7180" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 145.9 118.8 L 190.8 108.9 L 216.0 139.5 L 226.3 135.6" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Chest Press</title>
|
||||
<path d="M 78.4 160.7 L 184.9 160.7 L 184.9 150.3 L 78.4 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 114.0 132.0 L 86.0 40.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 133.0 L 146.0 133.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 114.0 132.0 L 86.0 40.0 L 86.0 40.0 L 114.0 132.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 133.0 L 146.0 133.0 L 146.0 133.0 L 106.0 133.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 135.0 L 122.0 151.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 151.0 L 138.0 151.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 151.0 L 138.0 151.0 L 138.0 151.0 L 108.0 151.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 121.9 122.8 L 166.4 111.2 L 162.0 150.4 L 172.9 151.5" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 104.2 40.2 L 99.0 69.3 L 122.0 50.3" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 104.2 40.2 L 98.3 40.2 L 98.3 42.1" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Chest Press</title>
|
||||
<path d="M 78.4 160.7 L 184.9 160.7 L 184.9 150.3 L 78.4 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 114.0 132.0 L 86.0 40.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 133.0 L 146.0 133.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 114.0 132.0 L 86.0 40.0 L 86.0 40.0 L 114.0 132.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 133.0 L 146.0 133.0 L 146.0 133.0 L 106.0 133.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 135.0 L 122.0 151.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 151.0 L 138.0 151.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 151.0 L 138.0 151.0 L 138.0 151.0 L 108.0 151.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 121.9 122.8 L 166.4 111.2 L 162.0 150.4 L 172.9 151.5" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 104.2 40.2 L 134.0 43.3 L 163.2 50.5" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 104.2 40.2 L 98.3 40.2 L 98.3 42.1" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -4,10 +4,10 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[114, 132], [86, 40]], "w": 8},
|
||||
{"kind": "line", "pts": [[106, 133], [146, 133]], "w": 8},
|
||||
{"kind": "line", "pts": [[114, 132], [86, 40]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[106, 133], [146, 133]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[122, 135], [122, 151]], "w": 5},
|
||||
{"kind": "line", "pts": [[108, 151], [138, 151]], "w": 4}
|
||||
{"kind": "line", "pts": [[108, 151], [138, 151]], "w": 4, "depth": 5}
|
||||
]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 8}
|
||||
],
|
||||
|
||||
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Chest Press</title>
|
||||
<path d="M 78.4 160.7 L 184.9 160.7 L 184.9 150.3 L 78.4 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 114.0 132.0 L 86.0 40.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 133.0 L 146.0 133.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 114.0 132.0 L 86.0 40.0 L 86.0 40.0 L 114.0 132.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 106.0 133.0 L 146.0 133.0 L 146.0 133.0 L 106.0 133.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 135.0 L 122.0 151.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 151.0 L 138.0 151.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 151.0 L 138.0 151.0 L 138.0 151.0 L 108.0 151.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 121.9 122.8 L 166.4 111.2 L 162.0 150.4 L 172.9 151.5" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 104.2 40.2 L 134.0 43.3 L 163.2 50.5" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 104.2 40.2 L 98.3 40.2 L 98.3 42.1" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1,7 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Lat Pull Down</title>
|
||||
<path d="M 127.5 160.7 L 220.0 160.7 L 220.0 150.3 L 127.5 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 130.0 L 162.0 130.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 130.0 L 162.0 130.0 L 162.0 130.0 L 126.0 130.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 144.0 132.0 L 144.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="272.0" cy="19.0" r="4" fill="#6b7180"/>
|
||||
<path d="M 272.0 19.0 L 196.0 3.8" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,7 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Lat Pull Down</title>
|
||||
<path d="M 127.5 160.7 L 220.0 160.7 L 220.0 150.3 L 127.5 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 130.0 L 162.0 130.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 130.0 L 162.0 130.0 L 162.0 130.0 L 126.0 130.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 144.0 132.0 L 144.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="272.0" cy="19.0" r="4" fill="#6b7180"/>
|
||||
<path d="M 272.0 19.0 L 196.0 37.3" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -4,7 +4,7 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[126, 130], [162, 130]], "w": 8},
|
||||
{"kind": "line", "pts": [[126, 130], [162, 130]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[144, 132], [144, 150]], "w": 5},
|
||||
{"kind": "circle", "c": [272, 19], "r": 4, "fill": true, "color": "prop"}
|
||||
]},
|
||||
|
||||
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 161 KiB |
@@ -1,7 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Lat Pull Down</title>
|
||||
<path d="M 127.5 160.7 L 220.0 160.7 L 220.0 150.3 L 127.5 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 130.0 L 162.0 130.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 130.0 L 162.0 130.0 L 162.0 130.0 L 126.0 130.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 144.0 132.0 L 144.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="272.0" cy="19.0" r="4" fill="#6b7180"/>
|
||||
<path d="M 272.0 19.0 L 196.0 37.3" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,12 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Leg Curl</title>
|
||||
<path d="M 75.2 160.6 L 221.1 160.6 L 221.1 150.2 L 75.2 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 130.0 105.0 L 158.0 106.0" stroke="#c5cad4" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0 L 86.0 48.0 L 110.0 122.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0 L 166.0 120.0 L 104.0 124.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 130.0 105.0 L 158.0 106.0 L 158.0 106.0 L 130.0 105.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 126.0 L 108.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 124.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0 L 166.0 150.0 L 104.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="114.0" cy="90.0" r="3.5" fill="#6b7180"/>
|
||||
<path id="leg_l" d="M 123.9 116.8 L 169.2 108.9 L 209.1 106.1 L 208.3 95.3" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 97.6 36.5 L 105.4 65.0 L 120.4 90.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -1,12 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Leg Curl</title>
|
||||
<path d="M 75.2 160.6 L 221.1 160.6 L 221.1 150.2 L 75.2 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 130.0 105.0 L 158.0 106.0" stroke="#c5cad4" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0 L 86.0 48.0 L 110.0 122.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0 L 166.0 120.0 L 104.0 124.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 130.0 105.0 L 158.0 106.0 L 158.0 106.0 L 130.0 105.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 126.0 L 108.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 124.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0 L 166.0 150.0 L 104.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="114.0" cy="90.0" r="3.5" fill="#6b7180"/>
|
||||
<path id="leg_l" d="M 123.9 116.8 L 169.2 108.9 L 165.7 148.1 L 176.7 149.1" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 97.6 36.5 L 105.4 65.0 L 120.4 90.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -4,12 +4,12 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9},
|
||||
{"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9},
|
||||
{"kind": "line", "pts": [[130, 105], [158, 106]], "w": 7},
|
||||
{"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[130, 105], [158, 106]], "w": 7, "depth": 7},
|
||||
{"kind": "line", "pts": [[108, 126], [108, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[160, 124], [160, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5, "depth": 5},
|
||||
{"kind": "circle", "c": [114, 90], "r": 3.5, "fill": true, "color": "prop"}
|
||||
]},
|
||||
{"type": "roller", "at": ["foot_r", "foot_l"], "side": -1, "r": 5, "back": 3}
|
||||
|
||||
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 176 KiB |
@@ -1,12 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Leg Curl</title>
|
||||
<path d="M 75.2 160.6 L 221.1 160.6 L 221.1 150.2 L 75.2 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 130.0 105.0 L 158.0 106.0" stroke="#c5cad4" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0 L 86.0 48.0 L 110.0 122.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0 L 166.0 120.0 L 104.0 124.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 130.0 105.0 L 158.0 106.0 L 158.0 106.0 L 130.0 105.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 126.0 L 108.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 124.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0 L 166.0 150.0 L 104.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="114.0" cy="90.0" r="3.5" fill="#6b7180"/>
|
||||
<path id="leg_l" d="M 123.9 116.8 L 169.2 108.9 L 165.7 148.1 L 176.7 149.1" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 97.6 36.5 L 105.4 65.0 L 120.4 90.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -1,11 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Leg Extension</title>
|
||||
<path d="M 75.2 160.6 L 221.1 160.6 L 221.1 150.2 L 75.2 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0 L 86.0 48.0 L 110.0 122.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0 L 166.0 120.0 L 104.0 124.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 126.0 L 108.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 124.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0 L 166.0 150.0 L 104.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="114.0" cy="90.0" r="3.5" fill="#6b7180"/>
|
||||
<path id="leg_l" d="M 123.9 116.8 L 169.2 108.9 L 174.1 148.0 L 185.0 146.7" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 97.6 36.5 L 105.4 65.0 L 120.4 90.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1,11 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Leg Extension</title>
|
||||
<path d="M 75.2 160.6 L 221.1 160.6 L 221.1 150.2 L 75.2 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0 L 86.0 48.0 L 110.0 122.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0 L 166.0 120.0 L 104.0 124.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 126.0 L 108.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 124.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0 L 166.0 150.0 L 104.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="114.0" cy="90.0" r="3.5" fill="#6b7180"/>
|
||||
<path id="leg_l" d="M 123.9 116.8 L 169.2 108.9 L 209.1 106.1 L 208.3 95.3" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 97.6 36.5 L 105.4 65.0 L 120.4 90.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -4,11 +4,11 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9},
|
||||
{"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9},
|
||||
{"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[108, 126], [108, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[160, 124], [160, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5, "depth": 5},
|
||||
{"kind": "circle", "c": [114, 90], "r": 3.5, "fill": true, "color": "prop"}
|
||||
]},
|
||||
{"type": "roller", "at": ["foot_r", "foot_l"], "side": 1, "r": 5, "back": 7}
|
||||
|
||||
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 142 KiB |
@@ -1,11 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Leg Extension</title>
|
||||
<path d="M 75.2 160.6 L 221.1 160.6 L 221.1 150.2 L 75.2 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 122.0 L 86.0 48.0 L 86.0 48.0 L 110.0 122.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 124.0 L 166.0 120.0 L 166.0 120.0 L 104.0 124.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 126.0 L 108.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 124.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 104.0 150.0 L 166.0 150.0 L 166.0 150.0 L 104.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="114.0" cy="90.0" r="3.5" fill="#6b7180"/>
|
||||
<path id="leg_l" d="M 123.9 116.8 L 169.2 108.9 L 209.1 106.1 L 208.3 95.3" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 97.6 36.5 L 105.4 65.0 L 120.4 90.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Leg Press</title>
|
||||
<path d="M 88.6 160.6 L 238.5 160.6 L 238.5 150.2 L 88.6 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 134.0 123.0 L 96.0 36.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 127.0 L 156.0 122.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 134.0 123.0 L 96.0 36.0 L 96.0 36.0 L 134.0 123.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 127.0 L 156.0 122.0 L 156.0 122.0 L 126.0 127.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 130.0 L 138.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 118.0 150.0 L 158.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 118.0 150.0 L 158.0 150.0 L 158.0 150.0 L 118.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 142.0 90.0 L 142.0 78.0" stroke="#c5cad4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="142.0" cy="77.0" r="3.5" fill="#6b7180"/>
|
||||
<path id="leg_l" d="M 145.9 116.8 L 161.7 74.2 L 189.0 103.0 L 197.0 95.6" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Leg Press</title>
|
||||
<path d="M 88.6 160.6 L 238.5 160.6 L 238.5 150.2 L 88.6 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 134.0 123.0 L 96.0 36.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 127.0 L 156.0 122.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 134.0 123.0 L 96.0 36.0 L 96.0 36.0 L 134.0 123.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 127.0 L 156.0 122.0 L 156.0 122.0 L 126.0 127.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 130.0 L 138.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 118.0 150.0 L 158.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 118.0 150.0 L 158.0 150.0 L 158.0 150.0 L 118.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 142.0 90.0 L 142.0 78.0" stroke="#c5cad4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="142.0" cy="77.0" r="3.5" fill="#6b7180"/>
|
||||
<path id="leg_l" d="M 145.9 116.8 L 185.1 93.0 L 224.0 102.0 L 226.5 91.5" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -4,10 +4,10 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[134, 123], [96, 36]], "w": 9},
|
||||
{"kind": "line", "pts": [[126, 127], [156, 122]], "w": 9},
|
||||
{"kind": "line", "pts": [[134, 123], [96, 36]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[126, 127], [156, 122]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[138, 130], [138, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[118, 150], [158, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[118, 150], [158, 150]], "w": 5, "depth": 5},
|
||||
{"kind": "line", "pts": [[142, 90], [142, 78]], "w": 3},
|
||||
{"kind": "circle", "c": [142, 77], "r": 3.5, "fill": true, "color": "prop"}
|
||||
]},
|
||||
|
||||
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 180 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Leg Press</title>
|
||||
<path d="M 88.6 160.6 L 238.5 160.6 L 238.5 150.2 L 88.6 150.2 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 134.0 123.0 L 96.0 36.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 127.0 L 156.0 122.0" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 134.0 123.0 L 96.0 36.0 L 96.0 36.0 L 134.0 123.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 126.0 127.0 L 156.0 122.0 L 156.0 122.0 L 126.0 127.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 130.0 L 138.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 118.0 150.0 L 158.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 118.0 150.0 L 158.0 150.0 L 158.0 150.0 L 118.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 142.0 90.0 L 142.0 78.0" stroke="#c5cad4" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="142.0" cy="77.0" r="3.5" fill="#6b7180"/>
|
||||
<path id="leg_l" d="M 145.9 116.8 L 185.1 93.0 L 224.0 102.0 L 226.5 91.5" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -1,9 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Rotary</title>
|
||||
<path d="M 130.0 144.5 L 130.0 166.5 L 190.0 166.5 L 190.0 144.5 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 130.0 L 182.0 130.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 130.0 L 182.0 130.0 L 182.0 130.0 L 138.0 130.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 132.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 144.0 150.0 L 176.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 144.0 150.0 L 176.0 150.0 L 176.0 150.0 L 144.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 167.1 36.8 L 160.0 38.3 L 152.9 39.8" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="pelvisBar" d="M 167.0 122.0 L 160.0 122.0 L 153.0 122.0" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="spine" d="M 160.0 122.0 Q 160.0 79.2 160.0 38.3" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -1,9 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Rotary</title>
|
||||
<path d="M 130.0 144.5 L 130.0 166.5 L 190.0 166.5 L 190.0 144.5 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 130.0 L 182.0 130.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 130.0 L 182.0 130.0 L 182.0 130.0 L 138.0 130.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 132.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 144.0 150.0 L 176.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 144.0 150.0 L 176.0 150.0 L 176.0 150.0 L 144.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 167.1 39.8 L 160.0 38.3 L 152.9 36.8" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="pelvisBar" d="M 167.0 122.0 L 160.0 122.0 L 153.0 122.0" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="spine" d="M 160.0 122.0 Q 160.0 79.2 160.0 38.3" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -5,9 +5,9 @@
|
||||
"working": ["spine"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[138, 130], [182, 130]], "w": 8},
|
||||
{"kind": "line", "pts": [[138, 130], [182, 130]], "w": 8, "depth": 10},
|
||||
{"kind": "line", "pts": [[160, 132], [160, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[144, 150], [176, 150]], "w": 5}
|
||||
{"kind": "line", "pts": [[144, 150], [176, 150]], "w": 5, "depth": 5}
|
||||
]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 14}
|
||||
],
|
||||
|
||||
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 149 KiB |
@@ -1,9 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Rotary</title>
|
||||
<path d="M 130.0 144.5 L 130.0 166.5 L 190.0 166.5 L 190.0 144.5 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 130.0 L 182.0 130.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 138.0 130.0 L 182.0 130.0 L 182.0 130.0 L 138.0 130.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 160.0 132.0 L 160.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 144.0 150.0 L 176.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 144.0 150.0 L 176.0 150.0 L 176.0 150.0 L 144.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 167.1 36.8 L 160.0 38.3 L 152.9 39.8" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="pelvisBar" d="M 167.0 122.0 L 160.0 122.0 L 153.0 122.0" stroke="#0d9488" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="spine" d="M 160.0 122.0 Q 160.0 79.2 160.0 38.3" stroke="#0d9488" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -132,8 +132,8 @@ scale drawings of the machine.
|
||||
```json
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[134, 123], [96, 36]], "w": 9},
|
||||
{"kind": "rect", "x": 54, "y": 104, "w": 40, "h": 8, "r": 3},
|
||||
{"kind": "line", "pts": [[134, 123], [96, 36]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[160, 64], [160, 120]], "w": 16, "z": -14},
|
||||
{"kind": "circle", "c": [142, 77], "r": 3.5, "fill": true, "color": "prop"}
|
||||
]},
|
||||
{"type": "cable", "from": [190, 8], "to": ["hand_r", "hand_l"]},
|
||||
@@ -143,23 +143,37 @@ scale drawings of the machine.
|
||||
]
|
||||
```
|
||||
|
||||
- **`scene`** — static shapes in canvas coordinates: `line` (polyline, stroke
|
||||
width `w`), `circle` (`fill: false` for an outline), `rect` (filled, corner
|
||||
radius `r`). A shape may set `"color": "prop"` to use the darker
|
||||
attached-item gray (e.g. a fixed handle the hands rest on). Scene shapes
|
||||
are view-locked billboards — they don't rotate with `--orbit`.
|
||||
- **`cable`** — a thin line from a fixed anchor `from` to a moving joint `to`;
|
||||
the machine's pulley line.
|
||||
Props are authored in the authored view but have **world-space 3D form**:
|
||||
under an orbiting camera the whole equipment layer rotates about the
|
||||
world-vertical axis through the root anchor, exactly like the figure and the
|
||||
mat. Everything resolves in the *authored* view — scene points, cable
|
||||
anchors, bar angles, pad perpendiculars, roller offsets — and the resolved
|
||||
constructs are then rotated; joint-attached items keep following the rotated
|
||||
figure's hands and feet, so equipment stays welded to the body. At the
|
||||
authored yaw nothing moves, so the authored look is exact.
|
||||
|
||||
- **`scene`** — static shapes: `line` (polyline, stroke width `w`) and
|
||||
`circle` (`fill: false` for an outline). Points are canvas `[x, y]` (or
|
||||
`[x, y, z]` where needed); a shape-level `"z"` sets its depth plane
|
||||
(positive toward the camera), and a line with `"depth"` is a **slab** —
|
||||
extruded that half-width through its plane, it stays a plain line edge-on
|
||||
and opens into a swept quad as the camera orbits (seats, backrests,
|
||||
platforms). A shape may set `"color": "prop"` to use the darker
|
||||
attached-item gray (e.g. a fixed handle the hands rest on).
|
||||
- **`cable`** — a thin line from a fixed anchor `from` (`[x, y]` or
|
||||
`[x, y, z]`) to a moving joint `to`; the machine's pulley line.
|
||||
- **`bar` / `dumbbell` / `pad`** — a segment centered on the joint(s) in
|
||||
`at` (a single joint, or the midpoint of a list). Joints are the extremities
|
||||
(`hand_r`, `foot_l`, …) plus the mid joints (`elbow_r`, `knee_l`, …), so a
|
||||
machine pad can ride a knee (`["knee_r", "knee_l"]`) or span a shin
|
||||
(`["knee_r", "foot_r"]`).
|
||||
`bar` lies at a fixed world `angle` (default 0 = horizontal — in side view a
|
||||
two-handed bar is drawn horizontal by convention); `dumbbell` and `pad`
|
||||
default to perpendicular to the lower bone (forearm/shin), or take an
|
||||
explicit `angle`. `plateR` puts filled discs on both ends (dumbbells default
|
||||
to 4.5). A prop whose limb is hidden that frame simply isn't drawn.
|
||||
`bar` lies at a fixed authored-view `angle` (default 0 = horizontal — in
|
||||
side view a two-handed bar is drawn horizontal by convention); `dumbbell`
|
||||
and `pad` default to perpendicular to the lower bone (forearm/shin), or
|
||||
take an explicit `angle`. Under orbit the segment rotates with the scene
|
||||
and foreshortens naturally. `plateR` puts filled discs on both ends
|
||||
(dumbbells default to 4.5). A prop whose limb is hidden that frame simply
|
||||
isn't drawn.
|
||||
- **`roller`** — a machine roller pad seen end-on: a filled disc riding the
|
||||
limb's lower bone near the joint in `at`, on the `side` (+1/−1) of the bone
|
||||
it presses — a leg extension's instep roller (`side: 1`), a leg curl's
|
||||
@@ -179,6 +193,7 @@ python3 render.py --figure=female # render with another skeleton profile
|
||||
python3 render.py --flip # view from the other side (camera + 180°)
|
||||
python3 render.py --strict # fail on any ROM violation, listing each
|
||||
python3 render.py --export # bake app resources into Workouts/Resources/ExerciseMotions
|
||||
python3 render.py --fixtures # regenerate WorkoutsTests/Fixtures/figure-fixtures.json
|
||||
```
|
||||
|
||||
`render.py` needs only Pillow (for GIFs/sheets; the SVGs have no dependency).
|
||||
@@ -192,6 +207,8 @@ the `--export` copies ship: `skeleton.json` plus uniquely-named
|
||||
and the exercise-library reference screen (`ExerciseInfo.swift` parses the
|
||||
info pages). The in-app solver is a line-for-line port of `kinematics.py`,
|
||||
held to it by `WorkoutsTests/Fixtures/figure-fixtures.json` — projected
|
||||
geometry snapshots the Swift solver must reproduce; regenerate them alongside
|
||||
any pipeline change. Re-run `python3 render.py --export` after editing any
|
||||
motion or info page; the library stays the source of truth.
|
||||
geometry snapshots (figure and prop primitives, including orbit-presentation
|
||||
samples) the Swift solver must reproduce; regenerate with
|
||||
`python3 render.py --fixtures` alongside any pipeline change. Re-run
|
||||
`python3 render.py --export` after editing any motion or info page; the
|
||||
library stays the source of truth.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Seated Row</title>
|
||||
<path d="M 118.3 160.7 L 216.0 160.7 L 216.0 150.3 L 118.3 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 132.0 L 156.0 132.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 159.0 46.0 L 157.0 96.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 176.0 150.0 L 206.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 132.0 L 156.0 132.0 L 156.0 132.0 L 122.0 132.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 159.0 46.0 L 157.0 96.0 L 157.0 96.0 L 159.0 46.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 176.0 150.0 L 206.0 150.0 L 206.0 150.0 L 176.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 244.0 120.0 L 202.0 63.9" stroke="#c5cad4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 145.9 122.8 L 189.4 108.1 L 192.0 147.4 L 203.0 146.7" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 155.5 38.9 L 174.0 62.2 L 204.0 63.0" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1,9 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Seated Row</title>
|
||||
<path d="M 118.3 160.7 L 216.0 160.7 L 216.0 150.3 L 118.3 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 132.0 L 156.0 132.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 159.0 46.0 L 157.0 96.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 176.0 150.0 L 206.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 132.0 L 156.0 132.0 L 156.0 132.0 L 122.0 132.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 159.0 46.0 L 157.0 96.0 L 157.0 96.0 L 159.0 46.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 176.0 150.0 L 206.0 150.0 L 206.0 150.0 L 176.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 244.0 120.0 L 160.0 73.8" stroke="#c5cad4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 145.9 122.8 L 189.4 108.1 L 192.0 147.4 L 203.0 146.7" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 155.5 38.9 L 134.8 60.3 L 162.0 72.8" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -4,9 +4,9 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[122, 132], [156, 132]], "w": 8},
|
||||
{"kind": "line", "pts": [[159, 46], [157, 96]], "w": 8},
|
||||
{"kind": "line", "pts": [[176, 150], [206, 150]], "w": 5}
|
||||
{"kind": "line", "pts": [[122, 132], [156, 132]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[159, 46], [157, 96]], "w": 8, "depth": 7},
|
||||
{"kind": "line", "pts": [[176, 150], [206, 150]], "w": 5, "depth": 6}
|
||||
]},
|
||||
{"type": "cable", "from": [244, 120], "to": ["hand_r", "hand_l"]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 7}
|
||||
|
||||
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 181 KiB |
@@ -1,9 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Seated Row</title>
|
||||
<path d="M 118.3 160.7 L 216.0 160.7 L 216.0 150.3 L 118.3 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 132.0 L 156.0 132.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 159.0 46.0 L 157.0 96.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 176.0 150.0 L 206.0 150.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 122.0 132.0 L 156.0 132.0 L 156.0 132.0 L 122.0 132.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 159.0 46.0 L 157.0 96.0 L 157.0 96.0 L 159.0 46.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 176.0 150.0 L 206.0 150.0 L 206.0 150.0 L 176.0 150.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 244.0 120.0 L 160.0 73.8" stroke="#c5cad4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="leg_l" d="M 145.9 122.8 L 189.4 108.1 L 192.0 147.4 L 203.0 146.7" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 155.5 38.9 L 134.8 60.3 L 162.0 72.8" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Shoulder Press</title>
|
||||
<path d="M 76.6 160.8 L 191.0 160.8 L 191.0 150.4 L 76.6 150.4 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 140.0 L 88.0 50.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 141.0 L 148.0 141.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 140.0 L 88.0 50.0 L 88.0 50.0 L 116.0 140.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 141.0 L 148.0 141.0 L 148.0 141.0 L 110.0 141.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" 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" 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="arm_l" d="M 101.0 50.1 L 116.0 75.7 L 116.0 46.2" 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"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Shoulder Press</title>
|
||||
<path d="M 76.6 160.8 L 191.0 160.8 L 191.0 150.4 L 76.6 150.4 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 140.0 L 88.0 50.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 141.0 L 148.0 141.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 140.0 L 88.0 50.0 L 88.0 50.0 L 116.0 140.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 141.0 L 148.0 141.0 L 148.0 141.0 L 110.0 141.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" 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" 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="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="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"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -4,10 +4,10 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[116, 140], [88, 50]], "w": 8},
|
||||
{"kind": "line", "pts": [[110, 141], [148, 141]], "w": 8},
|
||||
{"kind": "line", "pts": [[116, 140], [88, 50]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[110, 141], [148, 141]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[126, 143], [126, 151]], "w": 5},
|
||||
{"kind": "line", "pts": [[110, 151], [142, 151]], "w": 4}
|
||||
{"kind": "line", "pts": [[110, 151], [142, 151]], "w": 4, "depth": 5}
|
||||
]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "halfLen": 10}
|
||||
],
|
||||
|
||||
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 148 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Shoulder Press</title>
|
||||
<path d="M 76.6 160.8 L 191.0 160.8 L 191.0 150.4 L 76.6 150.4 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 140.0 L 88.0 50.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 141.0 L 148.0 141.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 140.0 L 88.0 50.0 L 88.0 50.0 L 116.0 140.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 141.0 L 148.0 141.0 L 148.0 141.0 L 110.0 141.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" 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" 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="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="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"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Tricep Press</title>
|
||||
<path d="M 82.7 160.7 L 182.8 160.7 L 182.8 150.3 L 82.7 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 92.0 42.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 135.0 L 146.0 135.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 92.0 42.0 L 92.0 42.0 L 116.0 134.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 135.0 L 146.0 135.0 L 146.0 135.0 L 108.0 135.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 124.0 137.0 L 124.0 151.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 151.0 L 140.0 151.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 151.0 L 140.0 151.0 L 140.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 123.9 124.8 L 168.0 111.8 L 160.0 150.4 L 170.8 152.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 110.6 41.4 L 98.5 68.4 L 128.0 63.0" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 110.6 41.4 L 104.7 41.3 L 104.7 43.2" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Tricep Press</title>
|
||||
<path d="M 82.7 160.7 L 182.8 160.7 L 182.8 150.3 L 82.7 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 92.0 42.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 135.0 L 146.0 135.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 92.0 42.0 L 92.0 42.0 L 116.0 134.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 135.0 L 146.0 135.0 L 146.0 135.0 L 108.0 135.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 124.0 137.0 L 124.0 151.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 151.0 L 140.0 151.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 151.0 L 140.0 151.0 L 140.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 123.9 124.8 L 168.0 111.8 L 160.0 150.4 L 170.8 152.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 110.6 41.4 L 113.2 70.8 L 128.0 96.5" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 110.6 41.4 L 104.7 41.3 L 104.7 43.2" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -4,10 +4,10 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[116, 134], [92, 42]], "w": 8},
|
||||
{"kind": "line", "pts": [[108, 135], [146, 135]], "w": 8},
|
||||
{"kind": "line", "pts": [[116, 134], [92, 42]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[108, 135], [146, 135]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[124, 137], [124, 151]], "w": 5},
|
||||
{"kind": "line", "pts": [[110, 151], [140, 151]], "w": 4}
|
||||
{"kind": "line", "pts": [[110, 151], [140, 151]], "w": 4, "depth": 5}
|
||||
]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 6}
|
||||
],
|
||||
|
||||
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 156 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180" fill="none">
|
||||
<title>Tricep Press</title>
|
||||
<path d="M 82.7 160.7 L 182.8 160.7 L 182.8 150.3 L 82.7 150.3 Z" fill="none" stroke="#b9bec9" stroke-width="3" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 92.0 42.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 135.0 L 146.0 135.0" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 116.0 134.0 L 92.0 42.0 L 92.0 42.0 L 116.0 134.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 108.0 135.0 L 146.0 135.0 L 146.0 135.0 L 108.0 135.0 Z" fill="#c5cad4" stroke="#c5cad4" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 124.0 137.0 L 124.0 151.0" stroke="#c5cad4" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 151.0 L 140.0 151.0" stroke="#c5cad4" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 110.0 151.0 L 140.0 151.0 L 140.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 123.9 124.8 L 168.0 111.8 L 160.0 150.4 L 170.8 152.6" stroke="#a9afba" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="arm_l" d="M 110.6 41.4 L 113.2 70.8 L 128.0 96.5" stroke="#86cfc5" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="girdle" d="M 110.6 41.4 L 104.7 41.3 L 104.7 43.2" stroke="#3a3f4b" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 302 KiB |
@@ -13,9 +13,11 @@ Outputs per exercise: frames/frame-N.svg, preview.gif (tweened, looping),
|
||||
visual.svg (the primary frame). `--sheet` writes contact-sheet.png of every
|
||||
key frame; `--demo` writes demo-sheet.png showing rig customizations (body
|
||||
profiles, flipped camera, theme); `--orbit` writes orbit.gif per named
|
||||
exercise (the camera sweeps 360 degrees while the motion loops). `--export`
|
||||
instead bakes each motion down to the legacy planar schema consumed by the
|
||||
in-app SwiftUI renderer. SVGs need no dependencies; GIFs/sheets need Pillow.
|
||||
exercise (the camera sweeps 360 degrees while the motion loops — props
|
||||
rotate with the figure). `--export` copies the app's bundled resources
|
||||
verbatim; `--fixtures` regenerates the projected-geometry snapshots pinning
|
||||
the in-app Swift solver to this pipeline. SVGs need no dependencies;
|
||||
GIFs/sheets need Pillow.
|
||||
"""
|
||||
|
||||
import copy
|
||||
@@ -227,24 +229,35 @@ def timeline(norms, fps=20):
|
||||
# figure in the recessive equipment gray; joint-attached items (`bar`,
|
||||
# `dumbbell`, `pad`) draw over the limbs in the darker prop gray, following
|
||||
# the resolved hand/foot positions frame by frame.
|
||||
#
|
||||
# Props have world-space 3D form: everything is authored (and resolved) in the
|
||||
# authored view — scene points and cable anchors carry an optional depth `z`
|
||||
# (+ toward the camera), a scene line an optional `depth` extrusion half-width
|
||||
# that makes it a slab — then rotated about the world-vertical axis through
|
||||
# the root anchor for the orbiting presentation, exactly like the figure and
|
||||
# the mat. Positions that follow the figure (joint anchors) come from the
|
||||
# *rotated* geometry; fixed points, directions, and offsets (scene shapes,
|
||||
# cable anchors, bar angles, pad perpendiculars, roller offsets) resolve in
|
||||
# the *authored* view and rotate — never recompute them in the rotated view.
|
||||
|
||||
def flip_props(props, width):
|
||||
"""Mirror the props horizontally, matching the flipped camera. Joint-
|
||||
attached props follow the mirrored limbs automatically; only fixed
|
||||
coordinates and world angles need mirroring."""
|
||||
"""Mirror the props horizontally, matching the flipped camera (which views
|
||||
from the opposite side, so authored depths flip sign too). Joint-attached
|
||||
props follow the mirrored limbs automatically; only fixed coordinates,
|
||||
depths, and world angles need mirroring."""
|
||||
def fx(p):
|
||||
return [width - p[0], p[1]]
|
||||
return [width - p[0], p[1], -p[2]] if len(p) > 2 else [width - p[0], p[1]]
|
||||
out = []
|
||||
for prop in props:
|
||||
p = json.loads(json.dumps(prop))
|
||||
if p["type"] == "scene":
|
||||
for s in p["shapes"]:
|
||||
if "z" in s:
|
||||
s["z"] = -s["z"]
|
||||
if s["kind"] == "line":
|
||||
s["pts"] = [fx(pt) for pt in s["pts"]]
|
||||
elif s["kind"] == "circle":
|
||||
s["c"] = fx(s["c"])
|
||||
elif s["kind"] == "rect":
|
||||
s["x"] = width - s["x"] - s["w"]
|
||||
elif p["type"] == "cable":
|
||||
p["from"] = fx(p["from"])
|
||||
elif p["type"] in ("bar", "pad") and "angle" in p:
|
||||
@@ -255,6 +268,16 @@ def flip_props(props, width):
|
||||
return out
|
||||
|
||||
|
||||
def prop_rotation(pitch, yaw_offset):
|
||||
"""View-space rotation carrying authored-view prop geometry to the camera
|
||||
`yaw_offset` degrees past the authored yaw: a rotation about the world-
|
||||
vertical axis through the root anchor, conjugated by the camera elevation.
|
||||
The identity at offset 0, so the authored view stays bit-exact."""
|
||||
if yaw_offset == 0:
|
||||
return K.IDENTITY
|
||||
return K.chain(K.rot_x(pitch), K.rot_y(-yaw_offset), K.rot_x(-pitch))
|
||||
|
||||
|
||||
def joint_points(geo, ref):
|
||||
"""Resolve a joint ref — `"hand_r"`, `"knee_l"`, or a midpoint list like
|
||||
`["knee_r", "foot_r"]` — to (point, unit direction of the bone ending at
|
||||
@@ -275,45 +298,90 @@ def joint_points(geo, ref):
|
||||
sum(p[1] for p in pts) / len(pts)), direction)
|
||||
|
||||
|
||||
def resolve_props(props, geo):
|
||||
"""Props -> drawable primitives for one frame: (background, foreground)."""
|
||||
def resolve_props(props, geo, anchor, rot=K.IDENTITY, auth_geo=None):
|
||||
"""Props -> drawable primitives for one frame: (background, foreground).
|
||||
|
||||
`geo` is the frame's drawn (possibly orbit-rotated) geometry, `auth_geo`
|
||||
the same frame at the authored camera (defaults to `geo`), `anchor` the
|
||||
frame's root canvas anchor, and `rot` the `prop_rotation` between them.
|
||||
Joint positions come from `geo`; everything authored — scene points,
|
||||
cable anchors, bar angles, pad perpendiculars, roller offsets — resolves
|
||||
against `auth_geo` and rotates through `rot`.
|
||||
"""
|
||||
auth = auth_geo if auth_geo is not None else geo
|
||||
|
||||
def place(pt, z=0.0):
|
||||
"""Authored canvas point (+ depth toward the camera) -> drawn canvas."""
|
||||
v = K.mvec(rot, (pt[0] - anchor[0], anchor[1] - pt[1], z))
|
||||
return (anchor[0] + v[0], anchor[1] - v[1])
|
||||
|
||||
def swing(vec, z=0.0):
|
||||
"""Authored canvas-space direction/offset -> drawn canvas (y-down)."""
|
||||
v = K.mvec(rot, (vec[0], -vec[1], z))
|
||||
return (v[0], -v[1])
|
||||
|
||||
bg, fg = [], []
|
||||
for p in props or []:
|
||||
t = p["type"]
|
||||
if t == "scene":
|
||||
for s in p["shapes"]:
|
||||
bg.append(dict(s, color=s.get("color", "equipment")))
|
||||
color = s.get("color", "equipment")
|
||||
z = s.get("z", 0.0)
|
||||
if s["kind"] == "line":
|
||||
depth = s.get("depth", 0.0)
|
||||
pts = [(pt[0], pt[1], z + (pt[2] if len(pt) > 2 else 0.0))
|
||||
for pt in s["pts"]]
|
||||
if depth:
|
||||
# An extruded slab: the polyline swept through +/-depth,
|
||||
# filled and outlined so it degenerates to the plain
|
||||
# line whenever the sweep collapses edge-on.
|
||||
quad = ([place(pt, pt[2] + depth) for pt in pts]
|
||||
+ [place(pt, pt[2] - depth) for pt in reversed(pts)])
|
||||
bg.append({"kind": "poly", "pts": [list(q) for q in quad],
|
||||
"w": s.get("w", 4), "color": color})
|
||||
else:
|
||||
bg.append({"kind": "line",
|
||||
"pts": [list(place(pt, pt[2])) for pt in pts],
|
||||
"w": s.get("w", 4), "color": color})
|
||||
elif s["kind"] == "circle":
|
||||
bg.append({"kind": "circle", "c": list(place(s["c"], z)),
|
||||
"r": s["r"], "fill": s.get("fill", False),
|
||||
"w": s.get("w", 3), "color": color})
|
||||
elif t == "cable":
|
||||
end, _ = joint_points(geo, p["to"])
|
||||
if end:
|
||||
bg.append({"kind": "line", "pts": [list(p["from"]), list(end)],
|
||||
start = place(p["from"], p["from"][2] if len(p["from"]) > 2 else 0.0)
|
||||
bg.append({"kind": "line", "pts": [list(start), list(end)],
|
||||
"w": p.get("w", 2), "color": "equipment"})
|
||||
elif t == "roller":
|
||||
# A machine roller pad seen end-on: a disc riding the limb's lower
|
||||
# bone near the joint, on the `side` (+1/-1) of the bone it presses.
|
||||
c, d = joint_points(geo, p["at"])
|
||||
if not c:
|
||||
# The offset resolves along the authored-view bone, then rotates.
|
||||
c, _ = joint_points(geo, p["at"])
|
||||
_, d = joint_points(auth, p["at"])
|
||||
if not c or not d:
|
||||
continue
|
||||
r = p.get("r", 5)
|
||||
back = p.get("back", 0)
|
||||
side = p.get("side", 1)
|
||||
px, py = d[1] * side, -d[0] * side
|
||||
center = (c[0] - d[0] * back + px * (r + 3),
|
||||
c[1] - d[1] * back + py * (r + 3))
|
||||
fg.append({"kind": "circle", "c": list(center), "r": r,
|
||||
off = swing((-d[0] * back + px * (r + 3), -d[1] * back + py * (r + 3)))
|
||||
fg.append({"kind": "circle", "c": [c[0] + off[0], c[1] + off[1]], "r": r,
|
||||
"fill": True, "color": "prop"})
|
||||
elif t in ("bar", "dumbbell", "pad"):
|
||||
c, d = joint_points(geo, p["at"])
|
||||
if not c:
|
||||
c, _ = joint_points(geo, p["at"])
|
||||
_, d = joint_points(auth, p["at"])
|
||||
if not c or not d:
|
||||
continue
|
||||
if t == "bar" or "angle" in p:
|
||||
ux, uy = dirv(p.get("angle", 0)) # fixed world angle
|
||||
axis = dirv(p.get("angle", 0)) # fixed authored-view angle
|
||||
else:
|
||||
ux, uy = -d[1], d[0] # perpendicular to the lower bone
|
||||
axis = (-d[1], d[0]) # perpendicular to the lower bone
|
||||
ux, uy = swing(axis) # foreshortens under orbit
|
||||
h = p.get("halfLen", {"bar": 24, "dumbbell": 7, "pad": 8}[t])
|
||||
a = (c[0] - ux * h, c[1] - uy * h)
|
||||
b = (c[0] + ux * h, c[1] + uy * h)
|
||||
fg.append({"kind": "line", "pts": [a, b],
|
||||
fg.append({"kind": "line", "pts": [list(a), list(b)],
|
||||
"w": p.get("w", {"bar": 4, "dumbbell": 3, "pad": 7}[t]),
|
||||
"color": "prop"})
|
||||
plate = p.get("plateR", 4.5 if t == "dumbbell" else 0)
|
||||
@@ -332,6 +400,11 @@ def svg_prims(prims, colors):
|
||||
d = "M " + " L ".join(f"{x:.1f} {y:.1f}" for x, y in p["pts"])
|
||||
lines.append(f' <path d="{d}" stroke="{color}" stroke-width="{p.get("w", 4)}"'
|
||||
f' stroke-linecap="round" stroke-linejoin="round"/>')
|
||||
elif p["kind"] == "poly":
|
||||
d = "M " + " L ".join(f"{x:.1f} {y:.1f}" for x, y in p["pts"]) + " Z"
|
||||
lines.append(f' <path d="{d}" fill="{color}" stroke="{color}"'
|
||||
f' stroke-width="{p.get("w", 4)}"'
|
||||
f' stroke-linecap="round" stroke-linejoin="round"/>')
|
||||
elif p["kind"] == "circle":
|
||||
cx, cy = p["c"]
|
||||
if p.get("fill"):
|
||||
@@ -339,17 +412,17 @@ def svg_prims(prims, colors):
|
||||
else:
|
||||
lines.append(f' <circle cx="{cx:.1f}" cy="{cy:.1f}" r="{p["r"]}"'
|
||||
f' stroke="{color}" stroke-width="{p.get("w", 3)}"/>')
|
||||
elif p["kind"] == "rect":
|
||||
lines.append(f' <rect x="{p["x"]}" y="{p["y"]}" width="{p["w"]}" height="{p["h"]}"'
|
||||
f' rx="{p.get("r", 2)}" fill="{colors[p.get("color", "equipment")]}"/>')
|
||||
return lines
|
||||
|
||||
|
||||
def draw_prims(d, prims, colors, scale):
|
||||
for p in prims:
|
||||
color = colors[p["color"]]
|
||||
if p["kind"] == "line":
|
||||
if p["kind"] in ("line", "poly"):
|
||||
pts = [(x * scale, y * scale) for x, y in p["pts"]]
|
||||
if p["kind"] == "poly":
|
||||
d.polygon(pts, fill=color)
|
||||
pts = pts + pts[:1]
|
||||
w = p.get("w", 4) * scale
|
||||
d.line(pts, fill=color, width=w, joint="curve")
|
||||
for x, y in (pts[0], pts[-1]):
|
||||
@@ -362,10 +435,6 @@ def draw_prims(d, prims, colors, scale):
|
||||
else:
|
||||
d.ellipse([cx - r, cy - r, cx + r, cy + r], outline=color,
|
||||
width=p.get("w", 3) * scale)
|
||||
elif p["kind"] == "rect":
|
||||
x, y = p["x"] * scale, p["y"] * scale
|
||||
d.rounded_rectangle([x, y, x + p["w"] * scale, y + p["h"] * scale],
|
||||
radius=p.get("r", 2) * scale, fill=color)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- drawing
|
||||
@@ -400,8 +469,8 @@ def quad_points(p0, ctrl, p2, n=24):
|
||||
return pts
|
||||
|
||||
|
||||
def svg_for_frame(name, geo, order, shade, working, colors, props=None):
|
||||
bg, fg = resolve_props(props, geo)
|
||||
def svg_for_frame(name, geo, order, shade, working, colors, prims=((), ())):
|
||||
bg, fg = prims
|
||||
w, h = CANVAS
|
||||
parts = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" fill="none">',
|
||||
f' <title>{name}</title>',
|
||||
@@ -443,10 +512,10 @@ def svg_for_frame(name, geo, order, shade, working, colors, props=None):
|
||||
return "\n".join(parts) + "\n"
|
||||
|
||||
|
||||
def draw_geo(geo, order, shade, working, colors, scale=2, font=None, props=None):
|
||||
def draw_geo(geo, order, shade, working, colors, scale=2, font=None, prims=((), ())):
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
bg, fg = resolve_props(props, geo)
|
||||
bg, fg = prims
|
||||
w, h = CANVAS[0] * scale, CANVAS[1] * scale
|
||||
img = Image.new("RGB", (w, h), "white")
|
||||
d = ImageDraw.Draw(img)
|
||||
@@ -545,21 +614,21 @@ def render_exercise(folder, figure="neutral", flip=False, strict=False):
|
||||
return geo, order, shade
|
||||
|
||||
resolved = []
|
||||
key_geos = []
|
||||
key_cells = []
|
||||
for nf in norms:
|
||||
out, geo, order, shade = frame_geometry(nf, prof, cam, flip, pitch, mat)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
resolved.append(out)
|
||||
key_geos.append((geo, order, shade))
|
||||
key_cells.append((geo, order, shade, resolve_props(props, geo, nf["root"]["pos"])))
|
||||
|
||||
frames_dir = folder / "frames"
|
||||
frames_dir.mkdir(exist_ok=True)
|
||||
for old in frames_dir.glob("frame-*.svg"):
|
||||
old.unlink()
|
||||
colors = PALETTES["default"]
|
||||
svgs = [svg_for_frame(motion["name"], geo, order, shade, working, colors, props)
|
||||
for geo, order, shade in key_geos]
|
||||
svgs = [svg_for_frame(motion["name"], geo, order, shade, working, colors, prims)
|
||||
for geo, order, shade, prims in key_cells]
|
||||
for i, svg in enumerate(svgs, start=1):
|
||||
(frames_dir / f"frame-{i}.svg").write_text(svg)
|
||||
(folder / "visual.svg").write_text(svgs[motion.get("primary", 1) - 1])
|
||||
@@ -569,7 +638,8 @@ def render_exercise(folder, figure="neutral", flip=False, strict=False):
|
||||
imgs = []
|
||||
for nf in timeline(resolved):
|
||||
geo, order, shade = geometry(nf)
|
||||
imgs.append(draw_geo(geo, order, shade, working, colors, font=font, props=props))
|
||||
prims = resolve_props(props, geo, nf["root"]["pos"])
|
||||
imgs.append(draw_geo(geo, order, shade, working, colors, font=font, prims=prims))
|
||||
imgs[0].save(folder / "preview.gif", save_all=True, append_images=imgs[1:],
|
||||
duration=50, loop=0)
|
||||
print(f" {motion['name']}: {len(svgs)} frames, preview.gif")
|
||||
@@ -578,29 +648,34 @@ def render_exercise(folder, figure="neutral", flip=False, strict=False):
|
||||
|
||||
|
||||
def render_orbit(folder, figure="neutral"):
|
||||
"""A full-turn demo: the camera sweeps 360 degrees while the motion loops.
|
||||
Scene props are view-locked billboards, so orbit shines on prop-free
|
||||
motions (bodyweight exercises)."""
|
||||
"""An orbiting demo of what the app shows: the camera sweeps 360 degrees
|
||||
while the motion loops (or swings a `camera.sweep` pendulum when the
|
||||
motion authors one). Props rotate with the figure about the root anchor."""
|
||||
motion = load_motion(folder)
|
||||
working = set(motion.get("working", []))
|
||||
hide = set(motion.get("hide", []))
|
||||
norms, prof, cam, pitch, props = prepare(motion, figure)
|
||||
resolved = [frame_geometry(nf, prof, cam, pitch=pitch)[0] for nf in norms]
|
||||
mat = mat_bounds(norms, prof, cam, pitch)
|
||||
sweep = motion.get("camera", {}).get("sweep")
|
||||
font = legend_font()
|
||||
colors = PALETTES["default"]
|
||||
ticks = timeline(resolved)
|
||||
imgs = []
|
||||
for i, nf in enumerate(ticks):
|
||||
yaw = cam + 360.0 * i / len(ticks)
|
||||
off = (sweep * math.sin(2 * math.pi * i / len(ticks)) if sweep
|
||||
else 360.0 * i / len(ticks))
|
||||
# Pins are canvas targets in the AUTHORED view: resolve the pose there,
|
||||
# then rotate the posed body - never re-pin in the rotated view.
|
||||
posed, _, _, _ = frame_geometry(nf, prof, cam, pitch=pitch)
|
||||
# then rotate the posed body - never re-pin in the rotated view. The
|
||||
# authored-view geometry also feeds the props' directions and offsets.
|
||||
posed, auth_geo, _, _ = frame_geometry(nf, prof, cam, pitch=pitch)
|
||||
posed["pins"] = {}
|
||||
_, geo, order, shade = frame_geometry(posed, prof, yaw, pitch=pitch, mat=mat)
|
||||
_, geo, order, shade = frame_geometry(posed, prof, cam + off, pitch=pitch, mat=mat)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
imgs.append(draw_geo(geo, order, shade, working, colors, font=font, props=props))
|
||||
prims = resolve_props(props, geo, nf["root"]["pos"],
|
||||
prop_rotation(pitch, off), auth_geo)
|
||||
imgs.append(draw_geo(geo, order, shade, working, colors, font=font, prims=prims))
|
||||
imgs[0].save(folder / "orbit.gif", save_all=True, append_images=imgs[1:],
|
||||
duration=50, loop=0)
|
||||
print(f" {motion['name']}: orbit.gif ({len(imgs)} frames)")
|
||||
@@ -618,9 +693,10 @@ def contact_sheet(folders, figure="neutral", out=None):
|
||||
_, geo, order, shade = frame_geometry(nf, prof, cam, pitch=pitch, mat=mat)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
prims = resolve_props(props, geo, nf["root"]["pos"])
|
||||
cells.append((f"{motion['name']} {i}/{len(norms)}",
|
||||
draw_geo(geo, order, shade, working, PALETTES["default"],
|
||||
font=font, props=props)))
|
||||
font=font, prims=prims)))
|
||||
save_sheet(cells, Path(out) if out else LIB / "contact-sheet.png")
|
||||
|
||||
|
||||
@@ -644,9 +720,10 @@ def demo_sheet(folder):
|
||||
_, geo, order, shade = frame_geometry(norms[idx], prof, cam, flip, pitch, mat)
|
||||
for limb in hide:
|
||||
geo.pop(limb, None)
|
||||
prims = resolve_props(props, geo, norms[idx]["root"]["pos"])
|
||||
cells.append((f"{motion['name']} — {label}",
|
||||
draw_geo(geo, order, shade, working, PALETTES[palette],
|
||||
font=font, props=props)))
|
||||
font=font, prims=prims)))
|
||||
save_sheet(cells, LIB / "demo-sheet.png", cols=3)
|
||||
|
||||
|
||||
@@ -689,6 +766,62 @@ def export_app_resources(folders):
|
||||
print(f" exported skeleton.json -> {out}")
|
||||
|
||||
|
||||
def write_fixtures(folders):
|
||||
"""Regenerate `WorkoutsTests/Fixtures/figure-fixtures.json` — the projected
|
||||
geometry snapshots that pin the Swift solver to this pipeline: per exercise
|
||||
and key frame the draw order, shading, spine, head, nose, and limb chains,
|
||||
plus a mid-tween sample and (for a spread of prop flavors) orbit samples
|
||||
taken through the presentation path — resolve at the authored camera, clear
|
||||
the pins, rotate the posed body — with the resolved prop primitives."""
|
||||
ORBIT_SAMPLED = {"Bird Dog", "Lat Pull Down", "Leg Extension", "Abductor"}
|
||||
OFFSETS = (37.0, 180.0)
|
||||
T = 0.5
|
||||
|
||||
def sample(geo, order, shade, prims=None):
|
||||
out = {"order": order, "shade": shade,
|
||||
"spine": [list(p) for p in geo["spine"]],
|
||||
"head": list(geo["head"])}
|
||||
if "nose" in geo:
|
||||
out["nose"] = [list(geo["nose"][0]), list(geo["nose"][1])]
|
||||
for limb in K.LIMBS:
|
||||
out[limb] = [list(p) for p in geo[limb]]
|
||||
if prims is not None:
|
||||
out["props"] = {"bg": prims[0], "fg": prims[1]}
|
||||
return out
|
||||
|
||||
exercises = []
|
||||
for folder in folders:
|
||||
motion = load_motion(folder)
|
||||
norms, prof, cam, pitch, props = prepare(motion, "neutral")
|
||||
resolved, frames = [], []
|
||||
for nf in norms:
|
||||
out, geo, order, shade = frame_geometry(nf, prof, cam)
|
||||
resolved.append(out)
|
||||
frames.append(sample(geo, order, shade))
|
||||
mid = K.lerp_frames(resolved[0], resolved[1], ease(T))
|
||||
_, geo, order, shade = frame_geometry(mid, prof, cam)
|
||||
entry = {"name": motion["name"], "camera": cam, "frames": frames,
|
||||
"tween": {"t": T, "sample": sample(geo, order, shade)}}
|
||||
if motion["name"] in ORBIT_SAMPLED:
|
||||
anchor = norms[0]["root"]["pos"]
|
||||
posed, auth_geo, _, _ = frame_geometry(norms[0], prof, cam)
|
||||
posed["pins"] = {}
|
||||
orbit = []
|
||||
for off in OFFSETS:
|
||||
_, geo, order, shade = frame_geometry(posed, prof, cam + off)
|
||||
prims = resolve_props(props, geo, anchor,
|
||||
prop_rotation(CAMERA_PITCH, off), auth_geo)
|
||||
orbit.append({"yaw": cam + off,
|
||||
"sample": sample(geo, order, shade, prims)})
|
||||
entry["orbit"] = orbit
|
||||
exercises.append(entry)
|
||||
|
||||
out_path = LIB.parent / "WorkoutsTests" / "Fixtures" / "figure-fixtures.json"
|
||||
out_path.write_text(json.dumps(
|
||||
{"profile": "neutral", "pitch": CAMERA_PITCH, "exercises": exercises}))
|
||||
print(f" {out_path.name} ({len(exercises)} exercises)")
|
||||
|
||||
|
||||
def main():
|
||||
flags = [a for a in sys.argv[1:] if a.startswith("--")]
|
||||
names = [a for a in sys.argv[1:] if not a.startswith("--")]
|
||||
@@ -704,6 +837,9 @@ def main():
|
||||
if "--export" in flags:
|
||||
export_app_resources(folders)
|
||||
return
|
||||
if "--fixtures" in flags:
|
||||
write_fixtures(folders)
|
||||
return
|
||||
if "--orbit" in flags:
|
||||
for folder in folders:
|
||||
render_orbit(folder, figure=figure)
|
||||
|
||||
@@ -46,11 +46,12 @@ struct FigureAnimation {
|
||||
/// describes one authored viewpoint, so machines keep their fixed camera.
|
||||
var orbits: Bool { props.isEmpty && hide.isEmpty }
|
||||
|
||||
/// Drawable geometry at `time`, with limbs listed in `hide` dropped so neither they
|
||||
/// nor any prop riding them are drawn.
|
||||
/// Drawable geometry at `time` — the figure plus the resolved equipment layers —
|
||||
/// with limbs listed in `hide` dropped so neither they nor any prop riding them
|
||||
/// are drawn.
|
||||
func geometry(at time: Double) -> FigureGeometry {
|
||||
let yaw = orbits ? 360 * (time / Self.orbitPeriod) : 0
|
||||
var geo = timeline.geometry(at: time, yawOffset: yaw)
|
||||
var geo = timeline.geometry(at: time, yawOffset: yaw, props: props)
|
||||
for name in hide {
|
||||
if let limb = FigureLimb(rawValue: name) { geo.limbs[limb] = nil }
|
||||
}
|
||||
@@ -121,14 +122,14 @@ struct ExerciseFigureView: View {
|
||||
}
|
||||
|
||||
// Equipment behind the figure: scene shapes and cables.
|
||||
drawBackgroundProps(&ctx, geo)
|
||||
drawProps(&ctx, geo.propsBackground)
|
||||
|
||||
// Parts far-to-near. The head paints last (opaque), preceded by the joint-attached
|
||||
// equipment so bars/pads sit over the limbs but behind the face.
|
||||
for part in geo.order {
|
||||
switch part {
|
||||
case "head":
|
||||
drawAttachedProps(&ctx, geo)
|
||||
drawProps(&ctx, geo.propsForeground)
|
||||
drawHead(&ctx, geo)
|
||||
case "spine":
|
||||
drawSpine(&ctx, geo)
|
||||
@@ -164,132 +165,39 @@ struct ExerciseFigureView: View {
|
||||
|
||||
// MARK: Props
|
||||
|
||||
/// Prop joint refs → (limb, chain index): extremities are index 2, mid joints
|
||||
/// (elbows/knees) index 1, so equipment can ride either joint. Kept 1:1 with
|
||||
/// the reference renderer's `JOINT_LIMB` (legs now end in a foot bone, so a foot
|
||||
/// prop rides the ankle at index 2).
|
||||
private static let propJoints: [String: (limb: FigureLimb, index: Int)] = [
|
||||
"hand_r": (.armR, 2), "elbow_r": (.armR, 1),
|
||||
"hand_l": (.armL, 2), "elbow_l": (.armL, 1),
|
||||
"foot_r": (.legR, 2), "knee_r": (.legR, 1),
|
||||
"foot_l": (.legL, 2), "knee_l": (.legL, 1),
|
||||
]
|
||||
|
||||
/// The joint point a prop follows (plus the unit direction of the bone ending
|
||||
/// at the first joint, for perpendicular items). Nil when any referenced limb
|
||||
/// isn't drawn this frame.
|
||||
private func jointAnchor(_ geo: FigureGeometry, _ ref: PropJointRef) -> (point: CGPoint, direction: CGVector)? {
|
||||
var points: [CGPoint] = []
|
||||
var direction: CGVector?
|
||||
for name in ref.names {
|
||||
guard let joint = Self.propJoints[name],
|
||||
let chain = geo.limbs[joint.limb], chain.count > joint.index else { return nil }
|
||||
let a = chain[joint.index - 1]
|
||||
let b = chain[joint.index]
|
||||
points.append(b)
|
||||
if direction == nil {
|
||||
let d = max(hypot(b.x - a.x, b.y - a.y), 1)
|
||||
direction = CGVector(dx: (b.x - a.x) / d, dy: (b.y - a.y) / d)
|
||||
}
|
||||
}
|
||||
guard let direction, !points.isEmpty else { return nil }
|
||||
let center = CGPoint(x: points.reduce(0) { $0 + $1.x } / CGFloat(points.count),
|
||||
y: points.reduce(0) { $0 + $1.y } / CGFloat(points.count))
|
||||
return (center, direction)
|
||||
}
|
||||
|
||||
private func drawBackgroundProps(_ ctx: inout GraphicsContext, _ geo: FigureGeometry) {
|
||||
for prop in figure.props {
|
||||
switch prop.type {
|
||||
case "scene":
|
||||
for shape in prop.shapes ?? [] { drawSceneShape(&ctx, shape) }
|
||||
case "cable":
|
||||
guard let from = prop.from, from.count == 2, let to = prop.to,
|
||||
let anchor = jointAnchor(geo, to) else { continue }
|
||||
stroke(&ctx, [CGPoint(x: from[0], y: from[1]), anchor.point],
|
||||
color: .figureEquipment, width: prop.w ?? 2)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bars sit at a fixed world angle (default horizontal); dumbbells and pads
|
||||
/// default to perpendicular to the lower bone. Kept 1:1 with the reference
|
||||
/// renderer's `resolve_props` — change them in lockstep.
|
||||
private func drawAttachedProps(_ ctx: inout GraphicsContext, _ geo: FigureGeometry) {
|
||||
for prop in figure.props {
|
||||
if prop.type == "roller" {
|
||||
drawRoller(&ctx, geo, prop)
|
||||
continue
|
||||
}
|
||||
let defaults: (halfLen: Double, width: Double, plateR: Double)
|
||||
switch prop.type {
|
||||
case "bar": defaults = (24, 4, 0)
|
||||
case "dumbbell": defaults = (7, 3, 4.5)
|
||||
case "pad": defaults = (8, 7, 0)
|
||||
default: continue
|
||||
}
|
||||
guard let at = prop.at, let anchor = jointAnchor(geo, at) else { continue }
|
||||
let axis: CGVector
|
||||
if prop.type == "bar" || prop.angle != nil {
|
||||
axis = MotionSolver.direction(prop.angle ?? 0)
|
||||
} else {
|
||||
axis = CGVector(dx: -anchor.direction.dy, dy: anchor.direction.dx)
|
||||
}
|
||||
let h = prop.halfLen ?? defaults.halfLen
|
||||
let a = CGPoint(x: anchor.point.x - axis.dx * h, y: anchor.point.y - axis.dy * h)
|
||||
let b = CGPoint(x: anchor.point.x + axis.dx * h, y: anchor.point.y + axis.dy * h)
|
||||
stroke(&ctx, [a, b], color: .figureProp, width: prop.w ?? defaults.width)
|
||||
let plateR = prop.plateR ?? defaults.plateR
|
||||
if plateR > 0 {
|
||||
for end in [a, b] {
|
||||
let rect = CGRect(x: end.x - plateR, y: end.y - plateR,
|
||||
width: 2 * plateR, height: 2 * plateR)
|
||||
ctx.fill(Path(ellipseIn: rect), with: .color(.figureProp))
|
||||
/// Draws one resolved equipment layer. The solver hands back ready canvas
|
||||
/// primitives (`MotionSolver.resolveProps`, kept 1:1 with the reference
|
||||
/// renderer); the view only maps inks to the adaptive palette.
|
||||
private func drawProps(_ ctx: inout GraphicsContext, _ prims: [PropPrimitive]) {
|
||||
for prim in prims {
|
||||
switch prim {
|
||||
case .line(let points, let width, let ink):
|
||||
stroke(&ctx, points, color: color(for: ink), width: width)
|
||||
case .poly(let points, let width, let ink):
|
||||
// An extruded scene slab: filled and outlined, so it degenerates
|
||||
// to the plain line whenever the sweep collapses edge-on.
|
||||
guard let first = points.first else { continue }
|
||||
var path = Path()
|
||||
path.addLines(points)
|
||||
path.addLine(to: first)
|
||||
path.closeSubpath()
|
||||
ctx.fill(path, with: .color(color(for: ink)))
|
||||
ctx.stroke(path, with: .color(color(for: ink)),
|
||||
style: StrokeStyle(lineWidth: width, lineCap: .round, lineJoin: .round))
|
||||
case .circle(let center, let radius, let width, let fill, let ink):
|
||||
let rect = CGRect(x: center.x - radius, y: center.y - radius,
|
||||
width: 2 * radius, height: 2 * radius)
|
||||
if fill {
|
||||
ctx.fill(Path(ellipseIn: rect), with: .color(color(for: ink)))
|
||||
} else {
|
||||
ctx.stroke(Path(ellipseIn: rect), with: .color(color(for: ink)), lineWidth: width)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A machine roller pad seen end-on: a disc riding the limb's lower bone near the
|
||||
/// joint, on the `side` (+1/−1) of the bone it presses. Kept 1:1 with the reference
|
||||
/// renderer's `resolve_props`.
|
||||
private func drawRoller(_ ctx: inout GraphicsContext, _ geo: FigureGeometry, _ prop: MotionProp) {
|
||||
guard let at = prop.at, let anchor = jointAnchor(geo, at) else { return }
|
||||
let r = prop.r ?? 5
|
||||
let back = prop.back ?? 0
|
||||
let side = prop.side ?? 1
|
||||
let px = anchor.direction.dy * side, py = -anchor.direction.dx * side
|
||||
let center = CGPoint(x: anchor.point.x - anchor.direction.dx * back + px * (r + 3),
|
||||
y: anchor.point.y - anchor.direction.dy * back + py * (r + 3))
|
||||
let rect = CGRect(x: center.x - r, y: center.y - r, width: 2 * r, height: 2 * r)
|
||||
ctx.fill(Path(ellipseIn: rect), with: .color(.figureProp))
|
||||
}
|
||||
|
||||
private func drawSceneShape(_ ctx: inout GraphicsContext, _ shape: PropSceneShape) {
|
||||
let color: Color = shape.color == "prop" ? .figureProp : .figureEquipment
|
||||
switch shape.kind {
|
||||
case "line":
|
||||
guard let pts = shape.pts else { return }
|
||||
stroke(&ctx, pts.compactMap { $0.count == 2 ? CGPoint(x: $0[0], y: $0[1]) : nil },
|
||||
color: color, width: shape.w ?? 4)
|
||||
case "circle":
|
||||
guard let c = shape.c, c.count == 2, let r = shape.r else { return }
|
||||
let rect = CGRect(x: c[0] - r, y: c[1] - r, width: 2 * r, height: 2 * r)
|
||||
if shape.fill ?? true {
|
||||
ctx.fill(Path(ellipseIn: rect), with: .color(color))
|
||||
} else {
|
||||
ctx.stroke(Path(ellipseIn: rect), with: .color(color), lineWidth: shape.w ?? 3)
|
||||
}
|
||||
case "rect":
|
||||
guard let x = shape.x, let y = shape.y, let w = shape.w, let h = shape.h else { return }
|
||||
let path = Path(roundedRect: CGRect(x: x, y: y, width: w, height: h),
|
||||
cornerRadius: shape.r ?? 2)
|
||||
ctx.fill(path, with: .color(color))
|
||||
default:
|
||||
break
|
||||
}
|
||||
private func color(for ink: PropInk) -> Color {
|
||||
ink == .prop ? .figureProp : .figureEquipment
|
||||
}
|
||||
|
||||
/// Near parts take the dark ink, far parts the light one; working parts swap for the
|
||||
|
||||
@@ -140,20 +140,22 @@ enum PropJointRef: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
/// One static shape of a `scene` prop, in canvas coordinates.
|
||||
/// One static shape of a `scene` prop, authored in canvas coordinates in the
|
||||
/// authored view, with world-space 3D form for the orbiting presentation.
|
||||
struct PropSceneShape: Codable {
|
||||
let kind: String // "line" | "circle" | "rect"
|
||||
/// line: polyline points; `w` is the stroke width.
|
||||
let kind: String // "line" | "circle"
|
||||
/// line: polyline points, `[x, y]` or `[x, y, z]`; `w` is the stroke width.
|
||||
let pts: [[Double]]?
|
||||
let w: Double?
|
||||
/// circle: center + radius; `fill` false draws an outline.
|
||||
let c: [Double]?
|
||||
let r: Double?
|
||||
let fill: Bool?
|
||||
/// rect: origin + size (`w`/`h`), corner radius `r`; always filled.
|
||||
let x: Double?
|
||||
let y: Double?
|
||||
let h: Double?
|
||||
/// The shape's depth plane (+ toward the camera in the authored view).
|
||||
let z: Double?
|
||||
/// Extrusion half-width: a line with `depth` is a slab (a seat, a platform)
|
||||
/// that opens into a swept quad as the camera orbits.
|
||||
let depth: Double?
|
||||
/// Optional palette override: `"prop"` for the darker attached-item gray.
|
||||
let color: String?
|
||||
}
|
||||
@@ -164,7 +166,7 @@ struct MotionProp: Codable {
|
||||
let type: String // "scene" | "cable" | "bar" | "dumbbell" | "pad" | "roller"
|
||||
/// scene: the static shapes.
|
||||
let shapes: [PropSceneShape]?
|
||||
/// cable: fixed anchor `[x, y]` → moving joint `to`.
|
||||
/// cable: fixed anchor `[x, y]` or `[x, y, z]` → moving joint `to`.
|
||||
let from: [Double]?
|
||||
let to: PropJointRef?
|
||||
/// bar/dumbbell/pad: the joint(s) the item is centered on.
|
||||
|
||||
@@ -150,6 +150,25 @@ struct NormalizedFrame {
|
||||
/// the far member draws light, behind, and nudged by the readability offset.
|
||||
enum Shade { case near, far }
|
||||
|
||||
/// The two equipment inks: recessive `equipment` gray for scene shapes and cables
|
||||
/// behind the figure, darker `prop` gray for joint-attached items over the limbs.
|
||||
enum PropInk {
|
||||
case equipment, prop
|
||||
|
||||
init(_ name: String?) { self = name == "prop" ? .prop : .equipment }
|
||||
}
|
||||
|
||||
/// One resolved equipment drawing primitive, in canvas coordinates — the shape the
|
||||
/// prop layer reduces to for a single frame (mirroring the reference renderer's
|
||||
/// `resolve_props` output; kept 1:1, change them in lockstep).
|
||||
enum PropPrimitive {
|
||||
case line(points: [CGPoint], width: Double, ink: PropInk)
|
||||
/// A closed, filled, outlined polygon — an extruded scene slab. Degenerates to
|
||||
/// the plain line whenever the sweep collapses edge-on (the authored view).
|
||||
case poly(points: [CGPoint], width: Double, ink: PropInk)
|
||||
case circle(center: CGPoint, radius: Double, width: Double, fill: Bool, ink: PropInk)
|
||||
}
|
||||
|
||||
/// A pose resolved to drawable canvas points (plus the depth-sorted draw order and
|
||||
/// per-limb shading the view needs).
|
||||
struct FigureGeometry {
|
||||
@@ -171,6 +190,10 @@ struct FigureGeometry {
|
||||
/// Parts far-to-near, `"head"` always last (`"spine"`, `"arm_r"`, … then `"head"`).
|
||||
var order: [String]
|
||||
var shade: [FigureLimb: Shade]
|
||||
/// The equipment layer resolved for this frame: scene shapes and cables drawn
|
||||
/// behind the figure, joint-attached items drawn over the limbs (before the head).
|
||||
var propsBackground: [PropPrimitive] = []
|
||||
var propsForeground: [PropPrimitive] = []
|
||||
}
|
||||
|
||||
/// FK output in view space (x right, y up, z toward the camera; origin at the root).
|
||||
@@ -500,6 +523,160 @@ enum MotionSolver {
|
||||
floor: floor, limbs: limbs, order: order, shade: shade)
|
||||
return (resolved, geo)
|
||||
}
|
||||
|
||||
// MARK: Props
|
||||
|
||||
/// Prop joint refs → (limb, chain index): extremities are index 2, mid joints
|
||||
/// (elbows/knees) index 1, so equipment can ride either joint. Kept 1:1 with
|
||||
/// the reference renderer's `JOINT_LIMB` (legs end in a foot bone, so a foot
|
||||
/// prop rides the ankle at index 2).
|
||||
private static let propJoints: [String: (limb: FigureLimb, index: Int)] = [
|
||||
"hand_r": (.armR, 2), "elbow_r": (.armR, 1),
|
||||
"hand_l": (.armL, 2), "elbow_l": (.armL, 1),
|
||||
"foot_r": (.legR, 2), "knee_r": (.legR, 1),
|
||||
"foot_l": (.legL, 2), "knee_l": (.legL, 1),
|
||||
]
|
||||
|
||||
/// The joint point a prop follows (plus the unit direction of the bone ending
|
||||
/// at the first joint, for perpendicular items). Nil when any referenced limb
|
||||
/// isn't drawn this frame.
|
||||
private static func jointAnchor(_ geo: FigureGeometry, _ ref: PropJointRef) -> (point: CGPoint, direction: CGVector)? {
|
||||
var points: [CGPoint] = []
|
||||
var direction: CGVector?
|
||||
for name in ref.names {
|
||||
guard let joint = propJoints[name],
|
||||
let chain = geo.limbs[joint.limb], chain.count > joint.index else { return nil }
|
||||
let a = chain[joint.index - 1]
|
||||
let b = chain[joint.index]
|
||||
points.append(b)
|
||||
if direction == nil {
|
||||
let d = max(hypot(b.x - a.x, b.y - a.y), 1)
|
||||
direction = CGVector(dx: (b.x - a.x) / d, dy: (b.y - a.y) / d)
|
||||
}
|
||||
}
|
||||
guard let direction, !points.isEmpty else { return nil }
|
||||
let center = CGPoint(x: points.reduce(0) { $0 + $1.x } / CGFloat(points.count),
|
||||
y: points.reduce(0) { $0 + $1.y } / CGFloat(points.count))
|
||||
return (center, direction)
|
||||
}
|
||||
|
||||
/// View-space rotation carrying authored-view prop geometry to the camera
|
||||
/// `yawOffset` degrees past the authored yaw: a rotation about the world-vertical
|
||||
/// axis through the root anchor, conjugated by the camera elevation. Nil (the
|
||||
/// identity) at offset 0, so the authored view stays bit-exact.
|
||||
static func propRotation(pitch: Double, yawOffset: Double) -> Mat3? {
|
||||
guard yawOffset != 0 else { return nil }
|
||||
return Mat3.rotX(pitch).times(Mat3.rotY(-yawOffset)).times(Mat3.rotX(-pitch))
|
||||
}
|
||||
|
||||
/// Props → drawable primitives for one frame: (background, foreground).
|
||||
///
|
||||
/// `geo` is the frame's drawn (possibly orbit-rotated) geometry, `authored` the
|
||||
/// same frame at the authored camera (pass `geo` itself when not orbiting),
|
||||
/// `anchor` the frame's root canvas anchor, and `rotation` the `propRotation`
|
||||
/// between them. Joint positions come from `geo`; everything authored — scene
|
||||
/// points, cable anchors, bar angles, pad perpendiculars, roller offsets —
|
||||
/// resolves against `authored` and rotates. Kept 1:1 with the reference
|
||||
/// renderer's `resolve_props` — change them in lockstep.
|
||||
static func resolveProps(_ props: [MotionProp], geo: FigureGeometry, authored: FigureGeometry,
|
||||
anchor: CGPoint, rotation: Mat3?) -> (bg: [PropPrimitive], fg: [PropPrimitive]) {
|
||||
/// Authored canvas point (+ depth toward the camera) → drawn canvas.
|
||||
func place(_ x: Double, _ y: Double, _ z: Double) -> CGPoint {
|
||||
guard let rotation else { return CGPoint(x: x, y: y) }
|
||||
let v = rotation.apply(Vec3(x - anchor.x, anchor.y - y, z))
|
||||
return CGPoint(x: anchor.x + v.x, y: anchor.y - v.y)
|
||||
}
|
||||
/// Authored canvas-space direction/offset → drawn canvas (y-down).
|
||||
func swing(_ vec: CGVector) -> CGVector {
|
||||
guard let rotation else { return vec }
|
||||
let v = rotation.apply(Vec3(vec.dx, -vec.dy, 0))
|
||||
return CGVector(dx: v.x, dy: -v.y)
|
||||
}
|
||||
|
||||
var bg: [PropPrimitive] = []
|
||||
var fg: [PropPrimitive] = []
|
||||
for prop in props {
|
||||
switch prop.type {
|
||||
case "scene":
|
||||
for shape in prop.shapes ?? [] {
|
||||
let ink = PropInk(shape.color)
|
||||
let z = shape.z ?? 0
|
||||
switch shape.kind {
|
||||
case "line":
|
||||
guard let pts = shape.pts else { continue }
|
||||
let lifted = pts.compactMap { pt -> (x: Double, y: Double, z: Double)? in
|
||||
pt.count >= 2 ? (pt[0], pt[1], z + (pt.count > 2 ? pt[2] : 0)) : nil
|
||||
}
|
||||
if let depth = shape.depth, depth != 0 {
|
||||
// An extruded slab: the polyline swept through ±depth,
|
||||
// filled and outlined so it degenerates to the plain
|
||||
// line whenever the sweep collapses edge-on.
|
||||
let quad = lifted.map { place($0.x, $0.y, $0.z + depth) }
|
||||
+ lifted.reversed().map { place($0.x, $0.y, $0.z - depth) }
|
||||
bg.append(.poly(points: quad, width: shape.w ?? 4, ink: ink))
|
||||
} else {
|
||||
bg.append(.line(points: lifted.map { place($0.x, $0.y, $0.z) },
|
||||
width: shape.w ?? 4, ink: ink))
|
||||
}
|
||||
case "circle":
|
||||
guard let c = shape.c, c.count == 2, let r = shape.r else { continue }
|
||||
bg.append(.circle(center: place(c[0], c[1], z), radius: r,
|
||||
width: shape.w ?? 3, fill: shape.fill ?? false, ink: ink))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
case "cable":
|
||||
guard let from = prop.from, from.count >= 2, let to = prop.to,
|
||||
let end = jointAnchor(geo, to) else { continue }
|
||||
let start = place(from[0], from[1], from.count > 2 ? from[2] : 0)
|
||||
bg.append(.line(points: [start, end.point], width: prop.w ?? 2, ink: .equipment))
|
||||
case "roller":
|
||||
// A machine roller pad seen end-on: a disc riding the limb's lower
|
||||
// bone near the joint, on the `side` (+1/−1) of the bone it presses.
|
||||
// The offset resolves along the authored-view bone, then rotates.
|
||||
guard let at = prop.at, let now = jointAnchor(geo, at),
|
||||
let auth = jointAnchor(authored, at) else { continue }
|
||||
let r = prop.r ?? 5
|
||||
let back = prop.back ?? 0
|
||||
let side = prop.side ?? 1
|
||||
let d = auth.direction
|
||||
let px = d.dy * side, py = -d.dx * side
|
||||
let off = swing(CGVector(dx: -d.dx * back + px * (r + 3),
|
||||
dy: -d.dy * back + py * (r + 3)))
|
||||
fg.append(.circle(center: CGPoint(x: now.point.x + off.dx, y: now.point.y + off.dy),
|
||||
radius: r, width: 3, fill: true, ink: .prop))
|
||||
case "bar", "dumbbell", "pad":
|
||||
let defaults: (halfLen: Double, width: Double, plateR: Double) = switch prop.type {
|
||||
case "bar": (24, 4, 0)
|
||||
case "dumbbell": (7, 3, 4.5)
|
||||
default: (8, 7, 0)
|
||||
}
|
||||
guard let at = prop.at, let now = jointAnchor(geo, at),
|
||||
let auth = jointAnchor(authored, at) else { continue }
|
||||
let axis: CGVector
|
||||
if prop.type == "bar" || prop.angle != nil {
|
||||
axis = direction(prop.angle ?? 0) // fixed authored-view angle
|
||||
} else {
|
||||
axis = CGVector(dx: -auth.direction.dy, dy: auth.direction.dx)
|
||||
}
|
||||
let u = swing(axis) // foreshortens under orbit
|
||||
let h = prop.halfLen ?? defaults.halfLen
|
||||
let a = CGPoint(x: now.point.x - u.dx * h, y: now.point.y - u.dy * h)
|
||||
let b = CGPoint(x: now.point.x + u.dx * h, y: now.point.y + u.dy * h)
|
||||
fg.append(.line(points: [a, b], width: prop.w ?? defaults.width, ink: .prop))
|
||||
let plateR = prop.plateR ?? defaults.plateR
|
||||
if plateR > 0 {
|
||||
for end in [a, b] {
|
||||
fg.append(.circle(center: end, radius: plateR, width: 3, fill: true, ink: .prop))
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return (bg, fg)
|
||||
}
|
||||
}
|
||||
|
||||
/// The full looping animation for one motion: key frames resolved to anatomical angles,
|
||||
@@ -566,13 +743,28 @@ struct MotionTimeline {
|
||||
/// targets in the *authored* view, so the pose resolves there first and the posed
|
||||
/// body is then rotated; re-pinning in a rotated view would glue hands to screen
|
||||
/// points that no longer correspond to anything (arms visibly "stuck" mid-orbit).
|
||||
func geometry(at time: Double, yawOffset: Double = 0) -> FigureGeometry {
|
||||
/// `props` are resolved into the geometry's primitive layers the same way: joint
|
||||
/// anchors follow the rotated figure, while authored constructs (scene points,
|
||||
/// cable anchors, bar angles, pad perpendiculars) resolve in the authored view
|
||||
/// and rotate about the root anchor's vertical axis.
|
||||
func geometry(at time: Double, yawOffset: Double = 0, props: [MotionProp] = []) -> FigureGeometry {
|
||||
let frame = frame(at: time)
|
||||
guard yawOffset != 0 else {
|
||||
return MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch, mat: mat).1
|
||||
var geo: FigureGeometry
|
||||
let authored: FigureGeometry
|
||||
if yawOffset == 0 {
|
||||
geo = MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch, mat: mat).1
|
||||
authored = geo
|
||||
} else {
|
||||
let (posedFrame, authoredGeo) = MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch)
|
||||
authored = authoredGeo
|
||||
var posed = posedFrame
|
||||
posed.pins = [:]
|
||||
geo = MotionSolver.frameGeometry(posed, prof: profile, cam: cam + yawOffset, pitch: pitch, mat: mat).1
|
||||
}
|
||||
var posed = MotionSolver.frameGeometry(frame, prof: profile, cam: cam, pitch: pitch).0
|
||||
posed.pins = [:]
|
||||
return MotionSolver.frameGeometry(posed, prof: profile, cam: cam + yawOffset, pitch: pitch, mat: mat).1
|
||||
guard !props.isEmpty else { return geo }
|
||||
let rotation = MotionSolver.propRotation(pitch: pitch, yawOffset: yawOffset)
|
||||
(geo.propsBackground, geo.propsForeground) = MotionSolver.resolveProps(
|
||||
props, geo: geo, authored: authored, anchor: frame.rootPos, rotation: rotation)
|
||||
return geo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
"working": ["spine"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[120, 128], [114, 62]], "w": 8},
|
||||
{"kind": "line", "pts": [[106, 130], [150, 130]], "w": 8},
|
||||
{"kind": "line", "pts": [[120, 128], [114, 62]], "w": 8, "depth": 7},
|
||||
{"kind": "line", "pts": [[106, 130], [150, 130]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[122, 132], [122, 150]], "w": 4},
|
||||
{"kind": "line", "pts": [[102, 150], [150, 150]], "w": 4},
|
||||
{"kind": "line", "pts": [[156, 149], [190, 149]], "w": 5}
|
||||
{"kind": "line", "pts": [[102, 150], [150, 150]], "w": 4, "depth": 5},
|
||||
{"kind": "line", "pts": [[156, 149], [190, 149]], "w": 5, "depth": 6}
|
||||
]},
|
||||
{"type": "pad", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 14, "w": 6}
|
||||
],
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "rect", "x": 152, "y": 56, "w": 16, "h": 72, "r": 5},
|
||||
{"kind": "rect", "x": 130, "y": 126, "w": 60, "h": 10, "r": 3},
|
||||
{"kind": "line", "pts": [[160, 64], [160, 120]], "w": 16, "z": -14, "depth": 6},
|
||||
{"kind": "line", "pts": [[135, 131], [185, 131]], "w": 10, "depth": 12},
|
||||
{"kind": "line", "pts": [[160, 136], [160, 150]], "w": 6}
|
||||
]},
|
||||
{"type": "pad", "at": ["knee_r"], "angle": 90, "halfLen": 13, "w": 6},
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "rect", "x": 152, "y": 56, "w": 16, "h": 72, "r": 5},
|
||||
{"kind": "rect", "x": 130, "y": 126, "w": 60, "h": 10, "r": 3},
|
||||
{"kind": "line", "pts": [[160, 64], [160, 120]], "w": 16, "z": -14, "depth": 6},
|
||||
{"kind": "line", "pts": [[135, 131], [185, 131]], "w": 10, "depth": 12},
|
||||
{"kind": "line", "pts": [[160, 136], [160, 150]], "w": 6}
|
||||
]},
|
||||
{"type": "pad", "at": ["knee_r"], "angle": 90, "halfLen": 13, "w": 6},
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[116, 134], [150, 134]], "w": 8},
|
||||
{"kind": "line", "pts": [[136, 52], [164, 70]], "w": 8},
|
||||
{"kind": "line", "pts": [[116, 134], [150, 134]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[136, 52], [164, 70]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[164, 70], [164, 128]], "w": 5}
|
||||
]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 6}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[124, 126], [158, 124]], "w": 9},
|
||||
{"kind": "line", "pts": [[124, 126], [158, 124]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[138, 128], [138, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[120, 150], [156, 150]], "w": 5},
|
||||
{"kind": "rect", "x": 198, "y": 147, "w": 34, "h": 6, "r": 1},
|
||||
{"kind": "line", "pts": [[120, 150], [156, 150]], "w": 5, "depth": 5},
|
||||
{"kind": "line", "pts": [[201, 150], [229, 150]], "w": 6, "depth": 7},
|
||||
{"kind": "line", "pts": [[183, 62], [183, 122]], "w": 4},
|
||||
{"kind": "line", "pts": [[164, 76], [183, 76]], "w": 3, "color": "prop"}
|
||||
]},
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[114, 132], [86, 40]], "w": 8},
|
||||
{"kind": "line", "pts": [[106, 133], [146, 133]], "w": 8},
|
||||
{"kind": "line", "pts": [[114, 132], [86, 40]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[106, 133], [146, 133]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[122, 135], [122, 151]], "w": 5},
|
||||
{"kind": "line", "pts": [[108, 151], [138, 151]], "w": 4}
|
||||
{"kind": "line", "pts": [[108, 151], [138, 151]], "w": 4, "depth": 5}
|
||||
]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 8}
|
||||
],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[126, 130], [162, 130]], "w": 8},
|
||||
{"kind": "line", "pts": [[126, 130], [162, 130]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[144, 132], [144, 150]], "w": 5},
|
||||
{"kind": "circle", "c": [272, 19], "r": 4, "fill": true, "color": "prop"}
|
||||
]},
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9},
|
||||
{"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9},
|
||||
{"kind": "line", "pts": [[130, 105], [158, 106]], "w": 7},
|
||||
{"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[130, 105], [158, 106]], "w": 7, "depth": 7},
|
||||
{"kind": "line", "pts": [[108, 126], [108, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[160, 124], [160, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5, "depth": 5},
|
||||
{"kind": "circle", "c": [114, 90], "r": 3.5, "fill": true, "color": "prop"}
|
||||
]},
|
||||
{"type": "roller", "at": ["foot_r", "foot_l"], "side": -1, "r": 5, "back": 3}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9},
|
||||
{"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9},
|
||||
{"kind": "line", "pts": [[110, 122], [86, 48]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[104, 124], [166, 120]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[108, 126], [108, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[160, 124], [160, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[104, 150], [166, 150]], "w": 5, "depth": 5},
|
||||
{"kind": "circle", "c": [114, 90], "r": 3.5, "fill": true, "color": "prop"}
|
||||
]},
|
||||
{"type": "roller", "at": ["foot_r", "foot_l"], "side": 1, "r": 5, "back": 7}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"working": ["leg_r", "leg_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[134, 123], [96, 36]], "w": 9},
|
||||
{"kind": "line", "pts": [[126, 127], [156, 122]], "w": 9},
|
||||
{"kind": "line", "pts": [[134, 123], [96, 36]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[126, 127], [156, 122]], "w": 9, "depth": 8},
|
||||
{"kind": "line", "pts": [[138, 130], [138, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[118, 150], [158, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[118, 150], [158, 150]], "w": 5, "depth": 5},
|
||||
{"kind": "line", "pts": [[142, 90], [142, 78]], "w": 3},
|
||||
{"kind": "circle", "c": [142, 77], "r": 3.5, "fill": true, "color": "prop"}
|
||||
]},
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
"working": ["spine"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[138, 130], [182, 130]], "w": 8},
|
||||
{"kind": "line", "pts": [[138, 130], [182, 130]], "w": 8, "depth": 10},
|
||||
{"kind": "line", "pts": [[160, 132], [160, 150]], "w": 5},
|
||||
{"kind": "line", "pts": [[144, 150], [176, 150]], "w": 5}
|
||||
{"kind": "line", "pts": [[144, 150], [176, 150]], "w": 5, "depth": 5}
|
||||
]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 14}
|
||||
],
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[122, 132], [156, 132]], "w": 8},
|
||||
{"kind": "line", "pts": [[159, 46], [157, 96]], "w": 8},
|
||||
{"kind": "line", "pts": [[176, 150], [206, 150]], "w": 5}
|
||||
{"kind": "line", "pts": [[122, 132], [156, 132]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[159, 46], [157, 96]], "w": 8, "depth": 7},
|
||||
{"kind": "line", "pts": [[176, 150], [206, 150]], "w": 5, "depth": 6}
|
||||
]},
|
||||
{"type": "cable", "from": [244, 120], "to": ["hand_r", "hand_l"]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 7}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[116, 140], [88, 50]], "w": 8},
|
||||
{"kind": "line", "pts": [[110, 141], [148, 141]], "w": 8},
|
||||
{"kind": "line", "pts": [[116, 140], [88, 50]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[110, 141], [148, 141]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[126, 143], [126, 151]], "w": 5},
|
||||
{"kind": "line", "pts": [[110, 151], [142, 151]], "w": 4}
|
||||
{"kind": "line", "pts": [[110, 151], [142, 151]], "w": 4, "depth": 5}
|
||||
]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "halfLen": 10}
|
||||
],
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"working": ["arm_r", "arm_l"],
|
||||
"props": [
|
||||
{"type": "scene", "shapes": [
|
||||
{"kind": "line", "pts": [[116, 134], [92, 42]], "w": 8},
|
||||
{"kind": "line", "pts": [[108, 135], [146, 135]], "w": 8},
|
||||
{"kind": "line", "pts": [[116, 134], [92, 42]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[108, 135], [146, 135]], "w": 8, "depth": 8},
|
||||
{"kind": "line", "pts": [[124, 137], [124, 151]], "w": 5},
|
||||
{"kind": "line", "pts": [[110, 151], [140, 151]], "w": 4}
|
||||
{"kind": "line", "pts": [[110, 151], [140, 151]], "w": 4, "depth": 5}
|
||||
]},
|
||||
{"type": "bar", "at": ["hand_r", "hand_l"], "angle": 90, "halfLen": 6}
|
||||
],
|
||||
|
||||
@@ -114,7 +114,9 @@ struct ExerciseMotionTests {
|
||||
|
||||
/// The projected-geometry ground truth: for every exercise and key frame, the draw
|
||||
/// order and near/far shading must match exactly and every point land within 0.5 px
|
||||
/// of the reference, plus the mid-tween sample and (Bird Dog) two orbit-camera views.
|
||||
/// of the reference, plus the mid-tween sample and orbit-camera views taken through
|
||||
/// the presentation path — resolve at the authored camera, clear the pins, rotate
|
||||
/// the posed body — with the equipment layer resolved into rotated primitives.
|
||||
@Test func figureFixturesMatchReference() throws {
|
||||
let bundle = Bundle(for: FigureFixtureMarker.self)
|
||||
let url = try #require(
|
||||
@@ -143,9 +145,20 @@ struct ExerciseMotionTests {
|
||||
expectMatch(MotionSolver.frameGeometry(mid, prof: profile, cam: cam).1,
|
||||
exercise.tween.sample, "\(exercise.name) tween")
|
||||
|
||||
for orbit in exercise.orbit ?? [] {
|
||||
expectMatch(MotionSolver.frameGeometry(norms[0], prof: profile, cam: orbit.yaw).1,
|
||||
orbit.sample, "\(exercise.name) orbit \(orbit.yaw)")
|
||||
guard let orbits = exercise.orbit, !orbits.isEmpty else { continue }
|
||||
let (posedFrame, authored) = MotionSolver.frameGeometry(norms[0], prof: profile, cam: cam)
|
||||
var posed = posedFrame
|
||||
posed.pins = [:]
|
||||
for orbit in orbits {
|
||||
let geo = MotionSolver.frameGeometry(posed, prof: profile, cam: orbit.yaw).1
|
||||
expectMatch(geo, orbit.sample, "\(exercise.name) orbit \(orbit.yaw)")
|
||||
guard let propFixture = orbit.sample.props else { continue }
|
||||
let rotation = MotionSolver.propRotation(pitch: fixtures.pitch, yawOffset: orbit.yaw - cam)
|
||||
let (bg, fg) = MotionSolver.resolveProps(
|
||||
resources.motion.props ?? [], geo: geo, authored: authored,
|
||||
anchor: norms[0].rootPos, rotation: rotation)
|
||||
expectPrims(bg, propFixture.bg, "\(exercise.name) orbit \(orbit.yaw) bg")
|
||||
expectPrims(fg, propFixture.fg, "\(exercise.name) orbit \(orbit.yaw) fg")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +171,7 @@ private final class FigureFixtureMarker {}
|
||||
|
||||
private struct FigureFixtures: Decodable {
|
||||
let profile: String
|
||||
let pitch: Double
|
||||
let exercises: [FixtureExercise]
|
||||
}
|
||||
|
||||
@@ -182,13 +196,30 @@ private struct FixtureSample: Decodable {
|
||||
let armL: [[Double]]
|
||||
let legR: [[Double]]
|
||||
let legL: [[Double]]
|
||||
let props: FixtureProps?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case order, shade, spine, head, nose
|
||||
case order, shade, spine, head, nose, props
|
||||
case armR = "arm_r", armL = "arm_l", legR = "leg_r", legL = "leg_l"
|
||||
}
|
||||
}
|
||||
|
||||
private struct FixtureProps: Decodable {
|
||||
let bg: [FixturePrim]
|
||||
let fg: [FixturePrim]
|
||||
}
|
||||
|
||||
/// One reference prop primitive: a line/poly (`pts`) or a circle (`c` + `r`).
|
||||
private struct FixturePrim: Decodable {
|
||||
let kind: String
|
||||
let pts: [[Double]]?
|
||||
let c: [Double]?
|
||||
let r: Double?
|
||||
let w: Double?
|
||||
let fill: Bool?
|
||||
let color: String
|
||||
}
|
||||
|
||||
private func expectClose(_ point: CGPoint, _ expected: [Double], _ label: String) {
|
||||
#expect(abs(Double(point.x) - expected[0]) < 0.5 && abs(Double(point.y) - expected[1]) < 0.5,
|
||||
"\(label): got (\(point.x), \(point.y)) expected \(expected)")
|
||||
@@ -227,3 +258,37 @@ private func expectMatch(_ geo: FigureGeometry, _ fixture: FixtureSample, _ labe
|
||||
expectChain(geo.limbs[.legR], fixture.legR, "\(label).leg_r")
|
||||
expectChain(geo.limbs[.legL], fixture.legL, "\(label).leg_l")
|
||||
}
|
||||
|
||||
/// One resolved equipment layer against its reference: same primitive kinds and inks
|
||||
/// in the same order, every point within 0.5 px, radii and widths matching.
|
||||
private func expectPrims(_ prims: [PropPrimitive], _ fixtures: [FixturePrim], _ label: String) {
|
||||
guard prims.count == fixtures.count else {
|
||||
Issue.record("\(label): \(prims.count) primitives, expected \(fixtures.count)")
|
||||
return
|
||||
}
|
||||
for (index, (prim, fixture)) in zip(prims, fixtures).enumerated() {
|
||||
let tag = "\(label)[\(index)]"
|
||||
switch prim {
|
||||
case .line(let points, let width, let ink):
|
||||
#expect(fixture.kind == "line", "\(tag) kind")
|
||||
expectChain(points, fixture.pts ?? [], tag)
|
||||
#expect(width == (fixture.w ?? 4), "\(tag) width")
|
||||
expectInk(ink, fixture.color, tag)
|
||||
case .poly(let points, let width, let ink):
|
||||
#expect(fixture.kind == "poly", "\(tag) kind")
|
||||
expectChain(points, fixture.pts ?? [], tag)
|
||||
#expect(width == (fixture.w ?? 4), "\(tag) width")
|
||||
expectInk(ink, fixture.color, tag)
|
||||
case .circle(let center, let radius, _, let fill, let ink):
|
||||
#expect(fixture.kind == "circle", "\(tag) kind")
|
||||
expectClose(center, fixture.c ?? [], tag)
|
||||
#expect(radius == fixture.r, "\(tag) radius")
|
||||
#expect(fill == (fixture.fill ?? false), "\(tag) fill")
|
||||
expectInk(ink, fixture.color, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func expectInk(_ ink: PropInk, _ expected: String, _ label: String) {
|
||||
#expect((ink == .prop ? "prop" : "equipment") == expected, "\(label) ink")
|
||||
}
|
||||
|
||||