diff --git a/src/Autoload/DrawingAlgos.gd b/src/Autoload/DrawingAlgos.gd index d3b89d733..1b8228eb2 100644 --- a/src/Autoload/DrawingAlgos.gd +++ b/src/Autoload/DrawingAlgos.gd @@ -25,39 +25,39 @@ func blend_layers( only_selected_cels := false, only_selected_layers := false, ) -> void: - var frame_index := project.frames.find(frame) + var frame_index: int = project.frames.find(frame) var previous_ordered_layers: Array[int] = project.ordered_layers project.order_layers(frame_index) var textures: Array[Image] = [] # Nx4 texture, where N is the number of layers and the first row are the blend modes, # the second are the opacities, the third are the origins and the fourth are the # clipping mask booleans. - var metadata_image := Image.create(project.layers.size(), 4, false, Image.FORMAT_R8) + var metadata_image: Image = Image.create(project.layers.size(), 4, false, Image.FORMAT_R8) for i in project.layers.size(): - var ordered_index := project.ordered_layers[i] - var layer := project.layers[ordered_index] - var include := true if layer.is_visible_in_hierarchy() else false + var ordered_index: int = project.ordered_layers[i] + var layer: BaseLayer = project.layers[ordered_index] + var include: bool = true if layer.is_visible_in_hierarchy() else false if only_selected_cels and include: var test_array := [frame_index, i] if not test_array in project.selected_cels: include = false if only_selected_layers and include: - var layer_is_selected := false + var layer_is_selected: bool = false for selected_cel in project.selected_cels: if i == selected_cel[1]: layer_is_selected = true break if not layer_is_selected: include = false - var cel := frame.cels[ordered_index] + var cel: BaseCel = frame.cels[ordered_index] if DisplayServer.get_name() == "headless": blend_layers_headless(image, project, layer, cel, origin) else: if layer is GroupLayer and layer.blend_mode != BaseLayer.BlendModes.PASS_THROUGH: - var cel_image := (layer as GroupLayer).blend_children(frame) + var cel_image: Image = (layer as GroupLayer).blend_children(frame) textures.append(cel_image) else: - var cel_image := layer.display_effects(cel) + var cel_image: Image = layer.display_effects(cel) textures.append(cel_image) if ( layer.is_blended_by_ancestor() @@ -67,13 +67,13 @@ func blend_layers( include = false set_layer_metadata_image(layer, cel, metadata_image, ordered_index, include) if DisplayServer.get_name() != "headless": - var texture_array := Texture2DArray.new() + var texture_array: Texture2DArray = Texture2DArray.new() texture_array.create_from_images(textures) var params := { "layers": texture_array, "metadata": ImageTexture.create_from_image(metadata_image), } - var blended := Image.create(project.size.x, project.size.y, false, image.get_format()) + var blended: Image = Image.create(project.size.x, project.size.y, false, image.get_format()) var gen := ShaderImageEffect.new() gen.generate_image(blended, blend_layers_shader, params, project.size) image.blend_rect(blended, Rect2i(Vector2i.ZERO, project.size), origin) diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd index 89ec8c49e..2b33f3d7a 100644 --- a/src/Autoload/Export.gd +++ b/src/Autoload/Export.gd @@ -149,7 +149,14 @@ func process_data(project := Global.current_project) -> void: process_spritesheet(project) -func process_spritesheet(project := Global.current_project) -> void: +func process_spritesheet(project := Global.current_project, info_cache: Dictionary = {}) -> void: + ## info_cache stores the position of frame in the spritesheet and recycles the last created + ## spritesheet to construct a new one (only if info_cache is provided). + ## This causes significant performance boost for higher row or column values + ## (no significant increase in lower values). + var old_spritesheet: Image + if not info_cache.is_empty(): + old_spritesheet = processed_images[0].image processed_images.clear() # Range of frames determined by tags var frames := _calculate_frames(project) @@ -192,6 +199,11 @@ func process_spritesheet(project := Global.current_project) -> void: var width := project.size.x * spritesheet_columns var height := project.size.y * spritesheet_rows var whole_image := Image.create(width, height, false, Image.FORMAT_RGBA8) + if not info_cache.is_empty() and old_spritesheet: + whole_image.blend_rect( + old_spritesheet, Rect2i(Vector2i.ZERO, old_spritesheet.get_size()), Vector2i.ZERO + ) + var old_origins = info_cache.values() var origin := Vector2i.ZERO var hh := 0 var vv := 0 @@ -252,8 +264,17 @@ func process_spritesheet(project := Global.current_project) -> void: origin.y = project.size.y * tag_origins[0] origin.x = 0 tag_origins[0] += 1 + old_origins.erase(origin) + if frame in info_cache: + if info_cache[frame] == origin: + continue + else: + whole_image.fill_rect(Rect2i(origin, project.size), Color.TRANSPARENT) + info_cache[frame] = origin _blend_layers(whole_image, frame, origin) - + ## remove redundant origins from image + for useless in old_origins: + whole_image.fill_rect(Rect2i(useless, project.size), Color.TRANSPARENT) processed_images.append(ProcessedImage.new(whole_image, 0)) @@ -671,7 +692,7 @@ func _blend_layers( # Attempt to read the image data directly from the pxo file, without having to blend # This is mostly useful for when running Pixelorama in headless mode # To handle exporting from a CLI - var zip_reader := ZIPReader.new() + var zip_reader: ZIPReader = ZIPReader.new() var err := zip_reader.open(project.save_path) if err == OK: var frame_index := project.frames.find(frame) + 1 @@ -679,8 +700,8 @@ func _blend_layers( if zip_reader.file_exists(image_path): # "Include blended" must be toggled on when saving the pxo file # in order for this to work. - var image_data := zip_reader.read_file(image_path) - var loaded_image := Image.create_from_data( + var image_data: PackedByteArray = zip_reader.read_file(image_path) + var loaded_image: Image = Image.create_from_data( project.size.x, project.size.y, image.has_mipmaps(), @@ -698,8 +719,8 @@ func _blend_layers( elif export_layers == SELECTED_LAYERS: DrawingAlgos.blend_layers(image, frame, origin, project, false, true) else: - var layer := project.layers[export_layers - 2] - var layer_image := Image.new() + var layer: BaseLayer = project.layers[export_layers - 2] + var layer_image: Image = Image.new() if layer is GroupLayer: layer_image.copy_from(layer.blend_children(frame, Vector2i.ZERO)) else: diff --git a/src/UI/Dialogs/ExportDialog.gd b/src/UI/Dialogs/ExportDialog.gd index 8924584e3..ac856adc3 100644 --- a/src/UI/Dialogs/ExportDialog.gd +++ b/src/UI/Dialogs/ExportDialog.gd @@ -6,7 +6,7 @@ signal about_to_preview(dict: Dictionary) var preview_current_frame := 0 var preview_frames: Array[Texture2D] = [] - +var spritesheet_info_cache = {} # Allow custom exporters to be added var image_exports: Array[Export.FileFormat] = [ Export.FileFormat.PNG, @@ -31,7 +31,7 @@ var _preview_images: Array[Export.ProcessedImage] @onready var previews: GridContainer = $"%Previews" @onready var spritesheet_orientation: OptionButton = $"%Orientation" -@onready var spritesheet_lines_count: SpinBox = $"%LinesCount" +@onready var spritesheet_lines_count: ValueSlider = $"%LinesCount" @onready var spritesheet_lines_count_label: Label = $"%LinesCountLabel" @onready var frames_option_button: OptionButton = $"%Frames" @@ -51,6 +51,7 @@ var _preview_images: Array[Export.ProcessedImage] @onready var export_progress_popup: Window = $ExportProgressBar @onready var export_progress_bar := %ProgressBar as ProgressBar @onready var frame_timer: Timer = $FrameTimer +@onready var spritesheet_update_timer: Timer = %SpritesheetUpdateTimer func _ready() -> void: @@ -84,7 +85,8 @@ func show_tab() -> void: ) Export.ExportTab.SPRITESHEET: frame_timer.stop() - Export.process_spritesheet() + spritesheet_info_cache.clear() + Export.process_spritesheet(Global.current_project, spritesheet_info_cache) spritesheet_orientation.selected = Export.orientation spritesheet_lines_count.max_value = Export.number_of_frames spritesheet_lines_count.value = Export.lines_count @@ -121,6 +123,7 @@ func add_image_preview(image: Image, canvas_number: int = -1) -> void: var container := create_preview_container() var preview := create_preview_rect() preview.texture = ImageTexture.create_from_image(image) + preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE container.add_child(preview) if canvas_number != -1: @@ -145,6 +148,7 @@ func add_animated_preview() -> void: var preview := create_preview_rect() preview.name = "Preview" preview.texture = preview_frames[preview_current_frame] + preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE container.add_child(preview) previews.add_child(container) @@ -305,9 +309,8 @@ func _on_Orientation_item_selected(id: Export.Orientation) -> void: Export.orientation = id _handle_orientation_ui() spritesheet_lines_count.value = Export.frames_divided_by_spritesheet_lines() - Export.process_spritesheet() - update_dimensions_label() - set_preview() + ## Due to the above line, we don't have to process the spritesheet again + ## the value_changed signal will do this for us. func _handle_orientation_ui() -> void: @@ -326,9 +329,8 @@ func _handle_orientation_ui() -> void: func _on_LinesCount_value_changed(value: float) -> void: Export.lines_count = value - Export.process_spritesheet() - update_dimensions_label() - set_preview() + ## Check if spritesheet needs to be updated (This is required when orientation gets changed) + spritesheet_update_timer.start() func _on_Direction_item_selected(id: Export.AnimationDirection) -> void: @@ -478,3 +480,9 @@ func _on_Layers_item_selected(id: int) -> void: func _on_SeparatorCharacter_text_changed(new_text: String) -> void: Export.separator_character = new_text + + +func _on_spritesheet_update_timer_timeout() -> void: + Export.process_spritesheet(Global.current_project, spritesheet_info_cache) + update_dimensions_label() + set_preview() diff --git a/src/UI/Dialogs/ExportDialog.tscn b/src/UI/Dialogs/ExportDialog.tscn index b61e2446e..d16ae0b47 100644 --- a/src/UI/Dialogs/ExportDialog.tscn +++ b/src/UI/Dialogs/ExportDialog.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=5 format=3 uid="uid://clgu8wb5o6oup"] +[gd_scene load_steps=6 format=3 uid="uid://clgu8wb5o6oup"] [ext_resource type="Script" path="res://src/UI/Dialogs/ExportDialog.gd" id="1"] [ext_resource type="PackedScene" uid="uid://3pmb60gpst7b" path="res://src/UI/Nodes/TransparentChecker.tscn" id="2"] [ext_resource type="Script" path="res://src/UI/Nodes/CollapsibleContainer.gd" id="3"] +[ext_resource type="PackedScene" uid="uid://yjhp0ssng2mp" path="res://src/UI/Nodes/ValueSlider.tscn" id="3_u6gtq"] [ext_resource type="Script" path="res://src/UI/Nodes/ValueSlider.gd" id="4"] [node name="ExportDialog" type="ConfirmationDialog"] @@ -84,6 +85,7 @@ item_count = 4 popup/item_0/text = "Columns" popup/item_0/id = 1 popup/item_1/text = "Rows" +popup/item_1/id = 1 popup/item_2/text = "Tags by column" popup/item_2/id = 2 popup/item_3/text = "Tags by row" @@ -94,11 +96,12 @@ unique_name_in_owner = true layout_mode = 2 text = "Columns:" -[node name="LinesCount" type="SpinBox" parent="VBoxContainer/VSplitContainer/VBoxContainer/GridContainer" groups=["ExportSpritesheetOptions"]] +[node name="LinesCount" parent="VBoxContainer/VSplitContainer/VBoxContainer/GridContainer" groups=["ExportSpritesheetOptions"] instance=ExtResource("3_u6gtq")] unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) layout_mode = 2 -size_flags_horizontal = 3 -mouse_default_cursor_shape = 2 +focus_mode = 0 +theme_type_variation = &"" min_value = 1.0 max_value = 1000.0 value = 1.0 @@ -358,6 +361,11 @@ size_flags_horizontal = 3 [node name="FrameTimer" type="Timer" parent="."] +[node name="SpritesheetUpdateTimer" type="Timer" parent="."] +unique_name_in_owner = true +wait_time = 0.2 +one_shot = true + [connection signal="about_to_popup" from="." to="." method="_on_ExportDialog_about_to_show"] [connection signal="confirmed" from="." to="." method="_on_ExportDialog_confirmed"] [connection signal="tab_clicked" from="VBoxContainer/TabBar" to="." method="_on_Tabs_tab_clicked"] @@ -384,3 +392,4 @@ size_flags_horizontal = 3 [connection signal="confirmed" from="FileExistsAlert" to="." method="_on_FileExistsAlert_confirmed"] [connection signal="custom_action" from="FileExistsAlert" to="." method="_on_FileExistsAlert_custom_action"] [connection signal="timeout" from="FrameTimer" to="." method="_on_FrameTimer_timeout"] +[connection signal="timeout" from="SpritesheetUpdateTimer" to="." method="_on_spritesheet_update_timer_timeout"]