From 3788da41cd5a366ec066edadc34d14d3fe1a8fce Mon Sep 17 00:00:00 2001 From: Aaron Franke Date: Thu, 11 Apr 2024 19:07:56 -0700 Subject: [PATCH] Fix 2.5D editor viewport and gizmo for Godot 4.x --- .../addons/node25d/main_screen/gizmo_25d.gd | 135 ++++++++++-------- .../addons/node25d/main_screen/gizmo_25d.tscn | 8 +- .../node25d/main_screen/main_screen_25d.tscn | 10 +- .../node25d/main_screen/viewport_25d.gd | 41 +++--- misc/2.5d/addons/node25d/node25d_plugin.gd | 4 + misc/2.5d/assets/demo_scene.tscn | 9 +- 6 files changed, 120 insertions(+), 87 deletions(-) diff --git a/misc/2.5d/addons/node25d/main_screen/gizmo_25d.gd b/misc/2.5d/addons/node25d/main_screen/gizmo_25d.gd index cd52f210..94037413 100644 --- a/misc/2.5d/addons/node25d/main_screen/gizmo_25d.gd +++ b/misc/2.5d/addons/node25d/main_screen/gizmo_25d.gd @@ -1,104 +1,127 @@ @tool extends Node2D + +# If the mouse is farther than this many pixels, it won't grab anything. +const DEADZONE_RADIUS: float = 20 +const DEADZONE_RADIUS_SQ: float = DEADZONE_RADIUS * DEADZONE_RADIUS # Not pixel perfect for all axes in all modes, but works well enough. # Rounding is not done until after the movement is finished. const ROUGHLY_ROUND_TO_PIXELS = true # Set when the node is created. var node_25d: Node25D -var spatial_node +var _spatial_node # Input from Viewport25D, represents if the mouse is clicked. var wants_to_move = false # Used to control the state of movement. var _moving = false -var _start_position = Vector2() +var _start_mouse_position := Vector2.ZERO # Stores state of closest or currently used axis. -var dominant_axis +var _dominant_axis -@onready var lines_root = $Lines -@onready var lines = [$Lines/X, $Lines/Y, $Lines/Z] +@onready var _lines = [$X, $Y, $Z] +@onready var _viewport_overlay: SubViewport = get_parent() +@onready var _viewport_25d_bg: ColorRect = _viewport_overlay.get_parent() func _process(_delta): - if not lines: + if not _lines: return # Somehow this node hasn't been set up yet. - if not node_25d: + if not node_25d or not _viewport_25d_bg: return # We're most likely viewing the Gizmo25D scene. + global_position = node_25d.global_position # While getting the mouse position works in any viewport, it doesn't do # anything significant unless the mouse is in the 2.5D viewport. - var mouse_position = get_local_mouse_position() + var mouse_position: Vector2 = _viewport_25d_bg.get_local_mouse_position() + var full_transform: Transform2D = _viewport_overlay.canvas_transform * global_transform + mouse_position = full_transform.affine_inverse() * mouse_position if not _moving: - # If the mouse is farther than this many pixels, it won't grab anything. - var closest_distance = 20.0 - dominant_axis = -1 - for i in range(3): - lines[i].modulate.a = 0.8 # Unrelated, but needs a loop too. - var distance = _distance_to_segment_at_index(i, mouse_position) - if distance < closest_distance: - closest_distance = distance - dominant_axis = i - if dominant_axis == -1: - # If we're not hovering over a line, ensure they are placed correctly. - lines_root.global_position = node_25d.global_position + determine_dominant_axis(mouse_position) + if _dominant_axis == -1: + # If we're not hovering over a line, nothing to do. return - - lines[dominant_axis].modulate.a = 1 + _lines[_dominant_axis].modulate.a = 1 if not wants_to_move: - _moving = false - elif wants_to_move and not _moving: + if _moving: + # When we're done moving, ensure the inspector is updated. + node_25d.notify_property_list_changed() + _moving = false + return + # By this point, we want to move. + if not _moving: _moving = true - _start_position = mouse_position - - if _moving: - # Change modulate of unselected axes. - lines[(dominant_axis + 1) % 3].modulate.a = 0.5 - lines[(dominant_axis + 2) % 3].modulate.a = 0.5 - # Calculate mouse movement and reset for next frame. - var mouse_diff = mouse_position - _start_position - _start_position = mouse_position - # Calculate movement. - var projected_diff = mouse_diff.project(lines[dominant_axis].points[1]) - var movement = projected_diff.length() / Node25D.SCALE - if is_equal_approx(PI, projected_diff.angle_to(lines[dominant_axis].points[1])): - movement *= -1 - # Apply movement. - spatial_node.transform.origin += spatial_node.transform.basis[dominant_axis] * movement - else: - # Make sure the gizmo is located at the object. - global_position = node_25d.global_position - if ROUGHLY_ROUND_TO_PIXELS: - spatial_node.transform.origin = (spatial_node.transform.origin * Node25D.SCALE).round() / Node25D.SCALE - # Move the gizmo lines appropriately. - lines_root.global_position = node_25d.global_position - node_25d.notify_property_list_changed() + _start_mouse_position = mouse_position + # By this point, we are moving. + move_using_mouse(mouse_position) -# Initializes after _ready due to the onready vars, called manually in Viewport25D.gd. +func determine_dominant_axis(mouse_position: Vector2) -> void: + var closest_distance = DEADZONE_RADIUS + _dominant_axis = -1 + for i in range(3): + _lines[i].modulate.a = 0.8 # Unrelated, but needs a loop too. + var distance = _distance_to_segment_at_index(i, mouse_position) + if distance < closest_distance: + closest_distance = distance + _dominant_axis = i + + +func move_using_mouse(mouse_position: Vector2) -> void: + # Change modulate of unselected axes. + _lines[(_dominant_axis + 1) % 3].modulate.a = 0.5 + _lines[(_dominant_axis + 2) % 3].modulate.a = 0.5 + # Calculate movement. + var mouse_diff: Vector2 = mouse_position - _start_mouse_position + var line_end_point: Vector2 = _lines[_dominant_axis].points[1] + var projected_diff: Vector2 = mouse_diff.project(line_end_point) + var movement: float = projected_diff.length() * global_scale.x / Node25D.SCALE + if is_equal_approx(PI, projected_diff.angle_to(line_end_point)): + movement *= -1 + # Apply movement. + var move_dir_3d: Vector3 = _spatial_node.transform.basis[_dominant_axis] + _spatial_node.transform.origin += move_dir_3d * movement + _snap_spatial_position() + # Move the gizmo appropriately. + global_position = node_25d.global_position + + +# Setup after _ready due to the onready vars, called manually in Viewport25D.gd. # Sets up the points based on the basis values of the Node25D. -func initialize(): +func setup(in_node_25d: Node25D): + node_25d = in_node_25d var basis = node_25d.get_basis() for i in range(3): - lines[i].points[1] = basis[i] * 3 + _lines[i].points[1] = basis[i] * 3 global_position = node_25d.global_position - spatial_node = node_25d.get_child(0) + _spatial_node = node_25d.get_child(0) + + +func set_zoom(zoom: float) -> void: + var new_scale: float = EditorInterface.get_editor_scale() / zoom + global_scale = Vector2(new_scale, new_scale) + + +func _snap_spatial_position(step_meters: float = 1.0 / Node25D.SCALE) -> void: + var scaled_px: Vector3 = _spatial_node.transform.origin / step_meters + _spatial_node.transform.origin = scaled_px.round() * step_meters # Figures out if the mouse is very close to a segment. This method is # specialized for this script, it assumes that each segment starts at # (0, 0) and it provides a deadzone around the origin. func _distance_to_segment_at_index(index, point): - if not lines: + if not _lines: return INF - if point.length_squared() < 400: + if point.length_squared() < DEADZONE_RADIUS_SQ: return INF - var segment_end = lines[index].points[1] + var segment_end: Vector2 = _lines[index].points[1] var length_squared = segment_end.length_squared() - if length_squared < 400: + if length_squared < DEADZONE_RADIUS_SQ: return INF var t = clamp(point.dot(segment_end) / length_squared, 0, 1) diff --git a/misc/2.5d/addons/node25d/main_screen/gizmo_25d.tscn b/misc/2.5d/addons/node25d/main_screen/gizmo_25d.tscn index 705bce6b..0b09772a 100644 --- a/misc/2.5d/addons/node25d/main_screen/gizmo_25d.tscn +++ b/misc/2.5d/addons/node25d/main_screen/gizmo_25d.tscn @@ -5,19 +5,17 @@ [node name="Gizmo25D" type="Node2D"] script = ExtResource("1") -[node name="Lines" type="Node2D" parent="."] - -[node name="X" type="Line2D" parent="Lines"] +[node name="X" type="Line2D" parent="."] modulate = Color(1, 1, 1, 0.8) points = PackedVector2Array(0, 0, 100, 0) default_color = Color(0.91, 0.273, 0, 1) -[node name="Y" type="Line2D" parent="Lines"] +[node name="Y" type="Line2D" parent="."] modulate = Color(1, 1, 1, 0.8) points = PackedVector2Array(0, 0, 0, -100) default_color = Color(0, 0.91, 0.273, 1) -[node name="Z" type="Line2D" parent="Lines"] +[node name="Z" type="Line2D" parent="."] modulate = Color(1, 1, 1, 0.8) points = PackedVector2Array(0, 0, 0, 100) default_color = Color(0.3, 0, 1, 1) diff --git a/misc/2.5d/addons/node25d/main_screen/main_screen_25d.tscn b/misc/2.5d/addons/node25d/main_screen/main_screen_25d.tscn index d515c30e..84392af5 100644 --- a/misc/2.5d/addons/node25d/main_screen/main_screen_25d.tscn +++ b/misc/2.5d/addons/node25d/main_screen/main_screen_25d.tscn @@ -61,16 +61,15 @@ size_flags_horizontal = 3 alignment = 2 [node name="ZoomOut" type="Button" parent="TopBar/Zoom"] -custom_minimum_size = Vector2(28, 2.08165e-12) +custom_minimum_size = Vector2(32, 2.08165e-12) layout_mode = 2 text = "-" [node name="ZoomPercent" type="Label" parent="TopBar/Zoom"] -custom_minimum_size = Vector2(80, 2.08165e-12) +custom_minimum_size = Vector2(100, 2.08165e-12) layout_mode = 2 text = "100%" horizontal_alignment = 1 -clip_text = true [node name="ZoomReset" type="Button" parent="TopBar/Zoom/ZoomPercent"] modulate = Color(1, 1, 1, 0) @@ -79,10 +78,13 @@ anchor_right = 1.0 anchor_bottom = 1.0 [node name="ZoomIn" type="Button" parent="TopBar/Zoom"] -custom_minimum_size = Vector2(28, 2.08165e-12) +custom_minimum_size = Vector2(32, 2.08165e-12) layout_mode = 2 text = "+" +[node name="Spacer" type="Control" parent="TopBar/Zoom"] +layout_mode = 2 + [node name="Viewport25D" type="ColorRect" parent="."] layout_mode = 2 size_flags_horizontal = 3 diff --git a/misc/2.5d/addons/node25d/main_screen/viewport_25d.gd b/misc/2.5d/addons/node25d/main_screen/viewport_25d.gd index bd3963ee..08039dad 100644 --- a/misc/2.5d/addons/node25d/main_screen/viewport_25d.gd +++ b/misc/2.5d/addons/node25d/main_screen/viewport_25d.gd @@ -56,8 +56,9 @@ func _process(_delta): var zoom = _get_zoom_amount() # SubViewport size. - var size = get_global_rect().size - viewport_2d.size = size + var vp_size = get_global_rect().size + viewport_2d.size = vp_size + viewport_overlay.size = vp_size # SubViewport transform. var viewport_trans = Transform2D.IDENTITY @@ -69,27 +70,31 @@ func _process(_delta): # Delete unused gizmos. var selection = editor_interface.get_selection().get_selected_nodes() - var overlay_children = viewport_overlay.get_children() - for overlay_child in overlay_children: + var gizmos = viewport_overlay.get_children() + for gizmo in gizmos: var contains = false for selected in selection: - if selected == overlay_child.node_25d and not view_mode_changed_this_frame: + if selected == gizmo.node_25d and not view_mode_changed_this_frame: contains = true if not contains: - overlay_child.queue_free() - + gizmo.queue_free() # Add new gizmos. for selected in selection: if selected is Node25D: - var new = true - for overlay_child in overlay_children: - if selected == overlay_child.node_25d: - new = false - if new: - var gizmo = gizmo_25d_scene.instantiate() - viewport_overlay.add_child(gizmo) - gizmo.node_25d = selected - gizmo.initialize() + _ensure_node25d_has_gizmo(selected, gizmos) + # Update gizmo zoom. + for gizmo in gizmos: + gizmo.set_zoom(zoom) + + +func _ensure_node25d_has_gizmo(node: Node25D, gizmos: Array[Node]) -> void: + var new = true + for gizmo in gizmos: + if node == gizmo.node_25d: + return + var gizmo = gizmo_25d_scene.instantiate() + viewport_overlay.add_child(gizmo) + gizmo.setup(node) # This only accepts input when the mouse is inside of the 2.5D viewport. @@ -104,7 +109,7 @@ func _gui_input(event): accept_event() elif event.button_index == MOUSE_BUTTON_MIDDLE: is_panning = true - pan_center = viewport_center - event.position + pan_center = viewport_center - event.position / _get_zoom_amount() accept_event() elif event.button_index == MOUSE_BUTTON_LEFT: var overlay_children = viewport_overlay.get_children() @@ -121,7 +126,7 @@ func _gui_input(event): accept_event() elif event is InputEventMouseMotion: if is_panning: - viewport_center = pan_center + event.position + viewport_center = pan_center + event.position / _get_zoom_amount() accept_event() diff --git a/misc/2.5d/addons/node25d/node25d_plugin.gd b/misc/2.5d/addons/node25d/node25d_plugin.gd index 8ef8892e..15e1cdd8 100644 --- a/misc/2.5d/addons/node25d/node25d_plugin.gd +++ b/misc/2.5d/addons/node25d/node25d_plugin.gd @@ -48,3 +48,7 @@ func _get_plugin_name(): func _get_plugin_icon(): return preload("res://addons/node25d/icons/viewport_25d.svg") + + +func _handles(obj: Object) -> bool: + return obj is Node25D diff --git a/misc/2.5d/assets/demo_scene.tscn b/misc/2.5d/assets/demo_scene.tscn index be573655..2e9cac50 100644 --- a/misc/2.5d/assets/demo_scene.tscn +++ b/misc/2.5d/assets/demo_scene.tscn @@ -24,21 +24,22 @@ size = Vector3(10, 1, 10) [node name="Player25D" parent="." instance=ExtResource("2")] z_index = -3956 +position = Vector2(0, -11.3137) [node name="Shadow25D" parent="." instance=ExtResource("3")] visible = true z_index = -3958 -position = Vector2(0, 10.7834) -spatial_position = Vector3(0, -0.476562, 0) +position = Vector2(3.5845e-13, 11.3137) +spatial_position = Vector3(1.12016e-14, -0.5, 1.12016e-14) [node name="Platform0" type="Node2D" parent="."] z_index = -3952 position = Vector2(-256, -113.137) script = ExtResource("4") -spatial_position = Vector3(-8, 5, 0) +spatial_position = Vector3(-8, 5, 2.08165e-12) [node name="PlatformMath" type="StaticBody3D" parent="Platform0"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8, 5, 0) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8, 5, 2.08165e-12) collision_layer = 1048575 collision_mask = 1048575