From a1d903c743e8bc246d0451b35646b98307a124e3 Mon Sep 17 00:00:00 2001 From: Mikko Mononen Date: Fri, 24 Apr 2026 13:44:23 +0300 Subject: [PATCH] DrawList: added more robust AddConvexPolyFilled(). (2183, 7972) - add bevel handling for sharp corners - add limit to the miter distance to avoid inner shape spikes - add support for ImDrawFlags_MiterOnly to keep well behaved rendering fast --- imgui.h | 5 +- imgui_draw.cpp | 219 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 178 insertions(+), 46 deletions(-) diff --git a/imgui.h b/imgui.h index acb8b439b..aa706eb2f 100644 --- a/imgui.h +++ b/imgui.h @@ -3548,7 +3548,8 @@ struct ImDrawList // - Concave polygon fill is more expensive than convex one: it has O(N^2) complexity. Provided as a convenience for the user but not used by the main library. IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, float thickness, ImDrawFlags flags = 0); IMGUI_API void AddPolylineLegacy(const ImVec2* points, int num_points, ImU32 col, float thickness, ImDrawFlags flags = 0); - IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col); + IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags = 0); + IMGUI_API void AddConvexPolyFilledLegacy(const ImVec2* points, int num_points, ImU32 col); IMGUI_API void AddConcavePolyFilled(const ImVec2* points, int num_points, ImU32 col); // Image primitives @@ -3565,7 +3566,7 @@ struct ImDrawList inline void PathClear() { _Path.Size = 0; } inline void PathLineTo(const ImVec2& pos) { _Path.push_back(pos); } inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); } - inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } + inline void PathFillConvex(ImU32 col, ImDrawFlags flags = 0) { AddConvexPolyFilled(_Path.Data, _Path.Size, col, flags); _Path.Size = 0; } inline void PathFillConcave(ImU32 col) { AddConcavePolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } inline void PathStroke(ImU32 col, float thickness = 1.0f, ImDrawFlags flags = 0) { AddPolyline(_Path.Data, _Path.Size, col, thickness, flags); _Path.Size = 0; } IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); diff --git a/imgui_draw.cpp b/imgui_draw.cpp index ba5351121..d81ff87a7 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -1047,18 +1047,6 @@ void ImDrawList::_AddPolylineThin(const ImVec2* points, ImVec2* normals, float* // Bevel if (sin_theta < 0.0f) { -/* IM_APPEND_VTX(pt_x - sd_x, pt_y - sd_y, uv0, col); - const int next_base_idx = (ImDrawIdx)_VtxCurrentIdx; - IM_APPEND_VTX(pt_x + sd_x, pt_y + sd_y, uv0, col); - IM_APPEND_VTX(p1.x + miter_offset_x, p1.y + miter_offset_y, uv1, col); - - // Bevel tri - IM_APPEND_TRI(base_idx+2, base_idx+3, base_idx+4); - // Connect prev to next - IM_APPEND_TRI(base_idx+0, base_idx+2, base_idx+4); - IM_APPEND_TRI(base_idx+0, base_idx+4, base_idx+1); - base_idx = next_base_idx;*/ - IM_APPEND_VTX(pt_x - sd_x, pt_y - sd_y, uv0, col); // 2 IM_APPEND_VTX(p1.x, p1.y, uv2, col); // 3 const int next_base_idx = (ImDrawIdx)_VtxCurrentIdx; @@ -1090,18 +1078,6 @@ void ImDrawList::_AddPolylineThin(const ImVec2* points, ImVec2* normals, float* IM_APPEND_TRI(base_idx + 3, base_idx + 4, base_idx + 5); IM_APPEND_TRI(base_idx + 3, base_idx + 5, base_idx + 2); base_idx = next_base_idx; - -/* IM_APPEND_VTX(pt_x + sd_x, pt_y + sd_y, uv1, col); - const int next_base_idx = (ImDrawIdx)_VtxCurrentIdx; - IM_APPEND_VTX(p1.x - miter_offset_x, p1.y - miter_offset_y, uv0, col); - IM_APPEND_VTX(pt_x - sd_x, pt_y - sd_y, uv1, col); - - // Bevel tri - IM_APPEND_TRI(base_idx + 3, base_idx + 4, base_idx + 2); - // Connect prev to next - IM_APPEND_TRI(base_idx + 0, base_idx + 3, base_idx + 2); - IM_APPEND_TRI(base_idx + 0, base_idx + 2, base_idx + 1); - base_idx = next_base_idx;*/ } } } @@ -1831,7 +1807,161 @@ void ImDrawList::AddPolylineLegacy(const ImVec2* points, const int points_count, // - We intentionally avoid using ImVec2 and its math operators here to reduce cost to a minimum for debug/non-inlined builds. // - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. -void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_count, ImU32 col) +void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_count, ImU32 col, ImDrawFlags flags) +{ + if (points_count < 3 || (col & IM_COL32_A_MASK) == 0) + return; + +/* if (ImGui::GetIO().KeyShift) + { + AddConvexPolyFilledLegacy(points, points_count, col); + return; + }*/ + + const ImVec2 uv = _Data->TexUvWhitePixel; + const float half_aa = _FringeScale * 0.5f; + ImU32 col_trans = col & ~IM_COL32_A_MASK; + const bool miters_only = (flags & ImDrawFlags_MiterOnly) != 0; + + const int idx_count = ((points_count - 2) + points_count * 3) * 3; + const int vtx_count = (points_count * 3); + PrimReserve(idx_count, vtx_count); + ImDrawVert* start_vtx_ptr = _VtxWritePtr; + ImDrawIdx* start_idx_ptr = _IdxWritePtr; + + // Compute normals and segment lengths + _Data->TempBuffer.reserve_discard(points_count * 2); + ImVec2* temp_normals = _Data->TempBuffer.Data; + float* temp_sqr_lengths = (float*)(_Data->TempBuffer.Data + points_count); + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + const ImVec2& p0 = points[i0]; + const ImVec2& p1 = points[i1]; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + float d2 = dx*dx + dy*dy; + if (d2 > 0.f) + { + float inv_len = ImRsqrt(d2); + dx *= inv_len; + dy *= inv_len; + } + temp_normals[i0].x = dy; + temp_normals[i0].y = -dx; + temp_sqr_lengths[i0] = d2; + } + + + unsigned int vtx_inner_idx = _VtxCurrentIdx; + ImDrawVert* inner_vtx_ptr = _VtxWritePtr; + _VtxWritePtr += points_count; + _VtxCurrentIdx += points_count; + + unsigned int prev_outer_idx = (unsigned int )-1; // We dont know oueter vert could yet, will need to patch once we're done. + + const float miter_distance_limit = half_aa * IM_POLYLINE_MITER_LIMIT; + const float miter_distance_limit_sqr = miter_distance_limit * miter_distance_limit; + + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + // Average normals + const ImVec2& n0 = temp_normals[i0]; + const ImVec2& n1 = temp_normals[i1]; + + // theta is the angle between two segments + const float cos_theta = n0.x * n1.x + n0.y * n1.y; + // miter offset formula is derived here: https://www.angusj.com/clipper2/Docs/Trigonometry.htm + float miter_scale_factor = ImMin(1000.f, (cos_theta > IM_POLYLINE_MITER_ANGLE_LIMIT) ? (half_aa / (1.0f + cos_theta)) : FLT_MAX); // avoid division by zero + + float dm_x = (n0.x + n1.x) * miter_scale_factor; + float dm_y = (n0.y + n1.y) * miter_scale_factor; + bool bevel = false; + + if (!miters_only) + { + const float miter_distance_sqr = dm_x * dm_x + dm_y * dm_y; + bevel = miter_distance_sqr > miter_distance_limit_sqr; + + const float segment_limit_sqr = ImMax(temp_sqr_lengths[i0], temp_sqr_lengths[i1]); + const bool overshoot = miter_distance_sqr > segment_limit_sqr; + if (overshoot) + { + // Make sure the miter stays within the polygon bounds. + const float scale = ImSqrt(segment_limit_sqr / miter_distance_sqr); + dm_x *= scale; + dm_y *= scale; + } + } + + // Inner + inner_vtx_ptr->pos.x = (points[i1].x - dm_x); inner_vtx_ptr->pos.y = (points[i1].y - dm_y); inner_vtx_ptr->uv = uv; inner_vtx_ptr->col = col; + inner_vtx_ptr++; + + unsigned int prev_inner_idx = vtx_inner_idx + i0; + unsigned int inner_idx = vtx_inner_idx + i1; + unsigned int outer_idx = _VtxCurrentIdx; + + // Outer + if (bevel) + { + _VtxWritePtr->pos.x = (points[i1].x + n0.x * half_aa); _VtxWritePtr->pos.y = (points[i1].y + n0.y * half_aa); _VtxWritePtr->uv = uv; _VtxWritePtr->col = col_trans; + _VtxWritePtr++; + _VtxCurrentIdx++; + _VtxWritePtr->pos.x = (points[i1].x + n1.x * half_aa); _VtxWritePtr->pos.y = (points[i1].y + n1.y * half_aa); _VtxWritePtr->uv = uv; _VtxWritePtr->col = col_trans; + _VtxWritePtr++; + _VtxCurrentIdx++; + } + else + { + _VtxWritePtr->pos.x = (points[i1].x + dm_x); _VtxWritePtr->pos.y = (points[i1].y + dm_y); _VtxWritePtr->uv = uv; _VtxWritePtr->col = col_trans; + _VtxWritePtr++; + _VtxCurrentIdx++; + } + + // Connect with previous + _IdxWritePtr[0] = (ImDrawIdx)prev_outer_idx; + _IdxWritePtr[1] = (ImDrawIdx)outer_idx; + _IdxWritePtr[2] = (ImDrawIdx)inner_idx; + + _IdxWritePtr[3] = (ImDrawIdx)prev_outer_idx; + _IdxWritePtr[4] = (ImDrawIdx)inner_idx; + _IdxWritePtr[5] = (ImDrawIdx)prev_inner_idx; + + _IdxWritePtr += 6; + + // Fill bevel + if (bevel) + { + _IdxWritePtr[0] = (ImDrawIdx)outer_idx; + _IdxWritePtr[1] = (ImDrawIdx)outer_idx+1; + _IdxWritePtr[2] = (ImDrawIdx)inner_idx; + _IdxWritePtr += 3; + outer_idx++; + } + + prev_outer_idx = outer_idx; + } + + // Patch first segment + start_idx_ptr[0] = (ImDrawIdx)prev_outer_idx; + start_idx_ptr[3] = (ImDrawIdx)prev_outer_idx; + + // Add indices for fill + for (int i = 2; i < points_count; i++) + { + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + i - 1); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx + i); + _IdxWritePtr += 3; + } + + const int idx_used = (int)(_IdxWritePtr - start_idx_ptr); + const int vtx_used = (int)(_VtxWritePtr - start_vtx_ptr); + IM_ASSERT(idx_used <= idx_count && vtx_used <= vtx_count); + + if (idx_used < idx_count || vtx_used < vtx_count) + PrimUnreserve(idx_count - idx_used, vtx_count - vtx_used); +} + +void ImDrawList::AddConvexPolyFilledLegacy(const ImVec2* points, const int points_count, ImU32 col) { if (points_count < 3 || (col & IM_COL32_A_MASK) == 0) return; @@ -1913,6 +2043,7 @@ void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_coun } } + void ImDrawList::_PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step) { if (radius < 0.5f) @@ -2507,10 +2638,10 @@ void ImDrawList::_AddRectTinyRounding(const ImVec2& p_min, const ImVec2& p_max, const ImVec2 inner_uv1(tex_uvs.x + ((0.5f + thickness - stem_offset) / _FringeScale) * _Data->FontAtlas->TexUvScale.x, tex_uvs.y); int base_idx = (int)_VtxCurrentIdx; - IM_POLYLINE_APPEND_VERTEX(0, 0, outer_uv0, col); - IM_POLYLINE_APPEND_VERTEX(0, 0, outer_uv1, col); - IM_POLYLINE_APPEND_VERTEX(0, 0, inner_uv1, col); - IM_POLYLINE_APPEND_VERTEX(0, 0, inner_uv0, col); + IM_APPEND_VTX(0, 0, outer_uv0, col); + IM_APPEND_VTX(0, 0, outer_uv1, col); + IM_APPEND_VTX(0, 0, inner_uv1, col); + IM_APPEND_VTX(0, 0, inner_uv0, col); for (int corner = 0; corner < 4; corner++) { @@ -2526,34 +2657,34 @@ void ImDrawList::_AddRectTinyRounding(const ImVec2& p_min, const ImVec2& p_max, for (int i = 0; i < arc_point_count; i++) { const ImVec2 dir = _Data->ArcFastVtx[a]; - IM_POLYLINE_APPEND_VERTEX(center.x - dir.x * rounding, center.y - dir.y * rounding, outer_uv0, col); + IM_APPEND_VTX(center.x - dir.x * rounding, center.y - dir.y * rounding, outer_uv0, col); a = (a + arc_step) % IM_DRAWLIST_ARCFAST_TABLE_SIZE; } } else { - IM_POLYLINE_APPEND_VERTEX(corner_pos[corner].x, corner_pos[corner].y, outer_uv0, col); + IM_APPEND_VTX(corner_pos[corner].x, corner_pos[corner].y, outer_uv0, col); } const int stem0_idx = (int)_VtxCurrentIdx; - IM_POLYLINE_APPEND_VERTEX(stem.x, stem.y, outer_uv1, col); + IM_APPEND_VTX(stem.x, stem.y, outer_uv1, col); const int stem1_idx = (int)_VtxCurrentIdx; - IM_POLYLINE_APPEND_VERTEX(stem.x, stem.y, inner_uv1, col); + IM_APPEND_VTX(stem.x, stem.y, inner_uv1, col); const int inner_idx = (int)_VtxCurrentIdx; - IM_POLYLINE_APPEND_VERTEX(inner.x, inner.y, inner_uv0, col); + IM_APPEND_VTX(inner.x, inner.y, inner_uv0, col); // Arc if (is_rounded) { for (int i = 0; i < arc_point_count-1; i++) - IM_POLYLINE_APPEND_TRI(stem0_idx, arc_idx + i, arc_idx + i + 1); + IM_APPEND_TRI(stem0_idx, arc_idx + i, arc_idx + i + 1); } // Connect with previous - IM_POLYLINE_APPEND_TRI(base_idx+0, arc_idx, stem0_idx); - IM_POLYLINE_APPEND_TRI(base_idx+0, stem0_idx, base_idx+1); - IM_POLYLINE_APPEND_TRI(base_idx+2, stem1_idx, inner_idx); - IM_POLYLINE_APPEND_TRI(base_idx+2, inner_idx, base_idx+3); + IM_APPEND_TRI(base_idx + 0, arc_idx, stem0_idx); + IM_APPEND_TRI(base_idx + 0, stem0_idx, base_idx + 1); + IM_APPEND_TRI(base_idx + 2, stem1_idx, inner_idx); + IM_APPEND_TRI(base_idx + 2, inner_idx, base_idx + 3); base_idx = stem0_idx - 1; @@ -2868,7 +2999,7 @@ void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 c else { PathRect(p_min, p_max, rounding, flags); - PathFillConvex(col); + PathFillConvex(col, ImDrawFlags_MiterOnly); } } @@ -3023,7 +3154,7 @@ void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); } - PathFillConvex(col); + PathFillConvex(col, ImDrawFlags_MiterOnly); } // Guaranteed to honor 'num_segments' @@ -3078,7 +3209,7 @@ void ImDrawList::AddNgonFilled(const ImVec2& center, float radius, ImU32 col, in // 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); - PathFillConvex(col); + PathFillConvex(col, ImDrawFlags_MiterOnly); } // Ellipse @@ -3283,7 +3414,7 @@ void ImDrawList::AddImageRounded(ImTextureRef tex_ref, const ImVec2& p_min, cons int vert_start_idx = VtxBuffer.Size; PathRect(p_min, p_max, rounding, flags); - PathFillConvex(col); + PathFillConvex(col, ImDrawFlags_MiterOnly); int vert_end_idx = VtxBuffer.Size; ImGui::ShadeVertsLinearUV(this, vert_start_idx, vert_end_idx, p_min, p_max, uv_min, uv_max, true); @@ -7826,7 +7957,7 @@ void ImGui::RenderRectFilledInRangeH(ImDrawList* draw_list, const ImRect& rect, draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e); // BR } } - draw_list->PathFillConvex(col); + draw_list->PathFillConvex(col, ImDrawFlags_MiterOnly); } void ImGui::RenderRectFilledWithHole(ImDrawList* draw_list, const ImRect& outer, const ImRect& inner, ImU32 col, float rounding)