mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-01-31 15:39:49 +00:00
550 lines
17 KiB
GDScript
550 lines
17 KiB
GDScript
extends Node
|
|
|
|
signal palette_selected(palette_name: String)
|
|
signal new_palette_imported
|
|
|
|
enum SortOptions { NEW_PALETTE, REVERSE, HUE, SATURATION, VALUE, RED, GREEN, BLUE, ALPHA }
|
|
## Presets for creating a new palette
|
|
enum NewPalettePresetType {
|
|
EMPTY = 0, FROM_CURRENT_PALETTE = 1, FROM_CURRENT_SPRITE = 2, FROM_CURRENT_SELECTION = 3
|
|
}
|
|
## Color options when user creates a new palette from current sprite or selection
|
|
enum GetColorsFrom { CURRENT_FRAME = 0, CURRENT_CEL = 1, ALL_FRAMES = 2 }
|
|
const DEFAULT_PALETTE_NAME := "Default"
|
|
var palettes_write_path := Global.home_data_directory.path_join("Palettes")
|
|
## All available palettes
|
|
var palettes := {}
|
|
## Currently displayed palette
|
|
var current_palette: Palette = null
|
|
|
|
# Indexes of colors that are selected in palette
|
|
# by left and right mouse button
|
|
var left_selected_color := -1
|
|
var right_selected_color := -1
|
|
|
|
|
|
func _ready() -> void:
|
|
_load_palettes()
|
|
|
|
|
|
func does_palette_exist(palette_name: String) -> bool:
|
|
for name_to_test: String in palettes.keys():
|
|
if name_to_test == palette_name:
|
|
return true
|
|
return false
|
|
|
|
|
|
func select_palette(palette_name: String) -> void:
|
|
current_palette = palettes.get(palette_name)
|
|
_clear_selected_colors()
|
|
Global.config_cache.set_value("data", "last_palette", current_palette.name)
|
|
palette_selected.emit(palette_name)
|
|
|
|
|
|
func is_any_palette_selected() -> bool:
|
|
if is_instance_valid(current_palette):
|
|
return true
|
|
return false
|
|
|
|
|
|
func _ensure_palette_directory_exists() -> void:
|
|
var dir := DirAccess.open(Global.home_data_directory)
|
|
if is_instance_valid(dir) and not dir.dir_exists(palettes_write_path):
|
|
dir.make_dir(palettes_write_path)
|
|
|
|
|
|
func _save_palette(palette: Palette = current_palette) -> void:
|
|
_ensure_palette_directory_exists()
|
|
if not is_instance_valid(palette):
|
|
return
|
|
var old_name := palette.path.get_basename().get_file()
|
|
# If the palette's name has changed, remove the old palette file
|
|
if old_name != palette.name:
|
|
DirAccess.remove_absolute(palette.path)
|
|
palettes.erase(old_name)
|
|
|
|
# Save palette
|
|
var save_path := palettes_write_path.path_join(palette.name) + ".json"
|
|
palette.path = save_path
|
|
var err := palette.save_to_file()
|
|
if err != OK:
|
|
Global.notification_label("Failed to save palette")
|
|
|
|
|
|
func copy_palette() -> void:
|
|
var new_palette_name := current_palette.name
|
|
while does_palette_exist(new_palette_name):
|
|
new_palette_name += " copy"
|
|
var comment := current_palette.comment
|
|
_create_new_palette_from_current_palette(new_palette_name, comment)
|
|
|
|
|
|
func create_new_palette(
|
|
preset: int,
|
|
palette_name: String,
|
|
comment: String,
|
|
width: int,
|
|
height: int,
|
|
add_alpha_colors: bool,
|
|
get_colors_from: int
|
|
) -> void:
|
|
_check_palette_settings_values(palette_name, width, height)
|
|
match preset:
|
|
NewPalettePresetType.EMPTY:
|
|
_create_new_empty_palette(palette_name, comment, width, height)
|
|
NewPalettePresetType.FROM_CURRENT_PALETTE:
|
|
_create_new_palette_from_current_palette(palette_name, comment)
|
|
NewPalettePresetType.FROM_CURRENT_SPRITE:
|
|
_create_new_palette_from_current_sprite(
|
|
palette_name, comment, width, height, add_alpha_colors, get_colors_from
|
|
)
|
|
NewPalettePresetType.FROM_CURRENT_SELECTION:
|
|
_create_new_palette_from_current_selection(
|
|
palette_name, comment, width, height, add_alpha_colors, get_colors_from
|
|
)
|
|
|
|
|
|
func _create_new_empty_palette(
|
|
palette_name: String, comment: String, width: int, height: int
|
|
) -> void:
|
|
var new_palette := Palette.new(palette_name, width, height, comment)
|
|
_save_palette(new_palette)
|
|
palettes[palette_name] = new_palette
|
|
select_palette(palette_name)
|
|
|
|
|
|
func _create_new_palette_from_current_palette(palette_name: String, comment: String) -> void:
|
|
if !current_palette:
|
|
return
|
|
var new_palette := current_palette.duplicate()
|
|
new_palette.name = palette_name
|
|
new_palette.comment = comment
|
|
new_palette.path = palettes_write_path.path_join(new_palette.name) + ".json"
|
|
_save_palette(new_palette)
|
|
palettes[palette_name] = new_palette
|
|
select_palette(palette_name)
|
|
|
|
|
|
func _create_new_palette_from_current_selection(
|
|
palette_name: String,
|
|
comment: String,
|
|
width: int,
|
|
height: int,
|
|
add_alpha_colors: bool,
|
|
get_colors_from: int
|
|
) -> void:
|
|
var new_palette := Palette.new(palette_name, width, height, comment)
|
|
var current_project := Global.current_project
|
|
var pixels: Array[Vector2i] = []
|
|
for x in current_project.size.x:
|
|
for y in current_project.size.y:
|
|
var pos := Vector2i(x, y)
|
|
if current_project.selection_map.is_pixel_selected(pos):
|
|
pixels.append(pos)
|
|
_fill_new_palette_with_colors(pixels, new_palette, add_alpha_colors, get_colors_from)
|
|
|
|
|
|
func _create_new_palette_from_current_sprite(
|
|
palette_name: String,
|
|
comment: String,
|
|
width: int,
|
|
height: int,
|
|
add_alpha_colors: bool,
|
|
get_colors_from: int
|
|
) -> void:
|
|
var new_palette := Palette.new(palette_name, width, height, comment)
|
|
var current_project := Global.current_project
|
|
var pixels: Array[Vector2i] = []
|
|
for x in current_project.size.x:
|
|
for y in current_project.size.y:
|
|
pixels.append(Vector2i(x, y))
|
|
_fill_new_palette_with_colors(pixels, new_palette, add_alpha_colors, get_colors_from)
|
|
|
|
|
|
## Fills [param new_palette] with the colors of the [param pixels] of the current sprite.
|
|
## Used when creating a new palette from the UI.
|
|
func _fill_new_palette_with_colors(
|
|
pixels: Array[Vector2i], new_palette: Palette, add_alpha_colors: bool, get_colors_from: int
|
|
) -> void:
|
|
var current_project := Global.current_project
|
|
var cels: Array[BaseCel] = []
|
|
match get_colors_from:
|
|
GetColorsFrom.CURRENT_CEL:
|
|
for cel_index in current_project.selected_cels:
|
|
var cel := current_project.frames[cel_index[0]].cels[cel_index[1]]
|
|
cels.append(cel)
|
|
GetColorsFrom.CURRENT_FRAME:
|
|
for cel in current_project.frames[current_project.current_frame].cels:
|
|
cels.append(cel)
|
|
GetColorsFrom.ALL_FRAMES:
|
|
for frame in current_project.frames:
|
|
for cel in frame.cels:
|
|
cels.append(cel)
|
|
|
|
for cel in cels:
|
|
var cel_image := Image.new()
|
|
cel_image.copy_from(cel.get_image())
|
|
if cel_image.is_invisible():
|
|
continue
|
|
for i in pixels:
|
|
var color := cel_image.get_pixelv(i)
|
|
if color.a > 0:
|
|
if not add_alpha_colors:
|
|
color.a = 1
|
|
if not new_palette.has_theme_color(color):
|
|
new_palette.add_color(color)
|
|
|
|
_save_palette(new_palette)
|
|
palettes[new_palette.name] = new_palette
|
|
select_palette(new_palette.name)
|
|
|
|
|
|
func current_palette_edit(palette_name: String, comment: String, width: int, height: int) -> void:
|
|
_check_palette_settings_values(palette_name, width, height)
|
|
current_palette.edit(palette_name, width, height, comment)
|
|
_save_palette()
|
|
palettes[palette_name] = current_palette
|
|
|
|
|
|
func _delete_palette(palette: Palette, permanent := true) -> void:
|
|
if not palette.path.is_empty():
|
|
if permanent:
|
|
DirAccess.remove_absolute(palette.path)
|
|
else:
|
|
OS.move_to_trash(palette.path)
|
|
palettes.erase(palette.name)
|
|
|
|
|
|
func current_palete_delete(permanent := true) -> void:
|
|
_delete_palette(current_palette, permanent)
|
|
|
|
if palettes.size() > 0:
|
|
select_palette(palettes.keys()[0])
|
|
else:
|
|
current_palette = null
|
|
|
|
|
|
func current_palette_add_color(mouse_button: int, start_index := 0) -> void:
|
|
if (
|
|
not current_palette.is_full()
|
|
and (mouse_button == MOUSE_BUTTON_LEFT or mouse_button == MOUSE_BUTTON_RIGHT)
|
|
):
|
|
# Get color on left or right tool
|
|
var color := Tools.get_assigned_color(mouse_button)
|
|
current_palette.add_color(color, start_index)
|
|
_save_palette()
|
|
|
|
|
|
func current_palette_get_color(index: int) -> Color:
|
|
return current_palette.get_color(index)
|
|
|
|
|
|
func current_palette_set_color(index: int, color: Color) -> void:
|
|
current_palette.set_color(index, color)
|
|
_save_palette()
|
|
|
|
|
|
func current_palette_delete_color(index: int) -> void:
|
|
current_palette.remove_color(index)
|
|
_save_palette()
|
|
|
|
|
|
func current_palette_sort_colors(id: SortOptions) -> void:
|
|
if id == SortOptions.NEW_PALETTE:
|
|
return
|
|
if id == SortOptions.REVERSE:
|
|
current_palette.reverse_colors()
|
|
else:
|
|
current_palette.sort(id)
|
|
_save_palette()
|
|
|
|
|
|
func current_palette_swap_colors(source_index: int, target_index: int) -> void:
|
|
current_palette.swap_colors(source_index, target_index)
|
|
_select_color(MOUSE_BUTTON_LEFT, target_index)
|
|
_save_palette()
|
|
|
|
|
|
func current_palette_copy_colors(from: int, to: int) -> void:
|
|
current_palette.copy_colors(from, to)
|
|
_save_palette()
|
|
|
|
|
|
func current_palette_insert_color(from: int, to: int) -> void:
|
|
var from_color: Palette.PaletteColor = current_palette.colors[from]
|
|
current_palette.remove_color(from)
|
|
current_palette.insert_color(to, from_color.color)
|
|
_save_palette()
|
|
|
|
|
|
func current_palette_get_selected_color_index(mouse_button: int) -> int:
|
|
match mouse_button:
|
|
MOUSE_BUTTON_LEFT:
|
|
return left_selected_color
|
|
MOUSE_BUTTON_RIGHT:
|
|
return right_selected_color
|
|
_:
|
|
return -1
|
|
|
|
|
|
func current_palette_select_color(mouse_button: int, index: int) -> void:
|
|
var color := current_palette_get_color(index)
|
|
if color == null:
|
|
return
|
|
|
|
match mouse_button:
|
|
MOUSE_BUTTON_LEFT:
|
|
Tools.assign_color(color, mouse_button)
|
|
MOUSE_BUTTON_RIGHT:
|
|
Tools.assign_color(color, mouse_button)
|
|
|
|
_select_color(mouse_button, index)
|
|
|
|
|
|
func _select_color(mouse_button: int, index: int) -> void:
|
|
match mouse_button:
|
|
MOUSE_BUTTON_LEFT:
|
|
left_selected_color = index
|
|
MOUSE_BUTTON_RIGHT:
|
|
right_selected_color = index
|
|
|
|
|
|
func _clear_selected_colors() -> void:
|
|
left_selected_color = -1
|
|
right_selected_color = -1
|
|
|
|
|
|
func current_palette_is_empty() -> bool:
|
|
return current_palette.is_empty()
|
|
|
|
|
|
func current_palette_is_full() -> bool:
|
|
return current_palette.is_full()
|
|
|
|
|
|
func _check_palette_settings_values(palette_name: String, width: int, height: int) -> bool:
|
|
# Just in case. These values should be not allowed in gui.
|
|
if palette_name.length() <= 0 or width <= 0 or height <= 0:
|
|
printerr("Palette width, height and name length must be greater than 0!")
|
|
return false
|
|
return true
|
|
|
|
|
|
func _load_palettes() -> void:
|
|
_ensure_palette_directory_exists()
|
|
var search_locations := Global.path_join_array(Global.data_directories, "Palettes")
|
|
var priority_ordered_files := _get_palette_priority_file_map(search_locations)
|
|
|
|
# Iterate backwards, so any palettes defined in default files
|
|
# get overwritten by those of the same name in user files
|
|
search_locations.reverse()
|
|
priority_ordered_files.reverse()
|
|
for i in range(search_locations.size()):
|
|
# If palette is not in palettes write path - make its copy in the write path
|
|
var make_copy := false
|
|
if search_locations[i] != palettes_write_path:
|
|
make_copy = true
|
|
|
|
var base_directory := search_locations[i]
|
|
var palette_files := priority_ordered_files[i]
|
|
for file_name in palette_files:
|
|
var path := base_directory.path_join(file_name)
|
|
import_palette_from_path(path, make_copy, true)
|
|
|
|
if not current_palette && palettes.size() > 0:
|
|
select_palette(palettes.keys()[0])
|
|
|
|
|
|
## This returns an array of arrays, with priorities.
|
|
## In particular, it takes an array of paths to look for
|
|
## arrays in, in order of file and palette override priority
|
|
## such that the files in the first directory override the second, third, etc.
|
|
## It returns an array of arrays, where each output array
|
|
## corresponds to the given input array at the same index, and
|
|
## contains the (relative to the given directory) palette files
|
|
## to load, excluding all ones already existing in higher-priority directories.
|
|
## This also means you can run backwards on the result
|
|
## so that palettes with the given palette name in the higher priority
|
|
## directories override those set in lower priority directories.
|
|
func _get_palette_priority_file_map(looking_paths: PackedStringArray) -> Array[PackedStringArray]:
|
|
var final_list: Array[PackedStringArray] = []
|
|
# Holds pattern files already found
|
|
var working_file_set: Dictionary = {}
|
|
for search_directory in looking_paths:
|
|
var to_add_files: PackedStringArray = []
|
|
var files := _get_palette_files(search_directory)
|
|
# files to check
|
|
for maybe_to_add in files:
|
|
if not maybe_to_add in working_file_set:
|
|
to_add_files.append(maybe_to_add)
|
|
working_file_set[maybe_to_add] = true
|
|
|
|
final_list.append(to_add_files)
|
|
return final_list
|
|
|
|
|
|
## Get the palette files in a single directory.
|
|
## if it does not exist, return []
|
|
func _get_palette_files(path: String) -> PackedStringArray:
|
|
var dir := DirAccess.open(path)
|
|
var results: PackedStringArray = []
|
|
|
|
if not is_instance_valid(dir) or not dir.dir_exists(path):
|
|
return []
|
|
|
|
dir.list_dir_begin()
|
|
|
|
while true:
|
|
var file_name := dir.get_next()
|
|
if file_name == "":
|
|
break
|
|
elif (
|
|
(not file_name.begins_with("."))
|
|
&& file_name.to_lower().ends_with("json")
|
|
&& not dir.current_is_dir()
|
|
):
|
|
results.append(file_name)
|
|
|
|
dir.list_dir_end()
|
|
return results
|
|
|
|
|
|
func import_palette_from_path(path: String, make_copy := false, is_initialising := false) -> void:
|
|
if does_palette_exist(path.get_basename().get_file()):
|
|
# If there is a palette with same name ignore import for now
|
|
return
|
|
|
|
var palette: Palette = null
|
|
match path.to_lower().get_extension():
|
|
"gpl":
|
|
if FileAccess.file_exists(path):
|
|
var text := FileAccess.open(path, FileAccess.READ).get_as_text()
|
|
palette = _import_gpl(path, text)
|
|
"pal":
|
|
if FileAccess.file_exists(path):
|
|
var text := FileAccess.open(path, FileAccess.READ).get_as_text()
|
|
palette = _import_pal_palette(path, text)
|
|
"png", "bmp", "hdr", "jpg", "jpeg", "svg", "tga", "webp":
|
|
var image := Image.new()
|
|
var err := image.load(path)
|
|
if !err:
|
|
palette = _import_image_palette(path, image)
|
|
"json":
|
|
if FileAccess.file_exists(path):
|
|
var text := FileAccess.open(path, FileAccess.READ).get_as_text()
|
|
palette = Palette.new(path.get_basename().get_file())
|
|
palette.path = path
|
|
palette.deserialize(text)
|
|
|
|
if is_instance_valid(palette):
|
|
if make_copy:
|
|
_save_palette(palette) # Makes a copy of the palette
|
|
palettes[palette.name] = palette
|
|
var default_palette_name: String = Global.config_cache.get_value(
|
|
"data", "last_palette", DEFAULT_PALETTE_NAME
|
|
)
|
|
if is_initialising:
|
|
# Store index of the default palette
|
|
if palette.name == default_palette_name:
|
|
select_palette(palette.name)
|
|
else:
|
|
new_palette_imported.emit()
|
|
select_palette(palette.name)
|
|
else:
|
|
Global.popup_error(tr("Can't load file '%s'.\nThis is not a valid palette file.") % [path])
|
|
|
|
|
|
## Refer to app/core/gimppalette-load.c of the GNU Image Manipulation Program for the "living spec"
|
|
func _import_gpl(path: String, text: String) -> Palette:
|
|
var result: Palette = null
|
|
var lines := text.split("\n")
|
|
var line_number := 0
|
|
var palette_name := path.get_basename().get_file()
|
|
var comments := ""
|
|
var colors := PackedColorArray()
|
|
|
|
for line in lines:
|
|
# Check if the file is a valid palette
|
|
if line_number == 0:
|
|
if not "GIMP Palette" in line:
|
|
return result
|
|
|
|
# Comments
|
|
if line.begins_with("#"):
|
|
comments += line.trim_prefix("#") + "\n"
|
|
# Some programs output palette name in a comment for old format
|
|
if line.begins_with("#Palette Name: "):
|
|
palette_name = line.replace("#Palette Name: ", "")
|
|
elif line.begins_with("Name: "):
|
|
palette_name = line.replace("Name: ", "")
|
|
elif line.begins_with("Columns: "):
|
|
# Number of colors in this palette. Unnecessary and often wrong
|
|
continue
|
|
elif line_number > 0 && line.length() >= 9:
|
|
line = line.replace("\t", " ")
|
|
var color_data: PackedStringArray = line.split(" ", false, 4)
|
|
var red: float = color_data[0].to_float() / 255.0
|
|
var green: float = color_data[1].to_float() / 255.0
|
|
var blue: float = color_data[2].to_float() / 255.0
|
|
var color := Color(red, green, blue)
|
|
if color_data.size() >= 4:
|
|
# Ignore color name for now - result.add_color(color, color_data[3])
|
|
colors.append(color)
|
|
else:
|
|
colors.append(color)
|
|
line_number += 1
|
|
|
|
if line_number > 0:
|
|
return _fill_imported_palette_with_colors(palette_name, colors, comments)
|
|
return result
|
|
|
|
|
|
func _import_pal_palette(path: String, text: String) -> Palette:
|
|
var result: Palette = null
|
|
var colors := PackedColorArray()
|
|
var lines := text.split("\n")
|
|
|
|
if not "JASC-PAL" in lines[0] or not "0100" in lines[1]:
|
|
return result
|
|
|
|
var num_colors := int(lines[2])
|
|
|
|
for i in range(3, num_colors + 3):
|
|
var color_data := lines[i].split(" ")
|
|
var red := color_data[0].to_float() / 255.0
|
|
var green := color_data[1].to_float() / 255.0
|
|
var blue := color_data[2].to_float() / 255.0
|
|
|
|
var color := Color(red, green, blue)
|
|
colors.append(color)
|
|
|
|
return _fill_imported_palette_with_colors(path.get_basename().get_file(), colors)
|
|
|
|
|
|
func _import_image_palette(path: String, image: Image) -> Palette:
|
|
var colors: PackedColorArray = []
|
|
var height := image.get_height()
|
|
var width := image.get_width()
|
|
|
|
# Iterate all pixels and store unique colors to palette
|
|
for y in range(0, height):
|
|
for x in range(0, width):
|
|
var color := image.get_pixel(x, y)
|
|
if !colors.has(color):
|
|
colors.append(color)
|
|
|
|
return _fill_imported_palette_with_colors(path.get_basename().get_file(), colors)
|
|
|
|
|
|
## Fills a new [Palette] with colors. Used when importing files.
|
|
## TODO: Somehow let the users choose the fixed height or width, instead of hardcoding 8.
|
|
func _fill_imported_palette_with_colors(
|
|
palette_name: String, colors: PackedColorArray, comment := ""
|
|
) -> Palette:
|
|
var height := ceili(colors.size() / 8.0)
|
|
var result := Palette.new(palette_name, 8, height, comment)
|
|
for color in colors:
|
|
result.add_color(color)
|
|
|
|
return result
|