mirror of
https://github.com/Orama-Interactive/Pixelorama.git
synced 2025-01-18 17:19:50 +00:00
APNG Exporter (#772)
* APNG: Initial refactorings of animation exporter internals * APNG: Make ExportDialog actually able to handle multiple file formats * APNG: Bugfix to FPS hint and such * APNG: Refactoring: Fix file format propagation * APNG: Make an "APNG exporter" which creates an empty PNG container This was the testbed of the previous integration commits. * APNG: The actual exporter! * APNG: Remove random src/Main.tscn changes * APNG: Format/lint * APNG: Format & Lint, part II
This commit is contained in:
parent
ac3d2baf87
commit
82acf3f8b1
|
@ -9,12 +9,22 @@
|
|||
config_version=4
|
||||
|
||||
_global_script_classes=[ {
|
||||
"base": "BaseAnimationExporter",
|
||||
"class": "APNGAnimationExporter",
|
||||
"language": "GDScript",
|
||||
"path": "res://src/Classes/AnimationExporters/APNGAnimationExporter.gd"
|
||||
}, {
|
||||
"base": "Reference",
|
||||
"class": "AnimationTag",
|
||||
"language": "GDScript",
|
||||
"path": "res://src/Classes/AnimationTag.gd"
|
||||
}, {
|
||||
"base": "Reference",
|
||||
"class": "BaseAnimationExporter",
|
||||
"language": "GDScript",
|
||||
"path": "res://src/Classes/AnimationExporters/BaseAnimationExporter.gd"
|
||||
}, {
|
||||
"base": "Reference",
|
||||
"class": "BaseCel",
|
||||
"language": "GDScript",
|
||||
"path": "res://src/Classes/BaseCel.gd"
|
||||
|
@ -49,6 +59,11 @@ _global_script_classes=[ {
|
|||
"language": "GDScript",
|
||||
"path": "res://src/Classes/Frame.gd"
|
||||
}, {
|
||||
"base": "BaseAnimationExporter",
|
||||
"class": "GIFAnimationExporter",
|
||||
"language": "GDScript",
|
||||
"path": "res://src/Classes/AnimationExporters/GIFAnimationExporter.gd"
|
||||
}, {
|
||||
"base": "Control",
|
||||
"class": "GradientEditNode",
|
||||
"language": "GDScript",
|
||||
|
@ -160,7 +175,9 @@ _global_script_classes=[ {
|
|||
"path": "res://src/UI/Nodes/ValueSlider.gd"
|
||||
} ]
|
||||
_global_script_class_icons={
|
||||
"APNGAnimationExporter": "",
|
||||
"AnimationTag": "",
|
||||
"BaseAnimationExporter": "",
|
||||
"BaseCel": "",
|
||||
"BaseLayer": "",
|
||||
"BaseTool": "",
|
||||
|
@ -168,6 +185,7 @@ _global_script_class_icons={
|
|||
"Canvas": "",
|
||||
"Drawer": "",
|
||||
"Frame": "",
|
||||
"GIFAnimationExporter": "",
|
||||
"GradientEditNode": "",
|
||||
"GroupCel": "",
|
||||
"GroupLayer": "",
|
||||
|
|
|
@ -4,11 +4,8 @@ enum ExportTab { FRAME = 0, SPRITESHEET = 1, ANIMATION = 2 }
|
|||
enum Orientation { ROWS = 0, COLUMNS = 1 }
|
||||
enum AnimationType { MULTIPLE_FILES = 0, ANIMATED = 1 }
|
||||
enum AnimationDirection { FORWARD = 0, BACKWARDS = 1, PING_PONG = 2 }
|
||||
enum FileFormat { PNG = 0, GIF = 1 }
|
||||
|
||||
# Gif exporter
|
||||
const GIFExporter = preload("res://addons/gdgifexporter/exporter.gd")
|
||||
const MedianCutQuantization = preload("res://addons/gdgifexporter/quantization/median_cut.gd")
|
||||
# See file_format_string, file_format_description, and ExportDialog.gd
|
||||
enum FileFormat { PNG = 0, GIF = 1, APNG = 2 }
|
||||
|
||||
var current_tab: int = ExportTab.FRAME
|
||||
# Frame options
|
||||
|
@ -199,14 +196,20 @@ func export_processed_images(ignore_overwrites: bool, export_dialog: AcceptDialo
|
|||
scale_processed_images()
|
||||
|
||||
if current_tab == ExportTab.ANIMATION and animation_type == AnimationType.ANIMATED:
|
||||
var exporter: BaseAnimationExporter
|
||||
if file_format == FileFormat.APNG:
|
||||
exporter = APNGAnimationExporter.new()
|
||||
else:
|
||||
exporter = GIFAnimationExporter.new()
|
||||
var details = {
|
||||
"exporter": exporter, "export_dialog": export_dialog, "export_paths": export_paths
|
||||
}
|
||||
if OS.get_name() == "HTML5":
|
||||
export_gif({"export_dialog": export_dialog, "export_paths": export_paths})
|
||||
export_animated(details)
|
||||
else:
|
||||
if gif_export_thread.is_active():
|
||||
gif_export_thread.wait_to_finish()
|
||||
gif_export_thread.start(
|
||||
self, "export_gif", {"export_dialog": export_dialog, "export_paths": export_paths}
|
||||
)
|
||||
gif_export_thread.start(self, "export_animated", details)
|
||||
else:
|
||||
for i in range(processed_images.size()):
|
||||
if OS.get_name() == "HTML5":
|
||||
|
@ -240,71 +243,63 @@ func export_processed_images(ignore_overwrites: bool, export_dialog: AcceptDialo
|
|||
return true
|
||||
|
||||
|
||||
func export_gif(args: Dictionary) -> void:
|
||||
# Export progress popup
|
||||
# One fraction per each frame, one fraction for write to disk
|
||||
export_progress_fraction = 100 / processed_images.size()
|
||||
export_progress = 0.0
|
||||
args["export_dialog"].set_export_progress_bar(export_progress)
|
||||
args["export_dialog"].toggle_export_progress_popup(true)
|
||||
|
||||
# Export and save gif
|
||||
var exporter = GIFExporter.new(
|
||||
processed_images[0].get_width(), processed_images[0].get_height()
|
||||
)
|
||||
func export_animated(args: Dictionary) -> void:
|
||||
var exporter: BaseAnimationExporter = args["exporter"]
|
||||
# This is an ExportDialog (which refers back here).
|
||||
var export_dialog = args["export_dialog"]
|
||||
# Array of Image
|
||||
var sequence = []
|
||||
# Array of float
|
||||
var durations = []
|
||||
match direction:
|
||||
AnimationDirection.FORWARD:
|
||||
for i in range(processed_images.size()):
|
||||
write_frame_to_gif(
|
||||
processed_images[i],
|
||||
Global.current_project.frames[i].duration * (1 / Global.current_project.fps),
|
||||
exporter,
|
||||
args["export_dialog"]
|
||||
)
|
||||
sequence.push_back(processed_images[i])
|
||||
durations.push_back(Global.current_project.frames[i].duration)
|
||||
AnimationDirection.BACKWARDS:
|
||||
for i in range(processed_images.size() - 1, -1, -1):
|
||||
write_frame_to_gif(
|
||||
processed_images[i],
|
||||
Global.current_project.frames[i].duration * (1 / Global.current_project.fps),
|
||||
exporter,
|
||||
args["export_dialog"]
|
||||
)
|
||||
sequence.push_back(processed_images[i])
|
||||
durations.push_back(Global.current_project.frames[i].duration)
|
||||
AnimationDirection.PING_PONG:
|
||||
export_progress_fraction = 100 / (processed_images.size() * 2)
|
||||
for i in range(0, processed_images.size()):
|
||||
write_frame_to_gif(
|
||||
processed_images[i],
|
||||
Global.current_project.frames[i].duration * (1 / Global.current_project.fps),
|
||||
exporter,
|
||||
args["export_dialog"]
|
||||
)
|
||||
sequence.push_back(processed_images[i])
|
||||
durations.push_back(Global.current_project.frames[i].duration)
|
||||
for i in range(processed_images.size() - 2, 0, -1):
|
||||
write_frame_to_gif(
|
||||
processed_images[i],
|
||||
Global.current_project.frames[i].duration * (1 / Global.current_project.fps),
|
||||
exporter,
|
||||
args["export_dialog"]
|
||||
)
|
||||
sequence.push_back(processed_images[i])
|
||||
durations.push_back(Global.current_project.frames[i].duration)
|
||||
|
||||
# Stuff we need to deal with across all images
|
||||
for i in range(processed_images.size()):
|
||||
durations[i] *= 1 / Global.current_project.fps
|
||||
|
||||
# Export progress popup
|
||||
# One fraction per each frame, one fraction for write to disk
|
||||
export_progress_fraction = 100.0 / len(sequence)
|
||||
export_progress = 0.0
|
||||
export_dialog.set_export_progress_bar(export_progress)
|
||||
export_dialog.toggle_export_progress_popup(true)
|
||||
|
||||
# Export and save gif
|
||||
var file_data = exporter.export_animation(
|
||||
sequence,
|
||||
durations,
|
||||
Global.current_project.fps,
|
||||
self,
|
||||
"increase_export_progress",
|
||||
[export_dialog]
|
||||
)
|
||||
|
||||
if OS.get_name() == "HTML5":
|
||||
JavaScript.download_buffer(
|
||||
exporter.export_file_data(), args["export_paths"][0], "image/gif"
|
||||
)
|
||||
|
||||
JavaScript.download_buffer(file_data, args["export_paths"][0], exporter.mime_type)
|
||||
else:
|
||||
var file: File = File.new()
|
||||
file.open(args["export_paths"][0], File.WRITE)
|
||||
file.store_buffer(exporter.export_file_data())
|
||||
file.store_buffer(file_data)
|
||||
file.close()
|
||||
args["export_dialog"].toggle_export_progress_popup(false)
|
||||
export_dialog.toggle_export_progress_popup(false)
|
||||
Global.notification_label("File(s) exported")
|
||||
|
||||
|
||||
func write_frame_to_gif(image: Image, wait_time: float, exporter: Reference, dialog: Node) -> void:
|
||||
exporter.add_frame(image, wait_time, MedianCutQuantization)
|
||||
increase_export_progress(dialog)
|
||||
|
||||
|
||||
func increase_export_progress(export_dialog: Node) -> void:
|
||||
export_progress += export_progress_fraction
|
||||
export_dialog.set_export_progress_bar(export_progress)
|
||||
|
@ -323,10 +318,24 @@ func scale_processed_images() -> void:
|
|||
|
||||
func file_format_string(format_enum: int) -> String:
|
||||
match format_enum:
|
||||
0: # PNG
|
||||
FileFormat.PNG:
|
||||
return ".png"
|
||||
1: # GIF
|
||||
FileFormat.GIF:
|
||||
return ".gif"
|
||||
FileFormat.APNG:
|
||||
return ".apng"
|
||||
_:
|
||||
return ""
|
||||
|
||||
|
||||
func file_format_description(format_enum: int) -> String:
|
||||
match format_enum:
|
||||
FileFormat.PNG:
|
||||
return "PNG Image"
|
||||
FileFormat.GIF:
|
||||
return "GIF Image"
|
||||
FileFormat.APNG:
|
||||
return "APNG Image"
|
||||
_:
|
||||
return ""
|
||||
|
||||
|
|
166
src/Classes/AnimationExporters/APNGAnimationExporter.gd
Normal file
166
src/Classes/AnimationExporters/APNGAnimationExporter.gd
Normal file
|
@ -0,0 +1,166 @@
|
|||
class_name APNGAnimationExporter
|
||||
extends BaseAnimationExporter
|
||||
# APNG exporter. To be clear, this is effectively magic.
|
||||
|
||||
var crc32_table := []
|
||||
|
||||
|
||||
func _init():
|
||||
mime_type = "image/apng"
|
||||
# Calculate CRC32 table.
|
||||
var range8 = range(8)
|
||||
for i in range(256):
|
||||
var crc = i
|
||||
for j in range8:
|
||||
if (crc & 1) != 0:
|
||||
crc = (crc >> 1) ^ 0xEDB88320
|
||||
else:
|
||||
crc >>= 1
|
||||
crc32_table.push_back(crc & 0xFFFFFFFF)
|
||||
|
||||
|
||||
# Performs the update step of CRC32 over some bytes.
|
||||
# Note that this is not the whole story.
|
||||
# The CRC must be initialized to 0xFFFFFFFF, then updated, then bitwise-inverted.
|
||||
func crc32_data(crc: int, data: PoolByteArray):
|
||||
var i = 0
|
||||
var l = len(data)
|
||||
while i < l:
|
||||
var lb = data[i] ^ (crc & 0xFF)
|
||||
crc = crc32_table[lb] ^ (crc >> 8)
|
||||
i += 1
|
||||
return crc
|
||||
|
||||
|
||||
func export_animation(
|
||||
images: Array,
|
||||
durations: Array,
|
||||
fps_hint: float,
|
||||
progress_report_obj: Object,
|
||||
progress_report_method,
|
||||
progress_report_args
|
||||
) -> PoolByteArray:
|
||||
var result = open_chunk()
|
||||
# Magic number
|
||||
result.put_32(0x89504E47)
|
||||
result.put_32(0x0D0A1A0A)
|
||||
# From here on out, all data is written in "chunks".
|
||||
# IHDR
|
||||
var image: Image = images[0]
|
||||
var chunk = open_chunk()
|
||||
chunk.put_32(image.get_width())
|
||||
chunk.put_32(image.get_height())
|
||||
chunk.put_32(0x08060000)
|
||||
chunk.put_8(0)
|
||||
write_chunk(result, "IHDR", chunk.data_array)
|
||||
# acTL
|
||||
chunk = open_chunk()
|
||||
chunk.put_32(len(images))
|
||||
chunk.put_32(0)
|
||||
write_chunk(result, "acTL", chunk.data_array)
|
||||
# For each frame... (note: first frame uses IDAT)
|
||||
var sequence = 0
|
||||
for i in range(len(images)):
|
||||
image = images[i]
|
||||
# fcTL
|
||||
chunk = open_chunk()
|
||||
chunk.put_32(sequence)
|
||||
sequence += 1
|
||||
# image w/h
|
||||
chunk.put_32(image.get_width())
|
||||
chunk.put_32(image.get_height())
|
||||
# offset x/y
|
||||
chunk.put_32(0)
|
||||
chunk.put_32(0)
|
||||
write_delay(chunk, durations[i], fps_hint)
|
||||
# dispose / blend
|
||||
chunk.put_8(0)
|
||||
chunk.put_8(0)
|
||||
write_chunk(result, "fcTL", chunk.data_array)
|
||||
# IDAT/fdAT
|
||||
chunk = open_chunk()
|
||||
if i != 0:
|
||||
chunk.put_32(sequence)
|
||||
sequence += 1
|
||||
# setup chunk interior...
|
||||
var ichk = open_chunk()
|
||||
write_padded_lines(ichk, image)
|
||||
chunk.put_data(ichk.data_array.compress(File.COMPRESSION_DEFLATE))
|
||||
# done with chunk interior
|
||||
if i == 0:
|
||||
write_chunk(result, "IDAT", chunk.data_array)
|
||||
else:
|
||||
write_chunk(result, "fdAT", chunk.data_array)
|
||||
# Done with this frame!
|
||||
progress_report_obj.callv(progress_report_method, progress_report_args)
|
||||
# Final chunk.
|
||||
write_chunk(result, "IEND", PoolByteArray())
|
||||
return result.data_array
|
||||
|
||||
|
||||
func write_delay(sp: StreamPeer, duration: float, fps_hint: float):
|
||||
# Obvious bounds checking
|
||||
duration = max(duration, 0)
|
||||
fps_hint = min(32767, max(fps_hint, 1))
|
||||
# The assumption behind this is that in most cases durations match the FPS hint.
|
||||
# And in most cases the FPS hint is integer.
|
||||
# So it follows that num = 1 and den = fps.
|
||||
# Precision is increased so we catch more complex cases.
|
||||
# But you should always get perfection for integers.
|
||||
var den = min(32767, max(fps_hint, 1))
|
||||
var num = max(duration, 0) * den
|
||||
# If the FPS hint brings us out of range before we start, try some obvious integers
|
||||
var fallback = 10000
|
||||
while num > 32767:
|
||||
num = max(duration, 0) * den
|
||||
den = fallback
|
||||
if fallback == 1:
|
||||
break
|
||||
fallback /= 10
|
||||
# If the fallback plan failed, give up and set the duration to 1 second.
|
||||
if num > 32767:
|
||||
sp.put_16(1)
|
||||
sp.put_16(1)
|
||||
return
|
||||
# Raise to highest safe precision
|
||||
# This is what handles the more complicated cases (usually).
|
||||
while num < 16384 and den < 16384:
|
||||
num *= 2
|
||||
den *= 2
|
||||
# Write out
|
||||
sp.put_16(int(round(num)))
|
||||
sp.put_16(int(round(den)))
|
||||
|
||||
|
||||
func write_padded_lines(sp: StreamPeer, img: Image):
|
||||
if img.get_format() != Image.FORMAT_RGBA8:
|
||||
push_warning("Image format in APNGAnimationExporter should only ever be RGBA8.")
|
||||
return
|
||||
var data = img.get_data()
|
||||
var y = 0
|
||||
var w = img.get_width()
|
||||
var h = img.get_height()
|
||||
var base = 0
|
||||
while y < h:
|
||||
var nl = base + (w * 4)
|
||||
var line = data.subarray(base, nl - 1)
|
||||
sp.put_8(0)
|
||||
sp.put_data(line)
|
||||
y += 1
|
||||
base = nl
|
||||
|
||||
|
||||
func open_chunk() -> StreamPeerBuffer:
|
||||
var result = StreamPeerBuffer.new()
|
||||
result.big_endian = true
|
||||
return result
|
||||
|
||||
|
||||
func write_chunk(sp: StreamPeer, type: String, data: PoolByteArray):
|
||||
sp.put_32(len(data))
|
||||
var at = type.to_ascii()
|
||||
sp.put_data(at)
|
||||
sp.put_data(data)
|
||||
var crc = crc32_data(0xFFFFFFFF, at)
|
||||
crc = crc32_data(crc, data) ^ 0xFFFFFFFF
|
||||
sp.put_32(crc)
|
22
src/Classes/AnimationExporters/BaseAnimationExporter.gd
Normal file
22
src/Classes/AnimationExporters/BaseAnimationExporter.gd
Normal file
|
@ -0,0 +1,22 @@
|
|||
class_name BaseAnimationExporter
|
||||
extends Reference
|
||||
# Represents a method for exporting animations.
|
||||
# Please do NOT use project globals in this code.
|
||||
|
||||
var mime_type: String
|
||||
|
||||
|
||||
# Exports an animation to a byte array of file data.
|
||||
# fps_hint is only a hint, animations may have higher FPSes than this.
|
||||
# The durations array (with durations listed in seconds) is the true reference.
|
||||
# progress_report_obj.callv(progress_report_method, progress_report_args) is
|
||||
# called after each frame is handled.
|
||||
func export_animation(
|
||||
_frames: Array,
|
||||
_durations: Array,
|
||||
_fps_hint: float,
|
||||
_progress_report_obj: Object,
|
||||
_progress_report_method,
|
||||
_progress_report_args
|
||||
) -> PoolByteArray:
|
||||
return PoolByteArray()
|
26
src/Classes/AnimationExporters/GIFAnimationExporter.gd
Normal file
26
src/Classes/AnimationExporters/GIFAnimationExporter.gd
Normal file
|
@ -0,0 +1,26 @@
|
|||
class_name GIFAnimationExporter
|
||||
extends BaseAnimationExporter
|
||||
# Acts as the interface between Pixelorama's format-independent interface and gdgifexporter.
|
||||
|
||||
# Gif exporter
|
||||
const GIFExporter = preload("res://addons/gdgifexporter/exporter.gd")
|
||||
const MedianCutQuantization = preload("res://addons/gdgifexporter/quantization/median_cut.gd")
|
||||
|
||||
|
||||
func _init():
|
||||
mime_type = "image/gif"
|
||||
|
||||
|
||||
func export_animation(
|
||||
images: Array,
|
||||
durations: Array,
|
||||
_fps_hint: float,
|
||||
progress_report_obj: Object,
|
||||
progress_report_method,
|
||||
progress_report_args
|
||||
) -> PoolByteArray:
|
||||
var exporter = GIFExporter.new(images[0].get_width(), images[0].get_height())
|
||||
for i in range(images.size()):
|
||||
exporter.add_frame(images[i], durations[i], MedianCutQuantization)
|
||||
progress_report_obj.callv(progress_report_method, progress_report_args)
|
||||
return exporter.export_file_data()
|
|
@ -61,10 +61,9 @@ func show_tab() -> void:
|
|||
spritesheet_options.hide()
|
||||
animation_options.hide()
|
||||
|
||||
set_file_format_selector()
|
||||
match Export.current_tab:
|
||||
Export.ExportTab.FRAME:
|
||||
Export.file_format = Export.FileFormat.PNG
|
||||
file_file_format.selected = Export.FileFormat.PNG
|
||||
frame_timer.stop()
|
||||
if not Export.was_exported:
|
||||
Export.frame_number = Global.current_project.current_frame + 1
|
||||
|
@ -76,12 +75,10 @@ func show_tab() -> void:
|
|||
frame_options.show()
|
||||
Export.ExportTab.SPRITESHEET:
|
||||
create_frame_tag_list()
|
||||
Export.file_format = Export.FileFormat.PNG
|
||||
if not Export.was_exported:
|
||||
Export.orientation = Export.Orientation.ROWS
|
||||
Export.lines_count = int(ceil(sqrt(Export.number_of_frames)))
|
||||
Export.process_spritesheet()
|
||||
file_file_format.selected = Export.FileFormat.PNG
|
||||
spritesheet_frames.select(Export.frame_current_tag)
|
||||
frame_timer.stop()
|
||||
spritesheet_orientation.selected = Export.orientation
|
||||
|
@ -90,7 +87,6 @@ func show_tab() -> void:
|
|||
spritesheet_lines_count_label.text = "Columns:"
|
||||
spritesheet_options.show()
|
||||
Export.ExportTab.ANIMATION:
|
||||
set_file_format_selector()
|
||||
Export.process_animation()
|
||||
animation_options_animation_type.selected = Export.animation_type
|
||||
animation_options_direction.selected = Export.direction
|
||||
|
@ -184,19 +180,40 @@ func remove_previews() -> void:
|
|||
|
||||
|
||||
func set_file_format_selector() -> void:
|
||||
multiple_animations_directories.visible = false
|
||||
match Export.animation_type:
|
||||
Export.AnimationType.MULTIPLE_FILES:
|
||||
Export.file_format = Export.FileFormat.PNG
|
||||
file_file_format.selected = Export.FileFormat.PNG
|
||||
frame_timer.stop()
|
||||
animation_options_animation_options.hide()
|
||||
multiple_animations_directories.pressed = Export.new_dir_for_each_frame_tag
|
||||
multiple_animations_directories.visible = true
|
||||
Export.AnimationType.ANIMATED:
|
||||
Export.file_format = Export.FileFormat.GIF
|
||||
file_file_format.selected = Export.FileFormat.GIF
|
||||
animation_options_animation_options.show()
|
||||
match Export.current_tab:
|
||||
Export.ExportTab.FRAME:
|
||||
_set_file_format_selector_suitable_file_formats([Export.FileFormat.PNG])
|
||||
Export.ExportTab.SPRITESHEET:
|
||||
_set_file_format_selector_suitable_file_formats([Export.FileFormat.PNG])
|
||||
Export.ExportTab.ANIMATION:
|
||||
multiple_animations_directories.visible = false
|
||||
match Export.animation_type:
|
||||
Export.AnimationType.MULTIPLE_FILES:
|
||||
_set_file_format_selector_suitable_file_formats([Export.FileFormat.PNG])
|
||||
frame_timer.stop()
|
||||
animation_options_animation_options.hide()
|
||||
multiple_animations_directories.pressed = Export.new_dir_for_each_frame_tag
|
||||
multiple_animations_directories.visible = true
|
||||
Export.AnimationType.ANIMATED:
|
||||
_set_file_format_selector_suitable_file_formats(
|
||||
[Export.FileFormat.GIF, Export.FileFormat.APNG]
|
||||
)
|
||||
animation_options_animation_options.show()
|
||||
|
||||
|
||||
# Updates the suitable list of file formats. First is preferred.
|
||||
# Note that if the current format is in the list, it stays for consistency.
|
||||
func _set_file_format_selector_suitable_file_formats(formats: Array):
|
||||
file_file_format.clear()
|
||||
var needs_update = true
|
||||
for i in formats:
|
||||
if Export.file_format == i:
|
||||
needs_update = false
|
||||
var label = Export.file_format_string(i) + "; " + Export.file_format_description(i)
|
||||
file_file_format.add_item(label, i)
|
||||
if needs_update:
|
||||
Export.file_format = formats[0]
|
||||
file_file_format.selected = file_file_format.get_item_index(Export.file_format)
|
||||
|
||||
|
||||
func create_frame_tag_list() -> void:
|
||||
|
@ -364,7 +381,8 @@ func _on_FileDialog_dir_selected(dir: String) -> void:
|
|||
Export.directory_path = dir
|
||||
|
||||
|
||||
func _on_FileFormat_item_selected(id: int) -> void:
|
||||
func _on_FileFormat_item_selected(idx: int) -> void:
|
||||
var id = file_file_format.get_item_id(idx)
|
||||
Global.current_project.file_format = id
|
||||
Export.file_format = id
|
||||
|
||||
|
|
|
@ -342,10 +342,6 @@ margin_right = 516.0
|
|||
margin_bottom = 24.0
|
||||
rect_min_size = Vector2( 130, 0 )
|
||||
mouse_default_cursor_shape = 8
|
||||
disabled = true
|
||||
text = ".png; PNG Image"
|
||||
items = [ ".png; PNG Image", null, false, 0, null, ".gif; GIF Image", null, false, 1, null ]
|
||||
selected = 0
|
||||
|
||||
[node name="Popups" type="Node" parent="."]
|
||||
|
||||
|
@ -361,8 +357,7 @@ window_title = "Open a Directory"
|
|||
resizable = true
|
||||
mode = 2
|
||||
access = 2
|
||||
current_dir = "/home/variable/Documents/Godot/Godot projects/Pixelorama-play_improvements"
|
||||
current_path = "/home/variable/Documents/Godot/Godot projects/Pixelorama-play_improvements/"
|
||||
show_hidden_files = true
|
||||
|
||||
[node name="PathValidationAlert" type="AcceptDialog" parent="Popups"]
|
||||
margin_left = 8.0
|
||||
|
|
Loading…
Reference in a new issue