diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f485e0..c0d9d0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Added - A `MidiPlayer` to play MIDI files with SoundFont. +- A `Debug2D` singleton to help the process of debugging in 2D by drawing. - A `GoostMath` singleton. - A project setting for configuring path to repository by Git plugin: `version_control/git/repository_path`. - An editor setting for `MixinScript` auto-switching behavior: `text_editor/files/open_first_script_on_editing_mixin_script` (disabled by default now). diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index f2ba2a1c..3872c5ad 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -31,8 +31,7 @@ void register_core_types() { #ifdef GOOST_GoostEngine _goost = memnew(GoostEngine); ClassDB::register_class(); - Engine::get_singleton()->add_singleton( - Engine::Singleton("GoostEngine", GoostEngine::get_singleton())); + Engine::get_singleton()->add_singleton(Engine::Singleton("GoostEngine", GoostEngine::get_singleton())); SceneTree::add_idle_callback(&GoostEngine::flush_calls); #endif ClassDB::register_class(); diff --git a/doc/Debug2D.xml b/doc/Debug2D.xml new file mode 100644 index 00000000..50391cd7 --- /dev/null +++ b/doc/Debug2D.xml @@ -0,0 +1,232 @@ + + + + Debug drawing in 2D. + + + A singleton which allows to draw various primitives in order to aid visual debugging in 2D. Unlike other nodes, this allows to draw outside of [method CanvasItem._draw] or [constant CanvasItem.NOTIFICATION_DRAW], so [Debug2D] can be used from everywhere in code, for example: + [codeblock] + func _ready(): + Debug2D.draw_line(Vector2(0, 0), Vector2(100, 100)) + [/codeblock] + When drawing each frame, you should call [method clear] prior to drawing, otherwise draw commands will accumulate infinitely, decreasing performance: + [codeblock] + func _process(delta): + Debug2D.clear() + Debug2D.draw_line(Vector2(0, 0), Vector2(100, 100)) + [/codeblock] + It's also possible to keep a history of draw commands by calling [method capture]: + [codeblock] + func _ready(): + var points = [Vector2(0, 0), Vector2(100, 0), Vector2(100, 100), Vector2(0, 100)] + for point in points: + Debug2D.draw_circle(8, point) + Debug2D.capture() + [/codeblock] + You can then access the captured snapshots using [method get_capture], which will return a special [DebugCapture] object, refer to [DebugCapture] documentation for more information on how to playback snapshots. + Default draw parameters such as color or line width can be configured via [ProjectSettings] (see [code]debug/draw[/code] section), or using one of the [code]draw_set_*[/code] methods. Arguments passed directly to methods will override parameters set by [code]draw_set_*[/code] methods, and [code]draw_set_*[/code] methods will override parameters defined in [ProjectSettings]. + [b]List of common parameters:[/b] + [code]color:[/code] Specifies draw color. + [code]filled:[/code] If [code]true[/code], then all geometrical primitives such as polygon, circle, rectangle etc. will be drawn with a solid color, otherwise only the outline is drawn. + [code]line_width:[/code] Specifies line with for methods such as [method draw_line], or line width of unfilled primitives. + [b]Note:[/b] the drawing works in debug builds only. + + + + + + + + Captures a new snapshot of all draw commands that were called to this moment. Can be called multiple times. + + + + + + Clears the canvas, all the draw calls from the queue of draw commands are removed. + + + + + + + + Calls a custom draw method. It's possible to call both built-in [CanvasItem] methods (starting with [code]draw_*[/code]), or methods defined via script. + + + + + + + + + + + + Draws an arrow. The [code]tip_size[/code] configures the size of the arrow's tip, where X coordinate corresponds to the width, and Y corresponds to the height. The [code]tip_offset[/code] allows to shift the tip towards the beginning along arrow's length, and is specified as a fraction of the arrow's length in the range of [code][0..1][/code]. + The following snippet shows how to draw a cyclic directed graph with vertices drawn as circles, where the arrow tip is perfectly aligned to circle's boundary: + [codeblock] + for i in points.size(): + var from = points[i] + var to = points[(i + 1) % points.size()] + + var radius = 8.0 + var length = (to - from).length() + var offset = radius / length + + Debug2D.draw_arrow(from, to, Color.white, 1, Vector2(8, 8), offset) + Debug2D.draw_circle(radius, from, Color.white) + [/codeblock] + + + + + + + + + + + Draws a circle. Unlike in [method CanvasItem.draw_circle], the total number of vertices is configured according to predefined arc tolerance to improve accuracy when drawing circles with large radius. + + + + + + + + + + Draws a line. + + + + + + + + + + Draws a polygon. + + + + + + + + + Draws a polyline. + + + + + + + + + + + Draws a rectangle. The total width and height is twice the half [code]extents[/code]. See also [method draw_region]. + + + + + + + + + + Draws a region [Rect2]. For example, you can draw a bounding rectangle of points: + [codeblock] + Debug2D.draw_region(GoostGeometry2D.bounding_rect(points)) + [/codeblock] + See also [method draw_rectangle]. + + + + + + + Resets all drawing options set with [method draw_set_color], [method draw_set_filled], and [method draw_set_line_width]. + + + + + + + Overrides the [code]color[/code] parameter for all future draw calls. + + + + + + + Overrides the [code]filled[/code] parameter for all future draw calls. + + + + + + + Overrides the [code]width[/code] parameter for all future draw calls. + + + + + + + + + Sets a custom transform for drawing via components. Anything drawn afterwards will be transformed by this. Equivalent to [method CanvasItem.draw_set_transform]. + + + + + + + Sets a custom transform for drawing via matrix. Anything drawn afterwards will be transformed by this. Equivalent to [method CanvasItem.draw_set_transform_matrix]. + + + + + + + + + Draws text at specified position using the default [Font]. Unlike other draw methods, the default color will not be affected by [method draw_set_color]. + + + + + + Returns the default base [CanvasItem] used for drawing. + + + + + + Returns [DebugCapture] object to manage history of draw commands. + + + + + + Update all draw calls to request redraw. Similar to [method CanvasItem update]. + + + + + + The current active canvas item used for drawing. To restore the default, assign the base canvas: + [codeblock] + Debug2D.canvas_item = Debug2D.get_base() + [/codeblock] + + + If [code]false[/code], then all debug drawing is disabled. + + + + + diff --git a/doc/DebugCapture.xml b/doc/DebugCapture.xml new file mode 100644 index 00000000..f5d7979c --- /dev/null +++ b/doc/DebugCapture.xml @@ -0,0 +1,59 @@ + + + + Manages draw state in [Debug2D]. + + + The class is used as a controller for [Debug2D] which allows to play back draw commands at specified snapshots recorded with [method Debug2D.capture], This is useful for things like visualizing algorithms step-by-step. + For example, you can rewind the drawing using left and right keys: + [codeblock] + func _input(event): + if event.is_action_pressed("ui_left"): + Debug2D.get_capture().draw_prev() + elif event.is_action_pressed("ui_right"): + Debug2D.get_capture().draw_next() + [/codeblock] + + + + + + + + + Draws a specific snapshot at index. + + + + + + Draws a next snapshot. + + + + + + Draws a previous snapshot. + + + + + + Return the number of draw snapshots previously captured with [method Debug2D.capture]. + + + + + + Makes [Debug2D] to draw all snapshots if [member accumulate] is [code]true[/code], otherwise only the first snapshot will be drawn. + + + + + + If [code]true[/code], the drawing canvas won't be cleared. Set this to [code]false[/code] if you need to draw each snapshot individually. + + + + + diff --git a/editor/icons/icon_debug_2_d.svg b/editor/icons/icon_debug_2_d.svg new file mode 100644 index 00000000..afb9f4b4 --- /dev/null +++ b/editor/icons/icon_debug_2_d.svg @@ -0,0 +1 @@ + diff --git a/goost.py b/goost.py index f57f75e2..8cb907c4 100644 --- a/goost.py +++ b/goost.py @@ -156,6 +156,8 @@ def add_depencency(self, goost_class): "CommandLineHelpFormat": "core", "CommandLineOption": "core", "CommandLineParser": "core", + "Debug2D": "scene", + "DebugCapture": "scene", "GoostEngine": "core", "GoostMath": "math", "GoostGeometry2D": "geometry", @@ -217,6 +219,7 @@ def add_depencency(self, goost_class): # If so, define them here explicitly so that they're automatically enabled. class_dependencies = { "CommandLineParser": ["CommandLineOption", "CommandLineHelpFormat"], + "Debug2D": ["DebugCapture", "GoostGeometry2D"], "GoostEngine" : "InvokeState", "GoostGeometry2D" : ["PolyBoolean2D", "PolyDecomp2D", "PolyOffset2D"], "LightTexture" : "GradientTexture2D", diff --git a/scene/2d/debug_2d.cpp b/scene/2d/debug_2d.cpp new file mode 100644 index 00000000..bfc4a562 --- /dev/null +++ b/scene/2d/debug_2d.cpp @@ -0,0 +1,557 @@ +#include "debug_2d.h" + +#include "goost/core/math/geometry/2d/goost_geometry_2d.h" + +#include "scene/resources/theme.h" +#include "scene/scene_string_names.h" + +#include "core/method_bind_ext.gen.inc" + +Debug2D *Debug2D::singleton = nullptr; + +void Debug2D::set_enabled(bool p_enabled) { + enabled = p_enabled; + update(); +} + +void Debug2D::set_canvas_item(Object *p_canvas_item) { + ERR_FAIL_NULL_MSG(p_canvas_item, "Invalid object"); + + CanvasItem *item = nullptr; + if (p_canvas_item) { + item = Object::cast_to(p_canvas_item); + } + ERR_FAIL_NULL_MSG(item, "Object does inherit CanvasItem"); + + canvas_item = item->get_instance_id(); + + if (!item->is_connected(SceneStringNames::get_singleton()->draw, this, "_on_canvas_item_draw")) { + item->connect(SceneStringNames::get_singleton()->draw, this, "_on_canvas_item_draw", varray(item)); + } +} + +Object *Debug2D::get_canvas_item() const { + return ObjectDB::get_instance(canvas_item); +} + +void Debug2D::draw(const StringName &p_method, const Array &p_args) { + DrawCommand c; + c.type = DrawCommand::CUSTOM; + c.args.push_back(p_method); + c.args.push_back(p_args); + _push_command(c); +} + +void Debug2D::draw_line(const Point2 &p_from, const Point2 &p_to, const Color &p_color, float p_width) { + DrawCommand c; + c.type = DrawCommand::LINE; + c.args.push_back(p_from); + c.args.push_back(p_to); + c.args.push_back(_option_get_value("color", p_color)); + c.args.push_back(_option_get_value("line_width", p_width)); + _push_command(c); +} + +void Debug2D::draw_arrow(const Point2 &p_from, const Point2 &p_to, const Color &p_color, float p_width, const Vector2 &p_tip_size, float p_tip_offset) { + ERR_FAIL_COND(p_tip_size.x <= 0); + ERR_FAIL_COND(p_tip_size.y <= 0); + + DrawCommand c; + c.type = DrawCommand::ARROW; + c.args.push_back(p_from); + c.args.push_back(p_to); + c.args.push_back(_option_get_value("color", p_color)); + c.args.push_back(_option_get_value("line_width", p_width)); + c.args.push_back(p_tip_size); + c.args.push_back(p_tip_offset); + _push_command(c); +} + +void Debug2D::draw_polyline(const Vector &p_polyline, const Color &p_color, real_t p_width) { + ERR_FAIL_COND(p_polyline.size() < 2); + + DrawCommand c; + c.type = DrawCommand::POLYLINE; + c.args.push_back(p_polyline); + c.args.push_back(_option_get_value("color", p_color)); + c.args.push_back(_option_get_value("line_width", p_width)); + _push_command(c); +} + +void Debug2D::draw_polygon(const Vector &p_polygon, const Color &p_color, bool p_filled, float p_width) { + ERR_FAIL_COND(p_polygon.size() < 3); + + DrawCommand c; + c.type = DrawCommand::POLYGON; + c.args.push_back(p_polygon); + c.args.push_back(_option_get_value("color", p_color)); + c.args.push_back(_option_get_value("filled", p_filled)); + c.args.push_back(_option_get_value("line_width", p_width)); + _push_command(c); +} + +void Debug2D::draw_region(const Rect2 &p_region, const Color &p_color, bool p_filled, float p_width) { + DrawCommand c; + c.type = DrawCommand::REGION; + c.args.push_back(p_region); + c.args.push_back(_option_get_value("color", p_color)); + c.args.push_back(_option_get_value("filled", p_filled)); + c.args.push_back(_option_get_value("line_width", p_width)); + _push_command(c); +} + +void Debug2D::draw_rectangle(const Vector2 &p_extents, const Vector2 &p_position, const Color &p_color, bool p_filled, float p_width) { + DrawCommand c; + c.type = DrawCommand::RECTANGLE; + c.args.push_back(p_extents); + c.args.push_back(p_position); + c.args.push_back(_option_get_value("color", p_color)); + c.args.push_back(_option_get_value("filled", p_filled)); + c.args.push_back(_option_get_value("line_width", p_width)); + _push_command(c); +} + +void Debug2D::draw_circle(real_t p_radius, const Vector2 &p_position, const Color &p_color, bool p_filled, float p_width) { + DrawCommand c; + c.type = DrawCommand::CIRCLE; + c.args.push_back(p_radius); + c.args.push_back(p_position); + c.args.push_back(_option_get_value("color", p_color)); + c.args.push_back(_option_get_value("filled", p_filled)); + c.args.push_back(_option_get_value("line_width", p_width)); + _push_command(c); +} + +void Debug2D::draw_text(const String &p_text, const Vector2 &p_position, const Color &p_color) { + DrawCommand c; + c.type = DrawCommand::TEXT; + c.args.push_back(p_text); + c.args.push_back(p_position); + c.args.push_back(_option_get_value("color", p_color)); + _push_command(c); +} + +void Debug2D::draw_set_transform(const Point2 &p_offset, float p_rotation, const Size2 &p_scale) { + Transform2D matrix(p_rotation, p_offset); + matrix.scale_basis(p_scale); + draw_set_transform_matrix(matrix); +} + +void Debug2D::draw_set_transform_matrix(const Transform2D &p_matrix) { + DrawCommand c; + c.type = DrawCommand::TRANSFORM; + c.args.push_back(p_matrix); + _push_command(c); +} + +void Debug2D::draw_set_color(const Color &p_color) { + draw_override["color"] = p_color; +} + +void Debug2D::draw_set_filled(bool p_filled) { + draw_override["filled"] = p_filled; +} + +void Debug2D::draw_set_line_width(real_t p_width) { + draw_override["line_width"] = p_width; +} + +void Debug2D::draw_reset(const String &p_option) { + if (p_option.empty()) { + draw_override["color"] = Variant(); + draw_override["filled"] = Variant(); + draw_override["line_width"] = Variant(); + } else { + ERR_FAIL_COND_MSG(!draw_override.has(p_option), "Draw option does not exist"); + draw_override[p_option] = Variant(); + } +} + +Variant Debug2D::_option_get_value(const String &p_option, const Variant &p_value) { + Variant ret; + + Variant def_value = default_value[p_option]; + Variant::Type type = def_value.get_type(); + + if (p_value != def_value) { + ret = p_value; + } else if (draw_override[p_option].get_type() == type) { // Not NIL. + ret = draw_override[p_option]; + } else { + ret = p_value; + } + return ret; +} + +void Debug2D::_push_command(DrawCommand &p_command) { + p_command.canvas_item = canvas_item; + commands.push_back(p_command); + capture_end += 1; + update(); +} + +void Debug2D::_draw_command(const DrawCommand &p_command, CanvasItem *p_item) { +#ifdef DEBUG_ENABLED + if (!enabled) { + return; + } + CanvasItem *item = Object::cast_to(ObjectDB::get_instance(p_command.canvas_item)); + if (!item) { + return; + } + if (item != p_item) { + return; + } + const DrawCommand &c = p_command; + + switch (c.type) { + case DrawCommand::LINE: { + item->draw_line(c.args[0], c.args[1], c.args[2], c.args[3], antialiased); + } break; + case DrawCommand::ARROW: { + Vector2 from = c.args[0]; + Vector2 to = c.args[1]; + Color color = c.args[2]; + float line_width = c.args[3]; + Vector2 tip_size = c.args[4]; + float tip_offset = c.args[5]; + + Vector2 vector = to - from; + real_t half_length = vector.length() * 0.5; + tip_size = Vector2(MAX(tip_size.x, line_width), tip_size.y); + + if (half_length < tip_size.y) { + float ratio = half_length / tip_size.y; + tip_size.y *= ratio; + tip_size.x = MAX(tip_size.x * ratio, line_width); + } + Transform2D trans(vector.angle(), to); + Vector2 dest; + if (tip_offset <= 0.0) { + dest = trans.xform(Vector2(-tip_size.y, 0)); + } else { + dest = to; + } + item->draw_line(from, dest, color, line_width, antialiased); + + Vector tip; + Vector2 shift = Vector2(-vector.length() * tip_offset, 0); + tip.push_back(trans.xform(shift)); + tip.push_back(trans.xform(Vector2(-tip_size.y + shift.x, tip_size.x * 0.5))); + tip.push_back(trans.xform(Vector2(-tip_size.y + shift.x, -tip_size.x * 0.5))); + + // Could use `draw_primitive()`, but it doesn't support antialiasing. + item->draw_colored_polygon(tip, color, Vector(), nullptr, nullptr, antialiased); + } break; + case DrawCommand::POLYLINE: { + item->draw_polyline(c.args[0], c.args[1], c.args[2], antialiased); + } break; + case DrawCommand::POLYGON: { + // Godot's `draw_polygon()` is not as robust as it could be. + // The following works better for rendering degenerate and + // self-intersecting polygons. + Vector polygon = c.args[0]; + Color color = c.args[1]; + bool filled = c.args[2]; + float width = c.args[3]; + + if (filled) { + const Vector> &triangles = GoostGeometry2D::triangulate_polygon(polygon); + if (triangles.empty()) { + break; + } + Vector vertices; + for (int i = 0; i < triangles.size(); ++i) { + const Vector &part = triangles[i]; + for (int j = 0; j < part.size(); ++j) { + vertices.push_back(part[j]); + } + } + const int indices_count = triangles.size() * 3; + Vector indices; + for (int i = 0; i < indices_count; ++i) { + indices.push_back(i); + } + Vector colors; + colors.push_back(color); + + VS::get_singleton()->canvas_item_add_triangle_array( + item->get_canvas_item(), indices, vertices, colors, Vector(), + Vector(), Vector(), RID(), -1, RID(), antialiased); + } else { + polygon.push_back(polygon[0]); // Close it. + item->draw_polyline(polygon, color, width, antialiased); + } + } break; + case DrawCommand::REGION: { + bool filled = c.args[2]; + // Get rid of annoying warnings, because that's an implementation detail. + if (filled) { + item->draw_rect(c.args[0], c.args[1], true); + } else { + item->draw_rect(c.args[0], c.args[1], false, c.args[3], antialiased); + } + } break; + case DrawCommand::RECTANGLE: { + Vector2 extents = c.args[0]; + Vector2 position = c.args[1]; + Color color = c.args[2]; + bool filled = c.args[3]; + + Rect2 rect = Rect2(position - extents, extents * 2); + if (filled) { + item->draw_rect(rect, color, true); + } else { + item->draw_rect(rect, color, false, c.args[4], antialiased); + } + } break; + case DrawCommand::CIRCLE: { + // Godot's `draw_circle()` produces fixed number of vertices. + // The following works better with large circles. + const Vector &circle = GoostGeometry2D::circle(c.args[0]); + _draw_shape(item, circle, c.args[1], c.args[2], c.args[3], c.args[4]); + } break; + case DrawCommand::TEXT: { + const Ref &font = Theme::get_default()->get_font("font", "Control"); + String text = c.args[0]; + Vector2 position = c.args[1]; + Color color = c.args[2]; + item->draw_string(font, position, text, color); + } break; + case DrawCommand::TRANSFORM: { + item->draw_set_transform_matrix(c.args[0]); + } break; + case DrawCommand::CUSTOM: { + String method = c.args[0]; + if (!item->has_method(method)) { + method = "draw_" + method; // Try this one. + } + item->callv(method, c.args[1]); + } break; + } +#endif +} + +void Debug2D::_draw_shape(CanvasItem *p_item, const Vector &p_points, const Vector2 &p_position, const Color &p_color, bool p_filled, float p_width) { + Vector points; + for (int i = 0; i < p_points.size(); ++i) { + points.push_back(p_points[i] + p_position); + } + if (p_filled) { + p_item->draw_colored_polygon(points, p_color, Vector(), nullptr, nullptr, antialiased); + } else { + points.push_back(points[0]); + p_item->draw_polyline(points, p_color, p_width, antialiased); + } +} + +void Debug2D::capture() { + state->snapshots.push_back(capture_begin); + state->snapshots.push_back(capture_end); + + capture_begin = capture_end; + capture_end = commands.size(); +} + +void Debug2D::update() { + if (!is_inside_tree()) { + return; + } + if (update_queued) { + return; + } + update_queued = true; + call_deferred("_update_draw_commands"); +} + +void Debug2D::_update_draw_commands() { + for (int i = 0; i < commands.size(); ++i) { + CanvasItem *item = Object::cast_to(ObjectDB::get_instance(commands[i].canvas_item)); + if (!item) { + continue; + } + item->update(); + } + update_queued = false; +} + +void Debug2D::clear() { + for (int i = 0; i < commands.size(); ++i) { + CanvasItem *item = Object::cast_to(ObjectDB::get_instance(commands[i].canvas_item)); + if (!item) { + continue; + } + if (item->is_connected(SceneStringNames::get_singleton()->draw, this, "_on_canvas_item_draw")) { + item->disconnect(SceneStringNames::get_singleton()->draw, this, "_on_canvas_item_draw"); + } + } + commands.clear(); + state->snapshots.clear(); + + capture_begin = 0; + capture_end = 0; + + set_canvas_item(base); + + draw_reset(); + update(); +} + +void Debug2D::_on_canvas_item_draw(Object *p_item) { +#ifdef DEBUG_ENABLED + if (!enabled) { + return; + } + CanvasItem *item = Object::cast_to(p_item); + ERR_FAIL_NULL(item); + + int snapshot_idx = 0; + int begin = capture_begin; + int end = capture_end; + + for (int i = 0; i < state->snapshots.size(); i += 2) { + begin = state->snapshots[i]; + end = state->snapshots[i + 1]; + + if (!state->accumulate) { + if (snapshot_idx != state->snapshot) { + // Do not accumulate drawings on top of each command. + ++snapshot_idx; + continue; + } + } + for (int j = begin; j < end; ++j) { + _draw_command(commands[j], item); + } + ++snapshot_idx; + if (state->snapshot >= 0 && snapshot_idx > state->snapshot) { + // Stop drawing at this point. + break; + } + } + // Process rest of the commands that were not explicitly captured. + // These type of commands will be drawn regardless. + if (state->snapshots.empty()) { + for (int j = 0; j < commands.size(); ++j) { + _draw_command(commands[j], item); + } + } else { + begin = state->snapshots[state->snapshots.size() - 1]; + end = commands.size(); + for (int j = begin; j < end; ++j) { + _draw_command(commands[j], item); + } + } +#endif +} + +Debug2D::Debug2D() { + ERR_FAIL_COND_MSG(singleton != nullptr, "Singleton already exists"); + singleton = this; + state.instance(); + + base = memnew(Node2D); + set_canvas_item(base); + + base->set_name("Canvas"); + add_child(base); + + draw_reset(); + + default_value["color"] = GLOBAL_GET("debug/draw/2d/color"); + default_value["filled"] = GLOBAL_GET("debug/draw/2d/filled"); + default_value["line_width"] = GLOBAL_GET("debug/draw/2d/line_width"); + + antialiased = GLOBAL_GET("debug/draw/2d/antialiased"); +} + +void Debug2D::_bind_methods() { + Color color = GLOBAL_GET("debug/draw/2d/color"); + bool filled = GLOBAL_GET("debug/draw/2d/filled"); + float line_width = GLOBAL_GET("debug/draw/2d/line_width"); + + ClassDB::bind_method(D_METHOD("_on_canvas_item_draw", "item"), &Debug2D::_on_canvas_item_draw); + ClassDB::bind_method(D_METHOD("_update_draw_commands"), &Debug2D::_update_draw_commands); + + ClassDB::bind_method(D_METHOD("set_enabled", "enabled"), &Debug2D::set_enabled); + ClassDB::bind_method(D_METHOD("is_enabled"), &Debug2D::is_enabled); + + ClassDB::bind_method(D_METHOD("set_canvas_item", "canvas_item"), &Debug2D::set_canvas_item); + ClassDB::bind_method(D_METHOD("get_canvas_item"), &Debug2D::get_canvas_item); + ClassDB::bind_method(D_METHOD("get_base"), &Debug2D::get_base); + + ClassDB::bind_method(D_METHOD("draw", "method", "args"), &Debug2D::draw, DEFVAL(Variant())); + + ClassDB::bind_method(D_METHOD("draw_line", "from", "to", "color", "width"), &Debug2D::draw_line, color, line_width); + ClassDB::bind_method(D_METHOD("draw_arrow", "from", "to", "color", "width", "tip_size", "tip_offset"), &Debug2D::draw_arrow, color, line_width, Vector2(8, 8), 0.0); + + ClassDB::bind_method(D_METHOD("draw_polyline", "polyline", "color", "width"), &Debug2D::draw_polyline, color, line_width); + ClassDB::bind_method(D_METHOD("draw_polygon", "polygon", "color", "filled", "width"), &Debug2D::draw_polygon, color, filled, line_width); + + ClassDB::bind_method(D_METHOD("draw_region", "region", "color", "filled", "width"), &Debug2D::draw_region, color, filled, line_width); + + ClassDB::bind_method(D_METHOD("draw_rectangle", "extents", "position", "color", "filled", "width"), &Debug2D::draw_rectangle, color, filled, line_width); + ClassDB::bind_method(D_METHOD("draw_circle", "radius", "position", "color", "filled", "width"), &Debug2D::draw_circle, DEFVAL(Vector2()), color, filled, line_width); + + ClassDB::bind_method(D_METHOD("draw_text", "text", "position", "color"), &Debug2D::draw_text, DEFVAL(Vector2()), Color(1, 1, 1)); // White by default! + + ClassDB::bind_method(D_METHOD("draw_set_transform", "position", "rotation", "scale"), &Debug2D::draw_set_transform, DEFVAL(0), DEFVAL(Size2(1, 1))); + ClassDB::bind_method(D_METHOD("draw_set_transform_matrix", "matrix"), &Debug2D::draw_set_transform_matrix); + + ClassDB::bind_method(D_METHOD("draw_set_color", "color"), &Debug2D::draw_set_color); + ClassDB::bind_method(D_METHOD("draw_set_filled", "filled"), &Debug2D::draw_set_filled); + ClassDB::bind_method(D_METHOD("draw_set_line_width", "width"), &Debug2D::draw_set_line_width); + ClassDB::bind_method(D_METHOD("draw_reset", "option"), &Debug2D::draw_reset, DEFVAL("")); + + ClassDB::bind_method(D_METHOD("get_capture"), &Debug2D::get_capture); + ClassDB::bind_method(D_METHOD("capture"), &Debug2D::capture); + + ClassDB::bind_method(D_METHOD("update"), &Debug2D::update); + ClassDB::bind_method(D_METHOD("clear"), &Debug2D::clear); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "enabled"), "set_enabled", "is_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "canvas_item"), "set_canvas_item", "get_canvas_item"); +} + +// DebugCapture + +void DebugCapture::draw(int p_index) { + ERR_FAIL_INDEX(p_index, get_count()); + snapshot = p_index; + Debug2D::get_singleton()->update(); +} + +void DebugCapture::draw_next() { + snapshot = CLAMP(snapshot + 1, 0, get_count() - 1); + Debug2D::get_singleton()->update(); +} + +void DebugCapture::draw_prev() { + snapshot = CLAMP(snapshot - 1, 0, get_count() - 1); + Debug2D::get_singleton()->update(); +} + +void DebugCapture::set_accumulate(bool p_accumulate) { + accumulate = p_accumulate; + Debug2D::get_singleton()->update(); +} + +void DebugCapture::reset() { + snapshot = -1; + Debug2D::get_singleton()->update(); +} + +void DebugCapture::_bind_methods() { + ClassDB::bind_method(D_METHOD("draw", "index"), &DebugCapture::draw); + ClassDB::bind_method(D_METHOD("draw_next"), &DebugCapture::draw_next); + ClassDB::bind_method(D_METHOD("draw_prev"), &DebugCapture::draw_prev); + + ClassDB::bind_method(D_METHOD("get_count"), &DebugCapture::get_count); + + ClassDB::bind_method(D_METHOD("reset"), &DebugCapture::reset); + + ClassDB::bind_method(D_METHOD("set_accumulate", "accumulate"), &DebugCapture::set_accumulate); + ClassDB::bind_method(D_METHOD("is_accumulating"), &DebugCapture::is_accumulating); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "accumulate"), "set_accumulate", "is_accumulating"); +} diff --git a/scene/2d/debug_2d.h b/scene/2d/debug_2d.h new file mode 100644 index 00000000..67f9345e --- /dev/null +++ b/scene/2d/debug_2d.h @@ -0,0 +1,130 @@ +#pragma once + +#include "core/engine.h" +#include "core/resource.h" +#include "scene/2d/node_2d.h" + +class DebugCapture; + +class Debug2D : public Node { + GDCLASS(Debug2D, Node); + +private: + static Debug2D *singleton; + + bool enabled = true; + + Node2D *base = nullptr; + ObjectID canvas_item; // Currently used item passed to draw commands. + + Dictionary draw_override; + HashMap default_value; + bool antialiased = false; + + struct DrawCommand { + enum Type { + LINE, + ARROW, + POLYLINE, + POLYGON, + REGION, + RECTANGLE, + CIRCLE, + TEXT, + TRANSFORM, + CUSTOM, + }; + ObjectID canvas_item; + Type type; + Vector args; + }; + Vector commands; + + Ref state; + int capture_begin = 0; + int capture_end = 0; + + bool update_queued = false; + +protected: + static void _bind_methods(); + + void _on_canvas_item_draw(Object *p_item); + void _push_command(DrawCommand &p_command); + void _draw_command(const DrawCommand &p_command, CanvasItem *p_item); + void _draw_shape(CanvasItem *p_item, const Vector &p_points, const Vector2 &p_position, const Color &p_color, bool p_filled, float p_width); + void _update_draw_commands(); + + Variant _option_get_value(const String &p_option, const Variant &p_value); + +public: + static Debug2D *get_singleton() { return singleton; } + + void set_enabled(bool p_enabled); + bool is_enabled() const { return enabled; } + + void set_canvas_item(Object *p_canvas_item); + Object *get_canvas_item() const; + Object *get_base() const { return base; } // The default canvas item. + + // Custom drawing using one of the `CanvasItem.draw_*` methods. + void draw(const StringName &p_method, const Array &p_args = Array()); + + // Geometry primitives. + void draw_line(const Point2 &p_from, const Point2 &p_to, const Color &p_color = Color(1, 1, 1), float p_width = 1.0); + void draw_arrow(const Point2 &p_from, const Point2 &p_to, const Color &p_color = Color(1, 1, 1), float p_width = 1.0, const Vector2 &p_tip_size = Vector2(8, 8), float p_tip_offset = 0.0); + + void draw_polyline(const Vector &p_polyline, const Color &p_color = Color(1, 1, 1), float p_width = 1.0); + void draw_polygon(const Vector &p_polygon, const Color &p_color = Color(1, 1, 1), bool p_filled = true, float p_width = 1.0); + + void draw_region(const Rect2 &p_region, const Color &p_color = Color(1, 1, 1), bool p_filled = true, float p_width = 1.0); + + void draw_rectangle(const Vector2 &p_extents, const Vector2 &p_position = Vector2(), const Color &p_color = Color(1, 1, 1), bool p_filled = true, float p_width = 1.0); + void draw_circle(real_t p_radius, const Vector2 &p_position = Vector2(), const Color &p_color = Color(1, 1, 1), bool p_filled = true, float p_width = 1.0); + + // User interface. + void draw_text(const String &p_text, const Vector2 &p_position = Vector2(), const Color &p_color = Color(1, 1, 1)); + + // Transform modifiers. + void draw_set_transform(const Point2 &p_offset, float p_rotation = 0.0, const Size2 &p_scale = Size2(1, 1)); + void draw_set_transform_matrix(const Transform2D &p_matrix); + + void draw_set_color(const Color &p_color); + void draw_set_filled(bool p_filled); + void draw_set_line_width(real_t p_width); + void draw_reset(const String &p_option = ""); + + // History of draw commands. + void capture(); + Ref get_capture() const { return state; } + + void update(); + void clear(); + + Debug2D(); +}; + +class DebugCapture : public Reference { + GDCLASS(DebugCapture, Reference); + + friend class Debug2D; + +protected: + static void _bind_methods(); + + Vector snapshots; + int snapshot = -1; + bool accumulate = true; + +public: + void draw(int p_index); + void draw_next(); + void draw_prev(); + + int get_count() { return snapshots.size() / 2; } + + void set_accumulate(bool p_accumulate); + bool is_accumulating() const { return accumulate; } + + void reset(); +}; diff --git a/scene/2d/visual_shape_2d.h b/scene/2d/visual_shape_2d.h index 78b970a2..96a34890 100644 --- a/scene/2d/visual_shape_2d.h +++ b/scene/2d/visual_shape_2d.h @@ -44,7 +44,5 @@ class VisualShape2D : public Node2D { bool is_debug_sync_visible_collision_shapes() const; String get_configuration_warning() const; - - VisualShape2D(){}; }; diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index f3fdde06..ec6e1671 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -1,12 +1,43 @@ #include "register_scene_types.h" -#include "physics/register_physics_types.h" +#include "2d/debug_2d.h" #include "audio/register_audio_types.h" +#include "physics/register_physics_types.h" + +#include "scene/main/scene_tree.h" +#include "scene/main/viewport.h" + +#include "core/goost_engine.h" #include "goost/classes_enabled.gen.h" namespace goost { +#if defined(GOOST_GEOMETRY_ENABLED) && defined(GOOST_Debug2D) + +static Debug2D *_debug_2d = nullptr; +static bool _debug_2d_added = false; + +static void _debug_2d_add_to_scene_tree() { + // This is quite hacky, but couldn't find another way. + // `SceneTree` is not accessible during `register_module_types()` in Godot. + // This is meant to be to replicate autoload behavior. + if (_debug_2d_added) { + return; + } + auto tree = SceneTree::get_singleton(); + if (!tree) { + return; + } + Debug2D::get_singleton()->set_name("Debug2D"); + tree->get_root()->add_child(Debug2D::get_singleton()); + + Debug2D::get_singleton()->set_enabled(GLOBAL_GET("debug/draw/2d/enabled")); + + _debug_2d_added = true; +} +#endif + void register_scene_types() { #if defined(GOOST_GEOMETRY_ENABLED) && defined(GOOST_PolyNode2D) ClassDB::register_class(); @@ -20,6 +51,22 @@ void register_scene_types() { ClassDB::register_class(); ClassDB::register_class(); +#if defined(GOOST_GEOMETRY_ENABLED) && defined(GOOST_Debug2D) + // Define project settings before registering classes. + GLOBAL_DEF("debug/draw/2d/enabled", true); + GLOBAL_DEF("debug/draw/2d/color", Color(0.0, 0.6, 0.7, 1)); + GLOBAL_DEF("debug/draw/2d/filled", true); + GLOBAL_DEF("debug/draw/2d/line_width", 1.0); + ProjectSettings::get_singleton()->set_custom_property_info("debug/draw/2d/line_width", PropertyInfo(Variant::REAL, "debug/draw/2d/line_width", PROPERTY_HINT_RANGE, "0.1,5.0,0.1,or_greater")); + GLOBAL_DEF("debug/draw/2d/antialiased", false); + + ClassDB::register_class(); + ClassDB::register_virtual_class(); + + _debug_2d = memnew(Debug2D); + Engine::get_singleton()->add_singleton(Engine::Singleton("Debug2D", Debug2D::get_singleton())); + SceneTree::add_idle_callback(&_debug_2d_add_to_scene_tree); +#endif #ifdef GOOST_AUDIO_ENABLED register_audio_types(); #endif @@ -43,6 +90,13 @@ void register_scene_types() { } void unregister_scene_types() { +#if defined(GOOST_GEOMETRY_ENABLED) && defined(GOOST_Debug2D) + // There's no need to free `Debug2D` instance manually because it's added to + // the `SceneTree`, but lets play safe and prevent memory leak in any case. + if (_debug_2d && ObjectDB::instance_validate(_debug_2d)) { + memdelete(_debug_2d); + } +#endif #ifdef GOOST_AUDIO_ENABLED unregister_audio_types(); #endif