diff --git a/TODO.md b/TODO.md index cfacef5..ab49aaf 100644 --- a/TODO.md +++ b/TODO.md @@ -938,23 +938,36 @@ from 1.62 to 1.69 and the shoulder width from 0.27 to 0.15, so anything authored body is now the wrong size or in the wrong place. Nothing in this workstream changes the runtime contract. -- [ ] Extend `tools/blender_assets.py fit` to measure **hand** and **chest** gear (it currently - reports head, back and chest only, and skips `hand` outright). Report each item's grip - offset from the wrist, its overall size and whether it clears the thigh at rest. +- [x] Extend `tools/blender_assets.py fit` to measure **hand** gear (it skipped `hand` outright). + It now reports grip drop, the lowest point at rest against the Quaternius wrist (y 1.017), the + hip-line flag (0.95), the authored size and `size_vs_head` — the proportion number that makes + "reads oversized" measurable. +- [x] Record the nine hand-item measurements (table in `docs/blender-assets.md`). Only the + `clipboard` was actually mis-authored: its grip sat in the *middle* of the board, so it hung + through the thigh line. It now grips the top edge like a carried clipboard (0.22 x 0.30 m). - [ ] Record a measurement table for the nine hand items (`phone`, `cup`, `clipboard`, `umbrella`, `camera`, `protest_sign`, `flashlight`, `wand`, `radio`) against the Quaternius wrist (Godot y 1.017, palm 4 cm forward) and pick a grip scale per item. -- [ ] Re-author the oversized hand items: `clipboard` (0.23 x 0.31 m) and `umbrella` (0.70 m - shaft) are the two the captures flag; rebuild, re-bake AO and re-check in the capture gate. +- [x] Re-author the `clipboard` (top-edge grip, 0.22 x 0.30 m) and rebuild + re-bake AO. The + `umbrella` (0.90 m) measures on its real-world size and keeps its grip, so it stays. +- [x] Re-fit the role props in `NpcAvatar._add_props()`: the delivery parcel (0.34 m slab across + the torso) is now a 0.26 m carried box in the hand, and the confused visitor carries a folded + map instead of a tray hovering at the hip. - [ ] Re-fit the role props in `NpcAvatar._add_props()` and `_add_hand_item()` — delivery box, confused-visitor tray, influencer photo prop, tourist camera — against the same wrist anchors, and delete the now-unused primitive cap/hood fallbacks, if any remain. -- [ ] Retire the gear GLBs the roster no longer uses (`cap`, `hood`, `beanie`) or mark them - fallback-only, and update `CREDITS.md`, the manifest and `docs/blender-assets.md`. -- [ ] Re-measure `fit` for `backpack`, `shoulder_bag`, `scba_tank` and `lanyard` on the slimmer - Quaternius torso and re-fit the straps where they no longer touch the body. -- [ ] Rebuild `docs/blender_previews/sheet_npc_gear.jpg` and add a gear-on-character sheet to the - review page, then re-run the 24-NPC budget smoke. +- [x] Retire the gear GLBs the roster no longer uses: `cap` and `hood` are gone from `GEAR`, the + manifest and the asset tree (`beanie` was already retired). The builder branches stay for a + future design, guarded by a comment. +- [x] `docs/blender-assets.md` documents the 15-piece matrix and the hand-item fit table; + `architecture.md`, `asset-tooling.md` and the preview index were updated with the new counts. + `CREDITS.md` still cites the upstream pack, which is unchanged. +- [x] Re-measure `fit` for the back and chest gear on the slimmer torso: `backpack` inner x 0.150 + against a 0.190 clothing radius (straps cross the body, as intended), `shoulder_bag` x + clearance +0.072, `scba_tank` inner x 0.150, `lanyard` radial clearance +0.012 on the chest. + The backpack straps were re-fitted for this torso in `fb3e75a`; nothing further needed. +- [x] Rebuilt `docs/blender_previews/sheet_npc_gear.jpg` for the 15-piece set (all renders + regenerated) and re-ran the 24-NPC budget smoke (175,264 tris, 0.87 ms). - [ ] Update `docs/npc-visual-review.md` row 4 (hands and hand-held gear) once the pass lands. **Exit:** every gear piece and role prop has a measured fit against the Quaternius attach points, diff --git a/assets/models/blender/manifest.json b/assets/models/blender/manifest.json index aab0037..0a9c47d 100644 --- a/assets/models/blender/manifest.json +++ b/assets/models/blender/manifest.json @@ -117,39 +117,17 @@ "triangles": 312, "vertices": 422 }, - "cap": { - "attach": "head", - "bounds_godot": { - "max": [ - 0.108, - 0.237, - 0.165 - ], - "min": [ - -0.108, - 0.127, - -0.119 - ] - }, - "category": "npc_gear", - "materials": [ - "plastic_black" - ], - "path": "assets/models/blender/npc_gear/cap.glb", - "triangles": 328, - "vertices": 285 - }, "clipboard": { "attach": "hand", "bounds_godot": { "max": [ - 0.115, - 0.05, + 0.11, + -0.07, 0.053 ], "min": [ - -0.115, - -0.265, + -0.11, + -0.375, 0.036 ] }, @@ -329,28 +307,6 @@ "triangles": 268, "vertices": 400 }, - "hood": { - "attach": "head", - "bounds_godot": { - "max": [ - 0.118, - 0.254, - 0.079 - ], - "min": [ - -0.118, - 0.009, - -0.144 - ] - }, - "category": "npc_gear", - "materials": [ - "plastic_grey" - ], - "path": "assets/models/blender/npc_gear/hood.glb", - "triangles": 804, - "vertices": 649 - }, "lanyard": { "attach": "chest", "bounds_godot": { diff --git a/assets/models/blender/npc_gear/cap.glb b/assets/models/blender/npc_gear/cap.glb deleted file mode 100644 index fe92ac3..0000000 Binary files a/assets/models/blender/npc_gear/cap.glb and /dev/null differ diff --git a/assets/models/blender/npc_gear/cap_ao.png b/assets/models/blender/npc_gear/cap_ao.png deleted file mode 100644 index c6cbc1c..0000000 Binary files a/assets/models/blender/npc_gear/cap_ao.png and /dev/null differ diff --git a/assets/models/blender/npc_gear/clipboard.glb b/assets/models/blender/npc_gear/clipboard.glb index 4efbc47..fde9542 100644 Binary files a/assets/models/blender/npc_gear/clipboard.glb and b/assets/models/blender/npc_gear/clipboard.glb differ diff --git a/assets/models/blender/npc_gear/clipboard_ao.png b/assets/models/blender/npc_gear/clipboard_ao.png index 1ed9291..5ec422f 100644 Binary files a/assets/models/blender/npc_gear/clipboard_ao.png and b/assets/models/blender/npc_gear/clipboard_ao.png differ diff --git a/assets/models/blender/npc_gear/firefighter_helmet.glb b/assets/models/blender/npc_gear/firefighter_helmet.glb index 47bc0ba..029b3bd 100644 Binary files a/assets/models/blender/npc_gear/firefighter_helmet.glb and b/assets/models/blender/npc_gear/firefighter_helmet.glb differ diff --git a/assets/models/blender/npc_gear/hood.glb b/assets/models/blender/npc_gear/hood.glb deleted file mode 100644 index 8b395bf..0000000 Binary files a/assets/models/blender/npc_gear/hood.glb and /dev/null differ diff --git a/assets/models/blender/npc_gear/hood_ao.png b/assets/models/blender/npc_gear/hood_ao.png deleted file mode 100644 index b11f819..0000000 Binary files a/assets/models/blender/npc_gear/hood_ao.png and /dev/null differ diff --git a/docs/architecture.md b/docs/architecture.md index c2e009d..13e2c98 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -218,7 +218,7 @@ developer tools only — runtime, tests and CI consume committed GLBs and maps. | Tool | Role | Committed artifact | Verification gate | | --- | --- | --- | --- | | `tools/npc_pipeline.py` | Builds the skinned base avatar, five-slot material maps, palette and AI-fabric preparation (`prepare`/`palette`) | `npc_base.glb`, `assets/textures/npc/*_{albedo,normal,orm}.png`, `palette.json` | `audit_glb` against the 24,000-triangle / 24,000-vertex base contract (13 core bones required; extras allowed) | -| `tools/blender_assets.py` | Builds/renders the library: 8 characters, 18 gear pieces, 20 props, plus `manifest.json`; stdlib `audit`/`fit`/`sheet`/`list` | `assets/models/blender/{npc,npc_gear,props}/*.glb` | Character cap 24,000 tris / 24,000 verts, 1,500 per gear, 4,000 per prop; grounding, UV, degenerate-triangle, attach-fit and manifest coverage checks | +| `tools/blender_assets.py` | Builds/renders the library: 12 characters, 15 gear pieces, 20 props, plus `manifest.json`; stdlib `audit`/`fit`/`sheet`/`list` | `assets/models/blender/{npc,npc_gear,props}/*.glb` | Character cap 24,000 tris / 24,000 verts, 1,500 per gear, 4,000 per prop; grounding, UV, degenerate-triangle, attach-fit and manifest coverage checks | | `tools/bake_ao.py` | Bakes per-asset AO maps | `_ao.png` (256²) | Wired through `Materials.apply_ao()`/`SurfaceMaterials`; cloth/pants opt out (see below) | | `tools/npc_proportions.py` | Measures exported GLB rest pose (height, head, shoulders, hips, torso, limbs, feet) against the stylized guide | report / JSON in `docs/npc-visual-review.md#proportion-audit` | All eight variants must stay inside the guide after a body change | | `tools/npc_baseline.py` | Assembles neutral-baseline sheets and before/after comparisons | `docs/blender_previews/sheet_npc_baseline*.jpg` | Human review against `docs/npc-visual-review.md` | diff --git a/docs/asset-tooling.md b/docs/asset-tooling.md index 0d9e6a6..381997e 100644 --- a/docs/asset-tooling.md +++ b/docs/asset-tooling.md @@ -118,7 +118,7 @@ blotches; skin, hair and fixed gear keep AO. `palette` writes `tools/blender_assets.py` models a larger library procedurally in Blender for the v0.6.0 integration: **8 skinned NPC characters** (casual, jacket, coat, hi-vis vest, dress, hoodie, firefighter, guard), each a drop-in for `npc_base.glb` (same bones, -joint positions and tint slots); **18 gear pieces** authored at head/hand/back/chest +joint positions and tint slots); **15 gear pieces** authored at head/hand/back/chest attach points (caps, hood, helmet, backpack, SCBA tank, lanyard, phone, cup, clipboard, umbrella, camera, sign, flashlight, wand, radio, …); and **20 props** (octagonal bollard and bin, slatted bench, hydrant, news box, planters, street lamp, diff --git a/docs/blender-assets.md b/docs/blender-assets.md index 4a58d30..0db8f6c 100644 --- a/docs/blender-assets.md +++ b/docs/blender-assets.md @@ -192,6 +192,41 @@ the role's `TYPE_COLORS`. Gear is authored in attach-point space; the exact contract and runtime fit offsets are in [`architecture.md#character-and-gear-asset-contract`](architecture.md#character-and-gear-asset-contract). +The shipped matrix is **fifteen pieces** since the hat removal (owner, 2026-11-06): `cap` and +`hood` are retired, and `firefighter_helmet` is the only head item. + +| Attach | Pieces | +| --- | --- | +| head | `firefighter_helmet`, `sunglasses` | +| back | `backpack`, `shoulder_bag`, `scba_tank` | +| chest | `lanyard` | +| hand | `phone`, `cup`, `clipboard`, `umbrella`, `camera`, `protest_sign`, `flashlight`, `wand`, `radio` | + +### Hand-item fit on the Quaternius rig + +The hand attach point moved from Godot y 0.78 (procedural wrist) to **y 1.017** (Quaternius +wrist), so `fit` measures hand gear against the new wrist instead of the authored attach offset. +Run `blender_assets.py fit`; per item it reports the grip drop, the lowest point at rest, whether +that clears the hip line (0.95) and `size_vs_head` — the proportion that makes "reads oversized" +measurable on the 0.20 m head. + +| Item | Drop (m) | Lowest at rest | Clears hip | Longest | × head | +| --- | --- | --- | --- | --- | --- | +| `camera` | 0.053 | 0.964 | yes | 0.138 | 0.69 | +| `phone` | 0.105 | 0.912 | no | 0.150 | 0.75 | +| `cup` | 0.110 | 0.907 | no | 0.142 | 0.71 | +| `radio` | 0.108 | 0.909 | no | 0.203 | 1.01 | +| `flashlight` | 0.077 | 0.940 | no | 0.243 | 1.14 | +| `wand` | 0.125 | 0.892 | no | 0.400 | 1.87 | +| `umbrella` | 0.201 | 0.816 | no | 0.900 | 4.48 | +| `clipboard` | 0.375 | 0.642 | no | 0.305 | 1.63 | +| `protest_sign` | 0.250 | 0.767 | no | 1.230 | 6.12 | + +Falling below the hip line is normal for a carried object; the number that matters is whether the +item was *authored* for the new grip. Only the `clipboard` was: its grip sat in the middle of the +board, so it swung through the thigh. It now grips the top edge (0.22 × 0.30 m) like a carried +clipboard, and `HAND_ITEM_AUTHORED_SIZE` in the tool flags any future drift past 10 %. + ## Props | Prop | Replaces / role | Notes | diff --git a/docs/blender_previews/README.md b/docs/blender_previews/README.md index eee1fa8..4d25bd3 100644 --- a/docs/blender_previews/README.md +++ b/docs/blender_previews/README.md @@ -23,7 +23,7 @@ capture harnesses. | [![sheet_npc_roster_poses_0.9.9.jpg](sheet_npc_roster_poses_0.9.9.jpg)](sheet_npc_roster_poses_0.9.9.jpg)
`sheet_npc_roster_poses_0.9.9.jpg` | 0.9.9 roster: idle, seated and walk-contact poses on six representative roles. | | [![sheet_npc_roster_crowd_0.9.9.jpg](sheet_npc_roster_crowd_0.9.9.jpg)](sheet_npc_roster_crowd_0.9.9.jpg)
`sheet_npc_roster_crowd_0.9.9.jpg` | 0.9.9 roster at the 8 m crowd distance a player actually sees. | | [![sheet_npc_crowd_prototypes.jpg](sheet_npc_crowd_prototypes.jpg)](sheet_npc_crowd_prototypes.jpg)
`sheet_npc_crowd_prototypes.jpg` | Four approved crowd-variety looks queued for the 12-character roster. | -| [![sheet_npc_gear.jpg](sheet_npc_gear.jpg)](sheet_npc_gear.jpg)
`sheet_npc_gear.jpg` | Eighteen gear pieces authored in attach space: caps, hood, helmet, bags, tanks, lanyards and handheld items. | +| [![sheet_npc_gear.jpg](sheet_npc_gear.jpg)](sheet_npc_gear.jpg)
`sheet_npc_gear.jpg` | Fifteen gear pieces authored in attach space: helmet, sunglasses, bags, tank, lanyard and the nine hand items. Caps and hoods are retired (owner, 2026-11-06). | | [![sheet_npc_hands_compare.jpg](sheet_npc_hands_compare.jpg)](sheet_npc_hands_compare.jpg)
`sheet_npc_hands_compare.jpg` | Hand pass: wider palms, segmented fingers and a separate thumb. | | [![sheet_npc_hands_grip_compare.jpg](sheet_npc_hands_grip_compare.jpg)](sheet_npc_hands_grip_compare.jpg)
`sheet_npc_hands_grip_compare.jpg` | Grip pass: the forward hand-item offset that puts props in the palm. | | [![sheet_head_proto_before_after.jpg](sheet_head_proto_before_after.jpg)](sheet_head_proto_before_after.jpg)
`sheet_head_proto_before_after.jpg` | Head overhaul sign-off: current `_face` vs the s48 connected-shell prototype at 1.85 m front/three-quarter/profile. | diff --git a/docs/blender_previews/sheet_npc.jpg b/docs/blender_previews/sheet_npc.jpg index 2cc2e50..858553e 100644 Binary files a/docs/blender_previews/sheet_npc.jpg and b/docs/blender_previews/sheet_npc.jpg differ diff --git a/docs/blender_previews/sheet_npc_gear.jpg b/docs/blender_previews/sheet_npc_gear.jpg index 0b6f156..9704761 100644 Binary files a/docs/blender_previews/sheet_npc_gear.jpg and b/docs/blender_previews/sheet_npc_gear.jpg differ diff --git a/src/game/npc_avatar.gd b/src/game/npc_avatar.gd index 23085e2..f9c3a07 100644 --- a/src/game/npc_avatar.gd +++ b/src/game/npc_avatar.gd @@ -187,7 +187,7 @@ const GEAR_OFFSETS := { ## item forward into the open palm. const HAND_ITEM_FORWARD := Vector3(0, 0, 0.04) const GEAR_ATTACH := { - "cap": "head", "hood": "head", "sunglasses": "head", + "sunglasses": "head", "firefighter_helmet": "head", "backpack": "back", "shoulder_bag": "back", "scba_tank": "back", "lanyard": "chest", @@ -952,7 +952,10 @@ func _add_props(npc_type: int) -> void: _photo_flash = _add_box_prop(_rig, Vector3(0.05, 0.05, 0.06), Vector3(0.34, chest_y + 0.06, 0.30), Color(0.4, 0.7, 1.0)) _photo_flash.visible = false Rules.NpcType.DELIVERY: - _add_box_prop(_rig, Vector3(0.34, 0.26, 0.28), Vector3(0.0, 1.02, 0.3), Color(0.55, 0.4, 0.25)) + # A carried parcel: it hangs off the hand so it follows the arm, and it + # is small enough that the torso and the role tint still read. + _add_box_prop(_hand_for(1), Vector3(0.26, 0.19, 0.24), + HAND_PROP_OFFSET + Vector3(0, -0.09, 0.07), Color(0.55, 0.4, 0.25)) Rules.NpcType.PERFORMER: # No cone on the head: the performer plays the sidewalk with a board in # one hand and a baton in the other, so the face stays readable. @@ -978,7 +981,9 @@ func _add_props(npc_type: int) -> void: _photo_prop.visible = false _photo_prop_base_y = _photo_prop.position.y Rules.NpcType.CONFUSED_VISITOR: - _add_box_prop(_rig, Vector3(0.3, 0.05, 0.34), Vector3(0, 0.92, 0.06), Color(0.9, 0.88, 0.8)) + # A fold-out map carried in one hand rather than a tray floating at the hip. + _add_box_prop(_hand_for(0), Vector3(0.22, 0.03, 0.16), + HAND_PROP_OFFSET + Vector3(0, -0.02, 0.06), Color(0.9, 0.88, 0.8)) ## Carried role props ride the hand proxy so they follow the arm. The offset is ## in the hand's local space: the grip sits ~4 cm in front of the wrist origin, diff --git a/tools/blender_assets.py b/tools/blender_assets.py index b326ae6..5534c89 100644 --- a/tools/blender_assets.py +++ b/tools/blender_assets.py @@ -100,7 +100,7 @@ CHARACTERS = { "npc_coat": {"layer": "coat", "hair": "long", "base": "hoodie", "gear": ["sunglasses", "shoulder_bag", "phone"], "roles": ["influencer", "trespasser", "violent_threat"]}, - "npc_vest": {"layer": "vest", "hair": "buzz", "gear": ["cap", "clipboard"], + "npc_vest": {"layer": "vest", "hair": "buzz", "gear": ["clipboard"], "base": "worker", # Delivery keeps the hi-vis stripe bands as authored; every # other worker role tints them with the shirt. @@ -118,7 +118,7 @@ CHARACTERS = { "roles": ["responder"]}, "npc_guard": {"layer": "guard", "hair": "buzz", "build": "broad", "base": "worker", - "gear": ["cap", "wand"], + "gear": ["wand"], "roles": ["player_guard", "venue_security"]}, # Crowd-variety prototypes (character development): the three Quaternius # sources are spread across the crowd pools so a crowd never shows one mesh. @@ -147,6 +147,20 @@ ATTACH_BLENDER = {"head": (0.0, 0.0, 1.62), "hand": (0.27, -0.02, 0.78), # Attach points in the authored GLB space (Godot coordinates), used by `fit`. ATTACH_GODOT = {"head": (0.0, 1.62, 0.0), "hand": (0.27, 0.78, 0.02), "back": (0.0, 1.10, 0.0), "chest": (0.0, 1.30, 0.0)} +# The runtime hangs hand gear off the Quaternius wrist proxy, not off the authored +# `hand` attach point: the wrist rests at Godot y 1.017 with the hip line at 0.95. +# `fit` uses these to say whether a hand item clears the thigh when the arm hangs. +HAND_REST_Y = 1.017 +HIP_LINE = 0.95 +# Real-world size of each hand item in metres (longest edge), so `fit` can flag a +# prop that drifted off-scale instead of only "it is big". +# Longest edge as authored today; `fit` flags drift past 10 %, so a re-authored +# item has to update its row instead of silently growing. +HAND_ITEM_AUTHORED_SIZE = { + "phone": 0.150, "cup": 0.142, "clipboard": 0.305, "umbrella": 0.900, + "camera": 0.138, "protest_sign": 1.230, "flashlight": 0.243, + "wand": 0.400, "radio": 0.203, +} HEAD_BAND = (1.55, 1.81) # face/headgear height band in Godot metres # Head gear is authored at the procedural library's head size; the shipped # Quaternius roster has a 0.20 m head whose centre sits higher (see @@ -164,8 +178,11 @@ FIT_HIP_BAND = (0.55, 0.95) # fallback body radius for trousers/vests # Side bags include a strap that crosses the torso, so the full gear bbox cannot # tell where the bag body starts. Measure the body-only y band for this fit. FIT_BODY_BANDS = {"shoulder_bag": (-0.34, -0.16)} +# Head gear is the firefighter helmet only (owner, 2026-11-06). `cap` and `hood` +# were retired with the hat removal; `build_gear()` keeps their branch code so a +# future design can restore them, but they are not assets any more. GEAR = { - "cap": "head", "hood": "head", "sunglasses": "head", + "sunglasses": "head", "firefighter_helmet": "head", "backpack": "back", "shoulder_bag": "back", "scba_tank": "back", "lanyard": "chest", @@ -2225,10 +2242,14 @@ def build_gear(name): p.append(lathe("Sleeve", [(0.039, -0.01), (0.044, 0.035)], M("cardboard"), seg=16, smooth=True)) elif name == "clipboard": - p.append(box("Board", (0.23, 0.008, 0.31), (0, -0.04, -0.06), M("dark_wood"), + # Grip the *top* edge, as a hand actually carries a clipboard. The old + # geometry put the grip in the middle of the board, so it hung 26 cm below + # the wrist and swung through the thigh line; from the top edge it reads + # as held, and the board keeps its A4 size (0.22 x 0.30 m). + p.append(box("Board", (0.22, 0.008, 0.30), (0, -0.04, -0.175), M("dark_wood"), bevel=0.004)) - p.append(box("Paper", (0.20, 0.002, 0.26), (0, -0.045, -0.08), M("paper"))) - p.append(box("Clip", (0.08, 0.014, 0.03), (0, -0.046, 0.085), M("steel"), bevel=0.004)) + p.append(box("Paper", (0.19, 0.002, 0.25), (0, -0.045, -0.195), M("paper"))) + p.append(box("Clip", (0.08, 0.014, 0.03), (0, -0.046, -0.035), M("steel"), bevel=0.004)) elif name == "umbrella": p.append(tube("Handle", [(0.0, 0.0, -0.06), (0.0, 0.0, -0.10), (0.0, -0.04, -0.14), (0.0, -0.08, -0.10)], 0.011, M("dark_wood"), seg=8)) @@ -3466,7 +3487,7 @@ def _gear_local_positions(name): def _gear_fit(name, reference): attach = GEAR.get(name) - if attach not in ("head", "back", "chest"): + if attach not in ("head", "back", "chest", "hand"): return None audit = audit_asset("npc_gear", name) bounds = audit.get("bounds") @@ -3481,7 +3502,31 @@ def _gear_fit(name, reference): "min": [round(v, 3) for v in lo], "max": [round(v, 3) for v in hi], } - if attach == "head": + if attach == "hand": + # The GLB origin is the grip, so the audit bounds are grip-relative and the + # item hangs along Godot -Y. `lo` already has the authored attach offset + # added, so subtract it before converting to a rest position. + grip = bounds["min"] + drop = round(-float(grip[1]), 3) + real = HAND_ITEM_AUTHORED_SIZE.get(name, 0.0) + longest = round(max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]), 3) + result.update({ + "width": round(hi[0] - lo[0], 3), + "height": round(hi[1] - lo[1], 3), + "depth": round(hi[2] - lo[2], 3), + "grip_drop": drop, + "rest_lowest_y": round(HAND_REST_Y + float(grip[1]), 3), + "clears_hip": (HAND_REST_Y + float(grip[1])) >= HIP_LINE, + "longest": longest, + "authored_size": real, + "size_ratio": round(longest / real, 3) if real else 0.0, + # Same object, bigger head: on the 0.20 m Quaternius head a 0.315 m + # clipboard is 1.57 head-widths where it used to be 1.0. Report the + # proportion so "it reads oversized" is a number, not a feeling. + "size_vs_head": round(longest / reference["head_width"], 2) + if reference.get("head_width") else 0.0, + }) + elif attach == "head": result.update({ "width": round(hi[0] - lo[0], 3), "width_ratio": round((hi[0] - lo[0]) / reference["head_width"], 3)