1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00

The Recorder panel now automatically records for the current project

Making its behavior more intuitive and consistent with the other panels. This also allows for multiple projects to be recorder at the same time, something that was not previous before. Changing projects now also changes the UI accordingly, depending on whether the current project is being recorded or not.

This change also fixes a memory leak, where either the first ever project or the last recorded one, stayed forever referenced in memory by the `project` variable.

Also fixed an issue where the recorder's settings size label was not showing the correct project size.
This commit is contained in:
Emmanouil Papadeas 2024-11-03 18:54:08 +02:00
parent 8beb79a33b
commit ec17e970e0
3 changed files with 86 additions and 123 deletions

View file

@ -3,6 +3,7 @@ class_name Project
extends RefCounted extends RefCounted
## A class for project properties. ## A class for project properties.
signal removed
signal serialized(dict: Dictionary) signal serialized(dict: Dictionary)
signal about_to_deserialize(dict: Dictionary) signal about_to_deserialize(dict: Dictionary)
signal resized signal resized
@ -137,6 +138,7 @@ func remove() -> void:
# Prevents memory leak (due to the layers' project reference stopping ref counting from freeing) # Prevents memory leak (due to the layers' project reference stopping ref counting from freeing)
layers.clear() layers.clear()
Global.projects.erase(self) Global.projects.erase(self)
removed.emit()
func remove_backup_file() -> void: func remove_backup_file() -> void:

View file

