aboutsummaryrefslogtreecommitdiff
path: root/src/addons/rmsmartshape/plugin.gd
diff options
context:
space:
mode:
authorRaffaele Picca <picster@pixelgod.net>2021-02-18 20:31:25 +0100
committerRaffaele Picca <picster@pixelgod.net>2021-02-18 20:31:25 +0100
commit6406cd4708aeec04c23ffdf098e3319a404e7ed9 (patch)
tree8faedb9b3f733cc6ee901407e3ac4d4ed8ae6012 /src/addons/rmsmartshape/plugin.gd
parentc9b3470e43aed98e2a93a332cfc92c3b9ca05163 (diff)
Initial Commit
Diffstat (limited to 'src/addons/rmsmartshape/plugin.gd')
-rw-r--r--src/addons/rmsmartshape/plugin.gd1503
1 files changed, 1503 insertions, 0 deletions
diff --git a/src/addons/rmsmartshape/plugin.gd b/src/addons/rmsmartshape/plugin.gd
new file mode 100644
index 0000000..7c554d4
--- /dev/null
+++ b/src/addons/rmsmartshape/plugin.gd
@@ -0,0 +1,1503 @@
+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)))