Bladeren bron

Fix 2.5D editor viewport and gizmo for Godot 4.x

Aaron Franke 1 jaar geleden
bovenliggende
commit
3788da41cd

+ 81 - 58
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()
-
-
-# Initializes after _ready due to the onready vars, called manually in Viewport25D.gd.
+		_start_mouse_position = mouse_position
+	# By this point, we are moving.
+	move_using_mouse(mouse_position)
+
+
+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)

+ 3 - 5
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)

+ 6 - 4
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

+ 23 - 18
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()
 
 

+ 4 - 0
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

+ 5 - 4
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