Skip to content

Commit c15cdb1

Browse files
committed
Add VisualShape2D node for drawing common shapes
1 parent 76fa7b2 commit c15cdb1

8 files changed

+936
-0
lines changed

doc/classes/VisualShape2D.xml

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<class name="VisualShape2D" inherits="Node2D" keywords="square, rectangle, circle, oval, triangle, capsule, hexagon" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
3+
<brief_description>
4+
2D shape-drawing node.
5+
</brief_description>
6+
<description>
7+
[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].
8+
For more advanced capabilities, see [Polygon2D], [Sprite2D], or [MeshInstance2D]. For custom drawing, see [method CanvasItem._draw] and the [CanvasItem] draw methods.
9+
</description>
10+
<tutorials>
11+
</tutorials>
12+
<methods>
13+
<method name="get_points" qualifiers="const">
14+
<return type="PackedVector2Array" />
15+
<description>
16+
Returns a list of points representing the vertices of the polygon.
17+
</description>
18+
</method>
19+
<method name="get_uvs" qualifiers="const">
20+
<return type="PackedVector2Array" />
21+
<description>
22+
Returns a list of UV values for each vertex of the polygon.
23+
</description>
24+
</method>
25+
</methods>
26+
<members>
27+
<member name="antialiased" type="bool" setter="set_antialiased" getter="is_antialiased" default="false">
28+
If [code]true[/code], the shape is antialiased. UVs are not supported on antialiased edges. Scaling may affect antialiasing quality.
29+
[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]).
30+
</member>
31+
<member name="color" type="Color" setter="set_color" getter="get_color" default="Color(1, 1, 1, 1)">
32+
The color of the shape.
33+
</member>
34+
<member name="offset" type="Vector2" setter="set_offset" getter="get_offset" default="Vector2(0, 0)">
35+
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.
36+
</member>
37+
<member name="outline_width" type="float" setter="set_outline_width" getter="get_outline_width" default="0.0">
38+
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.
39+
</member>
40+
<member name="resolution" type="int" setter="set_resolution" getter="get_resolution" default="64">
41+
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.
42+
</member>
43+
<member name="shape_type" type="int" setter="set_shape_type" getter="get_shape_type" enum="VisualShape2D.ShapeType" default="0">
44+
The type of shape to draw.
45+
</member>
46+
<member name="size" type="Vector2" setter="set_size" getter="get_size" default="Vector2(128, 128)">
47+
The size of the shape in pixels.
48+
</member>
49+
</members>
50+
<constants>
51+
<constant name="SHAPE_RECTANGLE" value="0" enum="ShapeType">
52+
A square or rectangle shape.
53+
</constant>
54+
<constant name="SHAPE_CIRCLE" value="1" enum="ShapeType">
55+
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.
56+
</constant>
57+
<constant name="SHAPE_EQUILATERAL_TRIANGLE" value="2" enum="ShapeType">
58+
An equilateral triangle shape, pointing up.
59+
</constant>
60+
<constant name="SHAPE_RIGHT_TRIANGLE" value="3" enum="ShapeType">
61+
A right triangle shape, with its right angle placed on the node's bottom-left corner.
62+
</constant>
63+
<constant name="SHAPE_CAPSULE" value="4" enum="ShapeType">
64+
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.
65+
</constant>
66+
</constants>
67+
</class>

editor/icons/VisualShape2D.svg

