mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-02-07 10:59:49 +00:00
f84f15b8ae
This has no effect on non-Steam builds. Steam achievements are mostly for fun, but can also be educational because they can let users know of certain features and functionalities. It's using the GodotSteam GDExtension, but because I do not want to bloat the GitHub repository with things that are not needed for most builds, I decided not to include the GDExtension files, and instead check if the `Steam` class exists in `ClassDB`. The new SteamManager class pretty much does nothing on non-Steam builds, so do not worry about bloat. In the future we could even take advantage of more of Steam's features, such as Cloud storage for pxo files.
867 lines
32 KiB
GDScript
867 lines
32 KiB
GDScript
# gdlint: ignore=max-public-methods
|
|
extends Node
|
|
|
|
signal project_saved
|
|
signal reference_image_imported
|
|
|
|
var preview_dialog_tscn := preload("res://src/UI/Dialogs/ImportPreviewDialog.tscn")
|
|
var preview_dialogs := [] ## Array of preview dialogs
|
|
var last_dialog_option := 0
|
|
var autosave_timer: Timer
|
|
|
|
# custom importer related dictionaries (received from extensions)
|
|
var custom_import_names := {} ## Contains importer names as keys and ids as values
|
|
var custom_importer_scenes := {} ## Contains ids keys and import option preloads as values
|
|
|
|
|
|
func _ready() -> void:
|
|
autosave_timer = Timer.new()
|
|
autosave_timer.one_shot = false
|
|
autosave_timer.timeout.connect(_on_Autosave_timeout)
|
|
add_child(autosave_timer)
|
|
update_autosave()
|
|
|
|
|
|
func handle_loading_file(file: String) -> void:
|
|
file = file.replace("\\", "/")
|
|
var file_ext := file.get_extension().to_lower()
|
|
if file_ext == "pxo": # Pixelorama project file
|
|
open_pxo_file(file)
|
|
|
|
elif file_ext == "tres": # Godot resource file
|
|
return
|
|
elif file_ext == "tscn": # Godot scene file
|
|
return
|
|
|
|
elif file_ext == "gpl" or file_ext == "pal" or file_ext == "json":
|
|
Palettes.import_palette_from_path(file, true)
|
|
|
|
elif file_ext in ["pck", "zip"]: # Godot resource pack file
|
|
Global.control.get_node("Extensions").install_extension(file)
|
|
|
|
elif file_ext == "shader" or file_ext == "gdshader": # Godot shader file
|
|
var shader := load(file)
|
|
if not shader is Shader:
|
|
return
|
|
var file_name: String = file.get_file().get_basename()
|
|
Global.control.find_child("ShaderEffect").change_shader(shader, file_name)
|
|
|
|
else: # Image files
|
|
# Attempt to load as APNG.
|
|
# Note that the APNG importer will *only* succeed for *animated* PNGs.
|
|
# This is intentional as still images should still act normally.
|
|
var apng_res := AImgIOAPNGImporter.load_from_file(file)
|
|
if apng_res[0] == null:
|
|
# No error - this is an APNG!
|
|
if typeof(apng_res[1]) == TYPE_ARRAY:
|
|
handle_loading_aimg(file, apng_res[1])
|
|
return
|
|
# Attempt to load as a regular image.
|
|
var image := Image.load_from_file(file)
|
|
if not is_instance_valid(image): # Failed to import as image
|
|
if handle_loading_video(file):
|
|
return # Succeeded in loading as video, so return early before the error appears
|
|
var file_name: String = file.get_file()
|
|
Global.popup_error(tr("Can't load file '%s'.") % [file_name])
|
|
return
|
|
handle_loading_image(file, image)
|
|
|
|
|
|
func add_import_option(import_name: StringName, import_scene: PackedScene) -> int:
|
|
# Change format name if another one uses the same name
|
|
var existing_format_names := (
|
|
ImportPreviewDialog.ImageImportOptions.keys() + custom_import_names.keys()
|
|
)
|
|
for i in range(existing_format_names.size()):
|
|
var test_name := import_name
|
|
if i != 0:
|
|
test_name = str(test_name, "_", i)
|
|
if !existing_format_names.has(test_name):
|
|
import_name = test_name
|
|
break
|
|
|
|
# Obtain a unique id
|
|
var id := ImportPreviewDialog.ImageImportOptions.size()
|
|
for i in custom_import_names.size():
|
|
var format_id := id + i
|
|
if !custom_import_names.values().has(i):
|
|
id = format_id
|
|
# Add to custom_file_formats
|
|
custom_import_names.merge({import_name: id})
|
|
custom_importer_scenes.merge({id: import_scene})
|
|
return id
|
|
|
|
|
|
## Mostly used for downloading images from the Internet. Tries multiple file extensions
|
|
## in case the extension of the file is wrong, which is common for images on the Internet.
|
|
func load_image_from_buffer(buffer: PackedByteArray) -> Image:
|
|
var image := Image.new()
|
|
var err := image.load_png_from_buffer(buffer)
|
|
if err != OK:
|
|
err = image.load_jpg_from_buffer(buffer)
|
|
if err != OK:
|
|
err = image.load_webp_from_buffer(buffer)
|
|
if err != OK:
|
|
err = image.load_tga_from_buffer(buffer)
|
|
if err != OK:
|
|
image.load_bmp_from_buffer(buffer)
|
|
return image
|
|
|
|
|
|
func handle_loading_image(file: String, image: Image) -> void:
|
|
if Global.projects.size() <= 1 and Global.current_project.is_empty():
|
|
open_image_as_new_tab(file, image)
|
|
return
|
|
var preview_dialog := preview_dialog_tscn.instantiate() as ImportPreviewDialog
|
|
# add custom importers to preview dialog
|
|
for import_name in custom_import_names.keys():
|
|
var id = custom_import_names[import_name]
|
|
var new_import_option = custom_importer_scenes[id].instantiate()
|
|
preview_dialog.custom_importers[id] = new_import_option
|
|
preview_dialogs.append(preview_dialog)
|
|
preview_dialog.path = file
|
|
preview_dialog.image = image
|
|
Global.control.add_child(preview_dialog)
|
|
preview_dialog.popup_centered()
|
|
Global.dialog_open(true)
|
|
|
|
|
|
## For loading the output of AImgIO as a project
|
|
func handle_loading_aimg(path: String, frames: Array) -> void:
|
|
var project := Project.new([], path.get_file(), frames[0].content.get_size())
|
|
project.layers.append(PixelLayer.new(project))
|
|
Global.projects.append(project)
|
|
|
|
# Determine FPS as 1, unless all frames agree.
|
|
project.fps = 1
|
|
var first_duration: float = frames[0].duration
|
|
var frames_agree := true
|
|
for v in frames:
|
|
var aimg_frame: AImgIOFrame = v
|
|
if aimg_frame.duration != first_duration:
|
|
frames_agree = false
|
|
break
|
|
if frames_agree and (first_duration > 0.0):
|
|
project.fps = 1.0 / first_duration
|
|
# Convert AImgIO frames to Pixelorama frames
|
|
for v in frames:
|
|
var aimg_frame: AImgIOFrame = v
|
|
var frame := Frame.new()
|
|
if not frames_agree:
|
|
frame.duration = aimg_frame.duration * project.fps
|
|
var content := aimg_frame.content
|
|
content.convert(Image.FORMAT_RGBA8)
|
|
frame.cels.append(PixelCel.new(content, 1))
|
|
project.frames.append(frame)
|
|
|
|
set_new_imported_tab(project, path)
|
|
|
|
|
|
## Uses FFMPEG to attempt to load a video file as a new project. Works by splitting the video file
|
|
## to multiple png images for each of the video's frames,
|
|
## and then it imports these images as frames of a new project.
|
|
## TODO: Don't allow large files (how large?) to be imported, to avoid crashes due to lack of memory
|
|
## TODO: Find the video's fps and use that for the new project.
|
|
func handle_loading_video(file: String) -> bool:
|
|
DirAccess.make_dir_absolute(Export.TEMP_PATH)
|
|
var temp_path_real := ProjectSettings.globalize_path(Export.TEMP_PATH)
|
|
var output_file_path := temp_path_real.path_join("%04d.png")
|
|
# ffmpeg -y -i input_file %04d.png
|
|
var ffmpeg_execute: PackedStringArray = ["-y", "-i", file, output_file_path]
|
|
var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, [], true)
|
|
if success < 0 or success > 1: # FFMPEG is probably not installed correctly
|
|
DirAccess.remove_absolute(Export.TEMP_PATH)
|
|
return false
|
|
var images_to_import: Array[Image] = []
|
|
var project_size := Vector2i.ZERO
|
|
var temp_dir := DirAccess.open(Export.TEMP_PATH)
|
|
for temp_file in temp_dir.get_files():
|
|
var temp_image := Image.load_from_file(Export.TEMP_PATH.path_join(temp_file))
|
|
temp_dir.remove(temp_file)
|
|
if not is_instance_valid(temp_image):
|
|
continue
|
|
images_to_import.append(temp_image)
|
|
if temp_image.get_width() > project_size.x:
|
|
project_size.x = temp_image.get_width()
|
|
if temp_image.get_height() > project_size.y:
|
|
project_size.y = temp_image.get_height()
|
|
DirAccess.remove_absolute(Export.TEMP_PATH)
|
|
if images_to_import.size() == 0 or project_size == Vector2i.ZERO:
|
|
return false # We didn't find any images, return
|
|
# If we found images, create a new project out of them
|
|
var new_project := Project.new([], file.get_basename().get_file(), project_size)
|
|
new_project.layers.append(PixelLayer.new(new_project))
|
|
for temp_image in images_to_import:
|
|
open_image_as_new_frame(temp_image, 0, new_project, false)
|
|
Global.projects.append(new_project)
|
|
Global.tabs.current_tab = Global.tabs.get_tab_count() - 1
|
|
Global.canvas.camera_zoom()
|
|
return true
|
|
|
|
|
|
func open_pxo_file(path: String, is_backup := false, replace_empty := true) -> void:
|
|
var empty_project := Global.current_project.is_empty() and replace_empty
|
|
var new_project: Project
|
|
var zip_reader := ZIPReader.new()
|
|
var err := zip_reader.open(path)
|
|
if err == FAILED:
|
|
# Most likely uses the old pxo format, load that
|
|
new_project = open_v0_pxo_file(path, empty_project)
|
|
if not is_instance_valid(new_project):
|
|
return
|
|
elif err != OK:
|
|
Global.popup_error(tr("File failed to open. Error code %s (%s)") % [err, error_string(err)])
|
|
return
|
|
else:
|
|
if empty_project:
|
|
new_project = Global.current_project
|
|
new_project.frames = []
|
|
new_project.layers = []
|
|
new_project.animation_tags.clear()
|
|
new_project.name = path.get_file().get_basename()
|
|
else:
|
|
new_project = Project.new([], path.get_file().get_basename())
|
|
var data_json := zip_reader.read_file("data.json").get_string_from_utf8()
|
|
var test_json_conv := JSON.new()
|
|
var error := test_json_conv.parse(data_json)
|
|
if error != OK:
|
|
print("Error, corrupt pxo file. Error code %s (%s)" % [error, error_string(error)])
|
|
zip_reader.close()
|
|
return
|
|
var result = test_json_conv.get_data()
|
|
if typeof(result) != TYPE_DICTIONARY:
|
|
print("Error, json parsed result is: %s" % typeof(result))
|
|
zip_reader.close()
|
|
return
|
|
|
|
new_project.deserialize(result, zip_reader)
|
|
if result.has("brushes"):
|
|
var brush_index := 0
|
|
for brush in result.brushes:
|
|
var b_width: int = brush.size_x
|
|
var b_height: int = brush.size_y
|
|
var image_data := zip_reader.read_file("image_data/brushes/brush_%s" % brush_index)
|
|
var image := Image.create_from_data(
|
|
b_width, b_height, false, Image.FORMAT_RGBA8, image_data
|
|
)
|
|
new_project.brushes.append(image)
|
|
Brushes.add_project_brush(image)
|
|
brush_index += 1
|
|
if result.has("tile_mask") and result.has("has_mask"):
|
|
if result.has_mask:
|
|
var t_width = result.tile_mask.size_x
|
|
var t_height = result.tile_mask.size_y
|
|
var image_data := zip_reader.read_file("image_data/tile_map")
|
|
var image := Image.create_from_data(
|
|
t_width, t_height, false, Image.FORMAT_RGBA8, image_data
|
|
)
|
|
new_project.tiles.tile_mask = image
|
|
else:
|
|
new_project.tiles.reset_mask()
|
|
zip_reader.close()
|
|
new_project.export_directory_path = path.get_base_dir()
|
|
|
|
if empty_project:
|
|
new_project.change_project()
|
|
Global.project_switched.emit()
|
|
Global.cel_switched.emit()
|
|
else:
|
|
Global.projects.append(new_project)
|
|
Global.tabs.current_tab = Global.tabs.get_tab_count() - 1
|
|
Global.canvas.camera_zoom()
|
|
|
|
if is_backup:
|
|
new_project.backup_path = path
|
|
else:
|
|
# Loading a backup should not change window title and save path
|
|
new_project.save_path = path
|
|
get_window().title = new_project.name + " - Pixelorama " + Global.current_version
|
|
Global.save_sprites_dialog.current_path = path
|
|
# Set last opened project path and save
|
|
Global.config_cache.set_value("data", "current_dir", path.get_base_dir())
|
|
Global.config_cache.set_value("data", "last_project_path", path)
|
|
Global.config_cache.save(Global.CONFIG_PATH)
|
|
new_project.file_name = path.get_file().trim_suffix(".pxo")
|
|
new_project.was_exported = false
|
|
Global.top_menu_container.file_menu.set_item_text(
|
|
Global.FileMenu.SAVE, tr("Save") + " %s" % path.get_file()
|
|
)
|
|
Global.top_menu_container.file_menu.set_item_text(Global.FileMenu.EXPORT, tr("Export"))
|
|
|
|
save_project_to_recent_list(path)
|
|
|
|
|
|
func open_v0_pxo_file(path: String, empty_project: bool) -> Project:
|
|
var file := FileAccess.open_compressed(path, FileAccess.READ, FileAccess.COMPRESSION_ZSTD)
|
|
if FileAccess.get_open_error() == ERR_FILE_UNRECOGNIZED:
|
|
# If the file is not compressed open it raw (pre-v0.7)
|
|
file = FileAccess.open(path, FileAccess.READ)
|
|
var err := FileAccess.get_open_error()
|
|
if err != OK:
|
|
Global.popup_error(tr("File failed to open. Error code %s (%s)") % [err, error_string(err)])
|
|
return null
|
|
|
|
var first_line := file.get_line()
|
|
var test_json_conv := JSON.new()
|
|
var error := test_json_conv.parse(first_line)
|
|
if error != OK:
|
|
print("Error, corrupt legacy pxo file. Error code %s (%s)" % [error, error_string(error)])
|
|
file.close()
|
|
return null
|
|
|
|
var result = test_json_conv.get_data()
|
|
if typeof(result) != TYPE_DICTIONARY:
|
|
print("Error, json parsed result is: %s" % typeof(result))
|
|
file.close()
|
|
return null
|
|
|
|
var new_project: Project
|
|
if empty_project:
|
|
new_project = Global.current_project
|
|
new_project.frames = []
|
|
new_project.layers = []
|
|
new_project.animation_tags.clear()
|
|
new_project.name = path.get_file().get_basename()
|
|
else:
|
|
new_project = Project.new([], path.get_file().get_basename())
|
|
new_project.deserialize(result, null, file)
|
|
if result.has("brushes"):
|
|
for brush in result.brushes:
|
|
var b_width = brush.size_x
|
|
var b_height = brush.size_y
|
|
var buffer := file.get_buffer(b_width * b_height * 4)
|
|
var image := Image.create_from_data(
|
|
b_width, b_height, false, Image.FORMAT_RGBA8, buffer
|
|
)
|
|
new_project.brushes.append(image)
|
|
Brushes.add_project_brush(image)
|
|
|
|
if result.has("tile_mask") and result.has("has_mask"):
|
|
if result.has_mask:
|
|
var t_width = result.tile_mask.size_x
|
|
var t_height = result.tile_mask.size_y
|
|
var buffer := file.get_buffer(t_width * t_height * 4)
|
|
var image := Image.create_from_data(
|
|
t_width, t_height, false, Image.FORMAT_RGBA8, buffer
|
|
)
|
|
new_project.tiles.tile_mask = image
|
|
else:
|
|
new_project.tiles.reset_mask()
|
|
file.close()
|
|
return new_project
|
|
|
|
|
|
func save_pxo_file(
|
|
path: String, autosave: bool, include_blended := false, project := Global.current_project
|
|
) -> bool:
|
|
if not autosave:
|
|
project.name = path.get_file().trim_suffix(".pxo")
|
|
var serialized_data := project.serialize()
|
|
if not serialized_data:
|
|
Global.popup_error(tr("File failed to save. Converting project data to dictionary failed."))
|
|
return false
|
|
var to_save := JSON.stringify(serialized_data)
|
|
if not to_save:
|
|
Global.popup_error(tr("File failed to save. Converting dictionary to JSON failed."))
|
|
return false
|
|
|
|
# Check if a file with the same name exists. If it does, rename the new file temporarily.
|
|
# Needed in case of a crash, so that the old file won't be replaced with an empty one.
|
|
var temp_path := path
|
|
if FileAccess.file_exists(path):
|
|
temp_path = path + "1"
|
|
|
|
var zip_packer := ZIPPacker.new()
|
|
var err := zip_packer.open(temp_path)
|
|
if err != OK:
|
|
if temp_path.is_valid_filename():
|
|
return false
|
|
Global.popup_error(tr("File failed to save. Error code %s (%s)") % [err, error_string(err)])
|
|
if zip_packer: # this would be null if we attempt to save filenames such as "//\\||.pxo"
|
|
zip_packer.close()
|
|
return false
|
|
zip_packer.start_file("data.json")
|
|
zip_packer.write_file(to_save.to_utf8_buffer())
|
|
zip_packer.close_file()
|
|
|
|
if not autosave:
|
|
project.save_path = path
|
|
|
|
var frame_index := 1
|
|
for frame in project.frames:
|
|
if not autosave and include_blended:
|
|
var blended := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
|
|
DrawingAlgos.blend_layers(blended, frame, Vector2i.ZERO, project)
|
|
zip_packer.start_file("image_data/final_images/%s" % frame_index)
|
|
zip_packer.write_file(blended.get_data())
|
|
zip_packer.close_file()
|
|
var cel_index := 1
|
|
for cel in frame.cels:
|
|
var cel_image := cel.get_image()
|
|
if is_instance_valid(cel_image) and cel is PixelCel:
|
|
zip_packer.start_file("image_data/frames/%s/layer_%s" % [frame_index, cel_index])
|
|
zip_packer.write_file(cel_image.get_data())
|
|
zip_packer.close_file()
|
|
cel_index += 1
|
|
frame_index += 1
|
|
var brush_index := 0
|
|
for brush in project.brushes:
|
|
zip_packer.start_file("image_data/brushes/brush_%s" % brush_index)
|
|
zip_packer.write_file(brush.get_data())
|
|
zip_packer.close_file()
|
|
brush_index += 1
|
|
if project.tiles.has_mask:
|
|
zip_packer.start_file("image_data/tile_map")
|
|
zip_packer.write_file(project.tiles.tile_mask.get_data())
|
|
zip_packer.close_file()
|
|
zip_packer.close()
|
|
|
|
if temp_path != path:
|
|
# Rename the new file to its proper name and remove the old file, if it exists.
|
|
DirAccess.rename_absolute(temp_path, path)
|
|
|
|
if OS.has_feature("web") and not autosave:
|
|
var file := FileAccess.open(path, FileAccess.READ)
|
|
if FileAccess.get_open_error() == OK:
|
|
var file_data := file.get_buffer(file.get_length())
|
|
JavaScriptBridge.download_buffer(file_data, path.get_file())
|
|
file.close()
|
|
# Remove the .pxo file from memory, as we don't need it anymore
|
|
DirAccess.remove_absolute(path)
|
|
|
|
if autosave:
|
|
Global.notification_label("File autosaved")
|
|
else:
|
|
# First remove backup then set current save path
|
|
if project.has_changed:
|
|
project.has_changed = false
|
|
Global.current_project.remove_backup_file()
|
|
Global.notification_label("File saved")
|
|
get_window().title = project.name + " - Pixelorama " + Global.current_version
|
|
|
|
# Set last opened project path and save
|
|
Global.config_cache.set_value("data", "current_dir", path.get_base_dir())
|
|
Global.config_cache.set_value("data", "last_project_path", path)
|
|
Global.config_cache.save(Global.CONFIG_PATH)
|
|
if !project.was_exported:
|
|
project.file_name = path.get_file().trim_suffix(".pxo")
|
|
project.export_directory_path = path.get_base_dir()
|
|
Global.top_menu_container.file_menu.set_item_text(
|
|
Global.FileMenu.SAVE, tr("Save") + " %s" % path.get_file()
|
|
)
|
|
project_saved.emit()
|
|
SteamManager.set_achievement("ACH_SAVE")
|
|
|
|
save_project_to_recent_list(path)
|
|
return true
|
|
|
|
|
|
func open_image_as_new_tab(path: String, image: Image) -> void:
|
|
var project := Project.new([], path.get_file(), image.get_size())
|
|
project.layers.append(PixelLayer.new(project))
|
|
Global.projects.append(project)
|
|
|
|
var frame := Frame.new()
|
|
image.convert(Image.FORMAT_RGBA8)
|
|
frame.cels.append(PixelCel.new(image, 1))
|
|
|
|
project.frames.append(frame)
|
|
set_new_imported_tab(project, path)
|
|
|
|
|
|
func open_image_as_spritesheet_tab_smart(
|
|
path: String, image: Image, sliced_rects: Array[Rect2i], frame_size: Vector2i
|
|
) -> void:
|
|
if sliced_rects.size() == 0: # Image is empty sprite (manually set data to be consistent)
|
|
frame_size = image.get_size()
|
|
sliced_rects.append(Rect2i(Vector2i.ZERO, frame_size))
|
|
var project := Project.new([], path.get_file(), frame_size)
|
|
project.layers.append(PixelLayer.new(project))
|
|
Global.projects.append(project)
|
|
for rect in sliced_rects:
|
|
var offset: Vector2 = (0.5 * (frame_size - rect.size)).floor()
|
|
var frame := Frame.new()
|
|
var cropped_image := Image.create(frame_size.x, frame_size.y, false, Image.FORMAT_RGBA8)
|
|
image.convert(Image.FORMAT_RGBA8)
|
|
cropped_image.blit_rect(image, rect, offset)
|
|
frame.cels.append(PixelCel.new(cropped_image, 1))
|
|
project.frames.append(frame)
|
|
set_new_imported_tab(project, path)
|
|
|
|
|
|
func open_image_as_spritesheet_tab(path: String, image: Image, horiz: int, vert: int) -> void:
|
|
horiz = mini(horiz, image.get_size().x)
|
|
vert = mini(vert, image.get_size().y)
|
|
var frame_width := image.get_size().x / horiz
|
|
var frame_height := image.get_size().y / vert
|
|
var project := Project.new([], path.get_file(), Vector2(frame_width, frame_height))
|
|
project.layers.append(PixelLayer.new(project))
|
|
Global.projects.append(project)
|
|
for yy in range(vert):
|
|
for xx in range(horiz):
|
|
var frame := Frame.new()
|
|
var cropped_image := image.get_region(
|
|
Rect2i(frame_width * xx, frame_height * yy, frame_width, frame_height)
|
|
)
|
|
project.size = cropped_image.get_size()
|
|
cropped_image.convert(Image.FORMAT_RGBA8)
|
|
frame.cels.append(PixelCel.new(cropped_image, 1))
|
|
project.frames.append(frame)
|
|
set_new_imported_tab(project, path)
|
|
|
|
|
|
func open_image_as_spritesheet_layer_smart(
|
|
_path: String,
|
|
image: Image,
|
|
file_name: String,
|
|
sliced_rects: Array[Rect2i],
|
|
start_frame: int,
|
|
frame_size: Vector2i
|
|
) -> void:
|
|
# Resize canvas to if "frame_size.x" or "frame_size.y" is too large
|
|
var project := Global.current_project
|
|
var project_width := maxi(frame_size.x, project.size.x)
|
|
var project_height := maxi(frame_size.y, project.size.y)
|
|
if project.size < Vector2i(project_width, project_height):
|
|
DrawingAlgos.resize_canvas(project_width, project_height, 0, 0)
|
|
|
|
# Initialize undo mechanism
|
|
project.undos += 1
|
|
project.undo_redo.create_action("Add Spritesheet Layer")
|
|
|
|
# Create new frames (if needed)
|
|
var new_frames_size := maxi(project.frames.size(), start_frame + sliced_rects.size())
|
|
var frames := []
|
|
var frame_indices := PackedInt32Array([])
|
|
if new_frames_size > project.frames.size():
|
|
var required_frames := new_frames_size - project.frames.size()
|
|
frame_indices = range(
|
|
project.current_frame + 1, project.current_frame + required_frames + 1
|
|
)
|
|
for i in required_frames:
|
|
var new_frame := Frame.new()
|
|
for l in range(project.layers.size()): # Create as many cels as there are layers
|
|
new_frame.cels.append(project.layers[l].new_empty_cel())
|
|
if project.layers[l].new_cels_linked:
|
|
var prev_cel := project.frames[project.current_frame].cels[l]
|
|
if prev_cel.link_set == null:
|
|
prev_cel.link_set = {}
|
|
project.undo_redo.add_do_method(
|
|
project.layers[l].link_cel.bind(prev_cel, prev_cel.link_set)
|
|
)
|
|
project.undo_redo.add_undo_method(
|
|
project.layers[l].link_cel.bind(prev_cel, null)
|
|
)
|
|
new_frame.cels[l].set_content(prev_cel.get_content(), prev_cel.image_texture)
|
|
new_frame.cels[l].link_set = prev_cel.link_set
|
|
frames.append(new_frame)
|
|
|
|
# Create new layer for spritesheet
|
|
var layer := PixelLayer.new(project, file_name)
|
|
var cels: Array[PixelCel] = []
|
|
for f in new_frames_size:
|
|
if f >= start_frame and f < (start_frame + sliced_rects.size()):
|
|
# Slice spritesheet
|
|
var offset: Vector2 = (0.5 * (frame_size - sliced_rects[f - start_frame].size)).floor()
|
|
image.convert(Image.FORMAT_RGBA8)
|
|
var cropped_image := Image.create(
|
|
project_width, project_height, false, Image.FORMAT_RGBA8
|
|
)
|
|
cropped_image.blit_rect(image, sliced_rects[f - start_frame], offset)
|
|
cels.append(PixelCel.new(cropped_image))
|
|
else:
|
|
cels.append(layer.new_empty_cel())
|
|
|
|
project.undo_redo.add_do_method(project.add_frames.bind(frames, frame_indices))
|
|
project.undo_redo.add_do_method(
|
|
project.add_layers.bind([layer], [project.layers.size()], [cels])
|
|
)
|
|
project.undo_redo.add_do_method(
|
|
project.change_cel.bind(new_frames_size - 1, project.layers.size())
|
|
)
|
|
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
|
|
|
|
project.undo_redo.add_undo_method(project.remove_layers.bind([project.layers.size()]))
|
|
project.undo_redo.add_undo_method(project.remove_frames.bind(frame_indices))
|
|
project.undo_redo.add_undo_method(
|
|
project.change_cel.bind(project.current_frame, project.current_layer)
|
|
)
|
|
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
|
|
project.undo_redo.commit_action()
|
|
|
|
|
|
func open_image_as_spritesheet_layer(
|
|
_path: String, image: Image, file_name: String, horizontal: int, vertical: int, start_frame: int
|
|
) -> void:
|
|
# Data needed to slice images
|
|
horizontal = mini(horizontal, image.get_size().x)
|
|
vertical = mini(vertical, image.get_size().y)
|
|
var frame_width := image.get_size().x / horizontal
|
|
var frame_height := image.get_size().y / vertical
|
|
|
|
# Resize canvas to if "frame_width" or "frame_height" is too large
|
|
var project := Global.current_project
|
|
var project_width := maxi(frame_width, project.size.x)
|
|
var project_height := maxi(frame_height, project.size.y)
|
|
if project.size < Vector2i(project_width, project_height):
|
|
DrawingAlgos.resize_canvas(project_width, project_height, 0, 0)
|
|
|
|
# Initialize undo mechanism
|
|
project.undos += 1
|
|
project.undo_redo.create_action("Add Spritesheet Layer")
|
|
|
|
# Create new frames (if needed)
|
|
var new_frames_size := maxi(project.frames.size(), start_frame + (vertical * horizontal))
|
|
var frames := []
|
|
var frame_indices := PackedInt32Array([])
|
|
if new_frames_size > project.frames.size():
|
|
var required_frames := new_frames_size - project.frames.size()
|
|
frame_indices = range(
|
|
project.current_frame + 1, project.current_frame + required_frames + 1
|
|
)
|
|
for i in required_frames:
|
|
var new_frame := Frame.new()
|
|
for l in range(project.layers.size()): # Create as many cels as there are layers
|
|
new_frame.cels.append(project.layers[l].new_empty_cel())
|
|
if project.layers[l].new_cels_linked:
|
|
var prev_cel := project.frames[project.current_frame].cels[l]
|
|
if prev_cel.link_set == null:
|
|
prev_cel.link_set = {}
|
|
project.undo_redo.add_do_method(
|
|
project.layers[l].link_cel.bind(prev_cel, prev_cel.link_set)
|
|
)
|
|
project.undo_redo.add_undo_method(
|
|
project.layers[l].link_cel.bind(prev_cel, null)
|
|
)
|
|
new_frame.cels[l].set_content(prev_cel.get_content(), prev_cel.image_texture)
|
|
new_frame.cels[l].link_set = prev_cel.link_set
|
|
frames.append(new_frame)
|
|
|
|
# Create new layer for spritesheet
|
|
var layer := PixelLayer.new(project, file_name)
|
|
var cels := []
|
|
for f in new_frames_size:
|
|
if f >= start_frame and f < (start_frame + (vertical * horizontal)):
|
|
# Slice spritesheet
|
|
var xx := (f - start_frame) % horizontal
|
|
var yy := (f - start_frame) / horizontal
|
|
image.convert(Image.FORMAT_RGBA8)
|
|
var cropped_image := Image.create(
|
|
project_width, project_height, false, Image.FORMAT_RGBA8
|
|
)
|
|
cropped_image.blit_rect(
|
|
image,
|
|
Rect2i(frame_width * xx, frame_height * yy, frame_width, frame_height),
|
|
Vector2i.ZERO
|
|
)
|
|
cels.append(PixelCel.new(cropped_image))
|
|
else:
|
|
cels.append(layer.new_empty_cel())
|
|
|
|
project.undo_redo.add_do_method(project.add_frames.bind(frames, frame_indices))
|
|
project.undo_redo.add_do_method(
|
|
project.add_layers.bind([layer], [project.layers.size()], [cels])
|
|
)
|
|
project.undo_redo.add_do_method(
|
|
project.change_cel.bind(new_frames_size - 1, project.layers.size())
|
|
)
|
|
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
|
|
|
|
project.undo_redo.add_undo_method(project.remove_layers.bind([project.layers.size()]))
|
|
project.undo_redo.add_undo_method(project.remove_frames.bind(frame_indices))
|
|
project.undo_redo.add_undo_method(
|
|
project.change_cel.bind(project.current_frame, project.current_layer)
|
|
)
|
|
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
|
|
project.undo_redo.commit_action()
|
|
|
|
|
|
func open_image_at_cel(image: Image, layer_index := 0, frame_index := 0) -> void:
|
|
var project := Global.current_project
|
|
var project_width := maxi(image.get_width(), project.size.x)
|
|
var project_height := maxi(image.get_height(), project.size.y)
|
|
if project.size < Vector2i(project_width, project_height):
|
|
DrawingAlgos.resize_canvas(project_width, project_height, 0, 0)
|
|
project.undos += 1
|
|
project.undo_redo.create_action("Replaced Cel")
|
|
|
|
var cel := project.frames[frame_index].cels[layer_index]
|
|
if not cel is PixelCel:
|
|
return
|
|
image.convert(Image.FORMAT_RGBA8)
|
|
var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8)
|
|
cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
|
|
Global.undo_redo_compress_images(
|
|
{cel.image: cel_image.data}, {cel.image: cel.image.data}, project
|
|
)
|
|
|
|
project.undo_redo.add_do_property(project, "selected_cels", [])
|
|
project.undo_redo.add_do_method(project.change_cel.bind(frame_index, layer_index))
|
|
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
|
|
|
|
project.undo_redo.add_undo_property(project, "selected_cels", [])
|
|
project.undo_redo.add_undo_method(
|
|
project.change_cel.bind(project.current_frame, project.current_layer)
|
|
)
|
|
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
|
|
project.undo_redo.commit_action()
|
|
|
|
|
|
func open_image_as_new_frame(
|
|
image: Image, layer_index := 0, project := Global.current_project, undo := true
|
|
) -> void:
|
|
var project_width := maxi(image.get_width(), project.size.x)
|
|
var project_height := maxi(image.get_height(), project.size.y)
|
|
if project.size < Vector2i(project_width, project_height):
|
|
DrawingAlgos.resize_canvas(project_width, project_height, 0, 0)
|
|
|
|
var frame := Frame.new()
|
|
for i in project.layers.size():
|
|
if i == layer_index:
|
|
image.convert(Image.FORMAT_RGBA8)
|
|
var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8)
|
|
cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
|
|
frame.cels.append(PixelCel.new(cel_image, 1))
|
|
else:
|
|
frame.cels.append(project.layers[i].new_empty_cel())
|
|
if not undo:
|
|
project.frames.append(frame)
|
|
return
|
|
project.undos += 1
|
|
project.undo_redo.create_action("Add Frame")
|
|
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
|
|
project.undo_redo.add_do_method(project.add_frames.bind([frame], [project.frames.size()]))
|
|
project.undo_redo.add_do_method(project.change_cel.bind(project.frames.size(), layer_index))
|
|
|
|
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
|
|
project.undo_redo.add_undo_method(project.remove_frames.bind([project.frames.size()]))
|
|
project.undo_redo.add_undo_method(
|
|
project.change_cel.bind(project.current_frame, project.current_layer)
|
|
)
|
|
project.undo_redo.commit_action()
|
|
|
|
|
|
func open_image_as_new_layer(image: Image, file_name: String, frame_index := 0) -> void:
|
|
var project := Global.current_project
|
|
var project_width := maxi(image.get_width(), project.size.x)
|
|
var project_height := maxi(image.get_height(), project.size.y)
|
|
if project.size < Vector2i(project_width, project_height):
|
|
DrawingAlgos.resize_canvas(project_width, project_height, 0, 0)
|
|
var layer := PixelLayer.new(project, file_name)
|
|
var cels := []
|
|
|
|
Global.current_project.undos += 1
|
|
Global.current_project.undo_redo.create_action("Add Layer")
|
|
for i in project.frames.size():
|
|
if i == frame_index:
|
|
image.convert(Image.FORMAT_RGBA8)
|
|
var cel_image := Image.create(project_width, project_height, false, Image.FORMAT_RGBA8)
|
|
cel_image.blit_rect(image, Rect2i(Vector2i.ZERO, image.get_size()), Vector2i.ZERO)
|
|
cels.append(PixelCel.new(cel_image, 1))
|
|
else:
|
|
cels.append(layer.new_empty_cel())
|
|
|
|
project.undo_redo.add_do_method(
|
|
project.add_layers.bind([layer], [project.layers.size()], [cels])
|
|
)
|
|
project.undo_redo.add_do_method(project.change_cel.bind(frame_index, project.layers.size()))
|
|
|
|
project.undo_redo.add_undo_method(project.remove_layers.bind([project.layers.size()]))
|
|
project.undo_redo.add_undo_method(
|
|
project.change_cel.bind(project.current_frame, project.current_layer)
|
|
)
|
|
|
|
project.undo_redo.add_undo_method(Global.undo_or_redo.bind(true))
|
|
project.undo_redo.add_do_method(Global.undo_or_redo.bind(false))
|
|
project.undo_redo.commit_action()
|
|
|
|
|
|
func import_reference_image_from_path(path: String) -> void:
|
|
var project := Global.current_project
|
|
var ri := ReferenceImage.new()
|
|
ri.project = project
|
|
ri.deserialize({"image_path": path})
|
|
Global.canvas.reference_image_container.add_child(ri)
|
|
reference_image_imported.emit()
|
|
|
|
|
|
## Useful for Web
|
|
func import_reference_image_from_image(image: Image) -> void:
|
|
var project := Global.current_project
|
|
var ri := ReferenceImage.new()
|
|
ri.project = project
|
|
ri.create_from_image(image)
|
|
Global.canvas.reference_image_container.add_child(ri)
|
|
reference_image_imported.emit()
|
|
|
|
|
|
func set_new_imported_tab(project: Project, path: String) -> void:
|
|
var prev_project_empty := Global.current_project.is_empty()
|
|
var prev_project_pos := Global.current_project_index
|
|
|
|
get_window().title = (
|
|
path.get_file() + " (" + tr("imported") + ") - Pixelorama " + Global.current_version
|
|
)
|
|
if project.has_changed:
|
|
get_window().title = get_window().title + "(*)"
|
|
var file_name := path.get_basename().get_file()
|
|
project.export_directory_path = path.get_base_dir()
|
|
project.file_name = file_name
|
|
project.was_exported = true
|
|
if path.get_extension().to_lower() == "png":
|
|
project.export_overwrite = true
|
|
|
|
Global.tabs.current_tab = Global.tabs.get_tab_count() - 1
|
|
Global.canvas.camera_zoom()
|
|
|
|
if prev_project_empty:
|
|
Global.tabs.delete_tab(prev_project_pos)
|
|
|
|
|
|
func update_autosave() -> void:
|
|
if not is_instance_valid(autosave_timer):
|
|
return
|
|
autosave_timer.stop()
|
|
# Interval parameter is in minutes, wait_time is seconds
|
|
autosave_timer.wait_time = Global.autosave_interval * 60
|
|
if Global.enable_autosave:
|
|
autosave_timer.start()
|
|
|
|
|
|
func _on_Autosave_timeout() -> void:
|
|
for i in Global.projects.size():
|
|
var project := Global.projects[i]
|
|
if project.backup_path.is_empty():
|
|
project.backup_path = (
|
|
"user://backups/backup-" + str(Time.get_unix_time_from_system()) + "-%s" % i
|
|
)
|
|
save_pxo_file(project.backup_path, true, false, project)
|
|
|
|
|
|
## Load the backup files
|
|
func reload_backup_file() -> void:
|
|
var dir := DirAccess.open("user://backups")
|
|
var i := 0
|
|
for file in dir.get_files():
|
|
open_pxo_file("user://backups".path_join(file), true, i == 0)
|
|
i += 1
|
|
Global.notification_label("Backup reloaded")
|
|
|
|
|
|
func save_project_to_recent_list(path: String) -> void:
|
|
var top_menu_container := Global.top_menu_container
|
|
if path.get_file().substr(0, 7) == "backup-" or path == "":
|
|
return
|
|
|
|
if top_menu_container.recent_projects.has(path):
|
|
top_menu_container.recent_projects.erase(path)
|
|
|
|
if top_menu_container.recent_projects.size() >= 5:
|
|
top_menu_container.recent_projects.pop_front()
|
|
top_menu_container.recent_projects.push_back(path)
|
|
|
|
Global.config_cache.set_value("data", "recent_projects", top_menu_container.recent_projects)
|
|
|
|
top_menu_container.recent_projects_submenu.clear()
|
|
top_menu_container.update_recent_projects_submenu()
|