@tool extends EditorPlugin class_name CurvedLines2D const SETTING_NAME_EDITING_ENABLED := "addons/curved_lines_2d/editing_enabled" const SETTING_NAME_HINTS_ENABLED := "addons/curved_lines_2d/hints_enabled" const SETTING_NAME_SHOW_POINT_NUMBERS := "addons/curved_lines_2d/show_point_numbers" const SETTING_NAME_STROKE_WIDTH := "addons/curved_lines_2d/stroke_width" const SETTING_NAME_STROKE_COLOR := "addons/curved_lines_2d/stroke_color" const SETTING_NAME_USE_LINE_2D_FOR_STROKE = "addons/curved_lines_2d/use_line_2d_for_stroke" const SETTING_NAME_FILL_COLOR := "addons/curved_lines_2d/fill_color" const SETTING_NAME_ADD_STROKE_ENABLED := "addons/curved_lines_2d/add_stroke_enabled" const SETTING_NAME_ADD_FILL_ENABLED := "addons/curved_lines_2d/add_fill_enabled" const SETTING_NAME_ADD_COLLISION_TYPE = "addons/curved_lines_2d/add_collision_type" const SETTING_NAME_PAINT_ORDER := "addons/curved_lines_2d/paint_order" const SETTING_NAME_DEFAULT_LINE_BEGIN_CAP := "addons/curved_lines_2d/line_begin_cap" const SETTING_NAME_DEFAULT_LINE_END_CAP := "addons/curved_lines_2d/line_end_cap" const SETTING_NAME_DEFAULT_LINE_JOINT_MODE := "addons/curved_lines_2d/line_joint_mode" const SETTING_NAME_SNAP_TO_PIXEL := "addons/curved_lines_2d/snap_to_pixel" const SETTING_NAME_SNAP_RESOLUTION := "addons/curved_lines_2d/snap_resolution" const SETTING_NAME_CURVE_UPDATE_CURVE_AT_RUNTIME := "addons/curved_lines_2d/update_curve_at_runtime" const SETTING_NAME_CURVE_RESOURCE_LOCAL_TO_SCENE := "addons/curved_lines_2d/make_resources_local_to_scene" const SETTING_NAME_CURVE_TOLERANCE_DEGREES := "addons/curved_lines_2d/default_tolerance_degrees" const SETTING_NAME_CURVE_MAX_STAGES := "addons/curved_lines_2d/default_max_stages" const SETTING_NAME_ANTIALIASED_LINE_2D := "addons/curved_lines_2d/antialiased_line_2d" const META_NAME_HOVER_POINT_IDX := "_hover_point_idx_" const META_NAME_HOVER_CP_IN_IDX := "_hover_cp_in_idx_" const META_NAME_HOVER_CP_OUT_IDX := "_hover_cp_out_idx_" const META_NAME_HOVER_CLOSEST_POINT := "_hover_closest_point_on_curve_" const META_NAME_HOVER_GRADIENT_FROM := "_hover_gradient_from_" const META_NAME_HOVER_GRADIENT_TO := "_hover_gradient_to_" const META_NAME_HOVER_GRADIENT_COLOR_STOP_IDX := "_hover_gradient_color_stop_idx_" const META_NAME_HOVER_CLOSEST_POINT_ON_GRADIENT_LINE := "_hover_closest_point_on_gradient_" const META_NAME_SELECT_HINT := "_select_hint_" const VIEWPORT_ORANGE := Color(0.737, 0.463, 0.337) enum PaintOrder { FILL_STROKE_MARKERS, STROKE_FILL_MARKERS, FILL_MARKERS_STROKE, MARKERS_FILL_STROKE, STROKE_MARKERS_FILL, MARKERS_STROKE_FILL } enum UndoRedoEntry { UNDOS, DOS, NAME, DO_PROPS, UNDO_PROPS } const PAINT_ORDER_MAP := { PaintOrder.FILL_STROKE_MARKERS: ['_add_fill_to_created_shape', '_add_stroke_to_created_shape', '_add_collision_to_created_shape'], PaintOrder.STROKE_FILL_MARKERS: ['_add_stroke_to_created_shape', '_add_fill_to_created_shape', '_add_collision_to_created_shape'], PaintOrder.FILL_MARKERS_STROKE: ['_add_fill_to_created_shape', '_add_collision_to_created_shape', '_add_stroke_to_created_shape'], PaintOrder.MARKERS_FILL_STROKE: ['_add_collision_to_created_shape', '_add_fill_to_created_shape', '_add_stroke_to_created_shape'], PaintOrder.STROKE_MARKERS_FILL: ['_add_stroke_to_created_shape', '_add_collision_to_created_shape', '_add_fill_to_created_shape'], PaintOrder.MARKERS_STROKE_FILL: ['_add_collision_to_created_shape', '_add_stroke_to_created_shape', '_add_fill_to_created_shape'] } const SHAPE_NAME_MAP := { ScalableVectorShape2D.ShapeType.RECT: "a rectangle", ScalableVectorShape2D.ShapeType.ELLIPSE: "a circle", ScalableVectorShape2D.ShapeType.PATH: "an empty shape" } const OPERATION_NAME_MAP := { Geometry2D.OPERATION_DIFFERENCE: { "verb": "cut out", "noun": "cutout" }, Geometry2D.OPERATION_INTERSECTION: { "verb": "clip off", "noun": "clip" }, Geometry2D.OPERATION_UNION: { "verb": "merge with", "noun": "merged shape" } } enum UniformTransformMode { NONE, TRANSLATE, ROTATE, SCALE } var plugin : Line2DGeneratorInspectorPlugin var scalable_vector_shapes_2d_dock var select_mode_button : Button var undo_redo : EditorUndoRedoManager var in_undo_redo_transaction := false var shape_preview : Curve2D = null var selection_candidate : Node = null var current_cutout_shape := ScalableVectorShape2D.ShapeType.RECT var current_clip_operation := Geometry2D.OPERATION_DIFFERENCE var undo_redo_transaction : Dictionary = { UndoRedoEntry.NAME: "", UndoRedoEntry.DOS: [], UndoRedoEntry.UNDOS: [], UndoRedoEntry.DO_PROPS: [], UndoRedoEntry.UNDO_PROPS: [] } var set_global_position_popup_panel : PopupPanel var arc_settings_popup_panel : PopupPanel var _vp_horizontal_scrollbar_locked_value := 0.0 var _locking_vp_horizontal_scrollbar := false var uniform_transform_edit_buttons : Control var _uniform_transform_mode := UniformTransformMode.NONE var _drag_start := Vector2.ZERO var _prev_uniform_rotate_angle := 0.0 var _stored_natural_center := Vector2.ZERO var _lmb_is_down_inside_viewport := false var merge_node_toggle_button : Button var _merge_box_rect := Rect2(Vector2.ZERO, Vector2.ZERO) func _enter_tree(): scalable_vector_shapes_2d_dock = preload("res://addons/curved_lines_2d/scalable_vector_shapes_2d_dock.tscn").instantiate() plugin = preload("res://addons/curved_lines_2d/line_2d_generator_inspector_plugin.gd").new() add_inspector_plugin(plugin) add_custom_type( "DrawablePath2D", "Path2D", preload("res://addons/curved_lines_2d/drawable_path_2d.gd"), preload("res://addons/curved_lines_2d/DrawablePath2D.svg") ) add_custom_type( "ScalableVectorShape2D", "Node2D", preload("res://addons/curved_lines_2d/scalable_vector_shape_2d.gd"), preload("res://addons/curved_lines_2d/DrawablePath2D.svg") ) add_custom_type( "AdaptableVectorShape3D", "Node3D", preload("res://addons/curved_lines_2d/adaptable_vector_shape_3d.gd"), preload("res://addons/curved_lines_2d/AdaptableVectorShape3D.svg") ) undo_redo = get_undo_redo() add_control_to_bottom_panel(scalable_vector_shapes_2d_dock as Control, "Scalable Vector Shapes 2D") EditorInterface.get_selection().selection_changed.connect(_on_selection_changed) undo_redo.version_changed.connect(update_overlays) make_bottom_panel_item_visible(scalable_vector_shapes_2d_dock) set_global_position_popup_panel = preload("res://addons/curved_lines_2d/set_global_position_popup_panel.tscn").instantiate() arc_settings_popup_panel = preload("res://addons/curved_lines_2d/arc_settings_popup_panel.tscn").instantiate() EditorInterface.get_base_control().add_child(set_global_position_popup_panel) EditorInterface.get_base_control().add_child(arc_settings_popup_panel) if not set_global_position_popup_panel.value_changed.is_connected(_on_global_position_for_handle_changed): set_global_position_popup_panel.value_changed.connect(_on_global_position_for_handle_changed) if not set_global_position_popup_panel.visibility_changed.is_connected(_commit_undo_redo_transaction): set_global_position_popup_panel.visibility_changed.connect(_commit_undo_redo_transaction) if not scalable_vector_shapes_2d_dock.shape_created.is_connected(_on_shape_created): scalable_vector_shapes_2d_dock.shape_created.connect(_on_shape_created) if not scalable_vector_shapes_2d_dock.set_shape_preview.is_connected(_on_shape_preview): scalable_vector_shapes_2d_dock.set_shape_preview.connect(_on_shape_preview) if not scalable_vector_shapes_2d_dock.edit_tab.rect_created.is_connected(_on_rect_created): scalable_vector_shapes_2d_dock.edit_tab.rect_created.connect(_on_rect_created) if not scalable_vector_shapes_2d_dock.edit_tab.ellipse_created.is_connected(_on_ellipse_created): scalable_vector_shapes_2d_dock.edit_tab.ellipse_created.connect(_on_ellipse_created) scene_changed.connect(_on_scene_changed) uniform_transform_edit_buttons = load("res://addons/curved_lines_2d/uniform_transform_edit_buttons.tscn").instantiate() var canvas_editor_buttons_container = EditorInterface.get_editor_viewport_2d().find_parent("*CanvasItemEditor*").find_child("*HFlowContainer*", true, false) canvas_editor_buttons_container.add_child(uniform_transform_edit_buttons) merge_node_toggle_button = Button.new() merge_node_toggle_button.tooltip_text = "Merge vertices (M)" merge_node_toggle_button.icon = load("res://addons/curved_lines_2d/MergeChain.svg") merge_node_toggle_button.toggle_mode = true merge_node_toggle_button.flat = true merge_node_toggle_button.toggled.connect(_on_merge_node_toggle_button_toggled) canvas_editor_buttons_container.add_child(merge_node_toggle_button) if not _get_select_mode_button().toggled.is_connected(_on_select_mode_toggled): _get_select_mode_button().toggled.connect(_on_select_mode_toggled) _on_select_mode_toggled(_get_select_mode_button().button_pressed) uniform_transform_edit_buttons.mode_changed.connect(_on_uniform_transform_mode_changed) uniform_transform_edit_buttons.flip_horizontal.connect(_flip_svs_horizontal) uniform_transform_edit_buttons.flip_vertical.connect(_flip_svs_vertical) func select_node_reversibly(target_node : Node) -> void: if is_instance_valid(target_node): EditorInterface.edit_node(target_node) func _on_merge_node_toggle_button_toggled(toggled_on : bool) -> void: _merge_box_rect.size = Vector2.ZERO if not toggled_on: return var scene_root := EditorInterface.get_edited_scene_root() if is_instance_valid(scene_root): EditorInterface.edit_node(scene_root) update_overlays() func _on_select_mode_toggled(toggled_on : bool) -> void: var current_selection := EditorInterface.get_selection().get_selected_nodes().pop_back() if toggled_on and _is_svs_valid(current_selection): uniform_transform_edit_buttons.enable() merge_node_toggle_button.show() elif toggled_on: uniform_transform_edit_buttons.hide() merge_node_toggle_button.show() else: uniform_transform_edit_buttons.hide() merge_node_toggle_button.hide() merge_node_toggle_button.button_pressed = false func _on_uniform_transform_mode_changed(new_mode : UniformTransformMode) -> void: _uniform_transform_mode = new_mode update_overlays() func _is_ctrl_or_cmd_pressed() -> bool: return Input.is_key_pressed(KEY_CTRL) or Input.is_key_pressed(KEY_META) func _on_shape_preview(curve : Curve2D): shape_preview = curve update_overlays() func _on_rect_created(width : float, height : float, rx : float, ry : float, scene_root : Node) -> void: var new_rect := ScalableVectorShape2D.new() new_rect.shape_type = ScalableVectorShape2D.ShapeType.RECT new_rect.size = Vector2(width, height) new_rect.rx = rx new_rect.ry = ry _create_shape(new_rect, scene_root, "Rectangle") func _on_ellipse_created(rx : float, ry : float, scene_root : Node) -> void: var new_ellipse := ScalableVectorShape2D.new() new_ellipse.shape_type = ScalableVectorShape2D.ShapeType.ELLIPSE new_ellipse.size = Vector2(rx * 2, ry * 2) _create_shape(new_ellipse, scene_root, "Ellipse") func _on_shape_created(curve : Curve2D, scene_root : Node, node_name : String) -> void: var new_shape := ScalableVectorShape2D.new() new_shape.curve = curve _create_shape(new_shape, scene_root, node_name) func _create_shape(new_shape : ScalableVectorShape2D, scene_root : Node, node_name : String, is_cutout_for : ScalableVectorShape2D = null) -> void: var current_selection := EditorInterface.get_selection().get_selected_nodes().pop_back() var parent = current_selection if current_selection is Node else scene_root new_shape.update_curve_at_runtime = _is_setting_update_curve_at_runtime() new_shape.curve.resource_local_to_scene = _is_making_curve_resources_local_to_scene() new_shape.arc_list.resource_local_to_scene = _is_making_curve_resources_local_to_scene() new_shape.tolerance_degrees = _get_default_tolerance_degrees() new_shape.max_stages = _get_default_max_stages() new_shape.name = node_name if not is_instance_valid(is_cutout_for): new_shape.position = Vector2.ZERO undo_redo.create_action("Add a %s to the scene " % node_name) undo_redo.add_do_method(parent, 'add_child', new_shape, true) undo_redo.add_do_method(new_shape, 'set_owner', scene_root) undo_redo.add_do_reference(new_shape) undo_redo.add_undo_method(parent, 'remove_child', new_shape) if is_instance_valid(is_cutout_for): var new_clip_paths := is_cutout_for.clip_paths.duplicate() match current_clip_operation: Geometry2D.OPERATION_UNION: new_shape.use_union_in_stead_of_clipping = true Geometry2D.OPERATION_INTERSECTION: new_shape.use_interect_when_clipping = true Geometry2D.OPERATION_DIFFERENCE: new_shape.use_interect_when_clipping = false new_shape.use_union_in_stead_of_clipping = false new_clip_paths.append(new_shape) undo_redo.add_do_property(is_cutout_for, 'clip_paths', new_clip_paths) undo_redo.add_undo_property(is_cutout_for, 'clip_paths', is_cutout_for.clip_paths) else: for draw_fn in PAINT_ORDER_MAP[_get_default_paint_order()]: call(draw_fn, new_shape, scene_root) undo_redo.add_do_method(self, 'select_node_reversibly', new_shape) undo_redo.add_undo_method(self, 'select_node_reversibly', parent) undo_redo.commit_action() new_shape.stroke_color = _get_default_stroke_color() new_shape.stroke_width = _get_default_stroke_width() new_shape.begin_cap_mode = _get_default_begin_cap() new_shape.end_cap_mode = _get_default_end_cap() new_shape.line_joint_mode = _get_default_joint_mode() if not is_instance_valid(is_cutout_for): _set_viewport_pos_to_selection() func _create_svs_vertex_merge_2d() -> void: var vertex_map := _find_merge_vertices() if vertex_map.size() < 2: return var new_vertex_merge := SVSVertexMerge2D.new() new_vertex_merge.name = "SVSVertexMerge2D" var scene_root := EditorInterface.get_edited_scene_root() var current_selection := EditorInterface.get_selection().get_selected_nodes().pop_back() var parent = current_selection if current_selection is Node else scene_root undo_redo.create_action("Add SVSVertexMerge2D") undo_redo.add_do_method(parent, 'add_child', new_vertex_merge, true) undo_redo.add_do_method(new_vertex_merge, 'set_owner', scene_root) undo_redo.add_do_property(new_vertex_merge, "vertex_map", vertex_map) undo_redo.add_undo_property(new_vertex_merge, "vertex_map", {}) undo_redo.add_undo_method(new_vertex_merge, 'set_owner', null) undo_redo.add_undo_method(parent, 'remove_child', new_vertex_merge) for svs in vertex_map.keys(): undo_redo.add_undo_property(svs, "curve", svs.curve.duplicate()) undo_redo.commit_action() func _add_fill_to_created_shape(new_shape : ScalableVectorShape2D, scene_root : Node) -> void: if _is_add_fill_enabled(): var polygon := Polygon2D.new() polygon.name = "Fill" polygon.color = _get_default_fill_color() undo_redo.add_do_property(new_shape, 'polygon', polygon) undo_redo.add_do_method(new_shape, 'add_child', polygon, true) undo_redo.add_do_method(polygon, 'set_owner', scene_root) undo_redo.add_do_reference(polygon) undo_redo.add_undo_method(new_shape, 'remove_child', polygon) func _add_stroke_to_created_shape(new_shape : ScalableVectorShape2D, scene_root : Node) -> void: if _is_add_stroke_enabled(): if _using_line_2d_for_stroke(): var line := Line2D.new() line.name = "Stroke" line.sharp_limit = 90.0 if CurvedLines2D._use_antialiased_line_2d(): line.texture = load("res://addons/curved_lines_2d/LumAlpha8.tex") line.texture_mode = Line2D.LINE_TEXTURE_TILE line.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC undo_redo.add_do_property(new_shape, 'line', line) undo_redo.add_do_method(new_shape, 'add_child', line, true) undo_redo.add_do_method(line, 'set_owner', scene_root) undo_redo.add_do_reference(line) undo_redo.add_undo_method(new_shape, 'remove_child', line) else: var poly_stroke := Polygon2D.new() poly_stroke.name = "Stroke" undo_redo.add_do_property(new_shape, 'poly_stroke', poly_stroke) undo_redo.add_do_method(new_shape, 'add_child', poly_stroke, true) undo_redo.add_do_method(poly_stroke, 'set_owner', scene_root) undo_redo.add_do_reference(poly_stroke) undo_redo.add_undo_method(new_shape, 'remove_child', poly_stroke) func _add_collision_to_created_shape(new_shape : ScalableVectorShape2D, scene_root : Node) -> void: if _add_collision_object_type() != ScalableVectorShape2D.CollisionObjectType.NONE: var collision : CollisionObject2D = null match _add_collision_object_type(): ScalableVectorShape2D.CollisionObjectType.STATIC_BODY_2D: collision = StaticBody2D.new() ScalableVectorShape2D.CollisionObjectType.AREA_2D: collision = Area2D.new() ScalableVectorShape2D.CollisionObjectType.ANIMATABLE_BODY_2D: collision = AnimatableBody2D.new() ScalableVectorShape2D.CollisionObjectType.RIGID_BODY_2D: collision = RigidBody2D.new() ScalableVectorShape2D.CollisionObjectType.CHARACTER_BODY_2D: collision = CharacterBody2D.new() ScalableVectorShape2D.CollisionObjectType.PHYSICAL_BONE_2D: collision = PhysicalBone2D.new() undo_redo.add_do_method(new_shape, 'add_child', collision, true) undo_redo.add_do_reference(collision) undo_redo.add_do_method(collision, 'set_owner', scene_root) undo_redo.add_do_property(new_shape, 'collision_object', collision) undo_redo.add_undo_method(new_shape, 'remove_child', collision) func _scene_can_export_animations() -> bool: return (EditorInterface.get_edited_scene_root() is CanvasItem and not EditorInterface.get_edited_scene_root().find_children("*", "AnimationPlayer").filter( func(an): return an.owner == EditorInterface.get_edited_scene_root() ).is_empty() and not EditorInterface.get_edited_scene_root() .find_children("*", "ScalableVectorShape2D").is_empty() ) func _on_selection_changed(): var scene_root := EditorInterface.get_edited_scene_root() var current_selection := EditorInterface.get_selection().get_selected_nodes().pop_back() if _is_editing_enabled() and is_instance_valid(scene_root): # inelegant fix to always keep an instance of Node selected, so # _forward_canvas_gui_input will still be called upon losing focus if (not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and EditorInterface.get_selection().get_selected_nodes().is_empty()): EditorInterface.edit_node(scene_root) if current_selection is AnimationPlayer and _scene_can_export_animations(): scalable_vector_shapes_2d_dock.set_selected_animation_player(current_selection) _on_select_mode_toggled(_get_select_mode_button().button_pressed) update_overlays() func _on_scene_changed(scn : Node): if _scene_can_export_animations(): var anim_pl = scn.find_children("*", "AnimationPlayer").filter( func(an): return an.owner == EditorInterface.get_edited_scene_root() ).pop_back() scalable_vector_shapes_2d_dock.set_selected_animation_player(anim_pl) else: scalable_vector_shapes_2d_dock.set_selected_animation_player(null) func _handles(object: Object) -> bool: return object is Node func _find_scalable_vector_shape_2d_nodes() -> Array[Node]: var scene_root := EditorInterface.get_edited_scene_root() if is_instance_valid(scene_root): var result := scene_root.find_children("*", "ScalableVectorShape2D") if scene_root is ScalableVectorShape2D: result.push_front(scene_root) return result return [] func _find_scalable_vector_shape_2d_nodes_at(pos : Vector2) -> Array[Node]: if is_instance_valid(EditorInterface.get_edited_scene_root()): return (_find_scalable_vector_shape_2d_nodes() .filter(func(x : ScalableVectorShape2D): return not x.has_meta("_edit_lock_")) .filter(func(x : ScalableVectorShape2D): return x.is_visible_in_tree()) .filter(func(x : ScalableVectorShape2D): return x.owner == EditorInterface.get_edited_scene_root()) .filter(func(x : ScalableVectorShape2D): return x.has_point(pos)) ) return [] func _is_change_pivot_button_active() -> bool: var results = ( EditorInterface.get_editor_viewport_2d().find_parent("*CanvasItemEditor*") .find_children("*Button*", "", true, false) ) if results.size() >= 6: return results[5].button_pressed return false func _get_select_mode_button() -> Button: if is_instance_valid(select_mode_button): return select_mode_button else: select_mode_button = ( EditorInterface.get_editor_viewport_2d().find_parent("*CanvasItemEditor*") .find_child("*Button*", true, false) ) return select_mode_button func _get_viewport_center() -> Vector2: var tr := EditorInterface.get_editor_viewport_2d().global_canvas_transform var og := tr.get_origin() var sz := Vector2(EditorInterface.get_editor_viewport_2d().size) return (sz / 2) / tr.get_scale() - og / tr.get_scale() func _set_viewport_pos_to_selection() -> void: EditorInterface.get_editor_viewport_2d().get_parent().grab_focus() var key_ev := InputEventKey.new() key_ev.keycode = KEY_F key_ev.pressed = true Input.parse_input_event(key_ev) func _vp_transform(p : Vector2) -> Vector2: var s := EditorInterface.get_editor_viewport_2d().get_final_transform().get_scale() var o := EditorInterface.get_editor_viewport_2d().get_final_transform().get_origin() return (p * s) + o func _is_svs_valid(svs : Object) -> bool: return is_instance_valid(svs) and svs is ScalableVectorShape2D and svs.curve func _get_hovered_handle_metadata(svs : ScalableVectorShape2D) -> Dictionary: if svs.has_meta(META_NAME_HOVER_POINT_IDX): return { 'global_pos': svs.to_global(svs.curve.get_point_position( svs.get_meta(META_NAME_HOVER_POINT_IDX) )), 'meta_name': META_NAME_HOVER_POINT_IDX, 'point_idx': svs.get_meta(META_NAME_HOVER_POINT_IDX) } elif svs.has_meta(META_NAME_HOVER_CP_IN_IDX): return { 'global_pos': svs.to_global(svs.curve.get_point_position( svs.get_meta(META_NAME_HOVER_CP_IN_IDX) ) + svs.curve.get_point_in( svs.get_meta(META_NAME_HOVER_CP_IN_IDX) )), 'meta_name': META_NAME_HOVER_CP_IN_IDX, 'point_idx': svs.get_meta(META_NAME_HOVER_CP_IN_IDX) } elif svs.has_meta(META_NAME_HOVER_CP_OUT_IDX): return { 'global_pos': svs.to_global(svs.curve.get_point_position( svs.get_meta(META_NAME_HOVER_CP_OUT_IDX) ) + svs.curve.get_point_out( svs.get_meta(META_NAME_HOVER_CP_OUT_IDX) )), 'meta_name': META_NAME_HOVER_CP_OUT_IDX, 'point_idx': svs.get_meta(META_NAME_HOVER_CP_OUT_IDX) } return {} func _curve_control_has_hover(svs : ScalableVectorShape2D) -> bool: return ( svs.has_meta(META_NAME_HOVER_POINT_IDX) or svs.has_meta(META_NAME_HOVER_CP_IN_IDX) or svs.has_meta(META_NAME_HOVER_CP_OUT_IDX) ) func _handle_has_hover(svs : ScalableVectorShape2D) -> bool: return ( svs.has_meta(META_NAME_HOVER_POINT_IDX) or svs.has_meta(META_NAME_HOVER_CP_IN_IDX) or svs.has_meta(META_NAME_HOVER_CP_OUT_IDX) or svs.has_meta(META_NAME_HOVER_GRADIENT_FROM) or svs.has_meta(META_NAME_HOVER_GRADIENT_TO) or svs.has_meta(META_NAME_HOVER_GRADIENT_COLOR_STOP_IDX) ) func _draw_control_point_handle(viewport_control : Control, svs : ScalableVectorShape2D, handle : Dictionary, prefix : String, is_hovered : bool, self_is_hovered : bool) -> String: if handle[prefix].length(): var color := VIEWPORT_ORANGE if is_hovered else Color.WHITE var width := 2 if is_hovered else 1 viewport_control.draw_line(_vp_transform(handle['point_position']), _vp_transform(handle[prefix + '_position']), Color.WEB_GRAY, 1, true) viewport_control.draw_circle(_vp_transform(handle[prefix + '_position']), 5, Color.DIM_GRAY) viewport_control.draw_circle(_vp_transform(handle[prefix + '_position']), 5, color, false, width) if self_is_hovered: var hint_txt := "Control point " + prefix if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): hint_txt += "\n - Drag to move\n - Right click to delete" hint_txt += "\n - Hold Shift + Drag to move mirrored" if Input.is_key_pressed(KEY_ALT): hint_txt += "\n - Click to set exact global position (Alt held)" else: hint_txt += "\n - Alt + Click to set exact global position" return hint_txt return "" func _draw_rect_control_point_handle(viewport_control : Control, svs : ScalableVectorShape2D, handle : Dictionary, prefix : String, is_hovered : bool) -> String: var prop_name := "rx" if prefix == "in" else "ry" var color := VIEWPORT_ORANGE if is_hovered else Color.WHITE var width := 2 if is_hovered else 1 viewport_control.draw_line( _vp_transform(handle['top_left_pos']), _vp_transform(handle[prefix + '_position']), Color.WEB_GRAY, 1, true) viewport_control.draw_circle(_vp_transform(handle[prefix + '_position']), 5, Color.DIM_GRAY) viewport_control.draw_circle(_vp_transform(handle[prefix + '_position']), 5, color, false, width) if is_hovered: var hint_txt := "Control point rounded corners " if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): var dir := "right / left" if prefix == 'in' else "up / down" hint_txt += "\n - Drag %s to move \n - Right click to remove rounded corners" % dir hint_txt += "\n - Hold Shift + Drag to drag only this handle" return hint_txt return "" func _draw_hint(viewport_control : Control, txt : String, force_draw := false) -> void: if set_global_position_popup_panel.visible: return if not _get_select_mode_button().button_pressed: return if not _are_hints_enabled() and not force_draw: return var txt_pos := (_vp_transform(EditorInterface.get_editor_viewport_2d().get_mouse_position()) + Vector2(15, 8)) var lines := txt.split("\n") for i in range(lines.size()): var text := lines[i] var pos := txt_pos + Vector2.DOWN * (i * (ThemeDB.fallback_font_size + ThemeDB.fallback_font_size * .2)) viewport_control.draw_string_outline(ThemeDB.fallback_font, pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, ThemeDB.fallback_font_size, 3, Color.BLACK) viewport_control.draw_string(ThemeDB.fallback_font, pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, ThemeDB.fallback_font_size, Color.WHITE_SMOKE) func _draw_point_number(viewport_control: Control, p : Vector2, text : String) -> void: if not _am_showing_point_numbers(): return var pos := _vp_transform(p) var width := 8 * (text.length() + 1) viewport_control.draw_string_outline(ThemeDB.fallback_font, pos + + Vector2(-width, 6), text, HORIZONTAL_ALIGNMENT_LEFT, width, ThemeDB.fallback_font_size, 3, Color.BLACK) viewport_control.draw_string(ThemeDB.fallback_font, pos + Vector2(-width, 6), text, HORIZONTAL_ALIGNMENT_LEFT, width, ThemeDB.fallback_font_size, Color.WHITE_SMOKE) func _draw_handles(viewport_control : Control, svs : ScalableVectorShape2D) -> void: if not _get_select_mode_button().button_pressed: return var hint_txt := "" var point_txt := "" var point_pos_txt := "" var point_hint_pos := Vector2.ZERO var handles = svs.get_curve_handles() for i in range(handles.size()): var handle = handles[i] var is_hovered : bool = svs.get_meta(META_NAME_HOVER_POINT_IDX, -1) == i var cp_in_is_hovered : bool = svs.get_meta(META_NAME_HOVER_CP_IN_IDX, -1) == i var cp_out_is_hovered : bool = svs.get_meta(META_NAME_HOVER_CP_OUT_IDX, -1) == i var color := VIEWPORT_ORANGE if is_hovered else Color.WHITE var width := 2 if is_hovered else 1 if is_hovered: point_pos_txt = "Global point position: (%.3f, %.3f)" % [handle["point_position"].x, handle["point_position"].y] elif cp_in_is_hovered: point_pos_txt = "Global curve handle position: (%.3f, %.3f)" % [handle["in_position"].x,handle["in_position"].y] elif cp_out_is_hovered: point_pos_txt = "Global curve handle position: (%.3f, %.3f)" % [handle["out_position"].x, handle["out_position"].y] if svs.shape_type == ScalableVectorShape2D.ShapeType.RECT: hint_txt += _draw_rect_control_point_handle(viewport_control, svs, handle, 'in', cp_in_is_hovered) if handle['out'].length(): hint_txt += _draw_rect_control_point_handle(viewport_control, svs, handle, 'out', cp_out_is_hovered) elif svs.shape_type == ScalableVectorShape2D.ShapeType.PATH: if not svs.is_arc_start(i - 1): hint_txt += _draw_control_point_handle(viewport_control, svs, handle, 'in', is_hovered or cp_in_is_hovered, cp_in_is_hovered) if not svs.is_arc_start(i): hint_txt +=_draw_control_point_handle(viewport_control, svs, handle, 'out', is_hovered or cp_out_is_hovered, cp_out_is_hovered) if handle['mirrored']: # mirrored handles var rect := Rect2(_vp_transform(handle['point_position']) - Vector2(5, 5), Vector2(10, 10)) viewport_control.draw_rect(rect, Color.DIM_GRAY, .5) viewport_control.draw_rect(rect, color, false, width) else: # unmirrored handles / zero length handles var p1 := _vp_transform(handle['point_position']) var pts := PackedVector2Array([ Vector2(p1.x - 8, p1.y), Vector2(p1.x, p1.y - 8), Vector2(p1.x + 8, p1.y), Vector2(p1.x, p1.y + 8) ]) viewport_control.draw_polygon(pts, [Color.DIM_GRAY]) pts.append(Vector2(p1.x - 8, p1.y)) viewport_control.draw_polyline(pts, color, width) if is_hovered: if svs.shape_type == ScalableVectorShape2D.ShapeType.PATH: point_txt = str(i) + handle['is_closed'] point_hint_pos = handle['point_position'] if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): if Input.is_key_pressed(KEY_SHIFT) and svs.shape_type == ScalableVectorShape2D.ShapeType.PATH: hint_txt += " - Release mouse to set curve handles" else: if svs.shape_type == ScalableVectorShape2D.ShapeType.RECT: hint_txt += " - Drag to resize rectange" elif svs.shape_type == ScalableVectorShape2D.ShapeType.ELLIPSE: hint_txt += " - Drag to resize ellipse" else: hint_txt += " - Drag to move" if handle['is_closed'].length() > 0: hint_txt += "\n - Double click to break loop" elif svs.shape_type == ScalableVectorShape2D.ShapeType.PATH: hint_txt += "\n - Right click to delete" if not svs.is_curve_closed() and ( (i == 0 and handles.size() > 2) or (i == handles.size() - 1 and i > 1) ): hint_txt += "\n - Double click to close loop" if svs.shape_type == ScalableVectorShape2D.ShapeType.PATH: hint_txt += "\n - Hold Shift + Drag to create curve handles" if Input.is_key_pressed(KEY_ALT): hint_txt += "\n - Click to set exact global position (Alt held)" else: hint_txt += "\n - Alt + Click to set exact global position" var gradient_handles := svs.get_gradient_handles() if not gradient_handles.is_empty(): var p1 := _vp_transform(gradient_handles['fill_from_pos']) var p2 := _vp_transform(gradient_handles['fill_to_pos']) var hint_color := svs.shape_hint_color if svs.shape_hint_color else Color.LIME_GREEN if svs.has_meta(META_NAME_HOVER_GRADIENT_FROM): hint_txt = "- Drag to move gradient start position" viewport_control.draw_circle(p1, 16, hint_color) viewport_control.draw_circle(p1, 12, Color.WHITE, false, 0.5, true) if svs.has_meta(META_NAME_HOVER_GRADIENT_TO): hint_txt = "- Drag to move gradient end position" viewport_control.draw_circle(p2, 16, hint_color) viewport_control.draw_circle(p2, 12, Color.WHITE, false, 0.5, true) for p : Vector2 in gradient_handles['stop_positions']: viewport_control.draw_circle(_vp_transform(p) + Vector2(1,1), 5, Color(0.0,0.0,0.0, 0.4), true, -1, true) viewport_control.draw_line(p1, p2, hint_color, .5, true) for idx in range(gradient_handles['stop_positions'].size()): var p := _vp_transform(gradient_handles['stop_positions'][idx]) var color := (Color.WHITE if svs.get_meta(META_NAME_HOVER_GRADIENT_COLOR_STOP_IDX, -1) == idx else Color.WEB_GRAY) viewport_control.draw_circle(p, 5, gradient_handles["stop_colors"][idx]) viewport_control.draw_circle(p, 5, color, false, 0.5, true) var p1_color := Color.WHITE if svs.has_meta(META_NAME_HOVER_GRADIENT_FROM) else hint_color var p2_color := Color.WHITE if svs.has_meta(META_NAME_HOVER_GRADIENT_TO) else hint_color _draw_crosshair(viewport_control, p1 , 8, 8, p1_color, 1) _draw_crosshair(viewport_control, p2 , 8, 8, p2_color, 1) if svs.has_meta(META_NAME_HOVER_GRADIENT_COLOR_STOP_IDX): hint_txt = "- Drag to move color stop\n- Right click to remove color stop" if (svs.has_meta(META_NAME_HOVER_CLOSEST_POINT_ON_GRADIENT_LINE) and not _is_ctrl_or_cmd_pressed() and not Input.is_key_pressed(KEY_SHIFT)): _draw_crosshair(viewport_control, svs.get_meta(META_NAME_HOVER_CLOSEST_POINT_ON_GRADIENT_LINE)) hint_txt = "- Double click to add color stop here" if not point_txt.is_empty(): _draw_point_number(viewport_control, point_hint_pos, point_txt) if not _are_hints_enabled() and _am_showing_point_numbers(): _draw_hint(viewport_control, point_pos_txt, true) elif _are_hints_enabled() and _am_showing_point_numbers() and point_pos_txt.length(): hint_txt += "\n\n - " + point_pos_txt if not hint_txt.is_empty(): _draw_hint(viewport_control, hint_txt) func _set_handle_hover(g_mouse_pos : Vector2, svs : ScalableVectorShape2D) -> void: var mouse_pos := _vp_transform(g_mouse_pos) var handles = svs.get_curve_handles() var gradient_handles = svs.get_gradient_handles() svs.remove_meta(META_NAME_HOVER_POINT_IDX) svs.remove_meta(META_NAME_HOVER_CP_IN_IDX) svs.remove_meta(META_NAME_HOVER_CP_OUT_IDX) svs.remove_meta(META_NAME_HOVER_GRADIENT_FROM) svs.remove_meta(META_NAME_HOVER_GRADIENT_TO) svs.remove_meta(META_NAME_HOVER_GRADIENT_COLOR_STOP_IDX) svs.remove_meta(META_NAME_HOVER_CLOSEST_POINT_ON_GRADIENT_LINE) for i in range(handles.size()): var handle = handles[i] if mouse_pos.distance_to(_vp_transform(handle['point_position'])) < 10: svs.set_meta(META_NAME_HOVER_POINT_IDX, i) elif mouse_pos.distance_to(_vp_transform(handle['in_position'])) < 10: svs.set_meta(META_NAME_HOVER_CP_IN_IDX, i) elif mouse_pos.distance_to(_vp_transform(handle['out_position'])) < 10: svs.set_meta(META_NAME_HOVER_CP_OUT_IDX, i) if not gradient_handles.is_empty() and not _handle_has_hover(svs): var stop_idx = gradient_handles['stop_positions'].find_custom(func(p): return mouse_pos.distance_to(_vp_transform(p)) < 6) if stop_idx > -1: svs.set_meta(META_NAME_HOVER_GRADIENT_COLOR_STOP_IDX, stop_idx) elif mouse_pos.distance_to(_vp_transform(gradient_handles['fill_from_pos'])) < 20: svs.set_meta(META_NAME_HOVER_GRADIENT_FROM, true) elif mouse_pos.distance_to(_vp_transform(gradient_handles['fill_to_pos'])) < 20: svs.set_meta(META_NAME_HOVER_GRADIENT_TO, true) else: var p := Geometry2D.get_closest_point_to_segment(mouse_pos, _vp_transform(gradient_handles['fill_from_pos']), _vp_transform(gradient_handles['fill_to_pos'])) if mouse_pos.distance_to(p) < 10: svs.set_meta(META_NAME_HOVER_CLOSEST_POINT_ON_GRADIENT_LINE, p) var closest_point_on_curve := svs.get_closest_point_on_curve(g_mouse_pos) if mouse_pos.distance_to(_vp_transform(closest_point_on_curve.point_position)) < 15: svs.set_meta(META_NAME_HOVER_CLOSEST_POINT, closest_point_on_curve) func _draw_curve(viewport_control : Control, svs : ScalableVectorShape2D, is_selected := true) -> void: var color := svs.shape_hint_color if svs.shape_hint_color else Color.LIME_GREEN if not is_selected: color = Color(0.5, 0.5, 0.5, 0.2) _draw_curve_def(viewport_control, svs, color, 1.0, is_selected) func _draw_curve_def(viewport_control : Control, svs : ScalableVectorShape2D, color : Color, width : float, antialiased : bool) -> void: if not svs.is_visible_in_tree(): return var points = svs.get_poly_points().map(_vp_transform) var last_p := Vector2.INF for p : Vector2 in points: if last_p != Vector2.INF: viewport_control.draw_line(last_p, p, color, width, antialiased) last_p = p if is_instance_valid(svs.line) and svs.line.closed and points.size() > 1: viewport_control.draw_dashed_line(last_p, points[0], color, width, 5.0, true, antialiased) func _draw_crosshair(viewport_control : Control, p : Vector2, orbit := 2.0, outer_orbit := 6.0, color := Color.WHITE, width := 1) -> void: if not _get_select_mode_button().button_pressed: return var line_len = outer_orbit + orbit viewport_control.draw_line(p - line_len * Vector2.UP, p - orbit * Vector2.UP, Color.WEB_GRAY, width + 1) viewport_control.draw_line(p - line_len * Vector2.RIGHT, p - orbit * Vector2.RIGHT, Color.WEB_GRAY, width + 1) viewport_control.draw_line(p - line_len * Vector2.DOWN, p - orbit * Vector2.DOWN, Color.WEB_GRAY, width + 1) viewport_control.draw_line(p - line_len * Vector2.LEFT, p - orbit * Vector2.LEFT, Color.WEB_GRAY, width + 1) viewport_control.draw_line(p - line_len * Vector2.UP, p - orbit * Vector2.UP, color, width) viewport_control.draw_line(p - line_len * Vector2.RIGHT, p - orbit * Vector2.RIGHT, color, width) viewport_control.draw_line(p - line_len * Vector2.DOWN, p - orbit * Vector2.DOWN, color, width) viewport_control.draw_line(p - line_len * Vector2.LEFT, p - orbit * Vector2.LEFT, color, width) func _draw_add_point_hint(viewport_control : Control, svs : ScalableVectorShape2D, only_cutout_hints : bool) -> void: var mouse_pos := EditorInterface.get_editor_viewport_2d().get_mouse_position() if _is_snapped_to_pixel(): mouse_pos = mouse_pos.snapped(Vector2.ONE * _get_snap_resolution()) var p := _vp_transform(mouse_pos) if _is_ctrl_or_cmd_pressed() and Input.is_key_pressed(KEY_SHIFT): if svs.has_fine_point(mouse_pos): _draw_crosshair(viewport_control, p) _draw_hint(viewport_control, "- Click to %s %s here (Ctrl+Shift held)\n" % [OPERATION_NAME_MAP[current_clip_operation]["verb"], SHAPE_NAME_MAP[current_cutout_shape]] + "- Use mousewheel to to change the shape of your %s (Ctrl+Shift held)\n" % OPERATION_NAME_MAP[current_clip_operation]["noun"] + "- Use right click to change the clipping operation (Ctrl+Shift held)" ) else: _draw_hint(viewport_control, "- Hover over the selected shape to %s %s (Ctrl+Shift held)\n" % [ OPERATION_NAME_MAP[current_clip_operation]["verb"], SHAPE_NAME_MAP[current_cutout_shape] ] + "- Use mousewheel to to change the shape of your %s (Ctrl+Shift held)\n" % OPERATION_NAME_MAP[current_clip_operation]["noun"] + "- Use right click to change the clipping operation (Ctrl+Shift held)" ) elif _is_ctrl_or_cmd_pressed() and not only_cutout_hints: _draw_crosshair(viewport_control, p) _draw_hint(viewport_control, "- Click to add point here (Ctrl held)") elif Input.is_key_pressed(KEY_SHIFT): _draw_hint(viewport_control, "- Use mousewheel to resize shape (Shift held)") elif not svs.has_meta(META_NAME_HOVER_CLOSEST_POINT_ON_GRADIENT_LINE): var hint := "- Hold Ctrl to add points to selected shape (or Cmd for mac) - Hold Shift to resize shape with mousewheel" if only_cutout_hints: hint = "- Hold Shift to resize shape with mousewheel" if svs.has_fine_point(mouse_pos): hint += "\n- Double click to subdivide all curve segments - Hold Ctrl+Shift to %s %s here (or Cmd+Shift for mac)\n" % [ OPERATION_NAME_MAP[current_clip_operation]["verb"], SHAPE_NAME_MAP[current_cutout_shape] ] _draw_hint(viewport_control, hint) func _draw_closest_point_on_curve(viewport_control : Control, svs : ScalableVectorShape2D) -> void: if _is_ctrl_or_cmd_pressed() or Input.is_key_pressed(KEY_SHIFT): _draw_add_point_hint(viewport_control, svs, false) return if svs.has_meta(META_NAME_HOVER_CLOSEST_POINT): var hint := "" var md_p : ClosestPointOnCurveMeta = svs.get_meta(META_NAME_HOVER_CLOSEST_POINT) if svs.is_arc_start(md_p.before_segment - 1): var arc_start_idx := md_p.before_segment - 1 var arc := svs.arc_list.get_arc_for_point(arc_start_idx) var arc_points := Array(svs.tessellate_arc_segment( svs.curve.get_point_position(arc_start_idx), arc.radius, arc.rotation_deg, arc.large_arc_flag, arc.sweep_flag, svs.curve.get_point_position(arc_start_idx + 1), )).map(func(p): return _vp_transform(svs.to_global(p))) viewport_control.draw_polyline(arc_points, Color.RED, 3.0, true) hint = "- Left click to open arc settings" hint += "\n- Right click to remove arc (straighten this line segment)" else: if Input.is_key_pressed(KEY_ALT): _draw_crosshair(viewport_control, _vp_transform(svs.to_global(svs.get_curve_segment_halfway_point(md_p.before_segment))), 3.0, 8, Color.ANTIQUE_WHITE, 2) else: _draw_crosshair(viewport_control, _vp_transform(md_p.point_position)) if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): if svs.curve.point_count > 1: if Input.is_key_pressed(KEY_ALT): hint += "\n- Left Click to add point halfway the line (Alt held)" else: hint = "- Double click to add point on the line" hint += "\n- Alt + Click to add point halfway the line" if md_p.before_segment < svs.curve.point_count: hint += "\n- Drag to change curve" hint += "\n- Right click to convert line segment to arc" else: _draw_add_point_hint(viewport_control, svs, false) if not hint.is_empty(): _draw_hint(viewport_control, hint) func _draw_outline_for_uniform_transforms(viewport_control : Control, svs : ScalableVectorShape2D) -> void: viewport_control.draw_polyline(svs.get_bounding_box().map(_vp_transform), VIEWPORT_ORANGE, 2.0) _draw_curve_def(viewport_control, svs, svs.shape_hint_color, 0.5, true) for idx in svs.curve.point_count: var p := svs.to_global(svs.curve.get_point_position(idx)) _draw_crosshair(viewport_control, _vp_transform(p), 2.0, 4.0, Color.WHITE) var natural_center := svs.to_global(svs.get_center()) if (_uniform_transform_mode == UniformTransformMode.ROTATE and _lmb_is_down_inside_viewport and Input.is_key_pressed(KEY_SHIFT) ): natural_center = _stored_natural_center _draw_crosshair(viewport_control, _vp_transform(natural_center), 2.0, 4.0, Color.WHITE) func _draw_canvas_for_uniform_translate(viewport_control : Control, svs : ScalableVectorShape2D) -> void: _draw_outline_for_uniform_transforms(viewport_control, svs) if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): _draw_hint(viewport_control, "- Drag to move all points (left mouse button held)") else: _draw_hint(viewport_control, "- Hold left mouse button to start moving all points" + "\n- Press Q to return to normal editing") func _draw_canvas_for_uniform_rotate(viewport_control : Control, svs : ScalableVectorShape2D) -> void: _draw_outline_for_uniform_transforms(viewport_control, svs) if _lmb_is_down_inside_viewport: var hint_text := "- Drag to rotate all points (left mouse button held)" if _is_ctrl_or_cmd_pressed(): hint_text += "\n- Rotating in steps of 5° (Ctrl held)" else: hint_text += "\n- Hold Ctrl to rotate in steps of 5°" if svs.shape_type == ScalableVectorShape2D.ShapeType.PATH: if Input.is_key_pressed(KEY_SHIFT): hint_text += "\n- Rotating around the natural center in stead of the pivot (Shift held)" else: hint_text += "\n- Hold Shift to rotate around the natural center in stead of the pivot" _draw_hint(viewport_control, hint_text) else: _draw_hint(viewport_control, "- Hold left mouse button to start rotating all points" + "\n- Press Q to return to normal editing") if _lmb_is_down_inside_viewport: var rotation_origin = svs.global_position if (Input.is_key_pressed(KEY_SHIFT) or svs.shape_type != ScalableVectorShape2D.ShapeType.PATH ): rotation_origin = _stored_natural_center var rotation_target := EditorInterface.get_editor_viewport_2d().get_mouse_position() if _is_ctrl_or_cmd_pressed(): var ang := snapped(rotation_origin.angle_to_point(rotation_target), deg_to_rad(5.0)) var dst := rotation_origin.distance_to(rotation_target) rotation_target = rotation_origin + Vector2.RIGHT.rotated(ang) * dst viewport_control.draw_line(_vp_transform(rotation_origin),_vp_transform(rotation_target), svs.shape_hint_color, 1, true) func _draw_canvas_for_uniform_scale(viewport_control : Control, svs : ScalableVectorShape2D) -> void: _draw_outline_for_uniform_transforms(viewport_control, svs) if _lmb_is_down_inside_viewport: var hint_text := "- Drag to scale all points (left mouse button held)" if svs.shape_type == ScalableVectorShape2D.ShapeType.PATH: if Input.is_key_pressed(KEY_SHIFT): hint_text += "\n- Scaling out from natural center (Shift held)" else: hint_text += "\n- Hold Shift to scale out from natural center in stead of pivot" _draw_hint(viewport_control, hint_text) else: _draw_hint(viewport_control, "- Hold left mouse button to start scaling all points" + "\n- Press Q to return to normal editing") if _lmb_is_down_inside_viewport: var origin = svs.global_position if (Input.is_key_pressed(KEY_SHIFT) or svs.shape_type != ScalableVectorShape2D.ShapeType.PATH ): origin = svs.to_global(svs.get_center()) var target := EditorInterface.get_editor_viewport_2d().get_mouse_position() viewport_control.draw_line(_vp_transform(origin),_vp_transform(target), svs.shape_hint_color, 1, true) func _find_merge_vertices() -> Dictionary[ScalableVectorShape2D, int]: var vertex_map : Dictionary[ScalableVectorShape2D, int] = {} for svs : ScalableVectorShape2D in EditorInterface.get_edited_scene_root().find_children("*", "ScalableVectorShape2D"): var point_was_found_inside_rect := false for idx in svs.curve.point_count: var p := svs.to_global(svs.curve.get_point_position(idx)) var p_inside_rect := _merge_box_rect.abs().has_point(_vp_transform(p)) if p_inside_rect and not point_was_found_inside_rect: vertex_map[svs] = idx point_was_found_inside_rect = true return vertex_map func _handle_draw_vertex_merge_box(viewport_control: Control) -> void: if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): _draw_hint(viewport_control, "Drag box around a point of 2 or more ScalableVectorShape2D to merge them") viewport_control.draw_rect(_merge_box_rect, Color.LIME, false, 1) for svs : ScalableVectorShape2D in EditorInterface.get_edited_scene_root().find_children("*", "ScalableVectorShape2D"): for idx in svs.curve.point_count: _draw_crosshair( viewport_control, _vp_transform(svs.to_global(svs.curve.get_point_position(idx))), 2.0, 4.0, VIEWPORT_ORANGE, 1 ) var vertex_map := _find_merge_vertices() for svs : ScalableVectorShape2D in vertex_map.keys(): _draw_crosshair(viewport_control, _vp_transform( svs.to_global(svs.curve.get_point_position(vertex_map[svs])) ), 2.0, 5.0, Color.WHITE, 2) if vertex_map.size() > 1: var entries = "" for k in vertex_map.keys(): entries += "\n - " + k.name _draw_hint(viewport_control, "\nMerge points of:%s" % entries) func _forward_canvas_draw_over_viewport(viewport_control: Control) -> void: if not _is_editing_enabled(): return if not is_instance_valid(EditorInterface.get_edited_scene_root()): return if merge_node_toggle_button.button_pressed: return _handle_draw_vertex_merge_box(viewport_control) var current_selection := EditorInterface.get_selection().get_selected_nodes().pop_back() if _is_svs_valid(current_selection) and _get_select_mode_button().button_pressed: if _uniform_transform_mode == UniformTransformMode.TRANSLATE: return _draw_canvas_for_uniform_translate(viewport_control, current_selection) elif _uniform_transform_mode == UniformTransformMode.SCALE: return _draw_canvas_for_uniform_scale(viewport_control, current_selection) elif _uniform_transform_mode == UniformTransformMode.ROTATE: return _draw_canvas_for_uniform_rotate(viewport_control, current_selection) var all_valid_svs_nodes := _find_scalable_vector_shape_2d_nodes().filter(_is_svs_valid) for result : ScalableVectorShape2D in all_valid_svs_nodes: if result == current_selection: viewport_control.draw_polyline(result.get_bounding_box().map(_vp_transform), VIEWPORT_ORANGE, 2.0) _draw_curve(viewport_control, result) _draw_handles(viewport_control, result) if not _handle_has_hover(result): if result.shape_type == ScalableVectorShape2D.ShapeType.PATH: if result.has_meta(META_NAME_HOVER_CLOSEST_POINT): _draw_closest_point_on_curve(viewport_control, result) else: _draw_add_point_hint(viewport_control, result, false) else: _draw_add_point_hint(viewport_control, result, true) elif result.has_meta(META_NAME_SELECT_HINT): viewport_control.draw_polyline(result.get_bounding_box().map(_vp_transform), Color.WEB_GRAY, 1.0) if not(result.line or result.collision_polygon or result.polygon): _draw_curve(viewport_control, result, false) if shape_preview: var points := Array(shape_preview.tessellate()) var stroke_width = (_get_default_stroke_width() * EditorInterface.get_editor_viewport_2d() .get_final_transform().get_scale().x) if current_selection is Node2D: points = points.map(current_selection.to_global) stroke_width *= current_selection.global_scale.x elif current_selection is Control: points = points.map(func(p): return current_selection.get_global_transform() * p) stroke_width *= current_selection.get_global_transform().get_scale().x points = points.map(_vp_transform) match _get_default_paint_order(): PaintOrder.MARKERS_STROKE_FILL, PaintOrder.STROKE_FILL_MARKERS, PaintOrder.STROKE_MARKERS_FILL: if _is_add_stroke_enabled(): viewport_control.draw_polyline(points, _get_default_stroke_color(), stroke_width) if _is_add_fill_enabled(): viewport_control.draw_polygon(points, [_get_default_fill_color()]) PaintOrder.MARKERS_FILL_STROKE, PaintOrder.FILL_STROKE_MARKERS, PaintOrder.FILL_MARKERS_STROKE, _: if _is_add_fill_enabled(): viewport_control.draw_polygon(points, [_get_default_fill_color()]) if _is_add_stroke_enabled(): viewport_control.draw_polyline(points, _get_default_stroke_color(), stroke_width) if not _is_add_fill_enabled() and not _is_add_stroke_enabled(): viewport_control.draw_polyline(points, Color.LIME, 1) func _start_undo_redo_transaction(name := "") -> void: in_undo_redo_transaction = true undo_redo_transaction = { UndoRedoEntry.NAME: name, UndoRedoEntry.DOS: [], UndoRedoEntry.UNDOS: [], UndoRedoEntry.DO_PROPS: [], UndoRedoEntry.UNDO_PROPS : [] } func _commit_undo_redo_transaction() -> void: if not in_undo_redo_transaction: return in_undo_redo_transaction = false undo_redo.create_action(undo_redo_transaction[UndoRedoEntry.NAME]) for do_method in undo_redo_transaction[UndoRedoEntry.DOS]: undo_redo.callv('add_do_method', do_method) for do_prop in undo_redo_transaction[UndoRedoEntry.DO_PROPS]: undo_redo.callv('add_do_property', do_prop) for undo_method in undo_redo_transaction[UndoRedoEntry.UNDOS]: undo_redo.callv('add_undo_method', undo_method) for undo_prop in undo_redo_transaction[UndoRedoEntry.UNDO_PROPS]: undo_redo.callv('add_undo_property', undo_prop) undo_redo.commit_action(false) undo_redo_transaction = { UndoRedoEntry.NAME: name, UndoRedoEntry.DOS: [], UndoRedoEntry.UNDOS: [], UndoRedoEntry.UNDO_PROPS: [], UndoRedoEntry.DO_PROPS: [] } func _on_global_position_for_handle_changed(global_pos : Vector2, meta_name: String, idx : int) -> void: var cur := EditorInterface.get_selection().get_selected_nodes().pop_back() if _is_svs_valid(cur): match(meta_name): META_NAME_HOVER_CP_IN_IDX: _update_curve_cp_in_position(cur, global_pos, idx) META_NAME_HOVER_CP_OUT_IDX: _update_curve_cp_out_position(cur, global_pos, idx) META_NAME_HOVER_POINT_IDX: _update_curve_point_position(cur, global_pos, idx) update_overlays() func _update_curve_point_position(current_selection : ScalableVectorShape2D, mouse_pos : Vector2, idx : int) -> void: if not in_undo_redo_transaction: _start_undo_redo_transaction("Move point on " + str(current_selection)) if idx == 0 and current_selection.is_curve_closed(): var idx_1 = current_selection.curve.point_count - 1 undo_redo_transaction[UndoRedoEntry.UNDOS].append([ current_selection.curve, 'set_point_position', idx_1, current_selection.curve.get_point_position(idx_1) ]) undo_redo_transaction[UndoRedoEntry.UNDOS].append([ current_selection.curve, 'set_point_position', idx, current_selection.curve.get_point_position(idx) ]) undo_redo_transaction[UndoRedoEntry.DOS] = [] if idx == 0 and current_selection.is_curve_closed(): var idx_1 = current_selection.curve.point_count - 1 undo_redo_transaction[UndoRedoEntry.DOS].append([ current_selection, 'set_global_curve_point_position', mouse_pos, idx_1, _is_snapped_to_pixel(), _get_snap_resolution() ]) current_selection.set_global_curve_point_position(mouse_pos, idx_1, _is_snapped_to_pixel(), _get_snap_resolution()) undo_redo_transaction[UndoRedoEntry.DOS].append([ current_selection, 'set_global_curve_point_position', mouse_pos, idx, _is_snapped_to_pixel(), _get_snap_resolution() ]) current_selection.set_global_curve_point_position(mouse_pos, idx, _is_snapped_to_pixel(), _get_snap_resolution()) func _update_rect_dimensions(svs : ScalableVectorShape2D, mouse_pos : Vector2) -> void: if not in_undo_redo_transaction: _start_undo_redo_transaction("Change rect size on " + str(svs)) undo_redo_transaction[UndoRedoEntry.UNDO_PROPS] = [[svs, 'size', svs.size]] if _is_snapped_to_pixel(): mouse_pos = mouse_pos.snapped(Vector2.ONE * _get_snap_resolution()) var top_left := (-svs.size * 0.5).rotated(svs.spin) + svs.offset svs.size = ((svs.to_local(mouse_pos)) - top_left).rotated(-svs.spin) undo_redo_transaction[UndoRedoEntry.DO_PROPS] = [[svs, 'size', svs.size]] func _update_rect_corner_radius(svs : ScalableVectorShape2D, mouse_pos : Vector2, prop_name : String, is_symmetrical : bool) -> void: if not in_undo_redo_transaction: _start_undo_redo_transaction("Change rect " + prop_name + " on " + str(svs)) undo_redo_transaction[UndoRedoEntry.UNDO_PROPS] = [ [svs, 'rx', svs.rx], [svs, 'ry', svs.ry] ] if _is_snapped_to_pixel(): mouse_pos = mouse_pos.snapped(Vector2.ONE * _get_snap_resolution()) var top_left := (-svs.size * 0.5).rotated(svs.spin) + svs.offset if prop_name == 'rx': svs.rx = svs.to_local(mouse_pos).rotated(-svs.spin).x - top_left.rotated(-svs.spin).x if is_symmetrical: svs.ry = svs.rx if prop_name == 'ry': svs.ry = svs.to_local(mouse_pos).rotated(-svs.spin).y - top_left.rotated(-svs.spin).y if is_symmetrical: svs.rx = svs.ry undo_redo_transaction[UndoRedoEntry.DO_PROPS] = [ [svs, 'rx', svs.rx], [svs, 'ry', svs.ry] ] func _update_curve_cp_in_position(current_selection : ScalableVectorShape2D, mouse_pos : Vector2, idx : int) -> void: if idx == 0: idx = current_selection.curve.point_count - 1 var cp_in_is_cp_out_of_loop_start := (Input.is_key_pressed(KEY_SHIFT) and not(idx == current_selection.curve.point_count - 1 and not current_selection.is_curve_closed()) ) if not in_undo_redo_transaction: _start_undo_redo_transaction("Move control point in %d on %s" % [idx, current_selection]) undo_redo_transaction[UndoRedoEntry.UNDOS].append([current_selection.curve, 'set_point_in', idx, current_selection.curve.get_point_in(idx)]) if cp_in_is_cp_out_of_loop_start: var idx_1 = 0 if idx == current_selection.curve.point_count - 1 else idx undo_redo_transaction[UndoRedoEntry.UNDOS].append([current_selection.curve, 'set_point_out', idx_1, current_selection.curve.get_point_out(idx_1)]) current_selection.set_global_curve_cp_in_position(mouse_pos, idx, _is_snapped_to_pixel(), _get_snap_resolution()) undo_redo_transaction[UndoRedoEntry.DOS] = [[ current_selection, 'set_global_curve_cp_in_position', mouse_pos, idx, _is_snapped_to_pixel(), _get_snap_resolution() ]] if cp_in_is_cp_out_of_loop_start: var idx_1 = 0 if idx == current_selection.curve.point_count - 1 else idx current_selection.curve.set_point_out(idx_1, -current_selection.curve.get_point_in(idx)) undo_redo_transaction[UndoRedoEntry.DOS].append([current_selection.curve, 'set_point_out', idx_1, -current_selection.curve.get_point_in(idx)]) func _update_gradient_from_position(svs : ScalableVectorShape2D, mouse_pos : Vector2) -> void: if _is_snapped_to_pixel(): mouse_pos = mouse_pos.snapped(Vector2.ONE * _get_snap_resolution()) if not in_undo_redo_transaction: _start_undo_redo_transaction("Move gradient from position for %s" % str(svs)) undo_redo_transaction[UndoRedoEntry.UNDO_PROPS].append([svs.polygon.texture, 'fill_from', svs.polygon.texture.fill_from]) var box := svs.get_bounding_rect() svs.polygon.texture.fill_from = (svs.to_local(mouse_pos) - box.position) / box.size undo_redo_transaction[UndoRedoEntry.DO_PROPS] = [[ svs.polygon.texture, 'fill_from', svs.polygon.texture.fill_from ]] func _update_gradient_to_position(svs : ScalableVectorShape2D, mouse_pos : Vector2) -> void: if _is_snapped_to_pixel(): mouse_pos = mouse_pos.snapped(Vector2.ONE * _get_snap_resolution()) if not in_undo_redo_transaction: _start_undo_redo_transaction("Move gradient from position for %s" % str(svs)) undo_redo_transaction[UndoRedoEntry.UNDO_PROPS].append([svs.polygon.texture, 'fill_to', svs.polygon.texture.fill_to]) var box := svs.get_bounding_rect() svs.polygon.texture.fill_to = (svs.to_local(mouse_pos) - box.position) / box.size undo_redo_transaction[UndoRedoEntry.DO_PROPS] = [[ svs.polygon.texture, 'fill_to', svs.polygon.texture.fill_to ]] func _get_gradient_offset(svs : ScalableVectorShape2D, mouse_pos : Vector2) -> float: var box := svs.get_bounding_rect() var gradient_tex : GradientTexture2D = svs.polygon.texture var p := ((svs.to_local(mouse_pos) - box.position) / box.size) var p1 := Geometry2D.get_closest_point_to_segment(p, gradient_tex.fill_from, gradient_tex.fill_to) return p1.distance_to(gradient_tex.fill_from) / gradient_tex.fill_from.distance_to(gradient_tex.fill_to) func _update_gradient_stop_color_pos(svs : ScalableVectorShape2D, mouse_pos : Vector2, idx : int) -> void: if _is_snapped_to_pixel(): mouse_pos = mouse_pos.snapped(Vector2.ONE * _get_snap_resolution()) var new_offset := _get_gradient_offset(svs, mouse_pos) if not in_undo_redo_transaction: _start_undo_redo_transaction("Move gradient offset %d on %s" % [idx, svs]) undo_redo_transaction[UndoRedoEntry.UNDOS].append([svs.polygon.texture.gradient, 'set_offset', idx, svs.polygon.texture.gradient.offsets[idx]]) undo_redo_transaction[UndoRedoEntry.DOS] = [[ svs.polygon.texture.gradient, 'set_offset', idx, new_offset ]] svs.polygon.texture.gradient.set_offset(idx, new_offset) func _add_color_stop(svs : ScalableVectorShape2D, mouse_pos : Vector2) -> void: var new_offset := _get_gradient_offset(svs, mouse_pos) var colors = Array(svs.polygon.texture.gradient.colors) var offsets = Array(svs.polygon.texture.gradient.offsets) var stops = {} for idx in range(colors.size()): stops[offsets[idx]] = colors[idx] stops[new_offset] = svs.polygon.texture.gradient.sample(new_offset) var stop_keys := stops.keys() stop_keys.sort() var new_colors = [] var new_offsets = [] for offset in stop_keys: new_colors.append(stops[offset]) new_offsets.append(offset) undo_redo.create_action("Add color stop to %s " % str(svs)) undo_redo.add_do_property(svs.polygon.texture.gradient, 'colors', new_colors) undo_redo.add_do_property(svs.polygon.texture.gradient, 'offsets', new_offsets) undo_redo.add_do_method(svs, 'notify_assigned_node_change') undo_redo.add_undo_property(svs.polygon.texture.gradient, 'colors', colors) undo_redo.add_undo_property(svs.polygon.texture.gradient, 'offsets', offsets) undo_redo.add_undo_method(svs, 'notify_assigned_node_change') undo_redo.commit_action() func _remove_color_stop(svs : ScalableVectorShape2D, remove_idx : int) -> void: var colors = Array(svs.polygon.texture.gradient.colors) var offsets = Array(svs.polygon.texture.gradient.offsets) var stops = {} for idx in range(colors.size()): if idx != remove_idx: stops[offsets[idx]] = colors[idx] var stop_keys := stops.keys() stop_keys.sort() var new_colors = [] var new_offsets = [] for offset in stop_keys: new_colors.append(stops[offset]) new_offsets.append(offset) undo_redo.create_action("Remove color stop from %s " % str(svs)) undo_redo.add_do_property(svs.polygon.texture.gradient, 'colors', new_colors) undo_redo.add_do_property(svs.polygon.texture.gradient, 'offsets', new_offsets) undo_redo.add_do_method(svs, 'notify_assigned_node_change') undo_redo.add_undo_property(svs.polygon.texture.gradient, 'colors', colors) undo_redo.add_undo_property(svs.polygon.texture.gradient, 'offsets', offsets) undo_redo.add_undo_method(svs, 'notify_assigned_node_change') undo_redo.commit_action() func _update_curve_cp_out_position(current_selection : ScalableVectorShape2D, mouse_pos : Vector2, idx : int) -> void: if idx == current_selection.curve.point_count - 1: idx = 0 var cp_out_is_cp_in_of_loop_end := (Input.is_key_pressed(KEY_SHIFT) and not(idx == 0 and not current_selection.is_curve_closed())) if not in_undo_redo_transaction: _start_undo_redo_transaction("Move control point out %d on %s" % [idx, current_selection]) undo_redo_transaction[UndoRedoEntry.UNDOS].append([current_selection.curve, 'set_point_out', idx, current_selection.curve.get_point_out(idx)]) if cp_out_is_cp_in_of_loop_end: var idx_1 = current_selection.curve.point_count - 1 if idx == 0 else idx undo_redo_transaction[UndoRedoEntry.UNDOS].append([current_selection.curve, 'set_point_in', idx_1, current_selection.curve.get_point_in(idx_1)]) current_selection.set_global_curve_cp_out_position(mouse_pos, idx, _is_snapped_to_pixel(), _get_snap_resolution()) undo_redo_transaction[UndoRedoEntry.DOS] = [[ current_selection, 'set_global_curve_cp_out_position', mouse_pos, idx, _is_snapped_to_pixel(), _get_snap_resolution() ]] if cp_out_is_cp_in_of_loop_end: var idx_1 = current_selection.curve.point_count - 1 if idx == 0 else idx current_selection.curve.set_point_in(idx_1, -current_selection.curve.get_point_out(idx)) undo_redo_transaction[UndoRedoEntry.DOS].append([current_selection.curve, 'set_point_in', idx_1, -current_selection.curve.get_point_out(idx)]) func _set_shape_origin(current_selection : ScalableVectorShape2D, mouse_pos : Vector2) -> void: undo_redo.create_action("Set origin on %s" % current_selection) undo_redo.add_do_method(current_selection, 'set_origin', mouse_pos) undo_redo.add_undo_method(current_selection, 'set_origin', current_selection.global_position) undo_redo.commit_action() func _get_curve_backup(curve_in : Curve2D) -> Curve2D: return curve_in.duplicate() func _resize_shape(svs : ScalableVectorShape2D, s : float) -> void: if svs.shape_type == ScalableVectorShape2D.ShapeType.PATH: if not in_undo_redo_transaction: _start_undo_redo_transaction("Resize shape %s" % str(svs)) undo_redo_transaction[UndoRedoEntry.UNDOS].append([ svs, 'replace_curve_points', _get_curve_backup(svs.curve)]) undo_redo_transaction[UndoRedoEntry.DOS] = [] for idx in range(svs.curve.point_count): svs.curve.set_point_position(idx, svs.curve.get_point_position(idx) * s) svs.curve.set_point_in(idx, svs.curve.get_point_in(idx) * s) svs.curve.set_point_out(idx, svs.curve.get_point_out(idx) * s) undo_redo_transaction[UndoRedoEntry.DOS].append([svs.curve, 'set_point_position', idx, svs.curve.get_point_position(idx) * s]) undo_redo_transaction[UndoRedoEntry.DOS].append([svs.curve, 'set_point_in', idx, svs.curve.get_point_in(idx) * s]) undo_redo_transaction[UndoRedoEntry.DOS].append([svs.curve, 'set_point_out', idx, svs.curve.get_point_out(idx) * s]) else: if not in_undo_redo_transaction: _start_undo_redo_transaction("Resize shape %s" % str(svs)) undo_redo_transaction[UndoRedoEntry.UNDO_PROPS] = [[svs, 'size', svs.size]] undo_redo_transaction[UndoRedoEntry.DO_PROPS] = [[svs, 'size', svs.size * s]] svs.size *= s func _remove_point_from_curve(current_selection : ScalableVectorShape2D, idx : int) -> void: var orig_n := current_selection.curve.point_count if current_selection.is_curve_closed() and idx == 0: idx = orig_n - 1 var backup := _get_curve_backup(current_selection.curve) undo_redo.create_action("Remove point %d from %s" % [idx, str(current_selection)]) undo_redo.add_do_method(current_selection.curve, 'set_point_in', 0, Vector2.ZERO) if orig_n > 2: undo_redo.add_do_method(current_selection.curve, 'set_point_out', orig_n - 2, Vector2.ZERO) var redo_arcs : Array[ScalableArc] = [] for a : ScalableArc in current_selection.arc_list.arcs: if a.start_point == idx or a.start_point == idx - 1: undo_redo.add_do_method(current_selection.arc_list, 'remove_arc_for_point', a.start_point) redo_arcs.append(a) undo_redo.add_do_method(current_selection.curve, 'remove_point', idx) undo_redo.add_do_method(current_selection.arc_list, 'handle_point_removed_at_index', idx) undo_redo.add_undo_method(current_selection, 'replace_curve_points', backup) undo_redo.add_undo_method(current_selection.arc_list, 'handle_point_added_at_index', idx) for a in redo_arcs: undo_redo.add_undo_reference(a) undo_redo.add_undo_method(current_selection.arc_list, 'add_arc', a) undo_redo.commit_action() func _remove_cp_in_from_curve(current_selection : ScalableVectorShape2D, idx : int) -> void: if idx == 0: idx = current_selection.curve.point_count - 1 undo_redo.create_action("Remove control point in %d from %s " % [idx, str(current_selection)]) undo_redo.add_do_method(current_selection.curve, 'set_point_in', idx, Vector2.ZERO) undo_redo.add_undo_method(current_selection.curve, 'set_point_in', idx, current_selection.curve.get_point_in(idx)) undo_redo.commit_action() func _remove_cp_out_from_curve(current_selection : ScalableVectorShape2D, idx : int) -> void: if idx == current_selection.curve.point_count - 1: idx = 0 undo_redo.create_action("Remove control point out %d from %s " % [idx, str(current_selection)]) undo_redo.add_do_method(current_selection.curve, 'set_point_out', idx, Vector2.ZERO) undo_redo.add_undo_method(current_selection.curve, 'set_point_out', idx, current_selection.curve.get_point_out(idx)) undo_redo.commit_action() func _remove_rounded_corners_from_rect(svs : ScalableVectorShape2D): undo_redo.create_action("Remove rounded corners from %s " % str(svs)) undo_redo.add_do_property(svs, 'rx', 0.0) undo_redo.add_do_property(svs, 'ry', 0.0) undo_redo.add_undo_property(svs, 'rx', svs.rx) undo_redo.add_undo_property(svs, 'ry', svs.ry) undo_redo.commit_action() func _add_point_to_curve(svs : ScalableVectorShape2D, local_pos : Vector2, cp_in := Vector2.ZERO, cp_out := Vector2.ZERO, idx := -1, do_commit := true) -> void: undo_redo.create_action("Add point at %s to %s " % [str(local_pos), str(svs)]) undo_redo.add_do_method(svs.curve, 'add_point', local_pos, cp_in, cp_out, idx) if idx < 0: undo_redo.add_undo_method(svs.curve, 'remove_point', svs.curve.point_count) else: undo_redo.add_do_method(svs.arc_list, 'handle_point_added_at_index', idx) undo_redo.add_undo_method(svs.curve, 'remove_point', idx) undo_redo.add_undo_method(svs.arc_list, 'handle_point_removed_at_index', idx) if not do_commit: return undo_redo.commit_action() func _create_arc(svs : ScalableVectorShape2D, start_point_idx : int) -> void: undo_redo.create_action("Remove arc for segment %d on %s " % [start_point_idx, str(svs)]) undo_redo.add_do_method(svs, 'add_arc', start_point_idx) undo_redo.add_undo_method(svs.arc_list, 'remove_arc_for_point', start_point_idx) undo_redo.commit_action() func _remove_arc(svs : ScalableVectorShape2D, start_point_idx : int) -> void: var redo_arc := svs.arc_list.get_arc_for_point(start_point_idx) if redo_arc == null: return undo_redo.create_action("Remove arc for segment %d on %s " % [start_point_idx, str(svs)]) undo_redo.add_do_method(svs.arc_list, 'remove_arc_for_point', start_point_idx) undo_redo.add_undo_method(svs.arc_list, 'add_arc', redo_arc) undo_redo.add_undo_reference(redo_arc) undo_redo.commit_action() func _add_point_on_position(svs : ScalableVectorShape2D, pos : Vector2) -> void: if svs.shape_type != ScalableVectorShape2D.ShapeType.PATH: return _add_point_to_curve(svs, svs.to_local(pos)) func _start_cutout_shape(svs : ScalableVectorShape2D, pos : Vector2) -> void: var new_shape = ScalableVectorShape2D.new() var mouse_pos := EditorInterface.get_editor_viewport_2d().get_mouse_position() if _is_snapped_to_pixel(): mouse_pos = mouse_pos.snapped(Vector2.ONE * _get_snap_resolution()) new_shape.curve = Curve2D.new() new_shape.position = svs.to_local(mouse_pos) new_shape.shape_type = current_cutout_shape new_shape.curve.add_point(Vector2.ZERO) _create_shape(new_shape, EditorInterface.get_edited_scene_root(), "CutoutOf%s" % svs.name, svs) func _add_point_on_curve_segment(svs : ScalableVectorShape2D, subdivide := false) -> void: if svs.shape_type != ScalableVectorShape2D.ShapeType.PATH: return if not svs.has_meta(META_NAME_HOVER_CLOSEST_POINT): return var md_closest_point : ClosestPointOnCurveMeta = svs.get_meta(META_NAME_HOVER_CLOSEST_POINT) if svs.is_arc_start(md_closest_point.before_segment - 1): return var placement_point := svs.get_curve_segment_halfway_point(md_closest_point.before_segment) if subdivide else md_closest_point.local_point_position if md_closest_point.before_segment >= svs.curve.point_count: _add_point_to_curve(svs, placement_point) else: if ( svs.curve.get_point_out(md_closest_point.before_segment - 1).length() > 0.0 or svs.curve.get_point_in(md_closest_point.before_segment).length() > 0.0 ): # This is a curved segment, so when a point is added, control points are recalculated var sliced_segment := svs.get_sliced_curve_segment(md_closest_point.before_segment, placement_point) _add_point_to_curve(svs, placement_point, Vector2.ZERO, Vector2.ZERO, md_closest_point.before_segment, false) undo_redo.add_do_method(svs.curve, "set_point_out", md_closest_point.before_segment - 1, sliced_segment.get_point_out(0)) undo_redo.add_undo_method(svs.curve, "set_point_out", md_closest_point.before_segment -1, svs.curve.get_point_out(md_closest_point.before_segment - 1)) undo_redo.add_do_method(svs.curve, "set_point_in", md_closest_point.before_segment, sliced_segment.get_point_in(1)) undo_redo.add_undo_method(svs.curve, "set_point_in", md_closest_point.before_segment, svs.curve.get_point_in(md_closest_point.before_segment)) undo_redo.add_do_method(svs.curve, "set_point_out", md_closest_point.before_segment, sliced_segment.get_point_out(1)) undo_redo.add_undo_method(svs.curve, "set_point_out", md_closest_point.before_segment, svs.curve.get_point_out(md_closest_point.before_segment)) undo_redo.add_do_method(svs.curve, "set_point_in", md_closest_point.before_segment + 1, sliced_segment.get_point_in(2)) undo_redo.add_undo_reference(svs.curve) undo_redo.commit_action() else: _add_point_to_curve(svs, placement_point, Vector2.ZERO, Vector2.ZERO, md_closest_point.before_segment) func _subdivide_curve(svs : ScalableVectorShape2D) -> void: undo_redo.create_action("Subdivide shape %s" % str(svs)) undo_redo.add_do_property(svs, 'curve', svs.get_subdivided_curve()) undo_redo.add_undo_property(svs, 'curve', svs.curve.duplicate()) undo_redo.commit_action() func _drag_curve_segment(svs : ScalableVectorShape2D, mouse_pos : Vector2) -> void: if svs.shape_type != ScalableVectorShape2D.ShapeType.PATH: return if not svs.has_meta(META_NAME_HOVER_CLOSEST_POINT): return var md_closest_point : ClosestPointOnCurveMeta = svs.get_meta(META_NAME_HOVER_CLOSEST_POINT) if svs.is_arc_start(md_closest_point.before_segment - 1) or md_closest_point.before_segment >= svs.curve.point_count or md_closest_point.before_segment < 1: return if _is_snapped_to_pixel(): mouse_pos = mouse_pos.snapped(Vector2.ONE * _get_snap_resolution()) # Compute control points based on mouse position to align middle of segment curve to it # using the quadratic Bézier control point var idx : int = md_closest_point.before_segment var segment_start_point := svs.curve.get_point_position(idx - 1) var segment_end_point := svs.curve.get_point_position(idx) var halfway_point := (segment_start_point + segment_end_point) / 2 var dir := halfway_point.direction_to(svs.to_local(mouse_pos)) var distance := halfway_point.distance_to(svs.to_local(mouse_pos)) var quadratic_bezier_control_point := halfway_point + distance * 2 * dir var new_point_out := (quadratic_bezier_control_point - segment_start_point) * (2.0 / 3.0) var new_point_in := (quadratic_bezier_control_point - segment_end_point) * (2.0 / 3.0) if not in_undo_redo_transaction: _start_undo_redo_transaction("Change curve segment %d->%d for %s" % [idx - 1, idx, str(svs)]) undo_redo_transaction[UndoRedoEntry.UNDOS].append([svs.curve, 'set_point_in', idx, svs.curve.get_point_in(idx)]) undo_redo_transaction[UndoRedoEntry.UNDOS].append([svs.curve, 'set_point_out', idx - 1, svs.curve.get_point_out(idx - 1)]) undo_redo_transaction[UndoRedoEntry.DOS] = [[svs.curve, 'set_point_out', idx - 1, new_point_out]] undo_redo_transaction[UndoRedoEntry.DOS].append([svs.curve, 'set_point_in', idx, new_point_in]) svs.curve.set_point_out(idx - 1, new_point_out) svs.curve.set_point_in(idx, new_point_in) md_closest_point["point_position"] = mouse_pos svs.set_meta(META_NAME_HOVER_CLOSEST_POINT, md_closest_point) update_overlays() func _toggle_loop_if_applies(svs : ScalableVectorShape2D, idx : int) -> void: if svs.shape_type != ScalableVectorShape2D.ShapeType.PATH: return if svs.curve.point_count < 3: return if idx == 0 or idx == svs.curve.point_count - 1: if svs.is_curve_closed(): _remove_point_from_curve(svs, svs.curve.point_count - 1) else: _add_point_to_curve(svs, svs.curve.get_point_position(0)) func _get_vp_h_scroll_bar() -> HScrollBar: var editor_vp := EditorInterface.get_editor_viewport_2d().find_parent("*CanvasItemEditor*") if editor_vp == null: return null return editor_vp.find_child("*HScrollBar*", true, false) func _handle_input_for_uniform_translate(event : InputEvent, svs : ScalableVectorShape2D) -> bool: var mouse_pos := EditorInterface.get_editor_viewport_2d().get_mouse_position() if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT: if (event as InputEventMouseButton).pressed: _drag_start = mouse_pos if not in_undo_redo_transaction: _start_undo_redo_transaction("Translate curve points for %s" % str(svs)) update_overlays() return true if event is InputEventMouseMotion: update_overlays() if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): var drag_delta := mouse_pos - _drag_start if _is_snapped_to_pixel(): drag_delta = drag_delta.snapped(Vector2.ONE * _get_snap_resolution()) if drag_delta.abs() > Vector2.ZERO: _drag_start = mouse_pos undo_redo_transaction[UndoRedoEntry.DOS].append([svs, 'translate_points_by', drag_delta]) undo_redo_transaction[UndoRedoEntry.UNDOS].append([svs, 'translate_points_by', -drag_delta]) svs.translate_points_by(drag_delta) return true return false func _flip_svs_horizontal(): var svs := EditorInterface.get_selection().get_selected_nodes().pop_front() if svs is ScalableVectorShape2D: undo_redo.create_action("Flip %s horizontally" % str(svs)) undo_redo.add_do_method(svs, 'flip_points') undo_redo.add_undo_method(svs, 'flip_points') undo_redo.commit_action() func _flip_svs_vertical(): var svs := EditorInterface.get_selection().get_selected_nodes().pop_front() if svs is ScalableVectorShape2D: undo_redo.create_action("Flip %s vertically" % str(svs)) undo_redo.add_do_method(svs, 'flip_points', Vector2(1, -1)) undo_redo.add_undo_method(svs, 'flip_points', Vector2(1, -1)) undo_redo.commit_action() func _handle_input_for_uniform_scale(event : InputEvent, svs : ScalableVectorShape2D) -> bool: var mouse_pos := EditorInterface.get_editor_viewport_2d().get_mouse_position() if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT: if (event as InputEventMouseButton).pressed: _drag_start = mouse_pos if not in_undo_redo_transaction: _start_undo_redo_transaction("Scale curve points for %s" % str(svs)) update_overlays() return true if event is InputEventMouseMotion: update_overlays() if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): var drag_delta := mouse_pos - _drag_start if _is_snapped_to_pixel(): drag_delta = drag_delta.snapped(Vector2.ONE * _get_snap_resolution()) if drag_delta.abs() > Vector2.ZERO: undo_redo_transaction[UndoRedoEntry.DOS].append([svs, 'scale_points_by', _drag_start, mouse_pos, Input.is_key_pressed(KEY_SHIFT)]) undo_redo_transaction[UndoRedoEntry.UNDOS].append([svs, 'scale_points_by', mouse_pos, _drag_start, Input.is_key_pressed(KEY_SHIFT)]) svs.scale_points_by(_drag_start, mouse_pos, Input.is_key_pressed(KEY_SHIFT)) _drag_start = mouse_pos return true return false func _handle_input_for_uniform_rotate(event : InputEvent, svs : ScalableVectorShape2D) -> bool: var mouse_pos := EditorInterface.get_editor_viewport_2d().get_mouse_position() if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT: if (event as InputEventMouseButton).pressed: _drag_start = mouse_pos _prev_uniform_rotate_angle = 0.0 _stored_natural_center = svs.to_global(svs.get_center()) if not in_undo_redo_transaction: _start_undo_redo_transaction("Rotate curve points for %s" % str(svs)) update_overlays() return true if event is InputEventMouseMotion: update_overlays() if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): var use_cntr := Input.is_key_pressed(KEY_SHIFT) or svs.shape_type != ScalableVectorShape2D.ShapeType.PATH var rotation_origin = ( _stored_natural_center if use_cntr else svs.global_position ) var rotation_target := EditorInterface.get_editor_viewport_2d().get_mouse_position() var ang := rotation_origin.angle_to_point(rotation_target) - rotation_origin.angle_to_point(_drag_start) if _is_ctrl_or_cmd_pressed(): ang = snappedf(ang, deg_to_rad(5.0)) if ang != _prev_uniform_rotate_angle: var drag_delta := ang - _prev_uniform_rotate_angle if use_cntr: undo_redo_transaction[UndoRedoEntry.DOS].append([svs, 'rotate_points_by', drag_delta, svs.to_local(rotation_origin)]) undo_redo_transaction[UndoRedoEntry.UNDOS].append([svs, 'rotate_points_by', -drag_delta, svs.to_local(rotation_origin)]) svs.rotate_points_by(drag_delta, svs.to_local(rotation_origin)) else: undo_redo_transaction[UndoRedoEntry.DOS].append([svs, 'rotate_points_by', drag_delta]) undo_redo_transaction[UndoRedoEntry.UNDOS].append([svs, 'rotate_points_by', -drag_delta]) svs.rotate_points_by(drag_delta) _prev_uniform_rotate_angle = ang return true return false func _handle_draw_merge_box_input(event) -> bool: if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT: if event.is_pressed(): _merge_box_rect.position = _vp_transform(EditorInterface.get_editor_viewport_2d().get_mouse_position()) else: _create_svs_vertex_merge_2d() merge_node_toggle_button.button_pressed = false update_overlays() return true if event is InputEventMouseMotion: if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): _merge_box_rect.size = _vp_transform(EditorInterface.get_editor_viewport_2d().get_mouse_position()) - _merge_box_rect.position update_overlays() return true func _forward_canvas_gui_input(event: InputEvent) -> bool: if merge_node_toggle_button.button_pressed: return _handle_draw_merge_box_input(event) if ( event is InputEventKey and (event as InputEventKey).pressed and (event as InputEventKey).keycode == KEY_M and _get_select_mode_button().button_pressed ): merge_node_toggle_button.button_pressed = true if event is InputEventMouseButton and (event as InputEventMouseButton).button_index == MOUSE_BUTTON_LEFT: _lmb_is_down_inside_viewport = (event as InputEventMouseButton).pressed if (in_undo_redo_transaction and event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)): _commit_undo_redo_transaction() if (in_undo_redo_transaction and event is InputEventKey and event.keycode == KEY_SHIFT and not Input.is_key_pressed(KEY_SHIFT)): _commit_undo_redo_transaction() if not _is_editing_enabled(): return false if not _is_change_pivot_button_active() and not _get_select_mode_button().button_pressed: return false if not is_instance_valid(EditorInterface.get_edited_scene_root()): return false var current_selection := EditorInterface.get_selection().get_selected_nodes().pop_back() if _is_svs_valid(current_selection) and _get_select_mode_button().button_pressed: if _uniform_transform_mode == UniformTransformMode.TRANSLATE: return _handle_input_for_uniform_translate(event, current_selection) elif _uniform_transform_mode == UniformTransformMode.SCALE: return _handle_input_for_uniform_scale(event, current_selection) elif _uniform_transform_mode == UniformTransformMode.ROTATE: return _handle_input_for_uniform_rotate(event, current_selection) if _is_svs_valid(current_selection) and _is_ctrl_or_cmd_pressed() and Input.is_key_pressed(KEY_SHIFT): var vp_horiz_scrollbar := _get_vp_h_scroll_bar() if vp_horiz_scrollbar is HScrollBar: if not _locking_vp_horizontal_scrollbar: _vp_horizontal_scrollbar_locked_value = vp_horiz_scrollbar.value _locking_vp_horizontal_scrollbar = true vp_horiz_scrollbar.value = _vp_horizontal_scrollbar_locked_value else: _locking_vp_horizontal_scrollbar = false if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: var mouse_pos := EditorInterface.get_editor_viewport_2d().get_mouse_position() if _is_change_pivot_button_active(): if _is_svs_valid(current_selection): _set_shape_origin(current_selection, mouse_pos) else: if _is_svs_valid(current_selection) and _handle_has_hover(current_selection): if event.double_click and current_selection.has_meta(META_NAME_HOVER_POINT_IDX): _toggle_loop_if_applies(current_selection, current_selection.get_meta(META_NAME_HOVER_POINT_IDX)) elif (_is_svs_valid(current_selection) and Input.is_key_pressed(KEY_ALT) and current_selection.shape_type == ScalableVectorShape2D.ShapeType.PATH and _curve_control_has_hover(current_selection)): set_global_position_popup_panel.popup_with_value( _get_hovered_handle_metadata(current_selection), _is_snapped_to_pixel(), _get_snap_resolution() ) return true elif _is_svs_valid(current_selection) and _is_ctrl_or_cmd_pressed() and Input.is_key_pressed(KEY_SHIFT): if _is_snapped_to_pixel(): mouse_pos = mouse_pos.snapped(Vector2.ONE * _get_snap_resolution()) if (not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and current_selection.has_fine_point(mouse_pos)): _start_cutout_shape(current_selection, mouse_pos) return true elif _is_svs_valid(current_selection) and _is_ctrl_or_cmd_pressed(): if _is_snapped_to_pixel(): mouse_pos = mouse_pos.snapped(Vector2.ONE * _get_snap_resolution()) if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): _add_point_on_position(current_selection, mouse_pos) return true elif _is_svs_valid(current_selection) and current_selection.has_meta(META_NAME_HOVER_CLOSEST_POINT): var cp_md : ClosestPointOnCurveMeta = current_selection.get_meta(META_NAME_HOVER_CLOSEST_POINT) if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and current_selection.is_arc_start(cp_md.before_segment - 1): arc_settings_popup_panel.popup_with_value(current_selection.arc_list.get_arc_for_point(cp_md.before_segment - 1)) elif Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and Input.is_key_pressed(KEY_ALT): _add_point_on_curve_segment(current_selection, true) elif event.double_click: _add_point_on_curve_segment(current_selection) return true elif _is_svs_valid(current_selection) and current_selection.has_meta(META_NAME_HOVER_CLOSEST_POINT_ON_GRADIENT_LINE): if event.double_click: _add_color_stop(current_selection, mouse_pos) return true elif _is_svs_valid(current_selection) and current_selection.has_fine_point(mouse_pos) and event.double_click: _subdivide_curve(current_selection) return true else: var results := _find_scalable_vector_shape_2d_nodes_at(mouse_pos) var refined_result := results.rfind_custom(func(x): return x.has_fine_point(mouse_pos)) if refined_result > -1 and results[refined_result]: if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): selection_candidate = results[refined_result] return true else: if selection_candidate == results[refined_result]: EditorInterface.edit_node(results[refined_result]) return true var result = results.pop_back() if is_instance_valid(result): if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): selection_candidate = result return true else: if selection_candidate == result: EditorInterface.edit_node(result) return true return false if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT: if _is_svs_valid(current_selection) and _is_ctrl_or_cmd_pressed() and Input.is_key_pressed(KEY_SHIFT): if not event.is_pressed(): current_clip_operation += 1 current_clip_operation = 2 if current_clip_operation < 0 else current_clip_operation current_clip_operation = 0 if current_clip_operation > 2 else current_clip_operation return true if _is_svs_valid(current_selection) and _handle_has_hover(current_selection): if not Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT): if current_selection.has_meta(META_NAME_HOVER_POINT_IDX) and current_selection.shape_type == ScalableVectorShape2D.ShapeType.PATH: _remove_point_from_curve(current_selection, current_selection.get_meta(META_NAME_HOVER_POINT_IDX)) elif current_selection.has_meta(META_NAME_HOVER_CP_IN_IDX): if current_selection.shape_type == ScalableVectorShape2D.ShapeType.RECT: _remove_rounded_corners_from_rect(current_selection) else: _remove_cp_in_from_curve(current_selection, current_selection.get_meta(META_NAME_HOVER_CP_IN_IDX)) elif current_selection.has_meta(META_NAME_HOVER_CP_OUT_IDX): if current_selection.shape_type == ScalableVectorShape2D.ShapeType.RECT: _remove_rounded_corners_from_rect(current_selection) else: _remove_cp_out_from_curve(current_selection, current_selection.get_meta(META_NAME_HOVER_CP_OUT_IDX)) elif current_selection.has_meta(META_NAME_HOVER_GRADIENT_COLOR_STOP_IDX): _remove_color_stop(current_selection, current_selection.get_meta(META_NAME_HOVER_GRADIENT_COLOR_STOP_IDX)) return true if _is_svs_valid(current_selection) and current_selection.has_meta(META_NAME_HOVER_CLOSEST_POINT) and not _handle_has_hover(current_selection): if not Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT): var cp_md = current_selection.get_meta(META_NAME_HOVER_CLOSEST_POINT) if current_selection.is_arc_start(cp_md.before_segment - 1): _remove_arc(current_selection, cp_md.before_segment - 1) else: _create_arc(current_selection, cp_md.before_segment - 1) return true if (event is InputEventMouseButton and Input.is_key_pressed(KEY_SHIFT) and event.button_index in [MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN]): if _is_svs_valid(current_selection): if _is_ctrl_or_cmd_pressed(): if not event.is_pressed(): current_cutout_shape += 1 if event.button_index == MOUSE_BUTTON_WHEEL_DOWN else -1 current_cutout_shape = 2 if current_cutout_shape < 0 else current_cutout_shape current_cutout_shape = 0 if current_cutout_shape > 2 else current_cutout_shape return true if event.button_index == MOUSE_BUTTON_WHEEL_DOWN: _resize_shape(current_selection, 0.99) else: _resize_shape(current_selection, 1.01) return true if event is InputEventMouseMotion: var mouse_pos := EditorInterface.get_editor_viewport_2d().get_mouse_position() for result in _find_scalable_vector_shape_2d_nodes(): result.remove_meta(META_NAME_SELECT_HINT) if _is_svs_valid(current_selection) and not _handle_has_hover(current_selection) and current_selection.has_meta(META_NAME_HOVER_CLOSEST_POINT): if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): _drag_curve_segment(current_selection, mouse_pos) return true if _is_svs_valid(current_selection): current_selection.remove_meta(META_NAME_HOVER_CLOSEST_POINT) if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and _is_svs_valid(current_selection): if _handle_has_hover(current_selection): if current_selection.has_meta(META_NAME_HOVER_POINT_IDX): var pt_idx : int = current_selection.get_meta(META_NAME_HOVER_POINT_IDX) if current_selection.shape_type != ScalableVectorShape2D.ShapeType.PATH: _update_rect_dimensions(current_selection, mouse_pos) elif Input.is_key_pressed(KEY_SHIFT): if pt_idx == 0: _update_curve_cp_out_position(current_selection, mouse_pos, pt_idx) else: _update_curve_cp_in_position(current_selection, mouse_pos, pt_idx) else: _update_curve_point_position(current_selection, mouse_pos, pt_idx) elif current_selection.has_meta(META_NAME_HOVER_CP_IN_IDX): if current_selection.shape_type == ScalableVectorShape2D.ShapeType.RECT: _update_rect_corner_radius(current_selection, mouse_pos, "rx", !Input.is_key_pressed(KEY_SHIFT)) else: _update_curve_cp_in_position(current_selection, mouse_pos, current_selection.get_meta(META_NAME_HOVER_CP_IN_IDX)) elif current_selection.has_meta(META_NAME_HOVER_CP_OUT_IDX): if current_selection.shape_type == ScalableVectorShape2D.ShapeType.RECT: _update_rect_corner_radius(current_selection, mouse_pos, "ry", !Input.is_key_pressed(KEY_SHIFT)) else: _update_curve_cp_out_position(current_selection, mouse_pos, current_selection.get_meta(META_NAME_HOVER_CP_OUT_IDX)) elif current_selection.has_meta(META_NAME_HOVER_GRADIENT_FROM): _update_gradient_from_position(current_selection, mouse_pos) elif current_selection.has_meta(META_NAME_HOVER_GRADIENT_TO): _update_gradient_to_position(current_selection, mouse_pos) elif current_selection.has_meta(META_NAME_HOVER_GRADIENT_COLOR_STOP_IDX): _update_gradient_stop_color_pos(current_selection, mouse_pos, current_selection.get_meta(META_NAME_HOVER_GRADIENT_COLOR_STOP_IDX)) update_overlays() return true else: for result : ScalableVectorShape2D in _find_scalable_vector_shape_2d_nodes_at(mouse_pos): result.set_meta(META_NAME_SELECT_HINT, true) if _is_svs_valid(current_selection): _set_handle_hover(mouse_pos, current_selection) update_overlays() return false static func _is_editing_enabled() -> bool: if ProjectSettings.has_setting(SETTING_NAME_EDITING_ENABLED): return ProjectSettings.get_setting(SETTING_NAME_EDITING_ENABLED) return true static func _are_hints_enabled() -> bool: if ProjectSettings.has_setting(SETTING_NAME_HINTS_ENABLED): return ProjectSettings.get_setting(SETTING_NAME_HINTS_ENABLED) return true static func _am_showing_point_numbers() -> bool: if ProjectSettings.has_setting(SETTING_NAME_SHOW_POINT_NUMBERS): return ProjectSettings.get_setting(SETTING_NAME_SHOW_POINT_NUMBERS) return true static func _get_default_stroke_width() -> float: if ProjectSettings.has_setting(SETTING_NAME_STROKE_WIDTH): return ProjectSettings.get_setting(SETTING_NAME_STROKE_WIDTH) return 10.0 static func _get_default_stroke_color() -> Color: if ProjectSettings.has_setting(SETTING_NAME_STROKE_COLOR): return ProjectSettings.get_setting(SETTING_NAME_STROKE_COLOR) return Color.WHITE static func _get_default_begin_cap() -> Line2D.LineCapMode: if ProjectSettings.has_setting(SETTING_NAME_DEFAULT_LINE_BEGIN_CAP): return ProjectSettings.get_setting(SETTING_NAME_DEFAULT_LINE_BEGIN_CAP) return Line2D.LineCapMode.LINE_CAP_NONE static func _get_default_end_cap() -> Line2D.LineCapMode: if ProjectSettings.has_setting(SETTING_NAME_DEFAULT_LINE_END_CAP): return ProjectSettings.get_setting(SETTING_NAME_DEFAULT_LINE_END_CAP) return Line2D.LineCapMode.LINE_CAP_NONE static func _get_default_joint_mode() -> Line2D.LineJointMode: if ProjectSettings.has_setting(SETTING_NAME_DEFAULT_LINE_JOINT_MODE): return ProjectSettings.get_setting(SETTING_NAME_DEFAULT_LINE_JOINT_MODE) return Line2D.LineJointMode.LINE_JOINT_SHARP static func _get_default_fill_color() -> Color: if ProjectSettings.has_setting(SETTING_NAME_FILL_COLOR): return ProjectSettings.get_setting(SETTING_NAME_FILL_COLOR) return Color.WHITE static func _is_add_stroke_enabled() -> bool: if ProjectSettings.has_setting(SETTING_NAME_ADD_STROKE_ENABLED): return ProjectSettings.get_setting(SETTING_NAME_ADD_STROKE_ENABLED) return true static func _using_line_2d_for_stroke() -> bool: if ProjectSettings.has_setting(SETTING_NAME_USE_LINE_2D_FOR_STROKE): return ProjectSettings.get_setting(SETTING_NAME_USE_LINE_2D_FOR_STROKE) return true static func _is_add_fill_enabled() -> bool: if ProjectSettings.has_setting(SETTING_NAME_ADD_FILL_ENABLED): return ProjectSettings.get_setting(SETTING_NAME_ADD_FILL_ENABLED) return true static func _add_collision_object_type() -> ScalableVectorShape2D.CollisionObjectType: if ProjectSettings.has_setting(SETTING_NAME_ADD_COLLISION_TYPE): return ProjectSettings.get_setting(SETTING_NAME_ADD_COLLISION_TYPE) return ScalableVectorShape2D.CollisionObjectType.NONE static func _get_default_paint_order() -> PaintOrder: if ProjectSettings.has_setting(SETTING_NAME_PAINT_ORDER): return ProjectSettings.get_setting(SETTING_NAME_PAINT_ORDER) return PaintOrder.FILL_STROKE_MARKERS static func _is_snapped_to_pixel() -> bool: if ProjectSettings.has_setting(SETTING_NAME_SNAP_TO_PIXEL): return ProjectSettings.get_setting(SETTING_NAME_SNAP_TO_PIXEL) return false static func _get_snap_resolution() -> float: if ProjectSettings.has_setting(SETTING_NAME_SNAP_RESOLUTION): return ProjectSettings.get_setting(SETTING_NAME_SNAP_RESOLUTION) return 1.0 static func _is_setting_update_curve_at_runtime() -> bool: if ProjectSettings.has_setting(SETTING_NAME_CURVE_UPDATE_CURVE_AT_RUNTIME): return ProjectSettings.get_setting(SETTING_NAME_CURVE_UPDATE_CURVE_AT_RUNTIME) return true static func _is_making_curve_resources_local_to_scene() -> bool: if ProjectSettings.has_setting(SETTING_NAME_CURVE_RESOURCE_LOCAL_TO_SCENE): return ProjectSettings.get_setting(SETTING_NAME_CURVE_RESOURCE_LOCAL_TO_SCENE) return true static func _use_antialiased_line_2d() -> bool: if ProjectSettings.has_setting(SETTING_NAME_ANTIALIASED_LINE_2D): return ProjectSettings.get_setting(SETTING_NAME_ANTIALIASED_LINE_2D) return false static func _get_default_tolerance_degrees() -> float: if ProjectSettings.has_setting(SETTING_NAME_CURVE_TOLERANCE_DEGREES): return ProjectSettings.get_setting(SETTING_NAME_CURVE_TOLERANCE_DEGREES) return 4.0 static func _get_default_max_stages() -> int: if ProjectSettings.has_setting(SETTING_NAME_CURVE_MAX_STAGES): return ProjectSettings.get_setting(SETTING_NAME_CURVE_MAX_STAGES) return 5 func _exit_tree(): if _get_select_mode_button().toggled.is_connected(_on_select_mode_toggled): _get_select_mode_button().toggled.disconnect(_on_select_mode_toggled) uniform_transform_edit_buttons.queue_free() merge_node_toggle_button.queue_free() remove_inspector_plugin(plugin) remove_custom_type("DrawablePath2D") remove_custom_type("ScalableVectorShape2D") remove_control_from_bottom_panel(scalable_vector_shapes_2d_dock) scalable_vector_shapes_2d_dock.free() set_global_position_popup_panel.free()