Skip to content

Commit

Permalink
Add support for adaptive arc drawing to PathArcTo()
Browse files Browse the repository at this point in the history
  • Loading branch information
thedmd committed Nov 28, 2020
1 parent 9801c8c commit 02b4ac4
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 19 deletions.
3 changes: 2 additions & 1 deletion imgui.h
Original file line number Diff line number Diff line change
Expand Up @@ -2148,8 +2148,9 @@ struct ImDrawList
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; } // Note: Anti-aliased filling requires points to be in clockwise order.
inline void PathStroke(ImU32 col, bool closed, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, closed, thickness); _Path.Size = 0; }
IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 10);
IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); // Pass num_segments = 0 for adaptive tessellation
IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle
IMGUI_API void PathArcToFast2(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step = 0);
IMGUI_API void PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0);
IMGUI_API void PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding = 0.0f, ImDrawCornerFlags rounding_corners = ImDrawCornerFlags_All);

Expand Down
123 changes: 109 additions & 14 deletions imgui_draw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ ImDrawListSharedData::ImDrawListSharedData()
const float a = ((float)i * 2 * IM_PI) / (float)IM_ARRAYSIZE(ArcFastVtx);
ArcFastVtx[i] = ImVec2(ImCos(a), ImSin(a));
}

ArcFastRadiusThreshold = IM_DRAWLIST_ARCFAST_RADIUS_THRESHOLD_CALC(IM_DRAWLIST_ARCFAST_SAMPLES, 1.6f);
}

void ImDrawListSharedData::SetCircleSegmentMaxError(float max_error)
Expand All @@ -366,6 +368,11 @@ void ImDrawListSharedData::SetCircleSegmentMaxError(float max_error)
const int segment_count = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, CircleSegmentMaxError);
CircleSegmentCounts[i] = (ImU8)ImMin(segment_count, 255);
}

if (IM_DRAWLIST_ARCFAST_SAMPLES >= 2 && max_error != 0.0f) // These are constrains of IM_DRAWLIST_ARCFAST_RADIUS_THRESHOLD_CALC
ArcFastRadiusThreshold = IM_DRAWLIST_ARCFAST_RADIUS_THRESHOLD_CALC(IM_DRAWLIST_ARCFAST_SAMPLES, max_error);
else
ArcFastRadiusThreshold = IM_DRAWLIST_ARCFAST_RADIUS_THRESHOLD_CALC(IM_DRAWLIST_ARCFAST_SAMPLES, 1.6f);
}

// Initialize before use in a new frame. We always have a command ready in the buffer.
Expand Down Expand Up @@ -1002,36 +1009,124 @@ void ImDrawList::PathArcToFast(const ImVec2& center, float radius, int a_min_of_
return;
}

// For legacy reason the PathArcToFast() always takes angles where 2*PI is represented by 12,
// but it is possible to set IM_DRAWLIST_ARCFAST_TESSELATION_MULTIPLIER to a higher value. This should compile to a no-op otherwise.
#if IM_DRAWLIST_ARCFAST_TESSELLATION_MULTIPLIER != 1
a_min_of_12 *= IM_DRAWLIST_ARCFAST_TESSELLATION_MULTIPLIER;
a_max_of_12 *= IM_DRAWLIST_ARCFAST_TESSELLATION_MULTIPLIER;
#endif
if (a_min_of_12 == 0 && a_max_of_12 == 11)
{
a_max_of_12 = IM_DRAWLIST_ARCFAST_SAMPLES - 1;
}
else
{
a_min_of_12 = a_min_of_12 * IM_DRAWLIST_ARCFAST_SAMPLES / 12;
a_max_of_12 = a_max_of_12 * IM_DRAWLIST_ARCFAST_SAMPLES / 12;
}

PathArcToFast2(center, radius, a_min_of_12, a_max_of_12);
}

