generated from mstar/godot-template
Code stuff
This commit is contained in:
parent
cf22890c16
commit
e58093b5a5
153 changed files with 11196 additions and 4 deletions
188
addons/gd-blender-3d-shortcuts/Utils.gd
Normal file
188
addons/gd-blender-3d-shortcuts/Utils.gd
Normal file
|
@ -0,0 +1,188 @@
|
|||
static func apply_transform(nodes, transform, cache_global_transforms):
|
||||
var i = 0
|
||||
for node in nodes:
|
||||
var cache_global_transform = cache_global_transforms[i]
|
||||
node.global_transform.origin = cache_global_transform.origin
|
||||
node.global_transform.origin += cache_global_transform.basis.get_rotation_quaternion() * transform.origin
|
||||
node.global_transform.basis.x = cache_global_transform.basis * transform.basis.x
|
||||
node.global_transform.basis.y = cache_global_transform.basis * transform.basis.y
|
||||
node.global_transform.basis.z = cache_global_transform.basis * transform.basis.z
|
||||
i += 1
|
||||
|
||||
static func apply_global_transform(nodes, transform, cache_transforms):
|
||||
var i = 0
|
||||
for node in nodes:
|
||||
node.global_transform = transform * cache_transforms[i]
|
||||
i += 1
|
||||
|
||||
static func revert_transform(nodes, cache_global_transforms):
|
||||
var i = 0
|
||||
for node in nodes:
|
||||
node.global_transform = cache_global_transforms[i]
|
||||
i += 1
|
||||
|
||||
static func reset_translation(nodes):
|
||||
for node in nodes:
|
||||
node.transform.origin = Vector3.ZERO
|
||||
|
||||
static func reset_rotation(nodes):
|
||||
for node in nodes:
|
||||
var scale = node.transform.basis.get_scale()
|
||||
node.transform.basis = Basis().scaled(scale)
|
||||
|
||||
static func reset_scale(nodes):
|
||||
for node in nodes:
|
||||
var quat = node.transform.basis.get_rotation_quaternion()
|
||||
node.transform.basis = Basis(quat)
|
||||
|
||||
static func hide_nodes(nodes, is_hide=true):
|
||||
for node in nodes:
|
||||
node.visible = !is_hide
|
||||
|
||||
static func recursive_get_children(node):
|
||||
var children = node.get_children()
|
||||
if children.size() == 0:
|
||||
return []
|
||||
else:
|
||||
for child in children:
|
||||
children += recursive_get_children(child)
|
||||
return children
|
||||
|
||||
static func get_spatial_editor(base_control):
|
||||
var children = recursive_get_children(base_control)
|
||||
for child in children:
|
||||
if child.get_class() == "Node3DEditor":
|
||||
return child
|
||||
|
||||
static func get_spatial_editor_viewport_container(spatial_editor):
|
||||
var children = recursive_get_children(spatial_editor)
|
||||
for child in children:
|
||||
if child.get_class() == "Node3DEditorViewportContainer":
|
||||
return child
|
||||
|
||||
static func get_spatial_editor_viewports(spatial_editor_viewport):
|
||||
var children = recursive_get_children(spatial_editor_viewport)
|
||||
var spatial_editor_viewports = []
|
||||
for child in children:
|
||||
if child.get_class() == "Node3DEditorViewport":
|
||||
spatial_editor_viewports.append(child)
|
||||
return spatial_editor_viewports
|
||||
|
||||
static func get_spatial_editor_viewport_viewport(spatial_editor_viewport):
|
||||
var children = recursive_get_children(spatial_editor_viewport)
|
||||
for child in children:
|
||||
if child.get_class() == "SubViewport":
|
||||
return child
|
||||
|
||||
static func get_spatial_editor_viewport_control(spatial_editor_viewport):
|
||||
var children = recursive_get_children(spatial_editor_viewport)
|
||||
for child in children:
|
||||
if child.get_class() == "Control":
|
||||
return child
|
||||
|
||||
static func get_focused_spatial_editor_viewport(spatial_editor_viewports):
|
||||
for viewport in spatial_editor_viewports:
|
||||
var viewport_control = get_spatial_editor_viewport_control(viewport)
|
||||
if viewport_control.get_rect().has_point(viewport_control.get_local_mouse_position()):
|
||||
return viewport
|
||||
|
||||
static func get_snap_dialog(spatial_editor):
|
||||
var children = recursive_get_children(spatial_editor)
|
||||
for child in children:
|
||||
if child.get_class() == "ConfirmationDialog":
|
||||
if child.title == "Snap Settings":
|
||||
return child
|
||||
|
||||
static func get_snap_dialog_line_edits(snap_dialog):
|
||||
var line_edits = []
|
||||
for child in recursive_get_children(snap_dialog):
|
||||
if child.get_class() == "LineEdit":
|
||||
line_edits.append(child)
|
||||
return line_edits
|
||||
|
||||
static func get_spatial_editor_local_space_button(spatial_editor):
|
||||
var children = recursive_get_children(spatial_editor)
|
||||
for child in children:
|
||||
if child.get_class() == "Button":
|
||||
if child.shortcut:
|
||||
if child.shortcut.get_as_text() == OS.get_keycode_string(KEY_T):# TODO: Check if user has custom shortcut
|
||||
return child
|
||||
|
||||
static func get_spatial_editor_snap_button(spatial_editor):
|
||||
var children = recursive_get_children(spatial_editor)
|
||||
for child in children:
|
||||
if child.get_class() == "Button":
|
||||
if child.shortcut:
|
||||
if child.shortcut.get_as_text() == OS.get_keycode_string(KEY_Y):# TODO: Check if user has custom shortcut
|
||||
return child
|
||||
|
||||
static func project_on_plane(camera, screen_point, plane):
|
||||
var from = camera.project_ray_origin(screen_point)
|
||||
var dir = camera.project_ray_normal(screen_point)
|
||||
var intersection = plane.intersects_ray(from, dir)
|
||||
return intersection if intersection else Vector3.ZERO
|
||||
|
||||
static func transform_to_plane(t):
|
||||
var a = t.basis.x
|
||||
var b = t.basis.z
|
||||
var c = a + b
|
||||
var o = t.origin
|
||||
return Plane(a + o, b + o, c + o)
|
||||
|
||||
# Return new position when out of bounds
|
||||
static func infinite_rect(rect, from, to):
|
||||
# Clamp from position to rect first, so it won't hit current side
|
||||
from = Vector2(clamp(from.x, rect.position.x + 2, rect.size.x - 2), clamp(from.y, rect.position.y + 2, rect.size.y - 2))
|
||||
# Intersect with sides of rect
|
||||
var intersection
|
||||
# Top
|
||||
intersection = Geometry2D.segment_intersects_segment(rect.position, Vector2(rect.size.x, rect.position.y), from, to)
|
||||
if intersection:
|
||||
return intersection
|
||||
# Left
|
||||
intersection = Geometry2D.segment_intersects_segment(rect.position, Vector2(rect.position.x, rect.size.y), from, to)
|
||||
if intersection:
|
||||
return intersection
|
||||
# Right
|
||||
intersection = Geometry2D.segment_intersects_segment(rect.size, Vector2(rect.size.x, rect.position.y), from, to)
|
||||
if intersection:
|
||||
return intersection
|
||||
# Bottom
|
||||
intersection = Geometry2D.segment_intersects_segment(rect.size, Vector2(rect.position.x, rect.size.y), from, to)
|
||||
if intersection:
|
||||
return intersection
|
||||
return null
|
||||
|
||||
static func draw_axis(im, origin, axis, length, color):
|
||||
var from = origin + (-axis * length / 2)
|
||||
var to = origin + (axis * length / 2)
|
||||
im.surface_begin(Mesh.PRIMITIVE_LINES)
|
||||
im.surface_set_color(color)
|
||||
im.surface_add_vertex(from)
|
||||
im.surface_add_vertex(to)
|
||||
im.surface_end()
|
||||
|
||||
static func draw_dashed_line(canvas_item, from, to, color, width, dash_length = 5, cap_end = false, antialiased = false):
|
||||
# See https://github.com/juddrgledhill/godot-dashed-line/blob/master/line_harness.gd
|
||||
var length = (to - from).length()
|
||||
var normal = (to - from).normalized()
|
||||
var dash_step = normal * dash_length
|
||||
|
||||
if length < dash_length: #not long enough to dash
|
||||
canvas_item.draw_line(from, to, color, width, antialiased)
|
||||
return
|
||||
|
||||
else:
|
||||
var draw_flag = true
|
||||
var segment_start = from
|
||||
var steps = length/dash_length
|
||||
for start_length in range(0, steps + 1):
|
||||
var segment_end = segment_start + dash_step
|
||||
if draw_flag:
|
||||
canvas_item.draw_line(segment_start, segment_end, color, width, antialiased)
|
||||
|
||||
segment_start = segment_end
|
||||
draw_flag = !draw_flag
|
||||
|
||||
if cap_end:
|
||||
canvas_item.draw_line(segment_start, to, color, width, antialiased)
|
7
addons/gd-blender-3d-shortcuts/plugin.cfg
Normal file
7
addons/gd-blender-3d-shortcuts/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="Blender 3D Shortcuts"
|
||||
description="Blender's 3D transforming shortcuts in Godot"
|
||||
author="imjp94"
|
||||
version="0.3.2"
|
||||
script="plugin.gd"
|
828
addons/gd-blender-3d-shortcuts/plugin.gd
Normal file
828
addons/gd-blender-3d-shortcuts/plugin.gd
Normal file
|
@ -0,0 +1,828 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const Utils = preload("Utils.gd")
|
||||
const PieMenuScn = preload("scenes/pie_menu/PieMenu.tscn")
|
||||
const PieMenuGroupScn = preload("scenes/pie_menu/PieMenuGroup.tscn")
|
||||
|
||||
const DEFAULT_LINE_COLOR = Color.WHITE
|
||||
# [name, value]
|
||||
const DEBUG_DRAW_OPTIONS = [
|
||||
["Normal", 0], ["Unshaded", 1], ["Lighting", 2], ["Overdraw", 3], ["Wireframe", 4],
|
||||
[
|
||||
"Advance",
|
||||
[
|
||||
["Shadows",
|
||||
[
|
||||
["Shadow Atlas", 9], ["Directional Shadow Atlas", 10], ["Directional Shadow Splits", 14]
|
||||
]
|
||||
],
|
||||
["Lights",
|
||||
[
|
||||
["Omni Lights Cluster", 20], ["Spot Lights Cluster", 21]
|
||||
]
|
||||
],
|
||||
["VoxelGI",
|
||||
[
|
||||
["VoxelGI Albedo", 6], ["VoxelGI Lighting", 7], ["VoxelGI Emission", 8]
|
||||
]
|
||||
],
|
||||
["SDFGI",
|
||||
[
|
||||
["SDFGI", 16], ["SDFGI Probes", 17], ["GI Buffer", 18]
|
||||
]
|
||||
],
|
||||
["Environment",
|
||||
[
|
||||
["SSAO", 12], ["SSIL", 13]
|
||||
]
|
||||
],
|
||||
["Decals",
|
||||
[
|
||||
["Decal Atlas", 15], ["Decal Cluster", 22]
|
||||
]
|
||||
],
|
||||
["Others",
|
||||
[
|
||||
["Normal Buffer", 5], ["Scene Luminance", 11], ["Disable LOD", 19], ["Cluster Reflection Probes", 23], ["Occluders", 24], ["Motion Vectors", 25]
|
||||
]
|
||||
],
|
||||
]
|
||||
],
|
||||
]
|
||||
|
||||
enum SESSION {
|
||||
TRANSLATE,
|
||||
ROTATE,
|
||||
SCALE,
|
||||
NONE
|
||||
}
|
||||
|
||||
var translate_snap_line_edit
|
||||
var rotate_snap_line_edit
|
||||
var scale_snap_line_edit
|
||||
var local_space_button
|
||||
var snap_button
|
||||
var overlay_control
|
||||
var spatial_editor_viewports
|
||||
var debug_draw_pie_menu
|
||||
var overlay_control_canvas_layer = CanvasLayer.new()
|
||||
|
||||
var overlay_label = Label.new()
|
||||
var axis_mesh_inst
|
||||
var axis_im = ImmediateMesh.new()
|
||||
var axis_im_material = StandardMaterial3D.new()
|
||||
|
||||
var current_session = SESSION.NONE
|
||||
var pivot_point = Vector3.ZERO
|
||||
var constraint_axis = Vector3.ONE
|
||||
var translate_snap = 1.0
|
||||
var rotate_snap = deg_to_rad(15.0)
|
||||
var scale_snap = 0.1
|
||||
var is_snapping = false
|
||||
var is_global = true
|
||||
var axis_length = 1000
|
||||
var precision_mode = false
|
||||
var precision_factor = 0.1
|
||||
|
||||
var _is_editing = false
|
||||
var _camera
|
||||
var _editing_transform = Transform3D.IDENTITY
|
||||
var _applying_transform = Transform3D.IDENTITY
|
||||
var _last_world_pos = Vector3.ZERO
|
||||
var _init_angle = NAN
|
||||
var _last_angle = 0
|
||||
var _last_center_offset = 0
|
||||
var _cummulative_center_offset = 0
|
||||
var _max_x = 0
|
||||
var _min_x = 0
|
||||
var _cache_global_transforms = []
|
||||
var _cache_transforms = [] # Nodes' local transform relative to pivot_point
|
||||
var _input_string = ""
|
||||
var _is_global_on_session = false
|
||||
var _is_warping_mouse = false
|
||||
var _is_pressing_right_mouse_button = false
|
||||
|
||||
|
||||
func _init():
|
||||
axis_im_material.flags_unshaded = true
|
||||
axis_im_material.vertex_color_use_as_albedo = true
|
||||
axis_im_material.flags_no_depth_test = true
|
||||
|
||||
overlay_label.set("custom_colors/font_color_shadow", Color.BLACK)
|
||||
|
||||
func _ready():
|
||||
var spatial_editor = Utils.get_spatial_editor(get_editor_interface().get_base_control())
|
||||
var snap_dialog = Utils.get_snap_dialog(spatial_editor)
|
||||
var snap_dialog_line_edits = Utils.get_snap_dialog_line_edits(snap_dialog)
|
||||
translate_snap_line_edit = snap_dialog_line_edits[0]
|
||||
rotate_snap_line_edit = snap_dialog_line_edits[1]
|
||||
scale_snap_line_edit = snap_dialog_line_edits[2]
|
||||
translate_snap_line_edit.connect("text_changed", _on_snap_value_changed.bind(SESSION.TRANSLATE))
|
||||
rotate_snap_line_edit.connect("text_changed", _on_snap_value_changed.bind(SESSION.ROTATE))
|
||||
scale_snap_line_edit.connect("text_changed", _on_snap_value_changed.bind(SESSION.SCALE))
|
||||
local_space_button = Utils.get_spatial_editor_local_space_button(spatial_editor)
|
||||
local_space_button.connect("toggled", _on_local_space_button_toggled)
|
||||
snap_button = Utils.get_spatial_editor_snap_button(spatial_editor)
|
||||
snap_button.connect("toggled", _on_snap_button_toggled)
|
||||
debug_draw_pie_menu = PieMenuGroupScn.instantiate()
|
||||
debug_draw_pie_menu.populate_menu(DEBUG_DRAW_OPTIONS, PieMenuScn.instantiate())
|
||||
debug_draw_pie_menu.theme_source_node = spatial_editor
|
||||
debug_draw_pie_menu.connect("item_focused", _on_PieMenu_item_focused)
|
||||
debug_draw_pie_menu.connect("item_selected", _on_PieMenu_item_selected)
|
||||
var spatial_editor_viewport_container = Utils.get_spatial_editor_viewport_container(spatial_editor)
|
||||
if spatial_editor_viewport_container:
|
||||
spatial_editor_viewports = Utils.get_spatial_editor_viewports(spatial_editor_viewport_container)
|
||||
sync_settings()
|
||||
|
||||
func _input(event):
|
||||
if event is InputEventKey:
|
||||
if event.pressed and not event.echo:
|
||||
match event.keycode:
|
||||
KEY_Z:
|
||||
var focus = find_focused_control(get_tree().root)
|
||||
|
||||
if focus != null:
|
||||
if focus.get_parent_control() != null:
|
||||
# This may be slightly fragile if this name changes or the control gets placed another level deeper internally
|
||||
if "Node3DEditorViewport" in focus.get_parent_control().name:
|
||||
if debug_draw_pie_menu.visible:
|
||||
debug_draw_pie_menu.hide()
|
||||
get_viewport().set_input_as_handled()
|
||||
else:
|
||||
if not (event.ctrl_pressed or event.alt_pressed or event.shift_pressed) and current_session == SESSION.NONE:
|
||||
show_debug_draw_pie_menu()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
# Hacky way to intercept default shortcut behavior when in session
|
||||
if current_session != SESSION.NONE:
|
||||
var event_text = event.as_text()
|
||||
if event_text.begins_with("Kp"):
|
||||
append_input_string(event_text.replace("Kp ", ""))
|
||||
get_viewport().set_input_as_handled()
|
||||
match event.keycode:
|
||||
KEY_Y:
|
||||
if event.shift_pressed:
|
||||
toggle_constraint_axis(Vector3.RIGHT + Vector3.BACK)
|
||||
else:
|
||||
toggle_constraint_axis(Vector3.UP)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
if current_session != SESSION.NONE and overlay_control:
|
||||
# Infinite mouse movement
|
||||
var rect = overlay_control.get_rect()
|
||||
var local_mouse_pos = overlay_control.get_local_mouse_position()
|
||||
if not rect.has_point(local_mouse_pos):
|
||||
var warp_pos = Utils.infinite_rect(rect, local_mouse_pos, -event.velocity.normalized() * rect.size.length())
|
||||
if warp_pos:
|
||||
Input.warp_mouse(overlay_control.global_position + warp_pos)
|
||||
_is_warping_mouse = true
|
||||
|
||||
func _on_snap_value_changed(text, session):
|
||||
match session:
|
||||
SESSION.TRANSLATE:
|
||||
translate_snap = text.to_float()
|
||||
SESSION.ROTATE:
|
||||
rotate_snap = deg_to_rad(text.to_float())
|
||||
SESSION.SCALE:
|
||||
scale_snap = text.to_float() / 100.0
|
||||
|
||||
func _on_PieMenu_item_focused(menu, index):
|
||||
var value = menu.buttons[index].get_meta("value", 0)
|
||||
if not (value is Array):
|
||||
switch_display_mode(value)
|
||||
|
||||
func _on_PieMenu_item_selected(menu, index):
|
||||
var value = menu.buttons[index].get_meta("value", 0)
|
||||
if not (value is Array):
|
||||
switch_display_mode(value)
|
||||
|
||||
func show_debug_draw_pie_menu():
|
||||
var spatial_editor_viewport = Utils.get_focused_spatial_editor_viewport(spatial_editor_viewports)
|
||||
overlay_control = Utils.get_spatial_editor_viewport_control(spatial_editor_viewport) if spatial_editor_viewport else null
|
||||
if not overlay_control:
|
||||
return false
|
||||
if overlay_control_canvas_layer.get_parent() != overlay_control:
|
||||
overlay_control.add_child(overlay_control_canvas_layer)
|
||||
if debug_draw_pie_menu.get_parent() != overlay_control_canvas_layer:
|
||||
overlay_control_canvas_layer.add_child(debug_draw_pie_menu)
|
||||
var viewport = Utils.get_spatial_editor_viewport_viewport(spatial_editor_viewport)
|
||||
|
||||
debug_draw_pie_menu.popup(overlay_control.get_global_mouse_position())
|
||||
return true
|
||||
|
||||
func _on_local_space_button_toggled(pressed):
|
||||
is_global = !pressed
|
||||
|
||||
func _on_snap_button_toggled(pressed):
|
||||
is_snapping = pressed
|
||||
|
||||
func _handles(object):
|
||||
if object is Node3D:
|
||||
_is_editing = get_editor_interface().get_selection().get_selected_nodes().size()
|
||||
return _is_editing
|
||||
elif object.get_class() == "MultiNodeEdit": # Explicitly handle MultiNodeEdit, otherwise, it will active when selected Resource
|
||||
_is_editing = get_editor_interface().get_selection().get_transformable_selected_nodes().size() > 0
|
||||
return _is_editing
|
||||
return false
|
||||
|
||||
func _edit(object):
|
||||
var scene_root = get_editor_interface().get_edited_scene_root()
|
||||
if scene_root:
|
||||
# Let editor free axis_mesh_inst as the scene closed,
|
||||
# then create new instance whenever needed
|
||||
if not is_instance_valid(axis_mesh_inst):
|
||||
axis_mesh_inst = MeshInstance3D.new()
|
||||
axis_mesh_inst.mesh = axis_im
|
||||
axis_mesh_inst.material_override = axis_im_material
|
||||
if axis_mesh_inst.get_parent() == null:
|
||||
scene_root.get_parent().add_child(axis_mesh_inst)
|
||||
else:
|
||||
if axis_mesh_inst.get_parent() != scene_root:
|
||||
axis_mesh_inst.get_parent().remove_child(axis_mesh_inst)
|
||||
scene_root.get_parent().add_child(axis_mesh_inst)
|
||||
|
||||
func find_focused_control(node):
|
||||
if node is Control and node.has_focus():
|
||||
return node
|
||||
|
||||
for child in node.get_children():
|
||||
var result = find_focused_control(child)
|
||||
|
||||
if result:
|
||||
return result
|
||||
|
||||
return null
|
||||
|
||||
func _forward_3d_gui_input(camera, event):
|
||||
var forward = false
|
||||
if current_session == SESSION.NONE:
|
||||
# solve conflict with free look
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
_is_pressing_right_mouse_button = event.is_pressed()
|
||||
if _is_editing:
|
||||
if event is InputEventKey:
|
||||
if event.pressed:
|
||||
match event.keycode:
|
||||
KEY_G:
|
||||
start_session(SESSION.TRANSLATE, camera, event)
|
||||
forward = true
|
||||
KEY_R:
|
||||
start_session(SESSION.ROTATE, camera, event)
|
||||
forward = true
|
||||
KEY_S:
|
||||
if not event.ctrl_pressed:
|
||||
# solve conflict with free look
|
||||
if not _is_pressing_right_mouse_button:
|
||||
start_session(SESSION.SCALE, camera, event)
|
||||
forward = true
|
||||
KEY_H:
|
||||
commit_hide_nodes()
|
||||
KEY_X:
|
||||
if event.shift_pressed:
|
||||
delete_selected_nodes()
|
||||
else:
|
||||
confirm_delete_selected_nodes()
|
||||
else:
|
||||
if event is InputEventKey:
|
||||
# Not sure why event.pressed always return false for numpad keys
|
||||
match event.keycode:
|
||||
KEY_KP_SUBTRACT:
|
||||
toggle_input_string_sign()
|
||||
return true
|
||||
KEY_KP_ENTER:
|
||||
commit_session()
|
||||
end_session()
|
||||
return true
|
||||
|
||||
if event.keycode == KEY_SHIFT:
|
||||
precision_mode = event.pressed
|
||||
forward = true
|
||||
|
||||
if event.pressed:
|
||||
var event_text = event.as_text()
|
||||
if append_input_string(event_text):
|
||||
return true
|
||||
match event.keycode:
|
||||
KEY_G:
|
||||
if current_session != SESSION.TRANSLATE:
|
||||
revert()
|
||||
clear_session()
|
||||
start_session(SESSION.TRANSLATE, camera, event)
|
||||
return true
|
||||
KEY_R:
|
||||
if current_session != SESSION.ROTATE:
|
||||
revert()
|
||||
clear_session()
|
||||
start_session(SESSION.ROTATE, camera, event)
|
||||
return true
|
||||
KEY_S:
|
||||
if not event.ctrl_pressed:
|
||||
if current_session != SESSION.SCALE:
|
||||
revert()
|
||||
clear_session()
|
||||
start_session(SESSION.SCALE, camera, event)
|
||||
return true
|
||||
KEY_X:
|
||||
if event.shift_pressed:
|
||||
toggle_constraint_axis(Vector3.UP + Vector3.BACK)
|
||||
else:
|
||||
toggle_constraint_axis(Vector3.RIGHT)
|
||||
return true
|
||||
KEY_Y:
|
||||
if event.shift_pressed:
|
||||
toggle_constraint_axis(Vector3.RIGHT + Vector3.BACK)
|
||||
else:
|
||||
toggle_constraint_axis(Vector3.UP)
|
||||
return true
|
||||
KEY_Z:
|
||||
if event.shift_pressed:
|
||||
toggle_constraint_axis(Vector3.RIGHT + Vector3.UP)
|
||||
else:
|
||||
toggle_constraint_axis(Vector3.BACK)
|
||||
return true
|
||||
KEY_MINUS:
|
||||
toggle_input_string_sign()
|
||||
return true
|
||||
KEY_BACKSPACE:
|
||||
trim_input_string()
|
||||
return true
|
||||
KEY_ENTER:
|
||||
commit_session()
|
||||
end_session()
|
||||
return true
|
||||
KEY_ESCAPE:
|
||||
revert()
|
||||
end_session()
|
||||
return true
|
||||
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed:
|
||||
if event.button_index == 2:
|
||||
revert()
|
||||
end_session()
|
||||
return true
|
||||
else:
|
||||
commit_session()
|
||||
end_session()
|
||||
forward = true
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
match current_session:
|
||||
SESSION.TRANSLATE, SESSION.ROTATE, SESSION.SCALE:
|
||||
mouse_transform(event)
|
||||
update_overlays()
|
||||
forward = true
|
||||
|
||||
return forward
|
||||
|
||||
func _forward_3d_draw_over_viewport(overlay):
|
||||
if current_session == SESSION.NONE:
|
||||
if overlay_label.get_parent() != null:
|
||||
overlay_label.get_parent().remove_child(overlay_label)
|
||||
return
|
||||
|
||||
var editor_settings = get_editor_interface().get_editor_settings()
|
||||
var line_color = DEFAULT_LINE_COLOR
|
||||
if editor_settings.has_setting("editors/3d/selection_box_color"):
|
||||
line_color = editor_settings.get_setting("editors/3d/selection_box_color")
|
||||
var snapped = "snapped" if is_snapping else ""
|
||||
var global_or_local = "global" if is_global else "local"
|
||||
var along_axis = ""
|
||||
if not constraint_axis.is_equal_approx(Vector3.ONE):
|
||||
if constraint_axis.x > 0:
|
||||
along_axis = "X"
|
||||
if constraint_axis.y > 0:
|
||||
along_axis += ", Y" if along_axis.length() else "Y"
|
||||
if constraint_axis.z > 0:
|
||||
along_axis += ", Z" if along_axis.length() else "Z"
|
||||
if along_axis.length():
|
||||
along_axis = "along " + along_axis
|
||||
|
||||
if overlay_label.get_parent() == null:
|
||||
overlay_control.add_child(overlay_label)
|
||||
overlay_label.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_LEFT)
|
||||
overlay_label.position += Vector2(8, -8)
|
||||
match current_session:
|
||||
SESSION.TRANSLATE:
|
||||
var translation = _applying_transform.origin
|
||||
overlay_label.text = ("Translate (%.3f, %.3f, %.3f) %s %s %s" % [translation.x, translation.y, translation.z, global_or_local, along_axis, snapped])
|
||||
SESSION.ROTATE:
|
||||
var rotation = _applying_transform.basis.get_euler()
|
||||
overlay_label.text = ("Rotate (%.3f, %.3f, %.3f) %s %s %s" % [rad_to_deg(rotation.x), rad_to_deg(rotation.y), rad_to_deg(rotation.z), global_or_local, along_axis, snapped])
|
||||
SESSION.SCALE:
|
||||
var scale = _applying_transform.basis.get_scale()
|
||||
overlay_label.text = ("Scale (%.3f, %.3f, %.3f) %s %s %s" % [scale.x, scale.y, scale.z, global_or_local, along_axis, snapped])
|
||||
if not _input_string.is_empty():
|
||||
overlay_label.text += "(%s)" % _input_string
|
||||
var is_pivot_point_behind_camera = _camera.is_position_behind(pivot_point)
|
||||
var screen_origin = overlay.size / 2.0 if is_pivot_point_behind_camera else _camera.unproject_position(pivot_point)
|
||||
Utils.draw_dashed_line(overlay, screen_origin, overlay.get_local_mouse_position(), line_color, 1, 5, true, true)
|
||||
|
||||
func text_transform(text):
|
||||
var input_value = text.to_float()
|
||||
match current_session:
|
||||
SESSION.TRANSLATE:
|
||||
_applying_transform.origin = constraint_axis * input_value
|
||||
SESSION.ROTATE:
|
||||
_applying_transform.basis = Basis().rotated((-_camera.global_transform.basis.z * constraint_axis).normalized(), deg_to_rad(input_value))
|
||||
SESSION.SCALE:
|
||||
if constraint_axis.x:
|
||||
_applying_transform.basis.x = Vector3.RIGHT * input_value
|
||||
if constraint_axis.y:
|
||||
_applying_transform.basis.y = Vector3.UP * input_value
|
||||
if constraint_axis.z:
|
||||
_applying_transform.basis.z = Vector3.BACK * input_value
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
var t = _applying_transform
|
||||
if is_global or (constraint_axis.is_equal_approx(Vector3.ONE) and current_session == SESSION.TRANSLATE):
|
||||
t.origin += pivot_point
|
||||
Utils.apply_global_transform(nodes, t, _cache_transforms)
|
||||
else:
|
||||
Utils.apply_transform(nodes, t, _cache_global_transforms)
|
||||
|
||||
func mouse_transform(event):
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
var is_single_node = nodes.size() == 1
|
||||
var node1 = nodes[0]
|
||||
var is_pivot_point_behind_camera = _camera.is_position_behind(pivot_point)
|
||||
if is_nan(_init_angle):
|
||||
var screen_origin = _camera.unproject_position(pivot_point)
|
||||
_init_angle = event.position.angle_to_point(screen_origin)
|
||||
# Translation offset
|
||||
var plane_transform = _camera.global_transform
|
||||
plane_transform.origin = pivot_point
|
||||
plane_transform.basis = plane_transform.basis.rotated(plane_transform.basis * Vector3.LEFT, deg_to_rad(90))
|
||||
if is_pivot_point_behind_camera:
|
||||
plane_transform.origin = _camera.global_transform.origin + -_camera.global_transform.basis.z * 10.0
|
||||
var plane = Utils.transform_to_plane(plane_transform)
|
||||
var axis_count = get_constraint_axis_count()
|
||||
if axis_count == 2:
|
||||
var normal = (Vector3.ONE - constraint_axis).normalized()
|
||||
if is_single_node and not is_global:
|
||||
normal = node1.global_transform.basis * normal
|
||||
var plane_dist = normal * plane_transform.origin
|
||||
plane = Plane(normal, plane_dist.x + plane_dist.y + plane_dist.z)
|
||||
var world_pos = Utils.project_on_plane(_camera, event.position, plane)
|
||||
if not is_global and is_single_node and axis_count < 3:
|
||||
var normalized_node1_basis = node1.global_transform.basis.scaled(Vector3.ONE / node1.global_transform.basis.get_scale())
|
||||
world_pos = world_pos * normalized_node1_basis
|
||||
if is_equal_approx(_last_world_pos.length(), 0):
|
||||
_last_world_pos = world_pos
|
||||
var offset = world_pos - _last_world_pos
|
||||
offset *= constraint_axis
|
||||
offset = offset.snapped(Vector3.ONE * 0.001)
|
||||
if _is_warping_mouse:
|
||||
offset = Vector3.ZERO
|
||||
# Rotation offset
|
||||
var screen_origin = _camera.unproject_position(pivot_point)
|
||||
if is_pivot_point_behind_camera:
|
||||
screen_origin = overlay_control.size / 2.0
|
||||
var angle = event.position.angle_to_point(screen_origin) - _init_angle
|
||||
var angle_offset = angle - _last_angle
|
||||
angle_offset = snapped(angle_offset, 0.001)
|
||||
# Scale offset
|
||||
if _max_x == 0:
|
||||
_max_x = event.position.x
|
||||
_min_x = _max_x - (_max_x - screen_origin.x) * 2
|
||||
var center_value = 2 * ((event.position.x - _min_x) / (_max_x - _min_x)) - 1
|
||||
if _last_center_offset == 0:
|
||||
_last_center_offset = center_value
|
||||
var center_offset = center_value - _last_center_offset
|
||||
center_offset = snapped(center_offset, 0.001)
|
||||
if _is_warping_mouse:
|
||||
center_offset = 0
|
||||
_cummulative_center_offset += center_offset
|
||||
if _input_string.is_empty():
|
||||
match current_session:
|
||||
SESSION.TRANSLATE:
|
||||
_editing_transform = _editing_transform.translated(offset)
|
||||
_applying_transform.origin = _editing_transform.origin
|
||||
if is_snapping:
|
||||
var snap = Vector3.ONE * (translate_snap if not precision_mode else translate_snap * precision_factor)
|
||||
_applying_transform.origin = _applying_transform.origin.snapped(snap)
|
||||
SESSION.ROTATE:
|
||||
var rotation_axis = (-_camera.global_transform.basis.z * constraint_axis).normalized()
|
||||
if not rotation_axis.is_equal_approx(Vector3.ZERO):
|
||||
_editing_transform.basis = _editing_transform.basis.rotated(rotation_axis, angle_offset)
|
||||
var quat = _editing_transform.basis.get_rotation_quaternion()
|
||||
if is_snapping:
|
||||
var snap = Vector3.ONE * (rotate_snap if not precision_mode else rotate_snap * precision_factor)
|
||||
quat.from_euler(quat.get_euler().snapped(snap))
|
||||
_applying_transform.basis = Basis(quat)
|
||||
SESSION.SCALE:
|
||||
if constraint_axis.x:
|
||||
_editing_transform.basis.x = Vector3.RIGHT * (1 + _cummulative_center_offset)
|
||||
if constraint_axis.y:
|
||||
_editing_transform.basis.y = Vector3.UP * (1 + _cummulative_center_offset)
|
||||
if constraint_axis.z:
|
||||
_editing_transform.basis.z = Vector3.BACK * (1 + _cummulative_center_offset)
|
||||
_applying_transform.basis = _editing_transform.basis
|
||||
if is_snapping:
|
||||
var snap = Vector3.ONE * (scale_snap if not precision_mode else scale_snap * precision_factor)
|
||||
_applying_transform.basis.x = _applying_transform.basis.x.snapped(snap)
|
||||
_applying_transform.basis.y = _applying_transform.basis.y.snapped(snap)
|
||||
_applying_transform.basis.z = _applying_transform.basis.z.snapped(snap)
|
||||
|
||||
var t = _applying_transform
|
||||
if is_global or (constraint_axis.is_equal_approx(Vector3.ONE) and current_session == SESSION.TRANSLATE):
|
||||
t.origin += pivot_point
|
||||
Utils.apply_global_transform(nodes, t, _cache_transforms)
|
||||
else:
|
||||
Utils.apply_transform(nodes, t, _cache_global_transforms)
|
||||
_last_world_pos = world_pos
|
||||
_last_center_offset = center_value
|
||||
_last_angle = angle
|
||||
_is_warping_mouse = false
|
||||
|
||||
func cache_selected_nodes_transforms():
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
var inversed_pivot_transform = Transform3D().translated(pivot_point).affine_inverse()
|
||||
for i in nodes.size():
|
||||
var node = nodes[i]
|
||||
_cache_global_transforms.append(node.global_transform)
|
||||
_cache_transforms.append(inversed_pivot_transform * node.global_transform)
|
||||
|
||||
func update_pivot_point():
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
var aabb = AABB()
|
||||
for i in nodes.size():
|
||||
var node = nodes[i]
|
||||
if i == 0:
|
||||
aabb.position = node.global_transform.origin
|
||||
aabb = aabb.expand(node.global_transform.origin)
|
||||
pivot_point = aabb.position + aabb.size / 2.0
|
||||
|
||||
func start_session(session, camera, event):
|
||||
if get_editor_interface().get_selection().get_transformable_selected_nodes().size() == 0:
|
||||
return
|
||||
current_session = session
|
||||
_camera = camera
|
||||
_is_global_on_session = is_global
|
||||
update_pivot_point()
|
||||
cache_selected_nodes_transforms()
|
||||
|
||||
if event.alt_pressed:
|
||||
commit_reset_transform()
|
||||
end_session()
|
||||
return
|
||||
|
||||
update_overlays()
|
||||
var spatial_editor_viewport = Utils.get_focused_spatial_editor_viewport(spatial_editor_viewports)
|
||||
overlay_control = Utils.get_spatial_editor_viewport_control(spatial_editor_viewport) if spatial_editor_viewport else null
|
||||
|
||||
func end_session():
|
||||
_is_editing = get_editor_interface().get_selection().get_transformable_selected_nodes().size() > 0
|
||||
# Manually set is_global to avoid triggering revert()
|
||||
if is_instance_valid(local_space_button):
|
||||
local_space_button.button_pressed = !_is_global_on_session
|
||||
is_global = _is_global_on_session
|
||||
clear_session()
|
||||
update_overlays()
|
||||
|
||||
func commit_session():
|
||||
var undo_redo = get_undo_redo()
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
Utils.revert_transform(nodes, _cache_global_transforms)
|
||||
undo_redo.create_action(SESSION.keys()[current_session].to_lower().capitalize())
|
||||
var t = _applying_transform
|
||||
if is_global or (constraint_axis.is_equal_approx(Vector3.ONE) and current_session == SESSION.TRANSLATE):
|
||||
t.origin += pivot_point
|
||||
undo_redo.add_do_method(Utils, "apply_global_transform", nodes, t, _cache_transforms)
|
||||
else:
|
||||
undo_redo.add_do_method(Utils, "apply_transform", nodes, t, _cache_global_transforms)
|
||||
undo_redo.add_undo_method(Utils, "revert_transform", nodes, _cache_global_transforms)
|
||||
undo_redo.commit_action()
|
||||
|
||||
func commit_reset_transform():
|
||||
var undo_redo = get_undo_redo()
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
match current_session:
|
||||
SESSION.TRANSLATE:
|
||||
undo_redo.create_action("Reset Translation")
|
||||
undo_redo.add_do_method(Utils, "reset_translation", nodes)
|
||||
undo_redo.add_undo_method(Utils, "revert_transform", nodes, _cache_global_transforms)
|
||||
undo_redo.commit_action()
|
||||
SESSION.ROTATE:
|
||||
undo_redo.create_action("Reset Rotation")
|
||||
undo_redo.add_do_method(Utils, "reset_rotation", nodes)
|
||||
undo_redo.add_undo_method(Utils, "revert_transform", nodes, _cache_global_transforms)
|
||||
undo_redo.commit_action()
|
||||
SESSION.SCALE:
|
||||
undo_redo.create_action("Reset Scale")
|
||||
undo_redo.add_do_method(Utils, "reset_scale", nodes)
|
||||
undo_redo.add_undo_method(Utils, "revert_transform", nodes, _cache_global_transforms)
|
||||
undo_redo.commit_action()
|
||||
current_session = SESSION.NONE
|
||||
|
||||
func commit_hide_nodes():
|
||||
var undo_redo = get_undo_redo()
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
undo_redo.create_action("Hide Nodes")
|
||||
undo_redo.add_do_method(Utils, "hide_nodes", nodes, true)
|
||||
undo_redo.add_undo_method(Utils, "hide_nodes", nodes, false)
|
||||
undo_redo.commit_action()
|
||||
|
||||
## Opens a popup dialog to confirm deletion of selected nodes.
|
||||
func confirm_delete_selected_nodes():
|
||||
var selected_nodes = get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected_nodes.is_empty():
|
||||
return
|
||||
|
||||
var editor_theme = get_editor_interface().get_base_control().theme
|
||||
var popup = ConfirmationDialog.new()
|
||||
popup.theme = editor_theme
|
||||
|
||||
# Setting dialog text dynamically depending on the selection to mimick Godot's normal behavior.
|
||||
popup.dialog_text = "Delete "
|
||||
var selection_size = selected_nodes.size()
|
||||
if selection_size == 1:
|
||||
popup.dialog_text += selected_nodes[0].get_name()
|
||||
elif selection_size > 1:
|
||||
popup.dialog_text += str(selection_size) + " nodes"
|
||||
for node in selected_nodes:
|
||||
if node.get_child_count() > 0:
|
||||
popup.dialog_text += " and children"
|
||||
break
|
||||
popup.dialog_text += "?"
|
||||
|
||||
add_child(popup)
|
||||
popup.popup_centered()
|
||||
popup.canceled.connect(popup.queue_free)
|
||||
popup.confirmed.connect(delete_selected_nodes)
|
||||
popup.confirmed.connect(popup.queue_free)
|
||||
|
||||
## Instantly deletes selected nodes and creates an undo history entry.
|
||||
func delete_selected_nodes():
|
||||
var undo_redo = get_undo_redo()
|
||||
|
||||
var selected_nodes = get_editor_interface().get_selection().get_selected_nodes()
|
||||
# Avoid creating an unnecessary history entry if no nodes are selected.
|
||||
if selected_nodes.is_empty():
|
||||
return
|
||||
|
||||
undo_redo.create_action("Delete Nodes", UndoRedo.MERGE_DISABLE)
|
||||
for node in selected_nodes:
|
||||
# We can't free nodes, they must be kept in memory for undo to work.
|
||||
# That's why we use remove_child instead and call UndoRedo.add_undo_reference() below.
|
||||
undo_redo.add_do_method(node.get_parent(), "remove_child", node)
|
||||
undo_redo.add_undo_method(node.get_parent(), "add_child", node, true)
|
||||
undo_redo.add_undo_method(node.get_parent(), "move_child", node, node.get_index())
|
||||
# Every node's owner must be set upon undoing, otherwise, it won't appear in the scene dock
|
||||
# and it'll be lost upon saving.
|
||||
undo_redo.add_undo_method(node, "set_owner", node.owner)
|
||||
for child in Utils.recursive_get_children(node):
|
||||
undo_redo.add_undo_method(child, "set_owner", node.owner)
|
||||
undo_redo.add_undo_reference(node)
|
||||
undo_redo.commit_action()
|
||||
|
||||
func revert():
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
Utils.revert_transform(nodes, _cache_global_transforms)
|
||||
_editing_transform = Transform3D.IDENTITY
|
||||
_applying_transform = Transform3D.IDENTITY
|
||||
_last_world_pos = Vector3.ZERO
|
||||
axis_im.clear_surfaces()
|
||||
|
||||
func clear_session():
|
||||
current_session = SESSION.NONE
|
||||
constraint_axis = Vector3.ONE
|
||||
pivot_point = Vector3.ZERO
|
||||
precision_mode = false
|
||||
_editing_transform = Transform3D.IDENTITY
|
||||
_applying_transform = Transform3D.IDENTITY
|
||||
_last_world_pos = Vector3.ZERO
|
||||
_init_angle = NAN
|
||||
_last_angle = 0
|
||||
_last_center_offset = 0
|
||||
_cummulative_center_offset = 0
|
||||
_max_x = 0
|
||||
_min_x = 0
|
||||
_cache_global_transforms = []
|
||||
_cache_transforms = []
|
||||
_input_string = ""
|
||||
_is_warping_mouse = false
|
||||
axis_im.clear_surfaces()
|
||||
|
||||
func sync_settings():
|
||||
if translate_snap_line_edit:
|
||||
translate_snap = translate_snap_line_edit.text.to_float()
|
||||
if rotate_snap_line_edit:
|
||||
rotate_snap = deg_to_rad(rotate_snap_line_edit.text.to_float())
|
||||
if scale_snap_line_edit:
|
||||
scale_snap = scale_snap_line_edit.text.to_float() / 100.0
|
||||
if local_space_button:
|
||||
is_global = !local_space_button.button_pressed
|
||||
if snap_button:
|
||||
is_snapping = snap_button.button_pressed
|
||||
|
||||
func switch_display_mode(debug_draw):
|
||||
var spatial_editor_viewport = Utils.get_focused_spatial_editor_viewport(spatial_editor_viewports)
|
||||
if spatial_editor_viewport:
|
||||
var viewport = Utils.get_spatial_editor_viewport_viewport(spatial_editor_viewport)
|
||||
viewport.debug_draw = debug_draw
|
||||
|
||||
# Repeatedly applying same axis will results in toggling is_global, just like pressing xx, yy or zz in blender
|
||||
func toggle_constraint_axis(axis):
|
||||
# Following order as below:
|
||||
# 1) Apply constraint on current mode
|
||||
# 2) Toggle mode
|
||||
# 3) Toggle mode again, and remove constraint
|
||||
if is_global == _is_global_on_session:
|
||||
if not constraint_axis.is_equal_approx(axis):
|
||||
# 1
|
||||
set_constraint_axis(axis)
|
||||
else:
|
||||
# 2
|
||||
set_is_global(!_is_global_on_session)
|
||||
else:
|
||||
if constraint_axis.is_equal_approx(axis):
|
||||
# 3
|
||||
set_is_global(_is_global_on_session)
|
||||
set_constraint_axis(Vector3.ONE)
|
||||
else:
|
||||
# Others situation
|
||||
set_constraint_axis(axis)
|
||||
|
||||
func toggle_input_string_sign():
|
||||
if _input_string.begins_with("-"):
|
||||
_input_string = _input_string.trim_prefix("-")
|
||||
else:
|
||||
_input_string = "-" + _input_string
|
||||
input_string_changed()
|
||||
|
||||
func trim_input_string():
|
||||
_input_string = _input_string.substr(0, _input_string.length() - 1)
|
||||
input_string_changed()
|
||||
|
||||
func append_input_string(text):
|
||||
text = "." if text == "Period" else text
|
||||
if text.is_valid_int() or text == ".":
|
||||
_input_string += text
|
||||
input_string_changed()
|
||||
return true
|
||||
|
||||
func input_string_changed():
|
||||
if not _input_string.is_empty():
|
||||
text_transform(_input_string)
|
||||
else:
|
||||
_applying_transform = Transform3D.IDENTITY
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
Utils.revert_transform(nodes, _cache_global_transforms)
|
||||
update_overlays()
|
||||
|
||||
func get_constraint_axis_count():
|
||||
var axis_count = 3
|
||||
if constraint_axis.x == 0:
|
||||
axis_count -= 1
|
||||
if constraint_axis.y == 0:
|
||||
axis_count -= 1
|
||||
if constraint_axis.z == 0:
|
||||
axis_count -= 1
|
||||
return axis_count
|
||||
|
||||
func set_constraint_axis(v):
|
||||
revert()
|
||||
if constraint_axis != v:
|
||||
constraint_axis = v
|
||||
draw_axises()
|
||||
else:
|
||||
constraint_axis = Vector3.ONE
|
||||
if not _input_string.is_empty():
|
||||
text_transform(_input_string)
|
||||
update_overlays()
|
||||
|
||||
func set_is_global(v):
|
||||
if is_global != v:
|
||||
if is_instance_valid(local_space_button):
|
||||
local_space_button.button_pressed = !v
|
||||
revert()
|
||||
is_global = v
|
||||
draw_axises()
|
||||
if not _input_string.is_empty():
|
||||
text_transform(_input_string)
|
||||
update_overlays()
|
||||
|
||||
func draw_axises():
|
||||
if not constraint_axis.is_equal_approx(Vector3.ONE):
|
||||
var nodes = get_editor_interface().get_selection().get_transformable_selected_nodes()
|
||||
var axis_lines = []
|
||||
if constraint_axis.x > 0:
|
||||
axis_lines.append({"axis": Vector3.RIGHT, "color": Color.RED})
|
||||
if constraint_axis.y > 0:
|
||||
axis_lines.append({"axis": Vector3.UP, "color": Color.GREEN})
|
||||
if constraint_axis.z > 0:
|
||||
axis_lines.append({"axis": Vector3.BACK, "color": Color.BLUE})
|
||||
|
||||
for axis_line in axis_lines:
|
||||
var axis = axis_line.get("axis")
|
||||
var color = axis_line.get("color")
|
||||
if is_global:
|
||||
var is_pivot_point_behind_camera = _camera.is_position_behind(pivot_point)
|
||||
var axis_origin = _camera.global_transform.origin + -_camera.global_transform.basis.z * 10.0 if is_pivot_point_behind_camera else pivot_point
|
||||
Utils.draw_axis(axis_im, axis_origin, axis, axis_length, color)
|
||||
else:
|
||||
for node in nodes:
|
||||
Utils.draw_axis(axis_im, node.global_transform.origin, node.global_transform.basis * axis, axis_length, color)
|
159
addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenu.gd
Normal file
159
addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenu.gd
Normal file
|
@ -0,0 +1,159 @@
|
|||
@tool
|
||||
extends Control
|
||||
|
||||
signal item_selected(index)
|
||||
signal item_focused(index)
|
||||
signal item_cancelled()
|
||||
|
||||
const button_margin = 6
|
||||
|
||||
@export var items := [] : set = set_items
|
||||
@export var selected_index = -1 : set = set_selected_index
|
||||
@export var radius = 100.0 : set = set_radius
|
||||
|
||||
var buttons = []
|
||||
var pie_menus = []
|
||||
|
||||
var focused_index = -1
|
||||
var theme_source_node = self : set = set_theme_source_node
|
||||
var grow_with_max_button_width = false
|
||||
|
||||
|
||||
func _ready():
|
||||
set_items(items)
|
||||
set_selected_index(selected_index)
|
||||
set_radius(radius)
|
||||
hide()
|
||||
connect("visibility_changed", _on_visiblity_changed)
|
||||
|
||||
func _input(event):
|
||||
if visible:
|
||||
if event is InputEventKey:
|
||||
if event.pressed:
|
||||
match event.keycode:
|
||||
KEY_ESCAPE:
|
||||
cancel()
|
||||
if event is InputEventMouseMotion:
|
||||
focus_item()
|
||||
get_viewport().set_input_as_handled()
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed:
|
||||
match event.button_index:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
select_item(focused_index)
|
||||
get_viewport().set_input_as_handled()
|
||||
MOUSE_BUTTON_RIGHT:
|
||||
cancel()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _on_visiblity_changed():
|
||||
if not visible:
|
||||
if selected_index != focused_index: # Cancellation
|
||||
focused_index = selected_index
|
||||
|
||||
func cancel():
|
||||
hide()
|
||||
get_viewport().set_input_as_handled()
|
||||
emit_signal("item_cancelled")
|
||||
|
||||
func select_item(index):
|
||||
set_button_style(selected_index, "normal", "normal")
|
||||
selected_index = index
|
||||
focused_index = selected_index
|
||||
hide()
|
||||
emit_signal("item_selected", selected_index)
|
||||
|
||||
func focus_item():
|
||||
queue_redraw()
|
||||
var pos = get_global_mouse_position()
|
||||
var count = max(buttons.size(), 1)
|
||||
var angle_offset = 2 * PI / count
|
||||
var angle = pos.angle_to_point(global_position) + PI / 2 # -90 deg initial offset
|
||||
if angle < 0:
|
||||
angle += 2 * PI
|
||||
|
||||
var index = (angle / angle_offset)
|
||||
var decimal = index - floor(index)
|
||||
index = floor(index)
|
||||
if decimal >= 0.5:
|
||||
index += 1
|
||||
if index > buttons.size()-1:
|
||||
index = 0
|
||||
|
||||
set_button_style(focused_index, "normal", "normal")
|
||||
focused_index = index
|
||||
set_button_style(focused_index, "normal", "hover")
|
||||
set_button_style(selected_index, "normal", "focus")
|
||||
emit_signal("item_focused", focused_index)
|
||||
|
||||
func popup(pos):
|
||||
global_position = pos
|
||||
show()
|
||||
|
||||
func populate_menu():
|
||||
clear_menu()
|
||||
buttons = []
|
||||
for i in items.size():
|
||||
var item = items[i]
|
||||
var is_array = item is Array
|
||||
var name = item if not is_array else item[0]
|
||||
var value = null if not is_array else item[1]
|
||||
var button = Button.new()
|
||||
button.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
button.text = name
|
||||
if value != null:
|
||||
button.set_meta("value", value)
|
||||
buttons.append(button)
|
||||
set_button_style(i, "hover", "hover")
|
||||
set_button_style(i, "pressed", "pressed")
|
||||
set_button_style(i, "focus", "focus")
|
||||
set_button_style(i, "disabled", "disabled")
|
||||
set_button_style(i, "normal", "normal")
|
||||
add_child(button)
|
||||
align()
|
||||
|
||||
set_button_style(selected_index, "normal", "focus")
|
||||
|
||||
func align():
|
||||
var final_radius = radius
|
||||
if grow_with_max_button_width:
|
||||
var max_button_width = 0.0
|
||||
for button in buttons:
|
||||
max_button_width = max(max_button_width, button.size.x)
|
||||
final_radius = max(radius, max_button_width)
|
||||
var count = max(buttons.size(), 1)
|
||||
var angle_offset = 2 * PI / count
|
||||
var angle = PI / 2 # 90 deg initial offset
|
||||
for button in buttons:
|
||||
button.position = Vector2(final_radius, 0.0).rotated(angle) - (button.size / 2.0)
|
||||
angle += angle_offset
|
||||
|
||||
func clear_menu():
|
||||
for button in buttons:
|
||||
button.queue_free()
|
||||
|
||||
func set_button_style(index, name, source):
|
||||
if index < 0 or index > buttons.size() - 1:
|
||||
return
|
||||
|
||||
buttons[index].set("theme_override_styles/%s" % name, get_theme_stylebox(source, "Button"))
|
||||
|
||||
func set_items(v):
|
||||
items = v
|
||||
if is_inside_tree():
|
||||
populate_menu()
|
||||
|
||||
func set_selected_index(v):
|
||||
set_button_style(selected_index, "normal", "normal")
|
||||
selected_index = v
|
||||
set_button_style(selected_index, "normal", "focus")
|
||||
|
||||
func set_radius(v):
|
||||
radius = v
|
||||
align()
|
||||
|
||||
func set_theme_source_node(v):
|
||||
theme_source_node = v
|
||||
for pie_menu in pie_menus:
|
||||
if pie_menu:
|
||||
pie_menu.theme_source_node = theme_source_node
|
11
addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenu.tscn
Normal file
11
addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenu.tscn
Normal file
|
@ -0,0 +1,11 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://bxummco35581e"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenu.gd" id="1"]
|
||||
|
||||
[node name="PieMenu" type="Control"]
|
||||
visible = false
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1")
|
113
addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenuGroup.gd
Normal file
113
addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenuGroup.gd
Normal file
|
@ -0,0 +1,113 @@
|
|||
@tool
|
||||
extends Control
|
||||
const PieMenuScn = preload("PieMenu.tscn")
|
||||
|
||||
signal item_focused(menu, index)
|
||||
signal item_selected(menu, index)
|
||||
signal item_cancelled(menu)
|
||||
|
||||
var root
|
||||
var page_index = [0]
|
||||
var theme_source_node = self : set = set_theme_source_node
|
||||
|
||||
|
||||
func _ready():
|
||||
hide()
|
||||
|
||||
func _on_item_cancelled(pie_menu):
|
||||
back()
|
||||
emit_signal("item_cancelled", pie_menu)
|
||||
|
||||
func _on_item_focused(index, pie_menu):
|
||||
var current_menu = get_current_menu()
|
||||
if current_menu == pie_menu:
|
||||
emit_signal("item_focused", current_menu, index)
|
||||
|
||||
func _on_item_selected(index):
|
||||
var last_menu = get_current_menu()
|
||||
page_index.append(index)
|
||||
var current_menu = get_current_menu()
|
||||
if current_menu:
|
||||
current_menu.selected_index = -1
|
||||
if current_menu.pie_menus.size() > 0: # Has next page
|
||||
current_menu.popup(global_position)
|
||||
else:
|
||||
# Final selection, revert page index
|
||||
if page_index.size() > 1:
|
||||
page_index.pop_back()
|
||||
last_menu = get_current_menu()
|
||||
page_index = [0]
|
||||
hide()
|
||||
emit_signal("item_selected", last_menu, index)
|
||||
|
||||
func popup(pos):
|
||||
global_position = pos
|
||||
var pie_menu = get_current_menu()
|
||||
pie_menu.popup(global_position)
|
||||
show()
|
||||
|
||||
func populate_menu(items, pie_menu):
|
||||
add_child(pie_menu)
|
||||
if not root:
|
||||
root = pie_menu
|
||||
root.connect("item_focused", _on_item_focused.bind(pie_menu))
|
||||
root.connect("item_selected", _on_item_selected)
|
||||
root.connect("item_cancelled", _on_item_cancelled.bind(pie_menu))
|
||||
|
||||
pie_menu.items = items
|
||||
|
||||
for i in items.size():
|
||||
var item = items[i]
|
||||
var is_array = item is Array
|
||||
# var name = item if not is_array else item[0]
|
||||
var value = null if not is_array else item[1]
|
||||
if value is Array:
|
||||
var new_pie_menu = PieMenuScn.instantiate()
|
||||
new_pie_menu.connect("item_focused", _on_item_focused.bind(new_pie_menu))
|
||||
new_pie_menu.connect("item_selected", _on_item_selected)
|
||||
new_pie_menu.connect("item_cancelled", _on_item_cancelled.bind(new_pie_menu))
|
||||
|
||||
populate_menu(value, new_pie_menu)
|
||||
pie_menu.pie_menus.append(new_pie_menu)
|
||||
else:
|
||||
pie_menu.pie_menus.append(null)
|
||||
return pie_menu
|
||||
|
||||
func clear_menu():
|
||||
if root:
|
||||
root.queue_free()
|
||||
|
||||
func back():
|
||||
var last_menu = get_current_menu()
|
||||
last_menu.hide()
|
||||
page_index.pop_back()
|
||||
if page_index.size() == 0:
|
||||
page_index = [0]
|
||||
hide()
|
||||
return
|
||||
else:
|
||||
var current_menu = get_current_menu()
|
||||
if current_menu:
|
||||
current_menu.popup(global_position)
|
||||
|
||||
func get_menu(indexes=[0]):
|
||||
var pie_menu = root
|
||||
for i in indexes.size():
|
||||
if i == 0:
|
||||
continue # root
|
||||
|
||||
var page = indexes[i]
|
||||
pie_menu = pie_menu.pie_menus[page]
|
||||
return pie_menu
|
||||
|
||||
func get_current_menu():
|
||||
return get_menu(page_index)
|
||||
|
||||
func set_theme_source_node(v):
|
||||
theme_source_node = v
|
||||
if not root:
|
||||
return
|
||||
|
||||
for pie_menu in root.pie_menus:
|
||||
if pie_menu:
|
||||
pie_menu.theme_source_node = theme_source_node
|
|
@ -0,0 +1,13 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://c4cfbaj52t05b"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/gd-blender-3d-shortcuts/scenes/pie_menu/PieMenuGroup.gd" id="1"]
|
||||
|
||||
[node name="PieMenuGroup" type="Control"]
|
||||
visible = false
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1")
|
Loading…
Add table
Add a link
Reference in a new issue