@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)