489 lines
17 KiB
GDScript
489 lines
17 KiB
GDScript
@tool
|
|
extends EditorPlugin
|
|
|
|
|
|
## This variable indicates whether the active tab is 3D
|
|
var is_in_3d_tab: bool = false
|
|
## The position of the mouse used to raycast into the 3D world
|
|
var mouse_position: Vector2
|
|
## The camera used for raycasting to calculate the position
|
|
## for the 3D cursor
|
|
var temp_camera: Camera3D
|
|
## The Editor Viewport used to get the mouse position
|
|
var editor_viewport: SubViewport
|
|
## The camera that displays what the user sees in the 3D editor tab
|
|
var editor_camera: Camera3D
|
|
## The root node of the active scene
|
|
var edited_scene_root: Node
|
|
## The scene used to instantiate the 3D Cursor
|
|
var cursor_scene: PackedScene
|
|
## The instance of the 3D Cursor
|
|
var cursor: Cursor3D
|
|
## The scene used to instantiate the pie menu for the 3D Cursor
|
|
var pie_menu_scene: PackedScene
|
|
## The instance of the pie menu for the 3D Cursor
|
|
var pie_menu: PieMenu
|
|
## A reference to the [EditorCommandPalette] singleton used to add
|
|
## some useful actions to the command palette such as '3D Cursor to Origin'
|
|
## or '3D Cursor to selected object' like in Blender
|
|
var command_palette: EditorCommandPalette
|
|
## The InputEvent holding the MouseButton event to trigger the
|
|
## set position function of the 3D Cursor
|
|
var input_event_set_3d_cursor: InputEventMouseButton
|
|
var input_event_show_pie_menu: InputEventKey
|
|
## The boolean that ensures the _recover_cursor function is executed once
|
|
var cursor_set: bool = false
|
|
## The instance of the Undo Redo class
|
|
var undo_redo: EditorUndoRedoManager
|
|
|
|
|
|
func _enter_tree() -> void:
|
|
# Register the switching of tabs in the editor. We only want the
|
|
# 3D Cursor functionality within the 3D tab
|
|
connect("main_screen_changed", _on_main_scene_changed)
|
|
# We want to place newly added Nodes that inherit [Node3D] at
|
|
# the location of the 3D Cursor. Therefore we listen to the
|
|
# node_added event
|
|
get_tree().connect("node_added", _on_node_added)
|
|
|
|
# Loading the 3D Cursor scene for later instancing
|
|
cursor_scene = preload("res://addons/godot_3d_cursor/3d_cursor.tscn")
|
|
pie_menu_scene = preload("res://addons/godot_3d_cursor/pie_menu.tscn")
|
|
|
|
command_palette = EditorInterface.get_command_palette()
|
|
# Adding the previously mentioned actions
|
|
command_palette.add_command("3D Cursor to Origin", "3D Cursor/3D Cursor to Origin", _3d_cursor_to_origin)
|
|
command_palette.add_command("3D Cursor to Selected Object", "3D Cursor/3D Cursor to Selected Object", _3d_cursor_to_selected_objects)
|
|
command_palette.add_command("Selected Object to 3D Cursor", "3D Cursor/Selected Object to 3D Cursor", _selected_object_to_3d_cursor)
|
|
# Adding the remove 3D Cursor in Scene action
|
|
command_palette.add_command("Remove 3D Cursor from Scene", "3D Cursor/Remove 3D Cursor from Scene", _remove_3d_cursor_from_scene)
|
|
command_palette.add_command("Toggle 3D Cursor", "3D Cursor/Toggle 3D Cursor", _toggle_3d_cursor)
|
|
|
|
editor_viewport = EditorInterface.get_editor_viewport_3d()
|
|
editor_camera = editor_viewport.get_camera_3d()
|
|
|
|
# Get the reference to the UndoRedo instance of the editor
|
|
undo_redo = get_undo_redo()
|
|
|
|
# Instantiating the pie menu for the 3D Cursor commands
|
|
pie_menu = pie_menu_scene.instantiate()
|
|
pie_menu.hide()
|
|
# Connecting the button events from the pie menu to the corresponding function
|
|
pie_menu.connect("cursor_to_origin_pressed", _3d_cursor_to_origin)
|
|
pie_menu.connect("cursor_to_selected_objects_pressed", _3d_cursor_to_selected_objects)
|
|
pie_menu.connect("selected_object_to_cursor_pressed", _selected_object_to_3d_cursor)
|
|
pie_menu.connect("remove_cursor_from_scene_pressed", _remove_3d_cursor_from_scene)
|
|
pie_menu.connect("toggle_cursor_pressed", _toggle_3d_cursor)
|
|
add_child(pie_menu)
|
|
|
|
|
|
# Setting up the InputMap so that we can set the 3D Cursor
|
|
# by Shift + Right Click
|
|
if not InputMap.has_action("3d_cursor_set_location"):
|
|
InputMap.add_action("3d_cursor_set_location")
|
|
input_event_set_3d_cursor = InputEventMouseButton.new()
|
|
input_event_set_3d_cursor.button_index = MOUSE_BUTTON_RIGHT
|
|
InputMap.action_add_event("3d_cursor_set_location", input_event_set_3d_cursor)
|
|
|
|
# Adding the action that shows the pie menu for the 3D Cursor commands.
|
|
if not InputMap.has_action("3d_cursor_show_pie_menu"):
|
|
InputMap.add_action("3d_cursor_show_pie_menu")
|
|
input_event_show_pie_menu = InputEventKey.new()
|
|
input_event_show_pie_menu.keycode = KEY_S
|
|
InputMap.action_add_event("3d_cursor_show_pie_menu", input_event_show_pie_menu)
|
|
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
# Removing listeners
|
|
disconnect("main_screen_changed", _on_main_scene_changed)
|
|
get_tree().disconnect("node_added", _on_node_added)
|
|
|
|
pie_menu.disconnect("cursor_to_origin_pressed", _3d_cursor_to_origin)
|
|
pie_menu.disconnect("cursor_to_selected_objects_pressed", _3d_cursor_to_selected_objects)
|
|
pie_menu.disconnect("selected_object_to_cursor_pressed", _selected_object_to_3d_cursor)
|
|
pie_menu.disconnect("remove_cursor_from_scene_pressed", _remove_3d_cursor_from_scene)
|
|
pie_menu.disconnect("toggle_cursor_pressed", _toggle_3d_cursor)
|
|
|
|
# Removing the actions from the [EditorCommandPalette]
|
|
command_palette.remove_command("3D Cursor/3D Cursor to Origin")
|
|
command_palette.remove_command("3D Cursor/3D Cursor to Selected Object")
|
|
command_palette.remove_command("3D Cursor/Selected Object to 3D Cursor")
|
|
command_palette.remove_command("3D Cursor/Remove 3D Cursor from Scene")
|
|
command_palette.remove_command("3D Cursor/Toggle 3D Cursor")
|
|
command_palette = null
|
|
|
|
# Removing the '3D Cursor set Location' action from the InputMap
|
|
if InputMap.has_action("3d_cursor_set_location"):
|
|
InputMap.action_erase_event("3d_cursor_set_location", input_event_set_3d_cursor)
|
|
InputMap.erase_action("3d_cursor_set_location")
|
|
|
|
# Removing the 'Show Pie Menu' action from the InputMap
|
|
if InputMap.has_action("3d_cursor_show_pie_menu"):
|
|
InputMap.action_erase_event("3d_cursor_show_pie_menu", input_event_show_pie_menu)
|
|
InputMap.erase_action("3d_cursor_show_pie_menu")
|
|
|
|
# Removing and freeing the helper objects
|
|
if temp_camera != null and editor_viewport != null:
|
|
editor_viewport.remove_child(temp_camera)
|
|
temp_camera.queue_free()
|
|
|
|
# Deleting the 3D Cursor
|
|
if cursor != null:
|
|
cursor.queue_free()
|
|
cursor_scene = null
|
|
|
|
# Deleting the pie menu
|
|
if pie_menu != null:
|
|
pie_menu.queue_free()
|
|
pie_menu_scene = null
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
# Only allow setting the 3D Cursors location in 3D tab
|
|
if not is_in_3d_tab:
|
|
return
|
|
|
|
# If the action is not yet set up: return
|
|
if not InputMap.has_action("3d_cursor_set_location"):
|
|
return
|
|
|
|
# Set the location of the 3D Cursor
|
|
if Input.is_key_pressed(KEY_SHIFT) and Input.is_action_just_pressed("3d_cursor_set_location"):
|
|
mouse_position = editor_viewport.get_mouse_position()
|
|
_get_selection()
|
|
|
|
if cursor == null or not cursor.is_inside_tree():
|
|
return
|
|
|
|
if Input.is_key_pressed(KEY_SHIFT) and Input.is_action_just_pressed("3d_cursor_show_pie_menu"):
|
|
pie_menu.visible = not pie_menu.visible
|
|
_set_visibility_toggle_label()
|
|
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if event.is_released():
|
|
return
|
|
|
|
if not pie_menu.visible:
|
|
return
|
|
|
|
if pie_menu.hit_any_button():
|
|
return
|
|
|
|
if event is InputEventKey and event.keycode == KEY_S and event.is_echo():
|
|
return
|
|
|
|
if event is InputEventKey or event is InputEventMouseButton:
|
|
pie_menu.hide()
|
|
# CAUTION: Do not mess with this statement! It can render your editor
|
|
# responseless. If it happens remove the plugin and restart the engine.
|
|
editor_viewport.set_input_as_handled()
|
|
|
|
|
|
## Checks whether the current active tab is named '3D'
|
|
## returns true if so, otherwise false
|
|
func _on_main_scene_changed(screen_name: String) -> void:
|
|
is_in_3d_tab = screen_name == "3D"
|
|
|
|
|
|
## Connected to the node_added event of the get_tree()
|
|
func _on_node_added(node: Node) -> void:
|
|
if not _cursor_available():
|
|
return
|
|
if EditorInterface.get_edited_scene_root() != cursor.owner:
|
|
return
|
|
if node.name == cursor.name:
|
|
return
|
|
if cursor.is_ancestor_of(node):
|
|
return
|
|
if not node is Node3D:
|
|
return
|
|
# Apply the position of the new node to the 3D Cursors position if the
|
|
# 3D cursor is available, the node is not the 3D cursor itself, the node
|
|
# is no descendant of the 3D Cursor and the node inherits [Node3D]
|
|
node.global_position = cursor.global_position
|
|
|
|
|
|
## Set the postion of the 3D Cursor to the origin (or [Vector3.ZERO])
|
|
func _3d_cursor_to_origin() -> void:
|
|
if not _cursor_available():
|
|
return
|
|
|
|
_create_undo_redo_action(
|
|
cursor,
|
|
"global_position",
|
|
Vector3.ZERO,
|
|
"Move 3D Cursor to Origin",
|
|
)
|
|
|
|
|
|
## Set the position of the 3D Cursor to the selected object and if multiple
|
|
## Nodes are selected to the average of the positions of all selected nodes
|
|
## that inherit [Node3D]
|
|
func _3d_cursor_to_selected_objects() -> void:
|
|
if not _cursor_available():
|
|
return
|
|
|
|
# Get the selection and through this the selected nodes as an Array of Nodes
|
|
var selection: EditorSelection = EditorInterface.get_selection()
|
|
var selected_nodes: Array[Node] = selection.get_selected_nodes()
|
|
|
|
if selected_nodes.is_empty():
|
|
return
|
|
if selected_nodes.size() == 1 and not selected_nodes.front() is Node3D:
|
|
return
|
|
|
|
# If only one Node is selected and it inherits Node3D set the position
|
|
# of the 3D Cursor to its position
|
|
if selected_nodes.size() == 1:
|
|
_create_undo_redo_action(
|
|
cursor,
|
|
"global_position",
|
|
selected_nodes.front().global_position,
|
|
"Move 3D Cursor to selected Object",
|
|
)
|
|
return
|
|
|
|
# Introduce a count variable to keep track of the amount of valid positions
|
|
# to calculate the average position later
|
|
var count = 0
|
|
var position_sum: Vector3 = Vector3.ZERO
|
|
|
|
for node in selected_nodes:
|
|
if not (node is Node3D or node is Cursor3D):
|
|
continue
|
|
|
|
# If the node is a valid object increment count and add the position
|
|
# to position_sum
|
|
count += 1
|
|
position_sum += node.global_position
|
|
|
|
if count == 0:
|
|
return
|
|
|
|
# Calculate the average position for multiple selected Nodes and set
|
|
# the 3D Cursor to this position
|
|
var average_position = position_sum / count
|
|
_create_undo_redo_action(
|
|
cursor,
|
|
"global_position",
|
|
average_position,
|
|
"Move 3D Cursor to selected Objects",
|
|
)
|
|
cursor.global_position = average_position
|
|
|
|
|
|
## Set the position of the selected object that inherits [Node3D]
|
|
## to the position of the 3D Cursor. If multiple nodes are selected the first
|
|
## valid node (i.e. a node that inherits [Node3D]) will be moved to
|
|
## position of the 3D Cursor. This funcitonality is disabled if the cursor
|
|
## is not set or hidden in the scene.
|
|
func _selected_object_to_3d_cursor() -> void:
|
|
if not _cursor_available():
|
|
return
|
|
|
|
# Get the selection and through this the selected nodes as an Array of Nodes
|
|
var selection: EditorSelection = EditorInterface.get_selection()
|
|
var selected_nodes: Array[Node] = selection.get_selected_nodes()
|
|
|
|
if selected_nodes.is_empty():
|
|
return
|
|
if selected_nodes.size() == 1 and not selected_nodes.front() is Node3D:
|
|
return
|
|
selected_nodes = selected_nodes.filter(func(node): return node is Node3D and not node is Cursor3D)
|
|
if selected_nodes.is_empty():
|
|
return
|
|
|
|
_create_undo_redo_action(
|
|
selected_nodes.front(),
|
|
"global_position",
|
|
cursor.global_position,
|
|
"Move Object to 3D Cursor"
|
|
)
|
|
|
|
|
|
## Disable the 3D Cursor to prevent the node placement at the position of
|
|
## the 3D Cursor.
|
|
func _toggle_3d_cursor() -> void:
|
|
if not _cursor_available(true):
|
|
return
|
|
|
|
cursor.visible = not cursor.visible
|
|
_set_visibility_toggle_label()
|
|
|
|
|
|
## Sets the correct label on the toggle visibility button in the pie menu
|
|
func _set_visibility_toggle_label() -> void:
|
|
pie_menu.change_toggle_label(cursor.visible)
|
|
|
|
|
|
## Remove every 3D Cursor from the scene including the active one.
|
|
func _remove_3d_cursor_from_scene() -> void:
|
|
if cursor == null:
|
|
return
|
|
|
|
# Remove the active 3D Cursor
|
|
cursor.queue_free()
|
|
cursor = null
|
|
|
|
# Get the root nodes children to filter for old instances of [Cursor3D]
|
|
var root_children = edited_scene_root.get_children()
|
|
if root_children.any(func(node): return node is Cursor3D):
|
|
# Iterate over all old instances and free them
|
|
for old_cursor: Cursor3D in root_children.filter(func(node): return node is Cursor3D):
|
|
old_cursor.queue_free()
|
|
|
|
|
|
## Check whether the 3D Cursor is set up and ready for use. A hidden 3D Cursor
|
|
## should also disable its functionality. Therefore this function yields false
|
|
## if the cursor is hidden in the scene
|
|
func _cursor_available(ignore_hidden = false) -> bool:
|
|
# CAUTION: Do not mess with this statement! It can render your editor
|
|
# responseless. If it happens remove the plugin and restart the engine.
|
|
editor_viewport.set_input_as_handled()
|
|
if cursor == null:
|
|
return false
|
|
if not cursor.is_inside_tree():
|
|
return false
|
|
if ignore_hidden and not cursor.is_visible_in_tree():
|
|
return true
|
|
if not cursor.is_visible_in_tree():
|
|
return false
|
|
return true
|
|
|
|
|
|
## This function uses raycasting to determine the position of the mouse click
|
|
## to set the position of the 3D Cursor. This means that it is necessary for
|
|
## the clicked on objects to have a collider the raycast can hit
|
|
func _get_selection() -> void:
|
|
# If the scene is switched stop
|
|
if edited_scene_root != null and edited_scene_root != EditorInterface.get_edited_scene_root() and cursor != null:
|
|
# Reset scene root, viewport and camera for new scene
|
|
edited_scene_root = null
|
|
editor_viewport = EditorInterface.get_editor_viewport_3d()
|
|
editor_camera = editor_viewport.get_camera_3d()
|
|
|
|
# Clear the 3D Cursor on the old screen.
|
|
cursor.queue_free()
|
|
cursor = null
|
|
|
|
if not cursor_set:
|
|
_recover_cursor()
|
|
|
|
if temp_camera == null:
|
|
# Set up the temp_camera to resemble the one of the 3D Viewport
|
|
_create_temp_camera()
|
|
|
|
# Get the transform of the camera from the 3D Viewport
|
|
var editor_camera_transform = _get_editor_camera_transform()
|
|
|
|
# Position the temp_camera so that it is exactly where the 3D Viewport
|
|
# camera is located
|
|
temp_camera.global_transform = editor_camera_transform
|
|
|
|
# if the editor_camera_transform is Transform3D.IDENTITY that means
|
|
# that for some reason the editor_camera is null.
|
|
if editor_camera_transform == Transform3D.IDENTITY:
|
|
return
|
|
|
|
# Set up the raycast parameters
|
|
var ray_origin = temp_camera.project_ray_origin(mouse_position)
|
|
var ray_end = temp_camera.project_position(mouse_position, 1000)
|
|
var ray_length = 1000
|
|
|
|
if edited_scene_root == null:
|
|
edited_scene_root = EditorInterface.get_edited_scene_root()
|
|
|
|
# The space state where the raycast should be performed in
|
|
var space_state = edited_scene_root.get_world_3d().direct_space_state
|
|
|
|
# Perform a raycast with the parameters above and store the result
|
|
var result = space_state.intersect_ray(PhysicsRayQueryParameters3D.create(ray_origin, ray_end))
|
|
|
|
var just_created: bool = false
|
|
|
|
# When the cursor is not yet created instantiate it, add it to the scene
|
|
# and position it at the collision detected by the raycast
|
|
if cursor == null:
|
|
cursor = cursor_scene.instantiate()
|
|
edited_scene_root.add_child(cursor)
|
|
cursor.owner = edited_scene_root
|
|
just_created = true
|
|
|
|
# If the cursor is not in the node tree at this point it means that the
|
|
# user probably deleted it. Then add it again
|
|
if not cursor.is_inside_tree():
|
|
edited_scene_root.add_child(cursor)
|
|
cursor.owner = edited_scene_root
|
|
just_created = true
|
|
|
|
# No collision means do nothing
|
|
if result.is_empty():
|
|
return
|
|
|
|
if just_created:
|
|
# Position the 3D Cursor to the position of the collision
|
|
cursor.global_transform.origin = result.position
|
|
return
|
|
|
|
_create_undo_redo_action(
|
|
cursor,
|
|
"global_position",
|
|
result.position,
|
|
"Set Position for 3D Cursor"
|
|
)
|
|
|
|
|
|
## This function creates the temp_camera and sets it up so that it resembles
|
|
## the camera from 3D Tab itself
|
|
func _create_temp_camera() -> void:
|
|
temp_camera = Camera3D.new()
|
|
temp_camera.hide()
|
|
|
|
# Add the temp_camera to the editor_viewport so that we can perform raycasts
|
|
# later on
|
|
editor_viewport.add_child(temp_camera)
|
|
|
|
# These are the most important settings the temp_camera needs to copy
|
|
# from the editor_camera so that their image is congruent
|
|
temp_camera.fov = editor_camera.fov
|
|
temp_camera.near = editor_camera.near
|
|
temp_camera.far = editor_camera.far
|
|
|
|
|
|
## This function returns the transform of the camera from the 3D Editor itself
|
|
func _get_editor_camera_transform() -> Transform3D:
|
|
if editor_camera != null:
|
|
return editor_camera.get_camera_transform()
|
|
return Transform3D.IDENTITY
|
|
|
|
|
|
## This function recovers any 3D Cursor present in the scene if you reload
|
|
## the project
|
|
func _recover_cursor() -> void:
|
|
# This boolean ensures this function is run exactly once
|
|
cursor_set = true
|
|
# Gets the children of the active scenes root node
|
|
var root_children = EditorInterface.get_edited_scene_root().get_children()
|
|
# Checks whether there are any nodes of type [Cursor3D] in the list of
|
|
# children
|
|
if root_children.any(func(node): return node is Cursor3D):
|
|
# Get the first and probably only instance of [Cursor3D] and assign
|
|
# it to the cursor variable. Now the 3D Cursor is considered recovered
|
|
cursor = root_children.filter(func(node): return node is Cursor3D).front()
|
|
|
|
|
|
func _create_undo_redo_action(node: Node3D, property: String, value: Variant, action_name: String = "") -> void:
|
|
if node == null or property.is_empty() or value == null:
|
|
return
|
|
|
|
if action_name.is_empty():
|
|
action_name = "Set " + property + " for " + node.name
|
|
|
|
undo_redo.create_action(action_name)
|
|
var old_value: Variant = node.get(property)
|
|
undo_redo.add_do_property(node, property, value)
|
|
undo_redo.add_undo_property(node, property, old_value)
|
|
undo_redo.commit_action()
|