feat: Implement draggable torso and head IK targets in the test harness

- Added draggable handles for the torso and head to the test harness.
- Updated `IK_HANDLE_PATHS` to include new entries for "Head" and "Torso".
- Implemented distinct colors for the torso (magenta) and head (yellow) markers.
- Added a visual aid (aim line) to indicate the head's LookAt target direction.
- Ensured that dragging the torso moves only the torso marker, allowing for limb stretching towards stationary targets.
This commit is contained in:
2026-08-21 12:39:31 -04:00
parent 6b273c049c
commit ab5c79ab6a
14 changed files with 1459 additions and 174 deletions
+72 -4
View File
@@ -24,6 +24,8 @@ const BONE_COLOR_RIGHT := Color(1.00, 0.50, 0.20)
const BONE_COLOR_CENTRAL := Color(1.00, 1.00, 1.00)
const HANDLE_COLOR_HAND := Color(0.00, 1.00, 0.00) # green
const HANDLE_COLOR_FOOT := Color(0.00, 0.50, 1.00) # blue
const HANDLE_COLOR_HEAD := Color(1.00, 1.00, 0.00) # yellow
const HANDLE_COLOR_TORSO := Color(1.00, 0.00, 1.00) # magenta
const SKELETON_PATH := "Skeleton2D"
@@ -35,11 +37,28 @@ const QUICK_LOADS: Array = [
]
## IK handle node paths (relative to rig root), keyed by handle name.
## The four limb targets drive TwoBoneIK; "Head" is the LookAt aim point and
## "Torso" is the hip anchor (dragging it translates the whole rig via its
## RemoteTransform2D — no target-following logic).
const IK_HANDLE_PATHS: Dictionary = {
"Left_Hand": "IK_Targets/Left_Hand",
"Right_Hand": "IK_Targets/Right_Hand",
"Left_Leg": "IK_Targets/Left_Leg",
"Right_Leg": "IK_Targets/Right_Leg",
"Head": "IK_Targets/Head",
"Torso": "IK_Targets/Torso",
}
## Leaf bone → IK target node path (relative to rig root), keyed by bone name.
## Used by the bone overlay to draw the forearm/shin segments out to their
## wrist/ankle targets. The Head leaf is NOT listed here — its IK target is a
## LookAt aim point, so it draws along its own bone direction instead (see
## _draw_bones' leaf fallback).
const LEAF_BONE_IK_PATHS: Dictionary = {
"LeftLowerArm": "IK_Targets/Left_Hand",
"RightLowerArm": "IK_Targets/Right_Hand",
"LeftLowerLeg": "IK_Targets/Left_Leg",
"RightLowerLeg": "IK_Targets/Right_Leg",
}
# ---------------------------------------------------------------------------
@@ -282,9 +301,35 @@ func _draw_bones() -> void:
var origin := bone.global_position
_debug_overlay.draw_circle(origin, dot_r, color)
var parent := bone.get_parent()
if parent is Bone2D:
_debug_overlay.draw_line((parent as Bone2D).global_position, origin, color, line_w)
# Collect the bone's Bone2D children (true bone segments, including the
# nested lower bones).
var bone_children: Array[Bone2D] = []
for child in bone.get_children():
if child is Bone2D:
bone_children.append(child as Bone2D)
if bone_children.is_empty():
# Leaf bone — draw out to its IK target (forearm/shin), falling
# back to the bone's own direction (bone_angle + rotation) when
# there is no target (e.g. the Head, whose target is a LookAt aim
# point, not a joint).
var target := _leaf_bone_target(bone)
if target != null:
_debug_overlay.draw_line(origin, target.global_position, color, line_w)
else:
var fallback := origin + Vector2(bone.get_length(), 0.0).rotated(deg_to_rad(bone.bone_angle)).rotated(bone.global_rotation)
_debug_overlay.draw_line(origin, fallback, color, line_w)
else:
for child_bone in bone_children:
_debug_overlay.draw_line(origin, child_bone.global_position, color, line_w)
func _leaf_bone_target(bone: Bone2D) -> Node2D:
if not LEAF_BONE_IK_PATHS.has(bone.name):
return null
if _rig == null or not is_instance_valid(_rig):
return null
return _rig.get_node_or_null(NodePath(str(LEAF_BONE_IK_PATHS[bone.name]))) as Node2D
func _draw_ik_handles() -> void:
@@ -296,11 +341,34 @@ func _draw_ik_handles() -> void:
var handle := _ik_handles[handle_name] as Marker2D
if handle == null or not is_instance_valid(handle):
continue
var color := HANDLE_COLOR_HAND if handle_name.ends_with("Hand") else HANDLE_COLOR_FOOT
var color := _handle_color(handle_name)
var pos := handle.global_position
_debug_overlay.draw_circle(pos, r, color)
_debug_overlay.draw_arc(pos, r, 0.0, TAU, 24, Color(1.0, 1.0, 1.0, 0.8), outline_w)
# Head aim line — visual aid showing what the head bone is aiming at.
var head_handle: Marker2D = _ik_handles.get("Head", null) as Marker2D
if head_handle != null and is_instance_valid(head_handle) \
and _skeleton != null and is_instance_valid(_skeleton):
var head_bone := _skeleton.get_node_or_null(NodePath("Torso/Head")) as Bone2D
if head_bone != null and is_instance_valid(head_bone):
_debug_overlay.draw_line(
head_bone.global_position,
head_handle.global_position,
Color(1.0, 1.0, 0.0, 0.5),
1.5 / zoom
)
func _handle_color(handle_name: String) -> Color:
match handle_name:
"Head":
return HANDLE_COLOR_HEAD
"Torso":
return HANDLE_COLOR_TORSO
_:
return HANDLE_COLOR_HAND if handle_name.ends_with("Hand") else HANDLE_COLOR_FOOT
func _bone_color(bone_name: String) -> Color:
if bone_name.begins_with("Left"):