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:
rendering application windows in SVG/HTML (or even LaTeX) formats for use in documentation, achieving a (close to) 1-in-1 correspondence with the actual appearance of the application.
rendering higher-level primitives in embedded systems, where might be optimized drawing APIs for common shapes, like rectangles.
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)
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;
}
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)
Standalone, minimal, complete and verifiable example: