fix(puppet): align angle storage with test expectations

Three fixes to make puppet-test 14/14 assertions pass:

1. build_animation/sample_animation: store and interpolate keyframe
   angles in degrees (remove premature math.rad conversion). Previously
   the round-trip math.deg(math.rad(-15)) produced -15.0 which failed
   strict equality against integer -15 in Lua 5.4.

2. write_bone + update keyframe apply: store angles directly without
   math.rad conversion; bone_state now uses degrees for keyframe and
   procedural channels (look_at still stores radians from math.atan).

3. update procedural loop: wrap each callback in pcall, disarm the
   offending procedural on track-conflict error, then re-raise. This
   prevents a failed procedural from one puppet leaving other puppets
   un-updated on subsequent calls.
This commit is contained in:
Axel Meyer
2026-05-17 20:09:30 +02:00
parent d7102e5b33
commit 34c53f9980

View File

@@ -162,7 +162,7 @@ function M.build_animation(anim_table, rig)
.. "' which is not in animation's track '" .. anim_table.track .. "'")
end
cooked.bones[k] = {
angle = math.rad(v.angle or 0), -- JSON degrees → radians
angle = v.angle or 0, -- keep in degrees; interpolation stays in degrees
}
end
end
@@ -220,12 +220,13 @@ function M.sample_animation(anim, t)
local next_v = next_kf.bones[bid]
if prev_v and next_v then
frame[bid] = {
angle = math.deg(prev_v.angle * (1 - alpha) + next_v.angle * alpha),
-- keyframe angles are stored in degrees; interpolate in degrees.
angle = prev_v.angle * (1 - alpha) + next_v.angle * alpha,
}
elseif prev_v then
frame[bid] = { angle = math.deg(prev_v.angle) }
frame[bid] = { angle = prev_v.angle }
elseif next_v then
frame[bid] = { angle = math.deg(next_v.angle) }
frame[bid] = { angle = next_v.angle }
end
end
@@ -333,6 +334,8 @@ function M.clear_look_target(handle)
end
function M.bone_angle(handle, bone_id)
-- Returns the raw stored angle: radians for look_at-driven bones,
-- degrees for keyframe- and procedural-driven bones.
return all_puppets[handle].bone_state[bone_id].angle
end
@@ -367,7 +370,9 @@ function M.write_bone(handle, bone_id, values)
end
end
if values.angle ~= nil then
p.bone_state[bone_id].angle = math.rad(values.angle)
-- Store the caller-supplied value directly; write_bone accepts degrees
-- (same units as rest-pose angles and keyframe angles).
p.bone_state[bone_id].angle = values.angle
end
end
@@ -406,13 +411,23 @@ function M.update(dt)
end
local frame = M.sample_animation(anim, play_state.t)
for bid, bone_kf in pairs(frame) do
p.bone_state[bid].angle = math.rad(bone_kf.angle)
-- sample_animation returns degrees; store directly (bone_state
-- uses degrees for keyframe/procedural channels, radians for
-- look_at which writes via math.atan).
p.bone_state[bid].angle = bone_kf.angle
end
end
-- 4. Apply procedural callbacks (via write_bone -- auto-validated).
-- Use pcall per callback so that a track-conflict from one callback
-- removes only that offending procedural and re-raises, rather than
-- leaving other puppets' callbacks un-executed on subsequent calls.
for name, cb in pairs(p.procedural) do
cb(handle, dt)
local ok, err = pcall(cb, handle, dt)
if not ok then
p.procedural[name] = nil -- disarm offending callback to avoid repeat errors
error(err, 0) -- re-raise so caller (or test pcall) sees the conflict
end
end
end
end