ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
61.2k stars 10.31k forks source link

Rendering to SVG and preserving info about higher-level primitives (rectangles, etc) #6384

Open iiinjoy opened 1 year ago

iiinjoy commented 1 year ago

Hello

Summary I implemented the feature to render the Dear ImGui window to SVG (see result.svg below). Need your advice on how to do it right.

Purpose and context I use Dear ImGui+implot in a highly interactive application with many line plots. One of the most requested features is to export plot in SVG format to use in reports.

Although Dear ImGui is mainly designed to render UI in 3D-pipeline-enabled applications (as the README says), I would like to highlight additional use cases, such as:

Implementation details

My first solution was to draw only triangles, like regular backends (OpenGL, DirectX etc) do. It worked, but (1) there were no fonts, and (2) there were seams at the junctions of triangles (SVG related problem).

The second (current) solution is to slightly modify the Dear ImGui to be able to preserve drawing information about higher-level primitives: rectangle, circle, pathstroke, polygon and, importantly, text. (see code example below). But not limited to this list, perhaps by creating an extensible open-ended enumeration.

Question How to properly preserve drawing information about higher-level primitives? 1) create a data structure for it and store it in the ImDrawList? (as in the code below) 2) create a hook in the ImDrawList for the user and call it every time a new primitive is added, moving primitives storing method to user-side? 3) another ways?

Thank you!

Version/Branch of Dear ImGui:

Version: 1.89.5 Branch: master

Back-end/Renderer/Compiler/OS

Back-ends: imgui_impl_glfw.cpp + imgui_impl_opengl3.cpp Operating System: Linux

Screenshot (result.svg)

result.svg

Standalone, minimal, complete and verifiable example:

    //main render loop
    // --snip--

    if (ImGui::Button("Export SVG")) {
        export_svg = true;
    }
    // --snip--

    // Rendering
    ImGui::Render();
    //  rendering in backend, i.e. OpenGL
    // --snip--

    if (export_svg) {
        ImDrawData *data = ImGui::GetDrawData();
        if (!data->Valid) continue;
        export_svg = false;

        // open file, print svg header to it
        // --snip--

        for (auto i = 0; i < data->CmdListsCount; ++i) {
            ImDrawList *list = data->CmdLists[i];
            for (auto j = 0; j < list->PrimBuffer.size(); ++j) {
                ImDrawList::Primitive &p = list->PrimBuffer[j];
                switch (p.type) {
                case ImDrawList::Primitive::Line: {
                    auto col = p.cols[0];
                    auto [r, g, b, opacity] = get_color(col);
                    float stroke_width = p.thickness < 1.0f ? 1.0f : p.thickness;
                    fprintf(svg, "<line x1=\"%g\" y1=\"%g\" x2=\"%g\" y2=\"%g\" stroke=\"rgb(%d, %d, %d)\" opacity=\"%g\" stroke-width=\"%g\" />\n",
                            p.ps[0].x, p.ps[0].y, p.ps[1].x, p.ps[1].y, r, g, b, opacity, stroke_width);
                    break;
                }
                case ImDrawList::Primitive::Rect: {
                    // --snip--
                    break;
                }
                case ImDrawList::Primitive::Polygon: {
                    // --snip--
                    break;
                }
                case ImDrawList::Primitive::Text: {
                    // --snip--
                    break;
                }
                case ImDrawList::Primitive::Circle: {
                    // --snip--
                }
                // --snip--

                }
            }
        }
    }

// imgui.h
struct ImDrawList
{
    // This is what you have to render
    ImVector<ImDrawCmd>     CmdBuffer;          // Draw commands. Typically 1 command = 1 GPU draw call, unless the command is a callback.
    ImVector<ImDrawIdx>     IdxBuffer;          // Index buffer. Each command consume ImDrawCmd::ElemCount of those
    ImVector<ImDrawVert>    VtxBuffer;          // Vertex buffer.
    ImDrawListFlags         Flags;              // Flags, you may poke into these to adjust anti-aliasing settings per-primitive.

    // new struct for primitives
    struct Primitive {
        enum Type: char {
            Line,
            Rect,
            Polygon,
            Text,
            Circle,
        };
        Type type;
        bool fill;
        bool multicolor;
        bool closed;
        float rounding;
        float thickness;
        ImVector<ImVec2> ps;
        ImU32 cols[4];
        ImGuiTextBuffer text;

        Primitive() {
            memset(this, 0, sizeof(*this));
        }
    };
    ImVector<Primitive> PrimBuffer;

    // --snip--
};

// imgui_draw.cpp
void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags) {
    // --snip--

    PrimBuffer.push_back(Primitive());
    Primitive &prim = PrimBuffer.back();
    prim.type = Primitive::Rect;
    prim.ps.resize(2);
    prim.ps[0] = p_min;
    prim.ps[1] = p_max;
    prim.cols[0] = col;
    prim.fill = true;
    prim.rounding = rounding;
}
iiinjoy commented 1 year ago

I'm trying to implement this in a different way, see gist.