# Pose Studio

Local-only parametric humanoid + posing tool (`/studio/pose-studio/`). One shared rig
is generated from body parameters and drawn at three levels of detail:

| LOD | what you see | source |
| --- | --- | --- |
| `skeleton` | region-coloured bones: skull, curved spine, 12 rib pairs + cartilage, sternum, clavicles/scapulae, humerus, radius + ulna (radius crosses with pronation), 27 hand bones, pelvis, femur, patella, tibia/fibula, foot bones | `src/lod-skeleton.js` |
| `simplified` | anatomical block-out: egg ribcage with costal arch, bucket pelvis, glutes, lofted limbs with muscle bulges, sphere joints, hands with fingers, wedge feet, optional contour lines | `src/lod-simplified.js` |
| `primitives` | literal bright blocks, one per mass, dark edges | `src/lod-primitives.js` |

Any number of figures live in a scene; each figure has body params, a floor
position + facing, and a pose (semantic joint angles + optional IK targets).
Everything round-trips through JSON (textarea in the panel, copy/download/load,
persisted to `localStorage`).

## Build / run

```bash
npm run build:pose-studio      # esbuild studio/pose-studio/src/main.js → studio/pose-studio/bundle.js
npm run watch:pose-studio      # same, watch mode
npm run dev                    # static server on http://localhost:8004/studio/pose-studio/
```

`bundle.js` is committed like the other bundled projects; rebuild after editing `src/`.

## Files

```
studio/pose-studio/
  index.html, styles.css        panel + viewport shell (site header, Verdana/blue/red palette)
  src/main.js                   app state, UI, persistence, JSON import/export, click-to-select
  src/params.js                 body parameter schema, defaults, body presets
  src/anatomy.js                params → joint rest positions, bone lengths, flesh dims, stats
  src/joints.js                 joint/dof definitions, pose format, Euler <-> semantic dofs
  src/rig.js                    Figure class: hierarchy, LOD attachment, applyPose (+ IK)
  src/ik.js                     two-bone IK, hand orientation, foot leveling, look-at
  src/collision.js              capsule/ellipsoid proxies + interpenetration resolver
  src/geometry.js               loft / superellipse rings / contour lines / ribcage egg / plates
  src/hand.js                   shared hand rig (fingers, thumb, curl/spread)
  src/lod-*.js                  the three renderers
  src/poses.js                  pose presets + scene presets
  src/viewer.js                 three.js viewport, cameras (persp/ortho), grid, ruler, overlays
  src/colors.js                 palettes
```

## Coordinate conventions

* metres, Y up, the figure stands on `y = 0` at its `position` and faces **+Z**
  at `yaw = 0`; the figure's own **left is +X**. `yaw` in degrees, positive turns
  the figure to its left (counter-clockwise from above): `yaw: 90` faces +X and
  its left is then world **−Z**; `yaw: -90` faces −X with its left at +Z.
* Every joint frame is world-aligned in the rest pose. Bones point −Y (limbs),
  +Y (spine), +Z (feet). Rotations use Euler order `XZY`.
* Rest pose = arms hanging straight down with palms facing the thighs (thumbs
  forward), legs straight, feet forward.

## Body parameters (`params`)

| key | range | meaning |
| --- | --- | --- |
| `sex` | 0..1 | 0 female → 1 male, continuous blend of proportions & masses |
| `height` | 120..220 | cm |
| `bmi` | 15..45 | drives soft-tissue girth per region (belly/hips/thighs gain more than hands/feet/head) |
| `muscle` | 0..1 | muscularity — deltoids, arms, chest, thighs, calves; slightly narrows the waist |
| `fatDist` | 0..1 | 0 = gynoid (hips/thighs/glutes) → 1 = android (belly/chest) |
| `bust` | 0..1 | breast size (female) / pec mass (male) |
| `headSize`, `neckLength`, `torsoLength`, `legLength` | ×0.6–1.5 | vertical stack multipliers; total is renormalised to `height` |
| `shoulderWidth`, `ribcageSize`, `hipWidth`, `armLength`, `handSize`, `footSize` | ×0.8–1.25 | proportion multipliers |

Reference means at ×1.0 (fractions of stature): hip joint 0.53, C7 0.825,
skull base 0.875, glenohumeral centres ±0.101 (m) / ±0.092 (f), shoulder→elbow
0.168, elbow→wrist 0.146, hand 0.108, knee 0.285, ankle 0.044, foot 0.152,
inseam ≈ 0.47, arm span ≈ 1.04 H (m) / 0.99 H (f), 7.5 heads tall.

