diff --git a/doc/classes/VisualShape2D.xml b/doc/classes/VisualShape2D.xml new file mode 100644 index 000000000000..a12a615423b1 --- /dev/null +++ b/doc/classes/VisualShape2D.xml @@ -0,0 +1,67 @@ + + + + 2D shape-drawing node. + + + [VisualShape2D] is a node that draws common geometric shapes like rectangles, circles, triangles, and capsules. It is very useful to prototype 2D applications, as it needs very little setup without needing a [Texture2D]. If a texture is wanted, [member CanvasItem.clip_children] and a child [Sprite2D] can be used, or the Editor can convert a [VisualShape2D] into a [Polygon2D] or [MeshInstance2D]. + For more advanced capabilities, see [Polygon2D], [Sprite2D], or [MeshInstance2D]. For custom drawing, see [method CanvasItem._draw] and the [CanvasItem] draw methods. + + + + + + + + Returns a list of points representing the vertices of the polygon. + + + + + + Returns a list of UV values for each vertex of the polygon. + + + + + + If [code]true[/code], the shape is antialiased. UVs are not supported on antialiased edges. Scaling may affect antialiasing quality. + [b]Note:[/b] This may cause unintentional artifacts when using transparency. Consider adding this to a [CanvasGroup] and using its [member CanvasItem.self_modulate] property for transparency. Alternatively, consider enabling MSAA ([member ProjectSettings.rendering/anti_aliasing/quality/msaa_2d]). + + + The color of the shape. + + + The offset amount of the shape in pixels. An offset of [code]Vector2(0, 0)[/code] will center the shape. This can be used to move the shape without changing its pivot point. + + + If greater than [code]0[/code], the shape uses an outline instead of being filled. This is the outline width of the shape in pixels. UVs are not supported on outlines. + + + The resolution to use when the [member shape_type] is [constant SHAPE_CIRCLE] or [constant SHAPE_CAPSULE]. This value determines the number of points in the shape. Higher values look smoother but may negatively affect performance. + + + The type of shape to draw. + + + The size of the shape in pixels. + + + + + A square or rectangle shape. + + + A circle or regular polygon shape. When [member resolution] is high, this approximates a circle. This can also be an oval if the [member size] is non-uniform. + + + An equilateral triangle shape, pointing up. + + + A right triangle shape, with its right angle placed on the node's bottom-left corner. + + + A vertical or horizontal capsule shape. The number of points is determined by the [member resolution], rounded up to the next even number, plus two. + + + diff --git a/editor/icons/VisualShape2D.svg b/editor/icons/VisualShape2D.svg new file mode 100644 index 000000000000..d47817639d91 --- /dev/null +++ b/editor/icons/VisualShape2D.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/plugins/visual_shape_2d_editor_plugin.cpp b/editor/plugins/visual_shape_2d_editor_plugin.cpp new file mode 100644 index 000000000000..5bded5410b96 --- /dev/null +++ b/editor/plugins/visual_shape_2d_editor_plugin.cpp @@ -0,0 +1,296 @@ +/**************************************************************************/ +/* visual_shape_2d_editor_plugin.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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 "visual_shape_2d_editor_plugin.h" + +#include "canvas_item_editor_plugin.h" +#include "core/math/geometry_2d.h" +#include "editor/editor_node.h" +#include "editor/editor_undo_redo_manager.h" +#include "editor/gui/editor_toaster.h" +#include "editor/scene_tree_dock.h" +#include "scene/2d/light_occluder_2d.h" +#include "scene/2d/mesh_instance_2d.h" +#include "scene/2d/physics/collision_shape_2d.h" +#include "scene/2d/polygon_2d.h" +#include "scene/2d/visual_shape_2d.h" +#include "scene/gui/menu_button.h" +#include "scene/resources/2d/capsule_shape_2d.h" +#include "scene/resources/2d/circle_shape_2d.h" +#include "scene/resources/2d/convex_polygon_shape_2d.h" +#include "scene/resources/2d/rectangle_shape_2d.h" + +void VisualShape2DEditor::edit(VisualShape2D *p_visual_shape_2d) { + visual_shape_2d = p_visual_shape_2d; +} + +void VisualShape2DEditor::_menu_option(int p_option) { + if (!visual_shape_2d) { + return; + } + + if ((p_option == MENU_OPTION_CONVERT_TO_MESH_2D || p_option == MENU_OPTION_CONVERT_TO_POLYGON_2D) && visual_shape_2d != get_tree()->get_edited_scene_root() && visual_shape_2d->get_owner() != get_tree()->get_edited_scene_root()) { + EditorToaster::get_singleton()->popup_str(TTR("Can't convert a VisualShape from a foreign scene."), EditorToaster::SEVERITY_ERROR); + return; + } + + switch (p_option) { + case MENU_OPTION_CONVERT_TO_MESH_2D: { + _convert_to_mesh_2d_node(); + } break; + case MENU_OPTION_CONVERT_TO_POLYGON_2D: { + _convert_to_polygon_2d_node(); + } break; + case MENU_OPTION_CREATE_COLLISION_SHAPE_2D: { + _create_collision_shape_2d_node(); + } break; + case MENU_OPTION_CREATE_LIGHT_OCCLUDER_2D: { + _create_light_occluder_2d_node(); + } break; + } +} + +void VisualShape2DEditor::_convert_to_mesh_2d_node() { + PackedVector2Array points = visual_shape_2d->get_points(); + if (points.size() < 3) { + EditorToaster::get_singleton()->popup_str(TTR("Invalid geometry, can't replace by mesh."), EditorToaster::SEVERITY_ERROR); + return; + } + + Vector poly = Geometry2D::triangulate_polygon(points); + + Ref mesh; + mesh.instantiate(); + + Array a; + a.resize(Mesh::ARRAY_MAX); + a[Mesh::ARRAY_VERTEX] = points; + a[Mesh::ARRAY_TEX_UV] = visual_shape_2d->get_uvs(); + a[Mesh::ARRAY_INDEX] = poly; + + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, a, Array(), Dictionary(), Mesh::ARRAY_FLAG_USE_2D_VERTICES); + + MeshInstance2D *mesh_instance = memnew(MeshInstance2D); + mesh_instance->set_mesh(mesh); + + EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); + ur->create_action(TTR("Convert to MeshInstance2D"), UndoRedo::MERGE_DISABLE, visual_shape_2d); + SceneTreeDock::get_singleton()->replace_node(visual_shape_2d, mesh_instance); + ur->commit_action(false); +} + +void VisualShape2DEditor::_convert_to_polygon_2d_node() { + PackedVector2Array points = visual_shape_2d->get_points(); + if (points.is_empty()) { + EditorToaster::get_singleton()->popup_str(TTR("Invalid geometry, can't create polygon."), EditorToaster::SEVERITY_ERROR); + return; + } + + int total_point_count = points.size(); + Point2 offset = visual_shape_2d->get_offset(); + + Polygon2D *polygon_2d_instance = memnew(Polygon2D); + + polygon_2d_instance->set_color(visual_shape_2d->get_color()); + polygon_2d_instance->set_offset(offset); + polygon_2d_instance->set_antialiased(visual_shape_2d->is_antialiased()); + + PackedVector2Array vertices; + vertices.resize(total_point_count); + Vector2 *vertices_write = vertices.ptrw(); + + PackedInt32Array index_array; + index_array.resize(total_point_count); + int *index_write = index_array.ptrw(); + + for (int i = 0; i < total_point_count; i++) { + vertices_write[i] = points[i] - offset; + index_write[i] = i; + } + + Array polys; + polys.push_back(index_array); + + PackedVector2Array uvs = Transform2D(0, visual_shape_2d->get_size(), 0, Point2(0, 0)).xform(visual_shape_2d->get_uvs()); + + polygon_2d_instance->set_polygon(vertices); + polygon_2d_instance->set_uv(uvs); + polygon_2d_instance->set_polygons(polys); + + EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); + ur->create_action(TTR("Convert to Polygon2D"), UndoRedo::MERGE_DISABLE, visual_shape_2d); + SceneTreeDock::get_singleton()->replace_node(visual_shape_2d, polygon_2d_instance); + ur->commit_action(false); +} + +void VisualShape2DEditor::_create_collision_shape_2d_node() { + CollisionShape2D *collision_shape_2d_instance = memnew(CollisionShape2D); + Size2 size = visual_shape_2d->get_size(); + + switch (visual_shape_2d->get_shape_type()) { + case VisualShape2D::SHAPE_RECTANGLE: { + Ref shape; + shape.instantiate(); + shape->set_size(size); + collision_shape_2d_instance->set_shape(shape); + collision_shape_2d_instance->translate(visual_shape_2d->get_offset()); + } break; + case VisualShape2D::SHAPE_CIRCLE: { + float semi_major = MAX(size.x, size.y) / 2.0; + float semi_minor = MIN(size.x, size.y) / 2.0; + float difference = (semi_major - semi_minor) / semi_minor; + // If there is more than a 10% difference, treat as an oval. + if (difference > 0.1) { + // Oval. + Ref shape; + shape.instantiate(); + shape->set_points(visual_shape_2d->get_points()); + collision_shape_2d_instance->set_shape(shape); + } else { + // Circle. + Ref shape; + shape.instantiate(); + shape->set_radius(semi_major); + collision_shape_2d_instance->set_shape(shape); + collision_shape_2d_instance->translate(visual_shape_2d->get_offset()); + } + } break; + case VisualShape2D::SHAPE_CAPSULE: { + Ref shape; + shape.instantiate(); + shape->set_radius(MIN(size.x, size.y) / 2); + shape->set_height(MAX(size.x, size.y)); + collision_shape_2d_instance->set_shape(shape); + if (size.x > size.y) { + collision_shape_2d_instance->rotate(Math_PI / 2.0); + } + collision_shape_2d_instance->translate(visual_shape_2d->get_offset()); + } break; + case VisualShape2D::SHAPE_EQUILATERAL_TRIANGLE: + case VisualShape2D::SHAPE_RIGHT_TRIANGLE: { + Ref shape; + shape.instantiate(); + shape->set_points(visual_shape_2d->get_points()); + collision_shape_2d_instance->set_shape(shape); + } break; + } + + EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); + ur->create_action(TTR("Create CollisionShape2D Sibling"), UndoRedo::MERGE_DISABLE, visual_shape_2d); + ur->add_do_method(this, "_add_as_sibling_or_child", visual_shape_2d, collision_shape_2d_instance); + ur->add_do_reference(collision_shape_2d_instance); + ur->add_undo_method(visual_shape_2d != get_tree()->get_edited_scene_root() ? visual_shape_2d->get_parent() : visual_shape_2d, "remove_child", collision_shape_2d_instance); + ur->commit_action(); +} + +void VisualShape2DEditor::_create_light_occluder_2d_node() { + PackedVector2Array points = visual_shape_2d->get_points(); + if (points.is_empty()) { + EditorToaster::get_singleton()->popup_str(TTR("Invalid geometry, can't create light occluder."), EditorToaster::SEVERITY_ERROR); + return; + } + + Ref polygon; + polygon.instantiate(); + polygon->set_polygon(points); + + LightOccluder2D *light_occluder_2d_instance = memnew(LightOccluder2D); + light_occluder_2d_instance->set_occluder_polygon(polygon); + + EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton(); + ur->create_action(TTR("Create LightOccluder2D Sibling"), UndoRedo::MERGE_DISABLE, visual_shape_2d); + ur->add_do_method(this, "_add_as_sibling_or_child", visual_shape_2d, light_occluder_2d_instance); + ur->add_do_reference(light_occluder_2d_instance); + ur->add_undo_method(visual_shape_2d != get_tree()->get_edited_scene_root() ? visual_shape_2d->get_parent() : visual_shape_2d, "remove_child", light_occluder_2d_instance); + ur->commit_action(); +} + +void VisualShape2DEditor::_add_as_sibling_or_child(Node *p_own_node, Node *p_new_node) { + // Can't make sibling if own node is scene root. + if (p_own_node != get_tree()->get_edited_scene_root()) { + p_own_node->get_parent()->add_child(p_new_node, true); + Object::cast_to(p_new_node)->set_transform(Object::cast_to(p_own_node)->get_transform() * Object::cast_to(p_new_node)->get_transform()); + } else { + p_own_node->add_child(p_new_node, true); + } + + p_new_node->set_owner(get_tree()->get_edited_scene_root()); +} + +void VisualShape2DEditor::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + options->set_button_icon(get_editor_theme_icon(SNAME("VisualShape2D"))); + + options->get_popup()->set_item_icon(MENU_OPTION_CONVERT_TO_MESH_2D, get_editor_theme_icon(SNAME("MeshInstance2D"))); + options->get_popup()->set_item_icon(MENU_OPTION_CONVERT_TO_POLYGON_2D, get_editor_theme_icon(SNAME("Polygon2D"))); + options->get_popup()->set_item_icon(MENU_OPTION_CREATE_COLLISION_SHAPE_2D, get_editor_theme_icon(SNAME("CollisionShape2D"))); + options->get_popup()->set_item_icon(MENU_OPTION_CREATE_LIGHT_OCCLUDER_2D, get_editor_theme_icon(SNAME("LightOccluder2D"))); + } break; + } +} + +void VisualShape2DEditor::_bind_methods() { + ClassDB::bind_method("_add_as_sibling_or_child", &VisualShape2DEditor::_add_as_sibling_or_child); +} + +VisualShape2DEditor::VisualShape2DEditor() { + options = memnew(MenuButton); + + CanvasItemEditor::get_singleton()->add_control_to_menu_panel(options); + + options->set_text(TTR("VisualShape2D")); + + options->get_popup()->add_item(TTR("Convert to MeshInstance2D"), MENU_OPTION_CONVERT_TO_MESH_2D); + options->get_popup()->add_item(TTR("Convert to Polygon2D"), MENU_OPTION_CONVERT_TO_POLYGON_2D); + options->get_popup()->add_item(TTR("Create CollisionShape2D Sibling"), MENU_OPTION_CREATE_COLLISION_SHAPE_2D); + options->get_popup()->add_item(TTR("Create LightOccluder2D Sibling"), MENU_OPTION_CREATE_LIGHT_OCCLUDER_2D); + options->set_switch_on_hover(true); + + options->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &VisualShape2DEditor::_menu_option)); +} + +void VisualShape2DEditorPlugin::edit(Object *p_object) { + visual_shape_editor->edit(Object::cast_to(p_object)); +} + +bool VisualShape2DEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("VisualShape2D"); +} + +void VisualShape2DEditorPlugin::make_visible(bool p_visible) { + visual_shape_editor->options->set_visible(p_visible); +} + +VisualShape2DEditorPlugin::VisualShape2DEditorPlugin() { + visual_shape_editor = memnew(VisualShape2DEditor); + EditorNode::get_singleton()->get_gui_base()->add_child(visual_shape_editor); + make_visible(false); +} diff --git a/editor/plugins/visual_shape_2d_editor_plugin.h b/editor/plugins/visual_shape_2d_editor_plugin.h new file mode 100644 index 000000000000..2352be848caa --- /dev/null +++ b/editor/plugins/visual_shape_2d_editor_plugin.h @@ -0,0 +1,88 @@ +/**************************************************************************/ +/* visual_shape_2d_editor_plugin.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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. */ +/**************************************************************************/ + +#ifndef VISUAL_SHAPE_2D_EDITOR_PLUGIN_H +#define VISUAL_SHAPE_2D_EDITOR_PLUGIN_H + +#include "editor/plugins/editor_plugin.h" + +class MenuButton; +class VisualShape2D; + +class VisualShape2DEditor : public Control { + GDCLASS(VisualShape2DEditor, Control); + + friend class VisualShape2DEditorPlugin; + + enum Menu { + MENU_OPTION_CONVERT_TO_MESH_2D, + MENU_OPTION_CONVERT_TO_POLYGON_2D, + MENU_OPTION_CREATE_COLLISION_SHAPE_2D, + MENU_OPTION_CREATE_LIGHT_OCCLUDER_2D, + }; + + VisualShape2D *visual_shape_2d = nullptr; + + MenuButton *options = nullptr; + + void _menu_option(int p_option); + + void _convert_to_mesh_2d_node(); + void _convert_to_polygon_2d_node(); + void _create_collision_shape_2d_node(); + void _create_light_occluder_2d_node(); + + void _add_as_sibling_or_child(Node *p_own_node, Node *p_new_node); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void edit(VisualShape2D *p_visual_shape_2d); + VisualShape2DEditor(); +}; + +class VisualShape2DEditorPlugin : public EditorPlugin { + GDCLASS(VisualShape2DEditorPlugin, EditorPlugin); + + VisualShape2DEditor *visual_shape_editor = nullptr; + +public: + virtual String get_name() const override { return "VisualShape2D"; } + bool has_main_screen() const override { return false; } + virtual void edit(Object *p_object) override; + virtual bool handles(Object *p_object) const override; + virtual void make_visible(bool p_visible) override; + + VisualShape2DEditorPlugin(); +}; + +#endif // VISUAL_SHAPE_2D_EDITOR_PLUGIN_H diff --git a/editor/register_editor_types.cpp b/editor/register_editor_types.cpp index 2d3cbfb1e3c5..ce41b850850e 100644 --- a/editor/register_editor_types.cpp +++ b/editor/register_editor_types.cpp @@ -128,6 +128,7 @@ #include "editor/plugins/tool_button_editor_plugin.h" #include "editor/plugins/version_control_editor_plugin.h" #include "editor/plugins/visual_shader_editor_plugin.h" +#include "editor/plugins/visual_shape_2d_editor_plugin.h" #include "editor/plugins/voxel_gi_editor_plugin.h" #include "editor/register_exporters.h" @@ -266,6 +267,7 @@ void register_editor_types() { EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); + EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); EditorPlugins::add_by_type(); diff --git a/scene/2d/visual_shape_2d.cpp b/scene/2d/visual_shape_2d.cpp new file mode 100644 index 000000000000..4292ae7e0f5f --- /dev/null +++ b/scene/2d/visual_shape_2d.cpp @@ -0,0 +1,372 @@ +/**************************************************************************/ +/* visual_shape_2d.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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 "visual_shape_2d.h" + +#include "core/math/geometry_2d.h" + +#ifdef TOOLS_ENABLED +Dictionary VisualShape2D::_edit_get_state() const { + Dictionary state = Node2D::_edit_get_state(); + state["size"] = size; + state["offset"] = offset; + return state; +} + +void VisualShape2D::_edit_set_state(const Dictionary &p_state) { + Node2D::_edit_set_state(p_state); + set_size(p_state["size"]); + set_offset(p_state["offset"]); +} + +void VisualShape2D::_edit_set_pivot(const Point2 &p_pivot) { + set_position(get_transform().xform(p_pivot)); + set_offset(get_offset() - p_pivot); +} + +Point2 VisualShape2D::_edit_get_pivot() const { + return Vector2(); +} + +bool VisualShape2D::_edit_use_pivot() const { + return true; +} + +void VisualShape2D::_edit_set_rect(const Rect2 &p_edit_rect) { + Rect2 r = _edit_get_rect(); + + Vector2 size_ratio; + + if (r.size.x != 0 && r.size.y != 0) { + size_ratio = p_edit_rect.size / r.size; + } + + Point2 new_pos = p_edit_rect.position - r.position * size_ratio; + + Transform2D postxf; + postxf.set_rotation_scale_and_skew(get_rotation(), get_scale(), get_skew()); + new_pos = postxf.xform(new_pos); + set_position(get_position() + new_pos); + + Size2 new_size = p_edit_rect.size; + set_size(new_size.maxf(0.0001)); + + Point2 new_offset = offset * size_ratio; + set_offset(new_offset); +} +#endif // TOOLS_ENABLED + +#ifdef DEBUG_ENABLED +bool VisualShape2D::_edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const { + switch (shape_type) { + case SHAPE_RECTANGLE: { + return get_rect().has_point(p_point); + } break; + case SHAPE_CIRCLE: { + Point2 rel_point = (p_point - offset) / (size / 2); + return rel_point.length_squared() <= 1.0; + } break; + case SHAPE_EQUILATERAL_TRIANGLE: + case SHAPE_RIGHT_TRIANGLE: + case SHAPE_CAPSULE: { + return Geometry2D::is_point_in_polygon(p_point, get_points()); + } break; + } + return false; +} + +Rect2 VisualShape2D::_edit_get_rect() const { + return get_rect(); +} + +bool VisualShape2D::_edit_use_rect() const { + return true; +} +#endif // DEBUG_ENABLED + +void VisualShape2D::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_DRAW: { + bool filled = outline_width <= 0; + switch (shape_type) { + case SHAPE_RECTANGLE: { + draw_rect(get_rect(), color, filled, filled ? -1 : outline_width, antialiased); + } break; + case SHAPE_CIRCLE: // Don't use draw_circle for UVs. + case SHAPE_EQUILATERAL_TRIANGLE: + case SHAPE_RIGHT_TRIANGLE: + case SHAPE_CAPSULE: { + if (!filled || antialiased) { + PackedVector2Array points = get_points(); + points.push_back(points[0]); + draw_polyline(points, color, filled ? 1 : outline_width, antialiased); + } + if (filled) { + draw_colored_polygon(get_points(), color, get_uvs()); + } + } break; + } + } break; + } +} + +PackedVector2Array VisualShape2D::_get_shape_points() const { + // In range [-1,1]. + PackedVector2Array points; + switch (shape_type) { + case SHAPE_RECTANGLE: { + points = { + Vector2(-1, 1), + Vector2(-1, -1), + Vector2(1, -1), + Vector2(1, 1) + }; + } break; + case SHAPE_CIRCLE: { + float angle_delta = Math_TAU / resolution; + points.resize(resolution); + Vector2 *points_write = points.ptrw(); + for (int i = 0; i < resolution; i++) { + // Start at top to orient shapes with odd resolution. + points_write[i] = Vector2::from_angle(i * angle_delta - Math_PI / 2.0); + } + } break; + case SHAPE_EQUILATERAL_TRIANGLE: { + points = { + Vector2(-1, 1), + Vector2(0, -1), + Vector2(1, 1) + }; + } break; + case SHAPE_RIGHT_TRIANGLE: { + points = { + Vector2(-1, 1), + Vector2(-1, -1), + Vector2(1, 1) + }; + } break; + case SHAPE_CAPSULE: { + // Also see CapsuleShape2D::_get_points(). + int capsule_res = resolution; + // Must be even. + if (resolution % 2 == 1) { + capsule_res += 1; + } + points.resize(capsule_res + 2); + Vector2 *points_write = points.ptrw(); + + int first_half = capsule_res / 2; + real_t angle_delta = Math_TAU / capsule_res; + real_t radius = MIN(size.x, size.y); + real_t height = MAX(size.x, size.y); + if (radius == height) { + height += 0.0001; + } + + Vector2 capsule_offset; + if (size.y >= size.x) { + capsule_offset = Vector2(0, 1.0 - radius / height); + } else { + capsule_offset = Vector2(1.0 - radius / height, 0); + } + + int index = 0; + for (int i = 0; i < capsule_res; i++) { + Vector2 circle_point; + if (size.y >= size.x) { + // Start at right for vertical capsules. + circle_point = Vector2::from_angle(i * angle_delta) * radius / size; + } else { + // Start at top for horizontal capsules. + circle_point = Vector2::from_angle(i * angle_delta - Math_PI / 2.0) * radius / size; + } + + if (i == 0) { + points_write[index++] = circle_point - capsule_offset; + } + points_write[index++] = circle_point + capsule_offset; + if (i == first_half) { + points_write[index++] = circle_point - capsule_offset; + capsule_offset *= -1; + } + } + } + } + return points; +} + +PackedVector2Array VisualShape2D::get_points() const { + return Transform2D(0, size / 2, 0, offset).xform(_get_shape_points()); +} + +PackedVector2Array VisualShape2D::get_uvs() const { + return Transform2D(0, Size2(0.5, 0.5), 0, Point2(0.5, 0.5)).xform(_get_shape_points()); +} + +Rect2 VisualShape2D::get_rect() const { + return Rect2(offset - size / 2, size); +} + +void VisualShape2D::set_shape_type(ShapeType p_shape_type) { + if (shape_type == p_shape_type) { + return; + } + if (shape_type == SHAPE_CAPSULE) { + size.x *= 2; + } + + shape_type = p_shape_type; + if (shape_type == SHAPE_CAPSULE) { + // Capsule default size should not have the same width and height. + size.x /= 2; + } + queue_redraw(); + notify_property_list_changed(); +} + +VisualShape2D::ShapeType VisualShape2D::get_shape_type() const { + return shape_type; +} + +void VisualShape2D::set_color(const Color &p_color) { + if (color == p_color) { + return; + } + color = p_color; + queue_redraw(); +} + +Color VisualShape2D::get_color() const { + return color; +} + +void VisualShape2D::set_size(const Size2 &p_size) { + ERR_FAIL_COND_MSG(p_size.x <= 0 || p_size.y <= 0, "Size must be greater than 0."); + if (size == p_size) { + return; + } + size = p_size; + queue_redraw(); +} + +Size2 VisualShape2D::get_size() const { + return size; +} + +void VisualShape2D::set_offset(const Point2 &p_offset) { + if (offset == p_offset) { + return; + } + offset = p_offset; + queue_redraw(); +} + +Point2 VisualShape2D::get_offset() const { + return offset; +} + +void VisualShape2D::set_antialiased(bool p_antialiased) { + if (antialiased == p_antialiased) { + return; + } + antialiased = p_antialiased; + queue_redraw(); +} + +bool VisualShape2D::is_antialiased() const { + return antialiased; +} + +void VisualShape2D::set_outline_width(float p_outline_width) { + if (outline_width == p_outline_width) { + return; + } + outline_width = p_outline_width; + queue_redraw(); +} + +float VisualShape2D::get_outline_width() const { + return outline_width; +} + +void VisualShape2D::set_resolution(int p_resolution) { + ERR_FAIL_COND_MSG(p_resolution < 3, "Resolution must be at least 3."); + if (resolution == p_resolution) { + return; + } + resolution = p_resolution; + queue_redraw(); +} + +int VisualShape2D::get_resolution() const { + return resolution; +} + +void VisualShape2D::_validate_property(PropertyInfo &p_property) const { + if (shape_type != SHAPE_CIRCLE && shape_type != SHAPE_CAPSULE && p_property.name == "resolution") { + // Resolution is only used by circles and capsules. + p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } +} + +void VisualShape2D::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_shape_type", "shape_type"), &VisualShape2D::set_shape_type); + ClassDB::bind_method(D_METHOD("get_shape_type"), &VisualShape2D::get_shape_type); + ClassDB::bind_method(D_METHOD("set_color", "color"), &VisualShape2D::set_color); + ClassDB::bind_method(D_METHOD("get_color"), &VisualShape2D::get_color); + ClassDB::bind_method(D_METHOD("set_size", "size"), &VisualShape2D::set_size); + ClassDB::bind_method(D_METHOD("get_size"), &VisualShape2D::get_size); + ClassDB::bind_method(D_METHOD("set_offset", "offset"), &VisualShape2D::set_offset); + ClassDB::bind_method(D_METHOD("get_offset"), &VisualShape2D::get_offset); + + ClassDB::bind_method(D_METHOD("set_resolution", "resolution"), &VisualShape2D::set_resolution); + ClassDB::bind_method(D_METHOD("get_resolution"), &VisualShape2D::get_resolution); + ClassDB::bind_method(D_METHOD("set_antialiased", "antialiased"), &VisualShape2D::set_antialiased); + ClassDB::bind_method(D_METHOD("is_antialiased"), &VisualShape2D::is_antialiased); + ClassDB::bind_method(D_METHOD("set_outline_width", "outline_width"), &VisualShape2D::set_outline_width); + ClassDB::bind_method(D_METHOD("get_outline_width"), &VisualShape2D::get_outline_width); + + ClassDB::bind_method(D_METHOD("get_points"), &VisualShape2D::get_points); + ClassDB::bind_method(D_METHOD("get_uvs"), &VisualShape2D::get_uvs); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "shape_type", PROPERTY_HINT_ENUM, "Rectangle,Circle,Equilateral Triangle,Right Triangle,Capsule"), "set_shape_type", "get_shape_type"); + ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_color", "get_color"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "size", PROPERTY_HINT_RANGE, "0.0001,99999,0.001,or_greater,hide_slider,suffix:px"), "set_size", "get_size"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset", PROPERTY_HINT_NONE, "suffix:px"), "set_offset", "get_offset"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "resolution", PROPERTY_HINT_RANGE, "3,1024,1,or_greater"), "set_resolution", "get_resolution"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "antialiased"), "set_antialiased", "is_antialiased"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "outline_width", PROPERTY_HINT_RANGE, "0,16,0.01,or_greater"), "set_outline_width", "get_outline_width"); + + BIND_ENUM_CONSTANT(SHAPE_RECTANGLE); + BIND_ENUM_CONSTANT(SHAPE_CIRCLE); + BIND_ENUM_CONSTANT(SHAPE_EQUILATERAL_TRIANGLE); + BIND_ENUM_CONSTANT(SHAPE_RIGHT_TRIANGLE); + BIND_ENUM_CONSTANT(SHAPE_CAPSULE); +} diff --git a/scene/2d/visual_shape_2d.h b/scene/2d/visual_shape_2d.h new file mode 100644 index 000000000000..5561b8aed8ac --- /dev/null +++ b/scene/2d/visual_shape_2d.h @@ -0,0 +1,108 @@ +/**************************************************************************/ +/* visual_shape_2d.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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. */ +/**************************************************************************/ + +#ifndef VISUAL_SHAPE_2D_H +#define VISUAL_SHAPE_2D_H + +#include "scene/2d/node_2d.h" + +class VisualShape2D : public Node2D { + GDCLASS(VisualShape2D, Node2D); + +public: + enum ShapeType { + SHAPE_RECTANGLE, + SHAPE_CIRCLE, + SHAPE_EQUILATERAL_TRIANGLE, + SHAPE_RIGHT_TRIANGLE, + SHAPE_CAPSULE, + }; + +private: + ShapeType shape_type = SHAPE_RECTANGLE; + Color color = Color(1, 1, 1, 1); + Size2 size = Size2(128, 128); + Point2 offset; + + bool antialiased = false; + float outline_width = 0; + int resolution = 64; + + PackedVector2Array _get_shape_points() const; + +protected: + void _notification(int p_what); + + static void _bind_methods(); + void _validate_property(PropertyInfo &p_property) const; + +public: +#ifdef TOOLS_ENABLED + virtual Dictionary _edit_get_state() const override; + virtual void _edit_set_state(const Dictionary &p_state) override; + + virtual void _edit_set_pivot(const Point2 &p_pivot) override; + virtual Point2 _edit_get_pivot() const override; + virtual bool _edit_use_pivot() const override; + virtual void _edit_set_rect(const Rect2 &p_edit_rect) override; + +#endif // TOOLS_ENABLED + +#ifdef DEBUG_ENABLED + virtual bool _edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const override; + + virtual Rect2 _edit_get_rect() const override; + virtual bool _edit_use_rect() const override; +#endif // DEBUG_ENABLED + + PackedVector2Array get_points() const; + PackedVector2Array get_uvs() const; + Rect2 get_rect() const; + + void set_shape_type(ShapeType p_shape_type); + ShapeType get_shape_type() const; + void set_color(const Color &p_color); + Color get_color() const; + void set_size(const Size2 &p_size); + Size2 get_size() const; + void set_offset(const Point2 &p_offset); + Point2 get_offset() const; + + void set_antialiased(bool p_antialiased); + bool is_antialiased() const; + void set_outline_width(float p_outline_width); + float get_outline_width() const; + void set_resolution(int p_resolution); + int get_resolution() const; +}; + +VARIANT_ENUM_CAST(VisualShape2D::ShapeType) + +#endif // VISUAL_SHAPE_2D_H diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index 8a048e9cc302..159dd7c5f93b 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -206,6 +206,7 @@ #include "scene/2d/tile_map_layer.h" #include "scene/2d/touch_screen_button.h" #include "scene/2d/visible_on_screen_notifier_2d.h" +#include "scene/2d/visual_shape_2d.h" #include "scene/resources/2d/capsule_shape_2d.h" #include "scene/resources/2d/circle_shape_2d.h" #include "scene/resources/2d/concave_polygon_shape_2d.h" @@ -814,6 +815,7 @@ void register_scene_types() { GDREGISTER_CLASS(LightOccluder2D); GDREGISTER_CLASS(OccluderPolygon2D); GDREGISTER_CLASS(BackBufferCopy); + GDREGISTER_CLASS(VisualShape2D); OS::get_singleton()->yield(); // may take time to init