void ImDrawList::PathArcToFast2(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step)
{
if ((a_min_sample > a_max_sample) || (a_step < 0))
return;

if (radius == 0.0f)
{
_Path.push_back(center);
return;
}

if (a_step == 0)
{
// Determine step size between samples. Small radii usually allow to skip more than
// one sample, reduce geometry complexity without harming visual output.
//
// For CircleSegmentMaxError = 1.6 number of of samples is 2.84,
// which is rounded down to 2 (Calculated from unclamped IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC).
// Error calculation is based on number of samples. If we use rounded down value, we
// will always get values greater than CircleSegmentMaxError. This is bad, because
// it will lead to higher step count and to visual artifacts. To keep error values
// under CircleSegmentMaxError we can simply add one to number of samples.
// This will lead to slightly higher than necessary precision for some radii,
// but will never introduce visible artifacts.
const float error_for_radius = IM_DRAWLIST_ARCFAST_RADIUS_ERROR_CALC(IM_DRAWLIST_ARCFAST_SAMPLES + 1, radius);
if (_Data->CircleSegmentMaxError > error_for_radius)
// Reduce step, but never go lower than 2 samples per quarter of the circle.
a_step = ImMin((int)ImSqrt(_Data->CircleSegmentMaxError / error_for_radius), IM_DRAWLIST_ARCFAST_SAMPLES / (4 * 2));
}

const int distance = a_max_sample - a_min_sample;
const bool extra_max_sample = ((a_max_sample - a_min_sample) % a_step) != 0;

_Path.reserve(_Path.Size + (a_max_of_12 - a_min_of_12 + 1));
for (int a = a_min_of_12; a <= a_max_of_12; a++)
const int samples = (distance) / a_step + (extra_max_sample ? 1 : 0);

_Path.reserve(_Path.Size + samples);

for (int a = a_min_sample; a <= a_max_sample; a += a_step)
{
const ImVec2& c = _Data->ArcFastVtx[a % IM_ARRAYSIZE(_Data->ArcFastVtx)];
_Path.push_back(ImVec2(center.x + c.x * radius, center.y + c.y * radius));
}

if (extra_max_sample)
{
const int a = a_max_sample;
const ImVec2& c = _Data->ArcFastVtx[a % IM_ARRAYSIZE(_Data->ArcFastVtx)];
_Path.push_back(ImVec2(center.x + c.x * radius, center.y + c.y * radius));
}
}

void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments)
{
if (a_min >= a_max)
return;

if (radius == 0.0f)
{
_Path.push_back(center);
return;
}

// Note that we are adding a point at both a_min and a_max.
// If you are trying to draw a full closed circle you don't want the overlapping points!
_Path.reserve(_Path.Size + (num_segments + 1));
for (int i = 0; i <= num_segments; i++)
if (num_segments <= 0 && radius <= _Data->ArcFastRadiusThreshold)
{
// Determine first and last sample in lookup table that belong to the arc.
const int a_min_sample = (int)ImCeil(IM_DRAWLIST_ARCFAST_SAMPLES * a_min / (IM_PI * 2.0f));
const int a_max_sample = (int)(IM_DRAWLIST_ARCFAST_SAMPLES * a_max / (IM_PI * 2.0f));
const int a_mid_samples = ImMax(a_max_sample - a_min_sample, 0);

const float a_min_segment_angle = a_min_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLES;
const float a_max_segment_angle = a_max_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLES;

const bool a_emit_start = (a_min_segment_angle - a_min) > 0.0f;
const bool a_emit_end = (a_max - a_max_segment_angle) > 0.0f;

_Path.reserve(_Path.Size + (a_mid_samples + 1 + (a_emit_start ? 1 : 0) + (a_emit_end ? 1 : 0)));

if (a_emit_start)
_Path.push_back(ImVec2(center.x + ImCos(a_min) * radius, center.y + ImSin(a_min) * radius));

if (a_max_sample >= a_min_sample)
PathArcToFast2(center, radius, a_min_sample, a_max_sample);

if (a_emit_end)
_Path.push_back(ImVec2(center.x + ImCos(a_max) * radius, center.y + ImSin(a_max) * radius));
}
else
{
const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min);
_Path.push_back(ImVec2(center.x + ImCos(a) * radius, center.y + ImSin(a) * radius));
if (num_segments <= 0)
{
const float arc_length = a_max - a_min;
const int circle_segment_count = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError);
const int arc_segment_count = ImMax((int)(circle_segment_count * arc_length / (IM_PI * 2.0f)), 2);

num_segments = arc_segment_count;
}

