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

Implement 3D layers (#840)

* Implement 3D layers

* Remove unneeded files

* Fix bug where a single hidden layer would ignore all of the layers on top when exporting

* Fix pxo loading

* Remove junk nodes from 3DShapeEdit

Seems like they were created when I copied from the old 3D Options.tscn panel to the new 3D Shape Edit tool.

* Make light gizmos half the size, and hide gizmos when rotating

* Fix crash when using the 3D shape edit tool on a group layer

* Remove unneeded code in Canvas.gd

* Add torus in the Cel3DObject.Type enumerator

Torus isn't currently supported in Godot 3.5, but it is in 3.6 and 4.0, so this is just future-proofing. May break compatibility with .pxo files that were exported with 3D layers before this change.

* Toggle 3D object visibility

* Change texts and some variable names

* Fill translation strings

* Fix crash on group blending, and make the code in Export.blend_layers() more general

* Fix errors when attempting to draw on a 3D cel

Can occur when multiple cels are selected, some of them 3D and some of them pixel

* Make scene properties and objects be per-cel instead of per-layer

Breaks compatibility with previous .pxo files that had 3D layers. Also introduces serialize() and deserialize() methods to BaseCel

* Use if not layer is get_script() in GroupLayer.blend_children()

* Flip the condition in GroupLayer.blend_children()

* Fix bug where locked/invisible layers could get drawn

Regression from c2f6bf0f3f

* Move gizmo code to 3DShapeEdit's draw_start(), move some undo/redo logic to 3DShapeEdit

* Move all of the undo/redo code to 3DShapeEdit, simplify code in Cel3D

* Store Cel3D image data to pxo, for easy usage by external software

This makes importing projects with 3D layers to other software, such as Godot using godot_pixelorama_importer easier.

* Make the linter happy

* Fix bug where the previously selected object would remain selected when it got removed with undo
This commit is contained in:
Emmanouil Papadeas 2023-03-31 21:58:56 +03:00 committed by GitHub
parent 5f290ae343
commit 91aea32864
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 3364 additions and 113 deletions

View file

@ -483,6 +483,10 @@ msgstr ""
msgid "Group layer:"
msgstr ""
#. A type of layer. A 3D layer contains data of 3D objects that are rasterized automatically by Pixelorama.
msgid "3D layer:"
msgstr ""
msgid "Direction:"
msgstr ""
@ -1703,10 +1707,20 @@ msgstr ""
msgid "Layers"
msgstr ""
#. Hint tooltip of the create new layer button, found on the left side of the timeline.
msgid "Create a new layer"
msgstr ""
msgid "Create a new group layer"
#. One of the options of the create new layer button.
msgid "Add Pixel Layer"
msgstr ""
#. One of the options of the create new layer button.
msgid "Add Group Layer"
msgstr ""
#. One of the options of the create new layer button.
msgid "Add 3D Layer"
msgstr ""
msgid "Remove current layer"
@ -2236,3 +2250,174 @@ msgid "Locked size\n\n"
"When enabled using the tool on the canvas will only move the cropping rectangle.\n\n"
"When disabled using the tool on the canvas will draw the rectangle."
msgstr ""
#. A tool used in 3D layers, that edits 3D objects.
msgid "3D Shape Edit"
msgstr ""
#. Used in 3D layers. A type of 3D object.
msgid "Box"
msgstr ""
#. Used in 3D layers. A type of 3D object.
msgid "Sphere"
msgstr ""
#. Used in 3D layers. A type of 3D object.
msgid "Capsule"
msgstr ""
#. Used in 3D layers. A type of 3D object.
msgid "Cylinder"
msgstr ""
#. Used in 3D layers. A type of 3D object.
msgid "Prism"
msgstr ""
#. Used in 3D layers. A type of 3D object.
msgid "Torus"
msgstr ""
#. Used in 3D layers. A type of 3D object.
msgid "Plane"
msgstr ""
#. Used in 3D layers. A type of light.
msgid "Directional light"
msgstr ""
#. Used in 3D layers. A type of light.
msgid "Spotlight"
msgstr ""
#. Used in 3D layers. A type of light.
msgid "Point light"
msgstr ""
#. Used in 3D layers. A type of 3D object.
msgid "Custom model"
msgstr ""
msgid "Selected object:"
msgstr ""
msgid "Add new object"
msgstr ""
msgid "Remove object"
msgstr ""
msgid "Camera"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool. Refers to camera projection mode.
msgid "Projection:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool. One of the modes of camera projection.
msgid "Perspective"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool. One of the modes of camera projection.
msgid "Orthogonal"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool. One of the modes of camera projection.
msgid "Frustum"
msgstr ""
msgid "Rotation:"
msgstr ""
msgid "Scale:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool.
msgid "Environment"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Environment category.
msgid "Ambient color:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Environment category.
msgid "Ambient color energy:"
msgstr ""
msgid "Visible:"
msgstr ""
#. Refers to the transformation options of an object, such as its position, rotation and scale. For technical details, see https://docs.godotengine.org/en/stable/tutorials/math/matrices_and_transforms.html
msgid "Transform"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool. A category with mesh-related options.
msgid "Mesh"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Mesh category if a Prism is selected. Refers to the displacement of the upper edge along the X axis.
msgid "Left to right:"
msgstr ""
#. Radius of a circle/spherical object.
msgid "Radius:"
msgstr ""
msgid "Height:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Mesh category if a spherical object is selected.
msgid "Radial segments:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Mesh category if a spherical object is selected. Refers to the number of segments along the height of the sphere.
msgid "Rings:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Mesh category if a Sphere is selected. If true, a hemisphere is created rather than a full sphere.
msgid "Is hemisphere:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Mesh category if a Cylinder is selected.
msgid "Top radius:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Mesh category if a Cylinder is selected.
msgid "Bottom radius:"
msgstr ""
msgid "Text:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Mesh category if a Text object is selected. Refers to the size of one pixel's width on the text to scale it in 3D.
msgid "Pixel size:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Mesh category if a Text object is selected. Step (in pixels) used to approximate Bézier curves.
msgid "Curve step:"
msgstr ""
msgid "Horizontal alignment:"
msgstr ""
msgid "Left"
msgstr ""
msgid "Right"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Light category if a light is selected. Refers to the energy of the light. The more energy, the brighter it shines.
msgid "Energy:"
msgstr ""
#. Found in the tool options of the 3D Shape Edit tool, under the Light category if a light is selected. If true, the light's effect is reversed, darkening areas and casting bright shadows.
msgid "Negative:"
msgstr ""
msgid "Shadow:"
msgstr ""
#. Refers to the range of something, like the range of a spotlight.
msgid "Range:"
msgstr ""

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7 1v3h2v-3zm-2.5352 2.0508-1.4141 1.4141 1.4141 1.4141 1.4141-1.4141zm7.0703 0-1.4141 1.4141 1.4141 1.4141 1.4141-1.4141zm-3.5352 1.9492c-1.6569 0-3 1.3432-3 3s1.3431 3 3 3 3-1.3432 3-3-1.3431-3-3-3zm-7 2v2h3v-2zm11 0v2h3v-2zm-7.5352 3.1211-1.4141 1.4141 1.4141 1.4141 1.4141-1.4141zm7.0703 0-1.4141 1.4141 1.4141 1.4141 1.4141-1.4141zm-4.5352 1.8789v3h2v-3z" fill="#fc7f7f" fill-opacity=".99608"/></svg>

After

Width:  |  Height:  |  Size: 498 B

View file

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/directional_light.svg-093cdb9a72dee590271da014181a4680.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/gizmos/directional_light.svg"
dest_files=[ "res://.import/directional_light.svg-093cdb9a72dee590271da014181a4680.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m8 1a5 5 0 0 0 -5 5 5 5 0 0 0 3 4.5762v2.4238h4v-2.4199a5 5 0 0 0 3-4.5801 5 5 0 0 0 -5-5zm0 2a3 3 0 0 1 3 3 3 3 0 0 1 -3 3 3 3 0 0 1 -3-3 3 3 0 0 1 3-3zm-1 11v1h2v-1z" fill="#fc7f7f" fill-opacity=".99608"/></svg>

After

Width:  |  Height:  |  Size: 306 B

View file

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/omni_light.svg-b0faa945d45257c6c9fecd256ed51eee.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/gizmos/omni_light.svg"
dest_files=[ "res://.import/omni_light.svg-b0faa945d45257c6c9fecd256ed51eee.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m6 1a1 1 0 0 0 -1 1v3.6934c-1.7861.86608-3 2.4605-3 4.3066h4a2 2 0 0 0 2 2 2 2 0 0 0 2-2h4c0-1.8462-1.2139-3.4406-3-4.3066v-3.6934a1 1 0 0 0 -1-1zm-1.0977 9.6348-1.7324 1 1 1.7305 1.7324-1zm6.1953 0-1 1.7305 1.7324 1 1-1.7305zm-4.0977 2.3652v2h2v-2z" fill="#fc7f7f" fill-opacity=".99608"/></svg>

After

Width:  |  Height:  |  Size: 388 B

View file

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/spot_light.svg-d309a6f5345413a6c8b9afd3cfe72f29.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/gizmos/spot_light.svg"
dest_files=[ "res://.import/spot_light.svg-d309a6f5345413a6c8b9afd3cfe72f29.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

View file

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/3dshapeedit.png-2d2aa73fafc7df7f7df84b5c0db6c1a6.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/tools/3dshapeedit.png"
dest_files=[ "res://.import/3dshapeedit.png-2d2aa73fafc7df7f7df84b5c0db6c1a6.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

View file

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/3dshapeedit.png-cca253df77476289c578814dc8af5d80.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/tools/cursors/3dshapeedit.png"
dest_files=[ "res://.import/3dshapeedit.png-cca253df77476289c578814dc8af5d80.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=false
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=false
svg/scale=1.0

View file

@ -74,6 +74,16 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://src/UI/Canvas/Canvas.gd"
}, {
"base": "BaseCel",
"class": "Cel3D",
"language": "GDScript",
"path": "res://src/Classes/Cel3D.gd"
}, {
"base": "Spatial",
"class": "Cel3DObject",
"language": "GDScript",
"path": "res://src/Classes/Cel3DObject.gd"
}, {
"base": "VBoxContainer",
"class": "CollapsibleContainer",
"language": "GDScript",
@ -124,6 +134,11 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://src/Classes/ImageEffect.gd"
}, {
"base": "BaseLayer",
"class": "Layer3D",
"language": "GDScript",
"path": "res://src/Classes/Layer3D.gd"
}, {
"base": "Button",
"class": "LayerButton",
"language": "GDScript",
@ -139,6 +154,11 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://src/UI/Nodes/NotificationLabel.gd"
}, {
"base": "Reference",
"class": "ObjParse",
"language": "GDScript",
"path": "res://src/Classes/ObjParse.gd"
}, {
"base": "Resource",
"class": "Palette",
"language": "GDScript",
@ -238,6 +258,11 @@ _global_script_classes=[ {
"class": "ValueSliderV2",
"language": "GDScript",
"path": "res://src/UI/Nodes/ValueSliderV2.gd"
}, {
"base": "HBoxContainer",
"class": "ValueSliderV3",
"language": "GDScript",
"path": "res://src/UI/Nodes/ValueSliderV3.gd"
} ]
_global_script_class_icons={
"AImgIOAPNGExporter": "",
@ -253,6 +278,8 @@ _global_script_class_icons={
"BaseTool": "",
"Brushes": "",
"Canvas": "",
"Cel3D": "",
"Cel3DObject": "",
"CollapsibleContainer": "",
"CropRect": "",
"Drawer": "",
@ -263,9 +290,11 @@ _global_script_class_icons={
"GroupLayer": "",
"Guide": "",
"ImageEffect": "",
"Layer3D": "",
"LayerButton": "",
"MaxMinEdit": "",
"NotificationLabel": "",
"ObjParse": "",
"Palette": "",
"PaletteColor": "",
"PaletteGrid": "",
@ -285,7 +314,8 @@ _global_script_class_icons={
"SymmetryGuide": "",
"Tiles": "",
"ValueSlider": "",
"ValueSliderV2": ""
"ValueSliderV2": "",
"ValueSliderV3": ""
}
[application]

View file

@ -432,7 +432,7 @@ func scale_image(width: int, height: int, interpolation: int) -> void:
for f in Global.current_project.frames:
for i in range(f.cels.size() - 1, -1, -1):
if f.cels[i] is GroupCel:
if not f.cels[i] is PixelCel:
continue
var sprite := Image.new()
sprite.copy_from(f.cels[i].image)

View file

@ -383,10 +383,10 @@ func blend_layers(
else:
var layer: BaseLayer = project.layers[export_layers - 2]
var layer_image := Image.new()
if layer is PixelLayer:
layer_image.copy_from(frame.cels[export_layers - 2].image)
elif layer is GroupLayer:
if layer is GroupLayer:
layer_image.copy_from(layer.blend_children(frame, Vector2.ZERO))
else:
layer_image.copy_from(frame.cels[export_layers - 2].get_image())
image.blend_rect(layer_image, Rect2(Vector2.ZERO, project.size), origin)
@ -396,20 +396,25 @@ func blend_all_layers(
) -> void:
var layer_i := 0
for cel in frame.cels:
if project.layers[layer_i].is_visible_in_hierarchy() and cel is PixelCel:
var cel_image := Image.new()
cel_image.copy_from(cel.image)
if cel.opacity < 1: # If we have cel transparency
cel_image.lock()
for xx in cel_image.get_size().x:
for yy in cel_image.get_size().y:
var pixel_color := cel_image.get_pixel(xx, yy)
var alpha: float = pixel_color.a * cel.opacity
cel_image.set_pixel(
xx, yy, Color(pixel_color.r, pixel_color.g, pixel_color.b, alpha)
)
cel_image.unlock()
image.blend_rect(cel_image, Rect2(Vector2.ZERO, project.size), origin)
if not project.layers[layer_i].is_visible_in_hierarchy():
layer_i += 1
continue
if cel is GroupCel:
layer_i += 1
continue
var cel_image := Image.new()
cel_image.copy_from(cel.get_image())
if cel.opacity < 1: # If we have cel transparency
cel_image.lock()
for xx in cel_image.get_size().x:
for yy in cel_image.get_size().y:
var pixel_color := cel_image.get_pixel(xx, yy)
var alpha: float = pixel_color.a * cel.opacity
cel_image.set_pixel(
xx, yy, Color(pixel_color.r, pixel_color.g, pixel_color.b, alpha)
)
cel_image.unlock()
image.blend_rect(cel_image, Rect2(Vector2.ZERO, project.size), origin)
layer_i += 1
@ -421,13 +426,13 @@ func blend_selected_cels(
var test_array := [project.current_frame, cel_ind]
if not test_array in project.selected_cels:
continue
if not frame.cels[cel_ind] is PixelCel:
if frame.cels[cel_ind] is GroupCel:
continue
if not project.layers[cel_ind].is_visible_in_hierarchy():
continue
var cel: PixelCel = frame.cels[cel_ind]
var cel: BaseCel = frame.cels[cel_ind]
var cel_image := Image.new()
cel_image.copy_from(cel.image)
cel_image.copy_from(cel.get_image())
if cel.opacity < 1: # If we have cel transparency
cel_image.lock()
for xx in cel_image.get_size().x:

View file

@ -274,10 +274,11 @@ class ToolAPI:
shortcut: String,
scene: PackedScene,
extra_hint := "",
extra_shortucts := []
extra_shortucts := [],
layer_types: PoolIntArray = []
) -> void:
var tool_class := Tools.Tool.new(
tool_name, display_name, shortcut, scene, extra_hint, extra_shortucts
tool_name, display_name, shortcut, scene, layer_types, extra_hint, extra_shortucts
)
Tools.tools[tool_name] = tool_class
Tools.add_tool_button(tool_class)

View file

@ -3,7 +3,7 @@ extends Node
signal project_changed
signal cel_changed
enum LayerTypes { PIXEL, GROUP }
enum LayerTypes { PIXEL, GROUP, THREE_D }
enum GridTypes { CARTESIAN, ISOMETRIC, ALL }
enum ColorFrom { THEME, CUSTOM }
enum ButtonSize { SMALL, BIG }
@ -159,6 +159,7 @@ var crop_left := 0
var crop_right := 0
# Nodes
var base_layer_button_node: PackedScene = preload("res://src/UI/Timeline/BaseLayerButton.tscn")
var pixel_layer_button_node: PackedScene = preload("res://src/UI/Timeline/PixelLayerButton.tscn")
var group_layer_button_node: PackedScene = preload("res://src/UI/Timeline/GroupLayerButton.tscn")
var pixel_cel_button_node: PackedScene = preload("res://src/UI/Timeline/PixelCelButton.tscn")
@ -479,7 +480,10 @@ func undo_or_redo(
for i in project.frames.size():
for j in project.layers.size():
var current_cel: BaseCel = project.frames[i].cels[j]
current_cel.image_texture.create_from_image(current_cel.get_image(), 0)
if current_cel is Cel3D:
current_cel.size_changed(project.size)
else:
current_cel.image_texture.create_from_image(current_cel.get_image(), 0)
canvas.camera_zoom()
canvas.grid.update()
canvas.pixel_grid.update()
@ -578,3 +582,21 @@ func update_hint_tooltips() -> void:
var first_key: InputEventKey = Keychain.action_get_first_key(event_type.action)
hint = first_key.as_text() if first_key else "None"
tip.hint_tooltip = tr(ui_tooltips[tip]) % hint
# Used in case some of the values in a dictionary are Strings, when they should be something else
func convert_dictionary_values(dict: Dictionary) -> void:
for key in dict:
if key == "id" or key == "type":
dict[key] = int(dict[key])
if typeof(dict[key]) != TYPE_STRING:
continue
if "transform" in key: # Convert a String to a Transform
var transform_string: String = dict[key].replace(" - ", ", ")
dict[key] = str2var("Transform(" + transform_string + ")")
elif "color" in key: # Convert a String to a Color
dict[key] = str2var("Color(" + dict[key] + ")")
elif "v2" in key: # Convert a String to a Vector2
dict[key] = str2var("Vector2" + dict[key])
elif "size" in key or "center_offset" in key: # Convert a String to a Vector3
dict[key] = str2var("Vector3" + dict[key])

View file

@ -45,6 +45,7 @@ var tools := {
"Polygonal Selection",
"polygon_select",
preload("res://src/Tools/SelectionTools/PolygonSelect.tscn"),
[],
"Double-click to connect the last point to the starting point"
),
"ColorSelect":
@ -75,7 +76,10 @@ var tools := {
"paint_selection",
preload("res://src/Tools/SelectionTools/PaintSelect.tscn")
),
"Move": Tool.new("Move", "Move", "move", preload("res://src/Tools/Move.tscn")),
"Move":
Tool.new(
"Move", "Move", "move", preload("res://src/Tools/Move.tscn"), [Global.LayerTypes.PIXEL]
),
"Zoom": Tool.new("Zoom", "Zoom", "zoom", preload("res://src/Tools/Zoom.tscn")),
"Pan": Tool.new("Pan", "Pan", "pan", preload("res://src/Tools/Pan.tscn")),
"ColorPicker":
@ -84,16 +88,20 @@ var tools := {
"Color Picker",
"colorpicker",
preload("res://src/Tools/ColorPicker.tscn"),
[],
"Select a color from a pixel of the sprite"
),
"Crop":
Tool.new("Crop", "Crop", "crop", preload("res://src/Tools/CropTool.tscn"), "Resize the canvas"),
Tool.new(
"Crop", "Crop", "crop", preload("res://src/Tools/CropTool.tscn"), [], "Resize the canvas"
),
"Pencil":
Tool.new(
"Pencil",
"Pencil",
"pencil",
preload("res://src/Tools/Pencil.tscn"),
[Global.LayerTypes.PIXEL],
"Hold %s to make a line",
["draw_create_line"]
),
@ -103,18 +111,33 @@ var tools := {
"Eraser",
"eraser",
preload("res://src/Tools/Eraser.tscn"),
[Global.LayerTypes.PIXEL],
"Hold %s to make a line",
["draw_create_line"]
),
"Bucket": Tool.new("Bucket", "Bucket", "fill", preload("res://src/Tools/Bucket.tscn")),
"Bucket":
Tool.new(
"Bucket",
"Bucket",
"fill",
preload("res://src/Tools/Bucket.tscn"),
[Global.LayerTypes.PIXEL]
),
"Shading":
Tool.new("Shading", "Shading Tool", "shading", preload("res://src/Tools/Shading.tscn")),
Tool.new(
"Shading",
"Shading Tool",
"shading",
preload("res://src/Tools/Shading.tscn"),
[Global.LayerTypes.PIXEL]
),
"LineTool":
Tool.new(
"LineTool",
"Line Tool",
"linetool",
preload("res://src/Tools/LineTool.tscn"),
[Global.LayerTypes.PIXEL],
"""Hold %s to snap the angle of the line
Hold %s to center the shape on the click origin
Hold %s to displace the shape's origin""",
@ -126,6 +149,7 @@ Hold %s to displace the shape's origin""",
"Rectangle Tool",
"rectangletool",
preload("res://src/Tools/RectangleTool.tscn"),
[Global.LayerTypes.PIXEL],
"""Hold %s to create a 1:1 shape
Hold %s to center the shape on the click origin
Hold %s to displace the shape's origin""",
@ -137,16 +161,34 @@ Hold %s to displace the shape's origin""",
"Ellipse Tool",
"ellipsetool",
preload("res://src/Tools/EllipseTool.tscn"),
[Global.LayerTypes.PIXEL],
"""Hold %s to create a 1:1 shape
Hold %s to center the shape on the click origin
Hold %s to displace the shape's origin""",
["shape_perfect", "shape_center", "shape_displace"]
),
"3DShapeEdit":
Tool.new(
"3DShapeEdit",
"3D Shape Edit",
"3dshapeedit",
preload("res://src/Tools/3DShapeEdit.tscn"),
[Global.LayerTypes.THREE_D]
),
}
var _tool_button_scene: PackedScene = preload("res://src/Tools/ToolButton.tscn")
var _slots := {}
var _panels := {}
var _curr_layer_type: int = Global.LayerTypes.PIXEL
var _left_tools_per_layer_type := {
Global.LayerTypes.PIXEL: "Pencil",
Global.LayerTypes.THREE_D: "3DShapeEdit",
}
var _right_tools_per_layer_type := {
Global.LayerTypes.PIXEL: "Eraser",
Global.LayerTypes.THREE_D: "Pan",
}
var _tool_buttons: Node
var _active_button := -1
var _last_position := Vector2.INF
@ -161,6 +203,7 @@ class Tool:
var shortcut := ""
var extra_hint := ""
var extra_shortcuts := [] # Array of String(s)
var layer_types: PoolIntArray = []
var button_node: BaseButton
func _init(
@ -168,6 +211,7 @@ class Tool:
_display_name: String,
_shortcut: String,
_scene: PackedScene,
_layer_types: PoolIntArray = [],
_extra_hint := "",
_extra_shortucts := []
) -> void:
@ -175,6 +219,7 @@ class Tool:
display_name = _display_name
shortcut = _shortcut
scene = _scene
layer_types = _layer_types
extra_hint = _extra_hint
extra_shortcuts = _extra_shortucts
icon = load("res://assets/graphics/tools/%s.png" % name.to_lower())
@ -234,6 +279,7 @@ class Slot:
func _ready() -> void:
Global.connect("cel_changed", self, "_cel_changed")
_tool_buttons = Global.control.find_node("ToolButtons")
for t in tools:
add_tool_button(tools[t])
@ -248,15 +294,19 @@ func _ready() -> void:
_panels[BUTTON_LEFT] = Global.control.find_node("LeftPanelContainer", true, false)
_panels[BUTTON_RIGHT] = Global.control.find_node("RightPanelContainer", true, false)
var default_left_tool: String = _left_tools_per_layer_type[0]
var default_right_tool: String = _right_tools_per_layer_type[0]
var tool_name: String = Global.config_cache.get_value(
_slots[BUTTON_LEFT].kname, "tool", "Pencil"
_slots[BUTTON_LEFT].kname, "tool", default_left_tool
)
if not tool_name in tools:
tool_name = "Pencil"
if not tool_name in tools or not _is_tool_available(Global.LayerTypes.PIXEL, tools[tool_name]):
tool_name = default_left_tool
set_tool(tool_name, BUTTON_LEFT)
tool_name = Global.config_cache.get_value(_slots[BUTTON_RIGHT].kname, "tool", "Eraser")
if not tool_name in tools:
tool_name = "Eraser"
tool_name = Global.config_cache.get_value(
_slots[BUTTON_RIGHT].kname, "tool", default_right_tool
)
if not tool_name in tools or not _is_tool_available(Global.LayerTypes.PIXEL, tools[tool_name]):
tool_name = default_right_tool
set_tool(tool_name, BUTTON_RIGHT)
update_tool_buttons()
@ -273,6 +323,9 @@ func _ready() -> void:
color_value = Global.config_cache.get_value(_slots[BUTTON_RIGHT].kname, "color", Color.white)
assign_color(color_value, BUTTON_RIGHT, false)
update_tool_cursors()
var layer: BaseLayer = Global.current_project.layers[Global.current_project.current_layer]
var layer_type := layer.get_layer_type()
_show_relevant_tools(layer_type)
func add_tool_button(t: Tool) -> void:
@ -293,7 +346,7 @@ func remove_tool(t: Tool) -> void:
func set_tool(name: String, button: int) -> void:
var slot = _slots[button]
var slot: Slot = _slots[button]
var panel: Node = _panels[button]
var node: Node = tools[name].scene.instance()
if button == BUTTON_LEFT: # As guides are only moved with left mouse
@ -307,9 +360,16 @@ func set_tool(name: String, button: int) -> void:
slot.button = button
panel.add_child(slot.tool_node)
if _curr_layer_type == Global.LayerTypes.GROUP:
return
if button == BUTTON_LEFT:
_left_tools_per_layer_type[_curr_layer_type] = name
elif button == BUTTON_RIGHT:
_right_tools_per_layer_type[_curr_layer_type] = name
func assign_tool(name: String, button: int) -> void:
var slot = _slots[button]
var slot: Slot = _slots[button]
var panel: Node = _panels[button]
if slot.tool_node != null:
@ -463,3 +523,33 @@ func get_alpha_dynamic(strength := 1.0) -> float:
elif dynamics_alpha == Dynamics.VELOCITY:
strength *= lerp(alpha_min, alpha_max, mouse_velocity)
return strength
func _cel_changed() -> void:
var layer: BaseLayer = Global.current_project.layers[Global.current_project.current_layer]
var layer_type := layer.get_layer_type()
# Do not make any changes when its the same type of layer, or a group layer
if layer_type == _curr_layer_type or layer_type == Global.LayerTypes.GROUP:
return
_show_relevant_tools(layer_type)
func _show_relevant_tools(layer_type: int) -> void:
# Hide tools that are not available in the current layer type
for button in _tool_buttons.get_children():
var tool_name: String = button.name
var t: Tool = tools[tool_name]
var hide_tool := _is_tool_available(layer_type, t)
button.visible = hide_tool
# Assign new tools if the layer type has changed
_curr_layer_type = layer_type
var new_tool_name: String = _left_tools_per_layer_type[layer_type]
assign_tool(new_tool_name, BUTTON_LEFT)
new_tool_name = _right_tools_per_layer_type[layer_type]
assign_tool(new_tool_name, BUTTON_RIGHT)
func _is_tool_available(layer_type: int, t: Tool) -> bool:
return t.layer_types.empty() or layer_type in t.layer_types

View file

@ -5,8 +5,8 @@ extends Reference
signal texture_changed
var opacity: float
var image_texture: ImageTexture
var opacity := 1.0
var image_texture: Texture setget , _get_image_texture
# If the cel is linked a ref to the link set Dictionary this cel is in, or null if not linked:
var link_set = null # { "cels": Array, "hue": float } or null
var transformed_content: Image # Used in transformations (moving, scaling etc with selections)
@ -14,6 +14,10 @@ var transformed_content: Image # Used in transformations (moving, scaling etc w
# Methods to Override:
func _get_image_texture() -> Texture:
return image_texture
# The content methods deal with the unique content of each cel type. For example, an Image for
# PixelLayers, or a Dictionary of settings for a procedural layer type, and null for Groups.
# Can be used for linking/unlinking cels, copying, and deleting content
@ -47,6 +51,14 @@ func update_texture() -> void:
return
func serialize() -> Dictionary:
return {"opacity": opacity}
func deserialize(dict: Dictionary) -> void:
opacity = dict["opacity"]
func save_image_data_to_pxo(_file: File) -> void:
return
@ -55,5 +67,9 @@ func load_image_data_from_pxo(_file: File, _project_size: Vector2) -> void:
return
func on_remove() -> void:
pass
func instantiate_cel_button() -> Node:
return null

View file

@ -164,6 +164,10 @@ func deserialize(dict: Dictionary) -> void:
cel_link_sets.append(link_set)
func get_layer_type() -> int:
return -1
func new_empty_cel() -> BaseCel:
return null

203
src/Classes/Cel3D.gd Normal file
View file

@ -0,0 +1,203 @@
class_name Cel3D
extends BaseCel
signal selected_object(object)
signal scene_property_changed
signal objects_changed
var size: Vector2
var viewport: Viewport
var parent_node: Spatial
var camera: Camera
var scene_properties := {}
# Key = Cel3DObject's id, Value = Dictionary containing the properties of the Cel3DObject
var object_properties := {}
var selected: Cel3DObject = null setget _set_selected
var current_object_id := 0 # Its value never decreases
func _init(_size: Vector2, from_pxo := false, _object_prop := {}, _scene_prop := {}) -> void:
size = _size
object_properties = _object_prop
scene_properties = _scene_prop
if scene_properties.empty():
var camera_transform := Transform()
camera_transform.origin = Vector3(0, 0, 3)
scene_properties = {
"camera_transform": camera_transform,
"camera_projection": Camera.PROJECTION_PERSPECTIVE,
"ambient_light_color": Color.black,
"ambient_light_energy": 1,
}
_add_nodes()
if not from_pxo:
if object_properties.empty():
var transform := Transform()
transform.origin = Vector3(-2.5, 0, 0)
object_properties[0] = {"type": Cel3DObject.Type.DIR_LIGHT, "transform": transform}
_add_object_node(0)
object_properties[1] = {"type": Cel3DObject.Type.BOX}
_add_object_node(1)
current_object_id = object_properties.size()
func _add_nodes() -> void:
viewport = Viewport.new()
viewport.size = size
viewport.own_world = true
viewport.transparent_bg = true
viewport.render_target_v_flip = true
var world := World.new()
var environment := Environment.new()
world.environment = environment
viewport.world = world
parent_node = Spatial.new()
camera = Camera.new()
camera.current = true
deserialize_scene_properties()
viewport.add_child(camera)
viewport.add_child(parent_node)
Global.canvas.add_child(viewport)
for object in object_properties:
_add_object_node(object)
image_texture = viewport.get_texture()
func _get_image_texture() -> Texture:
if not is_instance_valid(viewport):
_add_nodes()
return image_texture
func serialize_scene_properties() -> Dictionary:
if not is_instance_valid(camera):
return {}
return {
"camera_transform": camera.transform,
"camera_projection": camera.projection,
"ambient_light_color": viewport.world.environment.ambient_light_color,
"ambient_light_energy": viewport.world.environment.ambient_light_energy
}
func deserialize_scene_properties() -> void:
camera.transform = scene_properties["camera_transform"]
camera.projection = scene_properties["camera_projection"]
viewport.world.environment.ambient_light_color = scene_properties["ambient_light_color"]
viewport.world.environment.ambient_light_energy = scene_properties["ambient_light_energy"]
func _update_objects_transform(id: int) -> void: # Called by undo/redo
var properties: Dictionary = object_properties[id]
var object := get_object_from_id(id)
if not object:
print("Object with id %s not found" % id)
return
object.deserialize(properties)
func get_object_from_id(id: int) -> Cel3DObject:
for child in parent_node.get_children():
if not child is Cel3DObject:
continue
if child.id == id:
return child
return null
func size_changed(new_size: Vector2) -> void:
size = new_size
viewport.size = size
image_texture = viewport.get_texture()
func _scene_property_changed() -> void: # Called by undo/redo
deserialize_scene_properties()
emit_signal("scene_property_changed")
func _add_object_node(id: int) -> void:
var node3d := Cel3DObject.new()
node3d.id = id
node3d.cel = self
parent_node.add_child(node3d)
node3d.type = object_properties[id]["type"]
if object_properties.has(id):
if object_properties[id].has("id"):
node3d.deserialize(object_properties[id])
else:
if object_properties[id].has("transform"):
node3d.transform = object_properties[id]["transform"]
if object_properties[id].has("file_path"):
node3d.file_path = object_properties[id]["file_path"]
object_properties[id] = node3d.serialize()
emit_signal("objects_changed")
func _remove_object_node(id: int) -> void: # Called by undo/redo
var object := get_object_from_id(id)
if is_instance_valid(object):
if selected == object:
self.selected = null
object.queue_free()
emit_signal("objects_changed")
func _set_selected(value: Cel3DObject) -> void:
if value == selected:
return
if is_instance_valid(selected): # Unselect previous object if we selected something else
selected.unselect()
selected = value
if is_instance_valid(selected): # Select new object
selected.select()
emit_signal("selected_object", value)
# Overridden methods
func get_image() -> Image:
return viewport.get_texture().get_data()
func serialize() -> Dictionary:
var dict := .serialize()
dict["scene_properties"] = scene_properties
dict["object_properties"] = object_properties
return dict
func deserialize(dict: Dictionary) -> void:
.deserialize(dict)
scene_properties = dict["scene_properties"]
var objects_copy = dict["object_properties"]
for object in objects_copy:
if typeof(object) != TYPE_STRING:
return
Global.convert_dictionary_values(objects_copy[object])
object_properties[int(object)] = objects_copy[object]
current_object_id = object_properties.size()
Global.convert_dictionary_values(scene_properties)
deserialize_scene_properties()
for object in object_properties:
_add_object_node(object)
func on_remove() -> void:
if is_instance_valid(viewport):
viewport.queue_free()
func save_image_data_to_pxo(file: File) -> void:
file.store_buffer(get_image().get_data())
func load_image_data_from_pxo(file: File, project_size: Vector2) -> void:
# Don't do anything with it, just read it so that the file can move on
file.get_buffer(project_size.x * project_size.y * 4)
func instantiate_cel_button() -> Node:
return Global.pixel_cel_button_node.instance()

327
src/Classes/Cel3DObject.gd Normal file
View file

@ -0,0 +1,327 @@
class_name Cel3DObject
extends Spatial
signal property_changed
enum Type {
BOX,
SPHERE,
CAPSULE,
CYLINDER,
PRISM,
TORUS,
PLANE,
TEXT,
DIR_LIGHT,
SPOT_LIGHT,
OMNI_LIGHT,
IMPORTED
}
enum Gizmos { NONE, X_POS, Y_POS, Z_POS, X_ROT, Y_ROT, Z_ROT, X_SCALE, Y_SCALE, Z_SCALE }
var cel
var id := -1
var type: int = Type.BOX setget _set_type
var selected := false
var hovered := false
var box_shape: BoxShape
var camera: Camera
var file_path := "" setget _set_file_path # Only useful for Type.IMPORTED
var applying_gizmos: int = Gizmos.NONE
var node3d_type: VisualInstance
var dir_light_texture := preload("res://assets/graphics/gizmos/directional_light.svg")
var spot_light_texture := preload("res://assets/graphics/gizmos/spot_light.svg")
var omni_light_texture := preload("res://assets/graphics/gizmos/omni_light.svg")
onready var gizmos_3d: Node2D = Global.canvas.gizmos_3d
func _ready() -> void:
camera = get_viewport().get_camera()
var static_body := StaticBody.new()
var collision_shape := CollisionShape.new()
box_shape = BoxShape.new()
box_shape.extents = scale
collision_shape.shape = box_shape
static_body.add_child(collision_shape)
add_child(static_body)
func find_cel() -> bool:
var project = Global.current_project
return cel == project.frames[project.current_frame].cels[project.current_layer]
func serialize() -> Dictionary:
var dict := {
"id": id, "type": type, "transform": transform, "visible": visible, "file_path": file_path
}
if _is_mesh():
var mesh: Mesh = node3d_type.mesh
match type:
Type.BOX:
dict["mesh_size"] = mesh.size
Type.PLANE:
dict["mesh_sizev2"] = mesh.size
dict["mesh_center_offset"] = mesh.center_offset
Type.PRISM:
dict["mesh_size"] = mesh.size
dict["mesh_left_to_right"] = mesh.left_to_right
Type.SPHERE:
dict["mesh_radius"] = mesh.radius
dict["mesh_height"] = mesh.height
dict["mesh_radial_segments"] = mesh.radial_segments
dict["mesh_rings"] = mesh.rings
dict["mesh_is_hemisphere"] = mesh.is_hemisphere
Type.CAPSULE:
dict["mesh_radius"] = mesh.radius
dict["mesh_mid_height"] = mesh.mid_height
dict["mesh_radial_segments"] = mesh.radial_segments
dict["mesh_rings"] = mesh.rings
Type.CYLINDER:
dict["mesh_bottom_radius"] = mesh.bottom_radius
dict["mesh_top_radius"] = mesh.top_radius
dict["mesh_height"] = mesh.height
dict["mesh_radial_segments"] = mesh.radial_segments
dict["mesh_rings"] = mesh.rings
Type.TEXT:
dict["mesh_text"] = mesh.text
dict["mesh_pixel_size"] = mesh.pixel_size
dict["mesh_curve_step"] = mesh.curve_step
dict["mesh_horizontal_alignment"] = mesh.horizontal_alignment
else:
dict["light_color"] = node3d_type.light_color
dict["light_energy"] = node3d_type.light_energy
dict["light_negative"] = node3d_type.light_negative
dict["shadow_enabled"] = node3d_type.shadow_enabled
dict["shadow_color"] = node3d_type.shadow_color
match type:
Type.OMNI_LIGHT:
dict["omni_range"] = node3d_type.omni_range
Type.SPOT_LIGHT:
dict["spot_range"] = node3d_type.spot_range
dict["spot_angle"] = node3d_type.spot_angle
return dict
func deserialize(dict: Dictionary) -> void:
id = dict["id"]
self.type = dict["type"]
transform = dict["transform"]
visible = dict["visible"]
self.file_path = dict["file_path"]
if _is_mesh():
var mesh: Mesh = node3d_type.mesh
match type:
Type.BOX:
mesh.size = dict["mesh_size"]
Type.PLANE:
mesh.size = dict["mesh_sizev2"]
mesh.center_offset = dict["mesh_center_offset"]
Type.PRISM:
mesh.size = dict["mesh_size"]
mesh.left_to_right = dict["mesh_left_to_right"]
Type.SPHERE:
mesh.radius = dict["mesh_radius"]
mesh.height = dict["mesh_height"]
mesh.radial_segments = dict["mesh_radial_segments"]
mesh.rings = dict["mesh_rings"]
mesh.is_hemisphere = dict["mesh_is_hemisphere"]
Type.CAPSULE:
mesh.radius = dict["mesh_radius"]
mesh.mid_height = dict["mesh_mid_height"]
mesh.radial_segments = dict["mesh_radial_segments"]
mesh.rings = dict["mesh_rings"]
Type.CYLINDER:
mesh.bottom_radius = dict["mesh_bottom_radius"]
mesh.top_radius = dict["mesh_top_radius"]
mesh.height = dict["mesh_height"]
mesh.radial_segments = dict["mesh_radial_segments"]
mesh.rings = dict["mesh_rings"]
Type.TEXT:
mesh.text = dict["mesh_text"]
mesh.pixel_size = dict["mesh_pixel_size"]
mesh.curve_step = dict["mesh_curve_step"]
mesh.horizontal_alignment = dict["mesh_horizontal_alignment"]
else:
node3d_type.light_color = dict["light_color"]
node3d_type.light_energy = dict["light_energy"]
node3d_type.light_negative = dict["light_negative"]
node3d_type.shadow_enabled = dict["shadow_enabled"]
node3d_type.shadow_color = dict["shadow_color"]
match type:
Type.OMNI_LIGHT:
node3d_type.omni_range = dict["omni_range"]
Type.SPOT_LIGHT:
node3d_type.spot_range = dict["spot_range"]
node3d_type.spot_angle = dict["spot_angle"]
change_property()
func _is_mesh() -> bool:
return node3d_type is MeshInstance
func _set_type(value: int) -> void:
type = value
if is_instance_valid(node3d_type):
node3d_type.queue_free()
match type:
Type.BOX:
node3d_type = MeshInstance.new()
node3d_type.mesh = CubeMesh.new()
Type.SPHERE:
node3d_type = MeshInstance.new()
node3d_type.mesh = SphereMesh.new()
Type.CAPSULE:
node3d_type = MeshInstance.new()
node3d_type.mesh = CapsuleMesh.new()
Type.CYLINDER:
node3d_type = MeshInstance.new()
node3d_type.mesh = CylinderMesh.new()
Type.PRISM:
node3d_type = MeshInstance.new()
node3d_type.mesh = PrismMesh.new()
Type.PLANE:
node3d_type = MeshInstance.new()
node3d_type.mesh = PlaneMesh.new()
Type.TEXT:
node3d_type = MeshInstance.new()
var mesh := TextMesh.new()
mesh.font = Global.control.theme.default_font
mesh.text = "Sample"
node3d_type.mesh = mesh
Type.DIR_LIGHT:
node3d_type = DirectionalLight.new()
gizmos_3d.add_always_visible(self, dir_light_texture)
Type.SPOT_LIGHT:
node3d_type = SpotLight.new()
gizmos_3d.add_always_visible(self, spot_light_texture)
Type.OMNI_LIGHT:
node3d_type = OmniLight.new()
gizmos_3d.add_always_visible(self, omni_light_texture)
Type.IMPORTED:
node3d_type = MeshInstance.new()
var mesh: Mesh
if not file_path.empty():
mesh = ObjParse.load_obj(file_path)
node3d_type.mesh = mesh
add_child(node3d_type)
func _set_file_path(value: String) -> void:
file_path = value
if file_path.empty():
return
if type == Type.IMPORTED:
node3d_type.mesh = ObjParse.load_obj(file_path)
func _notification(what: int) -> void:
if what == NOTIFICATION_EXIT_TREE:
unselect()
gizmos_3d.remove_always_visible(self)
func select() -> void:
selected = true
gizmos_3d.get_points(camera, self)
func unselect() -> void:
selected = false
gizmos_3d.clear_points(self)
func hover() -> void:
if hovered:
return
hovered = true
if selected:
return
gizmos_3d.get_points(camera, self)
func unhover() -> void:
if not hovered:
return
hovered = false
if selected:
return
gizmos_3d.clear_points(self)
func change_transform(a: Vector3, b: Vector3) -> void:
var diff := a - b
match applying_gizmos:
Gizmos.X_POS:
move_axis(diff, transform.basis.x)
Gizmos.Y_POS:
move_axis(diff, transform.basis.y)
Gizmos.Z_POS:
move_axis(diff, transform.basis.z)
Gizmos.X_ROT:
change_rotation(a, b, transform.basis.x)
Gizmos.Y_ROT:
change_rotation(a, b, transform.basis.y)
Gizmos.Z_ROT:
change_rotation(a, b, transform.basis.z)
Gizmos.X_SCALE:
change_scale(diff, transform.basis.x, Vector3.RIGHT)
Gizmos.Y_SCALE:
change_scale(diff, transform.basis.y, Vector3.UP)
Gizmos.Z_SCALE:
change_scale(diff, transform.basis.z, Vector3.BACK)
_:
move(diff)
func move(position: Vector3) -> void:
translation += position
change_property()
func move_axis(diff: Vector3, axis: Vector3) -> void:
# Move the object in the direction it is facing, and restrict mouse movement in that axis
var axis_v2 := Vector2(axis.x, axis.y).normalized()
if axis_v2 == Vector2.ZERO:
axis_v2 = Vector2(axis.y, axis.z).normalized()
var diff_v2 := Vector2(diff.x, diff.y).normalized()
translation += axis * axis_v2.dot(diff_v2) * diff.length()
change_property()
func change_rotation(a: Vector3, b: Vector3, axis: Vector3) -> void:
var a_local := a - translation
var a_local_v2 := Vector2(a_local.x, a_local.y)
var b_local := b - translation
var b_local_v2 := Vector2(b_local.x, b_local.y)
var angle := b_local_v2.angle_to(a_local_v2)
# Rotate the object around a basis axis, instead of a fixed axis, such as
# Vector3.RIGHT, Vector3.UP or Vector3.BACK
rotate(axis.normalized(), angle)
rotation.x = wrapf(rotation.x, -PI, PI)
rotation.y = wrapf(rotation.y, -PI, PI)
rotation.z = wrapf(rotation.z, -PI, PI)
change_property()
func change_scale(diff: Vector3, axis: Vector3, dir: Vector3) -> void:
# Scale the object in the direction it is facing, and restrict mouse movement in that axis
var axis_v2 := Vector2(axis.x, axis.y).normalized()
if axis_v2 == Vector2.ZERO:
axis_v2 = Vector2(axis.y, axis.z).normalized()
var diff_v2 := Vector2(diff.x, diff.y).normalized()
scale += dir * axis_v2.dot(diff_v2) * diff.length()
change_property()
func change_property() -> void:
if selected:
select()
else:
# Check is needed in case this runs before _ready(), and thus onready variables
if is_instance_valid(gizmos_3d):
gizmos_3d.update()
emit_signal("property_changed")

View file

@ -10,7 +10,7 @@ func _init(_opacity := 1.0) -> void:
func get_image() -> Image:
var image = Image.new()
var image := Image.new()
image.create(
Global.current_project.size.x, Global.current_project.size.y, false, Image.FORMAT_RGBA8
)

View file

@ -18,10 +18,13 @@ func blend_children(frame: Frame, origin := Vector2.ZERO) -> Image:
for layer in children:
if not layer.is_visible_in_hierarchy():
continue
if layer is PixelLayer:
var cel: PixelCel = frame.cels[layer.index]
# Checks if layer is GroupLayer, cannot define this due to cyclic reference error
if layer is get_script():
image.blend_rect(layer.blend_children(frame, origin), blend_rect, origin)
else:
var cel: BaseCel = frame.cels[layer.index]
var cel_image := Image.new()
cel_image.copy_from(cel.image)
cel_image.copy_from(cel.get_image())
if cel.opacity < 1: # If we have cel transparency
cel_image.lock()
for xx in cel_image.get_size().x:
@ -33,8 +36,6 @@ func blend_children(frame: Frame, origin := Vector2.ZERO) -> Image:
)
cel_image.unlock()
image.blend_rect(cel_image, blend_rect, origin)
else: # Only if layer is GroupLayer, cannot define this due to cyclic reference error
image.blend_rect(layer.blend_children(frame, origin), blend_rect, origin)
return image
@ -42,8 +43,8 @@ func blend_children(frame: Frame, origin := Vector2.ZERO) -> Image:
func serialize() -> Dictionary:
var data = .serialize()
data["type"] = Global.LayerTypes.GROUP
var data := .serialize()
data["type"] = get_layer_type()
data["expanded"] = expanded
return data
@ -53,6 +54,10 @@ func deserialize(dict: Dictionary) -> void:
expanded = dict.expanded
func get_layer_type() -> int:
return Global.LayerTypes.GROUP
func new_empty_cel() -> BaseCel:
return GroupCel.new()

View file

@ -69,7 +69,9 @@ func _confirmed() -> void:
for cel_index in project.selected_cels:
if !project.layers[cel_index[1]].can_layer_get_drawn():
continue
var cel: PixelCel = project.frames[cel_index[0]].cels[cel_index[1]]
var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]]
if not cel is PixelCel:
continue
var cel_image: Image = cel.image
commit_action(cel_image)
_commit_undo("Draw", undo_data, project)
@ -78,6 +80,9 @@ func _confirmed() -> void:
var undo_data := _get_undo_data(project)
var i := 0
for cel in project.frames[project.current_frame].cels:
if not cel is PixelCel:
i += 1
continue
if project.layers[i].can_layer_get_drawn():
commit_action(cel.image)
i += 1
@ -88,6 +93,9 @@ func _confirmed() -> void:
for frame in project.frames:
var i := 0
for cel in frame.cels:
if not cel is PixelCel:
i += 1
continue
if project.layers[i].can_layer_get_drawn():
commit_action(cel.image)
i += 1
@ -99,6 +107,9 @@ func _confirmed() -> void:
for frame in _project.frames:
var i := 0
for cel in frame.cels:
if not cel is PixelCel:
i += 1
continue
if _project.layers[i].can_layer_get_drawn():
commit_action(cel.image, _project)
i += 1

32
src/Classes/Layer3D.gd Normal file
View file

@ -0,0 +1,32 @@
class_name Layer3D
extends BaseLayer
func _init(_project, _name := "") -> void:
project = _project
name = _name
# Overridden Methods:
func serialize() -> Dictionary:
var dict = .serialize()
dict["type"] = get_layer_type()
return dict
func get_layer_type() -> int:
return Global.LayerTypes.THREE_D
func new_empty_cel() -> BaseCel:
return Cel3D.new(project.size)
func can_layer_get_drawn() -> bool:
return is_visible_in_hierarchy() && !is_locked_in_hierarchy()
func instantiate_layer_button() -> Node:
return Global.base_layer_button_node.instance()

295
src/Classes/ObjParse.gd Normal file
View file

@ -0,0 +1,295 @@
class_name ObjParse
const DEBUG := false
# 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
# Public methods
# Create mesh from obj and mtl paths
static func load_obj(obj_path: String, mtl_path: String = "") -> Mesh:
if mtl_path == "":
mtl_path = search_mtl_path(obj_path)
var obj := get_data(obj_path)
var mats := {}
if mtl_path != "":
mats = _create_mtl(get_data(mtl_path), get_mtl_tex(mtl_path))
return _create_obj(obj, mats) if obj and mats else null
# Create mesh from obj, materials. Materials should be {"matname":data}
static func load_obj_from_buffer(obj_data: String, materials: Dictionary) -> Mesh:
return _create_obj(obj_data, materials)
# Create materials
static func load_mtl_from_buffer(mtl_data: String, textures: Dictionary) -> Dictionary:
return _create_mtl(mtl_data, textures)
# Get data from file path
static func get_data(path: String) -> String:
if path != "":
var file := File.new()
var err := file.open(path, File.READ)
if err == OK:
var res := file.get_as_text()
file.close()
return res
return ""
# Get textures from mtl path (return {"tex_path":data})
static func get_mtl_tex(mtl_path: String) -> Dictionary:
var file_paths := get_mtl_tex_paths(mtl_path)
var textures := {}
for k in file_paths:
textures[k] = _get_image(mtl_path, k).save_png_to_buffer()
return textures
# Get textures paths from mtl path
static func get_mtl_tex_paths(mtl_path: String) -> Array:
var file := File.new()
var err := file.open(mtl_path, File.READ)
var paths := []
if err == OK:
var lines := file.get_as_text().split("\n", false)
file.close()
for line in lines:
var parts = line.split(" ", false, 1)
if parts[0] in ["map_Kd", "map_Ks", "map_Ka"]:
if !parts[1] in paths:
paths.push_back(parts[1])
return paths
# Try to find mtl path from obj path
static func search_mtl_path(obj_path: String) -> String:
var mtl_path := obj_path.get_base_dir().plus_file(
obj_path.get_file().rsplit(".", false, 1)[0] + ".mtl"
)
var dir: Directory = Directory.new()
if !dir.file_exists(mtl_path):
mtl_path = obj_path.get_base_dir().plus_file(obj_path.get_file() + ".mtl")
if !dir.file_exists(mtl_path):
return ""
return mtl_path
# Private methods
static func _create_mtl(obj: String, textures: Dictionary) -> Dictionary:
var mats := {}
var current_mat: SpatialMaterial = null
var lines := obj.split("\n", false)
for line in lines:
var parts = line.split(" ", false)
match parts[0]:
"#":
# Comment
#print("Comment: "+line)
pass
"newmtl":
# Create a new material
if DEBUG:
print("Adding new material " + parts[1])
current_mat = SpatialMaterial.new()
mats[parts[1]] = current_mat
"Ka":
# Ambient color
#current_mat.albedo_color = Color(float(parts[1]), float(parts[2]), float(parts[3]))
pass
"Kd":
# Diffuse color
current_mat.albedo_color = Color(float(parts[1]), float(parts[2]), float(parts[3]))
if DEBUG:
print("Setting material color " + str(current_mat.albedo_color))
_:
if parts[0] in ["map_Kd", "map_Ks", "map_Ka"]:
var path = line.split(" ", false, 1)[1]
if textures.has(path):
current_mat.albedo_texture = _create_texture(textures[path])
return mats
static func _get_image(mtl_filepath: String, tex_filename: String) -> Image:
if DEBUG:
print(" Debug: Mapping texture file " + tex_filename)
var texfilepath := tex_filename
if tex_filename.is_rel_path():
texfilepath = mtl_filepath.get_base_dir().plus_file(tex_filename)
var filetype := texfilepath.get_extension()
if DEBUG:
print(" Debug: texture file path: " + texfilepath + " of type " + filetype)
var img: Image = Image.new()
img.load(texfilepath)
return img
static func _create_texture(data: PoolByteArray) -> ImageTexture:
var img: Image = Image.new()
var tex: ImageTexture = ImageTexture.new()
img.load_png_from_buffer(data)
tex.create_from_image(img)
return tex
static func _create_obj(obj: String, mats: Dictionary) -> Mesh:
# Setup
var mesh := ArrayMesh.new()
var vertices := PoolVector3Array()
var normals := PoolVector3Array()
var uvs := PoolVector2Array()
var faces := {}
var mat_name := "default"
var count_mtl := 0
# Parse
var lines := obj.split("\n", false)
for line in lines:
var parts = line.split(" ", false)
match parts[0]:
"#":
# Comment
#print("Comment: "+line)
pass
"v":
# Vertex
var n_v = Vector3(float(parts[1]), float(parts[2]), float(parts[3]))
vertices.append(n_v)
"vn":
# Normal
var n_vn = Vector3(float(parts[1]), float(parts[2]), float(parts[3]))
normals.append(n_vn)
"vt":
# UV
var n_uv = Vector2(float(parts[1]), 1 - float(parts[2]))
uvs.append(n_uv)
"usemtl":
# Material group
count_mtl += 1
mat_name = parts[1]
if not faces.has(mat_name):
var mats_keys := mats.keys()
if !mats.has(mat_name):
if mats_keys.size() > count_mtl:
mat_name = mats_keys[count_mtl]
faces[mat_name] = []
"f":
if not faces.has(mat_name):
var mats_keys := mats.keys()
if mats_keys.size() > count_mtl:
mat_name = mats_keys[count_mtl]
faces[mat_name] = []
# Face
if parts.size() == 4:
# Tri
var face = {"v": [], "vt": [], "vn": []}
for map in parts:
var vertices_index = map.split("/")
if str(vertices_index[0]) != "f":
face["v"].append(int(vertices_index[0]) - 1)
face["vt"].append(int(vertices_index[1]) - 1)
if vertices_index.size() > 2:
face["vn"].append(int(vertices_index[2]) - 1)
if faces.has(mat_name):
faces[mat_name].append(face)
elif parts.size() > 4:
# Triangulate
var points = []
for map in parts:
var vertices_index = map.split("/")
if str(vertices_index[0]) != "f":
var point = []
point.append(int(vertices_index[0]) - 1)
point.append(int(vertices_index[1]) - 1)
if vertices_index.size() > 2:
point.append(int(vertices_index[2]) - 1)
points.append(point)
for i in points.size():
if i != 0:
var face = {"v": [], "vt": [], "vn": []}
var point0 = points[0]
var point1 = points[i]
var point2 = points[i - 1]
face["v"].append(point0[0])
face["v"].append(point2[0])
face["v"].append(point1[0])
face["vt"].append(point0[1])
face["vt"].append(point2[1])
face["vt"].append(point1[1])
if point0.size() > 2:
face["vn"].append(point0[2])
if point2.size() > 2:
face["vn"].append(point2[2])
if point1.size() > 2:
face["vn"].append(point1[2])
faces[mat_name].append(face)
# Make tri
for matgroup in faces.keys():
if DEBUG:
print(
(
"Creating surface for matgroup "
+ matgroup
+ " with "
+ str(faces[matgroup].size())
+ " faces"
)
)
# Mesh Assembler
var st = SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
if !mats.has(matgroup):
mats[matgroup] = SpatialMaterial.new()
st.set_material(mats[matgroup])
for face in faces[matgroup]:
if face["v"].size() == 3:
# Vertices
var fan_v = PoolVector3Array()
fan_v.append(vertices[face["v"][0]])
fan_v.append(vertices[face["v"][2]])
fan_v.append(vertices[face["v"][1]])
# Normals
var fan_vn = PoolVector3Array()
if face["vn"].size() > 0:
fan_vn.append(normals[face["vn"][0]])
fan_vn.append(normals[face["vn"][2]])
fan_vn.append(normals[face["vn"][1]])
# Textures
var fan_vt = PoolVector2Array()
if face["vt"].size() > 0:
for k in [0, 2, 1]:
var f = face["vt"][k]
if f > -1:
var uv = uvs[f]
fan_vt.append(uv)
st.add_triangle_fan(fan_v, fan_vt, PoolColorArray(), PoolVector2Array(), fan_vn, [])
mesh = st.commit(mesh)
for k in mesh.get_surface_count():
var mat = mesh.surface_get_material(k)
mat_name = ""
for m in mats:
if mats[m] == mat:
mat_name = m
mesh.surface_set_name(k, mat_name)
# Finish
return mesh

View file

@ -70,5 +70,4 @@ func load_image_data_from_pxo(file: File, project_size: Vector2) -> void:
func instantiate_cel_button() -> Node:
var cel_button = Global.pixel_cel_button_node.instance()
return cel_button
return Global.pixel_cel_button_node.instance()

View file

@ -12,8 +12,8 @@ func _init(_project, _name := "") -> void:
func serialize() -> Dictionary:
var dict = .serialize()
dict["type"] = Global.LayerTypes.PIXEL
var dict := .serialize()
dict["type"] = get_layer_type()
dict["new_cels_linked"] = new_cels_linked
return dict
@ -23,6 +23,10 @@ func deserialize(dict: Dictionary) -> void:
new_cels_linked = dict.new_cels_linked
func get_layer_type() -> int:
return Global.LayerTypes.PIXEL
func new_empty_cel() -> BaseCel:
var image := Image.new()
image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8)

View file

@ -100,6 +100,10 @@ func remove() -> void:
c.queue_free()
for guide in guides:
guide.queue_free()
for frame in frames:
for l in layers.size():
var cel: BaseCel = frame.cels[l]
cel.on_remove()
# Prevents memory leak (due to the layers' project reference stopping ref counting from freeing)
layers.clear()
Global.projects.erase(self)
@ -296,7 +300,8 @@ func serialize() -> Dictionary:
for frame in frames:
var cel_data := []
for cel in frame.cels:
cel_data.append({"opacity": cel.opacity, "metadata": _serialize_metadata(cel)})
cel_data.append(cel.serialize())
cel_data[-1]["metadata"] = _serialize_metadata(cel)
frame_data.append(
{"cels": cel_data, "duration": frame.duration, "metadata": _serialize_metadata(frame)}
@ -356,6 +361,15 @@ func deserialize(dict: Dictionary) -> void:
if dict.has("save_path"):
OpenSave.current_save_paths[Global.projects.find(self)] = dict.save_path
if dict.has("frames") and dict.has("layers"):
for saved_layer in dict.layers:
match int(saved_layer.get("type", Global.LayerTypes.PIXEL)):
Global.LayerTypes.PIXEL:
layers.append(PixelLayer.new(self))
Global.LayerTypes.GROUP:
layers.append(GroupLayer.new(self))
Global.LayerTypes.THREE_D:
layers.append(Layer3D.new(self))
var frame_i := 0
for frame in dict.frames:
var cels := []
@ -363,9 +377,12 @@ func deserialize(dict: Dictionary) -> void:
for cel in frame.cels:
match int(dict.layers[cel_i].get("type", Global.LayerTypes.PIXEL)):
Global.LayerTypes.PIXEL:
cels.append(PixelCel.new(Image.new(), cel.opacity))
cels.append(PixelCel.new(Image.new()))
Global.LayerTypes.GROUP:
cels.append(GroupCel.new(cel.opacity))
cels.append(GroupCel.new())
Global.LayerTypes.THREE_D:
cels.append(Cel3D.new(size, true))
cels[cel_i].deserialize(cel)
_deserialize_metadata(cels[cel_i], cel)
cel_i += 1
var duration := 1.0
@ -379,12 +396,6 @@ func deserialize(dict: Dictionary) -> void:
frames.append(frame_class)
frame_i += 1
for saved_layer in dict.layers:
match int(saved_layer.get("type", Global.LayerTypes.PIXEL)):
Global.LayerTypes.PIXEL:
layers.append(PixelLayer.new(self))
Global.LayerTypes.GROUP:
layers.append(GroupLayer.new(self))
# Parent references to other layers are created when deserializing
# a layer, so loop again after creating them:
for layer_i in dict.layers.size():
@ -553,6 +564,7 @@ func toggle_layer_buttons() -> void:
current_layer == child_count
or layers[current_layer] is GroupLayer
or layers[current_layer - 1] is GroupLayer
or layers[current_layer - 1] is Layer3D
)
)
@ -680,6 +692,7 @@ func remove_frames(indices: Array) -> void: # indices should be in ascending or
# For each linked cel in the frame, update its layer's cel_link_sets
for l in layers.size():
var cel: BaseCel = frames[indices[i] - i].cels[l]
cel.on_remove()
if cel.link_set != null:
cel.link_set["cels"].erase(cel)
if cel.link_set["cels"].empty():
@ -761,6 +774,7 @@ func remove_layers(indices: Array) -> void:
# With each removed index, future indices need to be lowered, so subtract by i
layers.remove(indices[i] - i)
for frame in frames:
frame.cels[indices[i] - i].on_remove()
frame.cels.remove(indices[i] - i)
Global.animation_timeline.project_layer_removed(indices[i] - i)
# Update the layer indices and layer/cel buttons:

449
src/Tools/3DShapeEdit.gd Normal file
View file

@ -0,0 +1,449 @@
extends BaseTool
var _cel: Cel3D
var _can_start_timer := true
var _hovering: Cel3DObject = null
var _dragging := false
var _has_been_dragged := false
var _prev_mouse_pos := Vector2.ZERO
var _object_names := {
Cel3DObject.Type.BOX: "Box",
Cel3DObject.Type.SPHERE: "Sphere",
Cel3DObject.Type.CAPSULE: "Capsule",
Cel3DObject.Type.CYLINDER: "Cylinder",
Cel3DObject.Type.PRISM: "Prism",
Cel3DObject.Type.TORUS: "Torus",
Cel3DObject.Type.PLANE: "Plane",
Cel3DObject.Type.TEXT: "Text",
Cel3DObject.Type.DIR_LIGHT: "Directional light",
Cel3DObject.Type.SPOT_LIGHT: "Spotlight",
Cel3DObject.Type.OMNI_LIGHT: "Point light",
Cel3DObject.Type.IMPORTED: "Custom model",
}
onready var object_option_button := $"%ObjectOptionButton" as OptionButton
onready var new_object_menu_button := $"%NewObjectMenuButton" as MenuButton
onready var remove_object_button := $"%RemoveObject" as Button
onready var cel_options := $"%CelOptions" as Container
onready var object_options := $"%ObjectOptions" as Container
onready var mesh_options := $"%MeshOptions" as VBoxContainer
onready var light_options := $"%LightOptions" as VBoxContainer
onready var undo_redo_timer := $UndoRedoTimer as Timer
onready var load_model_dialog := $LoadModelDialog as FileDialog
onready var cel_properties := {
"camera:projection": $"%ProjectionOptionButton",
"camera:rotation_degrees": $"%CameraRotation",
"viewport:world:environment:ambient_light_color": $"%AmbientColorPickerButton",
"viewport:world:environment:ambient_light_energy": $"%AmbientEnergy",
}
onready var object_properties := {
"visible": $"%VisibleCheckBox",
"translation": $"%ObjectPosition",
"rotation_degrees": $"%ObjectRotation",
"scale": $"%ObjectScale",
"node3d_type:mesh:size": $"%MeshSize",
"node3d_type:mesh:sizev2": $"%MeshSizeV2",
"node3d_type:mesh:left_to_right": $"%MeshLeftToRight",
"node3d_type:mesh:radius": $"%MeshRadius",
"node3d_type:mesh:height": $"%MeshHeight",
"node3d_type:mesh:radial_segments": $"%MeshRadialSegments",
"node3d_type:mesh:rings": $"%MeshRings",
"node3d_type:mesh:is_hemisphere": $"%MeshIsHemisphere",
"node3d_type:mesh:mid_height": $"%MeshMidHeight",
"node3d_type:mesh:top_radius": $"%MeshTopRadius",
"node3d_type:mesh:bottom_radius": $"%MeshBottomRadius",
"node3d_type:mesh:text": $"%MeshText",
"node3d_type:mesh:pixel_size": $"%MeshPixelSize",
"node3d_type:mesh:curve_step": $"%MeshCurveStep",
"node3d_type:mesh:horizontal_alignment": $"%MeshHorizontalAlignment",
"node3d_type:light_color": $"%LightColor",
"node3d_type:light_energy": $"%LightEnergy",
"node3d_type:light_negative": $"%LightNegative",
"node3d_type:shadow_enabled": $"%ShadowEnabled",
"node3d_type:shadow_color": $"%ShadowColor",
"node3d_type:omni_range": $"%OmniRange",
"node3d_type:spot_range": $"%SpotRange",
"node3d_type:spot_angle": $"%SpotAngle",
}
func _ready() -> void:
Global.connect("cel_changed", self, "_cel_changed")
_cel_changed()
var new_object_popup := new_object_menu_button.get_popup()
for object in _object_names:
if object == Cel3DObject.Type.TORUS: # Remove when Godot 3.6 or 4.0 is used
continue
new_object_popup.add_item(_object_names[object], object)
new_object_popup.connect("id_pressed", self, "_new_object_popup_id_pressed")
for prop in cel_properties:
var node: Control = cel_properties[prop]
if node is ValueSliderV3:
node.connect("value_changed", self, "_cel_property_vector3_changed", [prop])
elif node is Range:
node.connect("value_changed", self, "_cel_property_value_changed", [prop])
elif node is OptionButton:
node.connect("item_selected", self, "_cel_property_item_selected", [prop])
elif node is ColorPickerButton:
node.connect("color_changed", self, "_cel_property_color_changed", [prop])
for prop in object_properties:
var node: Control = object_properties[prop]
if node is ValueSliderV3:
node.connect("value_changed", self, "_object_property_vector3_changed", [prop])
elif node is ValueSliderV2:
var property_path: String = prop
if property_path.ends_with("v2"):
property_path = property_path.replace("v2", "")
node.connect("value_changed", self, "_object_property_vector2_changed", [property_path])
elif node is Range:
node.connect("value_changed", self, "_object_property_value_changed", [prop])
elif node is OptionButton:
node.connect("item_selected", self, "_object_property_item_selected", [prop])
elif node is ColorPickerButton:
node.connect("color_changed", self, "_object_property_color_changed", [prop])
elif node is CheckBox:
node.connect("toggled", self, "_object_property_toggled", [prop])
elif node is LineEdit:
node.connect("text_changed", self, "_object_property_text_changed", [prop])
func draw_start(position: Vector2) -> void:
var project: Project = Global.current_project
if not project.get_current_cel() is Cel3D:
return
if not project.layers[project.current_layer].can_layer_get_drawn():
return
var found_cel := false
for frame_layer in project.selected_cels:
if _cel == project.frames[frame_layer[0]].cels[frame_layer[1]]:
found_cel = true
if not found_cel:
return
if is_instance_valid(_cel.selected):
# Needs canvas.current_pixel, because draw_start()'s position is floored
_cel.selected.applying_gizmos = Global.canvas.gizmos_3d.get_hovering_gizmo(
Global.canvas.current_pixel
)
if is_instance_valid(_hovering):
_cel.selected = _hovering
_dragging = true
_prev_mouse_pos = position
else: # We're not hovering
if is_instance_valid(_cel.selected):
# If we're not clicking on a gizmo, unselect
if _cel.selected.applying_gizmos == Cel3DObject.Gizmos.NONE:
_cel.selected = null
else:
_dragging = true
_prev_mouse_pos = position
func draw_move(position: Vector2) -> void:
if not Global.current_project.get_current_cel() is Cel3D:
return
var camera: Camera = _cel.camera
if _dragging:
_has_been_dragged = true
var proj_mouse_pos := camera.project_position(position, camera.translation.z)
var proj_prev_mouse_pos := camera.project_position(_prev_mouse_pos, camera.translation.z)
_cel.selected.change_transform(proj_mouse_pos, proj_prev_mouse_pos)
_prev_mouse_pos = position
func draw_end(_position: Vector2) -> void:
if not Global.current_project.get_current_cel() is Cel3D:
return
_dragging = false
if is_instance_valid(_cel.selected) and _has_been_dragged:
_cel.selected.applying_gizmos = Cel3DObject.Gizmos.NONE
_object_property_changed(_cel.selected)
_has_been_dragged = false
func cursor_move(position: Vector2) -> void:
.cursor_move(position)
if not Global.current_project.get_current_cel() is Cel3D:
return
# Hover logic
var camera: Camera = _cel.camera
var ray_from := camera.project_ray_origin(position)
var ray_to := ray_from + camera.project_ray_normal(position) * 20
var space_state := camera.get_world().direct_space_state
var selection := space_state.intersect_ray(ray_from, ray_to)
if selection.empty():
if is_instance_valid(_hovering):
_hovering.unhover()
_hovering = null
else:
if is_instance_valid(_hovering):
_hovering.unhover()
_hovering = selection["collider"].get_parent()
_hovering.hover()
func _on_ObjectOptionButton_item_selected(index: int) -> void:
if not _cel is Cel3D:
return
var id := object_option_button.get_item_id(index) - 1
var object := _cel.get_object_from_id(id)
if not is_instance_valid(object):
_cel.selected = null
return
_cel.selected = object
func _cel_changed() -> void:
if not Global.current_project.get_current_cel() is Cel3D:
get_child(0).visible = false # Just to ensure that the content of the tool is hidden
return
get_child(0).visible = true
_cel = Global.current_project.get_current_cel()
_cel.selected = null
if not _cel.is_connected("scene_property_changed", self, "_set_cel_node_values"):
_cel.connect("scene_property_changed", self, "_set_cel_node_values")
_cel.connect("objects_changed", self, "_fill_object_option_button")
_cel.connect("selected_object", self, "_selected_object")
cel_options.visible = true
object_options.visible = false
_set_cel_node_values()
_fill_object_option_button()
func _new_object_popup_id_pressed(id: int) -> void:
if id == Cel3DObject.Type.IMPORTED:
load_model_dialog.popup_centered()
Global.dialog_open(true)
else:
_add_object(id)
func _add_object(type: int, file_path := "") -> void:
var dict := {"type": type, "file_path": file_path}
var new_objects := _cel.object_properties.duplicate()
new_objects[_cel.current_object_id] = dict
var undo_redo: UndoRedo = Global.current_project.undo_redo
undo_redo.create_action("Add 3D object")
undo_redo.add_do_property(_cel, "object_properties", new_objects)
undo_redo.add_undo_property(_cel, "object_properties", _cel.object_properties)
undo_redo.add_do_method(_cel, "_add_object_node", _cel.current_object_id)
undo_redo.add_undo_method(_cel, "_remove_object_node", _cel.current_object_id)
undo_redo.add_do_method(Global, "undo_or_redo", false)
undo_redo.add_undo_method(Global, "undo_or_redo", true)
undo_redo.commit_action()
_cel.current_object_id += 1
func _on_RemoveObject_pressed() -> void:
if is_instance_valid(_cel.selected):
var new_objects := _cel.object_properties.duplicate()
new_objects.erase(_cel.selected.id)
var undo_redo: UndoRedo = Global.current_project.undo_redo
undo_redo.create_action("Remove 3D object")
undo_redo.add_do_property(_cel, "object_properties", new_objects)
undo_redo.add_undo_property(_cel, "object_properties", _cel.object_properties)
undo_redo.add_do_method(_cel, "_remove_object_node", _cel.selected.id)
undo_redo.add_undo_method(_cel, "_add_object_node", _cel.selected.id)
undo_redo.add_do_method(Global, "undo_or_redo", false)
undo_redo.add_undo_method(Global, "undo_or_redo", true)
undo_redo.commit_action()
_cel.selected = null
func _object_property_changed(object: Cel3DObject) -> void:
var undo_redo: UndoRedo = Global.current_project.undo_redo
var new_properties := _cel.object_properties.duplicate()
new_properties[object.id] = object.serialize()
undo_redo.create_action("Change object transform")
undo_redo.add_do_property(_cel, "object_properties", new_properties)
undo_redo.add_undo_property(_cel, "object_properties", _cel.object_properties)
undo_redo.add_do_method(_cel, "_update_objects_transform", object.id)
undo_redo.add_undo_method(_cel, "_update_objects_transform", object.id)
undo_redo.add_do_method(Global, "undo_or_redo", false)
undo_redo.add_undo_method(Global, "undo_or_redo", true)
undo_redo.commit_action()
func _selected_object(object: Cel3DObject) -> void:
if is_instance_valid(object):
cel_options.visible = false
object_options.visible = true
remove_object_button.disabled = false
for prop in object_properties: # Hide irrelevant nodes
var node: Control = object_properties[prop]
var property_path: String = prop
if property_path.ends_with("v2"):
property_path = property_path.replace("v2", "")
var property = object.get_indexed(property_path)
var property_exists: bool = property != null
# Differentiate between the mesh size of a box/prism (Vector3) and a plane (Vector2)
if node is ValueSliderV3 and typeof(property) != TYPE_VECTOR3:
property_exists = false
elif node is ValueSliderV2 and typeof(property) != TYPE_VECTOR2:
property_exists = false
if node.get_index() > 0:
var prev_node: Control = node.get_parent().get_child(node.get_index() - 1)
prev_node.visible = property_exists
node.visible = property_exists
mesh_options.visible = object.node3d_type is MeshInstance
light_options.visible = object.node3d_type is Light
_set_object_node_values()
if not object.is_connected("property_changed", self, "_set_object_node_values"):
object.connect("property_changed", self, "_set_object_node_values")
object_option_button.select(object_option_button.get_item_index(object.id + 1))
else:
cel_options.visible = true
object_options.visible = false
remove_object_button.disabled = true
object_option_button.select(0)
func _set_cel_node_values() -> void:
_can_start_timer = false
_set_node_values(_cel, cel_properties)
_can_start_timer = true
func _set_object_node_values() -> void:
var object: Cel3DObject = _cel.selected
if not is_instance_valid(object):
return
_can_start_timer = false
_set_node_values(object, object_properties)
_can_start_timer = true
func _set_node_values(to_edit: Object, properties: Dictionary) -> void:
for prop in properties:
var property_path: String = prop
if property_path.ends_with("v2"):
property_path = property_path.replace("v2", "")
var value = to_edit.get_indexed(property_path)
if value == null:
continue
if "scale" in prop:
value *= 100
var node: Control = properties[prop]
if node is Range or node is ValueSliderV3 or node is ValueSliderV2:
if typeof(node.value) != typeof(value) and typeof(value) != TYPE_INT:
continue
node.value = value
elif node is OptionButton:
node.selected = value
elif node is ColorPickerButton:
node.color = value
elif node is CheckBox:
node.pressed = value
elif node is LineEdit:
node.text = value
func _set_value_from_node(to_edit: Object, value, prop: String) -> void:
if not is_instance_valid(to_edit):
return
if "mesh_" in prop:
prop = prop.replace("mesh_", "")
to_edit = to_edit.node3d_type.mesh
if "scale" in prop:
value /= 100
to_edit.set_indexed(prop, value)
func _cel_property_vector3_changed(value: Vector3, prop: String) -> void:
_set_value_from_node(_cel, value, prop)
_value_handle_change()
Global.canvas.gizmos_3d.update()
func _cel_property_value_changed(value: float, prop: String) -> void:
_set_value_from_node(_cel, value, prop)
_value_handle_change()
Global.canvas.gizmos_3d.update()
func _cel_property_item_selected(value: int, prop: String) -> void:
_set_value_from_node(_cel, value, prop)
_value_handle_change()
Global.canvas.gizmos_3d.update()
func _cel_property_color_changed(value: Color, prop: String) -> void:
_set_value_from_node(_cel, value, prop)
_value_handle_change()
Global.canvas.gizmos_3d.update()
func _object_property_vector3_changed(value: Vector3, prop: String) -> void:
_set_value_from_node(_cel.selected, value, prop)
_value_handle_change()
func _object_property_vector2_changed(value: Vector2, prop: String) -> void:
_set_value_from_node(_cel.selected, value, prop)
_value_handle_change()
func _object_property_value_changed(value: float, prop: String) -> void:
_set_value_from_node(_cel.selected, value, prop)
_value_handle_change()
func _object_property_item_selected(value: int, prop: String) -> void:
_set_value_from_node(_cel.selected, value, prop)
_value_handle_change()
func _object_property_color_changed(value: Color, prop: String) -> void:
_set_value_from_node(_cel.selected, value, prop)
_value_handle_change()
func _object_property_toggled(value: bool, prop: String) -> void:
_set_value_from_node(_cel.selected, value, prop)
_value_handle_change()
func _object_property_text_changed(value: String, prop: String) -> void:
_set_value_from_node(_cel.selected, value, prop)
_value_handle_change()
func _value_handle_change() -> void:
if _can_start_timer:
undo_redo_timer.start()
func _fill_object_option_button() -> void:
if not _cel is Cel3D:
return
object_option_button.clear()
object_option_button.add_item("None", 0)
for id in _cel.object_properties:
var item_name: String = _object_names[_cel.object_properties[id]["type"]]
object_option_button.add_item(item_name, id + 1)
func _on_UndoRedoTimer_timeout() -> void:
if is_instance_valid(_cel.selected):
_object_property_changed(_cel.selected)
else:
var undo_redo: UndoRedo = Global.current_project.undo_redo
undo_redo.create_action("Change 3D layer properties")
undo_redo.add_do_property(_cel, "scene_properties", _cel.serialize_scene_properties())
undo_redo.add_undo_property(_cel, "scene_properties", _cel.scene_properties)
undo_redo.add_do_method(_cel, "_scene_property_changed")
undo_redo.add_undo_method(_cel, "_scene_property_changed")
undo_redo.add_do_method(Global, "undo_or_redo", false)
undo_redo.add_undo_method(Global, "undo_or_redo", true)
undo_redo.commit_action()
func _on_LoadModelDialog_files_selected(paths: PoolStringArray) -> void:
for path in paths:
_add_object(Cel3DObject.Type.IMPORTED, path)
func _on_LoadModelDialog_popup_hide() -> void:
Global.dialog_open(false)

822
src/Tools/3DShapeEdit.tscn Normal file
View file

@ -0,0 +1,822 @@
[gd_scene load_steps=10 format=2]
[ext_resource path="res://src/Tools/BaseTool.tscn" type="PackedScene" id=1]
[ext_resource path="res://src/Tools/3DShapeEdit.gd" type="Script" id=2]
[ext_resource path="res://src/UI/Nodes/ValueSliderV2.tscn" type="PackedScene" id=3]
[ext_resource path="res://src/UI/Nodes/ValueSlider.tscn" type="PackedScene" id=4]
[ext_resource path="res://src/UI/Nodes/ValueSlider.gd" type="Script" id=5]
[ext_resource path="res://src/UI/Nodes/CollapsibleContainer.gd" type="Script" id=6]
[ext_resource path="res://src/UI/Nodes/ValueSliderV3.tscn" type="PackedScene" id=7]
[sub_resource type="InputEventAction" id=33]
action = "delete"
[sub_resource type="ShortCut" id=34]
shortcut = SubResource( 33 )
[node name="3DShapeEdit" instance=ExtResource( 1 )]
script = ExtResource( 2 )
[node name="ColorRect" parent="." index="0"]
margin_right = 223.0
[node name="Label" parent="." index="1"]
margin_right = 223.0
[node name="HandleObjects" type="GridContainer" parent="." index="2"]
margin_top = 26.0
margin_right = 223.0
margin_bottom = 70.0
columns = 2
[node name="Label" type="Label" parent="HandleObjects" index="0"]
margin_top = 3.0
margin_right = 111.0
margin_bottom = 17.0
size_flags_horizontal = 3
text = "Selected object:"
[node name="ObjectOptionButton" type="OptionButton" parent="HandleObjects" index="1"]
unique_name_in_owner = true
margin_left = 115.0
margin_right = 223.0
margin_bottom = 20.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
text = "None"
items = [ "None", null, false, 0, null ]
selected = 0
[node name="NewObjectMenuButton" type="MenuButton" parent="HandleObjects" index="2"]
unique_name_in_owner = true
margin_top = 24.0
margin_right = 111.0
margin_bottom = 44.0
focus_mode = 2
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
text = "Add new object"
align = 0
[node name="RemoveObject" type="Button" parent="HandleObjects" index="3"]
unique_name_in_owner = true
margin_left = 115.0
margin_top = 24.0
margin_right = 223.0
margin_bottom = 44.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
disabled = true
shortcut = SubResource( 34 )
text = "Remove object"
[node name="CelOptions" type="VBoxContainer" parent="." index="3"]
unique_name_in_owner = true
margin_top = 74.0
margin_right = 223.0
margin_bottom = 226.0
size_flags_vertical = 3
[node name="CameraOptions" type="VBoxContainer" parent="CelOptions" index="0"]
margin_right = 223.0
margin_bottom = 128.0
theme_type_variation = "CollapsibleContainer"
script = ExtResource( 6 )
text = "Camera"
visible_content = true
[node name="GridContainer" type="GridContainer" parent="CelOptions/CameraOptions" index="1"]
margin_top = 24.0
margin_right = 223.0
margin_bottom = 128.0
columns = 2
[node name="ProjectionLabel" type="Label" parent="CelOptions/CameraOptions/GridContainer" index="0"]
margin_top = 3.0
margin_right = 110.0
margin_bottom = 17.0
size_flags_horizontal = 3
text = "Projection:"
[node name="ProjectionOptionButton" type="OptionButton" parent="CelOptions/CameraOptions/GridContainer" index="1"]
unique_name_in_owner = true
margin_left = 114.0
margin_right = 223.0
margin_bottom = 20.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
text = "Perspective"
items = [ "Perspective", null, false, 0, null, "Orthogonal", null, false, 1, null, "Frustum", null, false, 2, null ]
selected = 0
[node name="RotationLabel" type="Label" parent="CelOptions/CameraOptions/GridContainer" index="2"]
margin_top = 24.0
margin_right = 110.0
margin_bottom = 38.0
size_flags_horizontal = 3
size_flags_vertical = 0
text = "Rotation:"
[node name="CameraRotation" parent="CelOptions/CameraOptions/GridContainer" index="3" instance=ExtResource( 7 )]
unique_name_in_owner = true
margin_left = 114.0
margin_top = 24.0
margin_right = 223.0
margin_bottom = 104.0
size_flags_horizontal = 3
min_value = Vector3( -180, -180, -180 )
max_value = Vector3( 180, 180, 180 )
step = 0.1
suffix_x = "°"
suffix_y = "°"
suffix_z = "°"
[node name="EnvironmentOptions" type="VBoxContainer" parent="CelOptions" index="1"]
margin_top = 132.0
margin_right = 223.0
margin_bottom = 152.0
theme_type_variation = "CollapsibleContainer"
script = ExtResource( 6 )
text = "Environment"
[node name="GridContainer" type="GridContainer" parent="CelOptions/EnvironmentOptions" index="1"]
visible = false
margin_right = 1266.0
margin_bottom = 48.0
columns = 2
[node name="AmbientColorLabel" type="Label" parent="CelOptions/EnvironmentOptions/GridContainer" index="0"]
margin_top = 3.0
margin_right = 631.0
margin_bottom = 17.0
size_flags_horizontal = 3
text = "Ambient color:"
[node name="AmbientColorPickerButton" type="ColorPickerButton" parent="CelOptions/EnvironmentOptions/GridContainer" index="1"]
unique_name_in_owner = true
margin_left = 635.0
margin_right = 1266.0
margin_bottom = 20.0
size_flags_horizontal = 3
edit_alpha = false
[node name="AmbientEnergyLabel" type="Label" parent="CelOptions/EnvironmentOptions/GridContainer" index="2"]
margin_top = 29.0
margin_right = 631.0
margin_bottom = 43.0
size_flags_horizontal = 3
text = "Ambient color energy:"
[node name="AmbientEnergy" parent="CelOptions/EnvironmentOptions/GridContainer" index="3" instance=ExtResource( 4 )]
unique_name_in_owner = true
margin_left = 635.0
margin_top = 24.0
margin_right = 1266.0
margin_bottom = 48.0
max_value = 16.0
step = 0.01
value = 1.0
allow_greater = true
[node name="ObjectOptions" type="VBoxContainer" parent="." index="4"]
unique_name_in_owner = true
visible = false
margin_top = 408.0
margin_right = 1266.0
margin_bottom = 800.0
size_flags_vertical = 3
[node name="GlobalOptions" type="GridContainer" parent="ObjectOptions" index="0"]
margin_right = 40.0
margin_bottom = 40.0
columns = 2
[node name="VisibleLabel" type="Label" parent="ObjectOptions/GlobalOptions" index="0"]
margin_right = 40.0
margin_bottom = 14.0
size_flags_horizontal = 3
text = "Visible:"
[node name="VisibleCheckBox" type="CheckBox" parent="ObjectOptions/GlobalOptions" index="1"]
unique_name_in_owner = true
margin_right = 71.0
margin_bottom = 24.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
text = "On"
[node name="TransformOptions" type="VBoxContainer" parent="ObjectOptions" index="1"]
margin_right = 1266.0
margin_bottom = 272.0
theme_type_variation = "CollapsibleContainer"
script = ExtResource( 6 )
text = "Transform"
visible_content = true
[node name="GridContainer" type="GridContainer" parent="ObjectOptions/TransformOptions" index="1"]
margin_top = 24.0
margin_right = 1266.0
margin_bottom = 272.0
columns = 2
[node name="PositionLabel" type="Label" parent="ObjectOptions/TransformOptions/GridContainer" index="0"]
margin_right = 631.0
margin_bottom = 80.0
size_flags_horizontal = 3
size_flags_vertical = 1
text = "Position:"
[node name="ObjectPosition" parent="ObjectOptions/TransformOptions/GridContainer" index="1" instance=ExtResource( 7 )]
unique_name_in_owner = true
size_flags_horizontal = 3
min_value = Vector3( -20, -20, -20 )
max_value = Vector3( 20, 20, 20 )
step = 0.01
allow_greater = true
allow_lesser = true
suffix_x = "m"
suffix_y = "m"
suffix_z = "m"
[node name="RotationLabel" type="Label" parent="ObjectOptions/TransformOptions/GridContainer" index="2"]
margin_top = 84.0
margin_right = 631.0
margin_bottom = 164.0
size_flags_horizontal = 3
size_flags_vertical = 1
text = "Rotation:"
[node name="ObjectRotation" parent="ObjectOptions/TransformOptions/GridContainer" index="3" instance=ExtResource( 7 )]
unique_name_in_owner = true
margin_top = -226.0
margin_right = 631.0
margin_bottom = -146.0
size_flags_horizontal = 3
min_value = Vector3( -180, -180, -180 )
max_value = Vector3( 180, 180, 180 )
step = 0.1
suffix_x = "°"
suffix_y = "°"
suffix_z = "°"
[node name="ScaleLabel" type="Label" parent="ObjectOptions/TransformOptions/GridContainer" index="4"]
margin_top = 168.0
margin_right = 631.0
margin_bottom = 248.0
size_flags_horizontal = 3
size_flags_vertical = 1
text = "Scale:"
[node name="ObjectScale" parent="ObjectOptions/TransformOptions/GridContainer" index="5" instance=ExtResource( 7 )]
unique_name_in_owner = true
margin_bottom = 80.0
size_flags_horizontal = 3
value = Vector3( 100, 100, 100 )
step = 0.01
allow_greater = true
allow_lesser = true
show_ratio = true
suffix_x = "%"
suffix_y = "%"
suffix_z = "%"
[node name="MeshOptions" type="VBoxContainer" parent="ObjectOptions" index="2"]
unique_name_in_owner = true
margin_top = 276.0
margin_right = 1266.0
margin_bottom = 296.0
theme_type_variation = "CollapsibleContainer"
script = ExtResource( 6 )
text = "Mesh"
[node name="GridContainer" type="GridContainer" parent="ObjectOptions/MeshOptions" index="1"]
visible = false
margin_top = 24.0
margin_right = 1266.0
margin_bottom = 104.0
columns = 2
[node name="MeshSizeLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="0"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Size:"
[node name="MeshSize" parent="ObjectOptions/MeshOptions/GridContainer" index="1" instance=ExtResource( 7 )]
unique_name_in_owner = true
size_flags_horizontal = 3
max_value = Vector3( 10, 10, 10 )
step = 0.01
allow_greater = true
show_ratio = true
[node name="MeshSizeLabel2" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="2"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Size:"
[node name="MeshSizeV2" parent="ObjectOptions/MeshOptions/GridContainer" index="3" instance=ExtResource( 3 )]
unique_name_in_owner = true
margin_right = 52.0
size_flags_horizontal = 3
max_value = Vector2( 10, 10 )
allow_greater = true
show_ratio = true
snap_step = 0.01
[node name="MeshLeftToRightLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="4"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Left to right:"
[node name="MeshLeftToRight" type="TextureProgress" parent="ObjectOptions/MeshOptions/GridContainer" index="5"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
min_value = -2.0
max_value = 2.0
step = 0.1
value = 0.5
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="MeshRadiusLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="6"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Radius:"
[node name="MeshRadius" type="TextureProgress" parent="ObjectOptions/MeshOptions/GridContainer" index="7"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
min_value = 0.001
max_value = 10.0
step = 0.01
value = 1.0
allow_greater = true
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="MeshHeightLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="8"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Height:"
[node name="MeshHeight" type="TextureProgress" parent="ObjectOptions/MeshOptions/GridContainer" index="9"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
min_value = 0.001
max_value = 10.0
step = 0.01
value = 2.0
allow_greater = true
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="MeshRadialSegmentsLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="10"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Radial segments:"
[node name="MeshRadialSegments" type="TextureProgress" parent="ObjectOptions/MeshOptions/GridContainer" index="11"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
min_value = 4.0
max_value = 640.0
value = 64.0
allow_greater = true
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="MeshRingsLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="12"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Rings:"
[node name="MeshRings" type="TextureProgress" parent="ObjectOptions/MeshOptions/GridContainer" index="13"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
min_value = 1.0
max_value = 320.0
value = 32.0
allow_greater = true
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="MeshIsHemisphereLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="14"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Is hemisphere:"
[node name="MeshIsHemisphere" type="CheckBox" parent="ObjectOptions/MeshOptions/GridContainer" index="15"]
unique_name_in_owner = true
margin_right = 24.0
margin_bottom = 24.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
text = "On"
[node name="MeshMidHeightLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="16"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Height:"
[node name="MeshMidHeight" type="TextureProgress" parent="ObjectOptions/MeshOptions/GridContainer" index="17"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
min_value = 0.001
max_value = 10.0
step = 0.01
value = 1.0
allow_greater = true
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="MeshTopRadiusLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="18"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Top radius:"
[node name="MeshTopRadius" type="TextureProgress" parent="ObjectOptions/MeshOptions/GridContainer" index="19"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
max_value = 10.0
step = 0.01
value = 1.0
allow_greater = true
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="MeshBottomRadiusLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="20"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Bottom radius:"
[node name="MeshBottomRadius" type="TextureProgress" parent="ObjectOptions/MeshOptions/GridContainer" index="21"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
max_value = 10.0
step = 0.01
value = 1.0
allow_greater = true
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="MeshTextLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="22"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Text:"
[node name="MeshText" type="LineEdit" parent="ObjectOptions/MeshOptions/GridContainer" index="23"]
unique_name_in_owner = true
margin_right = 58.0
margin_bottom = 24.0
size_flags_horizontal = 3
[node name="MeshPixelSizeLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="24"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Pixel size:"
[node name="MeshPixelSize" type="TextureProgress" parent="ObjectOptions/MeshOptions/GridContainer" index="25"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
min_value = 0.001
max_value = 10.0
step = 0.001
value = 0.01
allow_greater = true
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
snap_step = 0.01
[node name="MeshCurveStepLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="26"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Curve step:"
[node name="MeshCurveStep" type="TextureProgress" parent="ObjectOptions/MeshOptions/GridContainer" index="27"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
min_value = 0.1
max_value = 10.0
step = 0.1
value = 0.5
allow_greater = true
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="MeshHorizontalAlignmentLabel" type="Label" parent="ObjectOptions/MeshOptions/GridContainer" index="28"]
margin_top = -583.0
margin_right = 631.0
margin_bottom = -569.0
size_flags_horizontal = 3
text = "Horizontal alignment:"
[node name="MeshHorizontalAlignment" type="OptionButton" parent="ObjectOptions/MeshOptions/GridContainer" index="29"]
unique_name_in_owner = true
margin_left = 635.0
margin_top = -586.0
margin_right = 1266.0
margin_bottom = -566.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
text = "Center"
items = [ "Left", null, false, 0, null, "Center", null, false, 1, null, "Right", null, false, 2, null ]
selected = 1
[node name="LightOptions" type="VBoxContainer" parent="ObjectOptions" index="3"]
unique_name_in_owner = true
margin_top = 276.0
margin_right = 1266.0
margin_bottom = 296.0
theme_type_variation = "CollapsibleContainer"
script = ExtResource( 6 )
text = "Light"
[node name="GridContainer" type="GridContainer" parent="ObjectOptions/LightOptions" index="1"]
visible = false
margin_top = 24.0
margin_right = 1266.0
margin_bottom = 104.0
columns = 2
[node name="LightColorLabel" type="Label" parent="ObjectOptions/LightOptions/GridContainer" index="0"]
margin_top = -499.0
margin_right = 631.0
margin_bottom = -485.0
size_flags_horizontal = 3
text = "Color:"
[node name="LightColor" type="ColorPickerButton" parent="ObjectOptions/LightOptions/GridContainer" index="1"]
unique_name_in_owner = true
margin_left = 635.0
margin_top = -502.0
margin_right = 1266.0
margin_bottom = -482.0
size_flags_horizontal = 3
color = Color( 1, 1, 1, 1 )
edit_alpha = false
[node name="LightEnergyLabel" type="Label" parent="ObjectOptions/LightOptions/GridContainer" index="2"]
margin_top = -473.0
margin_right = 631.0
margin_bottom = -459.0
size_flags_horizontal = 3
text = "Energy:"
[node name="LightEnergy" parent="ObjectOptions/LightOptions/GridContainer" index="3" instance=ExtResource( 4 )]
unique_name_in_owner = true
margin_left = 635.0
margin_top = -478.0
margin_right = 1266.0
margin_bottom = -454.0
max_value = 16.0
step = 0.01
value = 1.0
allow_greater = true
[node name="LightNegativeLabel" type="Label" parent="ObjectOptions/LightOptions/GridContainer" index="4"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Negative:"
[node name="LightNegative" type="CheckBox" parent="ObjectOptions/LightOptions/GridContainer" index="5"]
unique_name_in_owner = true
margin_right = 47.0
margin_bottom = 24.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
text = "On"
[node name="ShadowEnabledLabel" type="Label" parent="ObjectOptions/LightOptions/GridContainer" index="6"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Shadow:"
[node name="ShadowEnabled" type="CheckBox" parent="ObjectOptions/LightOptions/GridContainer" index="7"]
unique_name_in_owner = true
margin_right = 47.0
margin_bottom = 24.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
text = "On"
[node name="ShadowColorLabel" type="Label" parent="ObjectOptions/LightOptions/GridContainer" index="8"]
margin_top = -499.0
margin_right = 631.0
margin_bottom = -485.0
size_flags_horizontal = 3
text = "Shadow color:"
[node name="ShadowColor" type="ColorPickerButton" parent="ObjectOptions/LightOptions/GridContainer" index="9"]
unique_name_in_owner = true
margin_left = 635.0
margin_top = -502.0
margin_right = 1266.0
margin_bottom = -482.0
size_flags_horizontal = 3
edit_alpha = false
[node name="OmniRangeLabel" type="Label" parent="ObjectOptions/LightOptions/GridContainer" index="10"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Range:"
[node name="OmniRange" type="TextureProgress" parent="ObjectOptions/LightOptions/GridContainer" index="11"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
max_value = 4096.0
step = 0.01
value = 5.0
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="SpotRangeLabel" type="Label" parent="ObjectOptions/LightOptions/GridContainer" index="12"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Range:"
[node name="SpotRange" type="TextureProgress" parent="ObjectOptions/LightOptions/GridContainer" index="13"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
max_value = 4096.0
step = 0.01
value = 5.0
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="SpotAngleLabel" type="Label" parent="ObjectOptions/LightOptions/GridContainer" index="14"]
margin_top = 33.0
margin_right = 631.0
margin_bottom = 47.0
size_flags_horizontal = 3
text = "Angle:"
[node name="SpotAngle" type="TextureProgress" parent="ObjectOptions/LightOptions/GridContainer" index="15"]
unique_name_in_owner = true
margin_right = 6.0
margin_bottom = 6.0
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
max_value = 180.0
step = 0.01
value = 45.0
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 5 )
[node name="UndoRedoTimer" type="Timer" parent="." index="5"]
wait_time = 0.2
one_shot = true
[node name="LoadModelDialog" type="FileDialog" parent="." index="6"]
margin_top = 590.0
margin_right = 515.0
margin_bottom = 938.0
rect_min_size = Vector2( 515, 348 )
window_title = "Open File(s)"
resizable = true
mode = 1
access = 2
filters = PoolStringArray( "*.obj" )
show_hidden_files = true
[connection signal="item_selected" from="HandleObjects/ObjectOptionButton" to="." method="_on_ObjectOptionButton_item_selected"]
[connection signal="pressed" from="HandleObjects/RemoveObject" to="." method="_on_RemoveObject_pressed"]
[connection signal="timeout" from="UndoRedoTimer" to="." method="_on_UndoRedoTimer_timeout"]
[connection signal="files_selected" from="LoadModelDialog" to="." method="_on_LoadModelDialog_files_selected"]
[connection signal="popup_hide" from="LoadModelDialog" to="." method="_on_LoadModelDialog_popup_hide"]

View file

@ -260,8 +260,10 @@ func _get_selected_draw_images() -> Array: # Array of Images
var project: Project = Global.current_project
for cel_index in project.selected_cels:
var cel: BaseCel = project.frames[cel_index[0]].cels[cel_index[1]]
if not cel is PixelCel:
continue
if project.layers[cel_index[1]].can_layer_get_drawn():
images.append(cel.image)
images.append(cel.get_image())
return images

View file

@ -18,6 +18,7 @@ onready var crop_rect: CropRect = $CropRect
onready var indicators = $Indicators
onready var previews = $Previews
onready var mouse_guide_container = $MouseGuideContainer
onready var gizmos_3d: Node2D = $Gizmos3D
func _ready() -> void:

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=17 format=2]
[gd_scene load_steps=18 format=2]
[ext_resource path="res://src/UI/Canvas/Canvas.gd" type="Script" id=1]
[ext_resource path="res://src/UI/Canvas/Grid.gd" type="Script" id=2]
@ -13,6 +13,7 @@
[ext_resource path="res://src/UI/Canvas/MouseGuideContainer.tscn" type="PackedScene" id=11]
[ext_resource path="res://src/UI/Canvas/OnionSkinning.gd" type="Script" id=12]
[ext_resource path="res://src/UI/Canvas/CropRect.gd" type="Script" id=13]
[ext_resource path="res://src/UI/Canvas/Gizmos3D.gd" type="Script" id=14]
[sub_resource type="CanvasItemMaterial" id=1]
blend_mode = 4
@ -80,3 +81,6 @@ script = ExtResource( 12 )
script = ExtResource( 12 )
[node name="MouseGuideContainer" parent="." instance=ExtResource( 11 )]
[node name="Gizmos3D" type="Node2D" parent="."]
script = ExtResource( 14 )

240
src/UI/Canvas/Gizmos3D.gd Normal file
View file

@ -0,0 +1,240 @@
extends Node2D
enum { X, Y, Z }
const ARROW_LENGTH := 14
const LIGHT_ARROW_LENGTH := 25
const GIZMO_WIDTH := 1.1
const SCALE_CIRCLE_LENGTH := 8
const SCALE_CIRCLE_RADIUS := 1
const CHAR_SCALE := 0.16
var always_visible := {} # Key = Cel3DObject, Value = Texture
var points_per_object := {} # Key = Cel3DObject, Value = PoolVector2Array
var selected_color := Color.white
var hovered_color := Color.gray
var gizmos_origin: Vector2
var proj_right_local: Vector2
var proj_up_local: Vector2
var proj_back_local: Vector2
# Same vectors as `proj_x_local`, but with a smaller length, for the rotation & scale gizmos
var proj_right_local_scale: Vector2
var proj_up_local_scale: Vector2
var proj_back_local_scale: Vector2
var gizmo_pos_x := PoolVector2Array()
var gizmo_pos_y := PoolVector2Array()
var gizmo_pos_z := PoolVector2Array()
var gizmo_rot_x := PoolVector2Array()
var gizmo_rot_y := PoolVector2Array()
var gizmo_rot_z := PoolVector2Array()
func _ready() -> void:
set_process_input(false)
Global.connect("cel_changed", self, "_cel_changed")
Global.camera.connect("zoom_changed", self, "update")
func get_hovering_gizmo(pos: Vector2) -> int:
var draw_scale := Global.camera.zoom * 10
pos -= gizmos_origin
# Scale the position based on the zoom, has the same effect as enlarging the shapes
pos /= draw_scale
# Inflate the rotation polylines by one to make them easier to click
var rot_x_offset: PoolVector2Array = Geometry.offset_polyline_2d(gizmo_rot_x, 1)[0]
var rot_y_offset: PoolVector2Array = Geometry.offset_polyline_2d(gizmo_rot_y, 1)[0]
var rot_z_offset: PoolVector2Array = Geometry.offset_polyline_2d(gizmo_rot_z, 1)[0]
if Geometry.point_is_inside_triangle(pos, gizmo_pos_x[0], gizmo_pos_x[1], gizmo_pos_x[2]):
return Cel3DObject.Gizmos.X_POS
elif Geometry.point_is_inside_triangle(pos, gizmo_pos_y[0], gizmo_pos_y[1], gizmo_pos_y[2]):
return Cel3DObject.Gizmos.Y_POS
elif Geometry.point_is_inside_triangle(pos, gizmo_pos_z[0], gizmo_pos_z[1], gizmo_pos_z[2]):
return Cel3DObject.Gizmos.Z_POS
elif Geometry.is_point_in_circle(pos, proj_right_local_scale, SCALE_CIRCLE_RADIUS):
return Cel3DObject.Gizmos.X_SCALE
elif Geometry.is_point_in_circle(pos, proj_up_local_scale, SCALE_CIRCLE_RADIUS):
return Cel3DObject.Gizmos.Y_SCALE
elif Geometry.is_point_in_circle(pos, proj_back_local_scale, SCALE_CIRCLE_RADIUS):
return Cel3DObject.Gizmos.Z_SCALE
elif Geometry.is_point_in_polygon(pos, rot_x_offset):
return Cel3DObject.Gizmos.X_ROT
elif Geometry.is_point_in_polygon(pos, rot_y_offset):
return Cel3DObject.Gizmos.Y_ROT
elif Geometry.is_point_in_polygon(pos, rot_z_offset):
return Cel3DObject.Gizmos.Z_ROT
return Cel3DObject.Gizmos.NONE
func _cel_changed() -> void:
update()
set_process_input(Global.current_project.get_current_cel() is Cel3D)
func _find_selected_object() -> Cel3DObject:
for object in points_per_object:
if is_instance_valid(object) and object.selected:
return object
return null
func add_always_visible(object3d: Cel3DObject, texture: Texture) -> void:
always_visible[object3d] = texture
update()
func remove_always_visible(object3d: Cel3DObject) -> void:
always_visible.erase(object3d)
update()
func get_points(camera: Camera, object3d: Cel3DObject) -> void:
var debug_mesh := object3d.box_shape.get_debug_mesh()
var arrays := debug_mesh.surface_get_arrays(0)
var points := PoolVector2Array()
for vertex in arrays[ArrayMesh.ARRAY_VERTEX]:
var x_vertex: Vector3 = object3d.transform.xform(vertex)
var point := camera.unproject_position(x_vertex)
points.append(point)
points_per_object[object3d] = points
if object3d.selected:
gizmos_origin = camera.unproject_position(object3d.translation)
var right: Vector3 = object3d.translation + object3d.transform.basis.x
var up: Vector3 = object3d.translation + object3d.transform.basis.y
var back: Vector3 = object3d.translation + object3d.transform.basis.z
var proj_right: Vector2 = object3d.camera.unproject_position(right)
var proj_up: Vector2 = object3d.camera.unproject_position(up)
var proj_back: Vector2 = object3d.camera.unproject_position(back)
proj_right_local = proj_right - gizmos_origin
proj_up_local = proj_up - gizmos_origin
proj_back_local = proj_back - gizmos_origin
proj_right_local = _resize_vector(proj_right_local, ARROW_LENGTH)
proj_up_local = _resize_vector(proj_up_local, ARROW_LENGTH)
proj_back_local = _resize_vector(proj_back_local, ARROW_LENGTH)
proj_right_local_scale = _resize_vector(proj_right_local, SCALE_CIRCLE_LENGTH)
proj_up_local_scale = _resize_vector(proj_up_local, SCALE_CIRCLE_LENGTH)
proj_back_local_scale = _resize_vector(proj_back_local, SCALE_CIRCLE_LENGTH)
# Calculate position gizmos (arrows)
gizmo_pos_x = _find_arrow(proj_right_local)
gizmo_pos_y = _find_arrow(proj_up_local)
gizmo_pos_z = _find_arrow(proj_back_local)
# Calculate rotation gizmos
gizmo_rot_x = _find_curve(proj_up_local, proj_back_local)
gizmo_rot_y = _find_curve(proj_right_local, proj_back_local)
gizmo_rot_z = _find_curve(proj_right_local, proj_up_local)
update()
func clear_points(object3d: Cel3DObject) -> void:
points_per_object.erase(object3d)
update()
func _draw() -> void:
var draw_scale := Global.camera.zoom * 10
for object in always_visible:
if not always_visible[object]:
continue
if not object.find_cel():
continue
var texture: Texture = always_visible[object]
var center := Vector2(8, 8)
var pos: Vector2 = object.camera.unproject_position(object.translation)
var back: Vector3 = object.translation - object.transform.basis.z
var back_proj: Vector2 = object.camera.unproject_position(back) - pos
back_proj = _resize_vector(back_proj, LIGHT_ARROW_LENGTH)
draw_set_transform(pos, 0, draw_scale / 4)
draw_texture(texture, -center)
draw_set_transform(pos, 0, draw_scale / 2)
if object.type == Cel3DObject.Type.DIR_LIGHT:
draw_line(Vector2.ZERO, back_proj, Color.white)
var arrow := _find_arrow(back_proj)
_draw_arrow(arrow, Color.white)
draw_set_transform_matrix(Transform2D())
if points_per_object.empty():
return
for object in points_per_object:
if not object.find_cel():
if object.selected:
object.unselect()
continue
var points: PoolVector2Array = points_per_object[object]
if points.empty():
continue
if object.selected:
# Draw bounding box outline
draw_multiline(points, selected_color, 1.0, true)
if object.applying_gizmos == Cel3DObject.Gizmos.X_ROT:
draw_line(gizmos_origin, Global.canvas.current_pixel, Color.red)
continue
elif object.applying_gizmos == Cel3DObject.Gizmos.Y_ROT:
draw_line(gizmos_origin, Global.canvas.current_pixel, Color.green)
continue
elif object.applying_gizmos == Cel3DObject.Gizmos.Z_ROT:
draw_line(gizmos_origin, Global.canvas.current_pixel, Color.blue)
continue
draw_set_transform(gizmos_origin, 0, draw_scale)
# Draw position arrows
draw_line(Vector2.ZERO, proj_right_local, Color.red)
draw_line(Vector2.ZERO, proj_up_local, Color.green)
draw_line(Vector2.ZERO, proj_back_local, Color.blue)
_draw_arrow(gizmo_pos_x, Color.red)
_draw_arrow(gizmo_pos_y, Color.green)
_draw_arrow(gizmo_pos_z, Color.blue)
# Draw rotation curves
draw_polyline(gizmo_rot_x, Color.red, GIZMO_WIDTH)
draw_polyline(gizmo_rot_y, Color.green, GIZMO_WIDTH)
draw_polyline(gizmo_rot_z, Color.blue, GIZMO_WIDTH)
# Draw scale circles
draw_circle(proj_right_local_scale, SCALE_CIRCLE_RADIUS, Color.red)
draw_circle(proj_up_local_scale, SCALE_CIRCLE_RADIUS, Color.green)
draw_circle(proj_back_local_scale, SCALE_CIRCLE_RADIUS, Color.blue)
# Draw X, Y, Z characters on top of the scale circles
var font: Font = Global.control.theme.default_font
var font_height := font.get_height()
var char_position := Vector2(-font_height, font_height) * CHAR_SCALE / 4 * draw_scale
draw_set_transform(gizmos_origin + char_position, 0, draw_scale * CHAR_SCALE)
draw_char(font, proj_right_local_scale / CHAR_SCALE, "X", "")
draw_char(font, proj_up_local_scale / CHAR_SCALE, "Y", "")
draw_char(font, proj_back_local_scale / CHAR_SCALE, "Z", "")
draw_set_transform_matrix(Transform2D())
elif object.hovered:
draw_multiline(points, hovered_color, 1.0, true)
func _resize_vector(v: Vector2, l: float) -> Vector2:
return (v.normalized() * l).limit_length(v.length())
func _find_curve(a: Vector2, b: Vector2) -> PoolVector2Array:
var curve2d := Curve2D.new()
curve2d.bake_interval = 1
var control := b.linear_interpolate(a, 0.5)
a = _resize_vector(a, SCALE_CIRCLE_LENGTH)
b = _resize_vector(b, SCALE_CIRCLE_LENGTH)
control = control.normalized() * sqrt(pow(a.length() / 4, 2) * 2) # Thank you Pythagoras
curve2d.add_point(a, Vector2.ZERO, control)
curve2d.add_point(b, control)
return curve2d.get_baked_points()
func _find_arrow(a: Vector2, tilt := 0.5) -> PoolVector2Array:
var b := a + Vector2(-tilt, 1).rotated(a.angle() + PI / 2) * 2
var c := a + Vector2(tilt, 1).rotated(a.angle() + PI / 2) * 2
return PoolVector2Array([a, b, c])
func _draw_arrow(triangle: PoolVector2Array, color: Color) -> void:
draw_primitive(triangle, [color, color, color], [])

View file

@ -654,7 +654,7 @@ func _get_selected_draw_images() -> Array: # Array of Image(s)
if not cel is PixelCel:
continue
if project.layers[cel_index[1]].can_layer_get_drawn():
images.append(cel.image)
images.append(cel.get_image())
return images

View file

@ -202,6 +202,8 @@ func create_layer_list() -> void:
var layer_name := tr("Pixel layer:")
if layer is GroupLayer:
layer_name = tr("Group layer:")
elif layer is Layer3D:
layer_name = tr("3D layer:")
layer_name += " %s" % layer.get_layer_path()
layers_option_button.add_item(layer_name)

View file

@ -0,0 +1,193 @@
tool
class_name ValueSliderV3
extends HBoxContainer
signal value_changed(value)
signal ratio_toggled(button_pressed)
export var editable := true setget _set_editable
export var value := Vector3.ZERO setget _set_value
export var min_value := Vector3.ZERO setget _set_min_value
export var max_value := Vector3(100.0, 100.0, 100.0) setget _set_max_value
export var step := 1.0 setget _set_step
export var allow_greater := false setget _set_allow_greater
export var allow_lesser := false setget _set_allow_lesser
export var show_ratio := false setget _set_show_ratio
export(int, 1, 2) var grid_columns := 1 setget _set_grid_columns
export var slider_min_size := Vector2(32, 24) setget _set_slider_min_size
export var snap_step := 1.0 setget _set_snap_step
export var snap_by_default := false setget _set_snap_by_default
export var prefix_x := "X:" setget _set_prefix_x
export var prefix_y := "Y:" setget _set_prefix_y
export var prefix_z := "Z:" setget _set_prefix_z
export var suffix_x := "" setget _set_suffix_x
export var suffix_y := "" setget _set_suffix_y
export var suffix_z := "" setget _set_suffix_z
var ratio := Vector3.ONE
var _locked_ratio := false
var _can_emit_signal := true
func _ready() -> void:
if not Engine.editor_hint: # Pixelorama specific code
$Ratio.modulate = Global.modulate_icon_color
func get_sliders() -> Array:
return [$GridContainer/X, $GridContainer/Y, $GridContainer/Z]
func press_ratio_button(pressed: bool) -> void:
$"%RatioButton".pressed = pressed
# Greatest common divisor
func _gcd(a: int, b: int) -> int:
return a if b == 0 else _gcd(b, a % b)
func _on_X_value_changed(val: float) -> void:
value.x = val
if _locked_ratio:
self.value.y = max(min_value.y, (value.x / ratio.x) * ratio.y)
self.value.z = max(min_value.z, (value.x / ratio.x) * ratio.z)
if _can_emit_signal:
emit_signal("value_changed", value)
func _on_Y_value_changed(val: float) -> void:
value.y = val
if _locked_ratio:
self.value.x = max(min_value.x, (value.y / ratio.y) * ratio.x)
self.value.z = max(min_value.z, (value.y / ratio.y) * ratio.z)
if _can_emit_signal:
emit_signal("value_changed", value)
func _on_Z_value_changed(val: float) -> void:
value.z = val
if _locked_ratio:
self.value.x = max(min_value.x, (value.z / ratio.z) * ratio.x)
self.value.y = max(min_value.y, (value.z / ratio.z) * ratio.y)
if _can_emit_signal:
emit_signal("value_changed", value)
func _on_RatioButton_toggled(button_pressed: bool) -> void:
_locked_ratio = button_pressed
var divisor := _gcd(value.x, _gcd(value.y, value.z))
if divisor == 0:
ratio = Vector3.ONE
else:
ratio = value / divisor
emit_signal("ratio_toggled", button_pressed)
# Setters
func _set_editable(val: bool) -> void:
editable = val
for slider in get_sliders():
slider.editable = val
$"%RatioButton".disabled = not val
func _set_value(val: Vector3) -> void:
value = val
_can_emit_signal = false
$GridContainer/X.value = value.x
$GridContainer/Y.value = value.y
$GridContainer/Z.value = value.z
_can_emit_signal = true
func _set_min_value(val: Vector3) -> void:
min_value = val
$GridContainer/X.min_value = val.x
$GridContainer/Y.min_value = val.y
$GridContainer/Z.min_value = val.z
func _set_max_value(val: Vector3) -> void:
max_value = val
$GridContainer/X.max_value = val.x
$GridContainer/Y.max_value = val.y
$GridContainer/Z.max_value = val.z
func _set_step(val: float) -> void:
step = val
for slider in get_sliders():
slider.step = val
func _set_allow_greater(val: bool) -> void:
allow_greater = val
for slider in get_sliders():
slider.allow_greater = val
func _set_allow_lesser(val: bool) -> void:
allow_lesser = val
for slider in get_sliders():
slider.allow_lesser = val
func _set_show_ratio(val: bool) -> void:
show_ratio = val
$Ratio.visible = val
func _set_grid_columns(val: int) -> void:
grid_columns = val
$GridContainer.columns = val
func _set_slider_min_size(val: Vector2) -> void:
slider_min_size = val
for slider in get_sliders():
slider.rect_min_size = val
func _set_snap_step(val: float) -> void:
snap_step = val
for slider in get_sliders():
slider.snap_step = val
func _set_snap_by_default(val: bool) -> void:
snap_by_default = val
for slider in get_sliders():
slider.snap_by_default = val
func _set_prefix_x(val: String) -> void:
prefix_x = val
$GridContainer/X.prefix = val
func _set_prefix_y(val: String) -> void:
prefix_y = val
$GridContainer/Y.prefix = val
func _set_prefix_z(val: String) -> void:
prefix_z = val
$GridContainer/Z.prefix = val
func _set_suffix_x(val: String) -> void:
suffix_x = val
$GridContainer/X.suffix = val
func _set_suffix_y(val: String) -> void:
suffix_y = val
$GridContainer/Y.suffix = val
func _set_suffix_z(val: String) -> void:
suffix_z = val
$GridContainer/Z.suffix = val

View file

@ -0,0 +1,101 @@
[gd_scene load_steps=6 format=2]
[ext_resource path="res://src/UI/Nodes/ValueSlider.gd" type="Script" id=1]
[ext_resource path="res://src/UI/Nodes/ValueSliderV3.gd" type="Script" id=2]
[ext_resource path="res://assets/graphics/misc/lock_aspect_2.png" type="Texture" id=3]
[ext_resource path="res://assets/graphics/misc/lock_aspect_guides.png" type="Texture" id=4]
[ext_resource path="res://assets/graphics/misc/lock_aspect.png" type="Texture" id=5]
[node name="ValueSliderV3" type="HBoxContainer"]
margin_right = 45.0
margin_bottom = 52.0
script = ExtResource( 2 )
[node name="GridContainer" type="GridContainer" parent="."]
margin_right = 45.0
margin_bottom = 80.0
size_flags_horizontal = 3
[node name="X" type="TextureProgress" parent="GridContainer"]
margin_right = 45.0
margin_bottom = 24.0
rect_min_size = Vector2( 32, 24 )
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 1 )
prefix = "X:"
[node name="Y" type="TextureProgress" parent="GridContainer"]
margin_top = 28.0
margin_right = 45.0
margin_bottom = 52.0
rect_min_size = Vector2( 32, 24 )
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 1 )
prefix = "Y:"
[node name="Z" type="TextureProgress" parent="GridContainer"]
margin_top = 56.0
margin_right = 45.0
margin_bottom = 80.0
rect_min_size = Vector2( 32, 24 )
mouse_default_cursor_shape = 2
size_flags_horizontal = 3
theme_type_variation = "ValueSlider"
nine_patch_stretch = true
stretch_margin_left = 3
stretch_margin_top = 3
stretch_margin_right = 3
stretch_margin_bottom = 3
script = ExtResource( 1 )
prefix = "Z:"
[node name="Ratio" type="Control" parent="."]
visible = false
margin_left = 36.0
margin_right = 52.0
margin_bottom = 80.0
rect_min_size = Vector2( 16, 0 )
[node name="RatioGuides" type="NinePatchRect" parent="Ratio" groups=["UIButtons"]]
anchor_bottom = 1.0
margin_right = 9.0
rect_min_size = Vector2( 9, 0 )
texture = ExtResource( 4 )
region_rect = Rect2( 0, 0, 9, 44 )
patch_margin_top = 15
patch_margin_bottom = 13
[node name="RatioButton" type="TextureButton" parent="Ratio" groups=["UIButtons"]]
unique_name_in_owner = true
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -8.0
margin_top = -8.0
margin_right = 8.0
margin_bottom = 8.0
hint_tooltip = "Lock aspect ratio"
mouse_default_cursor_shape = 2
toggle_mode = true
texture_normal = ExtResource( 3 )
texture_pressed = ExtResource( 5 )
[connection signal="value_changed" from="GridContainer/X" to="." method="_on_X_value_changed"]
[connection signal="value_changed" from="GridContainer/Y" to="." method="_on_Y_value_changed"]
[connection signal="value_changed" from="GridContainer/Z" to="." method="_on_Z_value_changed"]
[connection signal="toggled" from="Ratio/RatioButton" to="." method="_on_RatioButton_toggled"]

View file

@ -17,6 +17,7 @@ var frame_button_node = preload("res://src/UI/Timeline/FrameButton.tscn")
onready var old_scroll: int = 0 # The previous scroll state of $ScrollContainer
onready var tag_spacer = find_node("TagSpacer")
onready var start_spacer = find_node("StartSpacer")
onready var add_layer_list: MenuButton = $"%AddLayerList"
onready var timeline_scroll: ScrollContainer = find_node("TimelineScroll")
onready var frame_scroll_container: Control = find_node("FrameScrollContainer")
@ -30,6 +31,7 @@ onready var drag_highlight: ColorRect = find_node("DragHighlight")
func _ready() -> void:
add_layer_list.get_popup().connect("id_pressed", self, "add_layer")
frame_scroll_bar.connect("value_changed", self, "_frame_scroll_changed")
Global.animation_timer.wait_time = 1 / Global.current_project.fps
fps_spinbox.value = Global.current_project.fps
@ -273,7 +275,13 @@ func copy_frames(indices := []) -> void:
new_frame.duration = src_frame.duration
for l in range(project.layers.size()):
var src_cel: BaseCel = project.frames[f].cels[l] # Cel we're copying from, the source
var new_cel: BaseCel = src_cel.get_script().new()
var new_cel: BaseCel
if src_cel is Cel3D:
new_cel = src_cel.get_script().new(
src_cel.size, false, src_cel.object_properties, src_cel.scene_properties
)
else:
new_cel = src_cel.get_script().new()
if project.layers[l].new_cels_linked:
if src_cel.link_set == null:
src_cel.link_set = {}
@ -561,6 +569,8 @@ func add_layer(type: int) -> void:
l = PixelLayer.new(project)
Global.LayerTypes.GROUP:
l = GroupLayer.new(project)
Global.LayerTypes.THREE_D:
l = Layer3D.new(project)
var cels := []
for f in project.frames:
@ -614,7 +624,13 @@ func _on_CloneLayer_pressed() -> void:
for frame in project.frames:
var src_cel: BaseCel = frame.cels[src_layer.index]
var new_cel: BaseCel = src_cel.get_script().new()
var new_cel: BaseCel
if src_cel is Cel3D:
new_cel = src_cel.get_script().new(
src_cel.size, false, src_cel.object_properties, src_cel.scene_properties
)
else:
new_cel = src_cel.get_script().new()
if src_cel.link_set == null:
new_cel.set_content(src_cel.copy_content())
@ -741,7 +757,7 @@ func change_layer_order(up: bool) -> void:
func _on_MergeDownLayer_pressed() -> void:
var project: Project = Global.current_project
var top_layer: PixelLayer = project.layers[project.current_layer]
var top_layer: BaseLayer = project.layers[project.current_layer]
var bottom_layer: PixelLayer = project.layers[project.current_layer - 1]
var top_cels := []
@ -752,7 +768,7 @@ func _on_MergeDownLayer_pressed() -> void:
top_cels.append(frame.cels[top_layer.index]) # Store for undo purposes
var top_image := Image.new()
top_image.copy_from(frame.cels[top_layer.index].image)
top_image.copy_from(frame.cels[top_layer.index].get_image())
top_image.lock()
if frame.cels[top_layer.index].opacity < 1: # If we have layer transparency

View file

@ -9,7 +9,7 @@
[ext_resource path="res://assets/graphics/layers/clone.png" type="Texture" id=7]
[ext_resource path="res://assets/graphics/timeline/move_arrow.png" type="Texture" id=8]
[ext_resource path="res://src/UI/Nodes/ValueSlider.tscn" type="PackedScene" id=9]
[ext_resource path="res://assets/graphics/layers/group_new.png" type="Texture" id=10]
[ext_resource path="res://assets/graphics/misc/value_arrow.svg" type="Texture" id=10]
[ext_resource path="res://src/UI/Timeline/FrameScrollContainer.gd" type="Script" id=11]
[ext_resource path="res://assets/graphics/timeline/new_frame.png" type="Texture" id=19]
[ext_resource path="res://assets/graphics/timeline/remove_frame.png" type="Texture" id=20]
@ -99,67 +99,66 @@ margin_bottom = 74.0
size_flags_horizontal = 3
[node name="LayerTools" type="PanelContainer" parent="TimelineContainer/TimelineButtons"]
margin_right = 222.0
margin_right = 221.0
margin_bottom = 74.0
[node name="VBoxContainer" type="VBoxContainer" parent="TimelineContainer/TimelineButtons/LayerTools"]
margin_left = 7.0
margin_top = 7.0
margin_right = 215.0
margin_right = 214.0
margin_bottom = 67.0
[node name="LayerButtons" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer"]
margin_right = 208.0
margin_right = 207.0
margin_bottom = 22.0
size_flags_vertical = 0
custom_constants/separation = 9
[node name="AddLayer" type="Button" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons" groups=["UIButtons"]]
margin_right = 22.0
margin_right = 44.0
margin_bottom = 22.0
rect_min_size = Vector2( 22, 22 )
rect_min_size = Vector2( 44, 22 )
hint_tooltip = "Create a new layer"
focus_mode = 0
mouse_default_cursor_shape = 2
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/AddLayer"]
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -11.0
margin_top = -11.0
margin_right = 11.0
margin_right = 22.0
margin_bottom = 11.0
size_flags_horizontal = 0
size_flags_vertical = 0
texture = ExtResource( 2 )
[node name="AddGroup" type="Button" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons" groups=["UIButtons"]]
margin_left = 31.0
margin_right = 53.0
margin_bottom = 22.0
rect_min_size = Vector2( 22, 22 )
hint_tooltip = "Create a new group layer"
focus_mode = 0
[node name="AddLayerList" type="MenuButton" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/AddLayer"]
unique_name_in_owner = true
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
margin_left = -22.0
margin_top = -10.0
margin_bottom = 10.0
rect_min_size = Vector2( 22, 0 )
mouse_default_cursor_shape = 2
items = [ "Add Pixel Layer", null, 0, false, false, 0, 0, null, "", false, "Add Group Layer", null, 0, false, false, 1, 0, null, "", false, "Add 3D Layer", null, 0, false, false, 2, 0, null, "", false ]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/AddGroup"]
[node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/AddLayer/AddLayerList"]
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -11.0
margin_top = -11.0
margin_right = 11.0
margin_bottom = 11.0
size_flags_horizontal = 0
size_flags_vertical = 0
margin_left = -6.0
margin_top = -6.0
margin_right = 6.0
margin_bottom = 6.0
texture = ExtResource( 10 )
[node name="RemoveLayer" type="Button" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons" groups=["UIButtons"]]
margin_left = 62.0
margin_right = 84.0
margin_left = 53.0
margin_right = 75.0
margin_bottom = 22.0
rect_min_size = Vector2( 22, 22 )
hint_tooltip = "Remove current layer"
@ -184,8 +183,8 @@ __meta__ = {
}
[node name="MoveUpLayer" type="Button" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons" groups=["UIButtons"]]
margin_left = 93.0
margin_right = 115.0
margin_left = 84.0
margin_right = 106.0
margin_bottom = 22.0
rect_min_size = Vector2( 22, 22 )
hint_tooltip = "Move up the current layer"
@ -210,8 +209,8 @@ __meta__ = {
}
[node name="MoveDownLayer" type="Button" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons" groups=["UIButtons"]]
margin_left = 124.0
margin_right = 146.0
margin_left = 115.0
margin_right = 137.0
margin_bottom = 22.0
rect_min_size = Vector2( 22, 22 )
hint_tooltip = "Move down the current layer"
@ -236,8 +235,8 @@ __meta__ = {
}
[node name="CloneLayer" type="Button" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons" groups=["UIButtons"]]
margin_left = 155.0
margin_right = 177.0
margin_left = 146.0
margin_right = 168.0
margin_bottom = 22.0
rect_min_size = Vector2( 22, 22 )
hint_tooltip = "Clone current layer"
@ -261,8 +260,8 @@ __meta__ = {
}
[node name="MergeDownLayer" type="Button" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons" groups=["UIButtons"]]
margin_left = 186.0
margin_right = 208.0
margin_left = 177.0
margin_right = 199.0
margin_bottom = 22.0
rect_min_size = Vector2( 22, 22 )
hint_tooltip = "Merge current layer with the one below"
@ -288,7 +287,7 @@ __meta__ = {
[node name="BlendingHBox" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer"]
margin_top = 36.0
margin_right = 208.0
margin_right = 207.0
margin_bottom = 60.0
size_flags_vertical = 10
@ -301,21 +300,21 @@ value = 100.0
prefix = "Opacity:"
[node name="VBoxContainer" type="VBoxContainer" parent="TimelineContainer/TimelineButtons"]
margin_left = 226.0
margin_left = 225.0
margin_right = 902.0
margin_bottom = 74.0
size_flags_horizontal = 3
[node name="AnimationToolsScrollContainer" type="ScrollContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer"]
margin_right = 676.0
margin_right = 677.0
margin_bottom = 38.0
size_flags_horizontal = 3
theme = SubResource( 20 )
scroll_vertical_enabled = false
[node name="AnimationTools" type="PanelContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/AnimationToolsScrollContainer"]
margin_left = 156.0
margin_right = 676.0
margin_left = 157.0
margin_right = 677.0
margin_bottom = 38.0
size_flags_horizontal = 10
@ -760,7 +759,7 @@ suffix = "FPS"
[node name="TagScroll" type="ScrollContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer"]
margin_top = 42.0
margin_right = 676.0
margin_right = 677.0
margin_bottom = 74.0
rect_min_size = Vector2( 0, 32 )
mouse_filter = 2
@ -769,7 +768,7 @@ theme = SubResource( 20 )
scroll_vertical_enabled = false
[node name="HBoxContainer" type="HBoxContainer" parent="TimelineContainer/TimelineButtons/VBoxContainer/TagScroll"]
margin_right = 676.0
margin_right = 677.0
margin_bottom = 32.0
size_flags_horizontal = 3
size_flags_vertical = 3
@ -779,7 +778,7 @@ custom_constants/separation = 0
margin_bottom = 32.0
[node name="TagContainer" type="Control" parent="TimelineContainer/TimelineButtons/VBoxContainer/TagScroll/HBoxContainer"]
margin_right = 676.0
margin_right = 677.0
margin_bottom = 32.0
size_flags_horizontal = 3
@ -980,7 +979,6 @@ mouse_filter = 2
color = Color( 0, 0.741176, 1, 0.501961 )
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/AddLayer" to="." method="add_layer" binds= [ 0 ]]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/AddGroup" to="." method="add_layer" binds= [ 1 ]]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/RemoveLayer" to="." method="_on_RemoveLayer_pressed"]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/MoveUpLayer" to="." method="change_layer_order" binds= [ true ]]
[connection signal="pressed" from="TimelineContainer/TimelineButtons/LayerTools/VBoxContainer/LayerButtons/MoveDownLayer" to="." method="change_layer_order" binds= [ false ]]

View file

@ -11,6 +11,8 @@ func _input(event: InputEvent) -> void:
return
for tool_name in Tools.tools: # Handle tool shortcuts
if not get_node(tool_name).visible:
continue
var t: Tools.Tool = Tools.tools[tool_name]
if InputMap.has_action("right_" + t.shortcut + "_tool"):
if (