diff --git a/export_presets.cfg b/export_presets.cfg new file mode 100644 index 0000000..38710e7 --- /dev/null +++ b/export_presets.cfg @@ -0,0 +1,64 @@ +[preset.0] + +name="Export Extension (PCK)" +platform="Windows Desktop" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="*.json" +exclude_filter="res://src/Extensions/Audia/API/*" +export_path="" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=false +texture_format/bptc=false +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false +binary_format/architecture="x86_64" +codesign/enable=false +codesign/timestamp=true +codesign/timestamp_server_url="" +codesign/digest_algorithm=1 +codesign/description="" +codesign/custom_options=PackedStringArray() +application/modify_resources=true +application/icon="" +application/console_wrapper_icon="" +application/icon_interpolation=4 +application/file_version="" +application/product_version="" +application/company_name="" +application/product_name="" +application/file_description="" +application/copyright="" +application/trademarks="" +application/export_angle=0 +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}' +$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}' +$trigger = New-ScheduledTaskTrigger -Once -At 00:00 +$settings = New-ScheduledTaskSettingsSet +$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings +Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true +Start-ScheduledTask -TaskName godot_remote_debug +while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 } +Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue" +ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue +Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue +Remove-Item -Recurse -Force '{temp_dir}'" +binary_format/64_bits=true +texture_format/no_bptc_fallbacks=true diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..7c234b3 --- /dev/null +++ b/project.godot @@ -0,0 +1,29 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Audia" +config/description="A pixelorama Extention (The \"Name\" and \"Description\" field are not related to extention system so they can be anything)" +config/features=PackedStringArray("4.2") +" field are not related to extention system so they can be anything)run/main_scene"="res://src/Extensions/Audia/Main.tscn" + +[autoload] + +ExtensionsApi="*res://src/Extensions/Audia/API/ExtensionsApi.gd" + +[physics] + +common/enable_pause_aware_picking=true + +[rendering] + +quality/driver/driver_name="GLES2" +vram_compression/import_etc=true diff --git a/src/Extensions/Audia/API/EmptyClasses/AnimationTag.gd b/src/Extensions/Audia/API/EmptyClasses/AnimationTag.gd new file mode 100644 index 0000000..730583a --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/AnimationTag.gd @@ -0,0 +1,7 @@ +class_name AnimationTag +extends RefCounted +# A class for frame tag properties + + +func _init(_name, _color, _from, _to) -> void: + pass diff --git a/src/Extensions/Audia/API/EmptyClasses/BaseCel.gd b/src/Extensions/Audia/API/EmptyClasses/BaseCel.gd new file mode 100644 index 0000000..8b0c6b4 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/BaseCel.gd @@ -0,0 +1,5 @@ +class_name BaseCel +extends RefCounted +## Base class for cel properties. +## The term "cel" comes from "celluloid" (https://en.wikipedia.org/wiki/Cel). + diff --git a/src/Extensions/Audia/API/EmptyClasses/BaseLayer.gd b/src/Extensions/Audia/API/EmptyClasses/BaseLayer.gd new file mode 100644 index 0000000..8fcdea2 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/BaseLayer.gd @@ -0,0 +1,2 @@ +class_name BaseLayer +extends RefCounted diff --git a/src/Extensions/Audia/API/EmptyClasses/BaseTool.gd b/src/Extensions/Audia/API/EmptyClasses/BaseTool.gd new file mode 100644 index 0000000..f1b9840 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/BaseTool.gd @@ -0,0 +1,2 @@ +class_name BaseTool +extends VBoxContainer diff --git a/src/Extensions/Audia/API/EmptyClasses/Cel3D.gd b/src/Extensions/Audia/API/EmptyClasses/Cel3D.gd new file mode 100644 index 0000000..9e2870b --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/Cel3D.gd @@ -0,0 +1,6 @@ +class_name Cel3D +extends BaseCel + + +func _init(_size: Vector2, from_pxo := false, _object_prop := {}, _scene_prop := {}) -> void: + pass diff --git a/src/Extensions/Audia/API/EmptyClasses/Cel3DObject.gd b/src/Extensions/Audia/API/EmptyClasses/Cel3DObject.gd new file mode 100644 index 0000000..e18c4cb --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/Cel3DObject.gd @@ -0,0 +1,3 @@ +class_name Cel3DObject +extends Node3D + diff --git a/src/Extensions/Audia/API/EmptyClasses/Drawers.gd b/src/Extensions/Audia/API/EmptyClasses/Drawers.gd new file mode 100644 index 0000000..3c3b2bd --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/Drawers.gd @@ -0,0 +1 @@ +class_name Drawer diff --git a/src/Extensions/Audia/API/EmptyClasses/Frame.gd b/src/Extensions/Audia/API/EmptyClasses/Frame.gd new file mode 100644 index 0000000..45c7252 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/Frame.gd @@ -0,0 +1,8 @@ +class_name Frame +extends RefCounted +# A class for frame properties. +# A frame is a collection of cels, for each layer. + + +func _init(_cels := [], _duration := 1.0) -> void: + pass diff --git a/src/Extensions/Audia/API/EmptyClasses/GIFAnimationExporter.gd b/src/Extensions/Audia/API/EmptyClasses/GIFAnimationExporter.gd new file mode 100644 index 0000000..69ab483 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/GIFAnimationExporter.gd @@ -0,0 +1,8 @@ +class_name GIFAnimationExporter +# Acts as the interface between the AImgIO format-independent interface and gdgifexporter. +# Note that if the interface needs changing for new features, do just change it! + + +func _init(): + pass + diff --git a/src/Extensions/Audia/API/EmptyClasses/GroupCel.gd b/src/Extensions/Audia/API/EmptyClasses/GroupCel.gd new file mode 100644 index 0000000..129e4e5 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/GroupCel.gd @@ -0,0 +1,8 @@ +class_name GroupCel +extends BaseCel +# A class for the properties of cels in GroupLayers. +# The term "cel" comes from "celluloid" (https://en.wikipedia.org/wiki/Cel). + + +func _init(_opacity := 1.0) -> void: + pass diff --git a/src/Extensions/Audia/API/EmptyClasses/GroupLayer.gd b/src/Extensions/Audia/API/EmptyClasses/GroupLayer.gd new file mode 100644 index 0000000..be84d97 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/GroupLayer.gd @@ -0,0 +1,9 @@ +class_name GroupLayer +extends BaseLayer +# A class for group layer properties + +var expanded := true + + +func _init(_project, _name := "") -> void: + pass diff --git a/src/Extensions/Audia/API/EmptyClasses/ImageEffect.gd b/src/Extensions/Audia/API/EmptyClasses/ImageEffect.gd new file mode 100644 index 0000000..cc7fb42 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/ImageEffect.gd @@ -0,0 +1,4 @@ +class_name ImageEffect +extends ConfirmationDialog +## Parent class for all image effects +## Methods that have "pass" are meant to be replaced by the inherited scripts diff --git a/src/Extensions/Audia/API/EmptyClasses/Layer3D.gd b/src/Extensions/Audia/API/EmptyClasses/Layer3D.gd new file mode 100644 index 0000000..209ac8a --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/Layer3D.gd @@ -0,0 +1,6 @@ +class_name Layer3D +extends BaseLayer + + +func _init(_project, _name := "") -> void: + pass diff --git a/src/Extensions/Audia/API/EmptyClasses/ObjParse.gd b/src/Extensions/Audia/API/EmptyClasses/ObjParse.gd new file mode 100644 index 0000000..3167903 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/ObjParse.gd @@ -0,0 +1,10 @@ +class_name ObjParse + +# Obj parser made by Ezcha, updated by Deakcor +# Created on 7/11/2018 +# https://ezcha.net +# https://github.com/Ezcha/gd-obj +# MIT License +# https://github.com/Ezcha/gd-obj/blob/master/LICENSE + +# Returns an array of materials from a MTL file diff --git a/src/Extensions/Audia/API/EmptyClasses/PixelCel.gd b/src/Extensions/Audia/API/EmptyClasses/PixelCel.gd new file mode 100644 index 0000000..e7b21ec --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/PixelCel.gd @@ -0,0 +1,9 @@ +class_name PixelCel +extends BaseCel +# A class for the properties of cels in PixelLayers. +# The term "cel" comes from "celluloid" (https://en.wikipedia.org/wiki/Cel). +# The "image" variable is where the image data of each cel are. + + +func _init(_image := Image.new(), _opacity := 1.0, _image_texture: ImageTexture = null) -> void: + pass diff --git a/src/Extensions/Audia/API/EmptyClasses/PixelLayer.gd b/src/Extensions/Audia/API/EmptyClasses/PixelLayer.gd new file mode 100644 index 0000000..9327b2b --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/PixelLayer.gd @@ -0,0 +1,7 @@ +class_name PixelLayer +extends BaseLayer +# A class for standard pixel layer properties. + + +func _init(_project, _name := "") -> void: + pass diff --git a/src/Extensions/Audia/API/EmptyClasses/Project.gd b/src/Extensions/Audia/API/EmptyClasses/Project.gd new file mode 100644 index 0000000..feb640b --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/Project.gd @@ -0,0 +1,8 @@ +# gdlint: ignore=max-public-methods +class_name Project +extends RefCounted +# A class for project properties. + + +func _init(_frames := [], _name := tr("untitled"), _size := Vector2(64, 64)) -> void: + pass diff --git a/src/Extensions/Audia/API/EmptyClasses/SelectionMap.gd b/src/Extensions/Audia/API/EmptyClasses/SelectionMap.gd new file mode 100644 index 0000000..9a76904 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/SelectionMap.gd @@ -0,0 +1,2 @@ +class_name SelectionMap +extends Image diff --git a/src/Extensions/Audia/API/EmptyClasses/SelectionTool.gd b/src/Extensions/Audia/API/EmptyClasses/SelectionTool.gd new file mode 100644 index 0000000..1c659c0 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/SelectionTool.gd @@ -0,0 +1,2 @@ +class_name SelectionTool +extends BaseTool diff --git a/src/Extensions/Audia/API/EmptyClasses/ShaderImageEffect.gd b/src/Extensions/Audia/API/EmptyClasses/ShaderImageEffect.gd new file mode 100644 index 0000000..b62177e --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/ShaderImageEffect.gd @@ -0,0 +1,3 @@ +class_name ShaderImageEffect +extends RefCounted +# Helper class to generate image effects using shaders diff --git a/src/Extensions/Audia/API/EmptyClasses/Tiles.gd b/src/Extensions/Audia/API/EmptyClasses/Tiles.gd new file mode 100644 index 0000000..299c732 --- /dev/null +++ b/src/Extensions/Audia/API/EmptyClasses/Tiles.gd @@ -0,0 +1,6 @@ +class_name Tiles +extends RefCounted + + +func _init(size: Vector2): + pass diff --git a/src/Extensions/Audia/API/ExtensionsApi.gd b/src/Extensions/Audia/API/ExtensionsApi.gd new file mode 100644 index 0000000..6c66dc5 --- /dev/null +++ b/src/Extensions/Audia/API/ExtensionsApi.gd @@ -0,0 +1,473 @@ +extends Node +# NOTE: Goto File-->Save then type "ExtensionsApi" in "Search Help" to read the +# curated documentation of the Api. +# If it still doesn't show try again after doing Project-->Reload current project + +## The Official ExtensionsAPI for pixelorama. +## +## This Api gives you the essentials to develop a working extension for Pixelorama.[br] +## The Api consists of many smaller Apis, each giving access to different areas of the Software. +## [br][br] +## Keep in mind that this API is targeted towards users who are not fully familiar with Pixelorama's +## source code. If you need to do something more complicated and more low-level, you would need to +## interact directly with the source code. +## [br][br] +## To access this anywhere in the extension use [code]get_node_or_null("/root/ExtensionsApi")[/code] +## +## @tutorial(Add Tutorial here): https://the/tutorial1/url.com + +## Gives access to the general, app related functions of pixelorama +## such as Autoloads, Software Version, Config file etc... +var general := GeneralAPI.new() +var menu := MenuAPI.new() ## Gives ability to add/remove items from menus in the top bar. +var dialog := DialogAPI.new() ## Gives access to Dialog related functions. +var panel := PanelAPI.new() ## Gives access to Tabs and Dockable Container related functions. +var theme := ThemeAPI.new() ## Gives access to theme related functions. +var tools := ToolAPI.new() ## Gives ability to add/remove tools. +var selection := SelectionAPI.new() ## Gives access to pixelorama's selection system. +var project := ProjectAPI.new() ## Gives access to project manipulation. +var export := ExportAPI.new() ## Gives access to adding custom exporters. +var import := ImportAPI.new() ## Gives access to adding custom import options. +var palette := PaletteAPI.new() ## Gives access to palettes. +var signals := SignalsAPI.new() ## Gives access to the basic commonly used signals. + + +# The API Methods Start Here +## Returns the version of the ExtensionsApi. +func get_api_version() -> int: + return 4 + + +## Returns the initial nodes of an extension named [param extension_name]. +## initial nodes are the nodes whose paths are in the [code]nodes[/code] key of an +## extension.json file. +func get_main_nodes(extension_name: StringName) -> Array[Node]: + return [] + + +## Gives Access to the general stuff. +## +## This part of Api provides stuff like commonly used Autoloads, App's version info etc +## the most basic (but important) stuff. +class GeneralAPI: + ## Returns the current version of pixelorama. + func get_pixelorama_version() -> String: + return "v1.0-stable" + + ## Returns the [ConfigFile] contains all the settings (Brushes, sizes, preferences, etc...). + func get_config_file() -> ConfigFile: + return + + ## Returns the Global autoload used by Pixelorama.[br] + ## Contains references to almost all UI Elements, Variables that indicate different + ## settings etc..., In short it is the most important autoload of Pixelorama. + func get_global(): + return + + ## Returns the DrawingAlgos autoload, contains different drawing algorithms used by Pixelorama. + func get_drawing_algos(): + return + + ## Gives you a new ShaderImageEffect class. this class can apply shader to an image.[br] + ## It contains method: + ## [code]generate_image(img: Image, shader: Shader, params: Dictionary, size: Vector2)[/code] + ## [br]Whose parameters are identified as: + ## [br][param img] --> image that the shader will be pasted to (Empty Image of size same as + ## project). + ## [br][param shader] --> preload of the shader. + ## [br][param params] --> a dictionary of params used by the shader. + ## [br][param size] --> It is the project's size. + func get_new_shader_image_effect() -> ShaderImageEffect: + return ShaderImageEffect.new() + + ## Returns parent of the nodes listed in extension.json -> "nodes". + func get_extensions_node() -> Node: + return + + ## Returns the main [code]Canvas[/code] node, + ## normally used to add a custom preview to the canvas. + func get_canvas(): + return + + +## Gives ability to add/remove items from menus in the top bar. +class MenuAPI: + enum { FILE, EDIT, SELECT, IMAGE, EFFECTS, VIEW, WINDOW, HELP } + + ## Adds a menu item of title [param item_name] to the [param menu_type] defined by + ## [enum @unnamed_enums]. + ## [br][param item_metadata] is usually a window node you want to appear when you click the + ## [param item_name]. That window node should also have a [param menu_item_clicked] + ## function inside its script.[br] + ## Index of the added item is returned (which can be used to remove menu item later on). + func add_menu_item(menu_type: int, item_name: String, item_metadata, item_id := -1) -> int: + return 0 + + ## Removes a menu item at index [param item_idx] from the [param menu_type] defined by + ## [enum @unnamed_enums]. + func remove_menu_item(menu_type: int, item_idx: int) -> void: + return + + +## Gives access to common dialog related functions. +class DialogAPI: + ## Shows an alert dialog with the given [param text]. + ## Useful for displaying messages like "Incompatible API" etc... + func show_error(text: String) -> void: + return + + ## Returns the node that is the parent of dialogs used in pixelorama. + func get_dialogs_parent_node() -> Node: + return + + ## Tells pixelorama that some dialog is about to open or close. + func dialog_open(open: bool) -> void: + return + + +## Gives access to Tabs and Dockable Container related functions. +class PanelAPI: + ## Sets the visibility of dockable tabs. + var tabs_visible: bool: + set(value): + pass + get: + return false + + ## Adds the [param node] as a tab. Initially it's placed on the same panel as the tools tab, + ## but can be changed through adding custom layouts. + func add_node_as_tab(node: Node) -> void: + return + + ## Removes the [param node] from the DockableContainer. + func remove_node_from_tab(node: Node) -> void: + return + + +## Gives access to theme related functions. +class ThemeAPI: + ## Adds the [param theme] to [code]Edit -> Preferences -> Interface -> Themes[/code]. + func add_theme(theme: Theme) -> void: + return + + ## Returns index of the [param theme] in preferences. + func find_theme_index(theme: Theme) -> int: + return 0 + + ## Returns the current theme resource. + func get_theme() -> Theme: + return + + ## Sets a theme located at a given [param idx] in preferences. If theme set successfully then + ## return [code]true[/code], else [code]false[/code]. + func set_theme(idx: int) -> bool: + return false + + ## Remove the [param theme] from preferences. + func remove_theme(theme: Theme) -> void: + return + + +## Gives ability to add/remove tools. +class ToolAPI: + # gdlint: ignore=constant-name + enum LayerTypes { PIXEL, GROUP, THREE_D } + + ## Adds a tool to pixelorama with name [param tool_name] (without spaces), + ## display name [param display_name], tool scene [param scene], layers that the tool works + ## on [param layer_types] defined by [constant LayerTypes], + ## [param extra_hint] (text that appears when mouse havers tool icon), primary shortcut + ## name [param shortcut] and any extra shortcuts [param extra_shortcuts]. + ## [br][br]At the moment extensions can't make their own shortcuts so you can ignore + ## [param shortcut] and [param extra_shortcuts]. + ## [br] to determine the position of tool in tool list, use [param insert_point] + ## (if you leave it empty then the added tool will be placed at bottom) + func add_tool( + tool_name: String, + display_name: String, + scene: String, + layer_types: PackedInt32Array = [], + extra_hint := "", + shortcut: String = "", + extra_shortcuts: PackedStringArray = [], + insert_point := -1 + ) -> void: + return + + ## Removes a tool with name [param tool_name] + ## and assign Pencil as left tool, Eraser as right tool. + func remove_tool(tool_name: String) -> void: + return + + +## Gives access to pixelorama's selection system. +class SelectionAPI: + ## Clears the selection. + func clear_selection() -> void: + return + + ## Select the entire region of current cel. + func select_all() -> void: + return + + ## Selects a portion defined by [param rect] of the current cel. + ## [param operation] influences it's behaviour with previous selection rects + ## (0 for adding, 1 for subtracting, 2 for intersection). + func select_rect(rect: Rect2i, operation := 0) -> void: + return + + ## Moves a selection to [param destination], + ## with content if [param with_content] is [code]true[/code]. + ## If [param transform_standby] is [code]true[/code] then the transformation will not be + ## applied immediatelyunless [kbd]Enter[/kbd] is pressed. + func move_selection( + destination: Vector2i, with_content := true, transform_standby := false + ) -> void: + return + + ## Resizes the selection to [param new_size], + ## with content if [param with_content] is [code]true[/code]. + ## If [param transform_standby] is [code]true[/code] then the transformation will not be + ## applied immediately unless [kbd]Enter[/kbd] is pressed. + func resize_selection( + new_size: Vector2i, with_content := true, transform_standby := false + ) -> void: + return + + ## Inverts the selection. + func invert() -> void: + return + + ## Makes a project brush out of the current selection's content. + func make_brush() -> void: + return + + ## Returns the portion of current cel's image enclosed by the selection. + ## It's similar to [method make_brush] but it returns the image instead. + func get_enclosed_image() -> Image: + return + + ## Copies the selection content (works in or between pixelorama instances only). + func copy() -> void: + return + + ## Pastes the selection content. + func paste(in_place := false) -> void: + return + + ## Deletes the drawing on current cel enclosed within the selection's area. + func delete_content(selected_cels := true) -> void: + return + + +## Gives access to basic project manipulation functions. +class ProjectAPI: + ## The project currently in focus + var current_project: Project: + set(value): + pass + get: + return + + ## Creates a new project in a new tab with one starting layer and frame, + ## name [param name], size [param size], fill color [param fill_color] and + ## frames [param frames]. The created project also gets returned.[br][br] + ## [param frames] is an [Array] of type [Frame]. Usually it can be left as [code][][/code]. + func new_project( + frames: Array[Frame] = [], + name := tr("untitled"), + size := Vector2(64, 64), + fill_color := Color.TRANSPARENT + ) -> Project: + return + + ## Creates and returns a new [Project] in a new tab, with an optional [param name]. + ## Unlike [method new_project], no starting frame/layer gets created. + ## Useful if you want to deserialize project data. + func new_empty_project(name := tr("untitled")) -> Project: + return + + ## Returns a dictionary containing all the project information. + func get_project_info(project: Project) -> Dictionary: + return {} + + ## Selects the cels and makes the last entry of [param selected_array] as the current cel + ## [param selected_array] is an [Array] of [Arrays] of 2 integers (frame & layer).[br] + ## Frames are counted from left to right, layers are counted from bottom to top. + ## Frames/layers start at "0" and end at [param project.frames.size() - 1] and + ## [param project.layers.size() - 1] respectively. + func select_cels(selected_array := [[0, 0]]) -> void: + return + + ## Returns the current cel. + ## Cel type can be checked using function [method get_class_name] inside the cel + ## type can be GroupCel, PixelCel, Cel3D, or BaseCel. + func get_current_cel() -> BaseCel: + return + + ## Frames are counted from left to right, layers are counted from bottom to top. + ## Frames/layers start at "0" and end at [param project.frames.size() - 1] and + ## [param project.layers.size() - 1] respectively. + func get_cel_at(project: Project, frame: int, layer: int) -> BaseCel: + return + + ## Sets an [param image] at [param frame] and [param layer] on the current project. + ## Frames are counted from left to right, layers are counted from bottom to top. + func set_pixelcel_image(image: Image, frame: int, layer: int) -> void: + return + + ## Adds a new frame in the current project after frame [param after_frame]. + func add_new_frame(after_frame: int) -> void: + print("invalid (after_frame)") + return + + + ## Adds a new Layer of name [param name] in the current project above layer [param above_layer] + ## ([param above_layer] = 0 is the bottom-most layer and so on). + ## [br][param type] = 0 --> PixelLayer, + ## [br][param type] = 1 --> GroupLayer, + ## [br][param type] = 2 --> 3DLayer + func add_new_layer(above_layer: int, name := "", type := 0) -> void: + print("invalid (type)") + print("invalid (above_layer)") + return + + +## Gives access to adding custom exporters. +class ExportAPI: + # gdlint: ignore=constant-name + enum ExportTab { IMAGE = 0, SPRITESHEET = 1} + + ## [param format_info] has keys: [code]extension[/code] and [code]description[/code] + ## whose values are of type [String] e.g:[codeblock] + ## format_info = {"extension": ".gif", "description": "GIF Image"} + ## [/codeblock] + ## [param exporter_generator] is a node with a script containing the method + ## [method override_export] which takes 1 argument of type Dictionary which is automatically + ## passed to [method override_export] at time of export and contains + ## keys: [code]processed_images[/code], [code]export_dialog[/code], + ## [code]export_paths[/code], [code]project[/code][br] + ## (Note: [code]processed_images[/code] is an array of ProcessedImage resource which further + ## has parameters [param image] and [param duration])[br] + ## If the value of [param tab] is not in [constant ExportTab] then the format will be added to + ## both tabs. Returns the index of exporter, which can be used to remove exporter later. + func add_export_option( + format_info: Dictionary, + exporter_generator: Object, + tab := ExportTab.IMAGE, + is_animated := true + ) -> int: + return 0 + + ## Removes the exporter with [param id] from Pixelorama. + func remove_export_option(id: int) -> void: + return + + +## Gives access to adding custom import options. +class ImportAPI: + ## [param import_scene] is a scene preload that will be instanced and added to "import options" + ## section of pixelorama's import dialogs and will appears whenever [param import_name] is + ## chosen from import menu. + ## [br] + ## [param import_scene] must have a a script containing:[br] + ## 1. An optional variable named [code]import_preview_dialog[/code] of type [ConfirmationDialog], + ## If present, it will automatically be assigned a reference to the relevant import dialog's + ## [code]ImportPreviewDialog[/code] class so that you can easily access variables and + ## methods of that class. (This variable is meant to be read-only)[br] + ## 2. The method [method initiate_import] which takes 2 arguments: [code]path[/code], + ## [code]image[/code], which are automatically passed to [method initiate_import] at + ## time of import. + func add_import_option(import_name: StringName, import_scene_preload: PackedScene) -> int: + return 0 + + ## Removes the import option with [param id] from Pixelorama. + func remove_import_option(id: int) -> void: + return + + +## Gives access to palettes. +class PaletteAPI: + ## Creates and adds a new [Palette] with name [param palette_name] with [param data] + ## in the form of a [Dictionary]. + func create_palette_from_data(palette_name: String, data: Dictionary) -> void: + return + + +## Gives access to the basic commonly used signals. +## +## Gives access to the basic commonly used signals. +## Some less common signals are not mentioned in Api but could be accessed through source directly. +class SignalsAPI: + # APP RELATED SIGNALS + ## Connects/disconnects a signal to [param callable], that emits + ## when pixelorama is just opened. + func signal_pixelorama_opened(callable: Callable, is_disconnecting := false) -> void: + return + + ## Connects/disconnects a signal to [param callable], that emits + ## when pixelorama is about to close. + func signal_pixelorama_about_to_close(callable: Callable, is_disconnecting := false) -> void: + return + + # PROJECT RELATED SIGNALS + ## Connects/disconnects a signal to [param callable], that emits + ## whenever a new project is created.[br] + ## [b]Binds: [/b]It has one bind of type [code]Project[/code] which is the newly created project + func signal_project_created(callable: Callable, is_disconnecting := false) -> void: + return + + ## Connects/disconnects a signal to [param callable], that emits + ## after a project is saved. + func signal_project_saved(callable: Callable, is_disconnecting := false) -> void: + return + + ## Connects/disconnects a signal to [param callable], that emits + ## whenever you switch to some other project. + func signal_project_switched(callable: Callable, is_disconnecting := false) -> void: + return + + ## Connects/disconnects a signal to [param callable], that emits + ## whenever you select a different cel. + func signal_cel_switched(callable: Callable, is_disconnecting := false) -> void: + return + + ## Connects/disconnects a signal to [param callable], that emits + ## whenever the project data are being modified. + func signal_project_data_changed(callable: Callable, is_disconnecting := false) -> void: + return + + # TOOL RELATED SIGNALS + ## Connects/disconnects a signal to [param callable], that emits + ## whenever a tool changes color.[br] + ## [b]Binds: [/b] It has two bind of type [Color] (indicating new color) + ## and [int] (Indicating button that tool is assigned to, see [enum @GlobalScope.MouseButton]) + func signal_tool_color_changed(callable: Callable, is_disconnecting := false) -> void: + return + + # TIMELINE RELATED SIGNALS + ## Connects/disconnects a signal to [param callable], that emits + ## whenever timeline animation starts.[br] + ## [b]Binds: [/b] It has one bind of type [bool] which indicated if animation is in + ## forward direction ([code]true[/code]) or backward direction ([code]false[/code]) + func signal_timeline_animation_started(callable: Callable, is_disconnecting := false) -> void: + return + + ## Connects/disconnects a signal to [param callable], that emits + ## whenever timeline animation stops. + func signal_timeline_animation_finished(callable: Callable, is_disconnecting := false) -> void: + return + + # UPDATER SIGNALS + ## Connects/disconnects a signal to [param callable], that emits + ## whenever texture of the currently focused cel changes. + func signal_current_cel_texture_changed(callable: Callable, is_disconnecting := false) -> void: + return + + ## Connects/disconnects a signal to [param callable], that emits + ## whenever preview is about to be drawn.[br] + ## [b]Binds: [/b]It has one bind of type [Dictionary] with keys: [code]exporter_id[/code], + ## [code]export_tab[/code], [code]preview_images[/code], [code]durations[/code] + ## [br] Use this if you plan on changing preview of export + func signal_export_about_to_preview(callable: Callable, is_disconnecting := false) -> void: + return diff --git a/src/Extensions/Audia/Classes/ShotcutMaker.gd b/src/Extensions/Audia/Classes/ShotcutMaker.gd new file mode 100644 index 0000000..6a072df --- /dev/null +++ b/src/Extensions/Audia/Classes/ShotcutMaker.gd @@ -0,0 +1,87 @@ +extends RefCounted + +var p_path: String +var dir_path: String + +const STARTING = (""" + + """) + +var producers := [] + +const MIDDLE = (""" + 2 + 1""") + +var entry := [] + +const END = (""" +""") + + +func compile(size: Vector2): + var final = STARTING.replace("", str(size.x)).replace("", str(size.y)) + for data in producers: + final += data + final += MIDDLE + for data in entry: + final += data + final += END + if p_path: + var file = FileAccess.open(p_path, FileAccess.WRITE) + file.store_string(final) + file.close() + + +func add_item_to_playlist(path: String, clip_duration: float): + if !p_path: + p_path = path.replace(path.get_file(), "project.mlt") + var raw = ("""\n + + pause + + 1 + 1 + 1 + 2 + qimage + was here + """) + var entry_raw = (""" """) + raw = raw.replace("", str("producer", producers.size())) + raw = raw.replace("", calculate_duration(clip_duration)) + entry_raw = entry_raw.replace("", str("producer", producers.size())) + entry_raw = entry_raw.replace("", calculate_duration(clip_duration)) + raw = raw.replace("", path.get_file()) + producers.append(raw) + entry.append(entry_raw) + + +func calculate_duration(clip_duration: float) -> String: + clip_duration = snappedf(clip_duration, 0.01) + var millis = clip_duration - floorf(clip_duration) + if millis > 0: + var millis_test = str_to_var(str(millis).get_slice(".", 1)) + var adder = "" + if millis_test < 100: + adder = "0" + if millis_test < 10: + adder += "0" + if millis < 0.1: + millis = str(adder, millis_test) + else: + millis = str(millis_test, adder) + else: + millis = "000" + var secs = floor(clip_duration) + var minutes = floor(secs / 60.0) + var hours = floor(minutes / 60.0) + secs = int(secs) % 60 + minutes = int(minutes) % 60 + if secs < 10: + secs = str("0", secs) + if minutes < 10: + minutes = str("0", minutes) + if hours < 10: + hours = str("0", hours) + return str(hours, ":", minutes, ":", secs, ".", millis) diff --git a/src/Extensions/Audia/Elements/3rd party/GDScriptAudioImport.gd b/src/Extensions/Audia/Elements/3rd party/GDScriptAudioImport.gd new file mode 100644 index 0000000..9b09e01 --- /dev/null +++ b/src/Extensions/Audia/Elements/3rd party/GDScriptAudioImport.gd @@ -0,0 +1,254 @@ +#GDScriptAudioImport v0.1 (using a fork that updates to godot 4) +# upstream: https://github.com/Stoxis/GDScriptAudioImport-Godot4/blob/master/GDScriptAudioImport.gd +# commit: 21b7d2922d37fd1e8606489e044465e9d8a2f810 +# changed lines 157-161 according to comment https://github.com/Gianclgar/GDScriptAudioImport/pull/20#issuecomment-2067591508 +# commented lines 34, 154, 160, 166 + + +#GDScriptAudioImport v0.1 + +#MIT License +# +#Copyright (c) 2020 Gianclgar (Giannino Clemente) gianclgar@gmail.com +# +#Permission is hereby granted, free of charge, to any person obtaining a copy +#of this software and associated documentation files (the "Software"), to deal +#in the Software without restriction, including without limitation the rights +#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +#copies of the Software, and to permit persons to whom the Software is +#furnished to do so, subject to the following conditions: +# +#The above copyright notice and this permission notice shall be included in all +#copies or substantial portions of the Software. +# +#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +#SOFTWARE. + +#I honestly don't care that much, Kopimi ftw, but it's my little baby and I want it to look nice :3 + +#class_name AudioLoader + +func report_errors(err, filepath): + # See: https://docs.godotengine.org/en/latest/classes/class_@globalscope.html#enum-globalscope-error + var result_hash = { + ERR_FILE_NOT_FOUND: "File: not found", + ERR_FILE_BAD_DRIVE: "File: Bad drive error", + ERR_FILE_BAD_PATH: "File: Bad path error.", + ERR_FILE_NO_PERMISSION: "File: No permission error.", + ERR_FILE_ALREADY_IN_USE: "File: Already in use error.", + ERR_FILE_CANT_OPEN: "File: Can't open error.", + ERR_FILE_CANT_WRITE: "File: Can't write error.", + ERR_FILE_CANT_READ: "File: Can't read error.", + ERR_FILE_UNRECOGNIZED: "File: Unrecognized error.", + ERR_FILE_CORRUPT: "File: Corrupt error.", + ERR_FILE_MISSING_DEPENDENCIES: "File: Missing dependencies error.", + ERR_FILE_EOF: "File: End of file (EOF) error." + } + if err in result_hash: + print("Error: ", result_hash[err], " ", filepath) + else: + print("Unknown error with file ", filepath, " error code: ", err) + +func loadfile(filepath): + var file = FileAccess.open(filepath, FileAccess.READ) + var err = file.get_error() + if err != OK: + report_errors(err, filepath) + file.close() + return AudioStreamWAV.new() + + var bytes = file.get_buffer(file.get_length()) + # if File is wav + if filepath.ends_with(".wav"): + var newstream = AudioStreamWAV.new() + + #--------------------------- + #parrrrseeeeee!!! :D + + var bits_per_sample = 0 + + for i in range(0, 100): + var those4bytes = str(char(bytes[i])+char(bytes[i+1])+char(bytes[i+2])+char(bytes[i+3])) + + if those4bytes == "RIFF": + print ("RIFF OK at bytes " + str(i) + "-" + str(i+3)) + #RIP bytes 4-7 integer for now + if those4bytes == "WAVE": + print ("WAVE OK at bytes " + str(i) + "-" + str(i+3)) + + if those4bytes == "fmt ": + print ("fmt OK at bytes " + str(i) + "-" + str(i+3)) + + #get format subchunk size, 4 bytes next to "fmt " are an int32 + var formatsubchunksize = bytes[i+4] + (bytes[i+5] << 8) + (bytes[i+6] << 16) + (bytes[i+7] << 24) + print ("Format subchunk size: " + str(formatsubchunksize)) + + #using formatsubchunk index so it's easier to understand what's going on + var fsc0 = i+8 #fsc0 is byte 8 after start of "fmt " + + #get format code [Bytes 0-1] + var format_code = bytes[fsc0] + (bytes[fsc0+1] << 8) + var format_name + if format_code == 0: format_name = "8_BITS" + elif format_code == 1: format_name = "16_BITS" + elif format_code == 2: format_name = "IMA_ADPCM" + else: + format_name = "UNKNOWN (trying to interpret as 16_BITS)" + format_code = 1 + print ("Format: " + str(format_code) + " " + format_name) + #assign format to our AudioStreamSample + newstream.format = format_code + + #get channel num [Bytes 2-3] + var channel_num = bytes[fsc0+2] + (bytes[fsc0+3] << 8) + print ("Number of channels: " + str(channel_num)) + #set our AudioStreamSample to stereo if needed + if channel_num == 2: newstream.stereo = true + + #get sample rate [Bytes 4-7] + var sample_rate = bytes[fsc0+4] + (bytes[fsc0+5] << 8) + (bytes[fsc0+6] << 16) + (bytes[fsc0+7] << 24) + print ("Sample rate: " + str(sample_rate)) + #set our AudioStreamSample mixrate + newstream.mix_rate = sample_rate + + #get byte_rate [Bytes 8-11] because we can + var byte_rate = bytes[fsc0+8] + (bytes[fsc0+9] << 8) + (bytes[fsc0+10] << 16) + (bytes[fsc0+11] << 24) + print ("Byte rate: " + str(byte_rate)) + + #same with bits*sample*channel [Bytes 12-13] + var bits_sample_channel = bytes[fsc0+12] + (bytes[fsc0+13] << 8) + print ("BitsPerSample * Channel / 8: " + str(bits_sample_channel)) + + #aaaand bits per sample/bitrate [Bytes 14-15] + bits_per_sample = bytes[fsc0+14] + (bytes[fsc0+15] << 8) + print ("Bits per sample: " + str(bits_per_sample)) + + if those4bytes == "data": + assert(bits_per_sample != 0) + + var audio_data_size = bytes[i+4] + (bytes[i+5] << 8) + (bytes[i+6] << 16) + (bytes[i+7] << 24) + print ("Audio data/stream size is " + str(audio_data_size) + " bytes") + + var data_entry_point = (i+8) + print ("Audio data starts at byte " + str(data_entry_point)) + + var data = bytes.slice(data_entry_point, data_entry_point + audio_data_size) + + if bits_per_sample in [24, 32]: + newstream.data = convert_to_16bit(data, bits_per_sample) + else: + newstream.data = data + # end of parsing + #--------------------------- + + #Calculate the size of each sample based on bits_per_sample + var sample_size = bits_per_sample / 8 + #get samples and set loop end + var samplenum = newstream.data.size() / sample_size + newstream.loop_end = samplenum + #newstream.loop_mode = 0 #change to 0 or delete this line if you don't want loop, also check out modes 2 and 3 in the docs + return newstream #:D + + #if file is ogg + elif filepath.ends_with(".ogg"): + var newstream = AudioStreamOggVorbis.load_from_buffer(bytes) + #newstream.loop = true #set to false or delete this line if you don't want to loop + return newstream + + #if file is mp3 + elif filepath.ends_with(".mp3"): + var newstream = AudioStreamMP3.new() + #newstream.loop = true #set to false or delete this line if you don't want to loop + newstream.data = bytes + return newstream + + else: + print ("ERROR: Wrong filetype or format") + file.close() + +# Converts .wav data from 24 or 32 bits to 16 +# +# These conversions are SLOW in GDScript +# on my one test song, 32 -> 16 was around 3x slower than 24 -> 16 +# +# I couldn't get threads to help very much +# They made the 24bit case about 2x faster in my test file +# And the 32bit case abour 50% slower +# I don't wanna risk it always being slower on other files +# And really, the solution would be to handle it in a low-level language +func convert_to_16bit(data: PackedByteArray, from: int) -> PackedByteArray: + print("converting to 16-bit from %d" % from) + var time = Time.get_ticks_msec() + if from == 24: + var j = 0 + for i in range(0, data.size(), 3): + data[j] = data[i+1] + data[j+1] = data[i+2] + j += 2 + data.resize(data.size() * 2 / 3) + if from == 32: + var spb := StreamPeerBuffer.new() + var single_float: float + var value: int + for i in range(0, data.size(), 4): + var sub_array = data.slice(i, i+4) + spb.data_array = PackedByteArray(sub_array) + single_float = spb.get_float() + value = single_float * 32768 + data[i/2] = value + data[i/2+1] = value >> 8 + data.resize(data.size() / 2) + print("Took %f seconds for slow conversion" % ((Time.get_ticks_msec() - time) / 1000.0)) + return data + +# ---------- REFERENCE --------------- +# note: typical values doesn't always match + +#Positions Typical Value Description +# +#1 - 4 "RIFF" Marks the file as a RIFF multimedia file. +# Characters are each 1 byte long. +# +#5 - 8 (integer) The overall file size in bytes (32-bit integer) +# minus 8 bytes. Typically, you'd fill this in after +# file creation is complete. +# +#9 - 12 "WAVE" RIFF file format header. For our purposes, it +# always equals "WAVE". +# +#13-16 "fmt " Format sub-chunk marker. Includes trailing null. +# +#17-20 16 Length of the rest of the format sub-chunk below. +# +#21-22 1 Audio format code, a 2 byte (16 bit) integer. +# 1 = PCM (pulse code modulation). +# +#23-24 2 Number of channels as a 2 byte (16 bit) integer. +# 1 = mono, 2 = stereo, etc. +# +#25-28 44100 Sample rate as a 4 byte (32 bit) integer. Common +# values are 44100 (CD), 48000 (DAT). Sample rate = +# number of samples per second, or Hertz. +# +#29-32 176400 (SampleRate * BitsPerSample * Channels) / 8 +# This is the Byte rate. +# +#33-34 4 (BitsPerSample * Channels) / 8 +# 1 = 8 bit mono, 2 = 8 bit stereo or 16 bit mono, 4 +# = 16 bit stereo. +# +#35-36 16 Bits per sample. +# +#37-40 "data" Data sub-chunk header. Marks the beginning of the +# raw data section. +# +#41-44 (integer) The number of bytes of the data section below this +# point. Also equal to (#ofSamples * #ofChannels * +# BitsPerSample) / 8 +# +#45+ The raw audio data. diff --git a/src/Extensions/Audia/Elements/AudioManager/AudioController.gd b/src/Extensions/Audia/Elements/AudioManager/AudioController.gd new file mode 100644 index 0000000..b551a16 --- /dev/null +++ b/src/Extensions/Audia/Elements/AudioManager/AudioController.gd @@ -0,0 +1,31 @@ +extends AudioStreamPlayer + +var timer: Timer +var end_time: float + +func start(current_frame: int, tag: AnimationTag, streamer: AudioStream): + var project = ExtensionsApi.project.current_project + timer = ExtensionsApi.general.get_global().animation_timer + + name = tag.name + + var start_time: float = 0 + for frame_idx in range(tag.from - 1, tag.to): + var frame: Frame = project.frames[frame_idx] + var duration = frame.duration * (1.0 / project.fps) + if frame_idx < current_frame: + start_time += duration + else: + end_time += duration + if streamer: + stream = streamer + $Timer.wait_time = end_time + $Timer.start() + play(start_time) + + +func _on_AudioController_finished() -> void: + queue_free() + +func _on_Timer_timeout() -> void: + stop() diff --git a/src/Extensions/Audia/Elements/AudioManager/AudioController.tscn b/src/Extensions/Audia/Elements/AudioManager/AudioController.tscn new file mode 100644 index 0000000..b823196 --- /dev/null +++ b/src/Extensions/Audia/Elements/AudioManager/AudioController.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://b3lucmb1cwcl2"] + +[ext_resource type="Script" path="res://src/Extensions/Audia/Elements/AudioManager/AudioController.gd" id="1"] + +[node name="AudioController" type="AudioStreamPlayer"] +script = ExtResource("1") + +[node name="Timer" type="Timer" parent="."] +one_shot = true + +[connection signal="finished" from="." to="." method="_on_AudioController_finished"] +[connection signal="timeout" from="Timer" to="." method="_on_Timer_timeout"] diff --git a/src/Extensions/Audia/Elements/MusicButton/MusicEntry.gd b/src/Extensions/Audia/Elements/MusicButton/MusicEntry.gd new file mode 100644 index 0000000..a135ef0 --- /dev/null +++ b/src/Extensions/Audia/Elements/MusicButton/MusicEntry.gd @@ -0,0 +1,96 @@ +extends PanelContainer + +var _path: String + +@onready var path_label: Label = $"%Path3D" +@onready var identifier_label: LineEdit = $"%Identifier" + +var old_p_size := Vector2.ZERO +var Audioloader = load("res://src/Extensions/Audia/Elements/3rd party/GDScriptAudioImport.gd") +var reference_data: Dictionary # only used to see which is which +var audio_stream: AudioStream + + +func _ready() -> void: + ExtensionsApi.signals.signal_cel_switched(check_if_timeline_refreshed) + + +func serialize() -> Dictionary: + var data = { + "path": _path, + "identifier": identifier_label.text, + } + return data + + +func deserialize(data: Dictionary) -> void: + if data.has("path"): + _path = data["path"] + path_label.text = _path + path_label.tooltip_text = _path + var new_loader = Audioloader.new() + audio_stream = new_loader.loadfile(_path) + if !FileAccess.file_exists(_path): + path_label.self_modulate = Color.ORANGE_RED + if data.has("identifier"): + identifier_label.text = data["identifier"] + reference_data = data + _update_tag(identifier_label.text) + + +func prepare_stream(): + var new_loader = Audioloader.new() + audio_stream = new_loader.loadfile(_path) + + +func _on_Identifier_text_changed(_new_text: String) -> void: + var project: Project = ExtensionsApi.project.current_project + var data: Array = project.get_meta("Music", []) + data.erase(reference_data) + _update_tag(reference_data["identifier"]) + reference_data = serialize() + data.append(reference_data) + + +func _on_Close_pressed() -> void: + var project: Project = ExtensionsApi.project.current_project + var data: Array = project.get_meta("Music", []) + data.erase(reference_data) + queue_free() + + +func _update_tag(old_name: String ,full_refresh := true): + var tag_container = ExtensionsApi.general.get_global().tag_container + for child: Control in tag_container.get_children(): + if child.tag.name == old_name: + for element in child.get_children(): + if element.is_in_group("AudioIndicator"): + element.queue_free() + if full_refresh: + if child.tag.name == identifier_label.text: + var new_indicator := TextureRect.new() + child.add_child(new_indicator) + new_indicator.add_to_group("AudioIndicator") + new_indicator.modulate = child.tag.color + new_indicator.texture = preload("res://src/Extensions/Audia/music_icon.png") + new_indicator.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + new_indicator.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + new_indicator.size = Vector2.ONE * child.size.y / 1.5 + + +func check_if_timeline_refreshed(): + var project = ExtensionsApi.project.current_project + var new_size = Vector2(project.frames.size(), project.layers.size()) + if old_p_size != new_size: + old_p_size = new_size + await get_tree().process_frame + refresh_self() + + +func refresh_self(): + _update_tag(identifier_label.text) + + +func _exit_tree() -> void: + _update_tag(identifier_label.text, false) + ExtensionsApi.signals.signal_cel_switched(check_if_timeline_refreshed, true) diff --git a/src/Extensions/Audia/Elements/MusicButton/MusicEntry.tscn b/src/Extensions/Audia/Elements/MusicButton/MusicEntry.tscn new file mode 100644 index 0000000..90989c9 --- /dev/null +++ b/src/Extensions/Audia/Elements/MusicButton/MusicEntry.tscn @@ -0,0 +1,57 @@ +[gd_scene load_steps=2 format=3 uid="uid://b6m6whsl7rckf"] + +[ext_resource type="Script" path="res://src/Extensions/Audia/Elements/MusicButton/MusicEntry.gd" id="1"] + +[node name="MusicEntry" type="PanelContainer"] +custom_minimum_size = Vector2(200, 70) +offset_right = 200.0 +offset_bottom = 70.0 +script = ExtResource("1") + +[node name="Panel" type="Panel" parent="."] +self_modulate = Color(1, 1, 1, 0.309804) +layout_mode = 2 + +[node name="Options" type="VBoxContainer" parent="Panel"] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 3.0 +offset_top = 3.0 +offset_right = -3.0 +offset_bottom = -1.0 + +[node name="HBoxContainer" type="HBoxContainer" parent="Panel/Options"] +layout_mode = 2 + +[node name="Path3D" type="Label" parent="Panel/Options/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +mouse_filter = 0 +text = "Path3D" +clip_text = true +max_lines_visible = 2 + +[node name="Close" type="Button" parent="Panel/Options/HBoxContainer"] +layout_mode = 2 +text = "X" + +[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/Options"] +layout_mode = 2 + +[node name="Label" type="Label" parent="Panel/Options/HBoxContainer2"] +layout_mode = 2 +text = "Play on Tag: " + +[node name="Identifier" type="LineEdit" parent="Panel/Options/HBoxContainer2"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "treats a tag with this name as an audio clip" +placeholder_text = "Identifier (Use Tag name)" +caret_blink = true +caret_blink_interval = 0.5 + +[connection signal="pressed" from="Panel/Options/HBoxContainer/Close" to="." method="_on_Close_pressed"] +[connection signal="text_changed" from="Panel/Options/HBoxContainer2/Identifier" to="." method="_on_Identifier_text_changed"] diff --git a/src/Extensions/Audia/Elements/MusicListContainer.gd b/src/Extensions/Audia/Elements/MusicListContainer.gd new file mode 100644 index 0000000..69afd42 --- /dev/null +++ b/src/Extensions/Audia/Elements/MusicListContainer.gd @@ -0,0 +1,210 @@ +extends PanelContainer + + +# references from pixelorama +var timer: Timer + +var audio_tags: Dictionary +var stream_player = preload("res://src/Extensions/Audia/Elements/AudioManager/AudioController.tscn") +var current_playing_frame := -1 +var is_playing := false +var streams_are_prepared := false + +@onready var option_button: OptionButton = $"%OptionButton" +@onready var list: VBoxContainer = $"%List" +@onready var audios: Node = $Audios + + +func _ready() -> void: + await get_tree().process_frame + # update the music library + update_list() + # Get references to widely used nodes + timer = ExtensionsApi.general.get_global().animation_timer + # signal connections + ExtensionsApi.signals.signal_project_switched(update_list) + ExtensionsApi.general.get_global().project_about_to_switch.connect(disconnect_old_project_signals) + ExtensionsApi.signals.signal_cel_switched(check_sanity) + timer.timeout.connect(manage_next_frame_music) + ExtensionsApi.general.get_global().animation_timeline.animation_started.connect(manage_setup) + ExtensionsApi.general.get_global().animation_timeline.animation_finished.connect(manage_setup) + get_tree().root.files_dropped.connect(add_audios) + # setup audio drivers list + setup_audio_drivers() + + +func disconnect_old_project_signals(): + var project := ExtensionsApi.project.current_project + project.undo_redo.version_changed.disconnect(check_tag_update) + + +func _exit_tree() -> void: + # disconnect signals connected in this script + ExtensionsApi.signals.signal_project_switched(update_list, true) + ExtensionsApi.signals.signal_cel_switched(check_sanity, true) + timer.timeout.disconnect(manage_next_frame_music) + ExtensionsApi.general.get_global().animation_timeline.animation_started.disconnect(manage_setup) + ExtensionsApi.general.get_global().animation_timeline.animation_finished.disconnect(manage_setup) + get_tree().root.files_dropped.disconnect(add_audios) + disconnect_old_project_signals() + + +func add_audios(file_paths: PackedStringArray, _screen: int = 0): + var project: Project = ExtensionsApi.project.current_project + var data: Array = project.get_meta("Music", []) + var valid_exts = ["mp3", "ogg", "wav"] + var audio_added := false + var old_paths := PackedStringArray() + for entry in data: + old_paths.append(entry["path"]) + for path in file_paths: + if path.get_extension().to_lower() in valid_exts: + audio_added = true + ExtensionsApi.dialog.show_error("Added asuccessfully") + if path in old_paths: + continue + var music_dict = { + "path": path, + "identifier": path.get_file().trim_suffix(str(".", path.get_extension())), + } + data.append(music_dict) + if audio_added: + project.set_meta("Music", data) + update_list() + + +func update_list(): + var project: Project = ExtensionsApi.project.current_project + # the project has changed, so change the undo_redo as well + if not project.undo_redo.version_changed.is_connected(check_tag_update): + project.undo_redo.version_changed.connect(check_tag_update) + + var data = project.get_meta("Music", []) + for old_entry in list.get_children(): + old_entry.queue_free() + for entry_data in data: + var entry = preload("res://src/Extensions/Audia/Elements/MusicButton/MusicEntry.tscn").instantiate() + list.add_child(entry) + entry.deserialize(entry_data) + + +func check_tag_update(): + var undo_redo = ExtensionsApi.project.current_project.undo_redo + if undo_redo.get_current_action_name() in ["Modify Frame Tag", "Resize Frame Tag"]: + var tag_container = ExtensionsApi.general.get_global().tag_container + # clear old indicators + for child in tag_container.get_children(): + var indicator = child.get_node_or_null("AudioIndicator") + if indicator: + indicator.queue_free() + for child in list.get_children(): + child.refresh_self() + + +func manage_setup(play_forward: bool = false): + audio_tags = {} + if play_forward: + for music_entry in list.get_children(): + music_entry.prepare_stream() + var data = music_entry.serialize() + if !data["identifier"] in audio_tags.keys(): + audio_tags[data["identifier"]] = music_entry.audio_stream.duplicate() + manage_start_stop(play_forward) + + +# manages initial setup when farward play button is pressed +func manage_start_stop(play_forward: bool): + if !play_forward: + for player in audios.get_children(): + player.queue_free() + current_playing_frame = -1 + is_playing = false + return + if is_playing: + return + is_playing = true + + var animation_tags: Array = ExtensionsApi.project.current_project.animation_tags + var current_frame: int = ExtensionsApi.project.current_project.current_frame + current_playing_frame = current_frame + for tag in animation_tags: + if current_frame + 1 >= tag.from && current_frame + 1 <= tag.to: + if tag.name in audio_tags.keys(): + var new_stream_player = stream_player.instantiate() + audios.add_child(new_stream_player) + new_stream_player.start(current_frame, tag, audio_tags[tag.name]) + + +# manages if a music should be played or not (i-e checks if we entered an audio tag) +func manage_next_frame_music(): + var animation_tags: Array = ExtensionsApi.project.current_project.animation_tags + var current_frame: int = ExtensionsApi.project.current_project.current_frame + for tag in animation_tags: + if current_frame + 1 == tag.from: + if tag.name in audio_tags.keys() and current_playing_frame != -1: + var new_stream_player = stream_player.instantiate() + audios.add_child(new_stream_player) + new_stream_player.start(current_frame, tag, audio_tags[tag.name]) + is_playing = true + if !is_playing and current_playing_frame != -1: + manage_start_stop(true) + current_playing_frame = current_frame + + +# Detects if user clicked on another frame during playing +func check_sanity(): + var real_current_frame: int = ExtensionsApi.project.current_project.current_frame + var expected_current_frame: int = current_playing_frame + 1 + if real_current_frame != expected_current_frame: + manage_start_stop(false) + if ( + real_current_frame > expected_current_frame + and current_playing_frame != -1 + ): + manage_start_stop(true) + + +# An Audio driver has been chosen +func _on_OptionButton_item_selected(index: int) -> void: + ProjectSettings.set_initial_value("audio/driver/driver", "Dummy") + var driver_name = get_real_driver_name(option_button.get_item_text(index)) + ProjectSettings.set_setting("audio/driver/driver", driver_name) + ProjectSettings.save_custom(ExtensionsApi.general.get_global().OVERRIDE_FILE) + ExtensionsApi.dialog.show_error("Please restart to allow changes to take effect") + + +func setup_audio_drivers(): + var current_audio_driver = ProjectSettings.get_setting("audio/driver/driver") + var to_select: int + # (commenting this till this gets resolved) + #for i in OS.get_audio_driver_count(): + #var driver_name = OS.get_audio_driver_name(i) + #option_button.add_item(OS.get_audio_driver_name(i)) + #if driver_name == current_audio_driver: + #to_select = i + var drivers = ["Dummy (No Audio)", "System Default"] + for i in drivers.size(): + var driver_name = drivers[i] + option_button.add_item(drivers[i]) + if get_real_driver_name(driver_name) == current_audio_driver: + to_select = i + if to_select: + option_button.select(to_select) + + +## Workarround for missing OS.get_audio_driver_count() and OS.get_audio_driver_name() +func get_real_driver_name(d_name) -> String: + match d_name: + "Dummy (No Audio)": + return "Dummy" + "System Default": + return "" + return "" + + +func _on_Load_pressed() -> void: + $Popups/FileDialog.popup_centered() + + +func _on_window_close_requested() -> void: + get_parent().visible = false diff --git a/src/Extensions/Audia/Elements/MusicListContainer.tscn b/src/Extensions/Audia/Elements/MusicListContainer.tscn new file mode 100644 index 0000000..c020419 --- /dev/null +++ b/src/Extensions/Audia/Elements/MusicListContainer.tscn @@ -0,0 +1,75 @@ +[gd_scene load_steps=2 format=3 uid="uid://c82pwekjvftdi"] + +[ext_resource type="Script" path="res://src/Extensions/Audia/Elements/MusicListContainer.gd" id="1"] + +[node name="Window" type="Window"] +position = Vector2i(0, 36) +size = Vector2i(435, 385) +visible = false +exclusive = true + +[node name="MusicListContainer" type="PanelContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1") + +[node name="VBoxContainer" type="VBoxContainer" parent="MusicListContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="MusicListContainer/VBoxContainer"] +layout_mode = 2 +theme_type_variation = &"HeaderSmall" +text = "Audio Driver:" + +[node name="OptionButton" type="OptionButton" parent="MusicListContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="Label2" type="RichTextLabel" parent="MusicListContainer/VBoxContainer"] +self_modulate = Color(0.8, 0.8, 0.8, 1) +layout_mode = 2 +text = "Accepts: mp3, ogg, wav +Usage: drag and drop. Set identifier to any tag's name." +fit_content = true +scroll_active = false + +[node name="Label3" type="Label" parent="MusicListContainer/VBoxContainer"] +layout_mode = 2 +theme_type_variation = &"HeaderSmall" +text = "Music Library:" + +[node name="ScrollContainer" type="ScrollContainer" parent="MusicListContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="List" type="VBoxContainer" parent="MusicListContainer/VBoxContainer/ScrollContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/separation = 6 + +[node name="Load" type="Button" parent="MusicListContainer/VBoxContainer"] +layout_mode = 2 +text = "Open Audios" + +[node name="Popups" type="Control" parent="MusicListContainer"] +layout_mode = 2 +mouse_filter = 2 + +[node name="FileDialog" type="FileDialog" parent="MusicListContainer/Popups"] +mode = 1 +title = "Open File(s)" +size = Vector2i(651, 414) +ok_button_text = "Open" +file_mode = 1 +access = 2 +filters = PackedStringArray("*.mp3", "*.ogg", "*.wav") + +[node name="Audios" type="Node" parent="MusicListContainer"] + +[connection signal="close_requested" from="." to="MusicListContainer" method="_on_window_close_requested"] +[connection signal="item_selected" from="MusicListContainer/VBoxContainer/OptionButton" to="MusicListContainer" method="_on_OptionButton_item_selected"] +[connection signal="pressed" from="MusicListContainer/VBoxContainer/Load" to="MusicListContainer" method="_on_Load_pressed"] +[connection signal="files_selected" from="MusicListContainer/Popups/FileDialog" to="MusicListContainer" method="add_audios"] diff --git a/src/Extensions/Audia/Main.gd b/src/Extensions/Audia/Main.gd new file mode 100644 index 0000000..f17eff1 --- /dev/null +++ b/src/Extensions/Audia/Main.gd @@ -0,0 +1,88 @@ +extends Node + +# some references to nodes that will be created later +var music_list_container_dialog: Window +var exporter_id: int +var menu_id: int + +# This script acts as a setup for the extension +func _enter_tree() -> void: + # add a test panel as a tab (this is an example) the tab is located at the same + # place as the (Tools tab) by default + music_list_container_dialog = preload( + "res://src/Extensions/Audia/Elements/MusicListContainer.tscn" + ).instantiate() + +# ExtensionsApi.panel.add_node_as_tab(music_list_container_dialog) + ExtensionsApi.dialog.get_dialogs_parent_node().add_child(music_list_container_dialog) + menu_id = ExtensionsApi.menu.add_menu_item(ExtensionsApi.menu.WINDOW, "Audia", self) + + var info := { + "extension": ".png", + "description": "Shotcut" + } + var export_tab := ExtensionsApi.export.ExportTab.IMAGE + exporter_id = ExtensionsApi.export.add_export_option(info, self, export_tab, false) + + +func menu_item_clicked(): + music_list_container_dialog.popup_centered() + + +func override_export(data: Dictionary): + # set variables + var dir_path: String + var project = data["project"] + var project_maker = load("res://src/Extensions/Audia/Classes/ShotcutMaker.gd").new() + var moved_audios := [] + var p_size: Vector2 + + # obtaining audios used in project + var audio_tags := {} + for child in music_list_container_dialog.get_node("MusicListContainer").list.get_children(): + var dict = child.serialize() + var tag = dict["identifier"] + var path = dict["path"] + if !tag in audio_tags.keys(): + audio_tags[tag] = path + + # save pngs + for image_idx in data["processed_images"].size(): + var save_path = data["export_paths"][image_idx] + var image: Image = data["processed_images"][image_idx].image + var duration = data["processed_images"][image_idx].duration + image.save_png(save_path) + # set the dir path and project size (one time setup) + if !dir_path: + dir_path = save_path.replace(save_path.get_file(), "") + p_size = image.get_size() + + # check if audio is to be played for this frame + for tag in project.animation_tags: + if tag.name in audio_tags.keys() and tag.from == image_idx + 1: # Audio Detected + var audio_path: String = audio_tags[tag.name] + var new_name = str(tag.name, ".", audio_path.get_extension()) + var new_path: String = dir_path.path_join(new_name) + # calculate duration for audio + var end_time := 0.0 + for frame_idx in range(tag.from - 1, tag.to): + var frame: Frame = project.frames[frame_idx] + var audio_duration = frame.duration * (1.0 / project.fps) + end_time += audio_duration + # if audio wasn't moved to aseet folder yet then move it there + if !new_path in moved_audios: + DirAccess.copy_absolute(audio_path, new_path) + moved_audios.append(new_path) + project_maker.add_item_to_playlist(new_path, end_time) + project_maker.add_item_to_playlist(data["export_paths"][image_idx], duration) + # Now Compile all this information into a ShotCut project + project_maker.compile(p_size) + return true + + +func _exit_tree() -> void: # Extension is being uninstalled or disabled + # remember to remove things that you added using this extension +# ExtensionsApi.panel.remove_node_from_tab(music_list_container_dialog) + ExtensionsApi.menu.remove_menu_item(ExtensionsApi.menu.WINDOW, menu_id) + music_list_container_dialog.queue_free() + ExtensionsApi.export.remove_export_option(exporter_id) diff --git a/src/Extensions/Audia/Main.tscn b/src/Extensions/Audia/Main.tscn new file mode 100644 index 0000000..469d837 --- /dev/null +++ b/src/Extensions/Audia/Main.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://src/Extensions/Audia/Main.gd" type="Script" id=1] + +[node name="Main" type="Node"] +script = ExtResource( 1 ) diff --git a/src/Extensions/Audia/extension.json b/src/Extensions/Audia/extension.json new file mode 100644 index 0000000..2b61cef --- /dev/null +++ b/src/Extensions/Audia/extension.json @@ -0,0 +1,10 @@ +{ +"author": "Variable", +"description": "Implements Audio reference", +"display_name": "Audia", +"license": "MIT", +"name": "Audia", +"nodes": [ "Main.tscn" ], +"supported_api_versions": [ 4 ], +"version": "1.0" +} diff --git a/src/Extensions/Audia/music_icon.png b/src/Extensions/Audia/music_icon.png new file mode 100644 index 0000000..e26e4eb Binary files /dev/null and b/src/Extensions/Audia/music_icon.png differ diff --git a/src/Extensions/Audia/music_icon.png.import b/src/Extensions/Audia/music_icon.png.import new file mode 100644 index 0000000..07c6f51 --- /dev/null +++ b/src/Extensions/Audia/music_icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bjhrjnoyri24c" +path="res://.godot/imported/music_icon.png-577b83091e6209b57164a59506d3eb9a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/Extensions/Audia/music_icon.png" +dest_files=["res://.godot/imported/music_icon.png-577b83091e6209b57164a59506d3eb9a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1