feat: Implement Stickman Factory and Test Harness for runtime rigging
- Added StickmanFactory to load and instantiate .stk files into master_rig.tscn. - Created a Test Harness scene for debugging bone scales, vector drawing offsets, and IK limits. - Updated stk_rig_adapter to fit head bone and mount head shapes as full geometry. - Enhanced README and PROJECT documentation to reflect new features and usage. - Introduced UI elements for file selection and visual debugging in the Test Harness. - Refactored code to improve clarity and maintainability, including removal of unused functions.
This commit is contained in:
+15
-49
@@ -61,6 +61,8 @@ const IK_RIGHT_HAND := "IK_Targets/Right_Hand"
|
||||
const IK_LEFT_LEG := "IK_Targets/Left_Leg"
|
||||
const IK_RIGHT_LEG := "IK_Targets/Right_Leg"
|
||||
|
||||
const HEAD_BONE_PATH := "Skeleton2D/Torso/Head"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -90,6 +92,7 @@ static func _fit_bones(stk_data: Dictionary, rig: Node2D) -> void:
|
||||
var la := float(proportions.get("lower_arm_length", DEFAULT_PROPORTIONS["lower_arm_length"]))
|
||||
var ul := float(proportions.get("upper_leg_length", DEFAULT_PROPORTIONS["upper_leg_length"]))
|
||||
var ll := float(proportions.get("lower_leg_length", DEFAULT_PROPORTIONS["lower_leg_length"]))
|
||||
var torso := float(proportions.get("torso_length", DEFAULT_PROPORTIONS["torso_length"]))
|
||||
|
||||
# Arms — upper length + lower-bone origin on X.
|
||||
_set_prop(rig, BONE_PATHS["left_upper_arm"], "length", ua)
|
||||
@@ -107,6 +110,12 @@ static func _fit_bones(stk_data: Dictionary, rig: Node2D) -> void:
|
||||
_set_prop(rig, BONE_PATHS["right_lower_leg"], "position", Vector2(0.0, ul))
|
||||
_set_prop(rig, BONE_PATHS["right_lower_leg"], "length", ll)
|
||||
|
||||
# Head — position the head bone at the top of the torso (preserve the
|
||||
# existing x so the rig's ~ -0.1288 x-offset is retained).
|
||||
var head_bone := rig.get_node_or_null(NodePath(HEAD_BONE_PATH))
|
||||
if head_bone is Node2D:
|
||||
_set_prop(rig, HEAD_BONE_PATH, "position", Vector2((head_bone as Node2D).position.x, -torso))
|
||||
|
||||
|
||||
static func _set_prop(rig: Node2D, path: String, prop: String, value: Variant) -> void:
|
||||
var node := rig.get_node_or_null(NodePath(path))
|
||||
@@ -183,16 +192,17 @@ static func _mount_shapes(stk_data: Dictionary, rig: Node2D) -> void:
|
||||
pivot = Vector2(float(pv.get("x", 0.0)), float(pv.get("y", 0.0)))
|
||||
part_length = float(pd.get("length", 0.0))
|
||||
|
||||
# Head: prefer setting the circle script's exports.
|
||||
if part_name == "head" and ("radius" in visual):
|
||||
_mount_head_circle(visual, shapes)
|
||||
continue
|
||||
|
||||
var bone_length := _bone_length_for(part_name, proportions)
|
||||
var scale_factor := 1.0
|
||||
if part_name != "head" and part_length > 0.0001:
|
||||
scale_factor = bone_length / part_length
|
||||
|
||||
# Phase 9: the Body/Head node is a plain Node2D carrying an inline
|
||||
# @tool circle-drawing script; clear it so the head is mounted with the
|
||||
# same full-geometry path as every other part.
|
||||
if part_name == "head":
|
||||
visual.set_script(null)
|
||||
|
||||
_reset_own_geometry(visual)
|
||||
_clear_visual_children(visual)
|
||||
|
||||
@@ -217,21 +227,6 @@ static func _bone_length_for(part_name: String, proportions: Dictionary) -> floa
|
||||
return 1.0
|
||||
|
||||
|
||||
static func _mount_head_circle(visual: Node, shapes: Array) -> void:
|
||||
_clear_visual_children(visual)
|
||||
if shapes.is_empty():
|
||||
visual.set("radius", 0.0)
|
||||
return
|
||||
|
||||
var radius := 0.0
|
||||
var bbox := _compute_shapes_bbox(shapes)
|
||||
if bbox.size.x > 0.0001 or bbox.size.y > 0.0001:
|
||||
var diameter: float = bbox.size.y if bbox.size.y > 0.0001 else bbox.size.x
|
||||
radius = diameter * 0.5
|
||||
visual.set("radius", radius)
|
||||
visual.set("color", _first_shape_color(shapes))
|
||||
|
||||
|
||||
static func _mount_shape(visual: Node, shape: Dictionary, pivot: Vector2, scale_factor: float) -> void:
|
||||
var pts := _transform_points(shape.get("points", []), pivot, scale_factor)
|
||||
if pts.size() < 2:
|
||||
@@ -288,32 +283,3 @@ static func _clear_visual_children(visual: Node) -> void:
|
||||
if child is Line2D or child is Polygon2D:
|
||||
visual.remove_child(child)
|
||||
child.queue_free()
|
||||
|
||||
|
||||
static func _compute_shapes_bbox(shapes: Array) -> Rect2:
|
||||
var min_x := INF
|
||||
var min_y := INF
|
||||
var max_x := -INF
|
||||
var max_y := -INF
|
||||
for shape in shapes:
|
||||
if not shape is Dictionary:
|
||||
continue
|
||||
var pts_var: Variant = (shape as Dictionary).get("points", [])
|
||||
if pts_var is Array:
|
||||
for p in pts_var as Array:
|
||||
if p is Dictionary:
|
||||
var d := p as Dictionary
|
||||
min_x = minf(min_x, float(d.get("x", 0.0)))
|
||||
min_y = minf(min_y, float(d.get("y", 0.0)))
|
||||
max_x = maxf(max_x, float(d.get("x", 0.0)))
|
||||
max_y = maxf(max_y, float(d.get("y", 0.0)))
|
||||
if min_x > max_x or min_y > max_y:
|
||||
return Rect2()
|
||||
return Rect2(Vector2(min_x, min_y), Vector2(max_x - min_x, max_y - min_y))
|
||||
|
||||
|
||||
static func _first_shape_color(shapes: Array) -> Color:
|
||||
for shape in shapes:
|
||||
if shape is Dictionary:
|
||||
return Color.from_string(str((shape as Dictionary).get("color", "#ffffff")), Color.WHITE)
|
||||
return Color.WHITE
|
||||
|
||||
Reference in New Issue
Block a user