@ -1,17 +1,17 @@
class_name RecorderPanel
extends PanelContainer extends PanelContainer
signal frame_saved
enum Mode { CANVAS, PIXELORAMA } enum Mode { CANVAS, PIXELORAMA }
var mode := Mode.CANVAS var mode := Mode.CANVAS
var chosen_dir := "" var chosen_dir := "":
set(value):
chosen_dir = value
if chosen_dir.ends_with("/"): # Remove end back-slashes if present
chosen_dir[-1] = ""
var recorded_projects := {} ## [Dictionary] of [Project] and [Recorder].
var save_dir := "" var save_dir := ""
var project: Project var skip_amount := 1 ## Number of "do" actions after which a frame can be captured.
var cache: Array[Image] = [] ## Images stored during recording
var frame_captured := 0 ## Used to visualize frames captured
var skip_amount := 1 ## Number of "do" actions after which a frame can be captured
var current_frame_no := 0 ## Used to compare with skip_amount to see if it can be captured
var resize_percent := 100 var resize_percent := 100
var _path_dialog: FileDialog: var _path_dialog: FileDialog:
get: get:
@ -28,7 +28,6 @@ var _path_dialog: FileDialog:
return _path_dialog return _path_dialog
@onready var captured_label := %CapturedLabel as Label @onready var captured_label := %CapturedLabel as Label
@onready var project_list := $"%TargetProjectOption" as OptionButton
@onready var start_button := $"%Start" as Button @onready var start_button := $"%Start" as Button
@onready var size_label := $"%Size" as Label @onready var size_label := $"%Size" as Label
@onready var path_field := $"%Path" as LineEdit @onready var path_field := $"%Path" as LineEdit
@ -36,125 +35,96 @@ var _path_dialog: FileDialog:
@onready var options_container := %OptionsContainer as VBoxContainer @onready var options_container := %OptionsContainer as VBoxContainer
class Recorder:
var project: Project
var recorder_panel: RecorderPanel
var actions_done := -1
var frames_captured := 0
var save_directory := ""
func _init(_project: Project, _recorder_panel: RecorderPanel) -> void:
project = _project
recorder_panel = _recorder_panel
# Create a new directory based on time
var time_dict := Time.get_time_dict_from_system()
var folder := str(
project.name, time_dict.hour, "_", time_dict.minute, "_", time_dict.second
)
var dir := DirAccess.open(recorder_panel.chosen_dir)
save_directory = recorder_panel.chosen_dir.path_join(folder)
dir.make_dir_recursive(save_directory)
project.removed.connect(recorder_panel.finalize_recording.bind(project))
project.undo_redo.version_changed.connect(capture_frame)
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
# Needed so that the project won't be forever remained in memory because of bind().
project.removed.disconnect(recorder_panel.finalize_recording)
func capture_frame() -> void:
actions_done += 1
if actions_done % recorder_panel.skip_amount != 0:
return
var image: Image
if recorder_panel.mode == RecorderPanel.Mode.PIXELORAMA:
image = recorder_panel.get_window().get_texture().get_image()
else:
var frame := project.frames[project.current_frame]
image = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project)
if recorder_panel.resize_percent != 100:
var resize := recorder_panel.resize_percent / 100
var new_width := image.get_width() * resize
var new_height := image.get_height() * resize
image.resize(new_width, new_height, Image.INTERPOLATE_NEAREST)
var save_file := str(project.name, "_", frames_captured, ".png")
image.save_png(save_directory.path_join(save_file))
frames_captured += 1
recorder_panel.captured_label.text = str("Saved: ", frames_captured)
func _ready() -> void: func _ready() -> void:
refresh_projects_list() Global.project_switched.connect(_on_project_switched)
project = Global.current_project
frame_saved.connect(_on_frame_saved)
# Make a recordings folder if there isn't one # Make a recordings folder if there isn't one
chosen_dir = Global.home_data_directory.path_join("Recordings") chosen_dir = Global.home_data_directory.path_join("Recordings")
DirAccess.make_dir_recursive_absolute(chosen_dir) DirAccess.make_dir_recursive_absolute(chosen_dir)
path_field.text = chosen_dir path_field.text = chosen_dir
size_label.text = str("(", project.size.x, "×", project.size.y, ")")
func _on_project_switched() -> void:
if recorded_projects.has(Global.current_project):
initialize_recording()
start_button.set_pressed_no_signal(true)
Global.change_button_texturerect(start_button.get_child(0), "stop.png")
else:
finalize_recording()
start_button.set_pressed_no_signal(false)
Global.change_button_texturerect(start_button.get_child(0), "start.png")
func initialize_recording() -> void: func initialize_recording() -> void:
connect_undo() # connect to detect changes in project
cache.clear() # clear the cache array to store new images
frame_captured = 0
current_frame_no = skip_amount - 1
# disable some options that are not required during recording # disable some options that are not required during recording
project_list.visible = false
captured_label.visible = true captured_label.visible = true
for child in options_container.get_children(): for child in options_container.get_children():
if !child.is_in_group("visible during recording"): if !child.is_in_group("visible during recording"):
child.visible = false child.visible = false
save_dir = chosen_dir
# Remove end back-slashes if present
if save_dir.ends_with("/"):
save_dir[-1] = ""
# Create a new directory based on time func finalize_recording(project := Global.current_project) -> void:
var time_dict := Time.get_time_dict_from_system() if recorded_projects.has(project):
var folder := str(project.name, time_dict.hour, "_", time_dict.minute, "_", time_dict.second) recorded_projects.erase(project)
var dir := DirAccess.open(save_dir) if project == Global.current_project:
save_dir = save_dir.path_join(folder) captured_label.visible = false
dir.make_dir_recursive(save_dir) for child in options_container.get_children():
child.visible = true
capture_frame() # capture first frame if mode == Mode.PIXELORAMA:
$Timer.start() size_label.get_parent().visible = false
func capture_frame() -> void:
current_frame_no += 1
if current_frame_no != skip_amount:
return
current_frame_no = 0
var image: Image
if mode == Mode.PIXELORAMA:
image = get_tree().root.get_viewport().get_texture().get_image()
else:
var frame := project.frames[project.current_frame]
image = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)
DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project)
if mode == Mode.CANVAS:
if resize_percent != 100:
var resize := resize_percent / 100
image.resize(
image.get_width() * resize, image.get_height() * resize, Image.INTERPOLATE_NEAREST
)
cache.append(image)
func _on_Timer_timeout() -> void:
# Saves frames little by little during recording
if cache.size() > 0:
save_frame(cache[0])
cache.remove_at(0)
func save_frame(img: Image) -> void:
var save_file := str(project.name, "_", frame_captured, ".png")
img.save_png(save_dir.path_join(save_file))
frame_saved.emit()
func _on_frame_saved() -> void:
frame_captured += 1
captured_label.text = str("Saved: ", frame_captured)
func finalize_recording() -> void:
$Timer.stop()
for img in cache:
save_frame(img)
cache.clear()
disconnect_undo()
project_list.visible = true
captured_label.visible = false
for child in options_container.get_children():
child.visible = true
if mode == Mode.PIXELORAMA:
size_label.get_parent().visible = false
func disconnect_undo() -> void:
project.undo_redo.version_changed.disconnect(capture_frame)
func connect_undo() -> void:
project.undo_redo.version_changed.connect(capture_frame)
func _on_TargetProjectOption_item_selected(index: int) -> void:
project = Global.projects[index]
func _on_TargetProjectOption_pressed() -> void:
refresh_projects_list()
func refresh_projects_list() -> void:
project_list.clear()
for proj in Global.projects:
project_list.add_item(proj.name)
func _on_Start_toggled(button_pressed: bool) -> void: func _on_Start_toggled(button_pressed: bool) -> void:
if button_pressed: if button_pressed:
recorded_projects[Global.current_project] = Recorder.new(Global.current_project, self)
initialize_recording() initialize_recording()
Global.change_button_texturerect(start_button.get_child(0), "stop.png") Global.change_button_texturerect(start_button.get_child(0), "stop.png")
else: else:
@ -163,7 +133,8 @@ func _on_Start_toggled(button_pressed: bool) -> void:
func _on_Settings_pressed() -> void: func _on_Settings_pressed() -> void:
options_dialog.popup_on_parent(Rect2(position, options_dialog.size)) _on_SpinBox_value_changed(resize_percent)
options_dialog.popup_on_parent(Rect2i(position, options_dialog.size))
func _on_SkipAmount_value_changed(value: float) -> void: func _on_SkipAmount_value_changed(value: float) -> void:
@ -181,7 +152,7 @@ func _on_Mode_toggled(button_pressed: bool) -> void:
func _on_SpinBox_value_changed(value: float) -> void: func _on_SpinBox_value_changed(value: float) -> void:
resize_percent = value resize_percent = value
var new_size: Vector2 = project.size * (resize_percent / 100.0) var new_size: Vector2 = Global.current_project.size * (resize_percent / 100.0)
size_label.text = str("(", new_size.x, "×", new_size.y, ")") size_label.text = str("(", new_size.x, "×", new_size.y, ")")

