diff --git a/lorien/Assets/Icons/text-block.png b/lorien/Assets/Icons/text-block.png new file mode 100644 index 00000000..3fced245 Binary files /dev/null and b/lorien/Assets/Icons/text-block.png differ diff --git a/lorien/Assets/Icons/text-block.png.import b/lorien/Assets/Icons/text-block.png.import new file mode 100644 index 00000000..1620fac1 --- /dev/null +++ b/lorien/Assets/Icons/text-block.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://desowbtf21cic" +path="res://.godot/imported/text-block.png-6805ad0a47d23e70568cf3e8fb3f0677.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/Icons/text-block.png" +dest_files=["res://.godot/imported/text-block.png-6805ad0a47d23e70568cf3e8fb3f0677.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/lorien/InfiniteCanvas/InfiniteCanvas.gd b/lorien/InfiniteCanvas/InfiniteCanvas.gd index bab70818..249eb05a 100644 --- a/lorien/InfiniteCanvas/InfiniteCanvas.gd +++ b/lorien/InfiniteCanvas/InfiniteCanvas.gd @@ -12,9 +12,11 @@ const PLAYER = preload("res://Misc/Player/Player.tscn") @onready var _circle_tool: CircleTool = $CircleTool @onready var _eraser_tool: EraserTool = $EraserTool @onready var _selection_tool: SelectionTool = $SelectionTool +@onready var _textbox_tool: TextBoxTool = $TextBoxTool @onready var _active_tool: CanvasTool = _brush_tool @onready var _active_tool_type: int = Types.Tool.BRUSH @onready var _strokes_parent: Node2D = $SubViewport/Strokes +@onready var _textboxes_parent: Control = $SubViewport/TextBoxes @onready var _camera: Camera2D = $SubViewport/Camera2D @onready var _viewport: SubViewport = $SubViewport @onready var _grid: InfiniteCanvasGrid = $SubViewport/Grid @@ -33,6 +35,7 @@ var _use_optimizer := true var _optimizer: BrushStrokeOptimizer var _player: Player = null var _player_enabled := false +var _textEdit : TextEdit = TextEdit.new() # ------------------------------------------------------------------------------------------------- func _ready() -> void: @@ -76,6 +79,7 @@ func _gui_input(event: InputEvent) -> void: # ------------------------------------------------------------------------------------------------- func _process_event(event: InputEvent) -> void: + if event is InputEventMouseMotion: info.current_pressure = event.pressure if info.pen_inverted != event.pen_inverted: @@ -92,10 +96,12 @@ func _process_event(event: InputEvent) -> void: if event.is_action("deselect_all_strokes"): if _active_tool == _selection_tool: _selection_tool.deselect_all_strokes() + _selection_tool.deselect_all_text_boxes() if event.is_action("delete_selected_strokes"): if _active_tool == _selection_tool: _delete_selected_strokes() + _delete_selected_text_boxes_strokes() if !get_tree().root.get_viewport().is_input_handled(): _camera.tool_event(event) @@ -133,6 +139,9 @@ func use_tool(tool_type: int) -> void: Types.Tool.SELECT: _active_tool = _selection_tool _use_optimizer = false + Types.Tool.TEXTBOX: + _active_tool = _textbox_tool + _use_optimizer = false if prev_tool != _active_tool: prev_tool.enabled = false @@ -185,6 +194,9 @@ func get_all_strokes() -> Array[BrushStroke]: return _current_project.strokes # ------------------------------------------------------------------------------------------------- +func get_all_text_boxes() -> Array[Label]: + return _current_project.textBoxes +# ------------------------------------------------------------------------------------------------- func enable() -> void: Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN) _camera.enable_input() @@ -296,6 +308,8 @@ func use_project(project: Project) -> void: # Cleanup old data for stroke in _strokes_parent.get_children(): _strokes_parent.remove_child(stroke) + for textBox in _textboxes_parent.get_children(): + _textboxes_parent.remove_child(textBox) info.point_count = 0 info.stroke_count = 0 @@ -309,6 +323,8 @@ func use_project(project: Project) -> void: _strokes_parent.add_child(stroke) info.stroke_count += 1 info.point_count += stroke.points.size() + for textBox in _current_project.textBoxes: + _textboxes_parent.add_child(textBox) _grid.queue_redraw() @@ -372,6 +388,19 @@ func _delete_selected_strokes() -> void: _current_project.undo_redo.commit_action() _current_project.dirty = true +# ------------------------------------------------------------------------------------------------- +func _delete_selected_text_boxes_strokes() -> void: + var text_boxes := _selection_tool.get_selected_text_boxes() + if !text_boxes.is_empty(): + _current_project.undo_redo.create_action("Delete Selection") + for text_box: Label in text_boxes: + _current_project.undo_redo.add_do_method(_do_delete_text_box.bind(text_box)) + _current_project.undo_redo.add_undo_reference(text_box) + _current_project.undo_redo.add_undo_method(_undo_delete_text_box.bind(text_box)) + _selection_tool.deselect_all_text_boxes() + _current_project.undo_redo.commit_action() + _current_project.dirty = true + # ------------------------------------------------------------------------------------------------- func _do_delete_stroke(stroke: BrushStroke) -> void: var index := _current_project.strokes.find(stroke) @@ -380,6 +409,12 @@ func _do_delete_stroke(stroke: BrushStroke) -> void: info.point_count -= stroke.points.size() info.stroke_count -= 1 +# ------------------------------------------------------------------------------------------------- +func _do_delete_text_box(text_box: Label) -> void: + var index := _current_project.textBoxes.find(text_box) + _current_project.textBoxes.remove_at(index) + _textboxes_parent.remove_child(text_box) + # FIXME: this adds strokes at the back and does not preserve stroke order; not sure how to do that except saving before # and after versions of the stroke arrays which is a nogo. # ------------------------------------------------------------------------------------------------- @@ -388,3 +423,54 @@ func _undo_delete_stroke(stroke: BrushStroke) -> void: _strokes_parent.add_child(stroke) info.point_count += stroke.points.size() info.stroke_count += 1 + +# FIXME: this adds text boxes at the back and does not preserve text boxes order; not sure how to do that except saving before +# and after versions of the text boxes arrays which is a nogo. +# ------------------------------------------------------------------------------------------------- +func _undo_delete_text_box(text_box: Label) -> void: + _current_project.textBoxes.append(text_box) + _textboxes_parent.add_child(text_box) + +# ------------------------------------------------------------------------------------------------- +func _create_label(textBox : Label) -> void: + _textboxes_parent.add_child(textBox) + _current_project.textBoxes.append(textBox) + enable() + +# ------------------------------------------------------------------------------------------------- +func _on_text_box_tool_show_text_box_dialog(dialogPosition : Vector2) -> void: + _textEdit = TextEdit.new() + _textEdit.set_position(dialogPosition) + _textEdit.custom_minimum_size = Vector2(350, 150) + _textEdit.SIZE_FILL + _textEdit.focus_exited.connect(_change_edit_to_label) + _textboxes_parent.add_child(_textEdit) + +# ------------------------------------------------------------------------------------------------- +func _change_edit_to_label() -> void: + if _textEdit.text != "": + _on_text_box_editor_text_box_ok(_textEdit.text, _textEdit.position) + _textbox_tool._state = _textbox_tool.State.NONE + _textEdit.queue_free() + _textbox_tool.reset() + enable() + + +# ------------------------------------------------------------------------------------------------- +func _on_text_box_editor_text_box_ok(value : String, labelPosition : Vector2) -> void: + var label : Label = Label.new() + label.text = value + label.set_position(labelPosition) + label.add_theme_color_override("font_color", _brush_color) + _create_label(label) + +# ------------------------------------------------------------------------------------------------- +func _on_text_box_tool_edit_existing_text_box(textBox : Label) -> void: + _textEdit = TextEdit.new() + _textEdit.custom_minimum_size = Vector2(350, 150) + _textEdit.SIZE_FILL + _textEdit.focus_exited.connect(_change_edit_to_label) + _textEdit.position = textBox.position + _textEdit.text = textBox.text + _textboxes_parent.add_child(_textEdit) + textBox.call_deferred("queue_free") diff --git a/lorien/InfiniteCanvas/InfiniteCanvas.tscn b/lorien/InfiniteCanvas/InfiniteCanvas.tscn index c0a94fdb..1a792196 100644 --- a/lorien/InfiniteCanvas/InfiniteCanvas.tscn +++ b/lorien/InfiniteCanvas/InfiniteCanvas.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=17 format=3 uid="uid://c4u5gk5n2aiom"] +[gd_scene load_steps=18 format=3 uid="uid://c4u5gk5n2aiom"] [ext_resource type="Script" path="res://InfiniteCanvas/InfiniteCanvas.gd" id="1"] [ext_resource type="Script" path="res://InfiniteCanvas/PanZoomCamera.gd" id="2"] @@ -9,6 +9,7 @@ [ext_resource type="Script" path="res://InfiniteCanvas/Tools/SelectionTool.gd" id="7"] [ext_resource type="PackedScene" path="res://InfiniteCanvas/Cursor/SelectionCursor/SelectionCursor.tscn" id="8"] [ext_resource type="Script" path="res://InfiniteCanvas/Tools/SelectionRectangle.gd" id="9"] +[ext_resource type="Script" path="res://InfiniteCanvas/Tools/TextBoxTool.gd" id="9_yqcq5"] [ext_resource type="Shader" path="res://InfiniteCanvas/Tools/selection_rectangle.gdshader" id="10"] [ext_resource type="Script" path="res://InfiniteCanvas/Tools/RectangleTool.gd" id="11"] [ext_resource type="Script" path="res://InfiniteCanvas/InfiniteCanvasGrid.gd" id="12"] @@ -55,6 +56,10 @@ script = ExtResource("7") selection_rectangle_path = NodePath("../SubViewport/SelectionRectangle") cursor_path = NodePath("../SubViewport/SelectionCursor") +[node name="TextBoxTool" type="Node" parent="."] +script = ExtResource("9_yqcq5") +cursor_path = NodePath("../SubViewport/SelectionCursor") + [node name="SubViewport" type="SubViewport" parent="."] handle_input_locally = false size = Vector2i(1920, 1080) @@ -80,3 +85,19 @@ script = ExtResource("9") [node name="BrushCursor" parent="SubViewport" instance=ExtResource("4")] [node name="SelectionCursor" parent="SubViewport" instance=ExtResource("8")] + +[node name="TextBoxes" type="Control" parent="SubViewport"] +layout_mode = 3 +anchors_preset = 0 +mouse_filter = 1 + +[node name="Label" type="Label" parent="SubViewport/TextBoxes"] +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 22.3333 + +[connection signal="focus_entered" from="." to="." method="_on_focus_entered"] +[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"] +[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"] +[connection signal="edit_existing_textBox" from="TextBoxTool" to="." method="_on_text_box_tool_edit_existing_text_box"] +[connection signal="show_textBoxDialog" from="TextBoxTool" to="." method="_on_text_box_tool_show_text_box_dialog"] diff --git a/lorien/InfiniteCanvas/Tools/EraserTool.gd b/lorien/InfiniteCanvas/Tools/EraserTool.gd index de1d821e..afbf8e3a 100644 --- a/lorien/InfiniteCanvas/Tools/EraserTool.gd +++ b/lorien/InfiniteCanvas/Tools/EraserTool.gd @@ -73,7 +73,8 @@ func _add_undoredo_action_for_erased_strokes() -> void: # ------------------------------------------------------------------------------------------------ func _update_bounding_boxes() -> void: var strokes := _canvas.get_all_strokes() - _bounding_box_cache = Utils.calculte_bounding_boxes(strokes, BOUNDING_BOX_MARGIN) + var text_boxes := _canvas.get_all_text_boxes() + _bounding_box_cache = Utils.calculte_bounding_boxes(strokes, text_boxes, BOUNDING_BOX_MARGIN) #$"../Viewport/DebugDraw".set_bounding_boxes(_bounding_box_cache.values()) # ------------------------------------------------------------------------------------------------ diff --git a/lorien/InfiniteCanvas/Tools/SelectionTool.gd b/lorien/InfiniteCanvas/Tools/SelectionTool.gd index 1ae69d1e..cc7e7a15 100644 --- a/lorien/InfiniteCanvas/Tools/SelectionTool.gd +++ b/lorien/InfiniteCanvas/Tools/SelectionTool.gd @@ -8,8 +8,11 @@ const MAX_FLOAT := 2147483646.0 const MIN_FLOAT := -2147483646.0 const META_OFFSET := "offset" const GROUP_SELECTED_STROKES := "selected_strokes" # selected strokes +const GROUP_SELECTED_TEXT_BOXES := "selected_text_boxes" const GROUP_STROKES_IN_SELECTION_RECTANGLE := "strokes_in_selection_rectangle" # strokes that are in selection rectangle but not commit (i.e. the user is still selecting) +const GROUP_TEXT_BOXES_IN_SELECTION_RECTANGLE := "text_boxes_in_selection_rectangle" # strokes that are in selection rectangle but not commit (i.e. the user is still selecting) const GROUP_MARKED_FOR_DESELECTION := "strokes_marked_for_deselection" # strokes that need to be deslected once LMB is released +const GROUP_MARKED_TEXT_BOXES_FOR_DESELECTION = "text_boxes_marked_for_deselection" const GROUP_COPIED_STROKES := "strokes_copied" # ------------------------------------------------------------------------------------------------- @@ -28,6 +31,7 @@ var _selecting_end_pos: Vector2 = Vector2.ZERO var _multi_selecting: bool var _mouse_moved_during_pressed := false var _stroke_positions_before_move := {} # BrushStroke -> Vector2 +var _text_box_positions_before_move := {} # TextBox -> Vector2 var _bounding_box_cache := {} # BrushStroke -> Rect2 # ------------------------------------------------------------------------------------------------ @@ -54,6 +58,7 @@ func tool_event(event: InputEvent) -> void: var strokes := get_tree().get_nodes_in_group(GROUP_COPIED_STROKES) if !strokes.is_empty(): deselect_all_strokes() + deselect_all_text_boxes() _cursor.mode = SelectionCursor.Mode.MOVE _paste_strokes(strokes) @@ -66,7 +71,7 @@ func tool_event(event: InputEvent) -> void: _state = State.SELECTING _multi_selecting = true _build_bounding_boxes() - elif get_selected_strokes().size() == 0: + elif get_selected_strokes().size() == 0 && get_selected_text_boxes().size() == 0: _state = State.SELECTING _multi_selecting = false _build_bounding_boxes() @@ -74,8 +79,11 @@ func tool_event(event: InputEvent) -> void: _state = State.MOVING _mouse_moved_during_pressed = false _offset_selected_strokes(_cursor.global_position) + _offset_selected_text_boxes(_cursor.global_position) for s: BrushStroke in get_selected_strokes(): _stroke_positions_before_move[s] = s.global_position + for t: Label in get_selected_text_boxes(): + _text_box_positions_before_move[t] = t.global_position # LMB up - stop selection or movement else: if _state == State.SELECTING: @@ -83,21 +91,27 @@ func tool_event(event: InputEvent) -> void: _selection_rectangle.reset() _selection_rectangle.queue_redraw() _commit_strokes_under_selection_rectangle() + _commit_text_boxes_under_selection_rectangle() _deselect_marked_strokes() - if get_selected_strokes().size() > 0: + _deselect_marked_text_boxes() + if get_selected_strokes().size() > 0 || get_selected_text_boxes().size() > 0: _cursor.mode = SelectionCursor.Mode.MOVE elif _state == State.MOVING: _state = State.NONE if _mouse_moved_during_pressed: _add_undoredo_action_for_moved_strokes() + _add_undoredo_action_for_moved_text_boxes() _stroke_positions_before_move.clear() + _text_box_positions_before_move.clear() else: deselect_all_strokes() + deselect_all_text_boxes() _mouse_moved_during_pressed = false # RMB down - just deselect elif event.button_index == MOUSE_BUTTON_RIGHT && event.pressed && _state == State.NONE: deselect_all_strokes() + deselect_all_text_boxes() # Mouse movement: move the selection elif event is InputEventMouseMotion: @@ -111,13 +125,14 @@ func tool_event(event: InputEvent) -> void: elif _state == State.MOVING: _mouse_moved_during_pressed = true _move_selected_strokes() + _move_selected_text_boxes() # Shift click - switch between move/select cursor mode elif event is InputEventKey: if event.keycode == KEY_SHIFT: if event.pressed: _cursor.mode = SelectionCursor.Mode.SELECT - elif get_selected_strokes().size() > 0: + elif get_selected_strokes().size() > 0 || get_selected_text_boxes().size() > 0: _cursor.mode = SelectionCursor.Mode.MOVE # ------------------------------------------------------------------------------------------------ @@ -131,6 +146,10 @@ func compute_selection(start_pos: Vector2, end_pos: Vector2) -> void: if selection_rect.has_point(abs_point): _set_stroke_selected(stroke) break + for textBox: Label in _canvas.get_all_text_boxes(): + var bounding_text_box_box: Rect2 = textBox.get_global_rect() + if selection_rect.intersects(bounding_text_box_box): + _set_text_box_selected(textBox) _canvas.info.selected_lines = get_selected_strokes().size() # ------------------------------------------------------------------------------------------------ @@ -172,11 +191,16 @@ func _duplicate_stroke(stroke: BrushStroke, offset: Vector2) -> BrushStroke: func _modify_strokes_colors(strokes: Array[BrushStroke], color: Color) -> void: for stroke: BrushStroke in strokes: stroke.color = color + +# ------------------------------------------------------------------------------------------------ +func _modify_text_boxes_colors(text_boxes: Array[Label], color: Color) -> void: + for text_box: Label in text_boxes: + text_box.add_theme_color_override("font_color", color) # ------------------------------------------------------------------------------------------------ func _build_bounding_boxes() -> void: _bounding_box_cache.clear() - _bounding_box_cache = Utils.calculte_bounding_boxes(_canvas.get_all_strokes()) + _bounding_box_cache = Utils.calculte_bounding_boxes(_canvas.get_all_strokes(), _canvas.get_all_text_boxes()) #$"../Viewport/DebugDraw".set_bounding_boxes(_bounding_box_cache.values()) # ------------------------------------------------------------------------------------------------ @@ -187,7 +211,16 @@ func _set_stroke_selected(stroke: BrushStroke) -> void: else: stroke.modulate = Config.DEFAULT_SELECTION_COLOR stroke.add_to_group(GROUP_STROKES_IN_SELECTION_RECTANGLE) - + +# ------------------------------------------------------------------------------------------------ +func _set_text_box_selected(text_box : Label) -> void: + if text_box.is_in_group(GROUP_SELECTED_TEXT_BOXES): + text_box.modulate = Color.WHITE + text_box.add_to_group(GROUP_MARKED_TEXT_BOXES_FOR_DESELECTION) + else: + text_box.modulate = Config.DEFAULT_SELECTION_COLOR + text_box.add_to_group(GROUP_TEXT_BOXES_IN_SELECTION_RECTANGLE) + # ------------------------------------------------------------------------------------------------ func _add_undoredo_action_for_moved_strokes() -> void: var project: Project = ProjectManager.get_active_project() @@ -195,6 +228,19 @@ func _add_undoredo_action_for_moved_strokes() -> void: for stroke: BrushStroke in _stroke_positions_before_move.keys(): project.undo_redo.add_do_property(stroke, "global_position", stroke.global_position) project.undo_redo.add_undo_property(stroke, "global_position", _stroke_positions_before_move[stroke]) + for text_box: Label in _text_box_positions_before_move.keys(): + project.undo_redo.add_do_property(text_box, "global_position", text_box.global_position) + project.undo_redo.add_undo_property(text_box, "global_position", _text_box_positions_before_move[text_box]) + project.undo_redo.commit_action() + project.dirty = true + +# ------------------------------------------------------------------------------------------------ +func _add_undoredo_action_for_moved_text_boxes() -> void: + var project: Project = ProjectManager.get_active_project() + project.undo_redo.create_action("Move Text_Boxes") + for text_box: Label in _text_box_positions_before_move.keys(): + project.undo_redo.add_do_property(text_box, "global_position", text_box.global_position) + project.undo_redo.add_undo_property(text_box, "global_position", _text_box_positions_before_move[text_box]) project.undo_redo.commit_action() project.dirty = true @@ -202,18 +248,34 @@ func _add_undoredo_action_for_moved_strokes() -> void: func _offset_selected_strokes(offset: Vector2) -> void: for stroke: BrushStroke in get_selected_strokes(): stroke.set_meta(META_OFFSET, stroke.position - offset) + +# ------------------------------------------------------------------------------------------------- +func _offset_selected_text_boxes(offset: Vector2) -> void: + for text_box: Label in get_selected_text_boxes(): + text_box.set_meta(META_OFFSET, text_box.position - offset) # ------------------------------------------------------------------------------------------------- func _move_selected_strokes() -> void: for stroke: BrushStroke in get_selected_strokes(): stroke.global_position = stroke.get_meta(META_OFFSET) + _cursor.global_position +# ------------------------------------------------------------------------------------------------- +func _move_selected_text_boxes() -> void: + for text_box: Label in get_selected_text_boxes(): + text_box.global_position = text_box.get_meta(META_OFFSET) + _cursor.global_position + # ------------------------------------------------------------------------------------------------ func _commit_strokes_under_selection_rectangle() -> void: for stroke: BrushStroke in get_tree().get_nodes_in_group(GROUP_STROKES_IN_SELECTION_RECTANGLE): stroke.remove_from_group(GROUP_STROKES_IN_SELECTION_RECTANGLE) stroke.add_to_group(GROUP_SELECTED_STROKES) +# ------------------------------------------------------------------------------------------------ +func _commit_text_boxes_under_selection_rectangle() -> void: + for text_box: Label in get_tree().get_nodes_in_group(GROUP_TEXT_BOXES_IN_SELECTION_RECTANGLE): + text_box.remove_from_group(GROUP_TEXT_BOXES_IN_SELECTION_RECTANGLE) + text_box.add_to_group(GROUP_SELECTED_TEXT_BOXES) + # ------------------------------------------------------------------------------------------------ func _deselect_marked_strokes() -> void: for stroke: BrushStroke in get_tree().get_nodes_in_group(GROUP_MARKED_FOR_DESELECTION): @@ -221,6 +283,13 @@ func _deselect_marked_strokes() -> void: stroke.remove_from_group(GROUP_SELECTED_STROKES) stroke.modulate = Color.WHITE +# ------------------------------------------------------------------------------------------------ +func _deselect_marked_text_boxes() -> void: + for textBox: Label in get_tree().get_nodes_in_group(GROUP_MARKED_TEXT_BOXES_FOR_DESELECTION): + textBox.remove_from_group(GROUP_MARKED_TEXT_BOXES_FOR_DESELECTION) + textBox.remove_from_group(GROUP_SELECTED_TEXT_BOXES) + textBox.modulate = Color.WHITE + # ------------------------------------------------------------------------------------------------ func deselect_all_strokes() -> void: var selected_strokes: Array = get_selected_strokes() @@ -234,6 +303,18 @@ func deselect_all_strokes() -> void: _canvas.info.selected_lines = 0 _cursor.mode = SelectionCursor.Mode.SELECT +# ------------------------------------------------------------------------------------------------ +func deselect_all_text_boxes() -> void: + var selected_text_boxes: Array = get_selected_text_boxes() + if selected_text_boxes.size(): + get_tree().set_group(GROUP_SELECTED_TEXT_BOXES, "modulate", Color.WHITE) + get_tree().set_group(GROUP_TEXT_BOXES_IN_SELECTION_RECTANGLE, "modulate", Color.WHITE) + Utils.remove_group_from_all_nodes(GROUP_SELECTED_TEXT_BOXES) + Utils.remove_group_from_all_nodes(GROUP_MARKED_TEXT_BOXES_FOR_DESELECTION) + Utils.remove_group_from_all_nodes(GROUP_TEXT_BOXES_IN_SELECTION_RECTANGLE) + + _cursor.mode = SelectionCursor.Mode.SELECT + # ------------------------------------------------------------------------------------------------ func is_selecting() -> bool: return _state == State.SELECTING @@ -248,10 +329,22 @@ func get_selected_strokes() -> Array[BrushStroke]: return strokes +# ------------------------------------------------------------------------------------------------ +func get_selected_text_boxes() -> Array[Label]: + # Can't cast from Array[Node] to Array[TextBox] directly (godot bug/missing feature?) + # so let's do it per item + var text_boxes: Array[Label] + for text_box in get_tree().get_nodes_in_group(GROUP_SELECTED_TEXT_BOXES): + text_boxes.append(text_box as Label) + + return text_boxes + # ------------------------------------------------------------------------------------------------ func _on_brush_color_changed(color: Color) -> void: var strokes := get_selected_strokes() + var text_boxes := get_selected_text_boxes() _modify_strokes_colors(strokes, color) + _modify_text_boxes_colors(text_boxes, color) # ------------------------------------------------------------------------------------------------ func reset() -> void: @@ -259,4 +352,6 @@ func reset() -> void: _selection_rectangle.reset() _selection_rectangle.queue_redraw() _commit_strokes_under_selection_rectangle() + _commit_text_boxes_under_selection_rectangle() deselect_all_strokes() + deselect_all_text_boxes() diff --git a/lorien/InfiniteCanvas/Tools/TextBoxTool.gd b/lorien/InfiniteCanvas/Tools/TextBoxTool.gd new file mode 100644 index 00000000..40fca2ac --- /dev/null +++ b/lorien/InfiniteCanvas/Tools/TextBoxTool.gd @@ -0,0 +1,36 @@ +class_name TextBoxTool +extends CanvasTool + +signal show_textBoxDialog +signal edit_existing_textBox + +enum State { + NONE, + CREATING, + EDITING +} + +@export +var _state = State.NONE +@export +var _position : Vector2 + +func _ready() -> void: + super() + _state = State.NONE + +# ------------------------------------------------------------------------------------------------- +func tool_event(event: InputEvent) -> void: + + if event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT: + if event.pressed && _state == State.NONE: + + for textBox : Control in get_parent()._textboxes_parent.get_children(): + if textBox is Label: + if textBox.get_rect().has_point(event.global_position): + edit_existing_textBox.emit(textBox) + _state = State.EDITING + + if _state == State.NONE: + show_textBoxDialog.emit(_cursor.global_position) diff --git a/lorien/Main.gd b/lorien/Main.gd index bc1d3846..b15c0fbe 100644 --- a/lorien/Main.gd +++ b/lorien/Main.gd @@ -136,6 +136,7 @@ func _exit_tree() -> void: # ------------------------------------------------------------------------------------------------- func _process(delta: float) -> void: + # Lower fps if user is idle var idle := (Time.get_ticks_msec() - _last_input_time) > Config.BACKGROUND_IDLE_TIME_THRESHOLD if !_player_enabled && !_canvas.is_drawing() && idle: @@ -153,7 +154,9 @@ func _process(delta: float) -> void: var active_project: Project = ProjectManager.get_active_project() if active_project != null: _menubar.update_tab_title(active_project) - + + var nodes_in_group = get_tree().get_nodes_in_group("text_boxes_in_selection_rectangle") + # ------------------------------------------------------------------------------------------------- func _unhandled_input(event: InputEvent) -> void: # Idle time over; let's set the fps high again @@ -528,7 +531,7 @@ func _on_export_confirmed(path: String) -> void: if project != null: var background := _canvas.get_background_color() var svg := SvgExporter.new() - svg.export_svg(project.strokes, background, path) + svg.export_svg(project.strokes, project.textBoxes, background, path) _: printerr("Unsupported format") diff --git a/lorien/Misc/SvgExporter.gd b/lorien/Misc/SvgExporter.gd index c4f4f149..15857b84 100644 --- a/lorien/Misc/SvgExporter.gd +++ b/lorien/Misc/SvgExporter.gd @@ -8,7 +8,7 @@ extends RefCounted const EDGE_MARGIN := 0.025 # ------------------------------------------------------------------------------------------------- -func export_svg(strokes: Array[BrushStroke], background: Color, path: String) -> void: +func export_svg(strokes: Array[BrushStroke], text_boxes: Array[Label], background: Color, path: String) -> void: var start_time := Time.get_ticks_msec() # Open file @@ -25,6 +25,13 @@ func export_svg(strokes: Array[BrushStroke], background: Color, path: String) -> min_dim.y = min(min_dim.y, stroke.top_left_pos.y + stroke.global_position.y) max_dim.x = max(max_dim.x, stroke.bottom_right_pos.x + stroke.global_position.x) max_dim.y = max(max_dim.y, stroke.bottom_right_pos.y + stroke.global_position.y) + + for text_box: Label in text_boxes: + min_dim.x = min(min_dim.x, text_box.get_rect().position.x) + min_dim.y = min(min_dim.y, text_box.get_rect().position.y) + max_dim.x = max(max_dim.x, text_box.get_rect().position.x+text_box.get_rect().size.x) + max_dim.y = max(max_dim.y, text_box.get_rect().position.y+text_box.get_rect().size.y) + var size := max_dim - min_dim var margin_size := size * EDGE_MARGIN size += margin_size*2.0 @@ -35,6 +42,8 @@ func export_svg(strokes: Array[BrushStroke], background: Color, path: String) -> _svg_rect(file, origin, size, background) for stroke: BrushStroke in strokes: _svg_polyline(file, stroke) + for text_box: Label in text_boxes: + _svg_text(file, text_box) _svg_end(file) # Flush and close the file @@ -71,3 +80,17 @@ func _svg_polyline(file: FileAccess, stroke: BrushStroke) -> void: file.store_string("%.1f %.1f" % [point.x, point.y]) idx += 1 file.store_string("\" style=\"fill:none;stroke:#%s;stroke-width:2\"/>\n" % stroke.color.to_html(false)) + +# ------------------------------------------------------------------------------------------------- +func _svg_text(file: FileAccess, text_box: Label) -> void: + file.store_string("") + file.store_string(text_box.text) + file.store_string("\n") + diff --git a/lorien/Misc/Types.gd b/lorien/Misc/Types.gd index 64ea2d7f..45a30599 100644 --- a/lorien/Misc/Types.gd +++ b/lorien/Misc/Types.gd @@ -9,6 +9,7 @@ enum Tool { LINE, ERASER, SELECT, + TEXTBOX, } # ------------------------------------------------------------------------------------------------- diff --git a/lorien/Misc/Utils.gd b/lorien/Misc/Utils.gd index 3d8617a0..72ba8951 100644 --- a/lorien/Misc/Utils.gd +++ b/lorien/Misc/Utils.gd @@ -43,7 +43,7 @@ func calculate_rect(start_pos: Vector2, end_pos: Vector2) -> Rect2: return area # ------------------------------------------------------------------------------------------------- -func calculte_bounding_boxes(strokes: Array[BrushStroke], margin: float = 0.0) -> Dictionary: +func calculte_bounding_boxes(strokes: Array[BrushStroke], text_boxes: Array[Label], margin: float = 0.0) -> Dictionary: var result := {} for stroke: BrushStroke in strokes: var top_left := stroke.position + stroke.top_left_pos @@ -52,6 +52,10 @@ func calculte_bounding_boxes(strokes: Array[BrushStroke], margin: float = 0.0) - if margin > 0: bounding_box = bounding_box.grow(margin) result[stroke] = bounding_box + + for text_box : Label in text_boxes: + var text_box_bounding_box := text_box.get_global_rect() + result[text_box] = text_box_bounding_box return result # ------------------------------------------------------------------------------------------------- diff --git a/lorien/ProjectManager/Project.gd b/lorien/ProjectManager/Project.gd index bf661d6f..42a87ab3 100644 --- a/lorien/ProjectManager/Project.gd +++ b/lorien/ProjectManager/Project.gd @@ -17,6 +17,7 @@ var loaded := false var filepath: String var meta_data: Dictionary var strokes: Array[BrushStroke] +var textBoxes : Array[Label] # ------------------------------------------------------------------------------------------------- func _init() -> void: @@ -34,6 +35,7 @@ func clear() -> void: undo_redo = null meta_data.clear() strokes.clear() + textBoxes.clear() # ------------------------------------------------------------------------------------------------- func add_stroke(stroke: BrushStroke) -> void: diff --git a/lorien/ProjectManager/Serializer.gd b/lorien/ProjectManager/Serializer.gd index bf348f28..d7fad55f 100644 --- a/lorien/ProjectManager/Serializer.gd +++ b/lorien/ProjectManager/Serializer.gd @@ -10,8 +10,9 @@ const COMPRESSION_METHOD = FileAccess.COMPRESSION_DEFLATE const POINT_ELEM_SIZE := 3 const VERSION_NUMBER := 1 -const TYPE_BRUSH_STROKE := 0 -const TYPE_ERASER_STROKE_DEPRECATED := 1 # Deprecated since v0; will be ignored when read; structually the same as normal brush stroke +const TYPE_BRUSH_STROKE : int = 0 +const TYPE_ERASER_STROKE_DEPRECATED : int = 1 # Deprecated since v0; will be ignored when read; structually the same as normal brush stroke +const TYPE_TEXT_BOX : int = 2 # ------------------------------------------------------------------------------------------------- static func save_project(project: Project) -> void: @@ -53,7 +54,18 @@ static func save_project(project: Project) -> void: file.store_8(pressure) p_idx += 1 - # Done + # Text Box Data + for textBox : Label in project.textBoxes: + file.store_8(TYPE_TEXT_BOX) + file.store_float(textBox.global_position.x) + file.store_float(textBox.global_position.y) + file.store_pascal_string(textBox.text) + var textBoxColor : Color = textBox.get("theme_override_colors/font_color") + file.store_8(textBoxColor.r8) + file.store_8(textBoxColor.g8) + file.store_8(textBoxColor.b8) + + # Done file.close() print("Saved %s in %d ms" % [project.filepath, (Time.get_ticks_msec() - start_time)]) @@ -109,6 +121,20 @@ static func load_project(project: Project) -> void: print("Skipped deprecated eraser stroke: %d points" % point_count) else: project.strokes.append(brush_stroke) + TYPE_TEXT_BOX: + var x = file.get_float() + var y = file.get_float() + var text = file.get_pascal_string() + var textBox : Label = Label.new() + textBox.set_global_position(Vector2(x,y)) + textBox.text = text; + var r := file.get_8() + var g := file.get_8() + var b := file.get_8() + var textBoxColor : Color = Color(r/255.0, g/255.0, b/255.0, 1.0) + textBox.add_theme_color_override("font_color", textBoxColor) + + project.textBoxes.append(textBox) _: printerr("Invalid type") diff --git a/lorien/UI/Components/PaletteButton.tscn b/lorien/UI/Components/PaletteButton.tscn index d09c1864..00671869 100644 --- a/lorien/UI/Components/PaletteButton.tscn +++ b/lorien/UI/Components/PaletteButton.tscn @@ -1,18 +1,24 @@ -[gd_scene load_steps=4 format=2] +[gd_scene load_steps=4 format=3 uid="uid://bgh8biiyio5bn"] -[ext_resource path="res://Assets/Textures/palette_button_outline.png" type="Texture2D" id=1] -[ext_resource path="res://Assets/Textures/palette_button.png" type="Texture2D" id=2] -[ext_resource path="res://UI/Components/PaletteButton.gd" type="Script" id=3] +[ext_resource type="Texture2D" uid="uid://d2x4s44tq3fd3" path="res://Assets/Textures/palette_button_outline.png" id="1"] +[ext_resource type="Texture2D" uid="uid://bb2kaiahpvl5m" path="res://Assets/Textures/palette_button.png" id="2"] +[ext_resource type="Script" path="res://UI/Components/PaletteButton.gd" id="3"] [node name="PaletteButton" type="Control"] +custom_minimum_size = Vector2(31, 31) +layout_mode = 3 +anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 offset_right = -1889.0 offset_bottom = -1049.0 -custom_minimum_size = Vector2( 31, 31 ) -script = ExtResource( 3 ) +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("3") [node name="Color" type="TextureRect" parent="."] +custom_minimum_size = Vector2(32, 32) +layout_mode = 0 anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 @@ -21,20 +27,14 @@ offset_left = -16.0 offset_top = -16.0 offset_right = 16.0 offset_bottom = 16.0 -custom_minimum_size = Vector2( 32, 32 ) -texture = ExtResource( 2 ) -expand = true +texture = ExtResource("2") +expand_mode = 1 stretch_mode = 6 -__meta__ = { -"_edit_use_anchors_": false -} [node name="Selection" type="TextureRect" parent="."] +layout_mode = 0 anchor_right = 1.0 anchor_bottom = 1.0 -texture = ExtResource( 1 ) -expand = true +texture = ExtResource("1") +expand_mode = 1 stretch_mode = 4 -__meta__ = { -"_edit_use_anchors_": false -} diff --git a/lorien/UI/Themes/dark/theme.tres b/lorien/UI/Themes/dark/theme.tres index f5895d05..d845838f 100644 --- a/lorien/UI/Themes/dark/theme.tres +++ b/lorien/UI/Themes/dark/theme.tres @@ -1,6 +1,5 @@ -[gd_resource type="Theme" load_steps=28 format=4 uid="uid://u5qnpgxqykiv"] +[gd_resource type="Theme" load_steps=26 format=4 uid="uid://u5qnpgxqykiv"] -[ext_resource type="Texture2D" uid="uid://bp1yka17gbjtu" path="res://Assets/Icons/close.png" id="1_qgxa7"] [ext_resource type="StyleBox" uid="uid://kdduww61cjw" path="res://UI/Themes/dark/tab_inactive.tres" id="2_n6mkw"] [ext_resource type="StyleBox" uid="uid://dtn7ehcyfik4a" path="res://UI/Themes/dark/tab_active.tres" id="3_rqnin"] @@ -67,20 +66,6 @@ corner_radius_top_right = 64 corner_radius_bottom_right = 64 corner_radius_bottom_left = 64 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cvp1a"] -bg_color = Color(0.266667, 0.270588, 0.290196, 1) -corner_radius_top_left = 64 -corner_radius_top_right = 64 -corner_radius_bottom_right = 64 -corner_radius_bottom_left = 64 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xp38c"] -bg_color = Color(0.207843, 0.211765, 0.227451, 1) -corner_radius_top_left = 64 -corner_radius_top_right = 64 -corner_radius_bottom_right = 64 -corner_radius_bottom_left = 64 - [sub_resource type="StyleBoxFlat" id="4"] corner_radius_top_left = 1 corner_radius_top_right = 1 @@ -2466,8 +2451,6 @@ Button/styles/focus = SubResource("StyleBoxFlat_rb6ee") Button/styles/hover = SubResource("StyleBoxFlat_g5iyp") Button/styles/normal = SubResource("StyleBoxFlat_k6u2e") Button/styles/pressed = SubResource("StyleBoxFlat_4l7x4") -CheckBox/styles/hover_pressed = SubResource("StyleBoxFlat_cvp1a") -CheckBox/styles/pressed = SubResource("StyleBoxFlat_xp38c") CheckButton/colors/font_color = Color(0.88, 0.88, 0.88, 1) CheckButton/colors/font_color_disabled = Color(0.9, 0.9, 0.9, 0.2) CheckButton/colors/font_color_hover = Color(0.94, 0.94, 0.94, 1) @@ -2578,7 +2561,7 @@ TabBar/constants/h_separation = 4 TabBar/constants/icon_max_width = 0 TabBar/constants/outline_size = 0 TabBar/fonts/font = SubResource("FontFile_1bm21") -TabBar/icons/close = ExtResource("1_qgxa7") +TabBar/icons/close = null TabBar/styles/button_highlight = SubResource("StyleBoxEmpty_me351") TabBar/styles/button_pressed = SubResource("StyleBoxEmpty_n0chp") TabBar/styles/tab_disabled = ExtResource("2_n6mkw") diff --git a/lorien/UI/Toolbar.gd b/lorien/UI/Toolbar.gd index 6f84dbac..0217a87b 100644 --- a/lorien/UI/Toolbar.gd +++ b/lorien/UI/Toolbar.gd @@ -33,6 +33,7 @@ const BUTTON_NORMAL_COLOR = Color.WHITE @onready var _tool_btn_line: FlatTextureButton = $Console/Left/LineToolButton @onready var _tool_btn_eraser: FlatTextureButton = $Console/Left/EraserToolButton @onready var _tool_btn_selection: FlatTextureButton = $Console/Left/SelectionToolButton +@onready var _tool_btn_textbox: FlatTextureButton = $Console/Left/TextBoxToolButton var _last_active_tool_button: FlatTextureButton @@ -65,6 +66,7 @@ func _ready() -> void: _tool_btn_line.pressed.connect(_on_line_tool_pressed) _tool_btn_eraser.pressed.connect(_on_eraser_tool_pressed) _tool_btn_selection.pressed.connect(_on_select_tool_pressed) + _tool_btn_textbox.pressed.connect(_on_textbox_tool_pressed) # ------------------------------------------------------------------------------------------------- func enable_tool(tool_type: Types.Tool) -> void: @@ -76,6 +78,7 @@ func enable_tool(tool_type: Types.Tool) -> void: Types.Tool.SELECT: btn = _tool_btn_selection Types.Tool.RECTANGLE: btn = _tool_btn_rectangle Types.Tool.CIRCLE: btn = _tool_btn_circle + Types.Tool.TEXTBOX: btn = _tool_btn_textbox btn.toggle() _change_active_tool_button(btn) @@ -112,6 +115,7 @@ func _on_keybinding_changed(action: KeybindingsManager.Action) -> void: "shortcut_line_tool": _tool_btn_line.tooltip_text = fmt % [tr("TOOLBAR_TOOLTIP_LINE_TOOL"), label] "shortcut_eraser_tool": _tool_btn_eraser.tooltip_text = fmt % [tr("TOOLBAR_TOOLTIP_ERASER_TOOL"), label] "shortcut_select_tool": _tool_btn_selection.tooltip_text = fmt % [tr("TOOLBAR_TOOLTIP_SELECT_TOOL"), label] + "shortcut_textbox_tool": _tool_btn_textbox.tooltip_text = fmt % [tr("TOOLBAR_TOOLTIP_TEXTBOX_TOOL"), label] # ------------------------------------------------------------------------------------------------- func _on_open_project_pressed() -> void: @@ -167,6 +171,11 @@ func _on_eraser_tool_pressed() -> void: func _on_select_tool_pressed() -> void: _change_active_tool_button(_tool_btn_selection) tool_changed.emit(Types.Tool.SELECT) + +# ------------------------------------------------------------------------------------------------- +func _on_textbox_tool_pressed() -> void: + _change_active_tool_button(_tool_btn_textbox) + tool_changed.emit(Types.Tool.TEXTBOX) # ------------------------------------------------------------------------------------------------- func _change_active_tool_button(btn: TextureButton) -> void: diff --git a/lorien/UI/Toolbar.tscn b/lorien/UI/Toolbar.tscn index e4323884..8636dd85 100644 --- a/lorien/UI/Toolbar.tscn +++ b/lorien/UI/Toolbar.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=19 format=3 uid="uid://c0ral10lvpo7f"] +[gd_scene load_steps=20 format=3 uid="uid://c0ral10lvpo7f"] [ext_resource type="Texture2D" uid="uid://c4kv3i7fmom58" path="res://Assets/Icons/save_file.png" id="1"] [ext_resource type="Texture2D" uid="uid://dxi6gc6npiskq" path="res://Assets/Icons/open_file.png" id="2"] @@ -13,6 +13,7 @@ [ext_resource type="Script" path="res://UI/Components/FlatTextureButton.gd" id="13"] [ext_resource type="Theme" uid="uid://u5qnpgxqykiv" path="res://UI/Themes/dark/theme.tres" id="15"] [ext_resource type="Texture2D" uid="uid://ddxis8f7tvg66" path="res://Assets/Icons/selection_tool.png" id="16"] +[ext_resource type="Texture2D" uid="uid://desowbtf21cic" path="res://Assets/Icons/text-block.png" id="16_hx67p"] [ext_resource type="Texture2D" uid="uid://0qicbkag5jd3" path="res://Assets/Icons/circle_tool.png" id="19"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_a5mfo"] @@ -183,6 +184,17 @@ script = ExtResource("13") hover_tint = Color(0.662745, 0.945098, 0.87451, 1) pressed_tint = Color(0.572549, 1, 0.894118, 1) +[node name="TextBoxToolButton" type="TextureButton" parent="Console/Left"] +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +tooltip_text = "TOOLBAR_TOOLTIP_SELECT_TOOL" +toggle_mode = true +texture_normal = ExtResource("16_hx67p") +script = ExtResource("13") +hover_tint = Color(0.662745, 0.945098, 0.87451, 1) +pressed_tint = Color(0.572549, 1, 0.894118, 1) + [node name="VSeparator4" type="VSeparator" parent="Console/Left"] layout_mode = 2 theme_override_styles/separator = SubResource("3") diff --git a/lorien/project.godot b/lorien/project.godot index 6d92db3a..e51564bc 100644 --- a/lorien/project.godot +++ b/lorien/project.godot @@ -51,6 +51,7 @@ gdscript/warnings/unused_argument=false window/size/viewport_width=1920 window/size/viewport_height=1080 +window/size/initial_position_type=3 window/size/window_width_override=1440 window/size/window_height_override=810 window/subwindows/embed_subwindows=false @@ -62,6 +63,10 @@ window/vsync/vsync_mode=false naming/scene_name_casing=1 naming/script_name_casing=1 +[gui] + +theme/default_font_multichannel_signed_distance_field=true + [input] ui_accept={