imgui icon indicating copy to clipboard operation
imgui copied to clipboard

Add new methods to ImDrawList that support drawing ellipses

Open Doohl opened this issue 4 years ago • 13 comments

Contained are three new ImDrawList functions that closely mimic the existing AddEllipse/PathArcTo functions:

  • ImDrawList::PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rotation, float a_min, float a_max, int num_segments = 10)
  • ImDrawList::AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 12, float thickness = 1.0f)
  • ImDrawList::AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 12)

Parameters for drawing an ellipse, more or less, were taken from Javascript's Canvas2D ellipse() method. Willing to make any modifications that will improve performance and/or clarity.

Though ellipses can currently be drawn with the existing bezier curve feature set, it does not seem proper - ellipses, I would argue, are primitive shapes that should be included.

CosmicExplorer_2019-08-22_22-04-35 [Example use: drawing 2D kepler orbits]

Doohl avatar Aug 23 '19 05:08 Doohl

Thank you @Doohl ! Could you update the "Custom Rendering" demo to make use of those functions? Also adding some comments on the PR now.

ocornut avatar Aug 23 '19 08:08 ocornut

Hello,

Thank you very much for this new method ! Just in time with my need :-) I tested your code (see screenshot). For the record, it works well, but I didn't investigate much. The only issue I have, is that I was not able to make rotation work.

miniDart_0 9 6_canvas06

The code is big and not public yet, since I've created a class named Canvas since, and something similar will be in miniDart 0.9.7 . See https://framagit.org/ericb/miniDart.

Below, the essential to understand:

 float radius_x = 1.0f + ImGui::GetMouseDragDelta().x;
 float radius_y = 1.0f + ImGui::GetMouseDragDelta().y;

 if (fabs(radius_x) <= 0.1f)
      radius_x = 1.0f; // avoid divide by zero

static float rotation = (ImGui::IsKeyPressed(ImGui::GetIO().KeyCtrl) == true) ? radius_y / radius_x : 0.0f;

The preview:

                                    case EMPTY_ELLIPSE:
                                        ImGui::GetOverlayDrawList()->AddEllipse(ImVec2(pTextCanvas->image_pos.x + aDrawnObject.objectPoints[0].x, pTextCanvas->image_pos.y + aDrawnObject.objectPoints[0].y),
                                                                               radius_x,
                                                                               radius_y,
                                                                               aDrawnObject.objBackgroundColor,
                                                                               rotation,
                                                                               32,
                                                                               aDrawnObject.thickness);
                                    break;

                                    case FILLED_ELLIPSE:
                                        ImGui::GetOverlayDrawList()->AddEllipseFilled(ImVec2(pTextCanvas->image_pos.x + aDrawnObject.objectPoints[0].x, pTextCanvas->image_pos.y + aDrawnObject.objectPoints[0].y),
                                                                               radius_x,
                                                                               radius_y,
                                                                               aDrawnObject.objBackgroundColor,
                                                                               rotation,
                                                                               32);
                                    break;