Body presets: `female-average`, `male-average`, `female-athletic`,
`male-athletic`, `female-curvy`, `male-heavy`, `female-petite`,
`male-tall-thin`, `female-tall`, `androgynous`.

## Pose format

All angles in **degrees**, everything optional, missing = 0. Positive
directions read the way an artist would say them:

| joint(s) | dofs (positive direction) |
| --- | --- |
| `root` | `pos: [x, y, z]` metres offset from the rest pelvis; `pitch` (lean forward), `yaw` (turn left), `roll` (lean left) |
| `spine`, `chest`, `neck`, `head` | `bend` (forward), `side` (toward the figure's left), `twist` (turn left) |
| `clavicleL/R` | `raise` (shrug up), `forward` (protract) |
| `shoulderL/R` | `flex` (arm forward, −60..180), `abduct` (away from the body, −40..180), `twist` (external rotation, ±90) |
| `elbowL/R` | `bend` (0..150), `pronate` (−90 supinate .. 90 pronate) |
| `handL/R` | `flex` (palm-ward, −75 extend .. 85), `deviate` (radial/thumb-ward), `twist`; shapes 0..1: `curl` (all fingers), `spread`, `thumb` (opposition), and per-finger overrides `index`, `middle`, `ring`, `little`, `thumbCurl` (absent = follow `curl`; e.g. pointing = `{ curl: 1, index: 0 }`) |
| `hipL/R` | `flex` (thigh forward, −30..130), `abduct` (out, −30..70), `twist` (external, ±45) |
| `kneeL/R` | `bend` (0..155), `twist` (±15) |
| `footL/R` | `lift` (toes up = dorsiflex, −50..30), `invert` (sole inward, ±30), `toeOut` (−30..45) |
| `toesL/R` | `curl` (down, −60..45) |

Left and right use the **same numbers** for the same anatomical motion, so
copying a left-side block to the right side mirrors it (`mirror l ↔ r` button).

### IK block

```jsonc
"ik": {
  "handL": {
    "target": [x, y, z],          // required
    "space": "figure" | "world",  // default "figure" (metres, relative to the figure origin/facing)
    "pole": [x, y, z],            // optional elbow hint (default: back-down-out)
    "palm": [x, y, z],            // optional palm-normal direction → wrist + pronation are solved
    "fingers": [x, y, z]          // optional finger direction
  },
  "footL": { "target": [...], "pole": [...], "level": true,     // level = keep the sole flat
             "toes": [x, y, z], "sole": [x, y, z] },             // or aim the foot explicitly
  "head":  { "lookAt": [x, y, z], "space": "world" }            // 40 % neck / 60 % head
}
```

`handR`, `footR` likewise. IK is solved after the Euler values (two-bone
analytic, pole vector defines the elbow/knee plane); the solved angles are
written back into the pose so the sliders and JSON show real numbers. Sliders
of IK-driven joints are locked; use *release* to freeze the solved angles.

### Joint limits

Every rotation that gets written — sliders, JSON, IK, hand/foot aiming, the
collision resolver — is clamped. Hinge-like joints use the per-dof ranges in
the table above. Shoulders and hips use anatomical **swing/twist** limits (an
elliptical cone for how far the bone may point from hanging: shoulder
front 175° / back 55° / out 175° / in 45°, hip 125° / 25° / 50° / 30°, plus a
twist range ±90° / ±45°), so a target that is out of reach makes the limb stop
at its limit instead of over-twisting. The resolver's swivel is bounded to
±45° from the authored pole.

### Example: two figures palm to palm (world-space targets)

A faces +X so her left is −Z: her left hand meets B's right hand at z = −0.19,
the other pair at z = +0.19 — arms stay parallel, not crossed. Targets are the
**wrist** positions, so keep half a palm thickness (≈ 1.4 cm) between them.

```json
{
  "figures": [
    { "name": "A", "params": { "sex": 0, "height": 162, "bmi": 22 }, "position": [-0.42, 0], "yaw": 90,
      "pose": { "ik": {
        "handL": { "target": [-0.014, 1.28, -0.19], "space": "world", "palm": [1, 0, 0], "fingers": [0, 1, 0] },
        "handR": { "target": [-0.014, 1.28, 0.19], "space": "world", "palm": [1, 0, 0], "fingers": [0, 1, 0] },
        "head": { "lookAt": [0.42, 1.62, 0], "space": "world" } } } },
    { "name": "B", "params": { "sex": 1, "height": 175, "bmi": 23 }, "position": [0.42, 0], "yaw": -90,
      "pose": { "ik": {
        "handL": { "target": [0.014, 1.28, 0.19], "space": "world", "palm": [-1, 0, 0], "fingers": [0, 1, 0] },
        "handR": { "target": [0.014, 1.28, -0.19], "space": "world", "palm": [-1, 0, 0], "fingers": [0, 1, 0] },
        "head": { "lookAt": [-0.42, 1.5, 0], "space": "world" } } } }
  ]
}
```

Paste into the *scene json* box → *apply json*. A bare pose object (no
`figures`) applies to the selected figure. `figures[i].pose` may also be a pose
preset id (`"neutral"`, `"sitting"`, …) when loading via `window.poseStudio.loadScene`.

## Props (surfaces a pose rests on)

Scene-level `props` array, drawn as neutral blocks with edges (shadows on):

```jsonc
"props": [
  { "type": "box", "size": [0.44, 0.46, 0.46], "position": [x, y, z], "yaw": 0 },          // world placed (centre)
  { "type": "box", "size": [1.6, 2.2, 0.1], "figure": "<figure id>", "offset": [0, 1.1, -0.2] } // follows that figure
]
```

`type` = `box` (`size` = w, h, d) or `cylinder` (`size` = radius, height). Pose
presets that need a surface bring their own figure-bound prop (`sitting` → seat
block, `lean-wall` → wall); the PROPS panel has *+ seat / + wall / + block*
and a remove button per prop. Props are visual only for now (the collision
resolver does not push limbs out of them).

## Study mode

*show only* (VIEW) draws just one part group of every figure — whole figure,
arms, hands, one arm, legs, feet, torso, head — for hand / arm studies. Scene
preset `study-hands` loads a figure with both palms toward the camera, arms
only, framed close. Each hand block in POSE has shape buttons: relaxed · flat ·
fist · spread · grip · point · peace · thumbs-up · ok (they set the shape dofs;
moving the whole-hand `curl` slider releases per-finger overrides).

## Collision handling

`collision: "soft" | "firm" | "off"` (scene-level, select in the POSE panel).
Every segment has a capsule / ellipsoid proxy with a **hard core** (bone) and a
**soft envelope** (flesh) — see `src/collision.js`. After Euler + IK, contacts
deeper than `squish` × (combined soft layers) are pushed apart iteratively:

* limbs on an IK chain **swivel** about the shoulder→hand / hip→foot axis
  (elbow/knee moves, the target stays); what swivel cannot fix **displaces the
  IK target** (a hand aimed inside a body slides out to its surface — arms
  folded on the chest just work);
* free (FK) limbs rotate at their chain joints, clamped to joint limits;
* trunk pushes are biased sideways (abduct rather than swing forward/back);
* torso, head and neck never move; adjacent segments (upper arm/forearm,
  thigh/pelvis…) are allowed to overlap.

The resolver's changes are runtime-only (`figure.runtime`): sliders and JSON
keep the authored numbers, and the same JSON always resolves the same way.
Contacts it cannot clear (beyond joint limits) are marked with red dots and
counted in the status readout; *collision proxies (debug)* in VIEW draws the
capsules/ellipsoids.

## Presets

Pose: `neutral`, `rest`, `a-pose`, `t-pose`, `contrapposto`, `walking`,
`sitting` (+ seat prop), `squat`, `hands-on-hips`, `reach-up`, `arms-folded`
(classic tight fold: forearms stacked on the chest, right underneath with its
palm cupping the left biceps inside the crook, left on top with its hand tucked
under the right triceps; left leg crossed in front), `lean-wall` (+ wall prop),
`push`.
Scene: `single-male`, `single-female`, `palms`, `lineup`, `sitting-pair`,
`crossed` (arms folded + legs crossed), `lean`, `study-hands`.

## View / reference aids

LOD switch (`s` / `b` / `p`), skeleton x-ray inside the flesh, contour lines,
region tints / ivory bones, rig joints + IK targets, 10 cm ground grid, height
ruler, head-unit lines, orthographic camera, view buttons (`1`–`6`: front, ¾,
left, back, right, top), frame all (`f`), PNG export.

## Console API

`window.poseStudio` → `{ THREE, state, viewer, loadScene(spec), loadScenePreset(id), applyPose(pose, figureIndex?), select(id), sceneToJSON() }`.

## Roadmap ideas

* more spine segments + shoulder girdle coupling (clavicle follows arm elevation)
* props as collision objects (sit/lean contact)
* muscle layer between skeleton and block-out (organic-structures style)
* age parameter (child proportions), custom measurement fitting (bust/waist/hip → params)
* draggable IK targets in the viewport (TransformControls)
