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