Gem slots could not be positioned by hand because translation is not the
gem's position. The gem model is `item/generated`: a 2x2 sprite block at the
dead centre (8,8) of a 16x16 sheet, so its geometry sits 8px from the model
origin on every axis. MIAPI's Transform.toMatrix is T * Rx * Ry * Rz * S with
no centring offset, so rotation pivots on that origin and throws the gem up to
12px away. Translation is whatever cancels the throw.
world = pivot + flip(T + R*S*c), flip = negate x,y, c = (8,8,8)
`flip` is the block-model(+y up) -> ModelPart(+y down) conversion. Inverting
it gives T from a desired anchor, so tools/solve_gems.py takes targets in
plain world pixels and asserts the result forward-evaluates back.
The helmet gem is the exception: it chains through the hat slot, which scales
by 1.25 and lifts by -3.925, and does NOT go through the flip. Solving it with
the flip inverts y and drops the gem past the chin to the floor. The chain is
recorded against two in-game sightings that pin it.
Also:
- Drop the stray no-origin model entry from all eight limb gem/case modules.
Without an origin a model falls through the slot's z180 and lands mirrored
in x and y - which is why a right-arm gem showed up above the left shoulder.
- Give the helmet and backtank gems origin-carrying modules. Their slots took
`gem_armor_medium`, whose only members declare no origin, so the gem was
drawn in the body pass and never appeared. Apothic cases included so
repointing the slots loses nothing.
- Compensate the z180 in the helmet model UVs. Geometry in these models is
already authored around the flip; UVs cannot be, so every face rendered
rotated 180. The face that shows on top is the one authored `down`.
- Remove the misaligned placeholder bezels from the four limb sockets.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>