What is drawn:

                  ///////////////////////////////////////////////////////////////////////////////////////////////
                //                                           DRAW ALL
                ///////////////////////////////////////////////////////////////////////////////////////////////

                // clip lines and objects within the canvas (if we resize it, etc.)
                draw_list->PushClipRect(ImVec2(0.0f, 0.0f), pTextCanvas->image_pos + subview_size);

                for (unsigned int i = 0; i < delayTabDrawnObjects.size(); i++)
                {
                    {
                        switch(delayTabDrawnObjects[i].anObjectType)
                        {
                            case EMPTY_RECTANGLE:
                            // already stored
                            draw_list->AddRect( pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[0],
                                                pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[1],
                                                delayTabDrawnObjects[i].objBackgroundColor, 0.0f, ~0 , delayTabDrawnObjects[i].thickness);
                            break;

                            case EMPTY_CIRCLE:
                            draw_list->AddCircle(ImVec2(pTextCanvas->image_pos.x + delayTabDrawnObjects[i].objectPoints[0].x, pTextCanvas->image_pos.y + delayTabDrawnObjects[i].objectPoints[1].y),
                                                 delayTabDrawnObjects[i].P1P4,
                                                 delayTabDrawnObjects[i].objBackgroundColor, 32, delayTabDrawnObjects[i].thickness);
                            break;

                            case FILLED_RECTANGLE:

                            draw_list->AddRectFilled(pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[0], pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[1], delayTabDrawnObjects[i].objBackgroundColor);
                            break;

                            case FILLED_CIRCLE:
                            draw_list->AddCircleFilled(pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[0],
                                                       delayTabDrawnObjects[i].P1P4,
                                                       delayTabDrawnObjects[i].objBackgroundColor,
                                                       32);
                            break;

                            case EMPTY_ELLIPSE:
                            draw_list->AddEllipse(pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[0],
                                                  delayTabDrawnObjects[i].radius_x,
                                                  delayTabDrawnObjects[i].radius_y,
                                                  delayTabDrawnObjects[i].objBackgroundColor,
                                                  delayTabDrawnObjects[i].rotation,
                                                  32,
                                                  delayTabDrawnObjects[i].thickness);
                            break;

                            case FILLED_ELLIPSE:
                            draw_list->AddEllipseFilled(pTextCanvas->image_pos + delayTabDrawnObjects[i].objectPoints[0],
                                                        delayTabDrawnObjects[i].radius_x,
                                                        delayTabDrawnObjects[i].radius_y,
                                                        delayTabDrawnObjects[i].objBackgroundColor,
                                                        delayTabDrawnObjects[i].rotation,
                                                        32);
                            break;

( and so on )

Thanks !

ebachard avatar Aug 24 '19 21:08 ebachard

Hello @ebachard

I'd say your problem is here:

static float rotation = (ImGui::IsKeyPressed(ImGui::GetIO().KeyCtrl) == true) ? radius_y / radius_x : 0.0f;

Specifically, (ImGui::IsKeyPressed(ImGui::GetIO().KeyCtrl) == true) is probably not the conditional you want to write. ImGui::GetIO().KeyCtrl is returning a bool and ImGui::IsKeyPressed is expecting an integer input (so it's being implicitly converted).

Instead, do:

static float rotation = ImGui::GetIO().KeyCtrl ? radius_y / radius_x : 0.0f;

Which will evaluate as true when the CTRL key is down. Hope you find this helpful!

Doohl avatar Aug 25 '19 01:08 Doohl

@Doohl : uff, you are right, I was mistaken with KeyCtrl use. Shame on me ...

In fact, in meantime, I identified another visibility issue, and the final working solution is:

float rotation = ImGui::GetIO().KeyCtrl ? radius_y / radius_x : 0.0f;

Thanks a lot for your help, and for your nice feature ! :-)

ebachard avatar Aug 25 '19 16:08 ebachard

@Doohl Question about the API.. while I can see the use for angle something feels odd which is that the rest of the ImDrawList doesn't have this parameter for other primitives (while it perfectly could have the parameter!). What do you think makes Ellipse different there?

ocornut avatar Aug 27 '19 18:08 ocornut

You are probably right, @ocornut there is not an outstanding reason for why I've added rotation to ellipses. It was a combination of my own personal need and the fact that Canvas2D's ellipse() has that argument.

I can remove that parameter for now and open an issue or PR for standardizing rotation for all primitives.

Doohl avatar Aug 27 '19 19:08 Doohl

The rotation is justified because most of the other primitives can be rotated by the user.

AddRect can be replaced by AddQuad with rotated points. Whereas if AddEllipse doesn’t offer the option, there’s no way to achieve a rotated output.

Only exception is AddRectFilledMultiColor() but that’s a rarely used and very simple function.

I guess what I am most curious about is how one may be using ellipses in a codebase. How about you?

ocornut avatar Aug 28 '19 21:08 ocornut

For background, I'm using ImGui's primitives for a quick program to render a 2D model of the solar system. Trying to avoid either writing up my own methods for primitives or using something like SDL_gfx.

Ellipses in my case require rotation, because in the context of a topdown 2D perspective of a Keplerian two-body solar system, the longitude of the periapsis defines the rotation of the ellipse that represents an orbit.

2019-08-28_14-49-13

So we could:

  • Leave the API with rotation included
  • Have overloads for rotation / no rotation (kinda messy!)
  • Look into other solutions, like an ImRotateStart / ImRotateEnd solution like in Issue #1286.

I speculate most people who would draw ellipses probably won't need them rotated like I do. But you do bring up a good point in that other primitives can already be rotated!

Doohl avatar Aug 28 '19 21:08 Doohl

Trying to avoid either writing up my own methods

You just did :)

My gut feeling right now is that we should go for your initial plan.

ocornut avatar Aug 29 '19 08:08 ocornut

Haha, I meant I was TRYING to avoid writing my own methods. Actually, I think a more performant solution for me would be to draw my ellipses w/ GLSL especially since their x/y radii can go up to like 5000 pixels... but it's always good to have a quick software solution.

I'll leave that last commit in the branch for posterity; if you decide to keep rotation or not. I figure you probably cherry pick commits anyway! Thank you for all the input.

Doohl avatar Aug 29 '19 09:08 Doohl

Hi, is there some update on this? The ellipse API with rotation is particularly useful when rendering SVG.

Matheus-Garbelini avatar Sep 03 '20 20:09 Matheus-Garbelini

Hello Matheus,

No update per se, But you can freely include this function in your codebase (as a loose function taking a ImDrawList* parameter) so its mostly a matter of copying that code here.

ocornut avatar Sep 03 '20 21:09 ocornut

Hi, @ocornut thanks. I had some issues using those functions manually so I decided to simply add a WebView (CEF) to ImGui. A bit overkill but it looks awesome if combining WebView on top of ImGui. Hopefully, I can provide an ImGui WebView Addon component when I have the time.

webview_renderer_svg

Matheus-Garbelini avatar Nov 01 '20 13:11 Matheus-Garbelini