mirror of
https://github.com/godotengine/godot-demo-projects.git
synced 2025-12-31 09:49:06 +03:00
Fix 2.5D editor viewport and gizmo for Godot 4.x
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user