diff --git a/modules/paint/nodes/curve_2d/editor/path_2d_editor_plugin.cpp b/modules/paint/nodes/curve_2d/editor/path_2d_editor_plugin.cpp new file mode 100644 index 000000000..709d4db9a --- /dev/null +++ b/modules/paint/nodes/curve_2d/editor/path_2d_editor_plugin.cpp @@ -0,0 +1,637 @@ +/*************************************************************************/ +/* path_2d_editor_plugin.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "path_2d_editor_plugin.h" + +#include "canvas_item_editor_plugin.h" +#include "core/os/keyboard.h" +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" +#include "core/object/class_db.h" +#include "core/math/color.h" +#include "core/math/math_defs.h" +#include "core/math/math_funcs.h" +#include "core/math/rect2.h" +#include "core/math/transform_2d.h" +#include "core/input/input_event.h" +#include "core/os/memory.h" +#include "core/object/undo_redo.h" +#include "core/variant/variant.h" +#include "editor/editor_node.h" +#include "scene/2d/path_2d.h" +#include "scene/gui/control.h" +#include "scene/gui/menu_button.h" +#include "scene/gui/popup_menu.h" +#include "scene/gui/separator.h" +#include "scene/gui/tool_button.h" +#include "scene/main/node.h" +#include "scene/resources/curve.h" +#include "scene/resources/texture.h" + +void Path2DEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_READY: { + //button_create->set_icon( get_theme_icon("Edit","EditorIcons")); + //button_edit->set_icon( get_theme_icon("MovePoint","EditorIcons")); + //set_pressed_button(button_edit); + //button_edit->set_pressed(true); + + } break; + case NOTIFICATION_PHYSICS_PROCESS: { + } break; + } +} +void Path2DEditor::_node_removed(Node *p_node) { + if (p_node == node) { + node = nullptr; + hide(); + } +} + +bool Path2DEditor::forward_gui_input(const Ref &p_event) { + if (!node) { + return false; + } + + if (!node->is_visible_in_tree()) { + return false; + } + + if (!node->get_curve().is_valid()) { + return false; + } + + real_t grab_threshold = EDITOR_GET("editors/poly_editor/point_grab_radius"); + + Ref mb = p_event; + if (mb.is_valid()) { + Transform2D xform = canvas_item_editor->get_canvas_transform() * node->get_global_transform(); + + Vector2 gpoint = mb->get_position(); + Vector2 cpoint = node->get_global_transform().affine_inverse().xform(canvas_item_editor->snap_point(canvas_item_editor->get_canvas_transform().affine_inverse().xform(mb->get_position()))); + + if (mb->is_pressed() && action == ACTION_NONE) { + Ref curve = node->get_curve(); + + for (int i = 0; i < curve->get_point_count(); i++) { + real_t dist_to_p = gpoint.distance_to(xform.xform(curve->get_point_position(i))); + real_t dist_to_p_out = gpoint.distance_to(xform.xform(curve->get_point_position(i) + curve->get_point_out(i))); + real_t dist_to_p_in = gpoint.distance_to(xform.xform(curve->get_point_position(i) + curve->get_point_in(i))); + + // Check for point movement start (for point + in/out controls). + if (mb->get_button_index() == BUTTON_LEFT) { + if (mode == MODE_EDIT && !mb->get_shift() && dist_to_p < grab_threshold) { + // Points can only be moved in edit mode. + + action = ACTION_MOVING_POINT; + action_point = i; + moving_from = curve->get_point_position(i); + moving_screen_from = gpoint; + return true; + } else if (mode == MODE_EDIT || mode == MODE_EDIT_CURVE) { + // In/out controls can be moved in multiple modes. + if (dist_to_p_out < grab_threshold && i < (curve->get_point_count() - 1)) { + action = ACTION_MOVING_OUT; + action_point = i; + moving_from = curve->get_point_out(i); + moving_screen_from = gpoint; + orig_in_length = curve->get_point_in(action_point).length(); + return true; + } else if (dist_to_p_in < grab_threshold && i > 0) { + action = ACTION_MOVING_IN; + action_point = i; + moving_from = curve->get_point_in(i); + moving_screen_from = gpoint; + orig_out_length = curve->get_point_out(action_point).length(); + return true; + } + } + } + + // Check for point deletion. + if ((mb->get_button_index() == BUTTON_RIGHT && mode == MODE_EDIT) || (mb->get_button_index() == BUTTON_LEFT && mode == MODE_DELETE)) { + if (dist_to_p < grab_threshold) { + undo_redo->create_action(TTR("Remove Point from Curve")); + undo_redo->add_do_method(curve.ptr(), "remove_point", i); + undo_redo->add_undo_method(curve.ptr(), "add_point", curve->get_point_position(i), curve->get_point_in(i), curve->get_point_out(i), i); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + return true; + } else if (dist_to_p_out < grab_threshold) { + undo_redo->create_action(TTR("Remove Out-Control from Curve")); + undo_redo->add_do_method(curve.ptr(), "set_point_out", i, Vector2()); + undo_redo->add_undo_method(curve.ptr(), "set_point_out", i, curve->get_point_out(i)); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + return true; + } else if (dist_to_p_in < grab_threshold) { + undo_redo->create_action(TTR("Remove In-Control from Curve")); + undo_redo->add_do_method(curve.ptr(), "set_point_in", i, Vector2()); + undo_redo->add_undo_method(curve.ptr(), "set_point_in", i, curve->get_point_in(i)); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + return true; + } + } + } + } + + // Check for point creation. + if (mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT && ((mb->get_command() && mode == MODE_EDIT) || mode == MODE_CREATE)) { + Ref curve = node->get_curve(); + + undo_redo->create_action(TTR("Add Point to Curve")); + undo_redo->add_do_method(curve.ptr(), "add_point", cpoint); + undo_redo->add_undo_method(curve.ptr(), "remove_point", curve->get_point_count()); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + + action = ACTION_MOVING_POINT; + action_point = curve->get_point_count() - 1; + moving_from = curve->get_point_position(action_point); + moving_screen_from = gpoint; + + canvas_item_editor->update_viewport(); + + return true; + } + + // Check for segment split. + if (mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT && mode == MODE_EDIT && on_edge) { + Vector2 gpoint2 = mb->get_position(); + Ref curve = node->get_curve(); + + int insertion_point = -1; + float mbLength = curve->get_closest_offset(xform.affine_inverse().xform(gpoint2)); + int len = curve->get_point_count(); + for (int i = 0; i < len - 1; i++) { + float compareLength = curve->get_closest_offset(curve->get_point_position(i + 1)); + if (mbLength >= curve->get_closest_offset(curve->get_point_position(i)) && mbLength <= compareLength) { + insertion_point = i; + } + } + if (insertion_point == -1) { + insertion_point = curve->get_point_count() - 2; + } + + undo_redo->create_action(TTR("Split Curve")); + undo_redo->add_do_method(curve.ptr(), "add_point", xform.affine_inverse().xform(gpoint2), Vector2(0, 0), Vector2(0, 0), insertion_point + 1); + undo_redo->add_undo_method(curve.ptr(), "remove_point", insertion_point + 1); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + + action = ACTION_MOVING_POINT; + action_point = insertion_point + 1; + moving_from = curve->get_point_position(action_point); + moving_screen_from = gpoint2; + + canvas_item_editor->update_viewport(); + + on_edge = false; + + return true; + } + + // Check for point movement completion. + if (!mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT && action != ACTION_NONE) { + Ref curve = node->get_curve(); + + Vector2 new_pos = moving_from + xform.affine_inverse().basis_xform(gpoint - moving_screen_from); + switch (action) { + case ACTION_NONE: + // N/A, handled in above condition. + break; + + case ACTION_MOVING_POINT: { + undo_redo->create_action(TTR("Move Point in Curve")); + undo_redo->add_do_method(curve.ptr(), "set_point_position", action_point, cpoint); + undo_redo->add_undo_method(curve.ptr(), "set_point_position", action_point, moving_from); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + + } break; + + case ACTION_MOVING_IN: { + undo_redo->create_action(TTR("Move In-Control in Curve")); + undo_redo->add_do_method(curve.ptr(), "set_point_in", action_point, new_pos); + undo_redo->add_undo_method(curve.ptr(), "set_point_in", action_point, moving_from); + + if (mirror_handle_angle) { + undo_redo->add_do_method(curve.ptr(), "set_point_out", action_point, mirror_handle_length ? -new_pos : (-new_pos.normalized() * orig_out_length)); + undo_redo->add_undo_method(curve.ptr(), "set_point_out", action_point, mirror_handle_length ? -moving_from : (-moving_from.normalized() * orig_out_length)); + } + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + + } break; + + case ACTION_MOVING_OUT: { + undo_redo->create_action(TTR("Move Out-Control in Curve")); + undo_redo->add_do_method(curve.ptr(), "set_point_out", action_point, new_pos); + undo_redo->add_undo_method(curve.ptr(), "set_point_out", action_point, moving_from); + + if (mirror_handle_angle) { + undo_redo->add_do_method(curve.ptr(), "set_point_in", action_point, mirror_handle_length ? -new_pos : (-new_pos.normalized() * orig_in_length)); + undo_redo->add_undo_method(curve.ptr(), "set_point_in", action_point, mirror_handle_length ? -moving_from : (-moving_from.normalized() * orig_in_length)); + } + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + + } break; + } + + action = ACTION_NONE; + + return true; + } + } + + Ref mm = p_event; + + if (mm.is_valid()) { + if (action == ACTION_NONE && mode == MODE_EDIT) { + // Handle Edge Follow + bool old_edge = on_edge; + + Transform2D xform = canvas_item_editor->get_canvas_transform() * node->get_global_transform(); + Vector2 gpoint = mm->get_position(); + + Ref curve = node->get_curve(); + if (curve == nullptr) { + return true; + } + if (curve->get_point_count() < 2) { + return true; + } + + // Find edge + edge_point = xform.xform(curve->get_closest_point(xform.affine_inverse().xform(mm->get_position()))); + on_edge = false; + if (edge_point.distance_to(gpoint) <= grab_threshold) { + on_edge = true; + } + // However, if near a control point or its in-out handles then not on edge + int len = curve->get_point_count(); + for (int i = 0; i < len; i++) { + Vector2 pp = curve->get_point_position(i); + Vector2 p = xform.xform(pp); + if (p.distance_to(gpoint) <= grab_threshold) { + on_edge = false; + break; + } + p = xform.xform(pp + curve->get_point_in(i)); + if (p.distance_to(gpoint) <= grab_threshold) { + on_edge = false; + break; + } + p = xform.xform(pp + curve->get_point_out(i)); + if (p.distance_to(gpoint) <= grab_threshold) { + on_edge = false; + break; + } + } + if (on_edge || old_edge != on_edge) { + canvas_item_editor->update_viewport(); + return true; + } + } + + if (action != ACTION_NONE) { + // Handle point/control movement. + Transform2D xform = canvas_item_editor->get_canvas_transform() * node->get_global_transform(); + Vector2 gpoint = mm->get_position(); + Vector2 cpoint = node->get_global_transform().affine_inverse().xform(canvas_item_editor->snap_point(canvas_item_editor->get_canvas_transform().affine_inverse().xform(mm->get_position()))); + + Ref curve = node->get_curve(); + + Vector2 new_pos = moving_from + xform.affine_inverse().basis_xform(gpoint - moving_screen_from); + + switch (action) { + case ACTION_NONE: + // N/A, handled in above condition. + break; + + case ACTION_MOVING_POINT: { + curve->set_point_position(action_point, cpoint); + } break; + + case ACTION_MOVING_IN: { + curve->set_point_in(action_point, new_pos); + + if (mirror_handle_angle) { + curve->set_point_out(action_point, mirror_handle_length ? -new_pos : (-new_pos.normalized() * orig_out_length)); + } + } break; + + case ACTION_MOVING_OUT: { + curve->set_point_out(action_point, new_pos); + + if (mirror_handle_angle) { + curve->set_point_in(action_point, mirror_handle_length ? -new_pos : (-new_pos.normalized() * orig_in_length)); + } + } break; + } + + canvas_item_editor->update_viewport(); + return true; + } + } + + return false; +} + +void Path2DEditor::forward_canvas_draw_over_viewport(Control *p_overlay) { + if (!node || !node->is_visible_in_tree() || !node->get_curve().is_valid()) { + return; + } + + Transform2D xform = canvas_item_editor->get_canvas_transform() * node->get_global_transform(); + + const Ref path_sharp_handle = get_theme_icon("EditorPathSharpHandle", "EditorIcons"); + const Ref path_smooth_handle = get_theme_icon("EditorPathSmoothHandle", "EditorIcons"); + // Both handle icons must be of the same size + const Size2 handle_size = path_sharp_handle->get_size(); + + const Ref curve_handle = get_theme_icon("EditorCurveHandle", "EditorIcons"); + const Size2 curve_handle_size = curve_handle->get_size(); + + Ref curve = node->get_curve(); + + int len = curve->get_point_count(); + Control *vpc = canvas_item_editor->get_viewport_control(); + + for (int i = 0; i < len; i++) { + Vector2 point = xform.xform(curve->get_point_position(i)); + // Determines the point icon to be used + bool smooth = false; + + if (i < len - 1) { + Vector2 pointout = xform.xform(curve->get_point_position(i) + curve->get_point_out(i)); + if (point != pointout) { + smooth = true; + // Draw the line with a dark and light color to be visible on all backgrounds + vpc->draw_line(point, pointout, Color(0, 0, 0, 0.5), Math::round(EDSCALE), true); + vpc->draw_line(point, pointout, Color(1, 1, 1, 0.5), Math::round(EDSCALE), true); + vpc->draw_texture_rect(curve_handle, Rect2(pointout - curve_handle_size * 0.5, curve_handle_size), false, Color(1, 1, 1, 0.75)); + } + } + + if (i > 0) { + Vector2 pointin = xform.xform(curve->get_point_position(i) + curve->get_point_in(i)); + if (point != pointin) { + smooth = true; + // Draw the line with a dark and light color to be visible on all backgrounds + vpc->draw_line(point, pointin, Color(0, 0, 0, 0.5), Math::round(EDSCALE), true); + vpc->draw_line(point, pointin, Color(1, 1, 1, 0.5), Math::round(EDSCALE), true); + vpc->draw_texture_rect(curve_handle, Rect2(pointin - curve_handle_size * 0.5, curve_handle_size), false, Color(1, 1, 1, 0.75)); + } + } + + vpc->draw_texture_rect( + smooth ? path_smooth_handle : path_sharp_handle, + Rect2(point - handle_size * 0.5, handle_size), + false); + } + + if (on_edge) { + Ref add_handle = get_theme_icon("EditorHandleAdd", "EditorIcons"); + p_overlay->draw_texture(add_handle, edge_point - add_handle->get_size() * 0.5); + } +} + +void Path2DEditor::_node_visibility_changed() { + if (!node) { + return; + } + + canvas_item_editor->update_viewport(); +} + +void Path2DEditor::edit(Node *p_path2d) { + if (!canvas_item_editor) { + canvas_item_editor = CanvasItemEditor::get_singleton(); + } + + if (p_path2d) { + node = Object::cast_to(p_path2d); + if (!node->is_connected("visibility_changed", this, "_node_visibility_changed")) { + node->connect("visibility_changed", this, "_node_visibility_changed"); + } + + } else { + // node may have been deleted at this point + if (node && node->is_connected("visibility_changed", this, "_node_visibility_changed")) { + node->disconnect("visibility_changed", this, "_node_visibility_changed"); + } + node = nullptr; + } +} + +void Path2DEditor::_bind_methods() { + //ClassDB::bind_method(D_METHOD("_menu_option"),&Path2DEditor::_menu_option); + ClassDB::bind_method(D_METHOD("_node_visibility_changed"), &Path2DEditor::_node_visibility_changed); + ClassDB::bind_method(D_METHOD("_mode_selected"), &Path2DEditor::_mode_selected); + ClassDB::bind_method(D_METHOD("_handle_option_pressed"), &Path2DEditor::_handle_option_pressed); +} + +void Path2DEditor::_mode_selected(int p_mode) { + if (p_mode == MODE_CREATE) { + curve_create->set_pressed(true); + curve_edit->set_pressed(false); + curve_edit_curve->set_pressed(false); + curve_del->set_pressed(false); + } else if (p_mode == MODE_EDIT) { + curve_create->set_pressed(false); + curve_edit->set_pressed(true); + curve_edit_curve->set_pressed(false); + curve_del->set_pressed(false); + } else if (p_mode == MODE_EDIT_CURVE) { + curve_create->set_pressed(false); + curve_edit->set_pressed(false); + curve_edit_curve->set_pressed(true); + curve_del->set_pressed(false); + } else if (p_mode == MODE_DELETE) { + curve_create->set_pressed(false); + curve_edit->set_pressed(false); + curve_edit_curve->set_pressed(false); + curve_del->set_pressed(true); + } else if (p_mode == ACTION_CLOSE) { + //? + + if (!node->get_curve().is_valid()) { + return; + } + if (node->get_curve()->get_point_count() < 3) { + return; + } + + Vector2 begin = node->get_curve()->get_point_position(0); + Vector2 end = node->get_curve()->get_point_position(node->get_curve()->get_point_count() - 1); + if (begin.distance_to(end) < CMP_EPSILON) { + return; + } + + undo_redo->create_action(TTR("Remove Point from Curve")); + undo_redo->add_do_method(node->get_curve().ptr(), "add_point", begin); + undo_redo->add_undo_method(node->get_curve().ptr(), "remove_point", node->get_curve()->get_point_count()); + undo_redo->add_do_method(canvas_item_editor, "update_viewport"); + undo_redo->add_undo_method(canvas_item_editor, "update_viewport"); + undo_redo->commit_action(); + return; + } + + mode = Mode(p_mode); +} + +void Path2DEditor::_handle_option_pressed(int p_option) { + PopupMenu *pm; + pm = handle_menu->get_popup(); + + switch (p_option) { + case HANDLE_OPTION_ANGLE: { + bool is_checked = pm->is_item_checked(HANDLE_OPTION_ANGLE); + mirror_handle_angle = !is_checked; + pm->set_item_checked(HANDLE_OPTION_ANGLE, mirror_handle_angle); + pm->set_item_disabled(HANDLE_OPTION_LENGTH, !mirror_handle_angle); + } break; + case HANDLE_OPTION_LENGTH: { + bool is_checked = pm->is_item_checked(HANDLE_OPTION_LENGTH); + mirror_handle_length = !is_checked; + pm->set_item_checked(HANDLE_OPTION_LENGTH, mirror_handle_length); + } break; + } +} + +Path2DEditor::Path2DEditor(EditorNode *p_editor) { + canvas_item_editor = nullptr; + editor = p_editor; + undo_redo = editor->get_undo_redo(); + mirror_handle_angle = true; + mirror_handle_length = true; + on_edge = false; + + mode = MODE_EDIT; + action = ACTION_NONE; + + base_hb = memnew(HBoxContainer); + CanvasItemEditor::get_singleton()->add_control_to_menu_panel(base_hb); + + sep = memnew(VSeparator); + base_hb->add_child(sep); + curve_edit = memnew(ToolButton); + curve_edit->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon("CurveEdit", "EditorIcons")); + curve_edit->set_toggle_mode(true); + curve_edit->set_focus_mode(Control::FOCUS_NONE); + curve_edit->set_tooltip(TTR("Select Points") + "\n" + TTR("Shift+Drag: Select Control Points") + "\n" + keycode_get_string(KEY_MASK_CMD) + TTR("Click: Add Point") + "\n" + TTR("Left Click: Split Segment (in curve)") + "\n" + TTR("Right Click: Delete Point")); + curve_edit->connect("pressed", this, "_mode_selected", varray(MODE_EDIT)); + base_hb->add_child(curve_edit); + curve_edit_curve = memnew(ToolButton); + curve_edit_curve->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon("CurveCurve", "EditorIcons")); + curve_edit_curve->set_toggle_mode(true); + curve_edit_curve->set_focus_mode(Control::FOCUS_NONE); + curve_edit_curve->set_tooltip(TTR("Select Control Points (Shift+Drag)")); + curve_edit_curve->connect("pressed", this, "_mode_selected", varray(MODE_EDIT_CURVE)); + base_hb->add_child(curve_edit_curve); + curve_create = memnew(ToolButton); + curve_create->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon("CurveCreate", "EditorIcons")); + curve_create->set_toggle_mode(true); + curve_create->set_focus_mode(Control::FOCUS_NONE); + curve_create->set_tooltip(TTR("Add Point (in empty space)")); + curve_create->connect("pressed", this, "_mode_selected", varray(MODE_CREATE)); + base_hb->add_child(curve_create); + curve_del = memnew(ToolButton); + curve_del->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon("CurveDelete", "EditorIcons")); + curve_del->set_toggle_mode(true); + curve_del->set_focus_mode(Control::FOCUS_NONE); + curve_del->set_tooltip(TTR("Delete Point")); + curve_del->connect("pressed", this, "_mode_selected", varray(MODE_DELETE)); + base_hb->add_child(curve_del); + curve_close = memnew(ToolButton); + curve_close->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon("CurveClose", "EditorIcons")); + curve_close->set_focus_mode(Control::FOCUS_NONE); + curve_close->set_tooltip(TTR("Close Curve")); + curve_close->connect("pressed", this, "_mode_selected", varray(ACTION_CLOSE)); + base_hb->add_child(curve_close); + + PopupMenu *menu; + + handle_menu = memnew(MenuButton); + handle_menu->set_text(TTR("Options")); + base_hb->add_child(handle_menu); + + menu = handle_menu->get_popup(); + menu->add_check_item(TTR("Mirror Handle Angles")); + menu->set_item_checked(HANDLE_OPTION_ANGLE, mirror_handle_angle); + menu->add_check_item(TTR("Mirror Handle Lengths")); + menu->set_item_checked(HANDLE_OPTION_LENGTH, mirror_handle_length); + menu->connect("id_pressed", this, "_handle_option_pressed"); + + base_hb->hide(); + + curve_edit->set_pressed(true); +} + +void Path2DEditorPlugin::edit(Object *p_object) { + path2d_editor->edit(Object::cast_to(p_object)); +} + +bool Path2DEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("Path2D"); +} + +void Path2DEditorPlugin::make_visible(bool p_visible) { + if (p_visible) { + path2d_editor->show(); + path2d_editor->base_hb->show(); + + } else { + path2d_editor->hide(); + path2d_editor->base_hb->hide(); + path2d_editor->edit(nullptr); + } +} + +Path2DEditorPlugin::Path2DEditorPlugin(EditorNode *p_node) { + editor = p_node; + path2d_editor = memnew(Path2DEditor(p_node)); + CanvasItemEditor::get_singleton()->add_control_to_menu_panel(path2d_editor); + path2d_editor->hide(); +} + +Path2DEditorPlugin::~Path2DEditorPlugin() { +} diff --git a/modules/paint/nodes/curve_2d/editor/path_2d_editor_plugin.h b/modules/paint/nodes/curve_2d/editor/path_2d_editor_plugin.h new file mode 100644 index 000000000..a149ac311 --- /dev/null +++ b/modules/paint/nodes/curve_2d/editor/path_2d_editor_plugin.h @@ -0,0 +1,146 @@ +#ifndef PATH_2D_EDITOR_PLUGIN_H +#define PATH_2D_EDITOR_PLUGIN_H +/*************************************************************************/ +/* path_2d_editor_plugin.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "scene/gui/box_container.h" +#include "editor/editor_plugin.h" + +#include "core/math/vector2.h" +#include "core/object/object.h" +#include "core/object/reference.h" +#include "core/string/ustring.h" + + +class CanvasItemEditor; +class Control; +class EditorNode; +class InputEvent; +class MenuButton; +class Node; +class Panel; +class Path2D; +class Separator; +class ToolButton; +class UndoRedo; + +class Path2DEditor : public HBoxContainer { + GDCLASS(Path2DEditor, HBoxContainer); + + UndoRedo *undo_redo; + + CanvasItemEditor *canvas_item_editor; + EditorNode *editor; + Panel *panel; + Path2D *node; + + HBoxContainer *base_hb; + Separator *sep; + + enum Mode { + MODE_CREATE, + MODE_EDIT, + MODE_EDIT_CURVE, + MODE_DELETE, + ACTION_CLOSE + }; + + Mode mode; + ToolButton *curve_create; + ToolButton *curve_edit; + ToolButton *curve_edit_curve; + ToolButton *curve_del; + ToolButton *curve_close; + MenuButton *handle_menu; + + bool mirror_handle_angle; + bool mirror_handle_length; + bool on_edge; + + enum HandleOption { + HANDLE_OPTION_ANGLE, + HANDLE_OPTION_LENGTH + }; + + enum Action { + + ACTION_NONE, + ACTION_MOVING_POINT, + ACTION_MOVING_IN, + ACTION_MOVING_OUT, + }; + + Action action; + int action_point; + Point2 moving_from; + Point2 moving_screen_from; + float orig_in_length; + float orig_out_length; + Vector2 edge_point; + + void _mode_selected(int p_mode); + void _handle_option_pressed(int p_option); + + void _node_visibility_changed(); + friend class Path2DEditorPlugin; + +protected: + void _notification(int p_what); + void _node_removed(Node *p_node); + static void _bind_methods(); + +public: + bool forward_gui_input(const Ref &p_event); + void forward_canvas_draw_over_viewport(Control *p_overlay); + void edit(Node *p_path2d); + Path2DEditor(EditorNode *p_editor); +}; + +class Path2DEditorPlugin : public EditorPlugin { + GDCLASS(Path2DEditorPlugin, EditorPlugin); + + Path2DEditor *path2d_editor; + EditorNode *editor; + +public: + virtual bool forward_canvas_gui_input(const Ref &p_event) { return path2d_editor->forward_gui_input(p_event); } + virtual void forward_canvas_draw_over_viewport(Control *p_overlay) { path2d_editor->forward_canvas_draw_over_viewport(p_overlay); } + + virtual String get_name() const { return "Path2D"; } + bool has_main_screen() const { return false; } + virtual void edit(Object *p_object); + virtual bool handles(Object *p_object) const; + virtual void make_visible(bool p_visible); + + Path2DEditorPlugin(EditorNode *p_node); + ~Path2DEditorPlugin(); +}; + +#endif // PATH_2D_EDITOR_PLUGIN_H diff --git a/modules/paint/nodes/curve_2d/path_2d.cpp b/modules/paint/nodes/curve_2d/path_2d.cpp new file mode 100644 index 000000000..ec438962d --- /dev/null +++ b/modules/paint/nodes/curve_2d/path_2d.cpp @@ -0,0 +1,412 @@ +/*************************************************************************/ +/* path_2d.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "path_2d.h" + +#include "core/config/engine.h" +#include "scene/resources/curve.h" +#include "scene/scene_string_names.h" + +#ifdef TOOLS_ENABLED +#include "editor/editor_scale.h" +#endif + +#ifdef TOOLS_ENABLED +Rect2 Path2D::_edit_get_rect() const { + if (!curve.is_valid() || curve->get_point_count() == 0) { + return Rect2(0, 0, 0, 0); + } + + Rect2 aabb = Rect2(curve->get_point_position(0), Vector2(0, 0)); + + for (int i = 0; i < curve->get_point_count(); i++) { + for (int j = 0; j <= 8; j++) { + real_t frac = j / 8.0; + Vector2 p = curve->interpolate(i, frac); + aabb.expand_to(p); + } + } + + return aabb; +} + +bool Path2D::_edit_use_rect() const { + return curve.is_valid() && curve->get_point_count() != 0; +} + +bool Path2D::_edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const { + if (curve.is_null()) { + return false; + } + + for (int i = 0; i < curve->get_point_count(); i++) { + Vector2 s[2]; + s[0] = curve->get_point_position(i); + + for (int j = 1; j <= 8; j++) { + real_t frac = j / 8.0; + s[1] = curve->interpolate(i, frac); + + Vector2 p = Geometry::get_closest_point_to_segment_2d(p_point, s); + if (p.distance_to(p_point) <= p_tolerance) { + return true; + } + + s[0] = s[1]; + } + } + + return false; +} +#endif + +void Path2D::_notification(int p_what) { + if (p_what == NOTIFICATION_DRAW && curve.is_valid()) { + //draw the curve!! + + if (!Engine::get_singleton()->is_editor_hint() && !get_tree()->is_debugging_navigation_hint()) { + return; + } + + if (curve->get_point_count() < 2) { + return; + } + +#ifdef TOOLS_ENABLED + const float line_width = 2 * EDSCALE; +#else + const float line_width = 2; +#endif + const Color color = Color(1.0, 1.0, 1.0, 1.0); + + _cached_draw_pts.resize(curve->get_point_count() * 8); + int count = 0; + + for (int i = 0; i < curve->get_point_count(); i++) { + for (int j = 0; j < 8; j++) { + real_t frac = j * (1.0 / 8.0); + Vector2 p = curve->interpolate(i, frac); + _cached_draw_pts.set(count++, p); + } + } + + draw_polyline(_cached_draw_pts, color, line_width, true); + } +} + +void Path2D::_curve_changed() { + if (!is_inside_tree()) { + return; + } + + if (!Engine::get_singleton()->is_editor_hint() && !get_tree()->is_debugging_navigation_hint()) { + return; + } + + update(); +} + +void Path2D::set_curve(const Ref &p_curve) { + if (curve.is_valid()) { + curve->disconnect("changed", this, "_curve_changed"); + } + + curve = p_curve; + + if (curve.is_valid()) { + curve->connect("changed", this, "_curve_changed"); + } + + _curve_changed(); +} + +Ref Path2D::get_curve() const { + return curve; +} + +void Path2D::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_curve", "curve"), &Path2D::set_curve); + ClassDB::bind_method(D_METHOD("get_curve"), &Path2D::get_curve); + ClassDB::bind_method(D_METHOD("_curve_changed"), &Path2D::_curve_changed); + + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve2D"), "set_curve", "get_curve"); +} + +Path2D::Path2D() { + set_curve(Ref(memnew(Curve2D))); //create one by default + set_self_modulate(Color(0.5, 0.6, 1.0, 0.7)); +} + +///////////////////////////////////////////////////////////////////////////////// + +void PathFollow2D::_update_transform() { + if (!path) { + return; + } + + Ref c = path->get_curve(); + if (!c.is_valid()) { + return; + } + + float path_length = c->get_baked_length(); + if (path_length == 0) { + return; + } + Vector2 pos = c->interpolate_baked(offset, cubic); + + if (rotate) { + float ahead = offset + lookahead; + + if (loop && ahead >= path_length) { + // If our lookahead will loop, we need to check if the path is closed. + int point_count = c->get_point_count(); + if (point_count > 0) { + Vector2 start_point = c->get_point_position(0); + Vector2 end_point = c->get_point_position(point_count - 1); + if (start_point == end_point) { + // Since the path is closed we want to 'smooth off' + // the corner at the start/end. + // So we wrap the lookahead back round. + ahead = Math::fmod(ahead, path_length); + } + } + } + + Vector2 ahead_pos = c->interpolate_baked(ahead, cubic); + + Vector2 tangent_to_curve; + if (ahead_pos == pos) { + // This will happen at the end of non-looping or non-closed paths. + // We'll try a look behind instead, in order to get a meaningful angle. + tangent_to_curve = + (pos - c->interpolate_baked(offset - lookahead, cubic)).normalized(); + } else { + tangent_to_curve = (ahead_pos - pos).normalized(); + } + + Vector2 normal_of_curve = -tangent_to_curve.tangent(); + + pos += tangent_to_curve * h_offset; + pos += normal_of_curve * v_offset; + + set_rotation(tangent_to_curve.angle()); + + } else { + pos.x += h_offset; + pos.y += v_offset; + } + + set_position(pos); +} + +void PathFollow2D::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + path = Object::cast_to(get_parent()); + if (path) { + _update_transform(); + } + + } break; + case NOTIFICATION_EXIT_TREE: { + path = nullptr; + } break; + } +} + +void PathFollow2D::set_cubic_interpolation(bool p_enable) { + cubic = p_enable; +} + +bool PathFollow2D::get_cubic_interpolation() const { + return cubic; +} + +void PathFollow2D::_validate_property(PropertyInfo &property) const { + if (property.name == "offset") { + float max = 10000; + if (path && path->get_curve().is_valid()) { + max = path->get_curve()->get_baked_length(); + } + + property.hint_string = "0," + rtos(max) + ",0.01,or_lesser,or_greater"; + } +} + +String PathFollow2D::get_configuration_warning() const { + if (!is_visible_in_tree() || !is_inside_tree()) { + return String(); + } + + String warning = Node2D::get_configuration_warning(); + if (!Object::cast_to(get_parent())) { + if (warning != String()) { + warning += "\n\n"; + } + warning += TTR("PathFollow2D only works when set as a child of a Path2D node."); + } + + return warning; +} + +void PathFollow2D::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_offset", "offset"), &PathFollow2D::set_offset); + ClassDB::bind_method(D_METHOD("get_offset"), &PathFollow2D::get_offset); + + ClassDB::bind_method(D_METHOD("set_h_offset", "h_offset"), &PathFollow2D::set_h_offset); + ClassDB::bind_method(D_METHOD("get_h_offset"), &PathFollow2D::get_h_offset); + + ClassDB::bind_method(D_METHOD("set_v_offset", "v_offset"), &PathFollow2D::set_v_offset); + ClassDB::bind_method(D_METHOD("get_v_offset"), &PathFollow2D::get_v_offset); + + ClassDB::bind_method(D_METHOD("set_unit_offset", "unit_offset"), &PathFollow2D::set_unit_offset); + ClassDB::bind_method(D_METHOD("get_unit_offset"), &PathFollow2D::get_unit_offset); + + ClassDB::bind_method(D_METHOD("set_rotate", "enable"), &PathFollow2D::set_rotate); + ClassDB::bind_method(D_METHOD("is_rotating"), &PathFollow2D::is_rotating); + + ClassDB::bind_method(D_METHOD("set_cubic_interpolation", "enable"), &PathFollow2D::set_cubic_interpolation); + ClassDB::bind_method(D_METHOD("get_cubic_interpolation"), &PathFollow2D::get_cubic_interpolation); + + ClassDB::bind_method(D_METHOD("set_loop", "loop"), &PathFollow2D::set_loop); + ClassDB::bind_method(D_METHOD("has_loop"), &PathFollow2D::has_loop); + + ClassDB::bind_method(D_METHOD("set_lookahead", "lookahead"), &PathFollow2D::set_lookahead); + ClassDB::bind_method(D_METHOD("get_lookahead"), &PathFollow2D::get_lookahead); + + ADD_PROPERTY(PropertyInfo(Variant::REAL, "offset", PROPERTY_HINT_RANGE, "0,10000,0.01,or_lesser,or_greater"), "set_offset", "get_offset"); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "unit_offset", PROPERTY_HINT_RANGE, "0,1,0.0001,or_lesser,or_greater", PROPERTY_USAGE_EDITOR), "set_unit_offset", "get_unit_offset"); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "h_offset"), "set_h_offset", "get_h_offset"); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "v_offset"), "set_v_offset", "get_v_offset"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "rotate"), "set_rotate", "is_rotating"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "cubic_interp"), "set_cubic_interpolation", "get_cubic_interpolation"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "has_loop"); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "lookahead", PROPERTY_HINT_RANGE, "0.001,1024.0,0.001"), "set_lookahead", "get_lookahead"); +} + +void PathFollow2D::set_offset(float p_offset) { + ERR_FAIL_COND(!isfinite(p_offset)); + offset = p_offset; + if (path) { + if (path->get_curve().is_valid()) { + float path_length = path->get_curve()->get_baked_length(); + + if (loop) { + offset = Math::fposmod(offset, path_length); + if (!Math::is_zero_approx(p_offset) && Math::is_zero_approx(offset)) { + offset = path_length; + } + } else { + offset = CLAMP(offset, 0, path_length); + } + } + + _update_transform(); + } + _change_notify("offset"); + _change_notify("unit_offset"); +} + +void PathFollow2D::set_h_offset(float p_h_offset) { + h_offset = p_h_offset; + if (path) { + _update_transform(); + } +} + +float PathFollow2D::get_h_offset() const { + return h_offset; +} + +void PathFollow2D::set_v_offset(float p_v_offset) { + v_offset = p_v_offset; + if (path) { + _update_transform(); + } +} + +float PathFollow2D::get_v_offset() const { + return v_offset; +} + +float PathFollow2D::get_offset() const { + return offset; +} + +void PathFollow2D::set_unit_offset(float p_unit_offset) { + if (path && path->get_curve().is_valid() && path->get_curve()->get_baked_length()) { + set_offset(p_unit_offset * path->get_curve()->get_baked_length()); + } +} + +float PathFollow2D::get_unit_offset() const { + if (path && path->get_curve().is_valid() && path->get_curve()->get_baked_length()) { + return get_offset() / path->get_curve()->get_baked_length(); + } else { + return 0; + } +} + +void PathFollow2D::set_lookahead(float p_lookahead) { + lookahead = p_lookahead; +} + +float PathFollow2D::get_lookahead() const { + return lookahead; +} + +void PathFollow2D::set_rotate(bool p_rotate) { + rotate = p_rotate; + _update_transform(); +} + +bool PathFollow2D::is_rotating() const { + return rotate; +} + +void PathFollow2D::set_loop(bool p_loop) { + loop = p_loop; +} + +bool PathFollow2D::has_loop() const { + return loop; +} + +PathFollow2D::PathFollow2D() { + offset = 0; + h_offset = 0; + v_offset = 0; + path = nullptr; + rotate = true; + cubic = true; + loop = true; + lookahead = 4; +} diff --git a/modules/paint/nodes/curve_2d/path_2d.h b/modules/paint/nodes/curve_2d/path_2d.h new file mode 100644 index 000000000..9a7f065a1 --- /dev/null +++ b/modules/paint/nodes/curve_2d/path_2d.h @@ -0,0 +1,114 @@ +#ifndef PATH_2D_H +#define PATH_2D_H +/*************************************************************************/ +/* path_2d.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "scene/2d/node_2d.h" + +class Curve2D; + +class Path2D : public Node2D { + GDCLASS(Path2D, Node2D); + + Ref curve; + Vector _cached_draw_pts; + + void _curve_changed(); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: +#ifdef TOOLS_ENABLED + virtual Rect2 _edit_get_rect() const; + virtual bool _edit_use_rect() const; + virtual bool _edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const; +#endif + + void set_curve(const Ref &p_curve); + Ref get_curve() const; + + Path2D(); +}; + +class PathFollow2D : public Node2D { + GDCLASS(PathFollow2D, Node2D); + +public: +private: + Path2D *path; + real_t offset; + real_t h_offset; + real_t v_offset; + real_t lookahead; + bool cubic; + bool loop; + bool rotate; + + void _update_transform(); + +protected: + virtual void _validate_property(PropertyInfo &property) const; + + void _notification(int p_what); + static void _bind_methods(); + +public: + void set_offset(float p_offset); + float get_offset() const; + + void set_h_offset(float p_h_offset); + float get_h_offset() const; + + void set_v_offset(float p_v_offset); + float get_v_offset() const; + + void set_unit_offset(float p_unit_offset); + float get_unit_offset() const; + + void set_lookahead(float p_lookahead); + float get_lookahead() const; + + void set_loop(bool p_loop); + bool has_loop() const; + + void set_rotate(bool p_rotate); + bool is_rotating() const; + + void set_cubic_interpolation(bool p_enable); + bool get_cubic_interpolation() const; + + String get_configuration_warning() const; + + PathFollow2D(); +}; + +#endif // PATH_2D_H diff --git a/modules/paint/nodes/polygon_2d/editor/polygon_2d_editor_plugin.cpp b/modules/paint/nodes/polygon_2d/editor/polygon_2d_editor_plugin.cpp new file mode 100644 index 000000000..8be63e820 --- /dev/null +++ b/modules/paint/nodes/polygon_2d/editor/polygon_2d_editor_plugin.cpp @@ -0,0 +1,1531 @@ +/*************************************************************************/ +/* polygon_2d_editor_plugin.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "polygon_2d_editor_plugin.h" + +#include "modules/modules_enabled.gen.h" + +#ifdef MODULE_SKELETON_2D_ENABLED +#include "modules/skeleton_2d/nodes/skeleton_2d.h" +#endif + +#include "canvas_item_editor_plugin.h" +#include "core/input/input.h" +#include "core/input/input_event.h" +#include "core/math/geometry.h" +#include "core/math/math_defs.h" +#include "core/math/math_funcs.h" +#include "core/math/rect2.h" +#include "core/math/transform_2d.h" +#include "core/object/class_db.h" +#include "core/object/undo_redo.h" +#include "core/os/keyboard.h" +#include "core/os/memory.h" +#include "core/string/node_path.h" +#include "core/string/string_name.h" +#include "core/string/ustring.h" +#include "core/typedefs.h" +#include "core/variant/variant.h" +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" +#include "scene/2d/canvas_item.h" +#include "scene/2d/polygon_2d.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/check_box.h" +#include "scene/gui/control.h" +#include "scene/gui/dialogs.h" +#include "scene/gui/label.h" +#include "scene/gui/menu_button.h" +#include "scene/gui/panel.h" +#include "scene/gui/popup_menu.h" +#include "scene/gui/scroll_bar.h" +#include "scene/gui/scroll_container.h" +#include "scene/gui/separator.h" +#include "scene/gui/slider.h" +#include "scene/gui/spin_box.h" +#include "scene/gui/split_container.h" +#include "scene/gui/texture_rect.h" +#include "scene/gui/tool_button.h" +#include "scene/main/node.h" +#include "scene/resources/texture.h" +#include "servers/rendering_server.h" + +class EditorNode; +class Node2D; + +Node2D *Polygon2DEditor::_get_node() const { + return node; +} + +void Polygon2DEditor::_set_node(Node *p_polygon) { + node = Object::cast_to(p_polygon); + _update_polygon_editing_state(); +} + +Vector2 Polygon2DEditor::_get_offset(int p_idx) const { + return node->get_offset(); +} + +int Polygon2DEditor::_get_polygon_count() const { + if (node->get_internal_vertex_count() > 0) { + return 0; //do not edit if internal vertices exist + } else { + return 1; + } +} + +void Polygon2DEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: { + uv_edit_draw->add_theme_style_override("panel", get_theme_stylebox("bg", "Tree")); + bone_scroll->add_theme_style_override("bg", get_theme_stylebox("bg", "Tree")); + } break; + case NOTIFICATION_READY: { + button_uv->set_icon(get_theme_icon("Uv", "EditorIcons")); + + uv_button[UV_MODE_CREATE]->set_icon(get_theme_icon("Edit", "EditorIcons")); + uv_button[UV_MODE_CREATE_INTERNAL]->set_icon(get_theme_icon("EditInternal", "EditorIcons")); + uv_button[UV_MODE_REMOVE_INTERNAL]->set_icon(get_theme_icon("RemoveInternal", "EditorIcons")); + uv_button[UV_MODE_EDIT_POINT]->set_icon(get_theme_icon("ToolSelect", "EditorIcons")); + uv_button[UV_MODE_MOVE]->set_icon(get_theme_icon("ToolMove", "EditorIcons")); + uv_button[UV_MODE_ROTATE]->set_icon(get_theme_icon("ToolRotate", "EditorIcons")); + uv_button[UV_MODE_SCALE]->set_icon(get_theme_icon("ToolScale", "EditorIcons")); + uv_button[UV_MODE_ADD_POLYGON]->set_icon(get_theme_icon("Edit", "EditorIcons")); + uv_button[UV_MODE_REMOVE_POLYGON]->set_icon(get_theme_icon("Close", "EditorIcons")); + uv_button[UV_MODE_PAINT_WEIGHT]->set_icon(get_theme_icon("Bucket", "EditorIcons")); + uv_button[UV_MODE_CLEAR_WEIGHT]->set_icon(get_theme_icon("Clear", "EditorIcons")); + + b_snap_grid->set_icon(get_theme_icon("Grid", "EditorIcons")); + b_snap_enable->set_icon(get_theme_icon("SnapGrid", "EditorIcons")); + uv_icon_zoom->set_texture(get_theme_icon("Zoom", "EditorIcons")); + + uv_vscroll->set_anchors_and_margins_preset(PRESET_RIGHT_WIDE); + uv_hscroll->set_anchors_and_margins_preset(PRESET_BOTTOM_WIDE); + } break; + case NOTIFICATION_VISIBILITY_CHANGED: { + if (!is_visible()) { + uv_edit->hide(); + } + } break; + } +} + +void Polygon2DEditor::_sync_bones() { +#ifdef MODULE_SKELETON_2D_ENABLED + Skeleton2D *skeleton = nullptr; + if (!node->has_node(node->get_skeleton())) { + error->set_text(TTR("The skeleton property of the Polygon2D does not point to a Skeleton2D node")); + error->popup_centered_minsize(); + } else { + Node *sn = node->get_node(node->get_skeleton()); + skeleton = Object::cast_to(sn); + } + + Array prev_bones = node->call("_get_bones"); + node->clear_bones(); + + if (!skeleton) { + error->set_text(TTR("The skeleton property of the Polygon2D does not point to a Skeleton2D node")); + error->popup_centered_minsize(); + } else { + for (int i = 0; i < skeleton->get_bone_count(); i++) { + NodePath path = skeleton->get_path_to(skeleton->get_bone(i)); + PoolVector weights; + int wc = node->get_polygon().size(); + + for (int j = 0; j < prev_bones.size(); j += 2) { + NodePath pvp = prev_bones[j]; + PoolVector pv = prev_bones[j + 1]; + if (pvp == path && pv.size() == wc) { + weights = pv; + } + } + + if (weights.size() == 0) { //create them + weights.resize(node->get_polygon().size()); + PoolVector::Write w = weights.write(); + for (int j = 0; j < wc; j++) { + w[j] = 0.0; + } + } + + node->add_bone(path, weights); + } + } + + Array new_bones = node->call("_get_bones"); + + undo_redo->create_action(TTR("Sync Bones")); + undo_redo->add_do_method(node, "_set_bones", new_bones); + undo_redo->add_undo_method(node, "_set_bones", prev_bones); + undo_redo->add_do_method(this, "_update_bone_list"); + undo_redo->add_undo_method(this, "_update_bone_list"); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); +#endif +} + +void Polygon2DEditor::_update_bone_list() { + NodePath selected; + while (bone_scroll_vb->get_child_count()) { + CheckBox *cb = Object::cast_to(bone_scroll_vb->get_child(0)); + if (cb && cb->is_pressed()) { + selected = cb->get_meta("bone_path"); + } + memdelete(bone_scroll_vb->get_child(0)); + } + + Ref bg; + bg.instance(); + for (int i = 0; i < node->get_bone_count(); i++) { + CheckBox *cb = memnew(CheckBox); + NodePath np = node->get_bone_path(i); + String name; + if (np.get_name_count()) { + name = np.get_name(np.get_name_count() - 1); + } + if (name == String()) { + name = "Bone " + itos(i); + } + cb->set_text(name); + cb->set_button_group(bg); + cb->set_meta("bone_path", np); + cb->set_focus_mode(FOCUS_NONE); + bone_scroll_vb->add_child(cb); + + if (np == selected || bone_scroll_vb->get_child_count() < 2) { + cb->set_pressed(true); + } + + cb->connect("pressed", this, "_bone_paint_selected", varray(i)); + } + + uv_edit_draw->update(); +} + +void Polygon2DEditor::_bone_paint_selected(int p_index) { + uv_edit_draw->update(); +} + +void Polygon2DEditor::_uv_edit_mode_select(int p_mode) { + if (p_mode == 0) { //uv + + uv_button[UV_MODE_CREATE]->hide(); + uv_button[UV_MODE_CREATE_INTERNAL]->hide(); + uv_button[UV_MODE_REMOVE_INTERNAL]->hide(); + for (int i = UV_MODE_EDIT_POINT; i <= UV_MODE_SCALE; i++) { + uv_button[i]->show(); + } + uv_button[UV_MODE_ADD_POLYGON]->hide(); + uv_button[UV_MODE_REMOVE_POLYGON]->hide(); + uv_button[UV_MODE_PAINT_WEIGHT]->hide(); + uv_button[UV_MODE_CLEAR_WEIGHT]->hide(); + _uv_mode(UV_MODE_EDIT_POINT); + + bone_scroll_main_vb->hide(); + bone_paint_strength->hide(); + bone_paint_radius->hide(); + bone_paint_radius_label->hide(); + } else if (p_mode == 1) { //poly + + for (int i = 0; i <= UV_MODE_SCALE; i++) { + uv_button[i]->show(); + } + uv_button[UV_MODE_ADD_POLYGON]->hide(); + uv_button[UV_MODE_REMOVE_POLYGON]->hide(); + uv_button[UV_MODE_PAINT_WEIGHT]->hide(); + uv_button[UV_MODE_CLEAR_WEIGHT]->hide(); + _uv_mode(UV_MODE_EDIT_POINT); + + bone_scroll_main_vb->hide(); + bone_paint_strength->hide(); + bone_paint_radius->hide(); + bone_paint_radius_label->hide(); + } else if (p_mode == 2) { //splits + + for (int i = 0; i <= UV_MODE_SCALE; i++) { + uv_button[i]->hide(); + } + uv_button[UV_MODE_ADD_POLYGON]->show(); + uv_button[UV_MODE_REMOVE_POLYGON]->show(); + uv_button[UV_MODE_PAINT_WEIGHT]->hide(); + uv_button[UV_MODE_CLEAR_WEIGHT]->hide(); + _uv_mode(UV_MODE_ADD_POLYGON); + + bone_scroll_main_vb->hide(); + bone_paint_strength->hide(); + bone_paint_radius->hide(); + bone_paint_radius_label->hide(); + } else if (p_mode == 3) { //bonesĀ“ + + for (int i = 0; i <= UV_MODE_REMOVE_POLYGON; i++) { + uv_button[i]->hide(); + } + uv_button[UV_MODE_PAINT_WEIGHT]->show(); + uv_button[UV_MODE_CLEAR_WEIGHT]->show(); + _uv_mode(UV_MODE_PAINT_WEIGHT); + + bone_scroll_main_vb->show(); + bone_paint_strength->show(); + bone_paint_radius->show(); + bone_paint_radius_label->show(); + _update_bone_list(); + bone_paint_pos = Vector2(-100000, -100000); //send brush away when switching + } + + uv_edit->set_size(uv_edit->get_size()); // Necessary readjustment of the popup window. + uv_edit_draw->update(); +} + +void Polygon2DEditor::_uv_edit_popup_hide() { + EditorSettings::get_singleton()->set("interface/dialogs/uv_editor_bounds", uv_edit->get_rect()); + + _cancel_editing(); +} + +void Polygon2DEditor::_menu_option(int p_option) { + switch (p_option) { + case MODE_EDIT_UV: { + if (node->get_texture().is_null()) { + error->set_text(TTR("No texture in this polygon.\nSet a texture to be able to edit UV.")); + error->popup_centered_minsize(); + return; + } + + PoolVector points = node->get_polygon(); + PoolVector uvs = node->get_uv(); + if (uvs.size() != points.size()) { + undo_redo->create_action(TTR("Create UV Map")); + undo_redo->add_do_method(node, "set_uv", points); + undo_redo->add_undo_method(node, "set_uv", uvs); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + } + + if (EditorSettings::get_singleton()->has_setting("interface/dialogs/uv_editor_bounds")) { + uv_edit->popup(EditorSettings::get_singleton()->get("interface/dialogs/uv_editor_bounds")); + } else { + uv_edit->popup_centered_ratio(0.85); + } + _update_bone_list(); + } break; + case UVEDIT_POLYGON_TO_UV: { + PoolVector points = node->get_polygon(); + if (points.size() == 0) { + break; + } + PoolVector uvs = node->get_uv(); + undo_redo->create_action(TTR("Create UV Map")); + undo_redo->add_do_method(node, "set_uv", points); + undo_redo->add_undo_method(node, "set_uv", uvs); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + } break; + case UVEDIT_UV_TO_POLYGON: { + PoolVector points = node->get_polygon(); + PoolVector uvs = node->get_uv(); + if (uvs.size() == 0) { + break; + } + + undo_redo->create_action(TTR("Create Polygon")); + undo_redo->add_do_method(node, "set_polygon", uvs); + undo_redo->add_undo_method(node, "set_polygon", points); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + } break; + case UVEDIT_UV_CLEAR: { + PoolVector uvs = node->get_uv(); + if (uvs.size() == 0) { + break; + } + undo_redo->create_action(TTR("Create UV Map")); + undo_redo->add_do_method(node, "set_uv", PoolVector()); + undo_redo->add_undo_method(node, "set_uv", uvs); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + } break; + case UVEDIT_GRID_SETTINGS: { + grid_settings->popup_centered_minsize(); + } break; + default: { + AbstractPolygon2DEditor::_menu_option(p_option); + } break; + } +} + +void Polygon2DEditor::_cancel_editing() { + if (uv_create) { + uv_drag = false; + uv_create = false; + node->set_uv(uv_create_uv_prev); + node->set_polygon(uv_create_poly_prev); + node->set_internal_vertex_count(uv_create_prev_internal_vertices); + node->set_vertex_colors(uv_create_colors_prev); + node->call("_set_bones", uv_create_bones_prev); + node->set_polygons(polygons_prev); + + _update_polygon_editing_state(); + } else if (uv_drag) { + uv_drag = false; + if (uv_edit_mode[0]->is_pressed()) { // Edit UV. + node->set_uv(points_prev); + } else if (uv_edit_mode[1]->is_pressed()) { // Edit polygon. + node->set_polygon(points_prev); + } + } + + polygon_create.clear(); +} + +void Polygon2DEditor::_update_polygon_editing_state() { + if (!_get_node()) { + return; + } + + if (node->get_internal_vertex_count() > 0) { + disable_polygon_editing(true, TTR("Polygon 2D has internal vertices, so it can no longer be edited in the viewport.")); + } else { + disable_polygon_editing(false, String()); + } +} + +void Polygon2DEditor::_commit_action() { + // Makes that undo/redoing actions made outside of the UV editor still affect its polygon. + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->add_do_method(CanvasItemEditor::get_singleton(), "update_viewport"); + undo_redo->add_undo_method(CanvasItemEditor::get_singleton(), "update_viewport"); + undo_redo->commit_action(); +} + +void Polygon2DEditor::_set_use_snap(bool p_use) { + use_snap = p_use; + EditorSettings::get_singleton()->set_project_metadata("polygon_2d_uv_editor", "snap_enabled", p_use); +} + +void Polygon2DEditor::_set_show_grid(bool p_show) { + snap_show_grid = p_show; + EditorSettings::get_singleton()->set_project_metadata("polygon_2d_uv_editor", "show_grid", p_show); + uv_edit_draw->update(); +} + +void Polygon2DEditor::_set_snap_off_x(float p_val) { + snap_offset.x = p_val; + EditorSettings::get_singleton()->set_project_metadata("polygon_2d_uv_editor", "snap_offset", snap_offset); + uv_edit_draw->update(); +} + +void Polygon2DEditor::_set_snap_off_y(float p_val) { + snap_offset.y = p_val; + EditorSettings::get_singleton()->set_project_metadata("polygon_2d_uv_editor", "snap_offset", snap_offset); + uv_edit_draw->update(); +} + +void Polygon2DEditor::_set_snap_step_x(float p_val) { + snap_step.x = p_val; + EditorSettings::get_singleton()->set_project_metadata("polygon_2d_uv_editor", "snap_step", snap_step); + uv_edit_draw->update(); +} + +void Polygon2DEditor::_set_snap_step_y(float p_val) { + snap_step.y = p_val; + EditorSettings::get_singleton()->set_project_metadata("polygon_2d_uv_editor", "snap_step", snap_step); + uv_edit_draw->update(); +} + +void Polygon2DEditor::_uv_mode(int p_mode) { + polygon_create.clear(); + uv_drag = false; + uv_create = false; + + uv_mode = UVMode(p_mode); + for (int i = 0; i < UV_MODE_MAX; i++) { + uv_button[i]->set_pressed(p_mode == i); + } +} + +void Polygon2DEditor::_uv_input(const Ref &p_input) { + if (!_get_node()) { + return; + } + + Transform2D mtx; + mtx.columns[2] = -uv_draw_ofs; + mtx.scale_basis(Vector2(uv_draw_zoom, uv_draw_zoom)); + + Ref mb = p_input; + + if (mb.is_valid()) { + if (mb->get_button_index() == BUTTON_LEFT) { + if (mb->is_pressed()) { + uv_drag_from = snap_point(Vector2(mb->get_position().x, mb->get_position().y)); + uv_drag = true; + points_prev = node->get_uv(); + + if (uv_edit_mode[0]->is_pressed()) { //edit uv + points_prev = node->get_uv(); + } else { //edit polygon + points_prev = node->get_polygon(); + } + + uv_move_current = uv_mode; + if (uv_move_current == UV_MODE_CREATE) { + if (!uv_create) { + points_prev.resize(0); + Vector2 tuv = mtx.affine_inverse().xform(snap_point(Vector2(mb->get_position().x, mb->get_position().y))); + points_prev.push_back(tuv); + uv_create_to = tuv; + point_drag_index = 0; + uv_drag_from = tuv; + uv_drag = true; + uv_create = true; + uv_create_uv_prev = node->get_uv(); + uv_create_poly_prev = node->get_polygon(); + uv_create_prev_internal_vertices = node->get_internal_vertex_count(); + uv_create_colors_prev = node->get_vertex_colors(); + uv_create_bones_prev = node->call("_get_bones"); + polygons_prev = node->get_polygons(); + disable_polygon_editing(false, String()); + node->set_polygon(points_prev); + node->set_uv(points_prev); + node->set_internal_vertex_count(0); + + uv_edit_draw->update(); + } else { + Vector2 tuv = mtx.affine_inverse().xform(snap_point(Vector2(mb->get_position().x, mb->get_position().y))); + + // Close the polygon if selected point is near start. Threshold for closing scaled by zoom level + if (points_prev.size() > 2 && tuv.distance_to(points_prev[0]) < (8 / uv_draw_zoom)) { + undo_redo->create_action(TTR("Create Polygon & UV")); + undo_redo->add_do_method(node, "set_uv", node->get_uv()); + undo_redo->add_undo_method(node, "set_uv", uv_create_uv_prev); + undo_redo->add_do_method(node, "set_polygon", node->get_polygon()); + undo_redo->add_undo_method(node, "set_polygon", uv_create_poly_prev); + undo_redo->add_do_method(node, "set_internal_vertex_count", 0); + undo_redo->add_undo_method(node, "set_internal_vertex_count", uv_create_prev_internal_vertices); + undo_redo->add_do_method(node, "set_vertex_colors", Vector()); + undo_redo->add_undo_method(node, "set_vertex_colors", uv_create_colors_prev); + undo_redo->add_do_method(node, "clear_bones"); + undo_redo->add_undo_method(node, "_set_bones", uv_create_bones_prev); + undo_redo->add_do_method(this, "_update_polygon_editing_state"); + undo_redo->add_undo_method(this, "_update_polygon_editing_state"); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + uv_drag = false; + uv_create = false; + + _uv_mode(UV_MODE_EDIT_POINT); + _menu_option(MODE_EDIT); + } else { + points_prev.push_back(tuv); + point_drag_index = points_prev.size() - 1; + uv_drag_from = tuv; + } + node->set_polygon(points_prev); + node->set_uv(points_prev); + } + + CanvasItemEditor::get_singleton()->update_viewport(); + } + + if (uv_move_current == UV_MODE_CREATE_INTERNAL) { + uv_create_uv_prev = node->get_uv(); + uv_create_poly_prev = node->get_polygon(); + uv_create_colors_prev = node->get_vertex_colors(); + uv_create_bones_prev = node->call("_get_bones"); + int internal_vertices = node->get_internal_vertex_count(); + + Vector2 pos = mtx.affine_inverse().xform(snap_point(Vector2(mb->get_position().x, mb->get_position().y))); + + uv_create_poly_prev.push_back(pos); + uv_create_uv_prev.push_back(pos); + if (uv_create_colors_prev.size()) { + uv_create_colors_prev.push_back(Color(1, 1, 1)); + } + + undo_redo->create_action(TTR("Create Internal Vertex")); + undo_redo->add_do_method(node, "set_uv", uv_create_uv_prev); + undo_redo->add_undo_method(node, "set_uv", node->get_uv()); + undo_redo->add_do_method(node, "set_polygon", uv_create_poly_prev); + undo_redo->add_undo_method(node, "set_polygon", node->get_polygon()); + undo_redo->add_do_method(node, "set_vertex_colors", uv_create_colors_prev); + undo_redo->add_undo_method(node, "set_vertex_colors", node->get_vertex_colors()); + for (int i = 0; i < node->get_bone_count(); i++) { + PoolVector bonew = node->get_bone_weights(i); + bonew.push_back(0); + undo_redo->add_do_method(node, "set_bone_weights", i, bonew); + undo_redo->add_undo_method(node, "set_bone_weights", i, node->get_bone_weights(i)); + } + undo_redo->add_do_method(node, "set_internal_vertex_count", internal_vertices + 1); + undo_redo->add_undo_method(node, "set_internal_vertex_count", internal_vertices); + undo_redo->add_do_method(this, "_update_polygon_editing_state"); + undo_redo->add_undo_method(this, "_update_polygon_editing_state"); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + } + + if (uv_move_current == UV_MODE_REMOVE_INTERNAL) { + uv_create_uv_prev = node->get_uv(); + uv_create_poly_prev = node->get_polygon(); + uv_create_colors_prev = node->get_vertex_colors(); + uv_create_bones_prev = node->call("_get_bones"); + int internal_vertices = node->get_internal_vertex_count(); + + if (internal_vertices <= 0) { + return; + } + + int closest = -1; + float closest_dist = 1e20; + + for (int i = points_prev.size() - internal_vertices; i < points_prev.size(); i++) { + Vector2 tuv = mtx.xform(uv_create_poly_prev[i]); + float dist = tuv.distance_to(Vector2(mb->get_position().x, mb->get_position().y)); + if (dist < 8 && dist < closest_dist) { + closest = i; + closest_dist = dist; + } + } + + if (closest == -1) { + return; + } + + uv_create_poly_prev.remove(closest); + uv_create_uv_prev.remove(closest); + if (uv_create_colors_prev.size()) { + uv_create_colors_prev.remove(closest); + } + + undo_redo->create_action(TTR("Remove Internal Vertex")); + undo_redo->add_do_method(node, "set_uv", uv_create_uv_prev); + undo_redo->add_undo_method(node, "set_uv", node->get_uv()); + undo_redo->add_do_method(node, "set_polygon", uv_create_poly_prev); + undo_redo->add_undo_method(node, "set_polygon", node->get_polygon()); + undo_redo->add_do_method(node, "set_vertex_colors", uv_create_colors_prev); + undo_redo->add_undo_method(node, "set_vertex_colors", node->get_vertex_colors()); + for (int i = 0; i < node->get_bone_count(); i++) { + PoolVector bonew = node->get_bone_weights(i); + bonew.remove(closest); + undo_redo->add_do_method(node, "set_bone_weights", i, bonew); + undo_redo->add_undo_method(node, "set_bone_weights", i, node->get_bone_weights(i)); + } + undo_redo->add_do_method(node, "set_internal_vertex_count", internal_vertices - 1); + undo_redo->add_undo_method(node, "set_internal_vertex_count", internal_vertices); + undo_redo->add_do_method(this, "_update_polygon_editing_state"); + undo_redo->add_undo_method(this, "_update_polygon_editing_state"); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + } + + if (uv_move_current == UV_MODE_EDIT_POINT) { + if (mb->get_shift() && mb->get_command()) { + uv_move_current = UV_MODE_SCALE; + } else if (mb->get_shift()) { + uv_move_current = UV_MODE_MOVE; + } else if (mb->get_command()) { + uv_move_current = UV_MODE_ROTATE; + } + } + + if (uv_move_current == UV_MODE_EDIT_POINT) { + point_drag_index = -1; + for (int i = 0; i < points_prev.size(); i++) { + Vector2 tuv = mtx.xform(points_prev[i]); + if (tuv.distance_to(Vector2(mb->get_position().x, mb->get_position().y)) < 8) { + uv_drag_from = tuv; + point_drag_index = i; + } + } + + if (point_drag_index == -1) { + uv_drag = false; + } + } + + if (uv_move_current == UV_MODE_ADD_POLYGON) { + int closest = -1; + float closest_dist = 1e20; + + for (int i = 0; i < points_prev.size(); i++) { + Vector2 tuv = mtx.xform(points_prev[i]); + float dist = tuv.distance_to(Vector2(mb->get_position().x, mb->get_position().y)); + if (dist < 8 && dist < closest_dist) { + closest = i; + closest_dist = dist; + } + } + + if (closest != -1) { + if (polygon_create.size() && closest == polygon_create[0]) { + //close + if (polygon_create.size() < 3) { + error->set_text(TTR("Invalid Polygon (need 3 different vertices)")); + error->popup_centered_minsize(); + } else { + Array polygons = node->get_polygons(); + polygons = polygons.duplicate(); //copy because its a reference + + //todo, could check whether it already exists? + polygons.push_back(polygon_create); + undo_redo->create_action(TTR("Add Custom Polygon")); + undo_redo->add_do_method(node, "set_polygons", polygons); + undo_redo->add_undo_method(node, "set_polygons", node->get_polygons()); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + } + + polygon_create.clear(); + } else if (polygon_create.find(closest) == -1) { + //add temporarily if not exists + polygon_create.push_back(closest); + } + } + } + + if (uv_move_current == UV_MODE_REMOVE_POLYGON) { + Array polygons = node->get_polygons(); + polygons = polygons.duplicate(); //copy because its a reference + + int erase_index = -1; + for (int i = polygons.size() - 1; i >= 0; i--) { + PoolVector points = polygons[i]; + Vector polys; + polys.resize(points.size()); + for (int j = 0; j < polys.size(); j++) { + int idx = points[j]; + if (idx < 0 || idx >= points_prev.size()) { + continue; + } + polys.write[j] = mtx.xform(points_prev[idx]); + } + + if (Geometry::is_point_in_polygon(Vector2(mb->get_position().x, mb->get_position().y), polys)) { + erase_index = i; + break; + } + } + + if (erase_index != -1) { + polygons.remove(erase_index); + undo_redo->create_action(TTR("Remove Custom Polygon")); + undo_redo->add_do_method(node, "set_polygons", polygons); + undo_redo->add_undo_method(node, "set_polygons", node->get_polygons()); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + } + } + + if (uv_move_current == UV_MODE_PAINT_WEIGHT || uv_move_current == UV_MODE_CLEAR_WEIGHT) { + int bone_selected = -1; + for (int i = 0; i < bone_scroll_vb->get_child_count(); i++) { + CheckBox *c = Object::cast_to(bone_scroll_vb->get_child(i)); + if (c && c->is_pressed()) { + bone_selected = i; + break; + } + } + + if (bone_selected != -1 && node->get_bone_weights(bone_selected).size() == points_prev.size()) { + prev_weights = node->get_bone_weights(bone_selected); + bone_painting = true; + bone_painting_bone = bone_selected; + } + } + } else { + if (uv_drag && !uv_create) { + if (uv_edit_mode[0]->is_pressed()) { // Edit UV. + undo_redo->create_action(TTR("Transform UV Map")); + undo_redo->add_do_method(node, "set_uv", node->get_uv()); + undo_redo->add_undo_method(node, "set_uv", points_prev); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + } else if (uv_edit_mode[1]->is_pressed() && uv_move_current == UV_MODE_EDIT_POINT) { // Edit polygon. + undo_redo->create_action(TTR("Transform Polygon")); + undo_redo->add_do_method(node, "set_polygon", node->get_polygon()); + undo_redo->add_undo_method(node, "set_polygon", points_prev); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + } + + uv_drag = false; + } + + if (bone_painting) { + undo_redo->create_action(TTR("Paint Bone Weights")); + undo_redo->add_do_method(node, "set_bone_weights", bone_painting_bone, node->get_bone_weights(bone_painting_bone)); + undo_redo->add_undo_method(node, "set_bone_weights", bone_painting_bone, prev_weights); + undo_redo->add_do_method(uv_edit_draw, "update"); + undo_redo->add_undo_method(uv_edit_draw, "update"); + undo_redo->commit_action(); + bone_painting = false; + } + } + } else if (mb->get_button_index() == BUTTON_RIGHT && mb->is_pressed()) { + _cancel_editing(); + + if (bone_painting) { + node->set_bone_weights(bone_painting_bone, prev_weights); + } + + uv_edit_draw->update(); + + } else if (mb->get_button_index() == BUTTON_WHEEL_UP && mb->is_pressed()) { + uv_zoom->set_value(uv_zoom->get_value() / (1 - (0.1 * mb->get_factor()))); + } else if (mb->get_button_index() == BUTTON_WHEEL_DOWN && mb->is_pressed()) { + uv_zoom->set_value(uv_zoom->get_value() * (1 - (0.1 * mb->get_factor()))); + } + } + + Ref mm = p_input; + + if (mm.is_valid()) { + if ((mm->get_button_mask() & BUTTON_MASK_MIDDLE) || Input::get_singleton()->is_key_pressed(KEY_SPACE)) { + Vector2 drag(mm->get_relative().x, mm->get_relative().y); + uv_hscroll->set_value(uv_hscroll->get_value() - drag.x); + uv_vscroll->set_value(uv_vscroll->get_value() - drag.y); + + } else if (uv_drag) { + Vector2 uv_drag_to = mm->get_position(); + uv_drag_to = snap_point(uv_drag_to); // FIXME: Only works correctly with 'UV_MODE_EDIT_POINT', it's imprecise with the rest. + Vector2 drag = mtx.affine_inverse().xform(uv_drag_to) - mtx.affine_inverse().xform(uv_drag_from); + + switch (uv_move_current) { + case UV_MODE_CREATE: { + if (uv_create) { + uv_create_to = mtx.affine_inverse().xform(snap_point(Vector2(mm->get_position().x, mm->get_position().y))); + } + } break; + case UV_MODE_EDIT_POINT: { + PoolVector uv_new = points_prev; + uv_new.set(point_drag_index, uv_new[point_drag_index] + drag); + + if (uv_edit_mode[0]->is_pressed()) { //edit uv + node->set_uv(uv_new); + } else if (uv_edit_mode[1]->is_pressed()) { //edit polygon + node->set_polygon(uv_new); + } + } break; + case UV_MODE_MOVE: { + PoolVector uv_new = points_prev; + for (int i = 0; i < uv_new.size(); i++) { + uv_new.set(i, uv_new[i] + drag); + } + + if (uv_edit_mode[0]->is_pressed()) { //edit uv + node->set_uv(uv_new); + } else if (uv_edit_mode[1]->is_pressed()) { //edit polygon + node->set_polygon(uv_new); + } + } break; + case UV_MODE_ROTATE: { + Vector2 center; + PoolVector uv_new = points_prev; + + for (int i = 0; i < uv_new.size(); i++) { + center += points_prev[i]; + } + center /= uv_new.size(); + + float angle = (uv_drag_from - mtx.xform(center)).normalized().angle_to((uv_drag_to - mtx.xform(center)).normalized()); + + for (int i = 0; i < uv_new.size(); i++) { + Vector2 rel = points_prev[i] - center; + rel = rel.rotated(angle); + uv_new.set(i, center + rel); + } + + if (uv_edit_mode[0]->is_pressed()) { //edit uv + node->set_uv(uv_new); + } else if (uv_edit_mode[1]->is_pressed()) { //edit polygon + node->set_polygon(uv_new); + } + } break; + case UV_MODE_SCALE: { + Vector2 center; + PoolVector uv_new = points_prev; + + for (int i = 0; i < uv_new.size(); i++) { + center += points_prev[i]; + } + center /= uv_new.size(); + + float from_dist = uv_drag_from.distance_to(mtx.xform(center)); + float to_dist = uv_drag_to.distance_to(mtx.xform(center)); + if (from_dist < 2) { + break; + } + + float scale = to_dist / from_dist; + + for (int i = 0; i < uv_new.size(); i++) { + Vector2 rel = points_prev[i] - center; + rel = rel * scale; + uv_new.set(i, center + rel); + } + + if (uv_edit_mode[0]->is_pressed()) { //edit uv + node->set_uv(uv_new); + } else if (uv_edit_mode[1]->is_pressed()) { //edit polygon + node->set_polygon(uv_new); + } + } break; + case UV_MODE_PAINT_WEIGHT: + case UV_MODE_CLEAR_WEIGHT: { + bone_paint_pos = Vector2(mm->get_position().x, mm->get_position().y); + } break; + default: { + } + } + + if (bone_painting) { + PoolVector painted_weights = node->get_bone_weights(bone_painting_bone); + + { + int pc = painted_weights.size(); + float amount = bone_paint_strength->get_value(); + float radius = bone_paint_radius->get_value() * EDSCALE; + + if (uv_mode == UV_MODE_CLEAR_WEIGHT) { + amount = -amount; + } + + PoolVector::Write w = painted_weights.write(); + PoolVector::Read r = prev_weights.read(); + PoolVector::Read rv = points_prev.read(); + + for (int i = 0; i < pc; i++) { + if (mtx.xform(rv[i]).distance_to(bone_paint_pos) < radius) { + w[i] = CLAMP(r[i] + amount, 0, 1); + } + } + } + + node->set_bone_weights(bone_painting_bone, painted_weights); + } + + uv_edit_draw->update(); + CanvasItemEditor::get_singleton()->update_viewport(); + } else if (polygon_create.size()) { + uv_create_to = mtx.affine_inverse().xform(Vector2(mm->get_position().x, mm->get_position().y)); + uv_edit_draw->update(); + } else if (uv_mode == UV_MODE_PAINT_WEIGHT || uv_mode == UV_MODE_CLEAR_WEIGHT) { + bone_paint_pos = Vector2(mm->get_position().x, mm->get_position().y); + uv_edit_draw->update(); + } + } + + Ref magnify_gesture = p_input; + if (magnify_gesture.is_valid()) { + uv_zoom->set_value(uv_zoom->get_value() * magnify_gesture->get_factor()); + } + + Ref pan_gesture = p_input; + if (pan_gesture.is_valid()) { + uv_hscroll->set_value(uv_hscroll->get_value() + uv_hscroll->get_page() * pan_gesture->get_delta().x / 8); + uv_vscroll->set_value(uv_vscroll->get_value() + uv_vscroll->get_page() * pan_gesture->get_delta().y / 8); + } +} + +void Polygon2DEditor::_uv_scroll_changed(float) { + if (updating_uv_scroll) { + return; + } + + uv_draw_ofs.x = uv_hscroll->get_value(); + uv_draw_ofs.y = uv_vscroll->get_value(); + uv_draw_zoom = uv_zoom->get_value(); + uv_edit_draw->update(); +} + +void Polygon2DEditor::_uv_draw() { + if (!uv_edit->is_visible() || !_get_node()) { + return; + } + + Ref base_tex = node->get_texture(); + if (base_tex.is_null()) { + return; + } + + String warning; + + Transform2D mtx; + mtx.columns[2] = -uv_draw_ofs; + mtx.scale_basis(Vector2(uv_draw_zoom, uv_draw_zoom)); + + RS::get_singleton()->canvas_item_add_set_transform(uv_edit_draw->get_canvas_item(), mtx); + uv_edit_draw->draw_texture(base_tex, Point2()); + RS::get_singleton()->canvas_item_add_set_transform(uv_edit_draw->get_canvas_item(), Transform2D()); + + if (snap_show_grid) { + Color grid_color = Color(1.0, 1.0, 1.0, 0.15); + Size2 s = uv_edit_draw->get_size(); + int last_cell = 0; + + if (snap_step.x != 0) { + for (int i = 0; i < s.width; i++) { + int cell = Math::fast_ftoi(Math::floor((mtx.affine_inverse().xform(Vector2(i, 0)).x - snap_offset.x) / snap_step.x)); + if (i == 0) { + last_cell = cell; + } + if (last_cell != cell) { + uv_edit_draw->draw_line(Point2(i, 0), Point2(i, s.height), grid_color, Math::round(EDSCALE)); + } + last_cell = cell; + } + } + + if (snap_step.y != 0) { + for (int i = 0; i < s.height; i++) { + int cell = Math::fast_ftoi(Math::floor((mtx.affine_inverse().xform(Vector2(0, i)).y - snap_offset.y) / snap_step.y)); + if (i == 0) { + last_cell = cell; + } + if (last_cell != cell) { + uv_edit_draw->draw_line(Point2(0, i), Point2(s.width, i), grid_color, Math::round(EDSCALE)); + } + last_cell = cell; + } + } + } + + Array polygons = node->get_polygons(); + + PoolVector uvs; + if (uv_edit_mode[0]->is_pressed()) { //edit uv + uvs = node->get_uv(); + } else { //edit polygon + uvs = node->get_polygon(); + } + + PoolVector::Read weight_r; + + if (uv_edit_mode[3]->is_pressed()) { + int bone_selected = -1; + for (int i = 0; i < bone_scroll_vb->get_child_count(); i++) { + CheckBox *c = Object::cast_to(bone_scroll_vb->get_child(i)); + if (c && c->is_pressed()) { + bone_selected = i; + break; + } + } + + if (bone_selected != -1 && node->get_bone_weights(bone_selected).size() == uvs.size()) { + weight_r = node->get_bone_weights(bone_selected).read(); + } + } + + // All UV points are sharp, so use the sharp handle icon + Ref handle = get_theme_icon("EditorPathSharpHandle", "EditorIcons"); + + Color poly_line_color = Color(0.9, 0.5, 0.5); + if (polygons.size() || polygon_create.size()) { + poly_line_color.a *= 0.25; + } + Color polygon_line_color = Color(0.5, 0.5, 0.9); + Vector polygon_fill_color; + { + Color pf = polygon_line_color; + pf.a *= 0.5; + polygon_fill_color.push_back(pf); + } + Color prev_color = Color(0.5, 0.5, 0.5); + Rect2 rect; + + int uv_draw_max = uvs.size(); + + uv_draw_max -= node->get_internal_vertex_count(); + if (uv_draw_max < 0) { + uv_draw_max = 0; + } + + for (int i = 0; i < uvs.size(); i++) { + int next = uv_draw_max > 0 ? (i + 1) % uv_draw_max : 0; + + if (i < uv_draw_max && uv_drag && uv_move_current == UV_MODE_EDIT_POINT && EDITOR_DEF("editors/poly_editor/show_previous_outline", true)) { + uv_edit_draw->draw_line(mtx.xform(points_prev[i]), mtx.xform(points_prev[next]), prev_color, Math::round(EDSCALE), true); + } + + Vector2 next_point = uvs[next]; + if (uv_create && i == uvs.size() - 1) { + next_point = uv_create_to; + } + if (i < uv_draw_max /*&& polygons.size() == 0 && polygon_create.size() == 0*/) { //if using or creating polygons, do not show outline (will show polygons instead) + uv_edit_draw->draw_line(mtx.xform(uvs[i]), mtx.xform(next_point), poly_line_color, Math::round(EDSCALE), true); + } + + rect.expand_to(mtx.basis_xform(uvs[i])); + } + + for (int i = 0; i < polygons.size(); i++) { + PoolVector points = polygons[i]; + Vector polypoints; + for (int j = 0; j < points.size(); j++) { + int next = (j + 1) % points.size(); + + int idx = points[j]; + int idx_next = points[next]; + if (idx < 0 || idx >= uvs.size()) { + continue; + } + polypoints.push_back(mtx.xform(uvs[idx])); + + if (idx_next < 0 || idx_next >= uvs.size()) { + continue; + } + uv_edit_draw->draw_line(mtx.xform(uvs[idx]), mtx.xform(uvs[idx_next]), polygon_line_color, Math::round(EDSCALE), true); + } + if (points.size() >= 3) { + uv_edit_draw->draw_polygon(polypoints, polygon_fill_color); + } + } + + for (int i = 0; i < uvs.size(); i++) { + if (weight_r.ptr()) { + Vector2 draw_pos = mtx.xform(uvs[i]); + float weight = weight_r[i]; + uv_edit_draw->draw_rect(Rect2(draw_pos - Vector2(2, 2) * EDSCALE, Vector2(5, 5) * EDSCALE), Color(weight, weight, weight, 1.0), Math::round(EDSCALE)); + } else { + if (i < uv_draw_max) { + uv_edit_draw->draw_texture(handle, mtx.xform(uvs[i]) - handle->get_size() * 0.5); + } else { + // Internal vertex + uv_edit_draw->draw_texture(handle, mtx.xform(uvs[i]) - handle->get_size() * 0.5, Color(0.6, 0.8, 1)); + } + } + } + + if (polygon_create.size()) { + for (int i = 0; i < polygon_create.size(); i++) { + Vector2 from = uvs[polygon_create[i]]; + Vector2 to = (i + 1) < polygon_create.size() ? uvs[polygon_create[i + 1]] : uv_create_to; + uv_edit_draw->draw_line(mtx.xform(from), mtx.xform(to), polygon_line_color, Math::round(EDSCALE), true); + } + } + +#ifdef MODULE_SKELETON_2D_ENABLED + if (uv_mode == UV_MODE_PAINT_WEIGHT || uv_mode == UV_MODE_CLEAR_WEIGHT) { + NodePath bone_path; + for (int i = 0; i < bone_scroll_vb->get_child_count(); i++) { + CheckBox *c = Object::cast_to(bone_scroll_vb->get_child(i)); + if (c && c->is_pressed()) { + bone_path = node->get_bone_path(i); + break; + } + } + + //draw skeleton + NodePath skeleton_path = node->get_skeleton(); + if (node->has_node(skeleton_path)) { + Skeleton2D *skeleton = Object::cast_to(node->get_node(skeleton_path)); + if (skeleton) { + for (int i = 0; i < skeleton->get_bone_count(); i++) { + Bone2D *bone = skeleton->get_bone(i); + if (bone->get_rest() == Transform2D(0, 0, 0, 0, 0, 0)) { + continue; //not set + } + + bool current = bone_path == skeleton->get_path_to(bone); + + bool found_child = false; + + for (int j = 0; j < bone->get_child_count(); j++) { + Bone2D *n = Object::cast_to(bone->get_child(j)); + if (!n) { + continue; + } + + found_child = true; + + Transform2D bone_xform = node->get_global_transform().affine_inverse() * (skeleton->get_global_transform() * bone->get_skeleton_rest()); + Transform2D endpoint_xform = bone_xform * n->get_transform(); + + Color color = current ? Color(1, 1, 1) : Color(0.5, 0.5, 0.5); + uv_edit_draw->draw_line(mtx.xform(bone_xform.get_origin()), mtx.xform(endpoint_xform.get_origin()), Color(0, 0, 0), Math::round((current ? 5 : 4) * EDSCALE)); + uv_edit_draw->draw_line(mtx.xform(bone_xform.get_origin()), mtx.xform(endpoint_xform.get_origin()), color, Math::round((current ? 3 : 2) * EDSCALE)); + } + + if (!found_child) { + //draw normally + Transform2D bone_xform = node->get_global_transform().affine_inverse() * (skeleton->get_global_transform() * bone->get_skeleton_rest()); + Transform2D endpoint_xform = bone_xform * Transform2D(0, Vector2(bone->get_length(), 0)); + + Color color = current ? Color(1, 1, 1) : Color(0.5, 0.5, 0.5); + uv_edit_draw->draw_line(mtx.xform(bone_xform.get_origin()), mtx.xform(endpoint_xform.get_origin()), Color(0, 0, 0), Math::round((current ? 5 : 4) * EDSCALE)); + uv_edit_draw->draw_line(mtx.xform(bone_xform.get_origin()), mtx.xform(endpoint_xform.get_origin()), color, Math::round((current ? 3 : 2) * EDSCALE)); + } + } + } + } + + //draw paint circle + uv_edit_draw->draw_circle(bone_paint_pos, bone_paint_radius->get_value() * EDSCALE, Color(1, 1, 1, 0.1)); + } +#endif + + rect.position -= uv_edit_draw->get_size(); + rect.size += uv_edit_draw->get_size() * 2.0; + + updating_uv_scroll = true; + + uv_hscroll->set_min(rect.position.x); + uv_hscroll->set_max(rect.position.x + rect.size.x); + if (ABS(rect.position.x - (rect.position.x + rect.size.x)) <= uv_edit_draw->get_size().x) { + uv_hscroll->hide(); + } else { + uv_hscroll->show(); + uv_hscroll->set_page(uv_edit_draw->get_size().x); + uv_hscroll->set_value(uv_draw_ofs.x); + } + + uv_vscroll->set_min(rect.position.y); + uv_vscroll->set_max(rect.position.y + rect.size.y); + if (ABS(rect.position.y - (rect.position.y + rect.size.y)) <= uv_edit_draw->get_size().y) { + uv_vscroll->hide(); + } else { + uv_vscroll->show(); + uv_vscroll->set_page(uv_edit_draw->get_size().y); + uv_vscroll->set_value(uv_draw_ofs.y); + } + + Size2 hmin = uv_hscroll->get_combined_minimum_size(); + Size2 vmin = uv_vscroll->get_combined_minimum_size(); + + // Avoid scrollbar overlapping. + uv_hscroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, uv_vscroll->is_visible() ? -vmin.width : 0); + uv_vscroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, uv_hscroll->is_visible() ? -hmin.height : 0); + + updating_uv_scroll = false; +} + +void Polygon2DEditor::_bind_methods() { + ClassDB::bind_method(D_METHOD("_uv_mode"), &Polygon2DEditor::_uv_mode); + ClassDB::bind_method(D_METHOD("_uv_draw"), &Polygon2DEditor::_uv_draw); + ClassDB::bind_method(D_METHOD("_uv_input"), &Polygon2DEditor::_uv_input); + ClassDB::bind_method(D_METHOD("_uv_scroll_changed"), &Polygon2DEditor::_uv_scroll_changed); + ClassDB::bind_method(D_METHOD("_set_use_snap"), &Polygon2DEditor::_set_use_snap); + ClassDB::bind_method(D_METHOD("_set_show_grid"), &Polygon2DEditor::_set_show_grid); + ClassDB::bind_method(D_METHOD("_set_snap_off_x"), &Polygon2DEditor::_set_snap_off_x); + ClassDB::bind_method(D_METHOD("_set_snap_off_y"), &Polygon2DEditor::_set_snap_off_y); + ClassDB::bind_method(D_METHOD("_set_snap_step_x"), &Polygon2DEditor::_set_snap_step_x); + ClassDB::bind_method(D_METHOD("_set_snap_step_y"), &Polygon2DEditor::_set_snap_step_y); + ClassDB::bind_method(D_METHOD("_uv_edit_mode_select"), &Polygon2DEditor::_uv_edit_mode_select); + ClassDB::bind_method(D_METHOD("_uv_edit_popup_hide"), &Polygon2DEditor::_uv_edit_popup_hide); + ClassDB::bind_method(D_METHOD("_sync_bones"), &Polygon2DEditor::_sync_bones); + ClassDB::bind_method(D_METHOD("_update_bone_list"), &Polygon2DEditor::_update_bone_list); + ClassDB::bind_method(D_METHOD("_update_polygon_editing_state"), &Polygon2DEditor::_update_polygon_editing_state); + ClassDB::bind_method(D_METHOD("_bone_paint_selected"), &Polygon2DEditor::_bone_paint_selected); +} + +Vector2 Polygon2DEditor::snap_point(Vector2 p_target) const { + if (use_snap) { + p_target.x = Math::snap_scalar(snap_offset.x * uv_draw_zoom - uv_draw_ofs.x, snap_step.x * uv_draw_zoom, p_target.x); + p_target.y = Math::snap_scalar(snap_offset.y * uv_draw_zoom - uv_draw_ofs.y, snap_step.y * uv_draw_zoom, p_target.y); + } + + return p_target; +} + +Polygon2DEditor::Polygon2DEditor(EditorNode *p_editor) : + AbstractPolygon2DEditor(p_editor) { + node = nullptr; + snap_offset = EditorSettings::get_singleton()->get_project_metadata("polygon_2d_uv_editor", "snap_offset", Vector2()); + snap_step = EditorSettings::get_singleton()->get_project_metadata("polygon_2d_uv_editor", "snap_step", Vector2(10, 10)); + use_snap = EditorSettings::get_singleton()->get_project_metadata("polygon_2d_uv_editor", "snap_enabled", false); + snap_show_grid = EditorSettings::get_singleton()->get_project_metadata("polygon_2d_uv_editor", "show_grid", false); + + button_uv = memnew(ToolButton); + add_child(button_uv); + button_uv->set_tooltip(TTR("Open Polygon 2D UV editor.")); + button_uv->connect("pressed", this, "_menu_option", varray(MODE_EDIT_UV)); + + uv_mode = UV_MODE_EDIT_POINT; + uv_edit = memnew(AcceptDialog); + add_child(uv_edit); + uv_edit->set_title(TTR("Polygon 2D UV Editor")); + uv_edit->set_resizable(true); + uv_edit->connect("popup_hide", this, "_uv_edit_popup_hide"); + + VBoxContainer *uv_main_vb = memnew(VBoxContainer); + uv_edit->add_child(uv_main_vb); + HBoxContainer *uv_mode_hb = memnew(HBoxContainer); + + uv_edit_group.instance(); + + uv_edit_mode[0] = memnew(ToolButton); + uv_mode_hb->add_child(uv_edit_mode[0]); + uv_edit_mode[0]->set_toggle_mode(true); + uv_edit_mode[1] = memnew(ToolButton); + uv_mode_hb->add_child(uv_edit_mode[1]); + uv_edit_mode[1]->set_toggle_mode(true); + uv_edit_mode[2] = memnew(ToolButton); + uv_mode_hb->add_child(uv_edit_mode[2]); + uv_edit_mode[2]->set_toggle_mode(true); + uv_edit_mode[3] = memnew(ToolButton); + uv_mode_hb->add_child(uv_edit_mode[3]); + uv_edit_mode[3]->set_toggle_mode(true); + + uv_edit_mode[0]->set_text(TTR("UV")); + uv_edit_mode[0]->set_pressed(true); + uv_edit_mode[1]->set_text(TTR("Points")); + uv_edit_mode[2]->set_text(TTR("Polygons")); + uv_edit_mode[3]->set_text(TTR("Bones")); + + uv_edit_mode[0]->set_button_group(uv_edit_group); + uv_edit_mode[1]->set_button_group(uv_edit_group); + uv_edit_mode[2]->set_button_group(uv_edit_group); + uv_edit_mode[3]->set_button_group(uv_edit_group); + + uv_edit_mode[0]->connect("pressed", this, "_uv_edit_mode_select", varray(0)); + uv_edit_mode[1]->connect("pressed", this, "_uv_edit_mode_select", varray(1)); + uv_edit_mode[2]->connect("pressed", this, "_uv_edit_mode_select", varray(2)); + uv_edit_mode[3]->connect("pressed", this, "_uv_edit_mode_select", varray(3)); + + uv_mode_hb->add_child(memnew(VSeparator)); + + uv_main_vb->add_child(uv_mode_hb); + for (int i = 0; i < UV_MODE_MAX; i++) { + uv_button[i] = memnew(ToolButton); + uv_button[i]->set_toggle_mode(true); + uv_mode_hb->add_child(uv_button[i]); + uv_button[i]->connect("pressed", this, "_uv_mode", varray(i)); + uv_button[i]->set_focus_mode(FOCUS_NONE); + } + + uv_button[UV_MODE_CREATE]->set_tooltip(TTR("Create Polygon")); + uv_button[UV_MODE_CREATE_INTERNAL]->set_tooltip(TTR("Create Internal Vertex")); + uv_button[UV_MODE_REMOVE_INTERNAL]->set_tooltip(TTR("Remove Internal Vertex")); +#ifdef OSX_ENABLED + uv_button[UV_MODE_EDIT_POINT]->set_tooltip(TTR("Move Points") + "\n" + TTR("Command: Rotate") + "\n" + TTR("Shift: Move All") + "\n" + TTR("Shift+Command: Scale")); +#else + uv_button[UV_MODE_EDIT_POINT]->set_tooltip(TTR("Move Points") + "\n" + TTR("Ctrl: Rotate") + "\n" + TTR("Shift: Move All") + "\n" + TTR("Shift+Ctrl: Scale")); +#endif + uv_button[UV_MODE_MOVE]->set_tooltip(TTR("Move Polygon")); + uv_button[UV_MODE_ROTATE]->set_tooltip(TTR("Rotate Polygon")); + uv_button[UV_MODE_SCALE]->set_tooltip(TTR("Scale Polygon")); + uv_button[UV_MODE_ADD_POLYGON]->set_tooltip(TTR("Create a custom polygon. Enables custom polygon rendering.")); + uv_button[UV_MODE_REMOVE_POLYGON]->set_tooltip(TTR("Remove a custom polygon. If none remain, custom polygon rendering is disabled.")); + uv_button[UV_MODE_PAINT_WEIGHT]->set_tooltip(TTR("Paint weights with specified intensity.")); + uv_button[UV_MODE_CLEAR_WEIGHT]->set_tooltip(TTR("Unpaint weights with specified intensity.")); + + uv_button[UV_MODE_CREATE]->hide(); + uv_button[UV_MODE_CREATE_INTERNAL]->hide(); + uv_button[UV_MODE_REMOVE_INTERNAL]->hide(); + uv_button[UV_MODE_ADD_POLYGON]->hide(); + uv_button[UV_MODE_REMOVE_POLYGON]->hide(); + uv_button[UV_MODE_PAINT_WEIGHT]->hide(); + uv_button[UV_MODE_CLEAR_WEIGHT]->hide(); + uv_button[UV_MODE_EDIT_POINT]->set_pressed(true); + + bone_paint_strength = memnew(HSlider); + uv_mode_hb->add_child(bone_paint_strength); + bone_paint_strength->set_custom_minimum_size(Size2(75 * EDSCALE, 0)); + bone_paint_strength->set_v_size_flags(SIZE_SHRINK_CENTER); + bone_paint_strength->set_min(0); + bone_paint_strength->set_max(1); + bone_paint_strength->set_step(0.01); + bone_paint_strength->set_value(0.5); + + bone_paint_radius_label = memnew(Label(TTR("Radius:"))); + uv_mode_hb->add_child(bone_paint_radius_label); + bone_paint_radius = memnew(SpinBox); + uv_mode_hb->add_child(bone_paint_radius); + + bone_paint_strength->hide(); + bone_paint_radius->hide(); + bone_paint_radius_label->hide(); + bone_paint_radius->set_min(1); + bone_paint_radius->set_max(100); + bone_paint_radius->set_step(1); + bone_paint_radius->set_value(32); + + HSplitContainer *uv_main_hsc = memnew(HSplitContainer); + uv_main_vb->add_child(uv_main_hsc); + uv_main_hsc->set_v_size_flags(SIZE_EXPAND_FILL); + uv_edit_draw = memnew(Panel); + uv_main_hsc->add_child(uv_edit_draw); + uv_edit_draw->set_h_size_flags(SIZE_EXPAND_FILL); + uv_edit_draw->set_custom_minimum_size(Size2(200, 200) * EDSCALE); + + Control *space = memnew(Control); + uv_mode_hb->add_child(space); + space->set_h_size_flags(SIZE_EXPAND_FILL); + + uv_menu = memnew(MenuButton); + uv_mode_hb->add_child(uv_menu); + uv_menu->set_text(TTR("Edit")); + uv_menu->get_popup()->add_item(TTR("Copy Polygon to UV"), UVEDIT_POLYGON_TO_UV); + uv_menu->get_popup()->add_item(TTR("Copy UV to Polygon"), UVEDIT_UV_TO_POLYGON); + uv_menu->get_popup()->add_separator(); + uv_menu->get_popup()->add_item(TTR("Clear UV"), UVEDIT_UV_CLEAR); + uv_menu->get_popup()->add_separator(); + uv_menu->get_popup()->add_item(TTR("Grid Settings"), UVEDIT_GRID_SETTINGS); + uv_menu->get_popup()->connect("id_pressed", this, "_menu_option"); + + uv_mode_hb->add_child(memnew(VSeparator)); + + b_snap_enable = memnew(ToolButton); + uv_mode_hb->add_child(b_snap_enable); + b_snap_enable->set_text(TTR("Snap")); + b_snap_enable->set_focus_mode(FOCUS_NONE); + b_snap_enable->set_toggle_mode(true); + b_snap_enable->set_pressed(use_snap); + b_snap_enable->set_tooltip(TTR("Enable Snap")); + b_snap_enable->connect("toggled", this, "_set_use_snap"); + + b_snap_grid = memnew(ToolButton); + uv_mode_hb->add_child(b_snap_grid); + b_snap_grid->set_text(TTR("Grid")); + b_snap_grid->set_focus_mode(FOCUS_NONE); + b_snap_grid->set_toggle_mode(true); + b_snap_grid->set_pressed(snap_show_grid); + b_snap_grid->set_tooltip(TTR("Show Grid")); + b_snap_grid->connect("toggled", this, "_set_show_grid"); + + grid_settings = memnew(AcceptDialog); + grid_settings->set_title(TTR("Configure Grid:")); + add_child(grid_settings); + VBoxContainer *grid_settings_vb = memnew(VBoxContainer); + grid_settings->add_child(grid_settings_vb); + + SpinBox *sb_off_x = memnew(SpinBox); + sb_off_x->set_min(-256); + sb_off_x->set_max(256); + sb_off_x->set_step(1); + sb_off_x->set_value(snap_offset.x); + sb_off_x->set_suffix("px"); + sb_off_x->connect("value_changed", this, "_set_snap_off_x"); + grid_settings_vb->add_margin_child(TTR("Grid Offset X:"), sb_off_x); + + SpinBox *sb_off_y = memnew(SpinBox); + sb_off_y->set_min(-256); + sb_off_y->set_max(256); + sb_off_y->set_step(1); + sb_off_y->set_value(snap_offset.y); + sb_off_y->set_suffix("px"); + sb_off_y->connect("value_changed", this, "_set_snap_off_y"); + grid_settings_vb->add_margin_child(TTR("Grid Offset Y:"), sb_off_y); + + SpinBox *sb_step_x = memnew(SpinBox); + sb_step_x->set_min(-256); + sb_step_x->set_max(256); + sb_step_x->set_step(1); + sb_step_x->set_value(snap_step.x); + sb_step_x->set_suffix("px"); + sb_step_x->connect("value_changed", this, "_set_snap_step_x"); + grid_settings_vb->add_margin_child(TTR("Grid Step X:"), sb_step_x); + + SpinBox *sb_step_y = memnew(SpinBox); + sb_step_y->set_min(-256); + sb_step_y->set_max(256); + sb_step_y->set_step(1); + sb_step_y->set_value(snap_step.y); + sb_step_y->set_suffix("px"); + sb_step_y->connect("value_changed", this, "_set_snap_step_y"); + grid_settings_vb->add_margin_child(TTR("Grid Step Y:"), sb_step_y); + + uv_mode_hb->add_child(memnew(VSeparator)); + uv_icon_zoom = memnew(TextureRect); + uv_icon_zoom->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED); + uv_mode_hb->add_child(uv_icon_zoom); + uv_zoom = memnew(HSlider); + uv_zoom->set_min(0.01); + uv_zoom->set_max(16); + uv_zoom->set_value(1); + uv_zoom->set_step(0.01); + uv_zoom->set_v_size_flags(SIZE_SHRINK_CENTER); + + uv_mode_hb->add_child(uv_zoom); + uv_zoom->set_custom_minimum_size(Size2(80 * EDSCALE, 0)); + uv_zoom_value = memnew(SpinBox); + uv_zoom->share(uv_zoom_value); + uv_zoom_value->set_custom_minimum_size(Size2(50, 0)); + uv_mode_hb->add_child(uv_zoom_value); + uv_zoom->connect("value_changed", this, "_uv_scroll_changed"); + + uv_vscroll = memnew(VScrollBar); + uv_vscroll->set_step(0.001); + uv_edit_draw->add_child(uv_vscroll); + uv_vscroll->connect("value_changed", this, "_uv_scroll_changed"); + uv_hscroll = memnew(HScrollBar); + uv_hscroll->set_step(0.001); + uv_edit_draw->add_child(uv_hscroll); + uv_hscroll->connect("value_changed", this, "_uv_scroll_changed"); + + bone_scroll_main_vb = memnew(VBoxContainer); + bone_scroll_main_vb->hide(); + bone_scroll_main_vb->set_custom_minimum_size(Size2(150 * EDSCALE, 0)); + sync_bones = memnew(Button(TTR("Sync Bones to Polygon"))); + bone_scroll_main_vb->add_child(sync_bones); + sync_bones->set_h_size_flags(0); + sync_bones->connect("pressed", this, "_sync_bones"); + uv_main_hsc->add_child(bone_scroll_main_vb); + bone_scroll = memnew(ScrollContainer); + bone_scroll->set_v_scroll(true); + bone_scroll->set_h_scroll(false); + bone_scroll_main_vb->add_child(bone_scroll); + bone_scroll->set_v_size_flags(SIZE_EXPAND_FILL); + bone_scroll_vb = memnew(VBoxContainer); + bone_scroll->add_child(bone_scroll_vb); + + uv_edit_draw->connect("draw", this, "_uv_draw"); + uv_edit_draw->connect("gui_input", this, "_uv_input"); + uv_draw_zoom = 1.0; + point_drag_index = -1; + uv_drag = false; + uv_create = false; + updating_uv_scroll = false; + bone_painting = false; + + error = memnew(AcceptDialog); + add_child(error); + + uv_edit_draw->set_clip_contents(true); +} + +Polygon2DEditorPlugin::Polygon2DEditorPlugin(EditorNode *p_node) : + AbstractPolygon2DEditorPlugin(p_node, memnew(Polygon2DEditor(p_node)), "Polygon2D") { +} diff --git a/modules/paint/nodes/polygon_2d/editor/polygon_2d_editor_plugin.h b/modules/paint/nodes/polygon_2d/editor/polygon_2d_editor_plugin.h new file mode 100644 index 000000000..072f3c7b7 --- /dev/null +++ b/modules/paint/nodes/polygon_2d/editor/polygon_2d_editor_plugin.h @@ -0,0 +1,198 @@ +#ifndef POLYGON_2D_EDITOR_PLUGIN_H +#define POLYGON_2D_EDITOR_PLUGIN_H +/*************************************************************************/ +/* polygon_2d_editor_plugin.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "editor/plugins/abstract_polygon_2d_editor.h" + +#include "core/variant/array.h" +#include "core/math/color.h" +#include "core/math/vector2.h" +#include "core/object/object.h" +#include "core/containers/pool_vector.h" +#include "core/object/reference.h" +#include "core/containers/vector.h" + +class AcceptDialog; +class Button; +class EditorNode; +class HScrollBar; +class HSlider; +class InputEvent; +class Label; +class MenuButton; +class Node2D; +class Node; +class Panel; +class Polygon2D; +class ScrollContainer; +class SpinBox; +class TextureRect; +class ToolButton; +class VBoxContainer; +class VScrollBar; + +class Polygon2DEditor : public AbstractPolygon2DEditor { + GDCLASS(Polygon2DEditor, AbstractPolygon2DEditor); + + enum Mode { + MODE_EDIT_UV = MODE_CONT, + UVEDIT_POLYGON_TO_UV, + UVEDIT_UV_TO_POLYGON, + UVEDIT_UV_CLEAR, + UVEDIT_GRID_SETTINGS + }; + + enum UVMode { + UV_MODE_CREATE, + UV_MODE_CREATE_INTERNAL, + UV_MODE_REMOVE_INTERNAL, + UV_MODE_EDIT_POINT, + UV_MODE_MOVE, + UV_MODE_ROTATE, + UV_MODE_SCALE, + UV_MODE_ADD_POLYGON, + UV_MODE_REMOVE_POLYGON, + UV_MODE_PAINT_WEIGHT, + UV_MODE_CLEAR_WEIGHT, + UV_MODE_MAX + }; + + ToolButton *uv_edit_mode[4]; + Ref uv_edit_group; + + Polygon2D *node; + + UVMode uv_mode; + AcceptDialog *uv_edit; + ToolButton *uv_button[UV_MODE_MAX]; + ToolButton *b_snap_enable; + ToolButton *b_snap_grid; + Panel *uv_edit_draw; + HSlider *uv_zoom; + SpinBox *uv_zoom_value; + HScrollBar *uv_hscroll; + VScrollBar *uv_vscroll; + MenuButton *uv_menu; + TextureRect *uv_icon_zoom; + + VBoxContainer *bone_scroll_main_vb; + ScrollContainer *bone_scroll; + VBoxContainer *bone_scroll_vb; + Button *sync_bones; + HSlider *bone_paint_strength; + SpinBox *bone_paint_radius; + Label *bone_paint_radius_label; + bool bone_painting; + int bone_painting_bone; + PoolVector prev_weights; + Vector2 bone_paint_pos; + AcceptDialog *grid_settings; + + void _sync_bones(); + void _update_bone_list(); + + Vector2 uv_draw_ofs; + float uv_draw_zoom; + PoolVector points_prev; + PoolVector uv_create_uv_prev; + PoolVector uv_create_poly_prev; + PoolVector uv_create_colors_prev; + int uv_create_prev_internal_vertices; + Array uv_create_bones_prev; + Array polygons_prev; + + Vector2 uv_create_to; + int point_drag_index; + bool uv_drag; + bool uv_create; + Vector polygon_create; + UVMode uv_move_current; + Vector2 uv_drag_from; + bool updating_uv_scroll; + + AcceptDialog *error; + + ToolButton *button_uv; + + bool use_snap; + bool snap_show_grid; + Vector2 snap_offset; + Vector2 snap_step; + + virtual void _menu_option(int p_option); + + void _cancel_editing(); + void _update_polygon_editing_state(); + + void _uv_scroll_changed(float); + void _uv_input(const Ref &p_input); + void _uv_draw(); + void _uv_mode(int p_mode); + + void _set_use_snap(bool p_use); + void _set_show_grid(bool p_show); + void _set_snap_off_x(float p_val); + void _set_snap_off_y(float p_val); + void _set_snap_step_x(float p_val); + void _set_snap_step_y(float p_val); + + void _uv_edit_mode_select(int p_mode); + void _uv_edit_popup_hide(); + void _bone_paint_selected(int p_index); + + int _get_polygon_count() const; + +protected: + virtual Node2D *_get_node() const; + virtual void _set_node(Node *p_polygon); + + virtual Vector2 _get_offset(int p_idx) const; + + virtual bool _has_uv() const { return true; }; + virtual void _commit_action(); + + void _notification(int p_what); + static void _bind_methods(); + + Vector2 snap_point(Vector2 p_target) const; + +public: + Polygon2DEditor(EditorNode *p_editor); +}; + +class Polygon2DEditorPlugin : public AbstractPolygon2DEditorPlugin { + GDCLASS(Polygon2DEditorPlugin, AbstractPolygon2DEditorPlugin); + +public: + Polygon2DEditorPlugin(EditorNode *p_node); +}; + +#endif // POLYGON_2D_EDITOR_PLUGIN_H diff --git a/modules/paint/nodes/polygon_2d/polygon_2d.cpp b/modules/paint/nodes/polygon_2d/polygon_2d.cpp new file mode 100644 index 000000000..51c922e3b --- /dev/null +++ b/modules/paint/nodes/polygon_2d/polygon_2d.cpp @@ -0,0 +1,690 @@ +/*************************************************************************/ +/* polygon_2d.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "polygon_2d.h" + +#include "core/math/geometry.h" + +#include "modules/modules_enabled.gen.h" + +#ifdef MODULE_SKELETON_2D_ENABLED +#include "modules/skeleton_2d/nodes/skeleton_2d.h" +#endif + +#ifdef TOOLS_ENABLED +Dictionary Polygon2D::_edit_get_state() const { + Dictionary state = Node2D::_edit_get_state(); + state["offset"] = offset; + return state; +} + +void Polygon2D::_edit_set_state(const Dictionary &p_state) { + Node2D::_edit_set_state(p_state); + set_offset(p_state["offset"]); +} + +void Polygon2D::_edit_set_pivot(const Point2 &p_pivot) { + set_position(get_transform().xform(p_pivot)); + set_offset(get_offset() - p_pivot); +} + +Point2 Polygon2D::_edit_get_pivot() const { + return Vector2(); +} + +bool Polygon2D::_edit_use_pivot() const { + return true; +} + +Rect2 Polygon2D::_edit_get_rect() const { + if (rect_cache_dirty) { + int l = polygon.size(); + PoolVector::Read r = polygon.read(); + item_rect = Rect2(); + for (int i = 0; i < l; i++) { + Vector2 pos = r[i] + offset; + if (i == 0) { + item_rect.position = pos; + } else { + item_rect.expand_to(pos); + } + } + rect_cache_dirty = false; + } + + return item_rect; +} + +bool Polygon2D::_edit_use_rect() const { + return polygon.size() > 0; +} + +bool Polygon2D::_edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const { + Vector polygon2d = Variant(polygon); + if (internal_vertices > 0) { + polygon2d.resize(polygon2d.size() - internal_vertices); + } + return Geometry::is_point_in_polygon(p_point - get_offset(), polygon2d); +} +#endif + +void Polygon2D::_skeleton_bone_setup_changed() { + update(); +} + +void Polygon2D::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + // Must re-establish any existing links with skeletons on re-entering the tree. + update(); + } break; + case NOTIFICATION_EXIT_TREE: { + // Always detach skeleton when exiting the tree, so skeletons don't inform + // Polygon2Ds outside the tree that they have moved (this would be useless work). + RS::get_singleton()->canvas_item_attach_skeleton(get_canvas_item(), RID()); + } break; + case NOTIFICATION_DRAW: { + if (polygon.size() < 3) { + return; + } + +#ifdef MODULE_SKELETON_2D_ENABLED + Skeleton2D *skeleton_node = nullptr; + if (has_node(skeleton)) { + skeleton_node = Object::cast_to(get_node(skeleton)); + } + + ObjectID new_skeleton_id = 0; + + if (skeleton_node) { + RS::get_singleton()->canvas_item_attach_skeleton(get_canvas_item(), skeleton_node->get_skeleton()); + new_skeleton_id = skeleton_node->get_instance_id(); + + // Sync the offset transform between the Polygon2D and the skeleton. + // This is needed for accurate culling in VisualServer. + Transform2D global_xform_skel = skeleton_node->get_global_transform(); + Transform2D global_xform_poly = get_global_transform(); + + // find the difference + Transform2D global_xform_offset = global_xform_skel.affine_inverse() * global_xform_poly; + RS::get_singleton()->canvas_item_set_skeleton_relative_xform(get_canvas_item(), global_xform_offset); + + } else { + RS::get_singleton()->canvas_item_attach_skeleton(get_canvas_item(), RID()); + } + + if (new_skeleton_id != current_skeleton_id) { + Object *old_skeleton = ObjectDB::get_instance(current_skeleton_id); + if (old_skeleton) { + old_skeleton->disconnect("bone_setup_changed", this, "_skeleton_bone_setup_changed"); + } + + if (skeleton_node) { + skeleton_node->connect("bone_setup_changed", this, "_skeleton_bone_setup_changed"); + } + + current_skeleton_id = new_skeleton_id; + } +#endif + + Vector points; + Vector uvs; + Vector bones; + Vector weights; + + int len = polygon.size(); + if ((invert || polygons.size() == 0) && internal_vertices > 0) { + //if no polygons are around, internal vertices must not be drawn, else let them be + len -= internal_vertices; + } + + if (len <= 0) { + return; + } + points.resize(len); + + { + PoolVector::Read polyr = polygon.read(); + for (int i = 0; i < len; i++) { + points.write[i] = polyr[i] + offset; + } + } + + if (invert) { + Rect2 bounds; + int highest_idx = -1; + float highest_y = -1e20; + float sum = 0; + + for (int i = 0; i < len; i++) { + if (i == 0) { + bounds.position = points[i]; + } else { + bounds.expand_to(points[i]); + } + if (points[i].y > highest_y) { + highest_idx = i; + highest_y = points[i].y; + } + int ni = (i + 1) % len; + sum += (points[ni].x - points[i].x) * (points[ni].y + points[i].y); + } + + bounds = bounds.grow(invert_border); + + Vector2 ep[7] = { + Vector2(points[highest_idx].x, points[highest_idx].y + invert_border), + Vector2(bounds.position + bounds.size), + Vector2(bounds.position + Vector2(bounds.size.x, 0)), + Vector2(bounds.position), + Vector2(bounds.position + Vector2(0, bounds.size.y)), + Vector2(points[highest_idx].x - CMP_EPSILON, points[highest_idx].y + invert_border), + Vector2(points[highest_idx].x - CMP_EPSILON, points[highest_idx].y), + }; + + if (sum > 0) { + SWAP(ep[1], ep[4]); + SWAP(ep[2], ep[3]); + SWAP(ep[5], ep[0]); + SWAP(ep[6], points.write[highest_idx]); + } + + points.resize(points.size() + 7); + for (int i = points.size() - 1; i >= highest_idx + 7; i--) { + points.write[i] = points[i - 7]; + } + + for (int i = 0; i < 7; i++) { + points.write[highest_idx + i + 1] = ep[i]; + } + + len = points.size(); + } + + if (texture.is_valid()) { + Transform2D texmat(tex_rot, tex_ofs); + texmat.scale(tex_scale); + Size2 tex_size = texture->get_size(); + + uvs.resize(len); + + if (points.size() == uv.size()) { + PoolVector::Read uvr = uv.read(); + + for (int i = 0; i < len; i++) { + uvs.write[i] = texmat.xform(uvr[i]) / tex_size; + } + + } else { + for (int i = 0; i < len; i++) { + uvs.write[i] = texmat.xform(points[i]) / tex_size; + } + } + } + +#ifdef MODULE_SKELETON_2D_ENABLED + if (skeleton_node && !invert && bone_weights.size()) { + //a skeleton is set! fill indices and weights + int vc = len; + bones.resize(vc * 4); + weights.resize(vc * 4); + + int *bonesw = bones.ptrw(); + float *weightsw = weights.ptrw(); + + for (int i = 0; i < vc * 4; i++) { + bonesw[i] = 0; + weightsw[i] = 0; + } + + for (int i = 0; i < bone_weights.size(); i++) { + if (bone_weights[i].weights.size() != points.size()) { + continue; //different number of vertices, sorry not using. + } + if (!skeleton_node->has_node(bone_weights[i].path)) { + continue; //node does not exist + } + Bone2D *bone = Object::cast_to(skeleton_node->get_node(bone_weights[i].path)); + if (!bone) { + continue; + } + + int bone_index = bone->get_index_in_skeleton(); + PoolVector::Read r = bone_weights[i].weights.read(); + for (int j = 0; j < vc; j++) { + if (r[j] == 0.0) { + continue; //weight is unpainted, skip + } + //find an index with a weight + for (int k = 0; k < 4; k++) { + if (weightsw[j * 4 + k] < r[j]) { + //this is less than this weight, insert weight! + for (int l = 3; l > k; l--) { + weightsw[j * 4 + l] = weightsw[j * 4 + l - 1]; + bonesw[j * 4 + l] = bonesw[j * 4 + l - 1]; + } + weightsw[j * 4 + k] = r[j]; + bonesw[j * 4 + k] = bone_index; + break; + } + } + } + } + + //normalize the weights + for (int i = 0; i < vc; i++) { + float tw = 0; + for (int j = 0; j < 4; j++) { + tw += weightsw[i * 4 + j]; + } + if (tw == 0) { + continue; //unpainted, do nothing + } + + //normalize + for (int j = 0; j < 4; j++) { + weightsw[i * 4 + j] /= tw; + } + } + } +#endif + + Vector colors; + if (vertex_colors.size() == points.size()) { + colors.resize(len); + PoolVector::Read color_r = vertex_colors.read(); + for (int i = 0; i < len; i++) { + colors.write[i] = color_r[i]; + } + } else { + colors.push_back(color); + } + + // Vector indices = Geometry::triangulate_polygon(points); + // RS::get_singleton()->canvas_item_add_triangle_array(get_canvas_item(), indices, points, colors, uvs, texture.is_valid() ? texture->get_rid() : RID()); + + if (invert || polygons.size() == 0) { + Vector indices = Geometry::triangulate_polygon(points); + if (indices.size()) { + RS::get_singleton()->canvas_item_add_triangle_array(get_canvas_item(), indices, points, colors, uvs, bones, weights, texture.is_valid() ? texture->get_rid() : RID(), -1, RID(), antialiased); + } + } else { + //draw individual polygons + Vector total_indices; + for (int i = 0; i < polygons.size(); i++) { + PoolVector src_indices = polygons[i]; + int ic = src_indices.size(); + if (ic < 3) { + continue; + } + PoolVector::Read r = src_indices.read(); + + Vector tmp_points; + tmp_points.resize(ic); + + for (int j = 0; j < ic; j++) { + int idx = r[j]; + ERR_CONTINUE(idx < 0 || idx >= points.size()); + tmp_points.write[j] = points[r[j]]; + } + Vector indices = Geometry::triangulate_polygon(tmp_points); + int ic2 = indices.size(); + const int *r2 = indices.ptr(); + + int bic = total_indices.size(); + total_indices.resize(bic + ic2); + int *w2 = total_indices.ptrw(); + + for (int j = 0; j < ic2; j++) { + w2[j + bic] = r[r2[j]]; + } + } + + if (total_indices.size()) { + RS::get_singleton()->canvas_item_add_triangle_array(get_canvas_item(), total_indices, points, colors, uvs, bones, weights, texture.is_valid() ? texture->get_rid() : RID(), -1, RID(), antialiased); + } + } + + } break; + } +} + +void Polygon2D::set_polygon(const PoolVector &p_polygon) { + polygon = p_polygon; + rect_cache_dirty = true; + update(); +} + +PoolVector Polygon2D::get_polygon() const { + return polygon; +} + +void Polygon2D::set_internal_vertex_count(int p_count) { + internal_vertices = p_count; +} + +int Polygon2D::get_internal_vertex_count() const { + return internal_vertices; +} + +void Polygon2D::set_uv(const PoolVector &p_uv) { + uv = p_uv; + update(); +} + +PoolVector Polygon2D::get_uv() const { + return uv; +} + +void Polygon2D::set_polygons(const Array &p_polygons) { + polygons = p_polygons; + update(); +} + +Array Polygon2D::get_polygons() const { + return polygons; +} + +void Polygon2D::set_color(const Color &p_color) { + color = p_color; + update(); +} +Color Polygon2D::get_color() const { + return color; +} + +void Polygon2D::set_vertex_colors(const PoolVector &p_colors) { + vertex_colors = p_colors; + update(); +} +PoolVector Polygon2D::get_vertex_colors() const { + return vertex_colors; +} + +void Polygon2D::set_texture(const Ref &p_texture) { + texture = p_texture; + + /*if (texture.is_valid()) { + uint32_t flags=texture->get_flags(); + flags&=~Texture::FLAG_REPEAT; + if (tex_tile) + flags|=Texture::FLAG_REPEAT; + + texture->set_flags(flags); + }*/ + update(); +} +Ref Polygon2D::get_texture() const { + return texture; +} + +void Polygon2D::set_texture_offset(const Vector2 &p_offset) { + tex_ofs = p_offset; + update(); +} +Vector2 Polygon2D::get_texture_offset() const { + return tex_ofs; +} + +void Polygon2D::set_texture_rotation(float p_rot) { + tex_rot = p_rot; + update(); +} +float Polygon2D::get_texture_rotation() const { + return tex_rot; +} + +void Polygon2D::set_texture_rotation_degrees(float p_rot) { + set_texture_rotation(Math::deg2rad(p_rot)); +} +float Polygon2D::get_texture_rotation_degrees() const { + return Math::rad2deg(get_texture_rotation()); +} + +void Polygon2D::set_texture_scale(const Size2 &p_scale) { + tex_scale = p_scale; + update(); +} +Size2 Polygon2D::get_texture_scale() const { + return tex_scale; +} + +void Polygon2D::set_invert(bool p_invert) { + invert = p_invert; + update(); +} +bool Polygon2D::get_invert() const { + return invert; +} + +void Polygon2D::set_antialiased(bool p_antialiased) { + antialiased = p_antialiased; + update(); +} +bool Polygon2D::get_antialiased() const { + return antialiased; +} + +void Polygon2D::set_invert_border(float p_invert_border) { + invert_border = p_invert_border; + update(); +} +float Polygon2D::get_invert_border() const { + return invert_border; +} + +void Polygon2D::set_offset(const Vector2 &p_offset) { + offset = p_offset; + rect_cache_dirty = true; + update(); + _change_notify("offset"); +} + +Vector2 Polygon2D::get_offset() const { + return offset; +} + +void Polygon2D::add_bone(const NodePath &p_path, const PoolVector &p_weights) { + Bone bone; + bone.path = p_path; + bone.weights = p_weights; + bone_weights.push_back(bone); +} +int Polygon2D::get_bone_count() const { + return bone_weights.size(); +} +NodePath Polygon2D::get_bone_path(int p_index) const { + ERR_FAIL_INDEX_V(p_index, bone_weights.size(), NodePath()); + return bone_weights[p_index].path; +} +PoolVector Polygon2D::get_bone_weights(int p_index) const { + ERR_FAIL_INDEX_V(p_index, bone_weights.size(), PoolVector()); + return bone_weights[p_index].weights; +} +void Polygon2D::erase_bone(int p_idx) { + ERR_FAIL_INDEX(p_idx, bone_weights.size()); + bone_weights.remove(p_idx); +} + +void Polygon2D::clear_bones() { + bone_weights.clear(); +} + +void Polygon2D::set_bone_weights(int p_index, const PoolVector &p_weights) { + ERR_FAIL_INDEX(p_index, bone_weights.size()); + bone_weights.write[p_index].weights = p_weights; + update(); +} +void Polygon2D::set_bone_path(int p_index, const NodePath &p_path) { + ERR_FAIL_INDEX(p_index, bone_weights.size()); + bone_weights.write[p_index].path = p_path; + update(); +} + +Array Polygon2D::_get_bones() const { + Array bones; + for (int i = 0; i < get_bone_count(); i++) { + // Convert path property to String to avoid errors due to invalid node path in editor, + // because it's relative to the Skeleton2D node and not Polygon2D. + bones.push_back(String(get_bone_path(i))); + bones.push_back(get_bone_weights(i)); + } + return bones; +} +void Polygon2D::_set_bones(const Array &p_bones) { + ERR_FAIL_COND(p_bones.size() & 1); + clear_bones(); + for (int i = 0; i < p_bones.size(); i += 2) { + // Convert back from String to NodePath. + add_bone(NodePath(p_bones[i].operator String()), p_bones[i + 1]); + } +} + +void Polygon2D::set_skeleton(const NodePath &p_skeleton) { + if (skeleton == p_skeleton) { + return; + } + skeleton = p_skeleton; + update(); +} + +NodePath Polygon2D::get_skeleton() const { + return skeleton; +} + +void Polygon2D::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_polygon", "polygon"), &Polygon2D::set_polygon); + ClassDB::bind_method(D_METHOD("get_polygon"), &Polygon2D::get_polygon); + + ClassDB::bind_method(D_METHOD("set_uv", "uv"), &Polygon2D::set_uv); + ClassDB::bind_method(D_METHOD("get_uv"), &Polygon2D::get_uv); + + ClassDB::bind_method(D_METHOD("set_color", "color"), &Polygon2D::set_color); + ClassDB::bind_method(D_METHOD("get_color"), &Polygon2D::get_color); + + ClassDB::bind_method(D_METHOD("set_polygons", "polygons"), &Polygon2D::set_polygons); + ClassDB::bind_method(D_METHOD("get_polygons"), &Polygon2D::get_polygons); + + ClassDB::bind_method(D_METHOD("set_vertex_colors", "vertex_colors"), &Polygon2D::set_vertex_colors); + ClassDB::bind_method(D_METHOD("get_vertex_colors"), &Polygon2D::get_vertex_colors); + + ClassDB::bind_method(D_METHOD("set_texture", "texture"), &Polygon2D::set_texture); + ClassDB::bind_method(D_METHOD("get_texture"), &Polygon2D::get_texture); + + ClassDB::bind_method(D_METHOD("set_texture_offset", "texture_offset"), &Polygon2D::set_texture_offset); + ClassDB::bind_method(D_METHOD("get_texture_offset"), &Polygon2D::get_texture_offset); + + ClassDB::bind_method(D_METHOD("set_texture_rotation", "texture_rotation"), &Polygon2D::set_texture_rotation); + ClassDB::bind_method(D_METHOD("get_texture_rotation"), &Polygon2D::get_texture_rotation); + + ClassDB::bind_method(D_METHOD("set_texture_rotation_degrees", "texture_rotation"), &Polygon2D::set_texture_rotation_degrees); + ClassDB::bind_method(D_METHOD("get_texture_rotation_degrees"), &Polygon2D::get_texture_rotation_degrees); + + ClassDB::bind_method(D_METHOD("set_texture_scale", "texture_scale"), &Polygon2D::set_texture_scale); + ClassDB::bind_method(D_METHOD("get_texture_scale"), &Polygon2D::get_texture_scale); + + ClassDB::bind_method(D_METHOD("set_invert", "invert"), &Polygon2D::set_invert); + ClassDB::bind_method(D_METHOD("get_invert"), &Polygon2D::get_invert); + + ClassDB::bind_method(D_METHOD("set_antialiased", "antialiased"), &Polygon2D::set_antialiased); + ClassDB::bind_method(D_METHOD("get_antialiased"), &Polygon2D::get_antialiased); + + ClassDB::bind_method(D_METHOD("set_invert_border", "invert_border"), &Polygon2D::set_invert_border); + ClassDB::bind_method(D_METHOD("get_invert_border"), &Polygon2D::get_invert_border); + + ClassDB::bind_method(D_METHOD("set_offset", "offset"), &Polygon2D::set_offset); + ClassDB::bind_method(D_METHOD("get_offset"), &Polygon2D::get_offset); + + ClassDB::bind_method(D_METHOD("add_bone", "path", "weights"), &Polygon2D::add_bone); + ClassDB::bind_method(D_METHOD("get_bone_count"), &Polygon2D::get_bone_count); + ClassDB::bind_method(D_METHOD("get_bone_path", "index"), &Polygon2D::get_bone_path); + ClassDB::bind_method(D_METHOD("get_bone_weights", "index"), &Polygon2D::get_bone_weights); + ClassDB::bind_method(D_METHOD("erase_bone", "index"), &Polygon2D::erase_bone); + ClassDB::bind_method(D_METHOD("clear_bones"), &Polygon2D::clear_bones); + ClassDB::bind_method(D_METHOD("set_bone_path", "index", "path"), &Polygon2D::set_bone_path); + ClassDB::bind_method(D_METHOD("set_bone_weights", "index", "weights"), &Polygon2D::set_bone_weights); + + ClassDB::bind_method(D_METHOD("set_skeleton", "skeleton"), &Polygon2D::set_skeleton); + ClassDB::bind_method(D_METHOD("get_skeleton"), &Polygon2D::get_skeleton); + + ClassDB::bind_method(D_METHOD("set_internal_vertex_count", "internal_vertex_count"), &Polygon2D::set_internal_vertex_count); + ClassDB::bind_method(D_METHOD("get_internal_vertex_count"), &Polygon2D::get_internal_vertex_count); + + ClassDB::bind_method(D_METHOD("_set_bones", "bones"), &Polygon2D::_set_bones); + ClassDB::bind_method(D_METHOD("_get_bones"), &Polygon2D::_get_bones); + + ClassDB::bind_method(D_METHOD("_skeleton_bone_setup_changed"), &Polygon2D::_skeleton_bone_setup_changed); + + ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_color", "get_color"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset"), "set_offset", "get_offset"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "antialiased"), "set_antialiased", "get_antialiased"); + ADD_GROUP("Texture", ""); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture"), "set_texture", "get_texture"); + ADD_GROUP("Texture", "texture_"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "texture_offset"), "set_texture_offset", "get_texture_offset"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "texture_scale", PROPERTY_HINT_LINK), "set_texture_scale", "get_texture_scale"); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "texture_rotation_degrees", PROPERTY_HINT_RANGE, "-360,360,0.1,or_lesser,or_greater"), "set_texture_rotation_degrees", "get_texture_rotation_degrees"); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "texture_rotation", PROPERTY_HINT_NONE, "", 0), "set_texture_rotation", "get_texture_rotation"); + ADD_GROUP("Skeleton", ""); + ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "skeleton", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Skeleton2D"), "set_skeleton", "get_skeleton"); + + ADD_GROUP("Invert", "invert_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "invert_enable"), "set_invert", "get_invert"); + ADD_PROPERTY(PropertyInfo(Variant::REAL, "invert_border", PROPERTY_HINT_RANGE, "0.1,16384,0.1"), "set_invert_border", "get_invert_border"); + + ADD_GROUP("Data", ""); + ADD_PROPERTY(PropertyInfo(Variant::POOL_VECTOR2_ARRAY, "polygon"), "set_polygon", "get_polygon"); + ADD_PROPERTY(PropertyInfo(Variant::POOL_VECTOR2_ARRAY, "uv"), "set_uv", "get_uv"); + ADD_PROPERTY(PropertyInfo(Variant::POOL_COLOR_ARRAY, "vertex_colors"), "set_vertex_colors", "get_vertex_colors"); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "polygons"), "set_polygons", "get_polygons"); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "bones", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR), "_set_bones", "_get_bones"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "internal_vertex_count", PROPERTY_HINT_RANGE, "0,1000"), "set_internal_vertex_count", "get_internal_vertex_count"); +} + +Polygon2D::Polygon2D() { + invert = false; + invert_border = 100; + antialiased = false; + tex_rot = 0; + tex_tile = true; + tex_scale = Vector2(1, 1); + color = Color(1, 1, 1); + rect_cache_dirty = true; + internal_vertices = 0; + current_skeleton_id = 0; +} + +Polygon2D::~Polygon2D() { + // Most definitely don't want to leave references to this deleted canvas item + // in the skeleton. + if (get_canvas_item().is_valid()) { + RS::get_singleton()->canvas_item_attach_skeleton(get_canvas_item(), RID()); + } +} \ No newline at end of file diff --git a/modules/paint/nodes/polygon_2d/polygon_2d.h b/modules/paint/nodes/polygon_2d/polygon_2d.h new file mode 100644 index 000000000..d3af3d916 --- /dev/null +++ b/modules/paint/nodes/polygon_2d/polygon_2d.h @@ -0,0 +1,153 @@ +#ifndef POLYGON_2D_H +#define POLYGON_2D_H + +/*************************************************************************/ +/* polygon_2d.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "scene/2d/node_2d.h" + +class Polygon2D : public Node2D { + GDCLASS(Polygon2D, Node2D); + + PoolVector polygon; + PoolVector uv; + PoolVector vertex_colors; + Array polygons; + int internal_vertices; + + struct Bone { + NodePath path; + PoolVector weights; + }; + + Vector bone_weights; + + Color color; + Ref texture; + Size2 tex_scale; + Vector2 tex_ofs; + bool tex_tile; + float tex_rot; + bool invert; + float invert_border; + bool antialiased; + + Vector2 offset; + mutable bool rect_cache_dirty; + mutable Rect2 item_rect; + + NodePath skeleton; + ObjectID current_skeleton_id; + + Array _get_bones() const; + void _set_bones(const Array &p_bones); + + void _skeleton_bone_setup_changed(); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: +#ifdef TOOLS_ENABLED + virtual Dictionary _edit_get_state() const; + virtual void _edit_set_state(const Dictionary &p_state); + + virtual void _edit_set_pivot(const Point2 &p_pivot); + virtual Point2 _edit_get_pivot() const; + virtual bool _edit_use_pivot() const; + virtual Rect2 _edit_get_rect() const; + virtual bool _edit_use_rect() const; + + virtual bool _edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const; +#endif + + void set_polygon(const PoolVector &p_polygon); + PoolVector get_polygon() const; + + void set_internal_vertex_count(int p_count); + int get_internal_vertex_count() const; + + void set_uv(const PoolVector &p_uv); + PoolVector get_uv() const; + + void set_polygons(const Array &p_polygons); + Array get_polygons() const; + + void set_color(const Color &p_color); + Color get_color() const; + + void set_vertex_colors(const PoolVector &p_colors); + PoolVector get_vertex_colors() const; + + void set_texture(const Ref &p_texture); + Ref get_texture() const; + + void set_texture_offset(const Vector2 &p_offset); + Vector2 get_texture_offset() const; + + void set_texture_rotation(float p_rot); + float get_texture_rotation() const; + + void set_texture_rotation_degrees(float p_rot); + float get_texture_rotation_degrees() const; + + void set_texture_scale(const Size2 &p_scale); + Size2 get_texture_scale() const; + + void set_invert(bool p_invert); + bool get_invert() const; + + void set_antialiased(bool p_antialiased); + bool get_antialiased() const; + + void set_invert_border(float p_invert_border); + float get_invert_border() const; + + void set_offset(const Vector2 &p_offset); + Vector2 get_offset() const; + + void add_bone(const NodePath &p_path = NodePath(), const PoolVector &p_weights = PoolVector()); + int get_bone_count() const; + NodePath get_bone_path(int p_index) const; + PoolVector get_bone_weights(int p_index) const; + void erase_bone(int p_idx); + void clear_bones(); + void set_bone_weights(int p_index, const PoolVector &p_weights); + void set_bone_path(int p_index, const NodePath &p_path); + + void set_skeleton(const NodePath &p_skeleton); + NodePath get_skeleton() const; + + Polygon2D(); + virtual ~Polygon2D(); +}; + +#endif // POLYGON_2D_H