From 5363af7f47573c8a9231bc44bc6252188affea08 Mon Sep 17 00:00:00 2001 From: omar Date: Wed, 8 Jan 2020 00:10:18 +0100 Subject: [PATCH] AddCircle, AddCircleFilled: Add auto-calculation of circle segment counts (amends) Tweak default max error value, Changelog, comments, path-fast for 12 segments circles, made LUT store ImU8 --- docs/CHANGELOG.txt | 3 ++ imgui.cpp | 9 ++---- imgui.h | 11 ++++--- imgui_demo.cpp | 31 ++++++++++++------- imgui_draw.cpp | 76 ++++++++++++++++++++++++++++------------------ imgui_internal.h | 20 ++++++------ 6 files changed, 89 insertions(+), 61 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 11afd5e4..f7c903f2 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -55,6 +55,7 @@ Breaking Changes: documented (can only unreserve from the last reserve call). If you suspect you ever used that feature before, #define IMGUI_DEBUG_PARANOID in imconfig.h to catch existing calls. [@ShironekoBen] +- ImDrawList::AddCircle()/AddCircleFilled() functions don't accept negative radius. - Limiting Columns()/BeginColumns() api to 64 columns with an assert. While the current code technically supports it, future code may not so we're putting the restriction ahead. - imgui_internal.h: changed ImRect() default constructor initializes all fields to 0.0f instead @@ -70,6 +71,8 @@ Other Changes: those improvements in 1.73 makes them unnecessary. (#2722, #2770). [@rokups] - ColorEdit: "Copy As" context-menu tool shows hex values with a '#' prefix instead of '0x'. - ColorEdit: "Copy As" content-menu tool shows hex values both with/without alpha when available. +- ImDrawList: AddCircle(), AddCircleFilled() API can now auto-tessellate when provided a segment + count of zero. Alter tessellation quality with 'style.CircleSegmentMaxError'. [@ShironekoBen] - ImDrawList: Add AddNgon(), AddNgonFilled() API with a guarantee on the explicit segment count. In the current branch they are essentially the same as AddCircle(), AddCircleFilled() but as we will rework the circle rendering functions to use textures and automatic segment count diff --git a/imgui.cpp b/imgui.cpp index 75b95ba5..6ec427e5 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -362,6 +362,7 @@ CODE When you are not sure about a old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all imgui files. You can read releases logs https://github.com/ocornut/imgui/releases for more details. + - 2020/01/22 (1.75) - ImDrawList::AddCircle()/AddCircleFilled() functions don't accept negative radius any more. - 2019/12/17 (1.75) - made Columns() limited to 64 columns by asserting above that limit. While the current code technically supports it, future code may not so we're putting the restriction ahead. - 2019/12/13 (1.75) - [imgui_internal.h] changed ImRect() default constructor initializes all fields to 0.0f instead of (FLT_MAX,FLT_MAX,-FLT_MAX,-FLT_MAX). If you used ImRect::Add() to create bounding boxes by adding multiple points into it, you may need to fix your initial value. - 2019/12/08 (1.75) - removed redirecting functions/enums that were marked obsolete in 1.53 (December 2017): @@ -988,7 +989,7 @@ ImGuiStyle::ImGuiStyle() AntiAliasedLines = true; // Enable anti-aliasing on lines/borders. Disable if you are really short on CPU/GPU. AntiAliasedFill = true; // Enable anti-aliasing on filled shapes (rounded rectangles, circles, etc.) CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. - CircleSegmentMaxError = 0.75f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + CircleSegmentMaxError = 1.60f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. // Default theme ImGui::StyleColorsDark(this); @@ -3624,7 +3625,7 @@ void ImGui::NewFrame() IM_ASSERT(g.Font->IsLoaded()); g.DrawListSharedData.ClipRectFullscreen = ImVec4(0.0f, 0.0f, g.IO.DisplaySize.x, g.IO.DisplaySize.y); g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; - g.DrawListSharedData.CircleSegmentMaxError = g.Style.CircleSegmentMaxError; + g.DrawListSharedData.SetCircleSegmentMaxError(g.Style.CircleSegmentMaxError); g.DrawListSharedData.InitialFlags = ImDrawListFlags_None; if (g.Style.AntiAliasedLines) g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLines; @@ -3633,10 +3634,6 @@ void ImGui::NewFrame() if (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AllowVtxOffset; - // Recalculate circle segment counts if the segment error has changed - if (g.DrawListSharedData.CircleSegmentMaxError != g.DrawListSharedData.CircleSegmentCountsMaxCircleSegmentError) - g.DrawListSharedData.RecalculateCircleSegmentCounts(); - g.BackgroundDrawList.Clear(); g.BackgroundDrawList.PushTextureID(g.IO.Fonts->TexID); g.BackgroundDrawList.PushClipRectFullScreen(); diff --git a/imgui.h b/imgui.h index 7582aafc..5fab99dc 100644 --- a/imgui.h +++ b/imgui.h @@ -1941,6 +1941,9 @@ struct ImDrawList // Primitives // - For rectangular primitives, "p_min" and "p_max" represent the upper-left and lower-right corners. + // - For circle primitives, use "num_segments == 0" to automatically calculate tessellation (preferred). + // In future versions we will use textures to provide cheaper and higher-quality circles. + // Use AddNgon() and AddNgonFilled() functions if you need to guaranteed a specific number of sides. IMGUI_API void AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness = 1.0f); IMGUI_API void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawCornerFlags rounding_corners = ImDrawCornerFlags_All, float thickness = 1.0f); // a: upper-left, b: lower-right (== upper-left + size), rounding_corners_flags: 4 bits corresponding to which corner to round IMGUI_API void AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawCornerFlags rounding_corners = ImDrawCornerFlags_All); // a: upper-left, b: lower-right (== upper-left + size) @@ -1949,10 +1952,10 @@ struct ImDrawList IMGUI_API void AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col); IMGUI_API void AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness = 1.0f); IMGUI_API void AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col); - IMGUI_API void AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments = 12, float thickness = 1.0f); // Draw a circle - use num_segments <= 0 to automatically calculate tessellation (preferred). Use AddNgon() instead if you need a specific segment count. - IMGUI_API void AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments = 12); // Draw a filled circle - use num_segments <= 0 to automatically calculate tessellation (preferred). Use AddNgonFilled() instead if you need a specific segment count. - IMGUI_API void AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness = 1.0f); // Draw an n-gon with a specific number of sides. Use AddCircle() instead if you want an actual circle and don't care about the exact side count. - IMGUI_API void AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments); // Draw a filled n-gon with a specific number of sides. Use AddCircleFilled() instead if you want an actual circle and don't care about the exact side count. + IMGUI_API void AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments = 12, float thickness = 1.0f); + IMGUI_API void AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments = 12); + IMGUI_API void AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness = 1.0f); + IMGUI_API void AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments); IMGUI_API void AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL); IMGUI_API void AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL, float wrap_width = 0.0f, const ImVec4* cpu_fine_clip_rect = NULL); IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, bool closed, float thickness); diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 8ce91784..05051913 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -3500,9 +3500,9 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::Checkbox("Anti-aliased lines", &style.AntiAliasedLines); ImGui::SameLine(); HelpMarker("When disabling anti-aliasing lines, you'll probably want to disable borders in your style as well."); ImGui::Checkbox("Anti-aliased fill", &style.AntiAliasedFill); ImGui::PushItemWidth(100); - ImGui::DragFloat("Curve Tessellation Tolerance", &style.CurveTessellationTol, 0.02f, 0.10f, FLT_MAX, "%.2f", 2.0f); + ImGui::DragFloat("Curve Tessellation Tolerance", &style.CurveTessellationTol, 0.02f, 0.10f, 10.0f, "%.2f"); if (style.CurveTessellationTol < 0.10f) style.CurveTessellationTol = 0.10f; - ImGui::DragFloat("Max circle segment error", &style.CircleSegmentMaxError, 0.01f, 0.1f, 10.0f, "%.2f", 1.0f); + ImGui::DragFloat("Circle segment Max Error", &style.CircleSegmentMaxError, 0.01f, 0.10f, 10.0f, "%.2f"); ImGui::DragFloat("Global Alpha", &style.Alpha, 0.005f, 0.20f, 1.0f, "%.2f"); // Not exposing zero here so user doesn't "lose" the UI (zero alpha clips all widgets). But application code could have a toggle to switch between zero and non-zero. ImGui::PopItemWidth(); @@ -4455,24 +4455,33 @@ static void ShowExampleAppCustomRendering(bool* p_open) static float sz = 36.0f; static float thickness = 3.0f; static int ngon_sides = 6; + static bool circle_segments_override = false; + static int circle_segments_override_v = 12; static ImVec4 colf = ImVec4(1.0f, 1.0f, 0.4f, 1.0f); + ImGui::PushItemWidth(-ImGui::GetFontSize() * 10); ImGui::DragFloat("Size", &sz, 0.2f, 2.0f, 72.0f, "%.0f"); ImGui::DragFloat("Thickness", &thickness, 0.05f, 1.0f, 8.0f, "%.02f"); - ImGui::SliderInt("n-gon sides", &ngon_sides, 3, 12); + ImGui::SliderInt("N-gon sides", &ngon_sides, 3, 12); + ImGui::Checkbox("##circlesegmentoverride", &circle_segments_override); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + if (ImGui::SliderInt("Circle segments", &circle_segments_override_v, 3, 40)) + circle_segments_override = true; ImGui::ColorEdit4("Color", &colf.x); + ImGui::PopItemWidth(); const ImVec2 p = ImGui::GetCursorScreenPos(); const ImU32 col = ImColor(colf); + const float spacing = 10.0f; + const ImDrawCornerFlags corners_none = 0; + const ImDrawCornerFlags corners_all = ImDrawCornerFlags_All; + const ImDrawCornerFlags corners_tl_br = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotRight; + const int circle_segments = circle_segments_override ? circle_segments_override_v : 0; float x = p.x + 4.0f, y = p.y + 4.0f; - float spacing = 10.0f; - ImDrawCornerFlags corners_none = 0; - ImDrawCornerFlags corners_all = ImDrawCornerFlags_All; - ImDrawCornerFlags corners_tl_br = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotRight; for (int n = 0; n < 2; n++) { // First line uses a thickness of 1.0f, second line uses the configurable thickness float th = (n == 0) ? 1.0f : thickness; - draw_list->AddNgon(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, ngon_sides, th); x += sz + spacing; // n-gon - draw_list->AddCircle(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, 0, th); x += sz + spacing; // Circle + draw_list->AddNgon(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, ngon_sides, th); x += sz + spacing; // N-gon + draw_list->AddCircle(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, circle_segments, th); x += sz + spacing; // Circle draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 0.0f, corners_none, th); x += sz + spacing; // Square draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f, corners_all, th); x += sz + spacing; // Square with all rounded corners draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f, corners_tl_br, th); x += sz + spacing; // Square with two rounded corners @@ -4485,8 +4494,8 @@ static void ShowExampleAppCustomRendering(bool* p_open) x = p.x + 4; y += sz + spacing; } - draw_list->AddNgonFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz*0.5f, col, ngon_sides); x += sz + spacing; // n-gon - draw_list->AddCircleFilled(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, 0); x += sz + spacing; // Circle + draw_list->AddNgonFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz*0.5f, col, ngon_sides); x += sz + spacing; // N-gon + draw_list->AddCircleFilled(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, circle_segments);x += sz + spacing; // Circle draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col); x += sz + spacing; // Square draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f); x += sz + spacing; // Square with all rounded corners draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f, corners_tl_br); x += sz + spacing; // Square with two rounded corners diff --git a/imgui_draw.cpp b/imgui_draw.cpp index 435e689b..2f3370e0 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -349,16 +349,29 @@ ImDrawListSharedData::ImDrawListSharedData() FontSize = 0.0f; CurveTessellationTol = 0.0f; CircleSegmentMaxError = 0.0f; - CircleSegmentCountsMaxCircleSegmentError = -FLT_MIN; // Impossible value to force recalculation ClipRectFullscreen = ImVec4(-8192.0f, -8192.0f, +8192.0f, +8192.0f); InitialFlags = ImDrawListFlags_None; - // Const data + // Lookup tables for (int i = 0; i < IM_ARRAYSIZE(CircleVtx12); i++) { const float a = ((float)i * 2 * IM_PI) / (float)IM_ARRAYSIZE(CircleVtx12); CircleVtx12[i] = ImVec2(ImCos(a), ImSin(a)); } + memset(CircleSegmentCounts, 0, sizeof(CircleSegmentCounts)); // This will be set by +} + +void ImDrawListSharedData::SetCircleSegmentMaxError(float max_error) +{ + if (CircleSegmentMaxError == max_error) + return; + CircleSegmentMaxError = max_error; + for (int i = 0; i < IM_ARRAYSIZE(CircleSegmentCounts); i++) + { + const float radius = i + 1.0f; + const int segment_count = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, CircleSegmentMaxError); + CircleSegmentCounts[i] = (ImU8)ImMin(segment_count, 255); + } } void ImDrawList::Clear() @@ -1085,60 +1098,63 @@ void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImV PathFillConvex(col); } -void ImDrawListSharedData::RecalculateCircleSegmentCounts() -{ - for (int i = 0; i < NumCircleSegmentCounts; i++) - { - const float radius = i + 1.0f; - CircleSegmentCounts[i] = ImClamp((int)((IM_PI * 2.0f) / ImAcos((radius - CircleSegmentMaxError) / radius)), 3, 10000); - } - - CircleSegmentCountsMaxCircleSegmentError = CircleSegmentMaxError; -} - void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) { - if ((col & IM_COL32_A_MASK) == 0 || (radius <= 0.0f)) + if ((col & IM_COL32_A_MASK) == 0 || radius <= 0.0f) return; - // Calculate number of segments if required + // Obtain segment count if (num_segments <= 0) { - int radius_int = (int)radius; - if (radius_int <= ImDrawListSharedData::NumCircleSegmentCounts) - num_segments = _Data->CircleSegmentCounts[radius_int - 1]; // Use cached value + // Automatic segment count + const int radius_idx = (int)radius - 1; + if (radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts)) + num_segments = _Data->CircleSegmentCounts[radius_idx]; // Use cached value else - num_segments = ImClamp((int)((IM_PI * 2.0f) / ImAcos((radius - _Data->CircleSegmentMaxError) / radius)), 3, 10000); + num_segments = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError); + } + else + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); } - else - num_segments = ImClamp(num_segments, 3, 10000); // Clamp to avoid drawing insanely tessellated shapes // Because we are filling a closed shape we remove 1 from the count of segments/points const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; - PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); + if (num_segments == 12) + PathArcToFast(center, radius - 0.5f, 0, 12); + else + PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); PathStroke(col, true, thickness); } void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) { - if ((col & IM_COL32_A_MASK) == 0 || (radius <= 0.0f)) + if ((col & IM_COL32_A_MASK) == 0 || radius <= 0.0f) return; - // Calculate number of segments if required + // Obtain segment count if (num_segments <= 0) { - int radius_int = (int)radius; - if (radius_int <= ImDrawListSharedData::NumCircleSegmentCounts) - num_segments = _Data->CircleSegmentCounts[radius_int - 1]; // Use cached value + // Automatic segment count + const int radius_idx = (int)radius - 1; + if (radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts)) + num_segments = _Data->CircleSegmentCounts[radius_idx]; // Use cached value else - num_segments = ImClamp((int)((IM_PI * 2.0f) / ImAcos((radius - _Data->CircleSegmentMaxError) / radius)), 3, 10000); + num_segments = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError); } else - num_segments = ImClamp(num_segments, 3, 10000); // Clamp to avoid drawing insanely tessellated shapes + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); + } // Because we are filling a closed shape we remove 1 from the count of segments/points const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; - PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); + if (num_segments == 12) + PathArcToFast(center, radius, 0, 12); + else + PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); PathFillConvex(col); } diff --git a/imgui_internal.h b/imgui_internal.h index 6679219d..e1279c05 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -848,7 +848,13 @@ struct ImGuiColumns } }; +// Helper function to calculate a circle's segment count given its radius and a "maximum error" value. +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN 12 +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX 512 +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(_RAD,_MAXERROR) ImClamp((int)((IM_PI * 2.0f) / ImAcos((_RAD - _MAXERROR) / _RAD)), IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX) + // Data shared between all ImDrawList instances +// You may want to create your own instance of this if you want to use ImDrawList completely without ImGui. In that case, watch out for future changes to this structure. struct IMGUI_API ImDrawListSharedData { ImVec2 TexUvWhitePixel; // UV of white pixel in the atlas @@ -859,18 +865,12 @@ struct IMGUI_API ImDrawListSharedData ImVec4 ClipRectFullscreen; // Value for PushClipRectFullscreen() ImDrawListFlags InitialFlags; // Initial flags at the beginning of the frame (it is possible to alter flags on a per-drawlist basis afterwards) - // Const data - // FIXME: Bake rounded corners fill/borders in atlas - ImVec2 CircleVtx12[12]; - - // Cached circle segment counts for the first radii (to avoid calculation overhead) - static const int NumCircleSegmentCounts = 64;// Number of circle segment counts to cache (i.e. the maximum radius before we calculate dynamically) - int CircleSegmentCounts[NumCircleSegmentCounts]; // The segment count for radius (array index + 1) - float CircleSegmentCountsMaxCircleSegmentError; // The MaxCircleSegmentError used to calculate these counts - - void RecalculateCircleSegmentCounts(); // Recalculate circle segment counts based on the current MaxCircleSegmentError + // [Internal] Lookup tables + ImVec2 CircleVtx12[12]; // FIXME: Bake rounded corners fill/borders in atlas + ImU8 CircleSegmentCounts[64]; // Precomputed segment count for given radius (array index + 1) before we calculate it dynamically (to avoid calculation overhead) ImDrawListSharedData(); + void SetCircleSegmentMaxError(float max_error); }; struct ImDrawDataBuilder