class_name StkRigAdapter extends RefCounted ## StkRigAdapter - Standalone runtime adapter (Phase 8). ## ## Fits an instantiated master_rig.tscn to a loaded .stk dictionary: re-fits ## the skeleton bone lengths, recalibrates the IK targets, and mounts the ## .stk vector shapes onto the rig's Body/ visual nodes. Consumed by a future ## runtime pipeline, never referenced by the editor. # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- const DEFAULT_LINE_WIDTH := 16.0 const ELBOW_REST_Y := -256.0 const DEFAULT_PROPORTIONS: Dictionary = { "upper_arm_length": 168.0, "lower_arm_length": 200.0, "upper_leg_length": 200.0, "lower_leg_length": 200.0, "torso_length": 391.5, } const PART_KEYS: PackedStringArray = [ "head", "torso", "left_upper_arm", "left_lower_arm", "right_upper_arm", "right_lower_arm", "left_upper_leg", "left_lower_leg", "right_upper_leg", "right_lower_leg", ] ## Bone node paths (relative to rig root), keyed by part name. const BONE_PATHS: Dictionary = { "left_upper_arm": "Skeleton2D/Torso/LeftUpperArm", "left_lower_arm": "Skeleton2D/Torso/LeftUpperArm/LeftLowerArm", "right_upper_arm": "Skeleton2D/Torso/RightUpperArm", "right_lower_arm": "Skeleton2D/Torso/RightUpperArm/RightLowerArm", "left_upper_leg": "Skeleton2D/Torso/LeftUpperLeg", "left_lower_leg": "Skeleton2D/Torso/LeftUpperLeg/LeftLowerLeg", "right_upper_leg": "Skeleton2D/Torso/RightUpperLeg", "right_lower_leg": "Skeleton2D/Torso/RightUpperLeg/RightLowerLeg", } ## Body visual node paths (relative to rig root), keyed by part name. const BODY_PATHS: Dictionary = { "head": "Body/Head", "torso": "Body/Body", "left_upper_arm": "Body/LeftUpperArm", "left_lower_arm": "Body/LeftLowerArm", "right_upper_arm": "Body/RightUpperArm", "right_lower_arm": "Body/RightLowerArm", "left_upper_leg": "Body/LeftUpperLeg", "left_lower_leg": "Body/LeftLowerLeg", "right_upper_leg": "Body/RightUpperLeg", "right_lower_leg": "Body/RightLowerLeg", } const IK_LEFT_HAND := "IK_Targets/Left_Hand" 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 # --------------------------------------------------------------------------- static func apply(stk_data: Dictionary, rig: Node2D) -> void: _fit_bones(stk_data, rig) _recalibrate_ik(stk_data, rig) _mount_shapes(stk_data, rig) # --------------------------------------------------------------------------- # Proportions # --------------------------------------------------------------------------- static func _get_proportions(stk_data: Dictionary) -> Dictionary: var raw: Variant = stk_data.get("proportions", {}) if raw is Dictionary: return raw as Dictionary return {} # --------------------------------------------------------------------------- # Bone fitting # --------------------------------------------------------------------------- static func _fit_bones(stk_data: Dictionary, rig: Node2D) -> void: var proportions := _get_proportions(stk_data) var ua := float(proportions.get("upper_arm_length", DEFAULT_PROPORTIONS["upper_arm_length"])) 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) _set_prop(rig, BONE_PATHS["left_lower_arm"], "position", Vector2(-ua, 0.0)) _set_prop(rig, BONE_PATHS["left_lower_arm"], "length", la) _set_prop(rig, BONE_PATHS["right_upper_arm"], "length", ua) _set_prop(rig, BONE_PATHS["right_lower_arm"], "position", Vector2(ua, 0.0)) _set_prop(rig, BONE_PATHS["right_lower_arm"], "length", la) # Legs — upper length + lower-bone origin on Y. _set_prop(rig, BONE_PATHS["left_upper_leg"], "length", ul) _set_prop(rig, BONE_PATHS["left_lower_leg"], "position", Vector2(0.0, ul)) _set_prop(rig, BONE_PATHS["left_lower_leg"], "length", ll) _set_prop(rig, BONE_PATHS["right_upper_leg"], "length", ul) _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)) if node == null: push_warning("StkRigAdapter: missing node '%s'; skipped '%s'." % [path, prop]) return node.set(prop, value) # --------------------------------------------------------------------------- # IK target recalibration # --------------------------------------------------------------------------- static func _recalibrate_ik(stk_data: Dictionary, rig: Node2D) -> void: var proportions := _get_proportions(stk_data) var ua := float(proportions.get("upper_arm_length", DEFAULT_PROPORTIONS["upper_arm_length"])) 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 left_leg := rig.get_node_or_null(NodePath(IK_LEFT_LEG)) as Node2D if left_leg != null: left_leg.position = Vector2(left_leg.position.x, ul + ll) else: push_warning("StkRigAdapter: missing IK target '%s'." % IK_LEFT_LEG) var right_leg := rig.get_node_or_null(NodePath(IK_RIGHT_LEG)) as Node2D if right_leg != null: right_leg.position = Vector2(right_leg.position.x, ul + ll) else: push_warning("StkRigAdapter: missing IK target '%s'." % IK_RIGHT_LEG) var left_hand := rig.get_node_or_null(NodePath(IK_LEFT_HAND)) as Node2D if left_hand != null: left_hand.position = Vector2(-ua, ELBOW_REST_Y - la) else: push_warning("StkRigAdapter: missing IK target '%s'." % IK_LEFT_HAND) var right_hand := rig.get_node_or_null(NodePath(IK_RIGHT_HAND)) as Node2D if right_hand != null: right_hand.position = Vector2(ua, ELBOW_REST_Y - la) else: push_warning("StkRigAdapter: missing IK target '%s'." % IK_RIGHT_HAND) # --------------------------------------------------------------------------- # Visual shape mount # --------------------------------------------------------------------------- static func _mount_shapes(stk_data: Dictionary, rig: Node2D) -> void: var body_parts_var: Variant = stk_data.get("body_parts", {}) if not body_parts_var is Dictionary: push_warning("StkRigAdapter: 'body_parts' missing or invalid; no shapes mounted.") return var body_parts: Dictionary = body_parts_var as Dictionary var proportions := _get_proportions(stk_data) for part_name: String in PART_KEYS: var visual := rig.get_node_or_null(NodePath(BODY_PATHS[part_name])) if visual == null: push_warning("StkRigAdapter: missing Body node for part '%s'; skipped." % part_name) continue var shapes: Array = [] var pivot := Vector2.ZERO var part_length := 0.0 var part_data: Variant = body_parts.get(part_name, {}) if part_data is Dictionary: var pd := part_data as Dictionary var shapes_var: Variant = pd.get("shapes", []) if shapes_var is Array: shapes = shapes_var as Array var pivot_var: Variant = pd.get("pivot", {}) if pivot_var is Dictionary: var pv := pivot_var as Dictionary pivot = Vector2(float(pv.get("x", 0.0)), float(pv.get("y", 0.0))) part_length = float(pd.get("length", 0.0)) 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) for shape in shapes: if shape is Dictionary: _mount_shape(visual, shape as Dictionary, pivot, scale_factor) static func _bone_length_for(part_name: String, proportions: Dictionary) -> float: match part_name: "left_upper_arm", "right_upper_arm": return float(proportions.get("upper_arm_length", DEFAULT_PROPORTIONS["upper_arm_length"])) "left_lower_arm", "right_lower_arm": return float(proportions.get("lower_arm_length", DEFAULT_PROPORTIONS["lower_arm_length"])) "left_upper_leg", "right_upper_leg": return float(proportions.get("upper_leg_length", DEFAULT_PROPORTIONS["upper_leg_length"])) "left_lower_leg", "right_lower_leg": return float(proportions.get("lower_leg_length", DEFAULT_PROPORTIONS["lower_leg_length"])) "torso": return float(proportions.get("torso_length", DEFAULT_PROPORTIONS["torso_length"])) _: return 1.0 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: return var color := Color.from_string(str(shape.get("color", "#ffffff")), Color.WHITE) var closed := bool(shape.get("closed", false)) if closed: var poly := Polygon2D.new() poly.polygon = pts poly.color = color visual.add_child(poly) var outline := Line2D.new() outline.points = pts outline.closed = true outline.width = DEFAULT_LINE_WIDTH outline.default_color = color visual.add_child(outline) else: var line := Line2D.new() line.points = pts line.width = DEFAULT_LINE_WIDTH line.default_color = color visual.add_child(line) static func _transform_points(pts_var: Variant, pivot: Vector2, scale_factor: float) -> PackedVector2Array: var out := PackedVector2Array() if pts_var is Array: for p in pts_var as Array: if p is Dictionary: var d := p as Dictionary var pt := Vector2(float(d.get("x", 0.0)), float(d.get("y", 0.0))) out.append((pt - pivot) * scale_factor) elif p is Vector2: out.append(((p as Vector2) - pivot) * scale_factor) elif pts_var is PackedVector2Array: for pt in pts_var as PackedVector2Array: out.append((pt - pivot) * scale_factor) return out static func _reset_own_geometry(visual: Node) -> void: if visual is Line2D: (visual as Line2D).points = PackedVector2Array() elif visual is Polygon2D: (visual as Polygon2D).polygon = PackedVector2Array() static func _clear_visual_children(visual: Node) -> void: for child in visual.get_children(): if child is Line2D or child is Polygon2D: visual.remove_child(child) child.queue_free()