Add StkRigAdapter for runtime skeleton fitting and shape mounting
- Implemented StkRigAdapter class to adapt a master rig to a loaded .stk dictionary. - Added methods for fitting bone lengths, recalibrating IK targets, and mounting vector shapes. - Defined constants for default proportions and bone paths. - Included error handling for missing nodes and invalid data structures.
This commit is contained in:
+99
-12
@@ -48,10 +48,23 @@ const DEFAULT_POSITIONS: Dictionary = {
|
||||
"right_lower_leg": Vector2(165, 210),
|
||||
}
|
||||
|
||||
const FILE_VERSION := "1.3"
|
||||
const FILE_VERSION := "1.4"
|
||||
const FILE_FILTER := "*.stk ; Stickman Files"
|
||||
|
||||
const SUPPORTED_VERSIONS: Array[String] = ["1.0", "1.1", "1.2", "1.3"]
|
||||
const SUPPORTED_VERSIONS: Array[String] = ["1.0", "1.1", "1.2", "1.3", "1.4"]
|
||||
|
||||
# Phase 8: rig proportions (master_rig.tscn rest pose — see spec §2).
|
||||
const 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 X_AXIS_PARTS: PackedStringArray = [
|
||||
"left_upper_arm", "left_lower_arm", "right_upper_arm", "right_lower_arm",
|
||||
]
|
||||
|
||||
const SETTINGS_PATH := "user://settings.json"
|
||||
const SETTINGS_VERSION := "1.0"
|
||||
@@ -89,7 +102,11 @@ var _shape_clipboard: Dictionary = {}
|
||||
# Phase 6: recent colors
|
||||
var _recent_colors: Array[String] = []
|
||||
|
||||
# Phase 7: pose silhouette guide (default ON per user decision)
|
||||
var _show_guide: bool = true
|
||||
|
||||
var _edit_menu: PopupMenu
|
||||
var _view_menu: PopupMenu
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
@@ -156,9 +173,13 @@ func _setup_menu_bar() -> void:
|
||||
var view_menu: PopupMenu = PopupMenu.new()
|
||||
view_menu.name = "ViewMenu"
|
||||
view_menu.add_item("Reset Views", 0)
|
||||
view_menu.add_separator()
|
||||
view_menu.add_item(_guide_menu_label(), 1)
|
||||
view_menu.id_pressed.connect(_on_view_menu_id_pressed)
|
||||
view_menu.about_to_popup.connect(_on_view_menu_about_to_popup)
|
||||
_menu_bar.add_child(view_menu)
|
||||
_menu_bar.set_menu_title(_menu_bar.get_menu_count() - 1, "View")
|
||||
_view_menu = view_menu
|
||||
|
||||
|
||||
func _setup_dialogs() -> void:
|
||||
@@ -225,7 +246,9 @@ func _on_edit_menu_id_pressed(id: int) -> void:
|
||||
1: # Snap to Grid (checkable)
|
||||
_snap_enabled = not _snap_enabled
|
||||
if _edit_menu:
|
||||
_edit_menu.set_item_text(1, _snap_menu_label())
|
||||
var idx := _edit_menu.get_item_index(1)
|
||||
if idx >= 0:
|
||||
_edit_menu.set_item_text(idx, _snap_menu_label())
|
||||
_save_settings()
|
||||
_broadcast_settings()
|
||||
_update_snap_status_label()
|
||||
@@ -233,15 +256,38 @@ func _on_edit_menu_id_pressed(id: int) -> void:
|
||||
|
||||
func _on_edit_menu_about_to_popup() -> void:
|
||||
if _edit_menu:
|
||||
_edit_menu.set_item_text(1, _snap_menu_label())
|
||||
var idx := _edit_menu.get_item_index(1)
|
||||
if idx >= 0:
|
||||
_edit_menu.set_item_text(idx, _snap_menu_label())
|
||||
|
||||
|
||||
func _on_view_menu_id_pressed(id: int) -> void:
|
||||
if id == 0: # Reset Views
|
||||
for panel in _body_part_panels.values():
|
||||
if panel is BodyPartPanel:
|
||||
(panel as BodyPartPanel).reset_view()
|
||||
_whole_preview.reset_view()
|
||||
match id:
|
||||
0: # Reset Views
|
||||
for panel in _body_part_panels.values():
|
||||
if panel is BodyPartPanel:
|
||||
(panel as BodyPartPanel).reset_view()
|
||||
_whole_preview.reset_view()
|
||||
1: # Hide/Show Pose Guide
|
||||
_show_guide = not _show_guide
|
||||
_save_settings()
|
||||
_broadcast_settings()
|
||||
_update_guide_menu_item()
|
||||
|
||||
|
||||
func _on_view_menu_about_to_popup() -> void:
|
||||
_update_guide_menu_item()
|
||||
|
||||
|
||||
func _update_guide_menu_item() -> void:
|
||||
if _view_menu:
|
||||
var idx := _view_menu.get_item_index(1)
|
||||
if idx >= 0:
|
||||
_view_menu.set_item_text(idx, _guide_menu_label())
|
||||
|
||||
|
||||
func _guide_menu_label() -> String:
|
||||
return "Hide Pose Guide" if _show_guide else "Show Pose Guide"
|
||||
|
||||
|
||||
func _on_grid_config_confirmed() -> void:
|
||||
@@ -327,6 +373,36 @@ func _on_preview_part_moved(_part_name: String, _new_position: Vector2) -> void:
|
||||
# Data collection / JSON helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _compute_part_pivot_length(shapes_arr: Array, part_name: String) -> Dictionary:
|
||||
var min_x := INF
|
||||
var min_y := INF
|
||||
var max_x := -INF
|
||||
var max_y := -INF
|
||||
for sd in shapes_arr:
|
||||
if not sd is Dictionary:
|
||||
continue
|
||||
for p in (sd as Dictionary).get("points", []):
|
||||
if not p is Dictionary:
|
||||
continue
|
||||
var px: float = float((p as Dictionary).get("x", 0.0))
|
||||
var py: float = float((p as Dictionary).get("y", 0.0))
|
||||
min_x = minf(min_x, px)
|
||||
min_y = minf(min_y, py)
|
||||
max_x = maxf(max_x, px)
|
||||
max_y = maxf(max_y, py)
|
||||
|
||||
if min_x > max_x or min_y > max_y:
|
||||
return { "pivot": { "x": 0.0, "y": 0.0 }, "length": 0.0 }
|
||||
|
||||
var pivot := { "x": (min_x + max_x) * 0.5, "y": (min_y + max_y) * 0.5 }
|
||||
var length: float
|
||||
if X_AXIS_PARTS.has(part_name):
|
||||
length = max_x - min_x
|
||||
else:
|
||||
length = max_y - min_y
|
||||
return { "pivot": pivot, "length": length }
|
||||
|
||||
|
||||
func _collect_all_shape_data() -> Dictionary:
|
||||
var all_data: Dictionary = {}
|
||||
for part_name: String in BODY_PART_NAMES:
|
||||
@@ -336,11 +412,14 @@ func _collect_all_shape_data() -> Dictionary:
|
||||
var pos := _whole_preview.get_part_position(part_name)
|
||||
var rot := _whole_preview.get_part_rotation(part_name)
|
||||
var scl := _whole_preview.get_part_scale(part_name)
|
||||
var pl := _compute_part_pivot_length(shapes_arr, part_name)
|
||||
all_data[part_name] = {
|
||||
"shapes": shapes_arr,
|
||||
"position": {"x": pos.x, "y": pos.y},
|
||||
"rotation": rot,
|
||||
"scale": {"x": scl.x, "y": scl.y}
|
||||
"scale": {"x": scl.x, "y": scl.y},
|
||||
"pivot": pl["pivot"],
|
||||
"length": pl["length"],
|
||||
}
|
||||
return all_data
|
||||
|
||||
@@ -353,6 +432,7 @@ func _build_json_data() -> Dictionary:
|
||||
"version": FILE_VERSION,
|
||||
"stickman_name": _stickman_name_edit.text.strip_edges(),
|
||||
"part_order": _whole_preview.get_part_order(),
|
||||
"proportions": PROPORTIONS.duplicate(),
|
||||
"body_parts": body_parts,
|
||||
"metadata": {
|
||||
"created_at": time_str,
|
||||
@@ -369,7 +449,7 @@ func _apply_json_data(json: Variant) -> String:
|
||||
|
||||
var version: String = dict.get("version", "")
|
||||
if version not in SUPPORTED_VERSIONS:
|
||||
return "Unsupported file version: '%s' (expected '1.0', '1.1', '1.2', or '1.3')" % version
|
||||
return "Unsupported file version: '%s' (expected '1.0', '1.1', '1.2', '1.3', or '1.4')" % version
|
||||
|
||||
_stickman_name_edit.text = dict.get("stickman_name", "")
|
||||
|
||||
@@ -527,6 +607,7 @@ func _load_settings() -> void:
|
||||
var d := json as Dictionary
|
||||
_grid_size = int(d.get("grid_size", DEFAULT_GRID_SIZE))
|
||||
_snap_enabled = bool(d.get("snap_to_grid", false))
|
||||
_show_guide = bool(d.get("show_pose_guide", true))
|
||||
_grid_size = clampi(_grid_size, MIN_GRID_SIZE, MAX_GRID_SIZE)
|
||||
|
||||
var raw_colors: Variant = d.get("recent_colors", [])
|
||||
@@ -539,7 +620,11 @@ func _load_settings() -> void:
|
||||
_recent_colors.pop_back()
|
||||
|
||||
if _edit_menu:
|
||||
_edit_menu.set_item_text(1, _snap_menu_label())
|
||||
var idx := _edit_menu.get_item_index(1)
|
||||
if idx >= 0:
|
||||
_edit_menu.set_item_text(idx, _snap_menu_label())
|
||||
if _view_menu:
|
||||
_update_guide_menu_item()
|
||||
|
||||
|
||||
func _save_settings() -> void:
|
||||
@@ -548,6 +633,7 @@ func _save_settings() -> void:
|
||||
"grid_size": _grid_size,
|
||||
"snap_to_grid": _snap_enabled,
|
||||
"recent_colors": _recent_colors,
|
||||
"show_pose_guide": _show_guide,
|
||||
}
|
||||
var file := FileAccess.open(SETTINGS_PATH, FileAccess.WRITE)
|
||||
if file:
|
||||
@@ -563,6 +649,7 @@ func _broadcast_settings() -> void:
|
||||
p.set_snap_enabled(_snap_enabled)
|
||||
_whole_preview.set_grid_size(_grid_size)
|
||||
_whole_preview.set_snap_enabled(_snap_enabled)
|
||||
_whole_preview.set_show_guide(_show_guide)
|
||||
|
||||
|
||||
func _snap_menu_label() -> String:
|
||||
|
||||
Reference in New Issue
Block a user