// Note that we are adding a point at both a_min and a_max.
// If you are trying to draw a full closed circle you don't want the overlapping points!
_Path.reserve(_Path.Size + (num_segments + 1));
for (int i = 0; i <= num_segments; i++)
{
const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min);
_Path.push_back(ImVec2(center.x + ImCos(a) * radius, center.y + ImSin(a) * radius));
}
}
}

Expand Down
17 changes: 13 additions & 4 deletions imgui_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ static inline float ImAbs(float x) { return fabsf(x); }
static inline double ImAbs(double x) { return fabs(x); }
static inline float ImSign(float x) { return (x < 0.0f) ? -1.0f : ((x > 0.0f) ? 1.0f : 0.0f); } // Sign operator - returns -1, 0 or 1 based on sign of argument
static inline double ImSign(double x) { return (x < 0.0) ? -1.0 : ((x > 0.0) ? 1.0 : 0.0); }
static inline float ImSqr(float x) { return x * x; }
static inline double ImSqr(double x) { return x * x; }
#endif
// - ImMin/ImMax/ImClamp/ImLerp/ImSwap are used by widgets which support variety of types: signed/unsigned int/long long float/double
// (Exceptionally using templates here but we could also redefine them for those types)
Expand Down Expand Up @@ -540,10 +542,16 @@ struct IMGUI_API ImChunkStream
#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)

// ImDrawList: You may set this to higher values (e.g. 2 or 3) to increase tessellation of fast rounded corners path.
#ifndef IM_DRAWLIST_ARCFAST_TESSELLATION_MULTIPLIER
#define IM_DRAWLIST_ARCFAST_TESSELLATION_MULTIPLIER 1
// ImDrawList: Lookup table size for adaptive arc drawing.
#ifndef IM_DRAWLIST_ARCFAST_SAMPLES
#define IM_DRAWLIST_ARCFAST_SAMPLES (48 * 6) // Number of samples in lookup table.
#endif
// Cutoff radius is calculated by formula (derived from IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC):
// threshold = -(err/(-1 + cos((2 pi)/N))) && err != 0 && N >= 2
#define IM_DRAWLIST_ARCFAST_RADIUS_THRESHOLD_CALC(_N,_MAXERROR) (-((_MAXERROR)/(-1.0f + ImCos((IM_PI * 2.0f) / (_N)))))
// Error calculation for given radius and segment count (derived from IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC):
// error = (2 rad (sin(pi / N)^2)) && rad != 0 && N >= 2
#define IM_DRAWLIST_ARCFAST_RADIUS_ERROR_CALC(_N,_RAD) (2.0f * (_RAD) * ImSqr(ImSin(IM_PI / (_N))))

// 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.
Expand All @@ -558,9 +566,10 @@ struct IMGUI_API ImDrawListSharedData
ImDrawListFlags InitialFlags; // Initial flags at the beginning of the frame (it is possible to alter flags on a per-drawlist basis afterwards)

// [Internal] Lookup tables
ImVec2 ArcFastVtx[12 * IM_DRAWLIST_ARCFAST_TESSELLATION_MULTIPLIER]; // FIXME: Bake rounded corners fill/borders in atlas
ImVec2 ArcFastVtx[IM_DRAWLIST_ARCFAST_SAMPLES]; // Sample points on the circle.
ImU8 CircleSegmentCounts[64]; // Precomputed segment count for given radius (array index + 1) before we calculate it dynamically (to avoid calculation overhead)
const ImVec4* TexUvLines; // UV of anti-aliased lines in the atlas
float ArcFastRadiusThreshold; // Cutoff radius after which arc drawing will fallback to slower PathArcTo()

ImDrawListSharedData();
void SetCircleSegmentMaxError(float max_error);
Expand Down

0 comments on commit 02b4ac4

Please sign in to comment.