tool extends EditorPlugin """ Common Abbreviations et = editor transform (viewport's canvas transform) - Snapping using the build in functionality isn't going to happen - https://github.com/godotengine/godot/issues/11180 - https://godotengine.org/qa/18051/tool-script-in-3-0 """ # Icons const ICON_HANDLE = preload("assets/icon_editor_handle.svg") const ICON_HANDLE_SELECTED = preload("assets/icon_editor_handle_selected.svg") const ICON_HANDLE_BEZIER = preload("assets/icon_editor_handle_bezier.svg") const ICON_HANDLE_CONTROL = preload("assets/icon_editor_handle_control.svg") const ICON_ADD_HANDLE = preload("assets/icon_editor_handle_add.svg") const ICON_CURVE_EDIT = preload("assets/icon_curve_edit.svg") const ICON_CURVE_CREATE = preload("assets/icon_curve_create.svg") const ICON_CURVE_DELETE = preload("assets/icon_curve_delete.svg") const ICON_PIVOT_POINT = preload("assets/icon_editor_position.svg") const ICON_COLLISION = preload("assets/icon_collision_polygon_2d.svg") const ICON_INTERP_LINEAR = preload("assets/InterpLinear.svg") const ICON_SNAP = preload("assets/icon_editor_snap.svg") const ICON_IMPORT_CLOSED = preload("assets/closed_shape.png") const ICON_IMPORT_OPEN = preload("assets/open_shape.png") const FUNC = preload("plugin-functionality.gd") enum MODE { EDIT_VERT, EDIT_EDGE, SET_PIVOT, CREATE_VERT } enum ACTION_VERT { NONE = 0, MOVE_VERT = 1, MOVE_CONTROL = 2, MOVE_CONTROL_IN = 3, MOVE_CONTROL_OUT = 4, MOVE_WIDTH_HANDLE = 5 } # Data related to an action being taken on points class ActionDataVert: #Type of Action from the ACTION_VERT enum var type: int = ACTION_VERT.NONE # The affected Verticies and their initial positions var keys = [] var starting_width = [] var starting_positions = [] var starting_positions_control_in = [] var starting_positions_control_out = [] func _init( _keys: Array, positions: Array, positions_in: Array, positions_out: Array, width: Array, t: int ): type = t keys = _keys starting_positions = positions starting_positions_control_in = positions_in starting_positions_control_out = positions_out starting_width = width func are_verts_selected() -> bool: return keys.size() > 0 func to_string() -> String: var s = "%s: %s = %s" return s % [type, keys, starting_positions] func is_single_vert_selected() -> bool: if keys.size() == 1: return true return false func current_point_key() -> int: if not is_single_vert_selected(): return -1 return keys[0] func current_point_index(s: SS2D_Shape_Base) -> int: if not is_single_vert_selected(): return -1 return s.get_point_index(keys[0]) # PRELOADS var GUI_SNAP_POPUP = preload("scenes/SnapPopup.tscn") var GUI_POINT_INFO_PANEL = preload("scenes/GUI_InfoPanel.tscn") var GUI_EDGE_INFO_PANEL = preload("scenes/GUI_Edge_InfoPanel.tscn") var gui_point_info_panel = GUI_POINT_INFO_PANEL.instance() var gui_edge_info_panel = GUI_EDGE_INFO_PANEL.instance() var gui_snap_settings = GUI_SNAP_POPUP.instance() # This is the shape node being edited var shape = null # For when a legacy shape is selected var legacy_shape = null # Toolbar Stuff var tb_hb: HBoxContainer = null var tb_hb_legacy_import: HBoxContainer = null var tb_import: ToolButton = null var tb_vert_create: ToolButton = null var tb_vert_edit: ToolButton = null var tb_edge_edit: ToolButton = null var tb_pivot: ToolButton = null var tb_collision: ToolButton = null var tb_snap: MenuButton = null # The PopupMenu that belongs to tb_snap var tb_snap_popup: PopupMenu = null # Edge Stuff var on_edge: bool = false var edge_point: Vector2 var edge_data: SS2D_Edge = null # Width Handle Stuff var on_width_handle: bool = false const WIDTH_HANDLE_OFFSET: float = 60.0 var closest_key: int var closest_edge_keys: Array = [-1, -1] var width_scaling: float # Track our mode of operation var current_mode: int = MODE.CREATE_VERT var previous_mode: int = MODE.CREATE_VERT # Undo stuff var undo: UndoRedo = null var undo_version: int = 0 var current_action = ActionDataVert.new([], [], [], [], [], ACTION_VERT.NONE) var cached_shape_global_transform: Transform2D # Action Move Variables var _mouse_motion_delta_starting_pos = Vector2(0, 0) ####### # GUI # ####### func gui_display_snap_settings(): var win_size = OS.get_window_size() gui_snap_settings.popup_centered_ratio(0.5) gui_snap_settings.set_as_minsize() # Get Centered gui_snap_settings.rect_position = (win_size / 2.0) - gui_snap_settings.rect_size / 2.0 # Move up gui_snap_settings.rect_position.y = (win_size.y / 8.0) func _snapping_item_selected(id: int): if id == 0: tb_snap_popup.set_item_checked(id, not tb_snap_popup.is_item_checked(id)) if id == 1: tb_snap_popup.set_item_checked(id, not tb_snap_popup.is_item_checked(id)) elif id == 3: gui_display_snap_settings() func _gui_build_toolbar(): tb_hb = HBoxContainer.new() add_control_to_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU, tb_hb) tb_hb_legacy_import = HBoxContainer.new() add_control_to_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU, tb_hb_legacy_import) tb_import = ToolButton.new() tb_import.icon = ICON_IMPORT_CLOSED tb_import.toggle_mode = false tb_import.pressed = false tb_import.hint_tooltip = SS2D_Strings.EN_TOOLTIP_IMPORT tb_import.connect("pressed", self, "_import_legacy") tb_hb_legacy_import.add_child(tb_import) var sep = VSeparator.new() tb_hb.add_child(sep) tb_vert_create = create_tool_button(ICON_CURVE_CREATE, SS2D_Strings.EN_TOOLTIP_CREATE_VERT) tb_vert_create.connect("pressed", self, "_enter_mode", [MODE.CREATE_VERT]) tb_vert_create.pressed = true tb_vert_edit = create_tool_button(ICON_CURVE_EDIT, SS2D_Strings.EN_TOOLTIP_EDIT_VERT) tb_vert_edit.connect("pressed", self, "_enter_mode", [MODE.EDIT_VERT]) tb_edge_edit = create_tool_button(ICON_INTERP_LINEAR, SS2D_Strings.EN_TOOLTIP_EDIT_EDGE) tb_edge_edit.connect("pressed", self, "_enter_mode", [MODE.EDIT_EDGE]) tb_pivot = create_tool_button(ICON_PIVOT_POINT, SS2D_Strings.EN_TOOLTIP_EDIT_VERT) tb_pivot.connect("pressed", self, "_enter_mode", [MODE.SET_PIVOT]) tb_collision = create_tool_button(ICON_COLLISION, SS2D_Strings.EN_TOOLTIP_COLLISION) tb_collision.connect("pressed", self, "_add_collision") tb_snap = MenuButton.new() tb_snap.hint_tooltip = SS2D_Strings.EN_TOOLTIP_SNAP tb_snap_popup = tb_snap.get_popup() tb_snap.icon = ICON_SNAP tb_snap_popup.add_check_item("Use Grid Snap") tb_snap_popup.add_check_item("Snap Relative") tb_snap_popup.add_separator() tb_snap_popup.add_item("Configure Snap...") tb_snap_popup.hide_on_checkable_item_selection = false tb_hb.add_child(tb_snap) tb_snap_popup.connect("id_pressed", self, "_snapping_item_selected") tb_hb.hide() func create_tool_button(icon, tooltip): var tb = ToolButton.new() tb.toggle_mode = true tb.icon = icon tb.hint_tooltip = tooltip tb_hb.add_child(tb) return tb func _gui_update_vert_info_panel(): var idx = current_action.current_point_index(shape) var key = current_action.current_point_key() if not is_key_valid(shape, key): gui_point_info_panel.visible = false return gui_point_info_panel.visible = true # Shrink panel gui_point_info_panel.rect_size = Vector2(1, 1) var properties = shape.get_point_properties(key) gui_point_info_panel.set_idx(idx) gui_point_info_panel.set_texture_idx(properties.texture_idx) gui_point_info_panel.set_width(properties.width) gui_point_info_panel.set_flip(properties.flip) func _gui_update_edge_info_panel(): # Don't update if already visible if gui_edge_info_panel.visible: return var indicies = [-1, -1] var override = null if on_edge: var t: Transform2D = get_et() * shape.get_global_transform() var offset = shape.get_closest_offset_straight_edge(t.affine_inverse().xform(edge_point)) var keys = _get_edge_point_keys_from_offset(offset, true) indicies = [shape.get_point_index(keys[0]), shape.get_point_index(keys[1])] if shape.has_material_override(keys): override = shape.get_material_override(keys) gui_edge_info_panel.set_indicies(indicies) if override != null: gui_edge_info_panel.set_material_override(true) gui_edge_info_panel.load_values_from_meta_material(override) else: gui_edge_info_panel.set_material_override(false) # Shrink panel to minimum size gui_edge_info_panel.rect_size = Vector2(1, 1) func _gui_update_info_panels(): match current_mode: MODE.EDIT_VERT: _gui_update_vert_info_panel() gui_edge_info_panel.visible = false MODE.EDIT_EDGE: _gui_update_edge_info_panel() gui_point_info_panel.visible = false ######### # GODOT # ######### # Called when saving # https://docs.godotengine.org/en/3.2/classes/class_editorplugin.html?highlight=switch%20scene%20tab func apply_changes(): gui_point_info_panel.visible = false gui_edge_info_panel.visible = false func _init(): pass func _ready(): undo = get_undo_redo() # Support the undo-redo actions _gui_build_toolbar() add_child(gui_point_info_panel) gui_point_info_panel.visible = false add_child(gui_edge_info_panel) gui_edge_info_panel.visible = false gui_edge_info_panel.connect("set_material_override", self, "_on_set_edge_material_override") gui_edge_info_panel.connect("set_render", self, "_on_set_edge_material_override_render") gui_edge_info_panel.connect("set_weld", self, "_on_set_edge_material_override_weld") gui_edge_info_panel.connect("set_z_index", self, "_on_set_edge_material_override_z_index") gui_edge_info_panel.connect("set_edge_material", self, "_on_set_edge_material") add_child(gui_snap_settings) func _enter_tree(): pass func _exit_tree(): gui_point_info_panel.visible = false gui_edge_info_panel.visible = false remove_control_from_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU, tb_hb) func forward_canvas_gui_input(event): if not is_shape_valid(shape): return false var et = get_et() var grab_threshold = get_editor_interface().get_editor_settings().get( "editors/poly_editor/point_grab_radius" ) var return_value = false if event is InputEventKey: return_value = _input_handle_keyboard_event(event) elif event is InputEventMouseButton: return_value = _input_handle_mouse_button_event(event, et, grab_threshold) elif event is InputEventMouseMotion: return_value = _input_handle_mouse_motion_event(event, et, grab_threshold) _gui_update_info_panels() return return_value func _process(delta): if not Engine.editor_hint: return if not is_shape_valid(shape): gui_point_info_panel.visible = false gui_edge_info_panel.visible = false shape = null update_overlays() return # Force update if global transforma has been changed if cached_shape_global_transform != shape.get_global_transform(): shape.set_as_dirty() cached_shape_global_transform = shape.get_global_transform() func handles(object): tb_hb.hide() tb_hb_legacy_import.hide() update_overlays() gui_point_info_panel.visible = false gui_edge_info_panel.visible = false if object is Resource: return false var rslt: bool = object is SS2D_Shape_Base or object is RMSmartShape2D return rslt func edit(object): on_edge = false deselect_verts() if is_shape_valid(shape): disconnect_shape(shape) if is_shape_valid(legacy_shape): disconnect_shape(legacy_shape) shape = null legacy_shape = null if object is RMSmartShape2D: tb_hb.hide() tb_hb_legacy_import.show() if object.closed_shape: tb_import.icon = ICON_IMPORT_CLOSED else: tb_import.icon = ICON_IMPORT_OPEN legacy_shape = object connect_shape(legacy_shape) else: tb_hb.show() tb_hb_legacy_import.hide() shape = object connect_shape(shape) update_overlays() func make_visible(visible): pass ############ # SNAPPING # ############ func use_global_snap() -> bool: return ! tb_snap_popup.is_item_checked(1) func use_snap() -> bool: return tb_snap_popup.is_item_checked(0) func get_snap_offset() -> Vector2: return gui_snap_settings.get_snap_offset() func get_snap_step() -> Vector2: return gui_snap_settings.get_snap_step() func snap(v: Vector2, force: bool = false) -> Vector2: if not use_snap() and not force: return v var step = get_snap_step() var offset = get_snap_offset() var t = Transform2D.IDENTITY if use_global_snap(): t = shape.get_global_transform() return snap_position(v, offset, step, t) static func snap_position( pos_global: Vector2, snap_offset: Vector2, snap_step: Vector2, local_t: Transform2D ) -> Vector2: # Move global position to local position to snap in local space var pos_local = local_t * pos_global # Snap in local space var x = pos_local.x if snap_step.x != 0: var delta = fmod(pos_local.x, snap_step.x) # Round up if delta >= (snap_step.x / 2.0): x = pos_local.x + (snap_step.x - delta) # Round down else: x = pos_local.x - delta var y = pos_local.y if snap_step.y != 0: var delta = fmod(pos_local.y, snap_step.y) # Round up if delta >= (snap_step.y / 2.0): y = pos_local.y + (snap_step.y - delta) # Round down else: y = pos_local.y - delta # Transform local position to global position var pos_global_snapped = (local_t.affine_inverse() * Vector2(x, y)) + snap_offset #print ("%s | %s | %s | %s" % [pos_global, pos_local, Vector2(x,y), pos_global_snapped]) return pos_global_snapped ########## # PLUGIN # ########## func _import_legacy(): call_deferred("_import_legacy_impl") func _import_legacy_impl(): if legacy_shape == null: push_error("LEGACY SHAPE IS NULL") return if not legacy_shape is RMSmartShape2D: push_error("LEGACY SHAPE NOT VALID") return var par = legacy_shape.get_parent() if par == null: push_error("LEGACY SHAPE PARENT IS NULL") return # Make new shape and set values var new_shape = null if legacy_shape.closed_shape: new_shape = SS2D_Shape_Closed.new() new_shape.name = "SS2D_Shape_Closed" else: new_shape = SS2D_Shape_Open.new() new_shape.name = "SS2D_Shape_Open" new_shape.import_from_legacy(legacy_shape) new_shape.transform = legacy_shape.transform # Add new to scene tree par.add_child(new_shape) new_shape.owner = get_editor_interface().get_edited_scene_root() # Remove Legacy from scene tree disconnect_shape(legacy_shape) par.remove_child(legacy_shape) legacy_shape.queue_free() legacy_shape = null # Edit the new shape #edit(new_shape) func _on_legacy_closed_changed(): if is_shape_valid(legacy_shape): if legacy_shape is RMSmartShape2D: if legacy_shape.closed_shape: tb_import.icon = ICON_IMPORT_CLOSED else: tb_import.icon = ICON_IMPORT_OPEN func disconnect_shape(s): if s.is_connected("points_modified", self, "_on_shape_point_modified"): s.disconnect("points_modified", self, "_on_shape_point_modified") # Legacy if s is RMSmartShape2D: if s.is_connected("on_closed_change", self, "_on_legacy_closed_changed"): s.disconnect("on_closed_change", self, "_on_legacy_closed_changed") func connect_shape(s): if not s.is_connected("points_modified", self, "_on_shape_point_modified"): s.connect("points_modified", self, "_on_shape_point_modified") if s is RMSmartShape2D: if not s.is_connected("on_closed_change", self, "_on_legacy_closed_changed"): s.connect("on_closed_change", self, "_on_legacy_closed_changed") static func get_material_override_from_indicies(shape: SS2D_Shape_Base, indicies: Array): var keys = [] for i in indicies: keys.push_back(shape.get_point_key_at_index(i)) if not shape.has_material_override(keys): return null return shape.get_material_override(keys) func _on_set_edge_material_override_render(enabled: bool): var override = get_material_override_from_indicies(shape, gui_edge_info_panel.indicies) if override != null: override.render = enabled func _on_set_edge_material_override_weld(enabled: bool): var override = get_material_override_from_indicies(shape, gui_edge_info_panel.indicies) if override != null: override.weld = enabled func _on_set_edge_material_override_z_index(z: int): var override = get_material_override_from_indicies(shape, gui_edge_info_panel.indicies) if override != null: override.z_index = z func _on_set_edge_material(m: SS2D_Material_Edge): var override = get_material_override_from_indicies(shape, gui_edge_info_panel.indicies) if override != null: override.edge_material = m func _on_set_edge_material_override(enabled: bool): var indicies = gui_edge_info_panel.indicies if indicies.has(-1) or indicies.size() != 2: return var keys = [] for i in indicies: keys.push_back(shape.get_point_key_at_index(i)) # Get the relevant Override data if any exists var override = null if shape.has_material_override(keys): override = shape.get_material_override(keys) if enabled: if override == null: override = SS2D_Material_Edge_Metadata.new() override.edge_material = null shape.set_material_override(keys, override) # Load override data into the info panel gui_edge_info_panel.load_values_from_meta_material(override) else: if override != null: shape.remove_material_override(keys) static func is_shape_valid(s) -> bool: if s == null: return false if not is_instance_valid(s): return false if not s.is_inside_tree(): return false return true func _on_shape_point_modified(): FUNC.action_invert_orientation(self, "update_overlays", undo, shape) func get_et() -> Transform2D: return get_editor_interface().get_edited_scene_root().get_viewport().global_canvas_transform static func is_key_valid(s: SS2D_Shape_Base, key: int) -> bool: if not is_shape_valid(s): return false return s.has_point(key) func _enter_mode(mode: int): if current_mode == mode: return for tb in [tb_vert_edit, tb_edge_edit, tb_pivot, tb_vert_create]: tb.pressed = false previous_mode = current_mode current_mode = mode match mode: MODE.CREATE_VERT: tb_vert_create.pressed = true MODE.EDIT_VERT: tb_vert_edit.pressed = true MODE.EDIT_EDGE: tb_edge_edit.pressed = true MODE.SET_PIVOT: tb_pivot.pressed = true _: tb_vert_edit.pressed = true update_overlays() func _set_pivot(point: Vector2): var et = get_et() var np: Vector2 = point var ct: Transform2D = shape.get_global_transform() ct.origin = np shape.disable_constraints() for i in shape.get_point_count(): var key = shape.get_point_key_at_index(i) var pt = shape.get_global_transform().xform(shape.get_point_position(key)) shape.set_point_position(key, ct.affine_inverse().xform(pt)) shape.enable_constraints() shape.position = shape.get_parent().get_global_transform().affine_inverse().xform(np) _enter_mode(current_mode) update_overlays() func _add_collision(): call_deferred("_add_deferred_collision") func _add_deferred_collision(): if not shape.get_parent() is StaticBody2D: var static_body: StaticBody2D = StaticBody2D.new() var t: Transform2D = shape.transform static_body.position = shape.position shape.position = Vector2.ZERO shape.get_parent().add_child(static_body) static_body.owner = get_editor_interface().get_edited_scene_root() shape.get_parent().remove_child(shape) static_body.add_child(shape) shape.owner = get_editor_interface().get_edited_scene_root() var poly: CollisionPolygon2D = CollisionPolygon2D.new() static_body.add_child(poly) poly.owner = get_editor_interface().get_edited_scene_root() # TODO: Make this a option at some point poly.modulate.a = 0.3 poly.visible = false shape.collision_polygon_node_path = shape.get_path_to(poly) shape.set_as_dirty() ############# # RENDERING # ############# func forward_canvas_draw_over_viewport(overlay: Control): # Something might force a draw which we had no control over, # in this case do some updating to be sure if not is_shape_valid(shape) or not is_inside_tree(): return if undo_version != undo.get_version(): if ( undo.get_current_action_name() == "Move CanvasItem" or undo.get_current_action_name() == "Rotate CanvasItem" or undo.get_current_action_name() == "Scale CanvasItem" ): shape.set_as_dirty() undo_version = undo.get_version() match current_mode: MODE.CREATE_VERT: draw_mode_edit_vert(overlay) if Input.is_key_pressed(KEY_ALT) and Input.is_key_pressed(KEY_SHIFT): draw_new_shape_preview(overlay) elif Input.is_key_pressed(KEY_ALT): draw_new_point_close_preview(overlay) else: draw_new_point_preview(overlay) MODE.EDIT_VERT: draw_mode_edit_vert(overlay) if Input.is_key_pressed(KEY_ALT): if Input.is_key_pressed(KEY_SHIFT): draw_new_shape_preview(overlay) elif not on_edge: draw_new_point_close_preview(overlay) MODE.EDIT_EDGE: draw_mode_edit_edge(overlay) shape.update() func draw_mode_edit_edge(overlay: Control): var t: Transform2D = get_et() * shape.get_global_transform() var verts = shape.get_vertices() var edges = shape.get_edges() var color_highlight = Color(1.0, 0.75, 0.75, 1.0) var color_normal = Color(1.0, 0.25, 0.25, 0.8) draw_shape_outline(overlay, t, verts, color_normal, 3.0) draw_vert_handles(overlay, t, verts, false) if current_action.type == ACTION_VERT.MOVE_VERT: var edge_point_keys = current_action.keys var p1 = shape.get_point_position(edge_point_keys[0]) var p2 = shape.get_point_position(edge_point_keys[1]) overlay.draw_line(t.xform(p1), t.xform(p2), color_highlight, 5.0) elif on_edge: var offset = shape.get_closest_offset_straight_edge(t.affine_inverse().xform(edge_point)) var edge_point_keys = _get_edge_point_keys_from_offset(offset, true) var p1 = shape.get_point_position(edge_point_keys[0]) var p2 = shape.get_point_position(edge_point_keys[1]) overlay.draw_line(t.xform(p1), t.xform(p2), color_highlight, 5.0) func draw_mode_edit_vert(overlay: Control): var t: Transform2D = get_et() * shape.get_global_transform() var verts = shape.get_vertices() var points = shape.get_tessellated_points() draw_shape_outline(overlay, t, points) draw_vert_handles(overlay, t, verts, true) if on_edge: overlay.draw_texture(ICON_ADD_HANDLE, edge_point - ICON_ADD_HANDLE.get_size() * 0.5) # Draw Highlighted Handle if current_action.is_single_vert_selected(): var tex = ICON_HANDLE_SELECTED overlay.draw_texture( tex, t.xform(verts[current_action.current_point_index(shape)]) - tex.get_size() * 0.5 ) func draw_shape_outline(overlay: Control, t: Transform2D, points, color = null, width = 2.0): # Draw Outline var prev_pt = null if color == null: color = shape.modulate for i in range(0, points.size(), 1): var pt = points[i] if prev_pt != null: overlay.draw_line(prev_pt, t.xform(pt), color, width, true) prev_pt = t.xform(pt) func draw_vert_handles(overlay: Control, t: Transform2D, verts, control_points: bool): for i in range(0, verts.size(), 1): # Draw Vert handles var key: int = shape.get_point_key_at_index(i) var hp: Vector2 = t.xform(verts[i]) var icon = ICON_HANDLE_BEZIER if Input.is_key_pressed(KEY_SHIFT) else ICON_HANDLE overlay.draw_texture(icon, hp - icon.get_size() * 0.5) # Draw Width handles # var normal = _get_vert_normal(t, verts, i) # var width_handle_icon = WIDTH_HANDLES[int((normal.angle() + PI / 8 + TAU) / PI * 4) % 4] # overlay.draw_texture(width_handle_icon, hp - width_handle_icon.get_size() * 0.5 + normal * WIDTH_HANDLE_OFFSET) # Draw Width handle var offset = WIDTH_HANDLE_OFFSET var width_handle_key = closest_key if ( Input.is_mouse_button_pressed(BUTTON_LEFT) and current_action.type == ACTION_VERT.MOVE_WIDTH_HANDLE ): offset *= width_scaling width_handle_key = current_action.keys[0] var width_handle_normal = _get_vert_normal(t, verts, shape.get_point_index(width_handle_key)) var vertex_position: Vector2 = t.xform(shape.get_point_position(width_handle_key)) var icon_position: Vector2 = vertex_position + width_handle_normal * offset var rect_size: Vector2 = Vector2.ONE * 10.0 var width_handle_color = Color("f53351") overlay.draw_line(vertex_position, icon_position, width_handle_color, 1.0, true) overlay.draw_set_transform(icon_position, width_handle_normal.angle(), Vector2.ONE) overlay.draw_rect(Rect2(-rect_size / 2.0, rect_size), width_handle_color, true, 1.0) overlay.draw_set_transform(Vector2.ZERO, 0, Vector2.ONE) # Draw Control point handles if control_points: for i in range(0, verts.size(), 1): var normal = _get_vert_normal(t, verts, i) var key = shape.get_point_key_at_index(i) var hp = t.xform(verts[i]) # Drawing the point-out for the last point makes no sense, as there's no point ahead of it if i < verts.size() - 1: var pointout = t.xform(verts[i] + shape.get_point_out(key)) if hp != pointout: _draw_control_point_line(overlay, hp, pointout, ICON_HANDLE_CONTROL) # Drawing the point-in for point 0 makes no sense, as there's no point behind it if i > 0: var pointin = t.xform(verts[i] + shape.get_point_in(key)) if hp != pointin: _draw_control_point_line(overlay, hp, pointin, ICON_HANDLE_CONTROL) func _draw_control_point_line(c: Control, vert: Vector2, cp: Vector2, tex: Texture): # Draw the line with a dark and light color to be visible on all backgrounds var color_dark = Color(0, 0, 0, 0.3) var color_light = Color(1, 1, 1, .5) var width = 2.0 var normal = (cp - vert).normalized() c.draw_line(vert + normal * 4 + Vector2.DOWN, cp + Vector2.DOWN, color_dark, width, true) c.draw_line(vert + normal * 4, cp, color_light, width, true) c.draw_texture(tex, cp - tex.get_size() * 0.5) func draw_new_point_preview(overlay: Control): # Draw lines to where a new point will be added var verts = shape.get_vertices() var t: Transform2D = get_et() * shape.get_global_transform() var color = Color(1, 1, 1, .5) var width = 2 var a var mouse = overlay.get_local_mouse_position() if is_shape_closed(shape): a = t.xform(verts[verts.size() - 2]) var b = t.xform(verts[0]) overlay.draw_line(mouse, b, color, width * .5, true) else: a = t.xform(verts[verts.size() - 1]) overlay.draw_line(mouse, a, color, width, true) overlay.draw_texture(ICON_ADD_HANDLE, mouse - ICON_ADD_HANDLE.get_size() * 0.5) func draw_new_point_close_preview(overlay: Control): # Draw lines to where a new point will be added var verts = shape.get_vertices() var t: Transform2D = get_et() * shape.get_global_transform() var color = Color(1, 1, 1, .5) var width = 2 var mouse = overlay.get_local_mouse_position() var a = t.xform(shape.get_point_position(closest_edge_keys[0])) var b = t.xform(shape.get_point_position(closest_edge_keys[1])) # var a = # var b = t.xform() overlay.draw_line(mouse, b, color, width, true) overlay.draw_line(mouse, a, color, width, true) overlay.draw_texture(ICON_ADD_HANDLE, mouse - ICON_ADD_HANDLE.get_size() * 0.5) func draw_new_shape_preview(overlay: Control): # Draw a plus where a new shape will be added var mouse = overlay.get_local_mouse_position() overlay.draw_texture(ICON_ADD_HANDLE, mouse - ICON_ADD_HANDLE.get_size() * 0.5) ########## # PLUGIN # ########## func deselect_verts(): current_action = ActionDataVert.new([], [], [], [], [], ACTION_VERT.NONE) func select_verticies(keys: Array, action: int) -> ActionDataVert: var from_positions = [] var from_positions_c_in = [] var from_positions_c_out = [] var from_widths = [] for key in keys: from_positions.push_back(shape.get_point_position(key)) from_positions_c_in.push_back(shape.get_point_in(key)) from_positions_c_out.push_back(shape.get_point_out(key)) from_widths.push_back(shape.get_point_width(key)) return ActionDataVert.new( keys, from_positions, from_positions_c_in, from_positions_c_out, from_widths, action ) func select_vertices_to_move(keys: Array, _mouse_starting_pos_viewport: Vector2): _mouse_motion_delta_starting_pos = _mouse_starting_pos_viewport current_action = select_verticies(keys, ACTION_VERT.MOVE_VERT) func select_control_points_to_move( keys: Array, _mouse_starting_pos_viewport: Vector2, action = ACTION_VERT.MOVE_CONTROL ): current_action = select_verticies(keys, action) _mouse_motion_delta_starting_pos = _mouse_starting_pos_viewport func select_width_handle_to_move(keys: Array, _mouse_starting_pos_viewport: Vector2): _mouse_motion_delta_starting_pos = _mouse_starting_pos_viewport current_action = select_verticies(keys, ACTION_VERT.MOVE_WIDTH_HANDLE) ######### # INPUT # ######### func _input_handle_right_click_press(mb_position: Vector2, grab_threshold: float) -> bool: if not shape.can_edit: return false if current_mode == MODE.EDIT_VERT or current_mode == MODE.CREATE_VERT: # Mouse over a single vertex? if current_action.is_single_vert_selected(): FUNC.action_delete_point(self, "update_overlays", undo, shape, current_action.keys[0]) undo_version = undo.get_version() deselect_verts() return true else: # Mouse over a control point? var et = get_et() var points_in = FUNC.get_intersecting_control_point_in( shape, et, mb_position, grab_threshold ) var points_out = FUNC.get_intersecting_control_point_out( shape, et, mb_position, grab_threshold ) if not points_in.empty(): FUNC.action_delete_point_in(self, "update_overlays", undo, shape, points_in[0]) undo_version = undo.get_version() return true elif not points_out.empty(): FUNC.action_delete_point_out(self, "update_overlays", undo, shape, points_out[0]) undo_version = undo.get_version() return true elif current_mode == MODE.EDIT_EDGE: if on_edge: gui_edge_info_panel.visible = not gui_edge_info_panel.visible gui_edge_info_panel.rect_position = mb_position + Vector2(256, -24) return false func _input_handle_left_click( mb: InputEventMouseButton, vp_m_pos: Vector2, t: Transform2D, et: Transform2D, grab_threshold: float ) -> bool: # Set Pivot? if current_mode == MODE.SET_PIVOT: var local_position = et.affine_inverse().xform(mb.position) if use_snap(): local_position = snap(local_position) FUNC.action_set_pivot(self, "_set_pivot", undo, shape, et, local_position) undo_version = undo.get_version() return true if current_mode == MODE.EDIT_VERT or current_mode == MODE.CREATE_VERT: gui_edge_info_panel.visible = false # Any nearby control points to move? if not Input.is_key_pressed(KEY_ALT): if _input_move_control_points(mb, vp_m_pos, grab_threshold): return true # Highlighting a vert to move or add control points to if current_action.is_single_vert_selected(): if on_width_handle: select_width_handle_to_move([current_action.current_point_key()], vp_m_pos) elif Input.is_key_pressed(KEY_SHIFT): select_control_points_to_move([current_action.current_point_key()], vp_m_pos) return true else: select_vertices_to_move([current_action.current_point_key()], vp_m_pos) return true # Split the Edge? if _input_split_edge(mb, vp_m_pos, t): return true if not on_edge: # Create new point if Input.is_key_pressed(KEY_ALT) or current_mode == MODE.CREATE_VERT: var local_position = t.affine_inverse().xform(mb.position) if use_snap(): local_position = snap(local_position) if Input.is_key_pressed(KEY_SHIFT) and Input.is_key_pressed(KEY_ALT): # Copy shape with a new single point var copy = copy_shape(shape) copy.set_point_array(SS2D_Point_Array.new()) copy.clear_all_material_overrides() var new_key = FUNC.action_add_point( self, "update_overlays", undo, copy, local_position ) select_vertices_to_move([new_key], vp_m_pos) _enter_mode(MODE.CREATE_VERT) var selection := get_editor_interface().get_selection() selection.clear() selection.add_node(copy) elif Input.is_key_pressed(KEY_ALT): var new_key = FUNC.action_add_point( self, "update_overlays", undo, shape, local_position, shape.get_point_index(closest_edge_keys[1]) ) else: var new_key = FUNC.action_add_point( self, "update_overlays", undo, shape, local_position ) select_vertices_to_move([new_key], vp_m_pos) undo_version = undo.get_version() return true elif current_mode == MODE.EDIT_EDGE: if gui_edge_info_panel.visible: gui_edge_info_panel.visible = false return true if on_edge: # Grab Edge (2 points) var offset = shape.get_closest_offset_straight_edge( t.affine_inverse().xform(edge_point) ) var edge_point_keys = _get_edge_point_keys_from_offset(offset, true) select_vertices_to_move([edge_point_keys[0], edge_point_keys[1]], vp_m_pos) return true return false func _input_handle_mouse_wheel(btn: int) -> bool: if not shape.can_edit: return false var key = current_action.current_point_key() if Input.is_key_pressed(KEY_SHIFT): var width = shape.get_point_width(key) var width_step = 0.1 if btn == BUTTON_WHEEL_DOWN: width_step *= -1 var new_width = width + width_step shape.set_point_width(key, new_width) else: var texture_idx_step = 1 if btn == BUTTON_WHEEL_DOWN: texture_idx_step *= -1 var tex_idx: int = shape.get_point_texture_index(key) + texture_idx_step shape.set_point_texture_index(key, tex_idx) shape.set_as_dirty() update_overlays() _gui_update_info_panels() return true func _input_handle_keyboard_event(event: InputEventKey) -> bool: if not shape.can_edit: return false var kb: InputEventKey = event if _is_valid_keyboard_scancode(kb): if current_action.is_single_vert_selected(): if kb.pressed and kb.scancode == KEY_SPACE: var key = current_action.current_point_key() shape.set_point_texture_flip(key, not shape.get_point_texture_flip(key)) shape.set_as_dirty() shape.update() _gui_update_info_panels() if kb.pressed and kb.scancode == KEY_ESCAPE: # Hide edge_info_panel if gui_edge_info_panel.visible: gui_edge_info_panel.visible = false if current_mode == MODE.CREATE_VERT: _enter_mode(MODE.EDIT_VERT) if kb.scancode == KEY_CONTROL: if kb.pressed and not kb.echo: on_edge = false current_action = select_verticies([closest_key], ACTION_VERT.NONE) else: deselect_verts() update_overlays() if kb.scancode == KEY_ALT: update_overlays() return true return false func _is_valid_keyboard_scancode(kb: InputEventKey) -> bool: match kb.scancode: KEY_ESCAPE: return true KEY_ENTER: return true KEY_SPACE: return true KEY_SHIFT: return true KEY_ALT: return true KEY_CONTROL: return true return false func _input_handle_mouse_button_event( event: InputEventMouseButton, et: Transform2D, grab_threshold: float ) -> bool: if not shape.can_edit: return false var t: Transform2D = et * shape.get_global_transform() var mb: InputEventMouseButton = event var viewport_mouse_position = et.affine_inverse().xform(mb.position) var mouse_wheel_spun = ( mb.pressed and (mb.button_index == BUTTON_WHEEL_DOWN or mb.button_index == BUTTON_WHEEL_UP) ) ####################################### # Mouse Button released if not mb.pressed and mb.button_index == BUTTON_LEFT: var rslt: bool = false var type = current_action.type var _in = type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_IN var _out = type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_OUT if type == ACTION_VERT.MOVE_VERT: FUNC.action_move_verticies(self, "update_overlays", undo, shape, current_action) undo_version = undo.get_version() rslt = true elif _in or _out: FUNC.action_move_control_points( self, "update_overlays", undo, shape, current_action, _in, _out ) undo_version = undo.get_version() rslt = true deselect_verts() return rslt ######################################### # Mouse Wheel on valid point elif mouse_wheel_spun and current_action.is_single_vert_selected(): return _input_handle_mouse_wheel(mb.button_index) ######################################### # Mouse left click elif mb.pressed and mb.button_index == BUTTON_LEFT: return _input_handle_left_click(mb, viewport_mouse_position, t, et, grab_threshold) ######################################### # Mouse right click elif mb.pressed and mb.button_index == BUTTON_RIGHT: return _input_handle_right_click_press(mb.position, grab_threshold) return false func _input_split_edge(mb: InputEventMouseButton, vp_m_pos: Vector2, t: Transform2D) -> bool: if not on_edge: return false var gpoint: Vector2 = mb.position var insertion_point: int = -1 var mb_offset = shape.get_closest_offset(t.affine_inverse().xform(gpoint)) insertion_point = shape.get_point_index(_get_edge_point_keys_from_offset(mb_offset)[1]) if insertion_point == -1: insertion_point = shape.get_point_count() - 1 var key = FUNC.action_split_curve( self, "update_overlays", undo, shape, insertion_point, gpoint, t ) undo_version = undo.get_version() select_vertices_to_move([key], vp_m_pos) on_edge = false return true func _input_move_control_points(mb: InputEventMouseButton, vp_m_pos: Vector2, grab_threshold: float) -> bool: var points_in = FUNC.get_intersecting_control_point_in( shape, get_et(), mb.position, grab_threshold ) var points_out = FUNC.get_intersecting_control_point_out( shape, get_et(), mb.position, grab_threshold ) if not points_in.empty(): select_control_points_to_move([points_in[0]], vp_m_pos, ACTION_VERT.MOVE_CONTROL_IN) return true elif not points_out.empty(): select_control_points_to_move([points_out[0]], vp_m_pos, ACTION_VERT.MOVE_CONTROL_OUT) return true return false func _get_edge_point_keys_from_offset(offset: float, straight: bool = false): for i in range(0, shape.get_point_count() - 1, 1): var key = shape.get_point_key_at_index(i) var key_next = shape.get_point_key_at_index(i + 1) var this_offset = 0 var next_offset = 0 if straight: this_offset = shape.get_closest_offset_straight_edge(shape.get_point_position(key)) next_offset = shape.get_closest_offset_straight_edge(shape.get_point_position(key_next)) else: this_offset = shape.get_closest_offset(shape.get_point_position(key)) next_offset = shape.get_closest_offset(shape.get_point_position(key_next)) if offset >= this_offset and offset <= next_offset: return [key, key_next] # for when the shape is closed and the final point has an offset of 0 if next_offset == 0 and offset >= this_offset: return [key, key_next] return [-1, -1] func _input_motion_is_on_edge(mm: InputEventMouseMotion, grab_threshold: float) -> bool: var xform: Transform2D = get_et() * shape.get_global_transform() if shape.get_point_count() < 2: return false # Find edge var closest_point = null if current_mode == MODE.EDIT_EDGE: closest_point = shape.get_closest_point_straight_edge( xform.affine_inverse().xform(mm.position) ) else: closest_point = shape.get_closest_point(xform.affine_inverse().xform(mm.position)) if closest_point != null: edge_point = xform.xform(closest_point) if edge_point.distance_to(mm.position) <= grab_threshold: return true return false func _input_find_closest_edge_keys(mm: InputEventMouseMotion): var xform: Transform2D = get_et() * shape.get_global_transform() if shape.get_point_count() < 2: return false # Find edge var closest_point = null closest_point = shape.get_closest_point_straight_edge(xform.affine_inverse().xform(mm.position)) var edge_point = xform.xform(closest_point) var offset = shape.get_closest_offset_straight_edge(xform.affine_inverse().xform(edge_point)) closest_edge_keys = _get_edge_point_keys_from_offset(offset, true) func get_mouse_over_vert_key(mm: InputEventMouseMotion, grab_threshold: float) -> int: var xform: Transform2D = get_et() * shape.get_global_transform() # However, if near a control point or one of its handles then we are not on the edge for k in shape.get_all_point_keys(): var pp: Vector2 = shape.get_point_position(k) var p: Vector2 = xform.xform(pp) if p.distance_to(mm.position) <= grab_threshold: return k return -1 func get_mouse_over_width_handle(mm: InputEventMouseMotion, grab_threshold: float) -> int: var xform: Transform2D = get_et() * shape.get_global_transform() for k in shape.get_all_point_keys(): var pp: Vector2 = shape.get_point_position(k) var normal: Vector2 = _get_vert_normal( xform, shape.get_vertices(), shape.get_point_index(k) ) var p: Vector2 = xform.xform(pp) + normal * WIDTH_HANDLE_OFFSET if p.distance_to(mm.position) <= grab_threshold: return k return -1 func _input_motion_move_control_points(delta: Vector2, _in: bool, _out: bool) -> bool: var rslt = false for i in range(0, current_action.keys.size(), 1): var key = current_action.keys[i] var from = current_action.starting_positions[i] var out_multiplier = 1 # Invert the delta for position_out if moving both at once if _out and _in: out_multiplier = -1 var new_position_in = delta + current_action.starting_positions_control_in[i] var new_position_out = ( (delta * out_multiplier) + current_action.starting_positions_control_out[i] ) if use_snap(): new_position_in = snap(new_position_in) new_position_out = snap(new_position_out) if _in: shape.set_point_in(key, new_position_in) rslt = true if _out: shape.set_point_out(key, new_position_out) rslt = true shape.set_as_dirty() update_overlays() return false func _input_motion_move_verts(delta: Vector2) -> bool: for i in range(0, current_action.keys.size(), 1): var key = current_action.keys[i] var from = current_action.starting_positions[i] var new_position = from + delta if use_snap(): new_position = snap(new_position) shape.set_point_position(key, new_position) update_overlays() return true func _input_motion_move_width_handle(mouse_position: Vector2, scale: Vector2) -> bool: for i in range(0, current_action.keys.size(), 1): var key = current_action.keys[i] var from_width = current_action.starting_width[i] var from_position = current_action.starting_positions[i] width_scaling = from_position.distance_to(mouse_position) / WIDTH_HANDLE_OFFSET * scale.x shape.set_point_width(key, round(from_width * width_scaling * 10) / 10) update_overlays() return true func get_closest_vert_to_point(s: SS2D_Shape_Base, p: Vector2) -> int: """ Will Return index of closest vert to point """ var gt = shape.get_global_transform() var verts = s.get_vertices() var transformed_point = gt.affine_inverse() * p var idx = -1 var closest_distance = -1 for i in verts.size(): var distance = verts[i].distance_to(transformed_point) if distance < closest_distance or closest_distance == -1: idx = s.get_point_key_at_index(i) closest_distance = distance return idx func _input_handle_mouse_motion_event( event: InputEventMouseMotion, et: Transform2D, grab_threshold: float ) -> bool: var t: Transform2D = et * shape.get_global_transform() var mm: InputEventMouseMotion = event var delta_current_pos = et.affine_inverse().xform(mm.position) gui_point_info_panel.rect_position = mm.position + Vector2(256, -24) var delta = delta_current_pos - _mouse_motion_delta_starting_pos closest_key = get_closest_vert_to_point(shape, delta_current_pos) if current_mode == MODE.EDIT_VERT or current_mode == MODE.CREATE_VERT: var type = current_action.type var _in = type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_IN var _out = type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_OUT if type == ACTION_VERT.MOVE_VERT: return _input_motion_move_verts(delta) elif _in or _out: return _input_motion_move_control_points(delta, _in, _out) elif type == ACTION_VERT.MOVE_WIDTH_HANDLE: return _input_motion_move_width_handle( et.affine_inverse().xform(mm.position), et.get_scale() ) var mouse_over_key = get_mouse_over_vert_key(event, grab_threshold) var mouse_over_width_handle = get_mouse_over_width_handle(event, grab_threshold) # Make the closest key grabable while holding down Control if ( Input.is_key_pressed(KEY_CONTROL) and not Input.is_key_pressed(KEY_ALT) and mouse_over_width_handle == -1 and mouse_over_key == -1 ): mouse_over_key = closest_key on_width_handle = false if mouse_over_key != -1: on_edge = false current_action = select_verticies([mouse_over_key], ACTION_VERT.NONE) elif mouse_over_width_handle != -1: on_edge = false on_width_handle = true current_action = select_verticies([mouse_over_width_handle], ACTION_VERT.NONE) elif Input.is_key_pressed(KEY_ALT): _input_find_closest_edge_keys(mm) else: deselect_verts() on_edge = _input_motion_is_on_edge(mm, grab_threshold) elif current_mode == MODE.EDIT_EDGE: # Don't update if edge panel is visible if gui_edge_info_panel.visible: return false var type = current_action.type if type == ACTION_VERT.MOVE_VERT: return _input_motion_move_verts(delta) else: deselect_verts() on_edge = _input_motion_is_on_edge(mm, grab_threshold) update_overlays() return false func _get_vert_normal(t: Transform2D, verts, i: int): var point: Vector2 = t.xform(verts[i]) var prev_point: Vector2 = t.xform(verts[(i - 1) % verts.size()]) var next_point: Vector2 = t.xform(verts[(i + 1) % verts.size()]) return ((prev_point - point).normalized().rotated(PI / 2) + (point - next_point).normalized().rotated(PI / 2)).normalized() func copy_shape(shape): var copy: SS2D_Shape_Base if shape is SS2D_Shape_Closed: copy = SS2D_Shape_Closed.new() if shape is SS2D_Shape_Open: copy = SS2D_Shape_Open.new() if shape is SS2D_Shape_Meta: copy = SS2D_Shape_Meta.new() copy.position = shape.position copy.scale = shape.scale copy.modulate = shape.modulate copy.shape_material = shape.shape_material copy.editor_debug = shape.editor_debug copy.flip_edges = shape.flip_edges copy.editor_debug = shape.editor_debug copy.collision_size = shape.collision_size copy.collision_offset = shape.collision_offset copy.tessellation_stages = shape.tessellation_stages copy.tessellation_tolerence = shape.tessellation_tolerence copy.curve_bake_interval = shape.curve_bake_interval copy.material_overrides = shape.material_overrides shape.get_parent().add_child(copy) copy.set_owner(get_tree().get_edited_scene_root()) if ( shape.collision_polygon_node_path != "" and shape.has_node(shape.collision_polygon_node_path) ): var collision_polygon_original = shape.get_node(shape.collision_polygon_node_path) var collision_polygon_new = CollisionPolygon2D.new() collision_polygon_new.visible = collision_polygon_original.visible collision_polygon_original.get_parent().add_child(collision_polygon_new) collision_polygon_new.set_owner(get_tree().get_edited_scene_root()) copy.collision_polygon_node_path = copy.get_path_to(collision_polygon_new) return copy func is_shape_closed(shape): if shape is SS2D_Shape_Open: return false if shape is SS2D_Shape_Meta: return shape.treat_as_closed() return true ######### # DEBUG # ######### func _debug_mouse_positions(mm, t): print("========================================") print("MouseDelta:%s" % str(_mouse_motion_delta_starting_pos)) print("= MousePositions =") print("Position: %s" % str(mm.position)) print("Relative: %s" % str(mm.relative)) print("= Transforms =") print("Transform: %s" % str(t)) print("Inverse: %s" % str(t.affine_inverse())) print("= Transformed Mouse positions =") print("Position: %s" % str(t.affine_inverse().xform(mm.position))) print("Relative: %s" % str(t.affine_inverse().xform(mm.relative))) print("MouseDelta:%s" % str(t.affine_inverse().xform(_mouse_motion_delta_starting_pos)))