diff --git a/src/Autoload/Import.gd b/src/Autoload/Import.gd index 42606453cf87..41815700a9b7 100644 --- a/src/Autoload/Import.gd +++ b/src/Autoload/Import.gd @@ -222,3 +222,198 @@ func import_patterns(priority_ordered_search_path: Array) -> void: image.convert(Image.FORMAT_RGBA8) var tooltip_name = pattern.get_basename() Global.patterns_popup.add(image, tooltip_name) + + +## Gets frame [member indices] of [member from_project] and dumps it in the current project. +func copy_frames_to_current_project( + from_project: Project, indices: Array, destination: int, new_tag_from: AnimationTag = null +) -> void: + var project: Project = Global.current_project + if !project: + return + if from_project == project: ## If we are copying tags within project + Global.animation_timeline.copy_frames(indices, destination, true, new_tag_from) + return + var new_animation_tags := project.animation_tags.duplicate() + # Loop through the tags to create new classes for them, so that they won't be the same + # as project.animation_tags's classes. Needed for undo/redo to work properly. + for i in new_animation_tags.size(): + new_animation_tags[i] = AnimationTag.new( + new_animation_tags[i].name, + new_animation_tags[i].color, + new_animation_tags[i].from, + new_animation_tags[i].to + ) + var imported_frames: Array[Frame] = [] # The copied frames + # the indices of newly copied frames + var copied_indices: PackedInt32Array = range( + destination + 1, (destination + 1) + indices.size() + ) + project.undo_redo.create_action("Import Frames") + # Step 1: calculate layers to generate + var layer_to_names := PackedStringArray() # names of currently existing layers + for l in project.layers: + layer_to_names.append(l.name) + + # the goal of this section is to mark existing layers with their indices else with -1 + var layer_from_to := {} # indices of layers from and to + for from in from_project.layers.size(): + var to := -1 + var pos := 0 + for i in layer_to_names.count(from_project.layers[from].name): + pos = layer_to_names.find(from_project.layers[from].name, pos) + # if layer types don't match, the destination is invalid. + if project.layers[pos].get_layer_type() != from_project.layers[from].get_layer_type(): + # Don't give up if there is another layer with the same name, check that one as well + pos += 1 + continue + # if destination is already assigned to another layer, then don't use it here. + if pos in layer_from_to.values(): + # Don't give up if there is another layer with the same name, check that one as well + pos += 1 + continue + to = pos + break + layer_from_to[from] = to + + # Step 2: generate required layers + var combined_copy := Array() # Makes calculations easy (contains preview of final layer order). + combined_copy.append_array(project.layers) + var added_layers := Array() # Array of layers + # Array of indices to add the respective layers (in added_layers) to + var added_idx := PackedInt32Array() + var added_cels := Array() # Array of an Array of cels (added in same order as their layer) + + # Create destinations for layers that don't have one yet + if layer_from_to.values().count(-1) > 0: + # As it is extracted from a dictionary, so i assume the keys aren't sorted + var from_layers_size = layer_from_to.keys().duplicate(true) + from_layers_size.sort() # it's values should now be from (layer size - 1) to zero + for i in from_layers_size: + if layer_from_to[i] == -1: + var from_layer := from_project.layers[i] + var type = from_layer.get_layer_type() + var l: BaseLayer + match type: + Global.LayerTypes.PIXEL: + l = PixelLayer.new(project) + Global.LayerTypes.GROUP: + l = GroupLayer.new(project) + Global.LayerTypes.THREE_D: + l = Layer3D.new(project) + Global.LayerTypes.TILEMAP: + l = LayerTileMap.new(project, from_layer.tileset) + l.place_only_mode = from_layer.place_only_mode + l.tile_size = from_layer.tile_size + l.tile_shape = from_layer.tile_shape + l.tile_layout = from_layer.tile_layout + l.tile_offset_axis = from_layer.tile_offset_axis + Global.LayerTypes.AUDIO: + l = AudioLayer.new(project) + l.audio = from_layer.audio + if l == null: # Ignore copying this layer if it isn't supported + continue + var cels := [] + for f in project.frames: + cels.append(l.new_empty_cel()) + l.name = from_project.layers[i].name # this will set it to the required layer name + + # Set an appropriate parent + var new_layer_idx = combined_copy.size() + layer_from_to[i] = new_layer_idx + var from_children = from_project.layers[i].get_children(false) + for from_child in from_children: # If this layer had children + var child_to_idx = layer_from_to[from_project.layers.find(from_child)] + var to_child = combined_copy[child_to_idx] + if to_child in added_layers: # if child was added recently + to_child.parent = l + + combined_copy.insert(new_layer_idx, l) + added_layers.append(l) # layer is now added + added_idx.append(new_layer_idx) # at index new_layer_idx + added_cels.append(cels) # with cels + + # Now initiate import + for f in indices: + var src_frame: Frame = from_project.frames[f] + var new_frame := Frame.new() + imported_frames.append(new_frame) + new_frame.duration = src_frame.duration + for to in combined_copy.size(): + var new_cel: BaseCel + if to in layer_from_to.values(): # We have data to Import to this layer index + var from = layer_from_to.find_key(to) + # Cel we're copying from, the source + var src_cel: BaseCel = from_project.frames[f].cels[from] + new_cel = src_cel.duplicate_cel() + if src_cel is Cel3D: + new_cel.size_changed(project.size) + elif src_cel is CelTileMap: + var copied_content := src_cel.copy_content() as Array + var src_img: ImageExtended = copied_content[0] + var empty := project.new_empty_image() + var copy := ImageExtended.new() + copy.copy_from_custom(empty, project.is_indexed()) + copy.blit_rect(src_img, Rect2(Vector2.ZERO, src_img.get_size()), Vector2.ZERO) + new_cel.set_content([copy, copied_content[1]]) + new_cel.set_indexed_mode(project.is_indexed()) + else: + # Add more types here if they have a copy_content() method. + if src_cel is PixelCel: + var src_img: ImageExtended = src_cel.copy_content() + var empty := project.new_empty_image() + var copy := ImageExtended.new() + copy.copy_from_custom(empty, project.is_indexed()) + copy.blit_rect( + src_img, Rect2(Vector2.ZERO, src_img.get_size()), Vector2.ZERO + ) + new_cel.set_content(copy) + new_cel.set_indexed_mode(project.is_indexed()) + + else: + new_cel = combined_copy[to].new_empty_cel() + new_frame.cels.append(new_cel) + + for tag in new_animation_tags: # Loop through the tags to see if the frame is in one + if copied_indices[0] >= tag.from && copied_indices[0] <= tag.to: + tag.to += 1 + elif copied_indices[0] < tag.from: + tag.from += 1 + tag.to += 1 + if new_tag_from: + new_animation_tags.append( + AnimationTag.new( + new_tag_from.name, new_tag_from.color, copied_indices[0] + 1, copied_indices[-1] + 1 + ) + ) + project.undo_redo.add_undo_method(project.remove_frames.bind(copied_indices)) + project.undo_redo.add_do_method(project.add_layers.bind(added_layers, added_idx, added_cels)) + project.undo_redo.add_do_method(project.add_frames.bind(imported_frames, copied_indices)) + project.undo_redo.add_undo_method(project.remove_layers.bind(added_idx)) + # Note: temporarily set the selected cels to an empty array (needed for undo/redo) + project.undo_redo.add_do_property(Global.current_project, "selected_cels", []) + project.undo_redo.add_undo_property(Global.current_project, "selected_cels", []) + + var all_new_cels := [] + # Select all the new frames so that it is easier to move/offset collectively if user wants + # To ease animation workflow, new current frame is the first copied frame instead of the last + var range_start: int = copied_indices[-1] + var range_end: int = copied_indices[0] + var frame_diff_sign := signi(range_end - range_start) + if frame_diff_sign == 0: + frame_diff_sign = 1 + for i in range(range_start, range_end + frame_diff_sign, frame_diff_sign): + for j in range(0, combined_copy.size()): + var frame_layer := [i, j] + if !all_new_cels.has(frame_layer): + all_new_cels.append(frame_layer) + project.undo_redo.add_do_property(Global.current_project, "selected_cels", all_new_cels) + project.undo_redo.add_undo_method( + project.change_cel.bind(project.current_frame, project.current_layer) + ) + project.undo_redo.add_do_method(project.change_cel.bind(range_end)) + project.undo_redo.add_do_property(project, "animation_tags", new_animation_tags) + project.undo_redo.add_undo_property(project, "animation_tags", project.animation_tags) + project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) + project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true)) + project.undo_redo.commit_action() diff --git a/src/UI/Dialogs/ImportTagDialog.gd b/src/UI/Dialogs/ImportTagDialog.gd index 957a5dea9885..af19f1bb5274 100644 --- a/src/UI/Dialogs/ImportTagDialog.gd +++ b/src/UI/Dialogs/ImportTagDialog.gd @@ -61,13 +61,13 @@ func _on_FromProject_changed(id: int) -> void: func _on_confirmed() -> void: var tag: AnimationTag = from_project.animation_tags[tag_id] - var frames := [] + var from_frames := [] for i in range(tag.from - 1, tag.to): - frames.append(i) + from_frames.append(i) if create_new_tags: - add_animation(frames, frame, tag) + Import.copy_frames_to_current_project(from_project, from_frames, frame, tag) else: - add_animation(frames, frame) + Import.copy_frames_to_current_project(from_project, from_frames, frame) func _on_TagList_id_pressed(id: int) -> void: @@ -80,196 +80,5 @@ func _on_TagList_empty_clicked(_at_position: Vector2, _mouse_button_index: int) get_ok_button().disabled = true -## Gets frame indices of [member from_project] and dumps it in the current project. -func add_animation(indices: Array, destination: int, from_tag: AnimationTag = null) -> void: - var project: Project = Global.current_project - if from_project == project: ## If we are copying tags within project - Global.animation_timeline.copy_frames(indices, destination, true, from_tag) - return - var new_animation_tags := project.animation_tags.duplicate() - # Loop through the tags to create new classes for them, so that they won't be the same - # as project.animation_tags's classes. Needed for undo/redo to work properly. - for i in new_animation_tags.size(): - new_animation_tags[i] = AnimationTag.new( - new_animation_tags[i].name, - new_animation_tags[i].color, - new_animation_tags[i].from, - new_animation_tags[i].to - ) - var imported_frames: Array[Frame] = [] # The copied frames - # the indices of newly copied frames - var copied_indices: PackedInt32Array = range( - destination + 1, (destination + 1) + indices.size() - ) - project.undo_redo.create_action("Import Tag") - # Step 1: calculate layers to generate - var layer_to_names := PackedStringArray() # names of currently existing layers - for l in project.layers: - layer_to_names.append(l.name) - - # the goal of this section is to mark existing layers with their indices else with -1 - var layer_from_to := {} # indices of layers from and to - for from in from_project.layers.size(): - var to := -1 - var pos := 0 - for i in layer_to_names.count(from_project.layers[from].name): - pos = layer_to_names.find(from_project.layers[from].name, pos) - # if layer types don't match, the destination is invalid. - if project.layers[pos].get_layer_type() != from_project.layers[from].get_layer_type(): - # Don't give up if there is another layer with the same name, check that one as well - pos += 1 - continue - # if destination is already assigned to another layer, then don't use it here. - if pos in layer_from_to.values(): - # Don't give up if there is another layer with the same name, check that one as well - pos += 1 - continue - to = pos - break - layer_from_to[from] = to - - # Step 2: generate required layers - var combined_copy := Array() # Makes calculations easy (contains preview of final layer order). - combined_copy.append_array(project.layers) - var added_layers := Array() # Array of layers - # Array of indices to add the respective layers (in added_layers) to - var added_idx := PackedInt32Array() - var added_cels := Array() # Array of an Array of cels (added in same order as their layer) - - # Create destinations for layers that don't have one yet - if layer_from_to.values().count(-1) > 0: - # As it is extracted from a dictionary, so i assume the keys aren't sorted - var from_layers_size = layer_from_to.keys().duplicate(true) - from_layers_size.sort() # it's values should now be from (layer size - 1) to zero - for i in from_layers_size: - if layer_from_to[i] == -1: - var from_layer := from_project.layers[i] - var type = from_layer.get_layer_type() - var l: BaseLayer - match type: - Global.LayerTypes.PIXEL: - l = PixelLayer.new(project) - Global.LayerTypes.GROUP: - l = GroupLayer.new(project) - Global.LayerTypes.THREE_D: - l = Layer3D.new(project) - Global.LayerTypes.TILEMAP: - l = LayerTileMap.new(project, from_layer.tileset) - l.place_only_mode = from_layer.place_only_mode - l.tile_size = from_layer.tile_size - l.tile_shape = from_layer.tile_shape - l.tile_layout = from_layer.tile_layout - l.tile_offset_axis = from_layer.tile_offset_axis - Global.LayerTypes.AUDIO: - l = AudioLayer.new(project) - l.audio = from_layer.audio - if l == null: # Ignore copying this layer if it isn't supported - continue - var cels := [] - for f in project.frames: - cels.append(l.new_empty_cel()) - l.name = from_project.layers[i].name # this will set it to the required layer name - - # Set an appropriate parent - var new_layer_idx = combined_copy.size() - layer_from_to[i] = new_layer_idx - var from_children = from_project.layers[i].get_children(false) - for from_child in from_children: # If this layer had children - var child_to_idx = layer_from_to[from_project.layers.find(from_child)] - var to_child = combined_copy[child_to_idx] - if to_child in added_layers: # if child was added recently - to_child.parent = l - - combined_copy.insert(new_layer_idx, l) - added_layers.append(l) # layer is now added - added_idx.append(new_layer_idx) # at index new_layer_idx - added_cels.append(cels) # with cels - - # Now initiate import - for f in indices: - var src_frame: Frame = from_project.frames[f] - var new_frame := Frame.new() - imported_frames.append(new_frame) - new_frame.duration = src_frame.duration - for to in combined_copy.size(): - var new_cel: BaseCel - if to in layer_from_to.values(): # We have data to Import to this layer index - var from = layer_from_to.find_key(to) - # Cel we're copying from, the source - var src_cel: BaseCel = from_project.frames[f].cels[from] - new_cel = src_cel.duplicate_cel() - if src_cel is Cel3D: - new_cel.size_changed(project.size) - elif src_cel is CelTileMap: - var copied_content := src_cel.copy_content() as Array - var src_img: ImageExtended = copied_content[0] - var empty := project.new_empty_image() - var copy := ImageExtended.new() - copy.copy_from_custom(empty, project.is_indexed()) - copy.blit_rect(src_img, Rect2(Vector2.ZERO, src_img.get_size()), Vector2.ZERO) - new_cel.set_content([copy, copied_content[1]]) - new_cel.set_indexed_mode(project.is_indexed()) - else: - # Add more types here if they have a copy_content() method. - if src_cel is PixelCel: - var src_img: ImageExtended = src_cel.copy_content() - var empty := project.new_empty_image() - var copy := ImageExtended.new() - copy.copy_from_custom(empty, project.is_indexed()) - copy.blit_rect( - src_img, Rect2(Vector2.ZERO, src_img.get_size()), Vector2.ZERO - ) - new_cel.set_content(copy) - new_cel.set_indexed_mode(project.is_indexed()) - - else: - new_cel = combined_copy[to].new_empty_cel() - new_frame.cels.append(new_cel) - - for tag in new_animation_tags: # Loop through the tags to see if the frame is in one - if copied_indices[0] >= tag.from && copied_indices[0] <= tag.to: - tag.to += 1 - elif copied_indices[0] < tag.from: - tag.from += 1 - tag.to += 1 - if from_tag: - new_animation_tags.append( - AnimationTag.new( - from_tag.name, from_tag.color, copied_indices[0] + 1, copied_indices[-1] + 1 - ) - ) - project.undo_redo.add_undo_method(project.remove_frames.bind(copied_indices)) - project.undo_redo.add_do_method(project.add_layers.bind(added_layers, added_idx, added_cels)) - project.undo_redo.add_do_method(project.add_frames.bind(imported_frames, copied_indices)) - project.undo_redo.add_undo_method(project.remove_layers.bind(added_idx)) - # Note: temporarily set the selected cels to an empty array (needed for undo/redo) - project.undo_redo.add_do_property(Global.current_project, "selected_cels", []) - project.undo_redo.add_undo_property(Global.current_project, "selected_cels", []) - - var all_new_cels := [] - # Select all the new frames so that it is easier to move/offset collectively if user wants - # To ease animation workflow, new current frame is the first copied frame instead of the last - var range_start: int = copied_indices[-1] - var range_end: int = copied_indices[0] - var frame_diff_sign := signi(range_end - range_start) - if frame_diff_sign == 0: - frame_diff_sign = 1 - for i in range(range_start, range_end + frame_diff_sign, frame_diff_sign): - for j in range(0, combined_copy.size()): - var frame_layer := [i, j] - if !all_new_cels.has(frame_layer): - all_new_cels.append(frame_layer) - project.undo_redo.add_do_property(Global.current_project, "selected_cels", all_new_cels) - project.undo_redo.add_undo_method( - project.change_cel.bind(project.current_frame, project.current_layer) - ) - project.undo_redo.add_do_method(project.change_cel.bind(range_end)) - project.undo_redo.add_do_property(project, "animation_tags", new_animation_tags) - project.undo_redo.add_undo_property(project, "animation_tags", project.animation_tags) - project.undo_redo.add_do_method(Global.undo_or_redo.bind(false)) - project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true)) - project.undo_redo.commit_action() - - func _on_close_requested() -> void: hide() diff --git a/src/UI/Tabs.gd b/src/UI/Tabs.gd index 811d4ab89100..099521ab0f1b 100644 --- a/src/UI/Tabs.gd +++ b/src/UI/Tabs.gd @@ -78,3 +78,15 @@ func delete_tab(tab: int) -> void: Global.current_project_index -= 1 if unsaved_changes_dialog.confirmed.is_connected(delete_tab): unsaved_changes_dialog.confirmed.disconnect(delete_tab) + + +func _can_drop_data(pos: Vector2, data) -> bool: + if typeof(data) != TYPE_ARRAY: + return false + if data[0] != "Frame": + return false + var hover_idx := get_tab_idx_at_point(pos) + if hover_idx >= 0 and hover_idx < Global.projects.size(): + if hover_idx != current_tab: + current_tab = hover_idx + return false diff --git a/src/UI/Timeline/FrameButton.gd b/src/UI/Timeline/FrameButton.gd index 542747820c34..dfcc271b0700 100644 --- a/src/UI/Timeline/FrameButton.gd +++ b/src/UI/Timeline/FrameButton.gd @@ -118,7 +118,7 @@ func _get_drag_data(_position: Vector2) -> Variant: button.text = text set_drag_preview(button) - return ["Frame", _get_frame_indices()] + return ["Frame", _get_frame_indices(), Global.current_project] func _can_drop_data(pos: Vector2, data) -> bool: @@ -140,20 +140,22 @@ func _can_drop_data(pos: Vector2, data) -> bool: frame_container.get_child(get_index() - 1) ) - var is_swapping := Input.is_action_pressed("ctrl") var drop_frames: PackedInt32Array = data[1] + var drop_project: Project = data[2] + var is_swapping := Input.is_action_pressed("ctrl") and drop_project == Global.current_project # Get offset var offset: int = 0 if drop_frames.size() > 0: offset = frame - Array(drop_frames).min() - # Can't move to same frame - for drop_frame in drop_frames: - var is_not_valid = ( - drop_frames.has(drop_frame + offset) if is_swapping else drop_frames.has(frame) - ) - if is_not_valid: - Global.animation_timeline.drag_highlight.visible = false - return false + if drop_project == Global.current_project: + # Can't move to same frame + for drop_frame in drop_frames: + var is_not_valid = ( + drop_frames.has(drop_frame + offset) if is_swapping else drop_frames.has(frame) + ) + if is_not_valid: + Global.animation_timeline.drag_highlight.visible = false + return false var region: Rect2 if is_swapping: # Swap frames var copy_drop_frames := drop_frames.duplicate() # to prevent overriting original array. @@ -170,10 +172,13 @@ func _can_drop_data(pos: Vector2, data) -> bool: func _drop_data(_pos: Vector2, data) -> void: + var drop_project: Project = data[2] var drop_frames: PackedInt32Array = data[1] var project := Global.current_project - project.undo_redo.create_action("Change Frame Order") - if Input.is_action_pressed("ctrl"): # Swap frames + if project == drop_project: # we only need to create action for changes within project + project.undo_redo.create_action("Change Frame Order") + var is_swapping := Input.is_action_pressed("ctrl") and drop_project == Global.current_project + if is_swapping: # Swap frames var swap_frame_positions := [] # Get offset var offset: int = 0 @@ -188,15 +193,19 @@ func _drop_data(_pos: Vector2, data) -> void: project.undo_redo.add_do_method(project.swap_frame.bind(drop_point, drop_frame)) project.undo_redo.add_undo_method(project.swap_frame.bind(drop_point, drop_frame)) project.undo_redo.add_do_property(project, "selected_cels", swap_frame_positions) - else: # Move frames + else: # Move frames (or copy if different project) var to_frame: int if _get_region_rect(0, 0.5).has_point(get_global_mouse_position()): # Left to_frame = frame else: # Right to_frame = frame + 1 - for drop_frame in drop_frames: - if drop_frame < frame: - to_frame -= 1 + if drop_project != Global.current_project: + Import.copy_frames_to_current_project(drop_project, drop_frames, to_frame - 1) + return # No need to do anything further + else: + for drop_frame in drop_frames: + if drop_frame < frame: + to_frame -= 1 var to_frames := PackedInt32Array(range(to_frame, to_frame + drop_frames.size())) if drop_frames != to_frames: # Code to RECALCULATE tags due to frame movement