+1
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
/**************************************************************************/
2+
/* visual_shape_2d_editor_plugin.cpp */
3+
/**************************************************************************/
4+
/* This file is part of: */
5+
/* GODOT ENGINE */
6+
/* https://godotengine.org */
7+
/**************************************************************************/
8+
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9+
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10+
/* */
11+
/* Permission is hereby granted, free of charge, to any person obtaining */
12+
/* a copy of this software and associated documentation files (the */
13+
/* "Software"), to deal in the Software without restriction, including */
14+
/* without limitation the rights to use, copy, modify, merge, publish, */
15+
/* distribute, sublicense, and/or sell copies of the Software, and to */
16+
/* permit persons to whom the Software is furnished to do so, subject to */
17+
/* the following conditions: */
18+
/* */
19+
/* The above copyright notice and this permission notice shall be */
20+
/* included in all copies or substantial portions of the Software. */
21+
/* */
22+
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23+
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24+
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25+
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26+
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27+
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28+
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29+
/**************************************************************************/
30+
31+
#include "visual_shape_2d_editor_plugin.h"
32+
33+
#include "canvas_item_editor_plugin.h"
34+
#include "core/math/geometry_2d.h"
35+
#include "editor/editor_node.h"
36+
#include "editor/editor_undo_redo_manager.h"
37+
#include "editor/gui/editor_toaster.h"
38+
#include "editor/scene_tree_dock.h"
39+
#include "scene/2d/light_occluder_2d.h"
40+
#include "scene/2d/mesh_instance_2d.h"
41+
#include "scene/2d/physics/collision_shape_2d.h"
42+
#include "scene/2d/polygon_2d.h"
43+
#include "scene/2d/visual_shape_2d.h"
44+
#include "scene/gui/menu_button.h"
45+
#include "scene/resources/2d/capsule_shape_2d.h"
46+
#include "scene/resources/2d/circle_shape_2d.h"
47+
#include "scene/resources/2d/convex_polygon_shape_2d.h"
48+
#include "scene/resources/2d/rectangle_shape_2d.h"
49+
50+
void VisualShape2DEditor::edit(VisualShape2D *p_visual_shape_2d) {
51+
visual_shape_2d = p_visual_shape_2d;
52+
}
53+
54+
void VisualShape2DEditor::_menu_option(int p_option) {
55+
if (!visual_shape_2d) {
56+
return;
57+
}
58+
59+
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()) {
60+
EditorToaster::get_singleton()->popup_str(TTR("Can't convert a VisualShape from a foreign scene."), EditorToaster::SEVERITY_ERROR);
61+
return;
62+
}
63+
64+
switch (p_option) {
65+
case MENU_OPTION_CONVERT_TO_MESH_2D: {
66+
_convert_to_mesh_2d_node();
67+
} break;
68+
case MENU_OPTION_CONVERT_TO_POLYGON_2D: {
69+
_convert_to_polygon_2d_node();
70+
} break;
71+
case MENU_OPTION_CREATE_COLLISION_SHAPE_2D: {
72+
_create_collision_shape_2d_node();
73+
} break;
74+
case MENU_OPTION_CREATE_LIGHT_OCCLUDER_2D: {
75+
_create_light_occluder_2d_node();
76+
} break;
77+
}
78+
}
79+
80+
void VisualShape2DEditor::_convert_to_mesh_2d_node() {
81+
PackedVector2Array points = visual_shape_2d->get_points();
82+
if (points.size() < 3) {
83+
EditorToaster::get_singleton()->popup_str(TTR("Invalid geometry, can't replace by mesh."), EditorToaster::SEVERITY_ERROR);
84+
return;
85+
}
86+
87+
Vector<int> poly = Geometry2D::triangulate_polygon(points);
88+
89+
Ref<ArrayMesh> mesh;
90+
mesh.instantiate();
91+
92+
Array a;
93+
a.resize(Mesh::ARRAY_MAX);
94+
a[Mesh::ARRAY_VERTEX] = points;
95+
a[Mesh::ARRAY_TEX_UV] = visual_shape_2d->get_uvs();
96+
a[Mesh::ARRAY_INDEX] = poly;
97+
98+
mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, a, Array(), Dictionary(), Mesh::ARRAY_FLAG_USE_2D_VERTICES);
99+
100+
MeshInstance2D *mesh_instance = memnew(MeshInstance2D);
101+
mesh_instance->set_mesh(mesh);
102+
103+
EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
104+
ur->create_action(TTR("Convert to MeshInstance2D"), UndoRedo::MERGE_DISABLE, visual_shape_2d);
105+
SceneTreeDock::get_singleton()->replace_node(visual_shape_2d, mesh_instance);
106+
ur->commit_action(false);
107+
}
108+
109+
void VisualShape2DEditor::_convert_to_polygon_2d_node() {
110+
PackedVector2Array points = visual_shape_2d->get_points();
111+
if (points.is_empty()) {
112+
EditorToaster::get_singleton()->popup_str(TTR("Invalid geometry, can't create polygon."), EditorToaster::SEVERITY_ERROR);
113+
return;
114+
}
115+
116+
int total_point_count = points.size();
117+
Point2 offset = visual_shape_2d->get_offset();
118+
119+
Polygon2D *polygon_2d_instance = memnew(Polygon2D);
120+
121+
polygon_2d_instance->set_color(visual_shape_2d->get_color());
122+
polygon_2d_instance->set_offset(offset);
123+
polygon_2d_instance->set_antialiased(visual_shape_2d->is_antialiased());
124+
125+
PackedVector2Array vertices;
126+
vertices.resize(total_point_count);
127+
Vector2 *vertices_write = vertices.ptrw();
128+
129+
PackedInt32Array index_array;
130+
index_array.resize(total_point_count);
131+
int *index_write = index_array.ptrw();
132+
133+
for (int i = 0; i < total_point_count; i++) {
134+
vertices_write[i] = points[i] - offset;
135+
index_write[i] = i;
136+
}
137+
138+
Array polys;
139+
polys.push_back(index_array);
140+
141+
PackedVector2Array uvs = Transform2D(0, visual_shape_2d->get_size(), 0, Point2(0, 0)).xform(visual_shape_2d->get_uvs());
142+
143+
polygon_2d_instance->set_polygon(vertices);
144+
polygon_2d_instance->set_uv(uvs);
145+
polygon_2d_instance->set_polygons(polys);
146+
147+
EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
148+
ur->create_action(TTR("Convert to Polygon2D"), UndoRedo::MERGE_DISABLE, visual_shape_2d);
149+
SceneTreeDock::get_singleton()->replace_node(visual_shape_2d, polygon_2d_instance);
150+
ur->commit_action(false);
151+
}
152+
153+
void VisualShape2DEditor::_create_collision_shape_2d_node() {
154+
CollisionShape2D *collision_shape_2d_instance = memnew(CollisionShape2D);
155+
Size2 size = visual_shape_2d->get_size();
156+
157+
switch (visual_shape_2d->get_shape_type()) {
158+
case VisualShape2D::SHAPE_RECTANGLE: {
159+
Ref<RectangleShape2D> shape;
160+
shape.instantiate();
161+
shape->set_size(size);
162+
collision_shape_2d_instance->set_shape(shape);
163+
collision_shape_2d_instance->translate(visual_shape_2d->get_offset());
164+
} break;
165+
case VisualShape2D::SHAPE_CIRCLE: {
166+
float semi_major = MAX(size.x, size.y) / 2.0;
167+
float semi_minor = MIN(size.x, size.y) / 2.0;
168+
float difference = (semi_major - semi_minor) / semi_minor;
169+
// If there is more than a 10% difference, treat as an oval.
170+
if (difference > 0.1) {
171+
// Oval.
172+
Ref<ConvexPolygonShape2D> shape;
173+
shape.instantiate();
174+
shape->set_points(visual_shape_2d->get_points());
175+
collision_shape_2d_instance->set_shape(shape);
176+
} else {
177+
// Circle.
178+
Ref<CircleShape2D> shape;
179+
shape.instantiate();
180+
shape->set_radius(semi_major);
181+
collision_shape_2d_instance->set_shape(shape);
182+
collision_shape_2d_instance->translate(visual_shape_2d->get_offset());
183+
}
184+
} break;
185+
case VisualShape2D::SHAPE_CAPSULE: {
186+
Ref<CapsuleShape2D> shape;
187+
shape.instantiate();
188+
shape->set_radius(MIN(size.x, size.y) / 2);
189+
shape->set_height(MAX(size.x, size.y));
190+
collision_shape_2d_instance->set_shape(shape);
191+
if (size.x > size.y) {
192+
collision_shape_2d_instance->rotate(Math_PI / 2.0);
193+
}
194+
collision_shape_2d_instance->translate(visual_shape_2d->get_offset());
195+
} break;
196+
case VisualShape2D::SHAPE_EQUILATERAL_TRIANGLE:
197+
case VisualShape2D::SHAPE_RIGHT_TRIANGLE: {
198+
Ref<ConvexPolygonShape2D> shape;
199+
shape.instantiate();
200+
shape->set_points(visual_shape_2d->get_points());
201+
collision_shape_2d_instance->set_shape(shape);
202+
} break;
203+
}
204+
205+
EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
206+
ur->create_action(TTR("Create CollisionShape2D Sibling"), UndoRedo::MERGE_DISABLE, visual_shape_2d);
207+
ur->add_do_method(this, "_add_as_sibling_or_child", visual_shape_2d, collision_shape_2d_instance);
208+
ur->add_do_reference(collision_shape_2d_instance);
209+
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);
210+
ur->commit_action();
211+
}
212+
213+
void VisualShape2DEditor::_create_light_occluder_2d_node() {
214+
PackedVector2Array points = visual_shape_2d->get_points();
215+
if (points.is_empty()) {
216+
EditorToaster::get_singleton()->popup_str(TTR("Invalid geometry, can't create light occluder."), EditorToaster::SEVERITY_ERROR);
217+
return;
218+
}
219+
220+
Ref<OccluderPolygon2D> polygon;
221+
polygon.instantiate();
222+
polygon->set_polygon(points);
223+
224+
LightOccluder2D *light_occluder_2d_instance = memnew(LightOccluder2D);
225+
light_occluder_2d_instance->set_occluder_polygon(polygon);
226+
227+
EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
228+
ur->create_action(TTR("Create LightOccluder2D Sibling"), UndoRedo::MERGE_DISABLE, visual_shape_2d);
229+
ur->add_do_method(this, "_add_as_sibling_or_child", visual_shape_2d, light_occluder_2d_instance);
230+
ur->add_do_reference(light_occluder_2d_instance);
231+
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);
232+
ur->commit_action();
233+
}
234+
235+
void VisualShape2DEditor::_add_as_sibling_or_child(Node *p_own_node, Node *p_new_node) {
236+
// Can't make sibling if own node is scene root.
237+
if (p_own_node != get_tree()->get_edited_scene_root()) {
238+
p_own_node->get_parent()->add_child(p_new_node, true);
239+
Object::cast_to<Node2D>(p_new_node)->set_transform(Object::cast_to<Node2D>(p_own_node)->get_transform() * Object::cast_to<Node2D>(p_new_node)->get_transform());
240+
} else {
241+
p_own_node->add_child(p_new_node, true);
242+
}
243+
244+
p_new_node->set_owner(get_tree()->get_edited_scene_root());
245+
}
246+
247+
void VisualShape2DEditor::_notification(int p_what) {
248+
switch (p_what) {
249+
case NOTIFICATION_THEME_CHANGED: {
250+
options->set_button_icon(get_editor_theme_icon(SNAME("VisualShape2D")));
251+
252+
options->get_popup()->set_item_icon(MENU_OPTION_CONVERT_TO_MESH_2D, get_editor_theme_icon(SNAME("MeshInstance2D")));
253+
options->get_popup()->set_item_icon(MENU_OPTION_CONVERT_TO_POLYGON_2D, get_editor_theme_icon(SNAME("Polygon2D")));
254+
options->get_popup()->set_item_icon(MENU_OPTION_CREATE_COLLISION_SHAPE_2D, get_editor_theme_icon(SNAME("CollisionShape2D")));
255+
options->get_popup()->set_item_icon(MENU_OPTION_CREATE_LIGHT_OCCLUDER_2D, get_editor_theme_icon(SNAME("LightOccluder2D")));
256+
} break;
257+
}
258+
}
259+
260+
void VisualShape2DEditor::_bind_methods() {
261+
ClassDB::bind_method("_add_as_sibling_or_child", &VisualShape2DEditor::_add_as_sibling_or_child);
262+
}
263+
264+
VisualShape2DEditor::VisualShape2DEditor() {
265+
options = memnew(MenuButton);
266+
267+
CanvasItemEditor::get_singleton()->add_control_to_menu_panel(options);
268+
269+
options->set_text(TTR("VisualShape2D"));
270+
271+
options->get_popup()->add_item(TTR("Convert to MeshInstance2D"), MENU_OPTION_CONVERT_TO_MESH_2D);
272+
options->get_popup()->add_item(TTR("Convert to Polygon2D"), MENU_OPTION_CONVERT_TO_POLYGON_2D);
273+
options->get_popup()->add_item(TTR("Create CollisionShape2D Sibling"), MENU_OPTION_CREATE_COLLISION_SHAPE_2D);
274+
options->get_popup()->add_item(TTR("Create LightOccluder2D Sibling"), MENU_OPTION_CREATE_LIGHT_OCCLUDER_2D);
275+
options->set_switch_on_hover(true);
276+
277+
options->get_popup()->connect(SceneStringName(id_pressed), callable_mp(this, &VisualShape2DEditor::_menu_option));
278+
}
279+
280+
void VisualShape2DEditorPlugin::edit(Object *p_object) {
281+
visual_shape_editor->edit(Object::cast_to<VisualShape2D>(p_object));
282+
}
283+
284+
bool VisualShape2DEditorPlugin::handles(Object *p_object) const {
285+
return p_object->is_class("VisualShape2D");
286+
}
287+
288+
void VisualShape2DEditorPlugin::make_visible(bool p_visible) {
289+
visual_shape_editor->options->set_visible(p_visible);
290+
}
291+
292+
VisualShape2DEditorPlugin::VisualShape2DEditorPlugin() {
293+
visual_shape_editor = memnew(VisualShape2DEditor);
294+
EditorNode::get_singleton()->get_gui_base()->add_child(visual_shape_editor);
295+
make_visible(false);
296+
}

0 commit comments

Comments
 (0)