View file

@ -31,13 +31,6 @@ unique_name_in_owner = true
visible = false visible = false
layout_mode = 2 layout_mode = 2
[node name="TargetProjectOption" type="OptionButton" parent="ScrollContainer/CenterContainer/GridContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
tooltip_text = "Choose project"
clip_text = true
[node name="Start" type="Button" parent="ScrollContainer/CenterContainer/GridContainer" groups=["UIButtons"]] [node name="Start" type="Button" parent="ScrollContainer/CenterContainer/GridContainer" groups=["UIButtons"]]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(32, 32) custom_minimum_size = Vector2(32, 32)
@ -143,6 +136,8 @@ text = "Capture frame every"
[node name="SkipAmount" type="SpinBox" parent="OptionsDialog/PanelContainer/OptionsContainer/ActionGap"] [node name="SkipAmount" type="SpinBox" parent="OptionsDialog/PanelContainer/OptionsContainer/ActionGap"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
min_value = 1.0
value = 1.0
suffix = "actions" suffix = "actions"
[node name="ModeHeader" type="HBoxContainer" parent="OptionsDialog/PanelContainer/OptionsContainer" groups=["visible during recording"]] [node name="ModeHeader" type="HBoxContainer" parent="OptionsDialog/PanelContainer/OptionsContainer" groups=["visible during recording"]]
@ -223,10 +218,6 @@ editable = false
layout_mode = 2 layout_mode = 2
text = "Choose" text = "Choose"
[node name="Timer" type="Timer" parent="."]
[connection signal="item_selected" from="ScrollContainer/CenterContainer/GridContainer/TargetProjectOption" to="." method="_on_TargetProjectOption_item_selected"]
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/TargetProjectOption" to="." method="_on_TargetProjectOption_pressed"]
[connection signal="toggled" from="ScrollContainer/CenterContainer/GridContainer/Start" to="." method="_on_Start_toggled"] [connection signal="toggled" from="ScrollContainer/CenterContainer/GridContainer/Start" to="." method="_on_Start_toggled"]
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/Settings" to="." method="_on_Settings_pressed"] [connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/Settings" to="." method="_on_Settings_pressed"]
[connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/OpenFolder" to="." method="_on_open_folder_pressed"] [connection signal="pressed" from="ScrollContainer/CenterContainer/GridContainer/OpenFolder" to="." method="_on_open_folder_pressed"]
@ -234,4 +225,3 @@ text = "Choose"
[connection signal="toggled" from="OptionsDialog/PanelContainer/OptionsContainer/ModeType/Mode" to="." method="_on_Mode_toggled"] [connection signal="toggled" from="OptionsDialog/PanelContainer/OptionsContainer/ModeType/Mode" to="." method="_on_Mode_toggled"]
[connection signal="value_changed" from="OptionsDialog/PanelContainer/OptionsContainer/OutputScale/Resize" to="." method="_on_SpinBox_value_changed"] [connection signal="value_changed" from="OptionsDialog/PanelContainer/OptionsContainer/OutputScale/Resize" to="." method="_on_SpinBox_value_changed"]
[connection signal="pressed" from="OptionsDialog/PanelContainer/OptionsContainer/PathContainer/Choose" to="." method="_on_Choose_pressed"] [connection signal="pressed" from="OptionsDialog/PanelContainer/OptionsContainer/PathContainer/Choose" to="." method="_on_Choose_pressed"]
[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"]