feat: Implement facing profiles and bend direction toggles in test harness
- Added functionality for skeleton IK bone switches, allowing users to dynamically change the bend direction of joints via a context menu. - Introduced facing profiles (LEFT, RIGHT, FORWARD) that modify the bend direction of limbs based on the selected profile. - Implemented body part z-ordering based on the facing profile, ensuring correct rendering order of limbs relative to the torso. - Added a coordinates display panel that shows the position and rotation of the Skeleton2D and its bones, as well as IK target positions. - Created a new script to generate a walk animation for the stickman rig, including keyframe tracks for various IK targets. - Updated documentation to reflect the new features and their specifications.
This commit is contained in:
@@ -28,6 +28,25 @@ 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"
|
||||
const BODY_CONTAINER_PATH := "Body"
|
||||
|
||||
## Width of the coordinates readout panel (Task 3).
|
||||
const COORDS_PANEL_WIDTH: float = 320.0
|
||||
|
||||
## Bone node paths (relative to Skeleton2D) shown in the coordinates panel, in
|
||||
## display order. The display name is the last path segment.
|
||||
const COORD_BONE_PATHS: Array[String] = [
|
||||
"Torso",
|
||||
"Torso/Head",
|
||||
"Torso/LeftUpperArm",
|
||||
"Torso/LeftUpperArm/LeftLowerArm",
|
||||
"Torso/RightUpperArm",
|
||||
"Torso/RightUpperArm/RightLowerArm",
|
||||
"Torso/LeftUpperLeg",
|
||||
"Torso/LeftUpperLeg/LeftLowerLeg",
|
||||
"Torso/RightUpperLeg",
|
||||
"Torso/RightUpperLeg/RightLowerLeg",
|
||||
]
|
||||
|
||||
## Quick-load buttons: [label, res:// path].
|
||||
const QUICK_LOADS: Array = [
|
||||
@@ -61,6 +80,64 @@ const LEAF_BONE_IK_PATHS: Dictionary = {
|
||||
"RightLowerLeg": "IK_Targets/Right_Leg",
|
||||
}
|
||||
|
||||
## Facing profiles for the rig's TwoBoneIK "Flip Bend Direction" flags (Phase
|
||||
## 9 / Task 1). LEFT = facing left, RIGHT = facing right, FORWARD = facing the
|
||||
## user. The enum values are used directly as the facing-menu item ids.
|
||||
enum FacingProfile { LEFT, RIGHT, FORWARD }
|
||||
|
||||
const JOINT_HIT_RADIUS_PX: float = 14.0 # screen-space grab radius for bend joints
|
||||
|
||||
## Bend joints (upper↔lower limb connectors) whose TwoBoneIK "Flip Bend
|
||||
## Direction" flag is user-controllable. Keyed by joint name; values are the
|
||||
## lower bone's NodePath relative to the Skeleton2D.
|
||||
const BEND_JOINTS: Array[String] = ["LeftArm", "RightArm", "LeftLeg", "RightLeg"]
|
||||
|
||||
const BEND_JOINT_BONE_PATHS: Dictionary = {
|
||||
"LeftArm": "Torso/LeftUpperArm/LeftLowerArm",
|
||||
"RightArm": "Torso/RightUpperArm/RightLowerArm",
|
||||
"LeftLeg": "Torso/LeftUpperLeg/LeftLowerLeg",
|
||||
"RightLeg": "Torso/RightUpperLeg/RightLowerLeg",
|
||||
}
|
||||
|
||||
## flip_bend_direction value per facing profile, keyed by bend-joint name.
|
||||
const PROFILE_FLAGS: Dictionary = {
|
||||
FacingProfile.LEFT: { "LeftArm": false, "RightArm": false, "LeftLeg": true, "RightLeg": true },
|
||||
FacingProfile.RIGHT: { "LeftArm": true, "RightArm": true, "LeftLeg": false, "RightLeg": false },
|
||||
FacingProfile.FORWARD: { "LeftArm": false, "RightArm": true, "LeftLeg": true, "RightLeg": false },
|
||||
}
|
||||
|
||||
## Body/* visual part node names in draw order (back-to-front) per facing
|
||||
## profile (Phase 9 / Task 2). First entry draws backmost, last frontmost.
|
||||
## Upper limbs stay behind lower limbs; on the far (behind-torso) side the arm
|
||||
## pair draws behind the leg pair, on the near (in-front) side the arm pair
|
||||
## draws in front of the leg pair; the head is always frontmost.
|
||||
const Z_ORDER_BY_PROFILE: Dictionary = {
|
||||
FacingProfile.FORWARD: [
|
||||
"Body",
|
||||
"LeftUpperLeg", "RightUpperLeg",
|
||||
"LeftLowerLeg", "RightLowerLeg",
|
||||
"LeftUpperArm", "RightUpperArm",
|
||||
"LeftLowerArm", "RightLowerArm",
|
||||
"Head",
|
||||
],
|
||||
FacingProfile.LEFT: [
|
||||
"LeftUpperArm", "LeftLowerArm",
|
||||
"LeftUpperLeg", "LeftLowerLeg",
|
||||
"Body",
|
||||
"RightUpperLeg", "RightLowerLeg",
|
||||
"RightUpperArm", "RightLowerArm",
|
||||
"Head",
|
||||
],
|
||||
FacingProfile.RIGHT: [
|
||||
"RightUpperArm", "RightLowerArm",
|
||||
"RightUpperLeg", "RightLowerLeg",
|
||||
"Body",
|
||||
"LeftUpperLeg", "LeftLowerLeg",
|
||||
"LeftUpperArm", "LeftLowerArm",
|
||||
"Head",
|
||||
],
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Runtime-built node references
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -72,6 +149,8 @@ var _camera: Camera2D
|
||||
var _debug_overlay: Node2D
|
||||
var _status_label: Label
|
||||
var _file_dialog: FileDialog
|
||||
var _coords_panel: PanelContainer
|
||||
var _coords_label: RichTextLabel
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
@@ -79,15 +158,27 @@ var _file_dialog: FileDialog
|
||||
|
||||
var _rig: Node2D = null
|
||||
var _skeleton: Skeleton2D = null
|
||||
var _body_container: Node2D = null
|
||||
var _ik_handles: Dictionary = {} # { String : Marker2D }
|
||||
var _coord_bones: Dictionary = {} # { String : Bone2D }
|
||||
|
||||
var _show_bones: bool = true
|
||||
var _show_ik: bool = true
|
||||
var _show_coords: bool = true
|
||||
|
||||
var _is_panning: bool = false
|
||||
var _pan_last: Vector2 = Vector2.ZERO
|
||||
var _dragging_handle: Marker2D = null
|
||||
|
||||
# Task 1: IK bend-direction state (survives respawn; matches authored defaults).
|
||||
var _facing_profile: int = FacingProfile.FORWARD
|
||||
var _context_joint: String = ""
|
||||
var _bend_joint_bones: Dictionary = {} # { String : Bone2D }
|
||||
var _bend_modifications: Dictionary = {} # { String : SkeletonModification2DTwoBoneIK }
|
||||
var _facing_button: MenuButton
|
||||
var _facing_menu: PopupMenu
|
||||
var _context_menu: PopupMenu
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -128,6 +219,16 @@ func _build_ui() -> void:
|
||||
hbox.add_theme_constant_override("separation", 8)
|
||||
top_bar.add_child(hbox)
|
||||
|
||||
_facing_button = MenuButton.new()
|
||||
_facing_button.text = "Facing"
|
||||
_facing_menu = _facing_button.get_popup()
|
||||
_facing_menu.add_item(_facing_menu_label(FacingProfile.LEFT), FacingProfile.LEFT)
|
||||
_facing_menu.add_item(_facing_menu_label(FacingProfile.RIGHT), FacingProfile.RIGHT)
|
||||
_facing_menu.add_item(_facing_menu_label(FacingProfile.FORWARD), FacingProfile.FORWARD)
|
||||
_facing_menu.id_pressed.connect(_on_facing_menu_id_pressed)
|
||||
_facing_menu.about_to_popup.connect(_update_facing_menu_labels)
|
||||
hbox.add_child(_facing_button)
|
||||
|
||||
var open_btn := Button.new()
|
||||
open_btn.text = "Open .stk…"
|
||||
open_btn.pressed.connect(_on_open_pressed)
|
||||
@@ -151,6 +252,12 @@ func _build_ui() -> void:
|
||||
ik_cb.toggled.connect(_on_show_ik_toggled)
|
||||
hbox.add_child(ik_cb)
|
||||
|
||||
var coords_cb := CheckBox.new()
|
||||
coords_cb.text = "Show Coords"
|
||||
coords_cb.button_pressed = true
|
||||
coords_cb.toggled.connect(_on_show_coords_toggled)
|
||||
hbox.add_child(coords_cb)
|
||||
|
||||
_status_label = Label.new()
|
||||
_status_label.text = "No file loaded"
|
||||
_status_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
@@ -184,6 +291,10 @@ func _build_ui() -> void:
|
||||
_debug_overlay.draw.connect(_on_debug_overlay_draw)
|
||||
_world.add_child(_debug_overlay)
|
||||
|
||||
# Coordinates readout panel (added after the viewport container so it
|
||||
# renders in front of it).
|
||||
_build_coords_panel()
|
||||
|
||||
# File dialog for "Open .stk…".
|
||||
_file_dialog = FileDialog.new()
|
||||
_file_dialog.title = "Open .stk"
|
||||
@@ -193,6 +304,55 @@ func _build_ui() -> void:
|
||||
_file_dialog.file_selected.connect(_on_file_selected)
|
||||
add_child(_file_dialog)
|
||||
|
||||
# Right-click context menu for toggling a bend joint's bend direction.
|
||||
_context_menu = PopupMenu.new()
|
||||
_context_menu.add_item("Invert Bend", 0)
|
||||
_context_menu.id_pressed.connect(_on_context_menu_id_pressed)
|
||||
add_child(_context_menu)
|
||||
|
||||
|
||||
func _build_coords_panel() -> void:
|
||||
_coords_panel = PanelContainer.new()
|
||||
_coords_panel.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
|
||||
_coords_panel.offset_top = 40.0
|
||||
_coords_panel.offset_right = -8.0
|
||||
_coords_panel.offset_left = -COORDS_PANEL_WIDTH
|
||||
_coords_panel.grow_vertical = Control.GROW_DIRECTION_END
|
||||
_coords_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
_coords_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.0, 0.0, 0.0, 0.55)
|
||||
style.border_color = Color(1.0, 1.0, 1.0, 0.12)
|
||||
style.border_width_left = 1
|
||||
style.border_width_top = 1
|
||||
style.border_width_right = 1
|
||||
style.border_width_bottom = 1
|
||||
style.corner_radius_top_left = 4
|
||||
style.corner_radius_top_right = 4
|
||||
style.corner_radius_bottom_right = 4
|
||||
style.corner_radius_bottom_left = 4
|
||||
style.content_margin_left = 8.0
|
||||
style.content_margin_top = 8.0
|
||||
style.content_margin_right = 8.0
|
||||
style.content_margin_bottom = 8.0
|
||||
_coords_panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
_coords_label = RichTextLabel.new()
|
||||
_coords_label.selection_enabled = true
|
||||
_coords_label.context_menu_enabled = true
|
||||
_coords_label.fit_content = true
|
||||
_coords_label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||
_coords_label.scroll_active = false
|
||||
_coords_label.focus_mode = Control.FOCUS_CLICK
|
||||
var font := SystemFont.new()
|
||||
font.font_names = PackedStringArray(["Consolas", "Menlo", "DejaVu Sans Mono", "Courier New"])
|
||||
_coords_label.add_theme_font_override("normal_font", font)
|
||||
_coords_label.add_theme_font_size_override("normal_font_size", 18)
|
||||
|
||||
_coords_panel.add_child(_coords_label)
|
||||
add_child(_coords_panel)
|
||||
|
||||
|
||||
func _on_viewport_container_resized() -> void:
|
||||
var size := _viewport_container.size
|
||||
@@ -223,6 +383,11 @@ func _handle_mouse_button(mb: InputEventMouseButton) -> void:
|
||||
_apply_zoom(mb.position, 1.0 / ZOOM_STEP)
|
||||
return
|
||||
|
||||
if mb.button_index == MOUSE_BUTTON_RIGHT:
|
||||
if mb.pressed:
|
||||
_handle_right_click(mb.position)
|
||||
return
|
||||
|
||||
if mb.button_index == MOUSE_BUTTON_LEFT:
|
||||
if mb.pressed:
|
||||
_dragging_handle = _hit_test_handle(_viewport_to_world(mb.position))
|
||||
@@ -230,6 +395,33 @@ func _handle_mouse_button(mb: InputEventMouseButton) -> void:
|
||||
_dragging_handle = null
|
||||
|
||||
|
||||
func _handle_right_click(screen_pos: Vector2) -> void:
|
||||
if _skeleton == null or not is_instance_valid(_skeleton):
|
||||
return
|
||||
var world_pos := _viewport_to_world(screen_pos)
|
||||
var joint := _hit_test_bend_joint(world_pos)
|
||||
if joint.is_empty():
|
||||
return
|
||||
_context_joint = joint
|
||||
_context_menu.set_item_text(0, _context_menu_label(joint))
|
||||
_context_menu.popup(Rect2i(Vector2i(screen_pos), Vector2i(1, 1)))
|
||||
|
||||
|
||||
func _hit_test_bend_joint(world_pos: Vector2) -> String:
|
||||
var hit_radius := JOINT_HIT_RADIUS_PX / _camera.zoom.x
|
||||
var best_joint := ""
|
||||
var best_dist := hit_radius
|
||||
for joint: String in BEND_JOINTS:
|
||||
var bone: Bone2D = _bend_joint_bones.get(joint) as Bone2D
|
||||
if bone == null or not is_instance_valid(bone):
|
||||
continue
|
||||
var dist := world_pos.distance_to(bone.global_position)
|
||||
if dist <= best_dist:
|
||||
best_joint = joint
|
||||
best_dist = dist
|
||||
return best_joint
|
||||
|
||||
|
||||
func _handle_mouse_motion(mm: InputEventMouseMotion) -> void:
|
||||
if _is_panning:
|
||||
var delta := mm.position - _pan_last
|
||||
@@ -377,6 +569,65 @@ func _bone_color(bone_name: String) -> Color:
|
||||
return BONE_COLOR_RIGHT
|
||||
return BONE_COLOR_CENTRAL
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Facing profile & bend-direction toggles
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _facing_menu_label(profile: int) -> String:
|
||||
return ("[√] " if profile == _facing_profile else "") + FacingProfile.keys()[profile].capitalize()
|
||||
|
||||
|
||||
func _update_facing_menu_labels() -> void:
|
||||
if _facing_menu == null:
|
||||
return
|
||||
for profile: int in [FacingProfile.LEFT, FacingProfile.RIGHT, FacingProfile.FORWARD]:
|
||||
var idx := _facing_menu.get_item_index(profile)
|
||||
if idx >= 0:
|
||||
_facing_menu.set_item_text(idx, _facing_menu_label(profile))
|
||||
|
||||
|
||||
func _on_facing_menu_id_pressed(id: int) -> void:
|
||||
_apply_facing_profile(id)
|
||||
_update_facing_menu_labels()
|
||||
|
||||
|
||||
func _apply_facing_profile(profile: int) -> void:
|
||||
_facing_profile = profile
|
||||
for joint: String in BEND_JOINTS:
|
||||
var mod: SkeletonModification2DTwoBoneIK = _bend_modifications.get(joint) as SkeletonModification2DTwoBoneIK
|
||||
if mod == null:
|
||||
continue
|
||||
mod.flip_bend_direction = bool(PROFILE_FLAGS[profile][joint])
|
||||
_apply_body_z_order()
|
||||
_debug_overlay.queue_redraw()
|
||||
|
||||
|
||||
func _apply_body_z_order() -> void:
|
||||
if _body_container == null or not is_instance_valid(_body_container):
|
||||
return
|
||||
var order: Array = Z_ORDER_BY_PROFILE.get(_facing_profile, Z_ORDER_BY_PROFILE[FacingProfile.FORWARD])
|
||||
for part_name: String in order:
|
||||
var part := _body_container.get_node_or_null(NodePath(part_name))
|
||||
if part != null:
|
||||
_body_container.move_child(part, _body_container.get_child_count() - 1)
|
||||
|
||||
|
||||
func _context_menu_label(joint: String) -> String:
|
||||
var mod: SkeletonModification2DTwoBoneIK = _bend_modifications.get(joint) as SkeletonModification2DTwoBoneIK
|
||||
if mod == null:
|
||||
return "Invert Bend"
|
||||
return "Normal Bend" if mod.flip_bend_direction else "Invert Bend"
|
||||
|
||||
|
||||
func _on_context_menu_id_pressed(_id: int) -> void:
|
||||
if _context_joint.is_empty():
|
||||
return
|
||||
var mod: SkeletonModification2DTwoBoneIK = _bend_modifications.get(_context_joint) as SkeletonModification2DTwoBoneIK
|
||||
if mod == null:
|
||||
return
|
||||
mod.flip_bend_direction = not mod.flip_bend_direction
|
||||
_debug_overlay.queue_redraw()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Spawn / swap
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -407,6 +658,7 @@ func _load_and_spawn(path: String) -> void:
|
||||
|
||||
_resolve_rig_nodes()
|
||||
_ensure_modification_stack_enabled()
|
||||
_apply_facing_profile(_facing_profile)
|
||||
|
||||
_status_label.text = path.get_file()
|
||||
_recenter_camera()
|
||||
@@ -418,7 +670,12 @@ func _free_current_rig() -> void:
|
||||
_rig.queue_free()
|
||||
_rig = null
|
||||
_skeleton = null
|
||||
_body_container = null
|
||||
_ik_handles.clear()
|
||||
_bend_joint_bones.clear()
|
||||
_bend_modifications.clear()
|
||||
_coord_bones.clear()
|
||||
_context_joint = ""
|
||||
_dragging_handle = null
|
||||
|
||||
|
||||
@@ -427,6 +684,10 @@ func _resolve_rig_nodes() -> void:
|
||||
if _skeleton == null:
|
||||
push_warning("TestHarness: missing '%s' node in rig." % SKELETON_PATH)
|
||||
|
||||
_body_container = _rig.get_node_or_null(NodePath(BODY_CONTAINER_PATH)) as Node2D
|
||||
if _body_container == null:
|
||||
push_warning("TestHarness: missing '%s' node in rig." % BODY_CONTAINER_PATH)
|
||||
|
||||
_ik_handles.clear()
|
||||
for handle_name: String in IK_HANDLE_PATHS:
|
||||
var handle := _rig.get_node_or_null(NodePath(IK_HANDLE_PATHS[handle_name])) as Marker2D
|
||||
@@ -435,6 +696,53 @@ func _resolve_rig_nodes() -> void:
|
||||
else:
|
||||
push_warning("TestHarness: missing IK handle '%s'." % IK_HANDLE_PATHS[handle_name])
|
||||
|
||||
_resolve_bend_joints()
|
||||
_resolve_coord_bones()
|
||||
|
||||
|
||||
func _resolve_bend_joints() -> void:
|
||||
_bend_joint_bones.clear()
|
||||
_bend_modifications.clear()
|
||||
if _skeleton == null:
|
||||
return
|
||||
|
||||
for joint: String in BEND_JOINTS:
|
||||
var path: String = BEND_JOINT_BONE_PATHS[joint]
|
||||
var bone := _skeleton.get_node_or_null(NodePath(path)) as Bone2D
|
||||
if bone != null and is_instance_valid(bone):
|
||||
_bend_joint_bones[joint] = bone
|
||||
else:
|
||||
push_warning("TestHarness: missing bend-joint bone '%s'." % path)
|
||||
|
||||
var stack: SkeletonModificationStack2D = _skeleton.modification_stack
|
||||
if stack == null:
|
||||
return
|
||||
for i: int in stack.modification_count:
|
||||
var mod := stack.get_modification(i)
|
||||
if not (mod is SkeletonModification2DTwoBoneIK):
|
||||
continue
|
||||
var ik := mod as SkeletonModification2DTwoBoneIK
|
||||
for joint: String in BEND_JOINTS:
|
||||
if ik.joint_two_bone2d_node == NodePath(BEND_JOINT_BONE_PATHS[joint]):
|
||||
_bend_modifications[joint] = ik
|
||||
break
|
||||
|
||||
for joint: String in BEND_JOINTS:
|
||||
if not _bend_modifications.has(joint):
|
||||
push_warning("TestHarness: missing TwoBoneIK modification for bend joint '%s'." % joint)
|
||||
|
||||
|
||||
func _resolve_coord_bones() -> void:
|
||||
_coord_bones.clear()
|
||||
if _skeleton == null:
|
||||
return
|
||||
for path: String in COORD_BONE_PATHS:
|
||||
var bone := _skeleton.get_node_or_null(NodePath(path)) as Bone2D
|
||||
if bone != null and is_instance_valid(bone):
|
||||
_coord_bones[path.get_file()] = bone
|
||||
else:
|
||||
push_warning("TestHarness: missing coordinate bone '%s'." % path)
|
||||
|
||||
|
||||
func _ensure_modification_stack_enabled() -> void:
|
||||
if _skeleton == null:
|
||||
@@ -467,3 +775,59 @@ func _on_show_bones_toggled(pressed: bool) -> void:
|
||||
func _on_show_ik_toggled(pressed: bool) -> void:
|
||||
_show_ik = pressed
|
||||
_debug_overlay.queue_redraw()
|
||||
|
||||
|
||||
func _on_show_coords_toggled(pressed: bool) -> void:
|
||||
_show_coords = pressed
|
||||
if _coords_panel != null:
|
||||
_coords_panel.visible = pressed
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Coordinates display
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not _show_coords or _coords_label == null:
|
||||
return
|
||||
_update_coords_display()
|
||||
|
||||
|
||||
func _update_coords_display() -> void:
|
||||
if _rig == null or not is_instance_valid(_rig):
|
||||
if _coords_label.text != "No rig loaded":
|
||||
_coords_label.text = "No rig loaded"
|
||||
return
|
||||
|
||||
var lines := PackedStringArray()
|
||||
|
||||
# Skeleton2D root.
|
||||
if _skeleton != null and is_instance_valid(_skeleton):
|
||||
lines.append("%s pos %s rot %s" % ["Skeleton2D".lpad(14), _fmt_vec2(_skeleton.global_position), _fmt_deg(_skeleton.global_rotation)])
|
||||
|
||||
# Bones.
|
||||
lines.append("Bones")
|
||||
for path: String in COORD_BONE_PATHS:
|
||||
var bone := _coord_bones.get(path.get_file(), null) as Bone2D
|
||||
if bone == null or not is_instance_valid(bone):
|
||||
continue
|
||||
lines.append("%s pos %s rot %s" % [path.get_file().lpad(14), _fmt_vec2(bone.global_position), _fmt_deg(bone.global_rotation)])
|
||||
|
||||
# IK targets.
|
||||
lines.append("IK Targets")
|
||||
for handle_name: String in IK_HANDLE_PATHS:
|
||||
var handle := _ik_handles.get(handle_name, null) as Marker2D
|
||||
if handle == null or not is_instance_valid(handle):
|
||||
continue
|
||||
lines.append("%s pos %s" % [handle_name.lpad(14), _fmt_vec2(handle.global_position)])
|
||||
|
||||
var text := "\n".join(lines)
|
||||
if _coords_label.text != text:
|
||||
_coords_label.text = text
|
||||
|
||||
|
||||
func _fmt_vec2(v: Vector2) -> String:
|
||||
return "(%8.1f, %8.1f)" % [v.x, v.y]
|
||||
|
||||
|
||||
func _fmt_deg(rad: float) -> String:
|
||||
return "%7.1f°" % rad_to_deg(rad)
|
||||
|
||||
Reference in New Issue
Block a user