1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-02-07 10:59:49 +00:00
Pixelorama/src/Autoload/OpenSave.gd
Emmanouil Papadeas f84f15b8ae Experiment with Steam achievements, using a new SteamManager class
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.
2024-07-22 03:11:29 +03:00

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()