//--------------------------------------------------
// ImPlot3D v0.1
// implot3d.cpp
// Date: 2024-11-16
// Author: Breno Cunha Queiroz (brenocq.com)
//
// Acknowledgments:
//  ImPlot3D is heavily inspired by ImPlot
//  (https://github.com/epezent/implot) by Evan Pezent,
//  and follows a similar code style and structure to
//  maintain consistency with ImPlot's API.
//--------------------------------------------------

// Table of Contents:
// [SECTION] Includes
// [SECTION] Macros
// [SECTION] Context
// [SECTION] Text Utils
// [SECTION] Legend Utils
// [SECTION] Mouse Position Utils
// [SECTION] Plot Box Utils
// [SECTION] Formatter
// [SECTION] Locator
// [SECTION] Context Menus
// [SECTION] Begin/End Plot
// [SECTION] Setup
// [SECTION] Plot Utils
// [SECTION] Setup Utils
// [SECTION] Miscellaneous
// [SECTION] Styles
// [SECTION] Colormaps
// [SECTION] Context Utils
// [SECTION] Style Utils
// [SECTION] ImPlot3DPoint
// [SECTION] ImPlot3DBox
// [SECTION] ImPlot3DRange
// [SECTION] ImPlot3DQuat
// [SECTION] ImDrawList3D
// [SECTION] ImPlot3DAxis
// [SECTION] ImPlot3DPlot
// [SECTION] ImPlot3DStyle

//-----------------------------------------------------------------------------
// [SECTION] Includes
//-----------------------------------------------------------------------------

#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif

#include "implot3d.h"
#include "implot3d_internal.h"

// Needed for Windows
#include <cmath>

#ifndef IMGUI_DISABLE

//-----------------------------------------------------------------------------
// [SECTION] Macros
//-----------------------------------------------------------------------------

#define IMPLOT3D_CHECK_CTX() IM_ASSERT_USER_ERROR(GImPlot3D != nullptr, "No current context. Did you call ImPlot3D::CreateContext() or ImPlot3D::SetCurrentContext()?")
#define IMPLOT3D_CHECK_PLOT() IM_ASSERT_USER_ERROR(GImPlot3D->CurrentPlot != nullptr, "No active plot. Did you call ImPlot3D::BeginPlot()?")

//-----------------------------------------------------------------------------
// [SECTION] Context
//-----------------------------------------------------------------------------

namespace ImPlot3D {

// Global ImPlot3D context
#ifndef GImPlot3D
ImPlot3DContext* GImPlot3D = nullptr;
#endif

static ImPlot3DQuat init_rotation = ImPlot3DQuat(-0.513269, -0.212596, -0.318184, 0.76819);

ImPlot3DContext* CreateContext() {
    ImPlot3DContext* ctx = IM_NEW(ImPlot3DContext)();
    if (GImPlot3D == nullptr)
        SetCurrentContext(ctx);
    InitializeContext(ctx);
    return ctx;
}

void DestroyContext(ImPlot3DContext* ctx) {
    if (ctx == nullptr)
        ctx = GImPlot3D;
    if (GImPlot3D == ctx)
        SetCurrentContext(nullptr);
    IM_DELETE(ctx);
}

ImPlot3DContext* GetCurrentContext() { return GImPlot3D; }

void SetCurrentContext(ImPlot3DContext* ctx) { GImPlot3D = ctx; }

//-----------------------------------------------------------------------------
// [SECTION] Text Utils
//-----------------------------------------------------------------------------

void AddTextRotated(ImDrawList* draw_list, ImVec2 pos, float angle, ImU32 col, const char* text_begin, const char* text_end) {
    if (!text_end)
        text_end = text_begin + strlen(text_begin);

    ImGuiContext& g = *GImGui;
    ImFont* font = g.Font;

    // Align to be pixel perfect
    pos.x = IM_FLOOR(pos.x);
    pos.y = IM_FLOOR(pos.y);

    const float scale = g.FontSize / font->LegacySize;//FontSize;

    // Measure the size of the text in unrotated coordinates
    ImVec2 text_size = font->CalcTextSizeA(g.FontSize, FLT_MAX, 0.0f, text_begin, text_end, nullptr);

    // Precompute sine and cosine of the angle (note: angle should be positive for rotation in ImGui)
    float cos_a = cosf(-angle);
    float sin_a = sinf(-angle);

    const char* s = text_begin;
    int chars_total = (int)(text_end - s);
    int chars_rendered = 0;
    const int vtx_count_max = chars_total * 4;
    const int idx_count_max = chars_total * 6;
    draw_list->PrimReserve(idx_count_max, vtx_count_max);

    // Adjust pen position to center the text
    ImVec2 pen = ImVec2(-text_size.x * 0.5f, -text_size.y * 0.5f);

    while (s < text_end) {
        unsigned int c = (unsigned int)*s;
        if (c < 0x80) {
            s += 1;
        } else {
            s += ImTextCharFromUtf8(&c, s, text_end);
            if (c == 0) // Malformed UTF-8?
                break;
        }

        const ImFontGlyph* glyph = font->GetFontBaked(font->LegacySize)->FindGlyph((ImWchar)c);
        if (glyph == nullptr) {
            continue;
        }

        // Glyph dimensions and positions
        ImVec2 glyph_offset = ImVec2(glyph->X0, glyph->Y0) * scale;
        ImVec2 glyph_size = ImVec2(glyph->X1 - glyph->X0, glyph->Y1 - glyph->Y0) * scale;

        // Corners of the glyph quad in unrotated space
        ImVec2 corners[4];
        corners[0] = pen + glyph_offset;
        corners[1] = pen + glyph_offset + ImVec2(glyph_size.x, 0);
        corners[2] = pen + glyph_offset + glyph_size;
        corners[3] = pen + glyph_offset + ImVec2(0, glyph_size.y);

        // Rotate and translate the corners
        for (int i = 0; i < 4; i++) {
            float x = corners[i].x;
            float y = corners[i].y;
            corners[i].x = x * cos_a - y * sin_a + pos.x;
            corners[i].y = x * sin_a + y * cos_a + pos.y;
        }

        // Texture coordinates
        ImVec2 uv0 = ImVec2(glyph->U0, glyph->V0);
        ImVec2 uv1 = ImVec2(glyph->U1, glyph->V1);

        // Render the glyph quad
        draw_list->PrimQuadUV(corners[0], corners[1], corners[2], corners[3],
                              uv0, ImVec2(glyph->U1, glyph->V0),
                              uv1, ImVec2(glyph->U0, glyph->V1),
                              col);

        // Advance the pen position
        pen.x += glyph->AdvanceX * scale;

        chars_rendered++;
    }

    // Return unused vertices
    int chars_skipped = chars_total - chars_rendered;
    draw_list->PrimUnreserve(chars_skipped * 6, chars_skipped * 4);
}

void AddTextCentered(ImDrawList* draw_list, ImVec2 top_center, ImU32 col, const char* text_begin) {
    const char* text_end = ImGui::FindRenderedTextEnd(text_begin);
    ImVec2 text_size = ImGui::CalcTextSize(text_begin, text_end, true);
    draw_list->AddText(ImVec2(top_center.x - text_size.x * 0.5f, top_center.y), col, text_begin, text_end);
}

//-----------------------------------------------------------------------------
// [SECTION] Legend Utils
//-----------------------------------------------------------------------------

ImVec2 GetLocationPos(const ImRect& outer_rect, const ImVec2& inner_size, ImPlot3DLocation loc, const ImVec2& pad) {
    ImVec2 pos;
    // Legend x coordinate
    if (ImPlot3D::ImHasFlag(loc, ImPlot3DLocation_West) && !ImPlot3D::ImHasFlag(loc, ImPlot3DLocation_East))
        pos.x = outer_rect.Min.x + pad.x;
    else if (!ImPlot3D::ImHasFlag(loc, ImPlot3DLocation_West) && ImPlot3D::ImHasFlag(loc, ImPlot3DLocation_East))
        pos.x = outer_rect.Max.x - pad.x - inner_size.x;
    else
        pos.x = outer_rect.GetCenter().x - inner_size.x * 0.5f;
    // Legend y coordinate
    if (ImPlot3D::ImHasFlag(loc, ImPlot3DLocation_North) && !ImPlot3D::ImHasFlag(loc, ImPlot3DLocation_South))
        pos.y = outer_rect.Min.y + pad.y;
    else if (!ImPlot3D::ImHasFlag(loc, ImPlot3DLocation_North) && ImPlot3D::ImHasFlag(loc, ImPlot3DLocation_South))
        pos.y = outer_rect.Max.y - pad.y - inner_size.y;
    else
        pos.y = outer_rect.GetCenter().y - inner_size.y * 0.5f;
    pos.x = IM_ROUND(pos.x);
    pos.y = IM_ROUND(pos.y);
    return pos;
}

ImVec2 CalcLegendSize(ImPlot3DItemGroup& items, const ImVec2& pad, const ImVec2& spacing, bool vertical) {
    const int nItems = items.GetLegendCount();
    const float txt_ht = ImGui::GetTextLineHeight();
    const float icon_size = txt_ht;
    // Get label max width
    float max_label_width = 0;
    float sum_label_width = 0;
    for (int i = 0; i < nItems; i++) {
        const char* label = items.GetLegendLabel(i);
        const float label_width = ImGui::CalcTextSize(label, nullptr, true).x;
        max_label_width = label_width > max_label_width ? label_width : max_label_width;
        sum_label_width += label_width;
    }
    // Compute legend size
    const ImVec2 legend_size = vertical ? ImVec2(pad.x * 2 + icon_size + max_label_width, pad.y * 2 + nItems * txt_ht + (nItems - 1) * spacing.y) : ImVec2(pad.x * 2 + icon_size * nItems + sum_label_width + (nItems - 1) * spacing.x, pad.y * 2 + txt_ht);
    return legend_size;
}

void ShowLegendEntries(ImPlot3DItemGroup& items, const ImRect& legend_bb, bool hovered, const ImVec2& pad, const ImVec2& spacing, bool vertical, ImDrawList& draw_list) {
    const float txt_ht = ImGui::GetTextLineHeight();
    const float icon_size = txt_ht;
    const float icon_shrink = 2;
    ImU32 col_txt = GetStyleColorU32(ImPlot3DCol_LegendText);
    ImU32 col_txt_dis = ImAlphaU32(col_txt, 0.25f);
    float sum_label_width = 0;

    const int num_items = items.GetLegendCount();
    if (num_items == 0)
        return;
    ImPlot3DContext& gp = *GImPlot3D;

    // Render legend items
    for (int i = 0; i < num_items; i++) {
        const int idx = i;
        ImPlot3DItem* item = items.GetLegendItem(idx);
        const char* label = items.GetLegendLabel(idx);
        const float label_width = ImGui::CalcTextSize(label, nullptr, true).x;
        const ImVec2 top_left = vertical ? legend_bb.Min + pad + ImVec2(0, i * (txt_ht + spacing.y)) : legend_bb.Min + pad + ImVec2(i * (icon_size + spacing.x) + sum_label_width, 0);
        sum_label_width += label_width;
        ImRect icon_bb;
        icon_bb.Min = top_left + ImVec2(icon_shrink, icon_shrink);
        icon_bb.Max = top_left + ImVec2(icon_size - icon_shrink, icon_size - icon_shrink);
        ImRect label_bb;
        label_bb.Min = top_left;
        label_bb.Max = top_left + ImVec2(label_width + icon_size, icon_size);
        ImU32 col_txt_hl;
        ImU32 col_item = ImAlphaU32(item->Color, 1);

        ImRect button_bb(icon_bb.Min, label_bb.Max);

        ImGui::KeepAliveID(item->ID);

        bool item_hov = false;
        bool item_hld = false;
        bool item_clk = ImPlot3D::ImHasFlag(items.Legend.Flags, ImPlot3DLegendFlags_NoButtons)
                            ? false
                            : ImGui::ButtonBehavior(button_bb, item->ID, &item_hov, &item_hld);

        if (item_clk)
            item->Show = !item->Show;

        const bool hovering = item_hov && !ImPlot3D::ImHasFlag(items.Legend.Flags, ImPlot3DLegendFlags_NoHighlightItem);

        if (hovering) {
            item->LegendHovered = true;
            col_txt_hl = ImPlot3D::ImMixU32(col_txt, col_item, 64);
        } else {
            item->LegendHovered = false;
            col_txt_hl = ImGui::GetColorU32(col_txt);
        }

        ImU32 col_icon;
        if (item_hld)
            col_icon = item->Show ? ImAlphaU32(col_item, 0.5f) : ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.5f);
        else if (item_hov)
            col_icon = item->Show ? ImAlphaU32(col_item, 0.75f) : ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.75f);
        else
            col_icon = item->Show ? col_item : col_txt_dis;

        draw_list.AddRectFilled(icon_bb.Min, icon_bb.Max, col_icon);
        const char* text_display_end = ImGui::FindRenderedTextEnd(label, nullptr);
        if (label != text_display_end)
            draw_list.AddText(top_left + ImVec2(icon_size, 0), item->Show ? col_txt_hl : col_txt_dis, label, text_display_end);
    }
}

void RenderLegend() {
    ImPlot3DContext& gp = *GImPlot3D;
    ImPlot3DPlot& plot = *gp.CurrentPlot;
    if (ImPlot3D::ImHasFlag(plot.Flags, ImPlot3DFlags_NoLegend) || plot.Items.GetLegendCount() == 0)
        return;
    ImGuiContext& g = *GImGui;
    ImGuiWindow* window = g.CurrentWindow;
    ImDrawList* draw_list = window->DrawList;
    const ImGuiIO& IO = ImGui::GetIO();

    ImPlot3DLegend& legend = plot.Items.Legend;
    const bool legend_horz = ImPlot3D::ImHasFlag(legend.Flags, ImPlot3DLegendFlags_Horizontal);
    const ImVec2 legend_size = CalcLegendSize(plot.Items, gp.Style.LegendInnerPadding, gp.Style.LegendSpacing, !legend_horz);
    const ImVec2 legend_pos = GetLocationPos(plot.PlotRect,
                                             legend_size,
                                             legend.Location,
                                             gp.Style.LegendPadding);
    legend.Rect = ImRect(legend_pos, legend_pos + legend_size);

    // Test hover
    legend.Hovered = legend.Rect.Contains(IO.MousePos);

    // Render background
    ImU32 col_bg = GetStyleColorU32(ImPlot3DCol_LegendBg);
    ImU32 col_bd = GetStyleColorU32(ImPlot3DCol_LegendBorder);
    draw_list->AddRectFilled(legend.Rect.Min, legend.Rect.Max, col_bg);
    draw_list->AddRect(legend.Rect.Min, legend.Rect.Max, col_bd);

    // Render legends
    ShowLegendEntries(plot.Items, legend.Rect, legend.Hovered, gp.Style.LegendInnerPadding, gp.Style.LegendSpacing, !legend_horz, *draw_list);
}

//-----------------------------------------------------------------------------
// [SECTION] Mouse Position Utils
//-----------------------------------------------------------------------------

void RenderMousePos() {
    ImPlot3DContext& gp = *GImPlot3D;
    ImPlot3DPlot& plot = *gp.CurrentPlot;
    if (ImPlot3D::ImHasFlag(plot.Flags, ImPlot3DFlags_NoMouseText))
        return;

    ImVec2 mouse_pos = ImGui::GetMousePos();
    ImPlot3DPoint mouse_plot_pos = PixelsToPlotPlane(mouse_pos, ImPlane3D_YZ, true);
    if (mouse_plot_pos.IsNaN())
        mouse_plot_pos = PixelsToPlotPlane(mouse_pos, ImPlane3D_XZ, true);
    if (mouse_plot_pos.IsNaN())
        mouse_plot_pos = PixelsToPlotPlane(mouse_pos, ImPlane3D_XY, true);

    char buff[IMPLOT3D_LABEL_MAX_SIZE];
    if (!mouse_plot_pos.IsNaN()) {
        ImGuiTextBuffer builder;
        builder.append("(");
        for (int i = 0; i < 3; i++) {
            ImPlot3DAxis& axis = plot.Axes[i];
            if (i > 0)
                builder.append(", ");
            axis.Formatter(mouse_plot_pos[i], buff, IMPLOT3D_LABEL_MAX_SIZE, axis.FormatterData);
            builder.append(buff);
        }
        builder.append(")");

        const ImVec2 size = ImGui::CalcTextSize(builder.c_str());
        // TODO custom location/padding
        const ImVec2 pos = GetLocationPos(plot.PlotRect, size, ImPlot3DLocation_SouthEast, ImVec2(10, 10));
        ImDrawList& draw_list = *ImGui::GetWindowDrawList();
        draw_list.AddText(pos, GetStyleColorU32(ImPlot3DCol_InlayText), builder.c_str());
    }
}

//-----------------------------------------------------------------------------
// [SECTION] Plot Box Utils
//-----------------------------------------------------------------------------

// Faces of the box (defined by 4 corner indices)
static const int faces[6][4] = {
    {0, 3, 7, 4}, // X-min face
    {0, 4, 5, 1}, // Y-min face
    {0, 1, 2, 3}, // Z-min face
    {1, 2, 6, 5}, // X-max face
    {3, 7, 6, 2}, // Y-max face
    {4, 5, 6, 7}, // Z-max face
};

// Edges of the box (defined by 2 corner indices)
static const int edges[12][2] = {
    // Bottom face edges
    {0, 1},
    {1, 2},
    {2, 3},
    {3, 0},
    // Top face edges
    {4, 5},
    {5, 6},
    {6, 7},
    {7, 4},
    // Vertical edges
    {0, 4},
    {1, 5},
    {2, 6},
    {3, 7},
};

// Face edges (4 edge indices for each face)
static const int face_edges[6][4] = {
    {3, 11, 8, 7},  // X-min face
    {0, 8, 4, 9},   // Y-min face
    {0, 1, 2, 3},   // Z-min face
    {1, 9, 5, 10},  // X-max face
    {2, 10, 6, 11}, // Y-max face
    {4, 5, 6, 7},   // Z-max face
};

// Lookup table for axis_corners based on active_faces (3D plot)
static const int axis_corners_lookup_3d[8][3][2] = {
    // Index 0: active_faces = {0, 0, 0}
    {{3, 2}, {1, 2}, {1, 5}},
    // Index 1: active_faces = {0, 0, 1}
    {{7, 6}, {5, 6}, {1, 5}},
    // Index 2: active_faces = {0, 1, 0}
    {{0, 1}, {1, 2}, {2, 6}},
    // Index 3: active_faces = {0, 1, 1}
    {{4, 5}, {5, 6}, {2, 6}},
    // Index 4: active_faces = {1, 0, 0}
    {{3, 2}, {0, 3}, {0, 4}},
    // Index 5: active_faces = {1, 0, 1}
    {{7, 6}, {4, 7}, {0, 4}},
    // Index 6: active_faces = {1, 1, 0}
    {{0, 1}, {0, 3}, {3, 7}},
    // Index 7: active_faces = {1, 1, 1}
    {{4, 5}, {4, 7}, {3, 7}},
};

int GetMouseOverPlane(const ImPlot3DPlot& plot, const bool* active_faces, const ImVec2* corners_pix, int* plane_out = nullptr) {
    ImGuiIO& io = ImGui::GetIO();
    ImVec2 mouse_pos = io.MousePos;
    if (plane_out)
        *plane_out = -1;

    // Check each active face
    for (int a = 0; a < 3; a++) {
        int face_idx = a + 3 * active_faces[a];
        ImVec2 p0 = corners_pix[faces[face_idx][0]];
        ImVec2 p1 = corners_pix[faces[face_idx][1]];
        ImVec2 p2 = corners_pix[faces[face_idx][2]];
        ImVec2 p3 = corners_pix[faces[face_idx][3]];

        // Check if the mouse is inside the face's quad (using a triangle check)
        if (ImTriangleContainsPoint(p0, p1, p2, mouse_pos) || ImTriangleContainsPoint(p2, p3, p0, mouse_pos)) {
            if (plane_out)
                *plane_out = a;
            return a; // Return the plane index: 0 -> YZ, 1 -> XZ, 2 -> XY
        }
    }

    return -1; // Not over any active plane
}

int GetMouseOverAxis(const ImPlot3DPlot& plot, const bool* active_faces, const ImVec2* corners_pix, const int plane_2d, int* edge_out = nullptr) {
    const float axis_proximity_threshold = 15.0f; // Distance in pixels to consider the mouse "close" to an axis

    ImGuiIO& io = ImGui::GetIO();
    ImVec2 mouse_pos = io.MousePos;
    if (edge_out)
        *edge_out = -1;

    bool visible_edges[12];
    for (int i = 0; i < 12; i++)
        visible_edges[i] = false;
    for (int a = 0; a < 3; a++) {
        int face_idx = a + 3 * active_faces[a];
        if (plane_2d != -1 && a != plane_2d)
            continue;
        for (size_t i = 0; i < 4; i++)
            visible_edges[face_edges[face_idx][i]] = true;
    }

    // Check each edge for proximity to the mouse
    for (int edge = 0; edge < 12; edge++) {
        if (!visible_edges[edge])
            continue;

        ImVec2 p0 = corners_pix[edges[edge][0]];
        ImVec2 p1 = corners_pix[edges[edge][1]];

        // Check distance to the edge
        ImVec2 closest_point = ImLineClosestPoint(p0, p1, mouse_pos);
        float dist = ImLengthSqr(mouse_pos - closest_point);
        if (dist <= axis_proximity_threshold) {
            if (edge_out)
                *edge_out = edge;

            // Determine which axis the edge belongs to
            if (edge == 0 || edge == 2 || edge == 4 || edge == 6)
                return 0; // X-axis
            else if (edge == 1 || edge == 3 || edge == 5 || edge == 7)
                return 1; // Y-axis
            else
                return 2; // Z-axis
        }
    }

    return -1; // Not over any axis
}

void RenderPlotBackground(ImDrawList* draw_list, const ImPlot3DPlot& plot, const ImVec2* corners_pix, const bool* active_faces, const int plane_2d) {
    const ImVec4 col_bg = GetStyleColorVec4(ImPlot3DCol_PlotBg);
    const ImVec4 col_bg_hov = col_bg + ImVec4(0.03, 0.03, 0.03, 0.0);

    int hovered_plane = -1;
    if (!plot.Held) {
        // If the mouse is not held, highlight plane hovering when mouse over it
        hovered_plane = GetMouseOverPlane(plot, active_faces, corners_pix);
        if (GetMouseOverAxis(plot, active_faces, corners_pix, plane_2d) != -1)
            hovered_plane = -1;
    } else {
        // If the mouse is held, highlight the held plane
        hovered_plane = plot.HeldPlaneIdx;
    }

    for (int a = 0; a < 3; a++) {
        int idx[4]; // Corner indices
        for (int i = 0; i < 4; i++)
            idx[i] = faces[a + 3 * active_faces[a]][i];
        const ImU32 col = ImGui::ColorConvertFloat4ToU32((hovered_plane == a) ? col_bg_hov : col_bg);
        draw_list->AddQuadFilled(corners_pix[idx[0]], corners_pix[idx[1]], corners_pix[idx[2]], corners_pix[idx[3]], col);
    }
}

void RenderPlotBorder(ImDrawList* draw_list, const ImPlot3DPlot& plot, const ImVec2* corners_pix, const bool* active_faces, const int plane_2d) {
    ImGuiIO& io = ImGui::GetIO();

    int hovered_edge = -1;
    if (!plot.Held)
        GetMouseOverAxis(plot, active_faces, corners_pix, plane_2d, &hovered_edge);
    else
        hovered_edge = plot.HeldEdgeIdx;

    bool render_edge[12];
    for (int i = 0; i < 12; i++)
        render_edge[i] = false;
    for (int a = 0; a < 3; a++) {
        int face_idx = a + 3 * active_faces[a];
        if (plane_2d != -1 && a != plane_2d)
            continue;
        for (size_t i = 0; i < 4; i++)
            render_edge[face_edges[face_idx][i]] = true;
    }

    ImU32 col_bd = GetStyleColorU32(ImPlot3DCol_PlotBorder);
    for (int i = 0; i < 12; i++) {
        if (render_edge[i]) {
            int idx0 = edges[i][0];
            int idx1 = edges[i][1];
            float thickness = i == hovered_edge ? 3.0f : 1.0f;
            draw_list->AddLine(corners_pix[idx0], corners_pix[idx1], col_bd, thickness);
        }
    }
}

void RenderGrid(ImDrawList* draw_list, const ImPlot3DPlot& plot, const ImPlot3DPoint* corners, const bool* active_faces, const int plane_2d) {
    ImVec4 col_grid = GetStyleColorVec4(ImPlot3DCol_AxisGrid);
    ImU32 col_grid_minor = ImGui::GetColorU32(col_grid * ImVec4(1, 1, 1, 0.3f));
    ImU32 col_grid_major = ImGui::GetColorU32(col_grid * ImVec4(1, 1, 1, 0.6f));
    for (int face = 0; face < 3; face++) {
        if (plane_2d != -1 && face != plane_2d)
            continue;
        int face_idx = face + 3 * active_faces[face];
        const ImPlot3DAxis& axis_u = plot.Axes[(face + 1) % 3];
        const ImPlot3DAxis& axis_v = plot.Axes[(face + 2) % 3];

        // Get the two axes (u and v) that define the face plane
        int idx0 = faces[face_idx][0];
        int idx1 = faces[face_idx][1];
        int idx2 = faces[face_idx][2];
        int idx3 = faces[face_idx][3];

        // Corners of the face in plot space
        ImPlot3DPoint p0 = corners[idx0];
        ImPlot3DPoint p1 = corners[idx1];
        ImPlot3DPoint p2 = corners[idx2];
        ImPlot3DPoint p3 = corners[idx3];

        // Vectors along the edges
        ImPlot3DPoint u_vec = p1 - p0;
        ImPlot3DPoint v_vec = p3 - p0;

        // Render grid lines along u axis (axis_u)
        if (!ImPlot3D::ImHasFlag(axis_u.Flags, ImPlot3DAxisFlags_NoGridLines))
            for (int t = 0; t < axis_u.Ticker.TickCount(); ++t) {
                const ImPlot3DTick& tick = axis_u.Ticker.Ticks[t];

                // Compute position along u
                float t_u = (tick.PlotPos - axis_u.Range.Min) / (axis_u.Range.Max - axis_u.Range.Min);
                ImPlot3DPoint p_start = p0 + u_vec * t_u;
                ImPlot3DPoint p_end = p3 + u_vec * t_u;

                // Convert to pixel coordinates
                ImVec2 p_start_pix = PlotToPixels(p_start);
                ImVec2 p_end_pix = PlotToPixels(p_end);

                // Get color
                ImU32 col_line = tick.Major ? col_grid_major : col_grid_minor;

                // Draw the grid line
                draw_list->AddLine(p_start_pix, p_end_pix, col_line);
            }

        // Render grid lines along v axis (axis_v)
        if (!ImPlot3D::ImHasFlag(axis_v.Flags, ImPlot3DAxisFlags_NoGridLines))
            for (int t = 0; t < axis_v.Ticker.TickCount(); ++t) {
                const ImPlot3DTick& tick = axis_v.Ticker.Ticks[t];

                // Compute position along v
                float t_v = (tick.PlotPos - axis_v.Range.Min) / (axis_v.Range.Max - axis_v.Range.Min);
                ImPlot3DPoint p_start = p0 + v_vec * t_v;
                ImPlot3DPoint p_end = p1 + v_vec * t_v;

                // Convert to pixel coordinates
                ImVec2 p_start_pix = PlotToPixels(p_start);
                ImVec2 p_end_pix = PlotToPixels(p_end);

                // Get color
                ImU32 col_line = tick.Major ? col_grid_major : col_grid_minor;

                // Draw the grid line
                draw_list->AddLine(p_start_pix, p_end_pix, col_line);
            }
    }
}

void RenderTickMarks(ImDrawList* draw_list, const ImPlot3DPlot& plot, const ImPlot3DPoint* corners, const ImVec2* corners_pix, const int axis_corners[3][2], const int plane_2d) {
    ImU32 col_tick = GetStyleColorU32(ImPlot3DCol_AxisTick);

    auto DeterminePlaneForAxis = [&](int axis_idx) {
        if (plane_2d != -1)
            return plane_2d;
        // If no plane chosen (-1), use:
        // X or Y axis -> XY plane (2)
        // Z axis -> YZ plane (0)
        if (axis_idx == 2)
            return 1; // Z-axis use XZ plane
        else
            return 2; // X or Y-axis use XY plane
    };

    for (int a = 0; a < 3; a++) {
        const ImPlot3DAxis& axis = plot.Axes[a];
        if (ImPlot3D::ImHasFlag(axis.Flags, ImPlot3DAxisFlags_NoTickMarks))
            continue;

        int idx0 = axis_corners[a][0];
        int idx1 = axis_corners[a][1];
        if (idx0 == idx1) // axis not visible or invalid
            continue;

        ImPlot3DPoint axis_start = corners[idx0];
        ImPlot3DPoint axis_end = corners[idx1];
        ImPlot3DPoint axis_dir = axis_end - axis_start;
        float axis_len = axis_dir.Length();
        if (axis_len < 1e-12f)
            continue;
        axis_dir /= axis_len;

        // Draw axis line
        ImVec2 axis_start_pix = corners_pix[idx0];
        ImVec2 axis_end_pix = corners_pix[idx1];
        draw_list->AddLine(axis_start_pix, axis_end_pix, col_tick);

        // Choose plane
        int chosen_plane = DeterminePlaneForAxis(a);

        // Project axis_dir onto chosen plane
        ImPlot3DPoint proj_dir = axis_dir;
        if (chosen_plane == 0) {
            // YZ plane: zero out x
            proj_dir.x = 0.0f;
        } else if (chosen_plane == 1) {
            // XZ plane: zero out y
            proj_dir.y = 0.0f;
        } else if (chosen_plane == 2) {
            // XY plane: zero out z
            proj_dir.z = 0.0f;
        }

        float proj_len = proj_dir.Length();
        if (proj_len < 1e-12f) {
            // Axis is parallel to plane normal or something degenerate, skip ticks
            continue;
        }
        proj_dir /= proj_len;

        // Rotate 90 degrees in chosen plane
        ImPlot3DPoint tick_dir;
        if (chosen_plane == 0) {
            // YZ plane
            // proj_dir=(0,py,pz), rotate 90°: (py,pz) -> (-pz,py)
            tick_dir = ImPlot3DPoint(0, -proj_dir.z, proj_dir.y);
        } else if (chosen_plane == 1) {
            // XZ plane (plane=1)
            // proj_dir=(px,0,pz), rotate 90°: (px,pz) -> (-pz,px)
            tick_dir = ImPlot3DPoint(-proj_dir.z, 0, proj_dir.x);
        } else {
            // XY plane
            // proj_dir=(px,py,0), rotate by 90°: (px,py) -> (-py,px)
            tick_dir = ImPlot3DPoint(-proj_dir.y, proj_dir.x, 0);
        }
        tick_dir.Normalize();

        // Tick lengths in NDC units
        const float major_size_ndc = 0.06f;
        const float minor_size_ndc = 0.03f;

        for (int t = 0; t < axis.Ticker.TickCount(); ++t) {
            const ImPlot3DTick& tick = axis.Ticker.Ticks[t];
            float v = (tick.PlotPos - axis.Range.Min) / (axis.Range.Max - axis.Range.Min);

            ImPlot3DPoint tick_pos_ndc = PlotToNDC(axis_start + axis_dir * (v * axis_len));

            // Half tick on each side of the axis line
            float size_tick_ndc = tick.Major ? major_size_ndc : minor_size_ndc;
            ImPlot3DPoint half_tick_ndc = tick_dir * (size_tick_ndc * 0.5f);

            ImPlot3DPoint T1_ndc = tick_pos_ndc - half_tick_ndc;
            ImPlot3DPoint T2_ndc = tick_pos_ndc + half_tick_ndc;

            ImVec2 T1_screen = NDCToPixels(T1_ndc);
            ImVec2 T2_screen = NDCToPixels(T2_ndc);

            draw_list->AddLine(T1_screen, T2_screen, col_tick);
        }
    }
}

void RenderTickLabels(ImDrawList* draw_list, const ImPlot3DPlot& plot, const ImPlot3DPoint* corners, const ImVec2* corners_pix, const int axis_corners[3][2]) {
    ImVec2 box_center_pix = PlotToPixels(plot.RangeCenter());
    ImU32 col_tick_txt = GetStyleColorU32(ImPlot3DCol_AxisText);

    for (int a = 0; a < 3; a++) {
        const ImPlot3DAxis& axis = plot.Axes[a];
        if (ImPlot3D::ImHasFlag(axis.Flags, ImPlot3DAxisFlags_NoTickLabels))
            continue;

        // Corner indices for this axis
        int idx0 = axis_corners[a][0];
        int idx1 = axis_corners[a][1];

        // If normal to the 2D plot, ignore the ticks
        if (idx0 == idx1)
            continue;

        // Start and end points of the axis in plot space
        ImPlot3DPoint axis_start = corners[idx0];
        ImPlot3DPoint axis_end = corners[idx1];

        // Direction vector along the axis
        ImPlot3DPoint axis_dir = axis_end - axis_start;

        // Convert axis start and end to screen space
        ImVec2 axis_start_pix = corners_pix[idx0];
        ImVec2 axis_end_pix = corners_pix[idx1];

        // Screen space axis direction
        ImVec2 axis_screen_dir = axis_end_pix - axis_start_pix;
        float axis_length = ImSqrt(ImLengthSqr(axis_screen_dir));
        if (axis_length != 0.0f)
            axis_screen_dir /= axis_length;
        else
            axis_screen_dir = ImVec2(1.0f, 0.0f); // Default direction if length is zero

        // Perpendicular direction in screen space
        ImVec2 offset_dir_pix = ImVec2(-axis_screen_dir.y, axis_screen_dir.x);

        // Make sure direction points away from cube center
        ImVec2 box_center_pix = PlotToPixels(plot.RangeCenter());
        ImVec2 axis_center_pix = (axis_start_pix + axis_end_pix) * 0.5f;
        ImVec2 center_to_axis_pix = axis_center_pix - box_center_pix;
        center_to_axis_pix /= ImSqrt(ImLengthSqr(center_to_axis_pix));
        if (ImDot(offset_dir_pix, center_to_axis_pix) < 0.0f)
            offset_dir_pix = -offset_dir_pix;

        // Adjust the offset magnitude
        float offset_magnitude = 20.0f; // TODO Calculate based on label size
        ImVec2 offset_pix = offset_dir_pix * offset_magnitude;

        // Compute angle perpendicular to axis in screen space
        float angle = atan2f(-axis_screen_dir.y, axis_screen_dir.x) + IM_PI * 0.5f;

        // Normalize angle to be between -π and π
        if (angle > IM_PI)
            angle -= 2 * IM_PI;
        if (angle < -IM_PI)
            angle += 2 * IM_PI;

        // Adjust angle to keep labels upright
        if (angle > IM_PI * 0.5f)
            angle -= IM_PI;
        if (angle < -IM_PI * 0.5f)
            angle += IM_PI;

        // Loop over ticks
        for (int t = 0; t < axis.Ticker.TickCount(); ++t) {
            const ImPlot3DTick& tick = axis.Ticker.Ticks[t];
            if (!tick.ShowLabel)
                continue;

            // Compute position along the axis
            float t_axis = (tick.PlotPos - axis.Range.Min) / (axis.Range.Max - axis.Range.Min);
            ImPlot3DPoint tick_pos = axis_start + axis_dir * t_axis;

            // Convert to pixel coordinates
            ImVec2 tick_pos_pix = PlotToPixels(tick_pos);

            // Get the tick label text
            const char* label = axis.Ticker.GetText(tick);

            // Adjust label position by offset
            ImVec2 label_pos_pix = tick_pos_pix + offset_pix;

            // Render the tick label
            AddTextRotated(draw_list, label_pos_pix, angle, col_tick_txt, label);
        }
    }
}

void RenderAxisLabels(ImDrawList* draw_list, const ImPlot3DPlot& plot, const ImPlot3DPoint* corners, const ImVec2* corners_pix, const int axis_corners[3][2]) {
    ImPlot3DPoint range_center = plot.RangeCenter();
    for (int a = 0; a < 3; a++) {
        const ImPlot3DAxis& axis = plot.Axes[a];
        if (!axis.HasLabel())
            continue;

        const char* label = axis.GetLabel();

        // Corner indices
        int idx0 = axis_corners[a][0];
        int idx1 = axis_corners[a][1];

        // If normal to the 2D plot, ignore axis label
        if (idx0 == idx1)
            continue;

        // Position at the end of the axis
        ImPlot3DPoint label_pos = (corners[idx0] + corners[idx1]) * 0.5f;
        // Add offset
        label_pos += (label_pos - range_center) * 0.4f;

        // Convert to pixel coordinates
        ImVec2 label_pos_pix = PlotToPixels(label_pos);

        // Adjust label position and angle
        ImU32 col_ax_txt = GetStyleColorU32(ImPlot3DCol_AxisText);

        // Compute text angle
        ImVec2 screen_delta = corners_pix[idx1] - corners_pix[idx0];
        float angle = atan2f(-screen_delta.y, screen_delta.x);
        if (angle > IM_PI * 0.5f)
            angle -= IM_PI;
        if (angle < -IM_PI * 0.5f)
            angle += IM_PI;

        AddTextRotated(draw_list, label_pos_pix, angle, col_ax_txt, label);
    }
}

// Function to compute active faces based on the rotation
// If the plot is close to 2D, plane_2d is set to the plane index (0 -> YZ, 1 -> XZ, 2 -> XY)
// plane_2d is set to -1 otherwise
void ComputeActiveFaces(bool* active_faces, const ImPlot3DQuat& rotation, int* plane_2d = nullptr) {
    if (plane_2d)
        *plane_2d = -1;

    ImPlot3DPoint rot_face_n[3] = {
        rotation * ImPlot3DPoint(1.0f, 0.0f, 0.0f),
        rotation * ImPlot3DPoint(0.0f, 1.0f, 0.0f),
        rotation * ImPlot3DPoint(0.0f, 0.0f, 1.0f),
    };

    int num_deg = 0; // Check number of planes that are degenerate (seen as a line)
    for (int i = 0; i < 3; ++i) {
        // Determine the active face based on the Z component
        if (fabs(rot_face_n[i].z) < 0.025) {
            // If aligned with the plane, choose the min face for bottom/left
            active_faces[i] = rot_face_n[i].x + rot_face_n[i].y < 0.0f;
            num_deg++;
        } else {
            // Otherwise, determine based on the Z component
            active_faces[i] = rot_face_n[i].z < 0.0f;
            // Set this plane as possible 2d plane
            if (plane_2d)
                *plane_2d = i;
        }
    }
    // Only return 2d plane if there are exactly 2 degenerate planes
    if (num_deg != 2 && plane_2d)
        *plane_2d = -1;
}

// Function to compute the box corners in plot space
void ComputeBoxCorners(ImPlot3DPoint* corners, const ImPlot3DPoint& range_min, const ImPlot3DPoint& range_max) {
    corners[0] = ImPlot3DPoint(range_min.x, range_min.y, range_min.z); // 0
    corners[1] = ImPlot3DPoint(range_max.x, range_min.y, range_min.z); // 1
    corners[2] = ImPlot3DPoint(range_max.x, range_max.y, range_min.z); // 2
    corners[3] = ImPlot3DPoint(range_min.x, range_max.y, range_min.z); // 3
    corners[4] = ImPlot3DPoint(range_min.x, range_min.y, range_max.z); // 4
    corners[5] = ImPlot3DPoint(range_max.x, range_min.y, range_max.z); // 5
    corners[6] = ImPlot3DPoint(range_max.x, range_max.y, range_max.z); // 6
    corners[7] = ImPlot3DPoint(range_min.x, range_max.y, range_max.z); // 7
}

// Function to compute the box corners in pixel space
void ComputeBoxCornersPix(ImVec2* corners_pix, const ImPlot3DPoint* corners) {
    for (int i = 0; i < 8; i++) {
        corners_pix[i] = PlotToPixels(corners[i]);
    }
}

void RenderPlotBox(ImDrawList* draw_list, const ImPlot3DPlot& plot) {
    // Get plot parameters
    const ImRect& plot_area = plot.PlotRect;
    const ImPlot3DQuat& rotation = plot.Rotation;
    ImPlot3DPoint range_min = plot.RangeMin();
    ImPlot3DPoint range_max = plot.RangeMax();
    ImPlot3DPoint range_center = plot.RangeCenter();

    // Compute active faces
    bool active_faces[3];
    int plane_2d = -1;
    ComputeActiveFaces(active_faces, rotation, &plane_2d);
    bool is_2d = plane_2d != -1;

    // Compute box corners in plot space
    ImPlot3DPoint corners[8];
    ComputeBoxCorners(corners, range_min, range_max);

    // Compute box corners in pixel space
    ImVec2 corners_pix[8];
    ComputeBoxCornersPix(corners_pix, corners);

    // Compute axes start and end corners (given current rotation)
    int axis_corners[3][2];
    if (is_2d) {
        int face = plane_2d + 3 * active_faces[plane_2d]; // Face of the 2D plot
        int common_edges[2] = {-1, -1};                   // Edges shared by the 3 faces

        // Find the common edges between the 3 faces
        for (int i = 0; i < 4; i++) {
            int edge = face_edges[face][i];
            for (int j = 0; j < 2; j++) {
                int axis = (plane_2d + 1 + j) % 3;
                int face_idx = axis + active_faces[axis] * 3;
                for (int k = 0; k < 4; k++) {
                    if (face_edges[face_idx][k] == edge) {
                        common_edges[j] = edge;
                        break;
                    }
                }
            }
        }

        // Get corners from 2 edges (origin is the corner in common)
        int origin_corner = -1;
        int x_corner = -1;
        int y_corner = -1;
        for (int i = 0; i < 2; i++)
            for (int j = 0; j < 2; j++)
                if (edges[common_edges[0]][i] == edges[common_edges[1]][j]) {
                    origin_corner = edges[common_edges[0]][i];
                    x_corner = edges[common_edges[0]][!i];
                    y_corner = edges[common_edges[1]][!j];
                }

        // Swap x and y if they are flipped
        ImVec2 x_vec = corners_pix[x_corner] - corners_pix[origin_corner];
        ImVec2 y_vec = corners_pix[y_corner] - corners_pix[origin_corner];
        if (y_vec.x > x_vec.x)
            ImSwap(x_corner, y_corner);

        // Check which 3d axis the 2d axis refers to
        ImPlot3DPoint origin_3d = corners[origin_corner];
        ImPlot3DPoint x_3d = (corners[x_corner] - origin_3d).Normalized();
        ImPlot3DPoint y_3d = (corners[y_corner] - origin_3d).Normalized();
        int x_axis = -1;
        bool x_inverted = false;
        int y_axis = -1;
        bool y_inverted = false;
        for (int i = 0; i < 2; i++) {
            int axis_i = (plane_2d + 1 + i) % 3;
            if (y_axis != -1 || (ImAbs(x_3d[axis_i]) > 1e-8f && x_axis == -1)) {
                x_axis = axis_i;
                x_inverted = x_3d[axis_i] < 0.0f;
            } else {
                y_axis = axis_i;
                y_inverted = y_3d[axis_i] < 0.0f;
            }
        }

        // Set the 3d axis corners based on the 2d axis corners
        axis_corners[plane_2d][0] = -1;
        axis_corners[plane_2d][1] = -1;
        if (x_inverted) {
            axis_corners[x_axis][0] = x_corner;
            axis_corners[x_axis][1] = origin_corner;
        } else {
            axis_corners[x_axis][0] = origin_corner;
            axis_corners[x_axis][1] = x_corner;
        }
        if (y_inverted) {
            axis_corners[y_axis][0] = y_corner;
            axis_corners[y_axis][1] = origin_corner;
        } else {
            axis_corners[y_axis][0] = origin_corner;
            axis_corners[y_axis][1] = y_corner;
        }
    } else {
        int index = (active_faces[0] << 2) | (active_faces[1] << 1) | (active_faces[2]);
        for (int a = 0; a < 3; a++) {
            axis_corners[a][0] = axis_corners_lookup_3d[index][a][0];
            axis_corners[a][1] = axis_corners_lookup_3d[index][a][1];
        }
    }

    // Render components
    RenderPlotBackground(draw_list, plot, corners_pix, active_faces, plane_2d);
    RenderPlotBorder(draw_list, plot, corners_pix, active_faces, plane_2d);
    RenderGrid(draw_list, plot, corners, active_faces, plane_2d);
    RenderTickMarks(draw_list, plot, corners, corners_pix, axis_corners, plane_2d);
    RenderTickLabels(draw_list, plot, corners, corners_pix, axis_corners);
    RenderAxisLabels(draw_list, plot, corners, corners_pix, axis_corners);
}

//-----------------------------------------------------------------------------
// [SECTION] Formatter
//-----------------------------------------------------------------------------

int Formatter_Default(float value, char* buff, int size, void* data) {
    char* fmt = (char*)data;
    return ImFormatString(buff, size, fmt, value);
}

//------------------------------------------------------------------------------
// [SECTION] Locator
//------------------------------------------------------------------------------

double NiceNum(double x, bool round) {
    double f;
    double nf;
    int expv = (int)floor(ImLog10(x));
    f = x / ImPow(10.0, (double)expv);
    if (round)
        if (f < 1.5)
            nf = 1;
        else if (f < 3)
            nf = 2;
        else if (f < 7)
            nf = 5;
        else
            nf = 10;
    else if (f <= 1)
        nf = 1;
    else if (f <= 2)
        nf = 2;
    else if (f <= 5)
        nf = 5;
    else
        nf = 10;
    return nf * ImPow(10.0, expv);
}

void Locator_Default(ImPlot3DTicker& ticker, const ImPlot3DRange& range, ImPlot3DFormatter formatter, void* formatter_data) {
    if (range.Min == range.Max)
        return;
    const int nMinor = 5;
    const int nMajor = 3;
    const int max_ticks_labels = 7;
    const double nice_range = NiceNum(range.Size() * 0.99, false);
    const double interval = NiceNum(nice_range / (nMajor - 1), true);
    const double graphmin = floor(range.Min / interval) * interval;
    const double graphmax = ceil(range.Max / interval) * interval;
    bool first_major_set = false;
    int first_major_idx = 0;
    const int idx0 = ticker.TickCount(); // ticker may have user custom ticks
    ImVec2 total_size(0, 0);
    for (double major = graphmin; major < graphmax + 0.5 * interval; major += interval) {
        // is this zero? combat zero formatting issues
        if (major - interval < 0 && major + interval > 0)
            major = 0;
        if (range.Contains(major)) {
            if (!first_major_set) {
                first_major_idx = ticker.TickCount();
                first_major_set = true;
            }
            total_size += ticker.AddTick(major, true, true, formatter, formatter_data).LabelSize;
        }
        for (int i = 1; i < nMinor; ++i) {
            double minor = major + i * interval / nMinor;
            if (range.Contains(minor)) {
                total_size += ticker.AddTick(minor, false, true, formatter, formatter_data).LabelSize;
            }
        }
    }

    // Prune tick labels
    if (ticker.TickCount() > max_ticks_labels) {
        for (int i = first_major_idx - 1; i >= idx0; i -= 2)
            ticker.Ticks[i].ShowLabel = false;
        for (int i = first_major_idx + 1; i < ticker.TickCount(); i += 2)
            ticker.Ticks[i].ShowLabel = false;
    }
}

//------------------------------------------------------------------------------
// [SECTION] Context Menus
//------------------------------------------------------------------------------

bool ShowLegendContextMenu(ImPlot3DLegend& legend, bool visible) {
    const float s = ImGui::GetFrameHeight();
    bool ret = false;
    if (ImGui::Checkbox("Show", &visible))
        ret = true;
    if (ImGui::RadioButton("H", ImPlot3D::ImHasFlag(legend.Flags, ImPlot3DLegendFlags_Horizontal)))
        legend.Flags |= ImPlot3DLegendFlags_Horizontal;
    ImGui::SameLine();
    if (ImGui::RadioButton("V", !ImPlot3D::ImHasFlag(legend.Flags, ImPlot3DLegendFlags_Horizontal)))
        legend.Flags &= ~ImPlot3DLegendFlags_Horizontal;
    ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 2));
    // clang-format off
    if (ImGui::Button("NW",ImVec2(1.5f*s,s))) { legend.Location = ImPlot3DLocation_NorthWest; } ImGui::SameLine();
    if (ImGui::Button("N", ImVec2(1.5f*s,s))) { legend.Location = ImPlot3DLocation_North;     } ImGui::SameLine();
    if (ImGui::Button("NE",ImVec2(1.5f*s,s))) { legend.Location = ImPlot3DLocation_NorthEast; }
    if (ImGui::Button("W", ImVec2(1.5f*s,s))) { legend.Location = ImPlot3DLocation_West;      } ImGui::SameLine();
    if (ImGui::InvisibleButton("C", ImVec2(1.5f*s,s))) {     } ImGui::SameLine();
    if (ImGui::Button("E", ImVec2(1.5f*s,s))) { legend.Location = ImPlot3DLocation_East;      }
    if (ImGui::Button("SW",ImVec2(1.5f*s,s))) { legend.Location = ImPlot3DLocation_SouthWest; } ImGui::SameLine();
    if (ImGui::Button("S", ImVec2(1.5f*s,s))) { legend.Location = ImPlot3DLocation_South;     } ImGui::SameLine();
    if (ImGui::Button("SE",ImVec2(1.5f*s,s))) { legend.Location = ImPlot3DLocation_SouthEast; }
    // clang-format on
    ImGui::PopStyleVar();
    return ret;
}

void ShowAxisContextMenu(ImPlot3DAxis& axis) {
    ImGui::PushItemWidth(75);
    bool always_locked = axis.IsRangeLocked() || axis.IsAutoFitting();
    bool label = axis.HasLabel();
    bool grid = axis.HasGridLines();
    bool ticks = axis.HasTickMarks();
    bool labels = axis.HasTickLabels();
    double drag_speed = (axis.Range.Size() <= FLT_EPSILON) ? FLT_EPSILON * 1.0e+13 : 0.01 * axis.Range.Size(); // recover from almost equal axis limits.

    ImGui::BeginDisabled(always_locked);
    ImGui::CheckboxFlags("##LockMin", (unsigned int*)&axis.Flags, ImPlot3DAxisFlags_LockMin);
    ImGui::EndDisabled();
    ImGui::SameLine();

    ImGui::BeginDisabled(axis.IsLockedMin() || always_locked);
    float temp_min = axis.Range.Min;
    if (ImGui::DragFloat("Min", &temp_min, (float)drag_speed, -HUGE_VAL, axis.Range.Max - FLT_EPSILON)) {
        axis.SetMin(temp_min, true);
    }
    ImGui::EndDisabled();

    ImGui::BeginDisabled(always_locked);
    ImGui::CheckboxFlags("##LockMax", (unsigned int*)&axis.Flags, ImPlot3DAxisFlags_LockMax);
    ImGui::EndDisabled();
    ImGui::SameLine();
    ImGui::BeginDisabled(axis.IsLockedMax() || always_locked);
    float temp_max = axis.Range.Max;
    if (ImGui::DragFloat("Max", &temp_max, (float)drag_speed, axis.Range.Min + FLT_EPSILON, HUGE_VAL)) {
        axis.SetMax(temp_max, true);
    }
    ImGui::EndDisabled();

    ImGui::Separator();

    // Flags
    ImGui::CheckboxFlags("Auto-Fit", (unsigned int*)&axis.Flags, ImPlot3DAxisFlags_AutoFit);
    ImGui::Separator();

    ImGui::BeginDisabled(axis.Label.empty());
    if (ImGui::Checkbox("Label", &label))
        ImFlipFlag(axis.Flags, ImPlot3DAxisFlags_NoLabel);
    ImGui::EndDisabled();

    if (ImGui::Checkbox("Grid Lines", &grid))
        ImFlipFlag(axis.Flags, ImPlot3DAxisFlags_NoGridLines);
    if (ImGui::Checkbox("Tick Marks", &ticks))
        ImFlipFlag(axis.Flags, ImPlot3DAxisFlags_NoTickMarks);
    if (ImGui::Checkbox("Tick Labels", &labels))
        ImFlipFlag(axis.Flags, ImPlot3DAxisFlags_NoTickLabels);
}

void ShowPlotContextMenu(ImPlot3DPlot& plot) {
    ImPlot3DContext& gp = *GImPlot3D;
    const bool owns_legend = gp.CurrentItems == &plot.Items;

    char buf[16] = {};

    const char* axis_labels[3] = {"X-Axis", "Y-Axis", "Z-Axis"};
    for (int i = 0; i < 3; i++) {
        ImPlot3DAxis& axis = plot.Axes[i];
        ImGui::PushID(i);
        ImFormatString(buf, sizeof(buf) - 1, i == 0 ? "X-Axis" : "X-Axis %d", i + 1);
        if (ImGui::BeginMenu(axis.HasLabel() ? axis.GetLabel() : axis_labels[i])) {
            ShowAxisContextMenu(axis);
            ImGui::EndMenu();
        }
        ImGui::PopID();
    }

    ImGui::Separator();
    if ((ImGui::BeginMenu("Legend"))) {
        if (ShowLegendContextMenu(plot.Items.Legend, !ImPlot3D::ImHasFlag(plot.Flags, ImPlot3DFlags_NoLegend)))
            ImFlipFlag(plot.Flags, ImPlot3DFlags_NoLegend);
        ImGui::EndMenu();
    }

    if ((ImGui::BeginMenu("Settings"))) {
        ImGui::BeginDisabled(plot.Title.empty());
        if (ImGui::MenuItem("Title", nullptr, plot.HasTitle()))
            ImFlipFlag(plot.Flags, ImPlot3DFlags_NoTitle);
        ImGui::EndDisabled();
        ImGui::EndMenu();
    }
}

//-----------------------------------------------------------------------------
// [SECTION] Begin/End Plot
//-----------------------------------------------------------------------------

bool BeginPlot(const char* title_id, const ImVec2& size, ImPlot3DFlags flags) {
    IMPLOT3D_CHECK_CTX();
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot == nullptr, "Mismatched BeginPlot()/EndPlot()!");

    // Get window
    ImGuiContext& g = *GImGui;
    ImGuiWindow* window = g.CurrentWindow;

    // Skip if needed
    if (window->SkipItems)
        return false;

    // Get or create plot
    const ImGuiID ID = window->GetID(title_id);
    const bool just_created = gp.Plots.GetByKey(ID) == nullptr;
    gp.CurrentPlot = gp.Plots.GetOrAddByKey(ID);
    gp.CurrentItems = &gp.CurrentPlot->Items;
    ImPlot3DPlot& plot = *gp.CurrentPlot;

    // Populate plot
    plot.ID = ID;
    plot.JustCreated = just_created;
    if (just_created) {
        plot.Rotation = init_rotation;
        plot.FitThisFrame = true;
        for (int i = 0; i < 3; i++) {
            plot.Axes[i] = ImPlot3DAxis();
            plot.Axes[i].FitThisFrame = true;
        }
    }
    if (plot.PreviousFlags != flags)
        plot.Flags = flags;
    plot.PreviousFlags = flags;
    plot.SetupLocked = false;
    plot.OpenContextThisFrame = false;

    // Populate title
    plot.SetTitle(title_id);

    // Calculate frame size
    ImVec2 frame_size = ImGui::CalcItemSize(size, gp.Style.PlotDefaultSize.x, gp.Style.PlotDefaultSize.y);
    if (frame_size.x < gp.Style.PlotMinSize.x && size.x < 0.0f)
        frame_size.x = gp.Style.PlotMinSize.x;
    if (frame_size.y < gp.Style.PlotMinSize.y && size.y < 0.0f)
        frame_size.y = gp.Style.PlotMinSize.y;

    // Create child window to capture scroll
    ImGui::BeginChild(title_id, frame_size, false, ImGuiWindowFlags_NoScrollbar);
    window = ImGui::GetCurrentWindow();
    window->ScrollMax.y = 1.0f;

    plot.FrameRect = ImRect(window->DC.CursorPos, window->DC.CursorPos + frame_size);
    ImGui::ItemSize(plot.FrameRect);
    if (!ImGui::ItemAdd(plot.FrameRect, plot.ID, &plot.FrameRect)) {
        gp.CurrentPlot = nullptr;
        gp.CurrentItems = nullptr;
        ImGui::EndChild();
        return false;
    }

    // Reset legend
    plot.Items.Legend.Reset();

    // Push frame rect clipping
    ImGui::PushClipRect(plot.FrameRect.Min, plot.FrameRect.Max, true);
    plot.DrawList._Flags = window->DrawList->Flags;
    plot.DrawList._SharedData = ImGui::GetDrawListSharedData();

    return true;
}

void EndPlot() {
    IMPLOT3D_CHECK_CTX();
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "Mismatched BeginPlot()/EndPlot()!");
    ImPlot3DPlot& plot = *gp.CurrentPlot;

    // Move triangles from 3D draw list to ImGui draw list
    plot.DrawList.SortedMoveToImGuiDrawList();

    // Handle data fitting
    if (plot.FitThisFrame) {
        plot.FitThisFrame = false;
        for (int i = 0; i < 3; i++) {
            if (plot.Axes[i].FitThisFrame) {
                plot.Axes[i].FitThisFrame = false;
                plot.Axes[i].ApplyFit();
            }
        }
    }

    // Lock setup if not already done
    SetupLock();

    // Reset legend hover
    plot.Items.Legend.Hovered = false;

    // Render legend
    RenderLegend();

    // Render mouse position
    RenderMousePos();

    // Legend context menu
    if (ImGui::BeginPopup("##LegendContext")) {
        ImGui::Text("Legend");
        ImGui::Separator();
        if (ShowLegendContextMenu(plot.Items.Legend, !ImPlot3D::ImHasFlag(plot.Flags, ImPlot3DFlags_NoLegend)))
            ImFlipFlag(plot.Flags, ImPlot3DFlags_NoLegend);
        ImGui::EndPopup();
    }

    // Axis context menus
    static const char* axis_contexts[3] = {"##XAxisContext", "##YAxisContext", "##ZAxisContext"};
    for (int i = 0; i < 3; i++) {
        ImPlot3DAxis& axis = plot.Axes[i];
        if (ImGui::BeginPopup(axis_contexts[i])) {
            ImGui::Text(axis.HasLabel() ? axis.GetLabel() : "%c-Axis", 'X' + i);
            ImGui::Separator();
            ShowAxisContextMenu(axis);
            ImGui::EndPopup();
        }
    }

    // Plot context menu
    if (ImGui::BeginPopup("##PlotContext")) {
        ShowPlotContextMenu(plot);
        ImGui::EndPopup();
    }

    // Pop frame rect clipping
    ImGui::PopClipRect();

    // End child window
    ImGui::EndChild();

    // Reset current plot
    gp.CurrentPlot = nullptr;
    gp.CurrentItems = nullptr;

    // Reset the plot items for the next frame
    for (int i = 0; i < plot.Items.GetItemCount(); i++)
        plot.Items.GetItemByIndex(i)->SeenThisFrame = false;
}

//-----------------------------------------------------------------------------
// [SECTION] Setup
//-----------------------------------------------------------------------------

void SetupAxis(ImAxis3D idx, const char* label, ImPlot3DAxisFlags flags) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr && !gp.CurrentPlot->SetupLocked,
                         "SetupAxis() needs to be called after BeginPlot() and before any setup locking functions (e.g. PlotX)!");

    // Get plot and axis
    ImPlot3DPlot& plot = *gp.CurrentPlot;
    ImPlot3DAxis& axis = plot.Axes[idx];
    if (axis.PreviousFlags != flags)
        axis.Flags = flags;
    axis.PreviousFlags = flags;
    axis.SetLabel(label);
}

void SetupAxisLimits(ImAxis3D idx, double min_lim, double max_lim, ImPlot3DCond cond) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr && !gp.CurrentPlot->SetupLocked,
                         "SetupAxisLimits() needs to be called after BeginPlot and before any setup locking functions (e.g. PlotX)!"); // get plot and axis
    ImPlot3DPlot& plot = *gp.CurrentPlot;
    ImPlot3DAxis& axis = plot.Axes[idx];
    if (!plot.Initialized || cond == ImPlot3DCond_Always) {
        axis.SetRange(min_lim, max_lim);
        axis.RangeCond = cond;
        axis.FitThisFrame = false;
    }
}

void SetupAxes(const char* x_label, const char* y_label, const char* z_label, ImPlot3DAxisFlags x_flags, ImPlot3DAxisFlags y_flags, ImPlot3DAxisFlags z_flags) {
    SetupAxis(ImAxis3D_X, x_label, x_flags);
    SetupAxis(ImAxis3D_Y, y_label, y_flags);
    SetupAxis(ImAxis3D_Z, z_label, z_flags);
}

void SetupAxesLimits(double x_min, double x_max, double y_min, double y_max, double z_min, double z_max, ImPlot3DCond cond) {
    SetupAxisLimits(ImAxis3D_X, x_min, x_max, cond);
    SetupAxisLimits(ImAxis3D_Y, y_min, y_max, cond);
    SetupAxisLimits(ImAxis3D_Z, z_min, z_max, cond);
    if (cond == ImPlot3DCond_Once)
        GImPlot3D->CurrentPlot->FitThisFrame = false;
}

void SetupLegend(ImPlot3DLocation location, ImPlot3DLegendFlags flags) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr && !gp.CurrentPlot->SetupLocked,
                         "SetupLegend() needs to be called after BeginPlot() and before any setup locking functions (e.g. PlotX)!");
    IM_ASSERT_USER_ERROR(gp.CurrentItems != nullptr,
                         "SetupLegend() needs to be called within an itemized context!");
    ImPlot3DLegend& legend = gp.CurrentItems->Legend;
    if (legend.PreviousLocation != location)
        legend.Location = location;
    legend.PreviousLocation = location;
    if (legend.PreviousFlags != flags)
        legend.Flags = flags;
    legend.PreviousFlags = flags;
}

//-----------------------------------------------------------------------------
// [SECTION] Plot Utils
//-----------------------------------------------------------------------------

ImPlot3DPlot* GetCurrentPlot() {
    return GImPlot3D->CurrentPlot;
}

void BustPlotCache() {
    ImPlot3DContext& gp = *GImPlot3D;
    gp.Plots.Clear();
}

ImVec2 PlotToPixels(const ImPlot3DPoint& point) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "PlotToPixels() needs to be called between BeginPlot() and EndPlot()!");
    return NDCToPixels(PlotToNDC(point));
}

ImVec2 PlotToPixels(double x, double y, double z) {
    return PlotToPixels(ImPlot3DPoint(x, y, z));
}

ImPlot3DRay PixelsToPlotRay(const ImVec2& pix) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "PixelsToPlotRay() needs to be called between BeginPlot() and EndPlot()!");
    return NDCRayToPlotRay(PixelsToNDCRay(pix));
}

ImPlot3DRay PixelsToPlotRay(double x, double y) {
    return PixelsToPlotRay(ImVec2(x, y));
}

ImPlot3DPoint PixelsToPlotPlane(const ImVec2& pix, ImPlane3D plane, bool mask) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "PixelsToPlotPlane() needs to be called between BeginPlot() and EndPlot()!");

    ImPlot3DPlot& plot = *gp.CurrentPlot;
    ImPlot3DRay ray = PixelsToNDCRay(pix);
    const ImPlot3DPoint& O = ray.Origin;
    const ImPlot3DPoint& D = ray.Direction;

    // Helper lambda to check intersection with a given coordinate and return intersection point if valid.
    auto IntersectPlane = [&](float coord) -> ImPlot3DPoint {
        // Solve for t in O[axis] + D[axis]*t = coord
        float denom = 0.0f;
        float numer = 0.0f;
        if (plane == ImPlane3D_YZ) {
            denom = D.x;
            numer = coord - O.x;
        } else if (plane == ImPlane3D_XZ) {
            denom = D.y;
            numer = coord - O.y;
        } else if (plane == ImPlane3D_XY) {
            denom = D.z;
            numer = coord - O.z;
        }

        if (ImAbs(denom) < 1e-12f) {
            // Ray is parallel or nearly parallel to the plane
            return ImPlot3DPoint(NAN, NAN, NAN);
        }

        float t = numer / denom;
        if (t < 0.0f) {
            // Intersection behind the ray origin
            return ImPlot3DPoint(NAN, NAN, NAN);
        }

        return O + D * t;
    };

    // Helper lambda to check if point P is within the plot box
    auto InRange = [&](const ImPlot3DPoint& P) {
        return P.x >= -0.5 && P.x <= 0.5 &&
               P.y >= -0.5 && P.y <= 0.5 &&
               P.z >= -0.5 && P.z <= 0.5;
    };

    // Compute which plane to intersect with
    bool active_faces[3];
    ComputeActiveFaces(active_faces, plot.Rotation);

    // Calculate intersection point with the planes
    ImPlot3DPoint P = IntersectPlane(active_faces[plane] ? 0.5 : -0.5);
    if (P.IsNaN())
        return P;

    // Handle mask (if one of the intersections is out of range, set it to NAN)
    if (mask) {
        switch (plane) {
            case ImPlane3D_YZ:
                if (!InRange(ImPlot3DPoint(0.0, P.y, P.z)))
                    return ImPlot3DPoint(NAN, NAN, NAN);
                break;
            case ImPlane3D_XZ:
                if (!InRange(ImPlot3DPoint(P.x, 0.0, P.z)))
                    return ImPlot3DPoint(NAN, NAN, NAN);
                break;
            case ImPlane3D_XY:
                if (!InRange(ImPlot3DPoint(P.x, P.y, 0.0)))
                    return ImPlot3DPoint(NAN, NAN, NAN);
                break;
        }
    }

    return NDCToPlot(P);
}

ImPlot3DPoint PixelsToPlotPlane(double x, double y, ImPlane3D plane, bool mask) {
    return PixelsToPlotPlane(ImVec2(x, y), plane, mask);
}

ImVec2 GetPlotPos() {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "GetPlotPos() needs to be called between BeginPlot() and EndPlot()!");
    SetupLock();
    return gp.CurrentPlot->PlotRect.Min;
}

ImVec2 GetPlotSize() {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "GetPlotSize() needs to be called between BeginPlot() and EndPlot()!");
    SetupLock();
    return gp.CurrentPlot->PlotRect.GetSize();
}

ImVec2 GetFramePos() {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "GetFramePos() needs to be called between BeginPlot() and EndPlot()!");
    return gp.CurrentPlot->FrameRect.Min;
}

ImVec2 GetFrameSize() {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "GetFrameSize() needs to be called between BeginPlot() and EndPlot()!");
    return gp.CurrentPlot->FrameRect.GetSize();
}

ImPlot3DPoint PlotToNDC(const ImPlot3DPoint& point) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "PlotToNDC() needs to be called between BeginPlot() and EndPlot()!");
    ImPlot3DPlot& plot = *gp.CurrentPlot;
    SetupLock();

    ImPlot3DPoint ndc_point;
    for (int i = 0; i < 3; i++)
        ndc_point[i] = plot.Axes[i].PlotToNDC(point[i]);
    return ndc_point;
}

ImPlot3DPoint NDCToPlot(const ImPlot3DPoint& point) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "NDCToPlot() needs to be called between BeginPlot() and EndPlot()!");
    ImPlot3DPlot& plot = *gp.CurrentPlot;
    SetupLock();

    ImPlot3DPoint plot_point;
    for (int i = 0; i < 3; i++)
        plot_point[i] = plot.Axes[i].NDCToPlot(point[i]);
    return plot_point;
}

ImVec2 NDCToPixels(const ImPlot3DPoint& point) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "NDCToPixels() needs to be called between BeginPlot() and EndPlot()!");
    ImPlot3DPlot& plot = *gp.CurrentPlot;
    SetupLock();

    float zoom = ImMin(plot.PlotRect.GetWidth(), plot.PlotRect.GetHeight()) / 1.8f;
    ImVec2 center = plot.PlotRect.GetCenter();
    ImPlot3DPoint point_pix = zoom * (plot.Rotation * point);
    point_pix.y *= -1.0f; // Invert y-axis
    point_pix.x += center.x;
    point_pix.y += center.y;

    return {point_pix.x, point_pix.y};
}

ImPlot3DRay PixelsToNDCRay(const ImVec2& pix) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "PixelsToNDCRay() needs to be called between BeginPlot() and EndPlot()!");
    ImPlot3DPlot& plot = *gp.CurrentPlot;
    SetupLock();

    // Calculate zoom factor and plot center
    float zoom = ImMin(plot.PlotRect.GetWidth(), plot.PlotRect.GetHeight()) / 1.8f;
    ImVec2 center = plot.PlotRect.GetCenter();

    // Undo screen transformations to get back to NDC space
    float x = (pix.x - center.x) / zoom;
    float y = -(pix.y - center.y) / zoom; // Invert y-axis

    // Define near and far points in NDC space along the z-axis
    ImPlot3DPoint ndc_near = plot.Rotation.Inverse() * ImPlot3DPoint(x, y, -10.0f);
    ImPlot3DPoint ndc_far = plot.Rotation.Inverse() * ImPlot3DPoint(x, y, 10.0f);

    // Create the ray in NDC space
    ImPlot3DRay ndc_ray;
    ndc_ray.Origin = ndc_near;
    ndc_ray.Direction = (ndc_far - ndc_near).Normalized();

    return ndc_ray;
}

ImPlot3DRay NDCRayToPlotRay(const ImPlot3DRay& ray) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "NDCRayToPlotRay() needs to be called between BeginPlot() and EndPlot()!");
    ImPlot3DPlot& plot = *gp.CurrentPlot;
    SetupLock();

    // Convert NDC origin and a point along the ray to plot coordinates
    ImPlot3DPoint plot_origin = NDCToPlot(ray.Origin);
    ImPlot3DPoint ndc_point_along_ray = ray.Origin + ray.Direction;
    ImPlot3DPoint plot_point_along_ray = NDCToPlot(ndc_point_along_ray);

    // Compute the direction in plot coordinates
    ImPlot3DPoint plot_direction = (plot_point_along_ray - plot_origin).Normalized();

    // Create the ray in plot coordinates
    ImPlot3DRay plot_ray;
    plot_ray.Origin = plot_origin;
    plot_ray.Direction = plot_direction;

    return plot_ray;
}

//-----------------------------------------------------------------------------
// [SECTION] Setup Utils
//-----------------------------------------------------------------------------

static const float MOUSE_CURSOR_DRAG_THRESHOLD = 5.0f;
static const float ANIMATION_ANGULAR_VELOCITY = 2 * 3.1415f;

void HandleInput(ImPlot3DPlot& plot) {
    ImGuiIO& IO = ImGui::GetIO();

    // clang-format off
    const ImGuiButtonFlags plot_button_flags = ImGuiButtonFlags_AllowOverlap
                                             | ImGuiButtonFlags_PressedOnClick
                                             | ImGuiButtonFlags_PressedOnDoubleClick
                                             | ImGuiButtonFlags_MouseButtonLeft
                                             | ImGuiButtonFlags_MouseButtonRight
                                             | ImGuiButtonFlags_MouseButtonMiddle;
    // clang-format on
    const bool plot_clicked = ImGui::ButtonBehavior(plot.PlotRect, plot.ID, &plot.Hovered, &plot.Held, plot_button_flags);
#if (IMGUI_VERSION_NUM < 18966)
    ImGui::SetItemAllowOverlap(); // Handled by ButtonBehavior()
#endif

    // State
    const ImVec2 rot_drag = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right);
    const bool rotating = ImLengthSqr(rot_drag) > MOUSE_CURSOR_DRAG_THRESHOLD;

    // Check if any axis/plane is hovered
    const ImPlot3DQuat& rotation = plot.Rotation;
    ImPlot3DPoint range_min = plot.RangeMin();
    ImPlot3DPoint range_max = plot.RangeMax();
    bool active_faces[3];
    int plane_2d = -1;
    ComputeActiveFaces(active_faces, rotation, &plane_2d);
    ImPlot3DPoint corners[8];
    ComputeBoxCorners(corners, range_min, range_max);
    ImVec2 corners_pix[8];
    ComputeBoxCornersPix(corners_pix, corners);
    int hovered_plane_idx = -1;
    int hovered_plane = GetMouseOverPlane(plot, active_faces, corners_pix, &hovered_plane_idx);
    int hovered_edge_idx = -1;
    int hovered_axis = GetMouseOverAxis(plot, active_faces, corners_pix, plane_2d, &hovered_edge_idx);
    if (hovered_axis != -1) {
        hovered_plane_idx = -1;
        hovered_plane = -1;
    }

    // If the user is no longer pressing the translation/zoom buttons, set axes as not held
    if (!ImGui::IsMouseDown(ImGuiMouseButton_Left) && !ImGui::IsMouseDown(ImGuiMouseButton_Middle)) {
        for (int i = 0; i < 3; i++)
            plot.Axes[i].Held = false;
    }

    // Reset held edge/plane indices (it will be set if mouse button is down)
    if (!plot.Held) {
        plot.HeldEdgeIdx = -1;
        plot.HeldPlaneIdx = -1;
    }

    // Check which axes should be transformed (fit/zoom/translate)
    bool any_axis_held = plot.Axes[0].Held || plot.Axes[1].Held || plot.Axes[2].Held;
    static bool transform_axis[3] = {false, false, false};
    if (!any_axis_held) {
        // Only update the transformation axes if the user is not already performing a transformation
        transform_axis[0] = transform_axis[1] = transform_axis[2] = false;
        if (hovered_axis != -1) {
            transform_axis[hovered_axis] = true;
        } else if (hovered_plane != -1) {
            transform_axis[(hovered_plane + 1) % 3] = true;
            transform_axis[(hovered_plane + 2) % 3] = true;
        } else {
            transform_axis[0] = transform_axis[1] = transform_axis[2] = true;
        }
    }

    // Handle translation/zoom fit with double click
    if (plot_clicked && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) || ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Middle)) {
        plot.FitThisFrame = true;
        for (int i = 0; i < 3; i++)
            plot.Axes[i].FitThisFrame = transform_axis[i];
    }

    // Handle auto fit
    for (int i = 0; i < 3; i++)
        if (plot.Axes[i].IsAutoFitting()) {
            plot.FitThisFrame = true;
            plot.Axes[i].FitThisFrame = true;
        }

    // Handle translation with right mouse button
    if (plot.Held && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
        ImVec2 delta(IO.MouseDelta.x, IO.MouseDelta.y);

        if (transform_axis[0] && transform_axis[1] && transform_axis[2]) {
            // Perform unconstrained translation (translate on the viewer plane)

            // Compute delta_pixels in 3D (invert y-axis)
            ImPlot3DPoint delta_pixels(delta.x, -delta.y, 0.0f);

            // Convert delta to NDC space
            float zoom = ImMin(plot.PlotRect.GetWidth(), plot.PlotRect.GetHeight()) / 1.8f;
            ImPlot3DPoint delta_NDC = plot.Rotation.Inverse() * (delta_pixels / zoom);

            // Convert delta to plot space
            ImPlot3DPoint delta_plot = delta_NDC * (plot.RangeMax() - plot.RangeMin());

            // Adjust plot range to translate the plot
            for (int i = 0; i < 3; i++) {
                if (transform_axis[i]) {
                    plot.Axes[i].SetRange(plot.Axes[i].Range.Min - delta_plot[i], plot.Axes[i].Range.Max - delta_plot[i]);
                    plot.Axes[i].Held = true;
                }
                // If no axis was held before (user started translating in this frame), set the held edge/plane indices
                if (!any_axis_held) {
                    plot.HeldEdgeIdx = hovered_edge_idx;
                    plot.HeldPlaneIdx = hovered_plane_idx;
                }
            }
        } else if (transform_axis[0] || transform_axis[1] || transform_axis[2]) {
            // Translate along plane/axis

            // Mouse delta in pixels
            ImVec2 mouse_pos = ImGui::GetMousePos();
            ImVec2 mouse_delta(IO.MouseDelta.x, IO.MouseDelta.y);

            // TODO Choose best plane given transform_axis and current view
            // For now it crashes when transforming only one axis in the 2D view
            ImPlane3D plane = ImPlane3D_XY;
            if (transform_axis[1] && transform_axis[2])
                plane = ImPlane3D_YZ;
            else if (transform_axis[0] && transform_axis[2])
                plane = ImPlane3D_XZ;
            else if (transform_axis[2])
                plane = ImPlane3D_YZ;

            ImPlot3DPoint mouse_plot = PixelsToPlotPlane(mouse_pos, plane, false);
            ImPlot3DPoint mouse_delta_plot = PixelsToPlotPlane(mouse_pos + mouse_delta, plane, false);
            ImPlot3DPoint delta_plot = mouse_delta_plot - mouse_plot;

            // Apply translation to the selected axes
            for (int i = 0; i < 3; i++) {
                if (transform_axis[i]) {
                    plot.Axes[i].SetRange(plot.Axes[i].Range.Min - delta_plot[i],
                                          plot.Axes[i].Range.Max - delta_plot[i]);
                    plot.Axes[i].Held = true;
                }
                if (!any_axis_held) {
                    plot.HeldEdgeIdx = hovered_edge_idx;
                    plot.HeldPlaneIdx = hovered_plane_idx;
                }
            }
        }
    }

    // Handle context click with right mouse button
    if (plot.Held && ImGui::IsMouseClicked(ImGuiMouseButton_Right))
        plot.ContextClick = true;
    if (rotating || ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right))
        plot.ContextClick = false;

    // Handle reset rotation with left mouse double click
    if (plot.Held && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right)) {
        plot.RotationAnimationEnd = plot.Rotation;

        // Calculate rotation to align the z-axis with the camera direction
        if (hovered_plane == -1) {
            plot.RotationAnimationEnd = init_rotation;
        } else {
            // Compute plane normal
            ImPlot3DPoint axis_normal = ImPlot3DPoint(0.0f, 0.0f, 0.0f);
            axis_normal[hovered_plane] = active_faces[hovered_plane] ? -1.0f : 1.0f;

            // Compute rotation to align the plane normal with the z-axis
            ImPlot3DQuat align_normal = ImPlot3DQuat::FromTwoVectors(plot.RotationAnimationEnd * axis_normal, ImPlot3DPoint(0.0f, 0.0f, 1.0f));
            plot.RotationAnimationEnd = align_normal * plot.RotationAnimationEnd;

            if (hovered_plane != 2) {
                // Compute rotation to point z-axis up
                ImPlot3DQuat align_up = ImPlot3DQuat::FromTwoVectors(plot.RotationAnimationEnd * ImPlot3DPoint(0.0f, 0.0f, 1.0f), ImPlot3DPoint(0.0f, 1.0f, 0.0f));
                plot.RotationAnimationEnd = align_up * plot.RotationAnimationEnd;
            } else {
                // Find the axis most aligned with the up direction
                ImPlot3DPoint up(0.0f, 1.0f, 0.0f);
                ImPlot3DPoint x_axis = plot.RotationAnimationEnd * ImPlot3DPoint(1.0f, 0.0f, 0.0f);
                ImPlot3DPoint y_axis = plot.RotationAnimationEnd * ImPlot3DPoint(0.0f, 1.0f, 0.0f);
                ImPlot3DPoint neg_x_axis = plot.RotationAnimationEnd * ImPlot3DPoint(-1.0f, 0.0f, 0.0f);
                ImPlot3DPoint neg_y_axis = plot.RotationAnimationEnd * ImPlot3DPoint(0.0f, -1.0f, 0.0f);

                struct AxisAlignment {
                    ImPlot3DPoint axis;
                    float dot;
                };

                AxisAlignment candidates[] = {
                    {x_axis, x_axis.Dot(up)},
                    {y_axis, y_axis.Dot(up)},
                    {neg_x_axis, neg_x_axis.Dot(up)},
                    {neg_y_axis, neg_y_axis.Dot(up)},
                };

                // Find the candidate with the maximum dot product
                AxisAlignment* best_candidate = &candidates[0];
                for (int i = 1; i < 4; ++i) {
                    if (candidates[i].dot > best_candidate->dot) {
                        best_candidate = &candidates[i];
                    }
                }

                // Compute the rotation to align the best candidate with the up direction
                ImPlot3DQuat align_up = ImPlot3DQuat::FromTwoVectors(best_candidate->axis, up);
                plot.RotationAnimationEnd = align_up * plot.RotationAnimationEnd;
            }
        }

        // Compute the angular distance between current and target rotation
        float dot_product = ImClamp(plot.Rotation.Dot(plot.RotationAnimationEnd), -1.0f, 1.0f);
        float angle = 2.0f * acosf(fabsf(dot_product));

        // Calculate animation time for constant the angular velocity
        plot.AnimationTime = angle / ANIMATION_ANGULAR_VELOCITY;
    }

    // Handle rotation with left mouse dragging
    if (plot.Held && ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
        ImVec2 delta(IO.MouseDelta.x, IO.MouseDelta.y);

        // Map delta to rotation angles (in radians)
        float angle_x = delta.x * (3.1415f / 180.0f);
        float angle_y = delta.y * (3.1415f / 180.0f);

        // Create quaternions for the rotations
        ImPlot3DQuat quat_x(angle_y, ImPlot3DPoint(1.0f, 0.0f, 0.0f));
        ImPlot3DQuat quat_z(angle_x, ImPlot3DPoint(0.0f, 0.0f, 1.0f));

        // Combine the new rotations with the current rotation
        plot.Rotation = quat_x * plot.Rotation * quat_z;
        plot.Rotation.Normalize();
    }

    // Handle zoom with mouse wheel
    if (plot.Hovered && (ImGui::IsMouseDown(ImGuiMouseButton_Middle) || IO.MouseWheel != 0)) {
        float delta = ImGui::IsMouseDown(ImGuiMouseButton_Middle) ? (-0.01f * IO.MouseDelta.y) : (-0.1f * IO.MouseWheel);
        float zoom = 1.0f + delta;
        for (int i = 0; i < 3; i++) {
            ImPlot3DAxis& axis = plot.Axes[i];
            float center = (axis.Range.Min + axis.Range.Max) * 0.5f;
            float size = axis.Range.Max - axis.Range.Min;
            size *= zoom;
            if (transform_axis[i]) {
                plot.Axes[i].SetRange(center - size * 0.5f, center + size * 0.5f);
                plot.Axes[i].Held = true;
            }
            // If no axis was held before (user started zoom in this frame), set the held edge/plane indices
            if (!any_axis_held) {
                plot.HeldEdgeIdx = hovered_edge_idx;
                plot.HeldPlaneIdx = hovered_plane_idx;
            }
        }
    }

    // Handle context menu (should not happen if it is not a double click action)
    bool not_double_click = (float)(ImGui::GetTime() - IO.MouseClickedTime[ImGuiMouseButton_Right]) > IO.MouseDoubleClickTime;
    if (plot.Hovered && plot.ContextClick && not_double_click && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
        plot.ContextClick = false;
        plot.OpenContextThisFrame = true;
    }

    // TODO Only open context menu if the mouse is not in the middle of double click action
    const char* axis_contexts[3] = {"##XAxisContext", "##YAxisContext", "##ZAxisContext"};
    if (plot.OpenContextThisFrame) {
        if (plot.Items.Legend.Hovered)
            ImGui::OpenPopup("##LegendContext");
        else if (hovered_axis != -1) {
            ImGui::OpenPopup(axis_contexts[hovered_axis]);
        } else if (hovered_plane != -1) {
            ImGui::OpenPopup(axis_contexts[hovered_plane]);
        } else if (plot.Hovered) {
            ImGui::OpenPopup("##PlotContext");
        }
    }
}

void SetupLock() {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentPlot != nullptr, "SetupLock() needs to be called between BeginPlot() and EndPlot()!");
    ImPlot3DPlot& plot = *gp.CurrentPlot;
    if (plot.SetupLocked)
        return;
    // Lock setup
    plot.SetupLocked = true;

    ImGuiContext& g = *GImGui;
    ImGuiWindow* window = g.CurrentWindow;
    ImDrawList* draw_list = window->DrawList;

    ImGui::PushClipRect(plot.FrameRect.Min, plot.FrameRect.Max, true);

    // Set default formatter/locator
    for (int i = 0; i < 3; i++) {
        ImPlot3DAxis& axis = plot.Axes[i];

        // Set formatter
        if (axis.Formatter == nullptr) {
            axis.Formatter = Formatter_Default;
            if (axis.FormatterData == nullptr)
                axis.FormatterData = (void*)IMPLOT3D_LABEL_FORMAT;
        }

        // Set locator
        if (axis.Locator == nullptr)
            axis.Locator = Locator_Default;
    }

    // Draw frame background
    ImU32 f_bg_color = GetStyleColorU32(ImPlot3DCol_FrameBg);
    draw_list->AddRectFilled(plot.FrameRect.Min, plot.FrameRect.Max, f_bg_color);

    // Compute canvas/canvas rectangle
    plot.CanvasRect = ImRect(plot.FrameRect.Min + gp.Style.PlotPadding, plot.FrameRect.Max - gp.Style.PlotPadding);
    plot.PlotRect = plot.CanvasRect;

    // Compute ticks
    for (int i = 0; i < 3; i++) {
        ImPlot3DAxis& axis = plot.Axes[i];
        axis.Ticker.Reset();
        axis.Locator(axis.Ticker, axis.Range, axis.Formatter, axis.FormatterData);
    }

    // Render title
    if (plot.HasTitle()) {
        ImU32 col = GetStyleColorU32(ImPlot3DCol_TitleText);
        ImVec2 top_center = ImVec2(plot.FrameRect.GetCenter().x, plot.CanvasRect.Min.y);
        AddTextCentered(draw_list, top_center, col, plot.GetTitle());
        plot.PlotRect.Min.y += ImGui::GetTextLineHeight() + gp.Style.LabelPadding.y;
    }

    // Handle animation
    if (plot.AnimationTime > 0.0f) {
        float dt = ImGui::GetIO().DeltaTime;
        float t = ImClamp(dt / plot.AnimationTime, 0.0f, 1.0f);
        plot.AnimationTime -= dt;
        if (plot.AnimationTime < 0.0f)
            plot.AnimationTime = 0.0f;
        plot.Rotation = ImPlot3DQuat::Slerp(plot.Rotation, plot.RotationAnimationEnd, t);
    }

    plot.Initialized = true;

    // Handle user input
    HandleInput(plot);

    // Render plot box
    RenderPlotBox(draw_list, plot);

    ImGui::PopClipRect();
}

//-----------------------------------------------------------------------------
// [SECTION] Miscellaneous
//-----------------------------------------------------------------------------

ImDrawList* GetPlotDrawList() {
    return ImGui::GetWindowDrawList();
}

//-----------------------------------------------------------------------------
// [SECTION] Styles
//-----------------------------------------------------------------------------

struct ImPlot3DStyleVarInfo {
    ImGuiDataType Type;
    ImU32 Count;
    ImU32 Offset;
    void* GetVarPtr(ImPlot3DStyle* style) const { return (void*)((unsigned char*)style + Offset); }
};

static const ImPlot3DStyleVarInfo GPlot3DStyleVarInfo[] =
    {
        // Item style
        {ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlot3DStyle, LineWeight)},   // ImPlot3DStyleVar_LineWeight
        {ImGuiDataType_S32, 1, (ImU32)IM_OFFSETOF(ImPlot3DStyle, Marker)},         // ImPlot3DStyleVar_Marker
        {ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlot3DStyle, MarkerSize)},   // ImPlot3DStyleVar_MarkerSize
        {ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlot3DStyle, MarkerWeight)}, // ImPlot3DStyleVar_MarkerWeight
        {ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImPlot3DStyle, FillAlpha)},    // ImPlot3DStyleVar_FillAlpha

        // Plot style
        {ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlot3DStyle, PlotDefaultSize)}, // ImPlot3DStyleVar_Plot3DDefaultSize
        {ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlot3DStyle, PlotMinSize)},     // ImPlot3DStyleVar_Plot3DMinSize
        {ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlot3DStyle, PlotPadding)},     // ImPlot3DStyleVar_Plot3DPadding

        // Label style
        {ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlot3DStyle, LabelPadding)},       // ImPlot3DStyleVar_LabelPaddine
        {ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlot3DStyle, LegendPadding)},      // ImPlot3DStyleVar_LegendPadding
        {ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlot3DStyle, LegendInnerPadding)}, // ImPlot3DStyleVar_LegendInnerPadding
        {ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImPlot3DStyle, LegendSpacing)},      // ImPlot3DStyleVar_LegendSpacing
};

static const ImPlot3DStyleVarInfo* GetPlotStyleVarInfo(ImPlot3DStyleVar idx) {
    IM_ASSERT(idx >= 0 && idx < ImPlot3DStyleVar_COUNT);
    IM_ASSERT(IM_ARRAYSIZE(GPlot3DStyleVarInfo) == ImPlot3DStyleVar_COUNT);
    return &GPlot3DStyleVarInfo[idx];
}

ImPlot3DStyle& GetStyle() { return GImPlot3D->Style; }

void StyleColorsAuto(ImPlot3DStyle* dst) {
    ImPlot3DStyle* style = dst ? dst : &ImPlot3D::GetStyle();
    ImVec4* colors = style->Colors;

    colors[ImPlot3DCol_Line] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_Fill] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_MarkerOutline] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_MarkerFill] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_TitleText] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_InlayText] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_FrameBg] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_PlotBg] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_PlotBorder] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_LegendBg] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_LegendBorder] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_LegendText] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_AxisText] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_AxisGrid] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_AxisTick] = IMPLOT3D_AUTO_COL;
}

void StyleColorsDark(ImPlot3DStyle* dst) {
    ImPlot3DStyle* style = dst ? dst : &ImPlot3D::GetStyle();
    ImVec4* colors = style->Colors;

    colors[ImPlot3DCol_Line] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_Fill] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_MarkerOutline] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_MarkerFill] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_TitleText] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
    colors[ImPlot3DCol_InlayText] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
    colors[ImPlot3DCol_FrameBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.07f);
    colors[ImPlot3DCol_PlotBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.50f);
    colors[ImPlot3DCol_PlotBorder] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f);
    colors[ImPlot3DCol_LegendBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f);
    colors[ImPlot3DCol_LegendBorder] = ImVec4(0.43f, 0.43f, 0.50f, 0.50f);
    colors[ImPlot3DCol_LegendText] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
    colors[ImPlot3DCol_AxisText] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
    colors[ImPlot3DCol_AxisGrid] = ImVec4(1.00f, 1.00f, 1.00f, 0.25f);
    colors[ImPlot3DCol_AxisTick] = IMPLOT3D_AUTO_COL;
}

void StyleColorsLight(ImPlot3DStyle* dst) {
    ImPlot3DStyle* style = dst ? dst : &ImPlot3D::GetStyle();
    ImVec4* colors = style->Colors;

    colors[ImPlot3DCol_Line] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_Fill] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_MarkerOutline] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_MarkerFill] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_TitleText] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
    colors[ImPlot3DCol_InlayText] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
    colors[ImPlot3DCol_FrameBg] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
    colors[ImPlot3DCol_PlotBg] = ImVec4(0.42f, 0.57f, 1.00f, 0.13f);
    colors[ImPlot3DCol_PlotBorder] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
    colors[ImPlot3DCol_LegendBg] = ImVec4(1.00f, 1.00f, 1.00f, 0.98f);
    colors[ImPlot3DCol_LegendBorder] = ImVec4(0.82f, 0.82f, 0.82f, 0.80f);
    colors[ImPlot3DCol_LegendText] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
    colors[ImPlot3DCol_AxisText] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f);
    colors[ImPlot3DCol_AxisGrid] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f);
    colors[ImPlot3DCol_AxisTick] = IMPLOT3D_AUTO_COL;
}

void StyleColorsClassic(ImPlot3DStyle* dst) {
    ImPlot3DStyle* style = dst ? dst : &ImPlot3D::GetStyle();
    ImVec4* colors = style->Colors;

    colors[ImPlot3DCol_Line] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_Fill] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_MarkerOutline] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_MarkerFill] = IMPLOT3D_AUTO_COL;
    colors[ImPlot3DCol_TitleText] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
    colors[ImPlot3DCol_InlayText] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
    colors[ImPlot3DCol_FrameBg] = ImVec4(0.43f, 0.43f, 0.43f, 0.39f);
    colors[ImPlot3DCol_PlotBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.35f);
    colors[ImPlot3DCol_PlotBorder] = ImVec4(0.50f, 0.50f, 0.50f, 0.50f);
    colors[ImPlot3DCol_LegendBg] = ImVec4(0.11f, 0.11f, 0.14f, 0.92f);
    colors[ImPlot3DCol_LegendBorder] = ImVec4(0.50f, 0.50f, 0.50f, 0.50f);
    colors[ImPlot3DCol_LegendText] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
    colors[ImPlot3DCol_AxisText] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
    colors[ImPlot3DCol_AxisGrid] = ImVec4(0.90f, 0.90f, 0.90f, 0.25f);
    colors[ImPlot3DCol_AxisTick] = IMPLOT3D_AUTO_COL;
}

void PushStyleColor(ImPlot3DCol idx, ImU32 col) {
    ImPlot3DContext& gp = *GImPlot3D;
    ImGuiColorMod backup;
    backup.Col = (ImGuiCol)idx;
    backup.BackupValue = gp.Style.Colors[idx];
    gp.ColorModifiers.push_back(backup);
    gp.Style.Colors[idx] = ImGui::ColorConvertU32ToFloat4(col);
}

void PushStyleColor(ImPlot3DCol idx, const ImVec4& col) {
    ImPlot3DContext& gp = *GImPlot3D;
    ImGuiColorMod backup;
    backup.Col = (ImGuiCol)idx;
    backup.BackupValue = gp.Style.Colors[idx];
    gp.ColorModifiers.push_back(backup);
    gp.Style.Colors[idx] = col;
}

void PopStyleColor(int count) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(count <= gp.ColorModifiers.Size, "You can't pop more modifiers than have been pushed!");
    while (count > 0) {
        ImGuiColorMod& backup = gp.ColorModifiers.back();
        gp.Style.Colors[backup.Col] = backup.BackupValue;
        gp.ColorModifiers.pop_back();
        count--;
    }
}

void PushStyleVar(ImPlot3DStyleVar idx, float val) {
    ImPlot3DContext& gp = *GImPlot3D;
    const ImPlot3DStyleVarInfo* var_info = GetPlotStyleVarInfo(idx);
    if (var_info->Type == ImGuiDataType_Float && var_info->Count == 1) {
        float* pvar = (float*)var_info->GetVarPtr(&gp.Style);
        gp.StyleModifiers.push_back(ImGuiStyleMod((ImGuiStyleVar)idx, *pvar));
        *pvar = val;
        return;
    }
    IM_ASSERT(0 && "Called PushStyleVar() float variant but variable is not a float!");
}

void PushStyleVar(ImPlot3DStyleVar idx, int val) {
    ImPlot3DContext& gp = *GImPlot3D;
    const ImPlot3DStyleVarInfo* var_info = GetPlotStyleVarInfo(idx);
    if (var_info->Type == ImGuiDataType_S32 && var_info->Count == 1) {
        int* pvar = (int*)var_info->GetVarPtr(&gp.Style);
        gp.StyleModifiers.push_back(ImGuiStyleMod((ImGuiStyleVar)idx, *pvar));
        *pvar = val;
        return;
    } else if (var_info->Type == ImGuiDataType_Float && var_info->Count == 1) {
        float* pvar = (float*)var_info->GetVarPtr(&gp.Style);
        gp.StyleModifiers.push_back(ImGuiStyleMod((ImGuiStyleVar)idx, *pvar));
        *pvar = (float)val;
        return;
    }
    IM_ASSERT(0 && "Called PushStyleVar() int variant but variable is not a int!");
}

void PushStyleVar(ImPlot3DStyleVar idx, const ImVec2& val) {
    ImPlot3DContext& gp = *GImPlot3D;
    const ImPlot3DStyleVarInfo* var_info = GetPlotStyleVarInfo(idx);
    if (var_info->Type == ImGuiDataType_Float && var_info->Count == 2) {
        ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&gp.Style);
        gp.StyleModifiers.push_back(ImGuiStyleMod((ImGuiStyleVar)idx, *pvar));
        *pvar = val;
        return;
    }
    IM_ASSERT(0 && "Called PushStyleVar() ImVec2 variant but variable is not a ImVec2!");
}

void PopStyleVar(int count) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(count <= gp.StyleModifiers.Size, "You can't pop more modifiers than have been pushed!");
    while (count > 0) {
        ImGuiStyleMod& backup = gp.StyleModifiers.back();
        const ImPlot3DStyleVarInfo* info = GetPlotStyleVarInfo(backup.VarIdx);
        void* data = info->GetVarPtr(&gp.Style);
        if (info->Type == ImGuiDataType_Float && info->Count == 1) {
            ((float*)data)[0] = backup.BackupFloat[0];
        } else if (info->Type == ImGuiDataType_Float && info->Count == 2) {
            ((float*)data)[0] = backup.BackupFloat[0];
            ((float*)data)[1] = backup.BackupFloat[1];
        } else if (info->Type == ImGuiDataType_S32 && info->Count == 1) {
            ((int*)data)[0] = backup.BackupInt[0];
        }
        gp.StyleModifiers.pop_back();
        count--;
    }
}

ImVec4 GetStyleColorVec4(ImPlot3DCol idx) {
    return IsColorAuto(idx) ? GetAutoColor(idx) : GImPlot3D->Style.Colors[idx];
}

ImU32 GetStyleColorU32(ImPlot3DCol idx) {
    return ImGui::ColorConvertFloat4ToU32(ImPlot3D::GetStyleColorVec4(idx));
}

//------------------------------------------------------------------------------
// [SECTION] Colormaps
//------------------------------------------------------------------------------

ImPlot3DColormap AddColormap(const char* name, const ImVec4* colormap, int size, bool qual) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(size > 1, "The colormap size must be greater than 1!");
    IM_ASSERT_USER_ERROR(gp.ColormapData.GetIndex(name) == -1, "The colormap name has already been used!");
    ImVector<ImU32> buffer;
    buffer.resize(size);
    for (int i = 0; i < size; ++i)
        buffer[i] = ImGui::ColorConvertFloat4ToU32(colormap[i]);
    return gp.ColormapData.Append(name, buffer.Data, size, qual);
}

ImPlot3DColormap AddColormap(const char* name, const ImU32* colormap, int size, bool qual) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(size > 1, "The colormap size must be greater than 1!");
    IM_ASSERT_USER_ERROR(gp.ColormapData.GetIndex(name) == -1, "The colormap name has already be used!");
    return gp.ColormapData.Append(name, colormap, size, qual);
}

int GetColormapCount() {
    ImPlot3DContext& gp = *GImPlot3D;
    return gp.ColormapData.Count;
}

const char* GetColormapName(ImPlot3DColormap colormap) {
    ImPlot3DContext& gp = *GImPlot3D;
    return gp.ColormapData.GetName(colormap);
}

ImPlot3DColormap GetColormapIndex(const char* name) {
    ImPlot3DContext& gp = *GImPlot3D;
    return gp.ColormapData.GetIndex(name);
}

void PushColormap(ImPlot3DColormap colormap) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(colormap >= 0 && colormap < gp.ColormapData.Count, "The colormap index is invalid!");
    gp.ColormapModifiers.push_back(gp.Style.Colormap);
    gp.Style.Colormap = colormap;
}

void PushColormap(const char* name) {
    ImPlot3DContext& gp = *GImPlot3D;
    ImPlot3DColormap idx = gp.ColormapData.GetIndex(name);
    IM_ASSERT_USER_ERROR(idx != -1, "The colormap name is invalid!");
    PushColormap(idx);
}

void PopColormap(int count) {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(count <= gp.ColormapModifiers.Size, "You can't pop more modifiers than have been pushed!");
    while (count > 0) {
        const ImPlot3DColormap& backup = gp.ColormapModifiers.back();
        gp.Style.Colormap = backup;
        gp.ColormapModifiers.pop_back();
        count--;
    }
}

ImU32 NextColormapColorU32() {
    ImPlot3DContext& gp = *GImPlot3D;
    IM_ASSERT_USER_ERROR(gp.CurrentItems != nullptr, "NextColormapColor() needs to be called between BeginPlot() and EndPlot()!");
    int idx = gp.CurrentItems->ColormapIdx % gp.ColormapData.GetKeyCount(gp.Style.Colormap);
    ImU32 col = gp.ColormapData.GetKeyColor(gp.Style.Colormap, idx);
    gp.CurrentItems->ColormapIdx++;
    return col;
}

ImVec4 NextColormapColor() {
    return ImGui::ColorConvertU32ToFloat4(NextColormapColorU32());
}

int GetColormapSize(ImPlot3DColormap cmap) {
    ImPlot3DContext& gp = *GImPlot3D;
    cmap = cmap == IMPLOT3D_AUTO ? gp.Style.Colormap : cmap;
    IM_ASSERT_USER_ERROR(cmap >= 0 && cmap < gp.ColormapData.Count, "Invalid colormap index!");
    return gp.ColormapData.GetKeyCount(cmap);
}

ImU32 GetColormapColorU32(int idx, ImPlot3DColormap cmap) {
    ImPlot3DContext& gp = *GImPlot3D;
    cmap = cmap == IMPLOT3D_AUTO ? gp.Style.Colormap : cmap;
    IM_ASSERT_USER_ERROR(cmap >= 0 && cmap < gp.ColormapData.Count, "Invalid colormap index!");
    idx = idx % gp.ColormapData.GetKeyCount(cmap);
    return gp.ColormapData.GetKeyColor(cmap, idx);
}

ImVec4 GetColormapColor(int idx, ImPlot3DColormap cmap) {
    return ImGui::ColorConvertU32ToFloat4(GetColormapColorU32(idx, cmap));
}

ImU32 SampleColormapU32(float t, ImPlot3DColormap cmap) {
    ImPlot3DContext& gp = *GImPlot3D;
    cmap = cmap == IMPLOT3D_AUTO ? gp.Style.Colormap : cmap;
    IM_ASSERT_USER_ERROR(cmap >= 0 && cmap < gp.ColormapData.Count, "Invalid colormap index!");
    return gp.ColormapData.LerpTable(cmap, t);
}

ImVec4 SampleColormap(float t, ImPlot3DColormap cmap) {
    return ImGui::ColorConvertU32ToFloat4(SampleColormapU32(t, cmap));
}

//-----------------------------------------------------------------------------
// [SECTION] Context Utils
//-----------------------------------------------------------------------------

#define IMPLOT3D_APPEND_CMAP(name, qual) ctx->ColormapData.Append(#name, name, sizeof(name) / sizeof(ImU32), qual)
#define IM_RGB(r, g, b) IM_COL32(r, g, b, 255)

void InitializeContext(ImPlot3DContext* ctx) {
    ResetContext(ctx);

    const ImU32 Deep[] = {4289753676, 4283598045, 4285048917, 4283584196, 4289950337, 4284512403, 4291005402, 4287401100, 4285839820, 4291671396};
    const ImU32 Dark[] = {4280031972, 4290281015, 4283084621, 4288892568, 4278222847, 4281597951, 4280833702, 4290740727, 4288256409};
    const ImU32 Pastel[] = {4289639675, 4293119411, 4291161036, 4293184478, 4289124862, 4291624959, 4290631909, 4293712637, 4294111986};
    const ImU32 Paired[] = {4293119554, 4290017311, 4287291314, 4281114675, 4288256763, 4280031971, 4285513725, 4278222847, 4292260554, 4288298346, 4288282623, 4280834481};
    const ImU32 Viridis[] = {4283695428, 4285867080, 4287054913, 4287455029, 4287526954, 4287402273, 4286883874, 4285579076, 4283552122, 4280737725, 4280674301};
    const ImU32 Plasma[] = {4287039501, 4288480321, 4289200234, 4288941455, 4287638193, 4286072780, 4284638433, 4283139314, 4281771772, 4280667900, 4280416752};
    const ImU32 Hot[] = {4278190144, 4278190208, 4278190271, 4278190335, 4278206719, 4278223103, 4278239231, 4278255615, 4283826175, 4289396735, 4294967295};
    const ImU32 Cool[] = {4294967040, 4294960666, 4294954035, 4294947661, 4294941030, 4294934656, 4294928025, 4294921651, 4294915020, 4294908646, 4294902015};
    const ImU32 Pink[] = {4278190154, 4282532475, 4284308894, 4285690554, 4286879686, 4287870160, 4288794330, 4289651940, 4291685869, 4293392118, 4294967295};
    const ImU32 Jet[] = {4289331200, 4294901760, 4294923520, 4294945280, 4294967040, 4289396565, 4283826090, 4278255615, 4278233855, 4278212095, 4278190335};
    const ImU32 Twilight[] = {IM_RGB(226, 217, 226), IM_RGB(166, 191, 202), IM_RGB(109, 144, 192), IM_RGB(95, 88, 176), IM_RGB(83, 30, 124), IM_RGB(47, 20, 54), IM_RGB(100, 25, 75), IM_RGB(159, 60, 80), IM_RGB(192, 117, 94), IM_RGB(208, 179, 158), IM_RGB(226, 217, 226)};
    const ImU32 RdBu[] = {IM_RGB(103, 0, 31), IM_RGB(178, 24, 43), IM_RGB(214, 96, 77), IM_RGB(244, 165, 130), IM_RGB(253, 219, 199), IM_RGB(247, 247, 247), IM_RGB(209, 229, 240), IM_RGB(146, 197, 222), IM_RGB(67, 147, 195), IM_RGB(33, 102, 172), IM_RGB(5, 48, 97)};
    const ImU32 BrBG[] = {IM_RGB(84, 48, 5), IM_RGB(140, 81, 10), IM_RGB(191, 129, 45), IM_RGB(223, 194, 125), IM_RGB(246, 232, 195), IM_RGB(245, 245, 245), IM_RGB(199, 234, 229), IM_RGB(128, 205, 193), IM_RGB(53, 151, 143), IM_RGB(1, 102, 94), IM_RGB(0, 60, 48)};
    const ImU32 PiYG[] = {IM_RGB(142, 1, 82), IM_RGB(197, 27, 125), IM_RGB(222, 119, 174), IM_RGB(241, 182, 218), IM_RGB(253, 224, 239), IM_RGB(247, 247, 247), IM_RGB(230, 245, 208), IM_RGB(184, 225, 134), IM_RGB(127, 188, 65), IM_RGB(77, 146, 33), IM_RGB(39, 100, 25)};
    const ImU32 Spectral[] = {IM_RGB(158, 1, 66), IM_RGB(213, 62, 79), IM_RGB(244, 109, 67), IM_RGB(253, 174, 97), IM_RGB(254, 224, 139), IM_RGB(255, 255, 191), IM_RGB(230, 245, 152), IM_RGB(171, 221, 164), IM_RGB(102, 194, 165), IM_RGB(50, 136, 189), IM_RGB(94, 79, 162)};
    const ImU32 Greys[] = {IM_COL32_WHITE, IM_COL32_BLACK};

    IMPLOT3D_APPEND_CMAP(Deep, true);
    IMPLOT3D_APPEND_CMAP(Dark, true);
    IMPLOT3D_APPEND_CMAP(Pastel, true);
    IMPLOT3D_APPEND_CMAP(Paired, true);
    IMPLOT3D_APPEND_CMAP(Viridis, false);
    IMPLOT3D_APPEND_CMAP(Plasma, false);
    IMPLOT3D_APPEND_CMAP(Hot, false);
    IMPLOT3D_APPEND_CMAP(Cool, false);
    IMPLOT3D_APPEND_CMAP(Pink, false);
    IMPLOT3D_APPEND_CMAP(Jet, false);
    IMPLOT3D_APPEND_CMAP(Twilight, false);
    IMPLOT3D_APPEND_CMAP(RdBu, false);
    IMPLOT3D_APPEND_CMAP(BrBG, false);
    IMPLOT3D_APPEND_CMAP(PiYG, false);
    IMPLOT3D_APPEND_CMAP(Spectral, false);
    IMPLOT3D_APPEND_CMAP(Greys, false);
}

void ResetContext(ImPlot3DContext* ctx) {
    ctx->Plots.Clear();
    ctx->CurrentPlot = nullptr;
    ctx->CurrentItems = nullptr;
    ctx->NextItemData.Reset();
    ctx->Style = ImPlot3DStyle();
}

//-----------------------------------------------------------------------------
// [SECTION] Style Utils
//-----------------------------------------------------------------------------

bool IsColorAuto(const ImVec4& col) {
    return col.w == -1.0f;
}

bool IsColorAuto(ImPlot3DCol idx) {
    return IsColorAuto(GImPlot3D->Style.Colors[idx]);
}

ImVec4 GetAutoColor(ImPlot3DCol idx) {
    switch (idx) {
        case ImPlot3DCol_Line: return IMPLOT3D_AUTO_COL;          // Plot dependent
        case ImPlot3DCol_Fill: return IMPLOT3D_AUTO_COL;          // Plot dependent
        case ImPlot3DCol_MarkerOutline: return IMPLOT3D_AUTO_COL; // Plot dependent
        case ImPlot3DCol_MarkerFill: return IMPLOT3D_AUTO_COL;    // Plot dependent
        case ImPlot3DCol_TitleText: return ImGui::GetStyleColorVec4(ImGuiCol_Text);
        case ImPlot3DCol_InlayText: return ImGui::GetStyleColorVec4(ImGuiCol_Text);
        case ImPlot3DCol_FrameBg: return ImGui::GetStyleColorVec4(ImGuiCol_FrameBg);
        case ImPlot3DCol_PlotBg: return ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
        case ImPlot3DCol_PlotBorder: return ImGui::GetStyleColorVec4(ImGuiCol_Border);
        case ImPlot3DCol_LegendBg: return ImGui::GetStyleColorVec4(ImGuiCol_PopupBg);
        case ImPlot3DCol_LegendBorder: return ImGui::GetStyleColorVec4(ImGuiCol_Border);
        case ImPlot3DCol_LegendText: return ImGui::GetStyleColorVec4(ImGuiCol_Text);
        case ImPlot3DCol_AxisText: return ImGui::GetStyleColorVec4(ImGuiCol_Text);
        case ImPlot3DCol_AxisGrid: return ImGui::GetStyleColorVec4(ImGuiCol_Text) * ImVec4(1, 1, 1, 0.25f);
        case ImPlot3DCol_AxisTick: return GetStyleColorVec4(ImPlot3DCol_AxisGrid);
        default: return IMPLOT3D_AUTO_COL;
    }
}

const char* GetStyleColorName(ImPlot3DCol idx) {
    static const char* color_names[ImPlot3DCol_COUNT] = {
        "Line",
        "Fill",
        "MarkerOutline",
        "MarkerFill",
        "TitleText",
        "InlayText",
        "FrameBg",
        "PlotBg",
        "PlotBorder",
        "LegendBg",
        "LegendBorder",
        "LegendText",
        "AxisText",
        "AxisGrid",
        "AxisTick",
    };
    return color_names[idx];
}

const ImPlot3DNextItemData& GetItemData() { return GImPlot3D->NextItemData; }

} // namespace ImPlot3D

//-----------------------------------------------------------------------------
// [SECTION] ImPlot3DPoint
//-----------------------------------------------------------------------------

ImPlot3DPoint ImPlot3DPoint::operator*(float rhs) const { return ImPlot3DPoint(x * rhs, y * rhs, z * rhs); }
ImPlot3DPoint ImPlot3DPoint::operator/(float rhs) const { return ImPlot3DPoint(x / rhs, y / rhs, z / rhs); }
ImPlot3DPoint ImPlot3DPoint::operator+(const ImPlot3DPoint& rhs) const { return ImPlot3DPoint(x + rhs.x, y + rhs.y, z + rhs.z); }
ImPlot3DPoint ImPlot3DPoint::operator-(const ImPlot3DPoint& rhs) const { return ImPlot3DPoint(x - rhs.x, y - rhs.y, z - rhs.z); }
ImPlot3DPoint ImPlot3DPoint::operator*(const ImPlot3DPoint& rhs) const { return ImPlot3DPoint(x * rhs.x, y * rhs.y, z * rhs.z); }
ImPlot3DPoint ImPlot3DPoint::operator/(const ImPlot3DPoint& rhs) const { return ImPlot3DPoint(x / rhs.x, y / rhs.y, z / rhs.z); }
ImPlot3DPoint ImPlot3DPoint::operator-() const { return ImPlot3DPoint(-x, -y, -z); }

ImPlot3DPoint& ImPlot3DPoint::operator*=(float rhs) {
    x *= rhs;
    y *= rhs;
    z *= rhs;
    return *this;
}
ImPlot3DPoint& ImPlot3DPoint::operator/=(float rhs) {
    x /= rhs;
    y /= rhs;
    z /= rhs;
    return *this;
}
ImPlot3DPoint& ImPlot3DPoint::operator+=(const ImPlot3DPoint& rhs) {
    x += rhs.x;
    y += rhs.y;
    z += rhs.z;
    return *this;
}
ImPlot3DPoint& ImPlot3DPoint::operator-=(const ImPlot3DPoint& rhs) {
    x -= rhs.x;
    y -= rhs.y;
    z -= rhs.z;
    return *this;
}
ImPlot3DPoint& ImPlot3DPoint::operator*=(const ImPlot3DPoint& rhs) {
    x *= rhs.x;
    y *= rhs.y;
    z *= rhs.z;
    return *this;
}
ImPlot3DPoint& ImPlot3DPoint::operator/=(const ImPlot3DPoint& rhs) {
    x /= rhs.x;
    y /= rhs.y;
    z /= rhs.z;
    return *this;
}

bool ImPlot3DPoint::operator==(const ImPlot3DPoint& rhs) const { return x == rhs.x && y == rhs.y && z == rhs.z; }
bool ImPlot3DPoint::operator!=(const ImPlot3DPoint& rhs) const { return !(*this == rhs); }

float ImPlot3DPoint::Dot(const ImPlot3DPoint& rhs) const { return x * rhs.x + y * rhs.y + z * rhs.z; }

ImPlot3DPoint ImPlot3DPoint::Cross(const ImPlot3DPoint& rhs) const {
    return ImPlot3DPoint(y * rhs.z - z * rhs.y, z * rhs.x - x * rhs.z, x * rhs.y - y * rhs.x);
}

float ImPlot3DPoint::Length() const { return ImSqrt(x * x + y * y + z * z); }

float ImPlot3DPoint::LengthSquared() const { return x * x + y * y + z * z; }

void ImPlot3DPoint::Normalize() {
    float l = Length();
    x /= l;
    y /= l;
    z /= l;
}

ImPlot3DPoint ImPlot3DPoint::Normalized() const {
    float l = Length();
    return ImPlot3DPoint(x / l, y / l, z / l);
}

ImPlot3DPoint operator*(float lhs, const ImPlot3DPoint& rhs) {
    return ImPlot3DPoint(lhs * rhs.x, lhs * rhs.y, lhs * rhs.z);
}

bool ImPlot3DPoint::IsNaN() const {
    return ImPlot3D::ImNan(x) || ImPlot3D::ImNan(y) || ImPlot3D::ImNan(z);
}

//-----------------------------------------------------------------------------
// [SECTION] ImPlot3DBox
//-----------------------------------------------------------------------------

void ImPlot3DBox::Expand(const ImPlot3DPoint& point) {
    Min.x = ImMin(Min.x, point.x);
    Min.y = ImMin(Min.y, point.y);
    Min.z = ImMin(Min.z, point.z);
    Max.x = ImMax(Max.x, point.x);
    Max.y = ImMax(Max.y, point.y);
    Max.z = ImMax(Max.z, point.z);
}

bool ImPlot3DBox::Contains(const ImPlot3DPoint& point) const {
    return (point.x >= Min.x && point.x <= Max.x) &&
           (point.y >= Min.y && point.y <= Max.y) &&
           (point.z >= Min.z && point.z <= Max.z);
}

bool ImPlot3DBox::ClipLineSegment(const ImPlot3DPoint& p0, const ImPlot3DPoint& p1, ImPlot3DPoint& p0_clipped, ImPlot3DPoint& p1_clipped) const {
    // Check if the line segment is completely inside the box
    if (Contains(p0) && Contains(p1)) {
        p0_clipped = p0;
        p1_clipped = p1;
        return true;
    }

    // Perform Liang-Barsky 3D clipping
    double t0 = 0.0;
    double t1 = 1.0;
    ImPlot3DPoint d = p1 - p0;

    // Define the clipping boundaries
    const double xmin = Min.x, xmax = Max.x;
    const double ymin = Min.y, ymax = Max.y;
    const double zmin = Min.z, zmax = Max.z;

    // Lambda function to update t0 and t1
    auto update = [&](double p, double q) -> bool {
        if (p == 0.0) {
            if (q < 0.0)
                return false; // Line is parallel and outside the boundary
            else
                return true; // Line is parallel and inside or coincident with boundary
        }
        double r = q / p;
        if (p < 0.0) {
            if (r > t1)
                return false; // Line is outside
            if (r > t0)
                t0 = r; // Move up t0
        } else {
            if (r < t0)
                return false; // Line is outside
            if (r < t1)
                t1 = r; // Move down t1
        }
        return true;
    };

    // Clip against each boundary
    if (!update(-d.x, p0.x - xmin))
        return false; // Left
    if (!update(d.x, xmax - p0.x))
        return false; // Right
    if (!update(-d.y, p0.y - ymin))
        return false; // Bottom
    if (!update(d.y, ymax - p0.y))
        return false; // Top
    if (!update(-d.z, p0.z - zmin))
        return false; // Near
    if (!update(d.z, zmax - p0.z))
        return false; // Far

    // Compute clipped points
    p0_clipped = p0 + d * t0;
    p1_clipped = p0 + d * t1;

    return true;
}

//-----------------------------------------------------------------------------
// [SECTION] ImPlot3DRange
//-----------------------------------------------------------------------------

void ImPlot3DRange::Expand(float value) {
    Min = ImMin(Min, value);
    Max = ImMax(Max, value);
}

bool ImPlot3DRange::Contains(float value) const {
    return value >= Min && value <= Max;
}

//-----------------------------------------------------------------------------
// [SECTION] ImPlot3DQuat
//-----------------------------------------------------------------------------

ImPlot3DQuat::ImPlot3DQuat(float _angle, const ImPlot3DPoint& _axis) {
    float half_angle = _angle * 0.5f;
    float s = std::sin(half_angle);
    x = s * _axis.x;
    y = s * _axis.y;
    z = s * _axis.z;
    w = std::cos(half_angle);
}

ImPlot3DQuat ImPlot3DQuat::FromTwoVectors(const ImPlot3DPoint& v0, const ImPlot3DPoint& v1) {
    ImPlot3DQuat q;

    // Compute the dot product and lengths of the vectors
    float dot = v0.Dot(v1);
    float length_v0 = v0.Length();
    float length_v1 = v1.Length();

    // Normalize the dot product
    float normalized_dot = dot / (length_v0 * length_v1);

    // Handle edge cases: if vectors are very close or identical
    const float epsilon = 1e-6f;
    if (std::fabs(normalized_dot - 1.0f) < epsilon) {
        // v0 and v1 are nearly identical; return an identity quaternion
        q.x = 0.0f;
        q.y = 0.0f;
        q.z = 0.0f;
        q.w = 1.0f;
        return q;
    }

    // Handle edge case: if vectors are opposite
    if (std::fabs(normalized_dot + 1.0f) < epsilon) {
        // v0 and v1 are opposite; choose an arbitrary orthogonal axis
        ImPlot3DPoint arbitrary_axis = std::fabs(v0.x) > std::fabs(v0.z) ? ImPlot3DPoint(-v0.y, v0.x, 0.0f)
                                                                         : ImPlot3DPoint(0.0f, -v0.z, v0.y);
        arbitrary_axis.Normalize();
        q.x = arbitrary_axis.x;
        q.y = arbitrary_axis.y;
        q.z = arbitrary_axis.z;
        q.w = 0.0f;
        return q;
    }

    // General case
    ImPlot3DPoint axis = v0.Cross(v1);
    axis.Normalize();
    float angle = std::acos(normalized_dot);
    float half_angle = angle * 0.5f;
    float s = std::sin(half_angle);
    q.x = s * axis.x;
    q.y = s * axis.y;
    q.z = s * axis.z;
    q.w = std::cos(half_angle);

    return q;
}

float ImPlot3DQuat::Length() const {
    return std::sqrt(x * x + y * y + z * z + w * w);
}

ImPlot3DQuat ImPlot3DQuat::Normalized() const {
    float l = Length();
    return ImPlot3DQuat(x / l, y / l, z / l, w / l);
}

ImPlot3DQuat ImPlot3DQuat::Conjugate() const {
    return ImPlot3DQuat(-x, -y, -z, w);
}

ImPlot3DQuat ImPlot3DQuat::Inverse() const {
    float l_squared = x * x + y * y + z * z + w * w;
    return ImPlot3DQuat(-x / l_squared, -y / l_squared, -z / l_squared, w / l_squared);
}

ImPlot3DQuat ImPlot3DQuat::operator*(const ImPlot3DQuat& rhs) const {
    return ImPlot3DQuat(
        w * rhs.x + x * rhs.w + y * rhs.z - z * rhs.y,
        w * rhs.y - x * rhs.z + y * rhs.w + z * rhs.x,
        w * rhs.z + x * rhs.y - y * rhs.x + z * rhs.w,
        w * rhs.w - x * rhs.x - y * rhs.y - z * rhs.z);
}

ImPlot3DQuat& ImPlot3DQuat::Normalize() {
    float l = Length();
    x /= l;
    y /= l;
    z /= l;
    w /= l;
    return *this;
}

ImPlot3DPoint ImPlot3DQuat::operator*(const ImPlot3DPoint& point) const {
    // Extract vector part of the quaternion
    ImPlot3DPoint qv(x, y, z);

    // Compute the cross products needed for rotation
    ImPlot3DPoint uv = qv.Cross(point); // uv = qv x point
    ImPlot3DPoint uuv = qv.Cross(uv);   // uuv = qv x uv

    // Compute the rotated vector
    return point + (uv * w * 2.0f) + (uuv * 2.0f);
}

bool ImPlot3DQuat::operator==(const ImPlot3DQuat& rhs) const {
    return x == rhs.x && y == rhs.y && z == rhs.z && w == rhs.w;
}

bool ImPlot3DQuat::operator!=(const ImPlot3DQuat& rhs) const {
    return !(*this == rhs);
}

ImPlot3DQuat ImPlot3DQuat::Slerp(const ImPlot3DQuat& q1, const ImPlot3DQuat& q2, float t) {
    // Clamp t to [0, 1]
    t = ImClamp(t, 0.0f, 1.0f);

    // Compute the dot product (cosine of the angle between quaternions)
    float dot = q1.x * q2.x + q1.y * q2.y + q1.z * q2.z + q1.w * q2.w;

    // If the dot product is negative, negate one quaternion to take the shorter path
    ImPlot3DQuat q2_ = q2;
    if (dot < 0.0f) {
        q2_ = ImPlot3DQuat(-q2.x, -q2.y, -q2.z, -q2.w);
        dot = -dot;
    }

    // If the quaternions are very close, use linear interpolation to avoid numerical instability
    if (dot > 0.9995f) {
        return ImPlot3DQuat(
                   q1.x + t * (q2_.x - q1.x),
                   q1.y + t * (q2_.y - q1.y),
                   q1.z + t * (q2_.z - q1.z),
                   q1.w + t * (q2_.w - q1.w))
            .Normalized();
    }

    // Compute the angle and the interpolation factors
    float theta_0 = std::acos(dot);        // Angle between input quaternions
    float theta = theta_0 * t;             // Interpolated angle
    float sin_theta = std::sin(theta);     // Sine of interpolated angle
    float sin_theta_0 = std::sin(theta_0); // Sine of original angle

    float s1 = std::cos(theta) - dot * sin_theta / sin_theta_0;
    float s2 = sin_theta / sin_theta_0;

    // Interpolate and return the result
    return ImPlot3DQuat(
        s1 * q1.x + s2 * q2_.x,
        s1 * q1.y + s2 * q2_.y,
        s1 * q1.z + s2 * q2_.z,
        s1 * q1.w + s2 * q2_.w);
}

float ImPlot3DQuat::Dot(const ImPlot3DQuat& rhs) const {
    return x * rhs.x + y * rhs.y + z * rhs.z + w * rhs.w;
}

//-----------------------------------------------------------------------------
// [SECTION] ImDrawList3D
//-----------------------------------------------------------------------------

void ImDrawList3D::PrimReserve(int idx_count, int vtx_count) {
    IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0 && idx_count % 3 == 0);

    int vtx_buffer_old_size = VtxBuffer.Size;
    VtxBuffer.resize(vtx_buffer_old_size + vtx_count);
    _VtxWritePtr = VtxBuffer.Data + vtx_buffer_old_size;

    int idx_buffer_old_size = IdxBuffer.Size;
    IdxBuffer.resize(idx_buffer_old_size + idx_count);
    _IdxWritePtr = IdxBuffer.Data + idx_buffer_old_size;

    int z_buffer_old_size = ZBuffer.Size;
    ZBuffer.resize(z_buffer_old_size + idx_count / 3);
    _ZWritePtr = ZBuffer.Data + z_buffer_old_size;
}

void ImDrawList3D::PrimUnreserve(int idx_count, int vtx_count) {
    IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0 && idx_count % 3 == 0);

    VtxBuffer.shrink(VtxBuffer.Size - vtx_count);
    IdxBuffer.shrink(IdxBuffer.Size - idx_count);
    ZBuffer.shrink(ZBuffer.Size - idx_count / 3);
}

void ImDrawList3D::SortedMoveToImGuiDrawList() {
    ImDrawList& draw_list = *ImGui::GetWindowDrawList();

    const int tri_count = ZBuffer.Size;
    if (tri_count == 0) {
        // No triangles, just clear and return
        VtxBuffer.clear();
        IdxBuffer.clear();
        ZBuffer.clear();
        _VtxCurrentIdx = 0;
        _VtxWritePtr = VtxBuffer.Data;
        _IdxWritePtr = IdxBuffer.Data;
        _ZWritePtr = ZBuffer.Data;
        return;
    }

    // Build an array of (z, tri_idx)
    struct TriRef {
        float z;
        int tri_idx;
    };
    TriRef* tris = (TriRef*)IM_ALLOC(sizeof(TriRef) * tri_count);
    for (int i = 0; i < tri_count; i++) {
        tris[i].z = ZBuffer[i];
        tris[i].tri_idx = i;
    }

    // Sort by z (distance from viewer)
    ImQsort(tris, (size_t)tri_count, sizeof(TriRef),
            [](const void* a, const void* b) {
                float za = ((const TriRef*)a)->z;
                float zb = ((const TriRef*)b)->z;
                return (za < zb) ? -1 : (za > zb) ? 1
                                                  : 0;
            });

    // Reserve space in the ImGui draw list
    draw_list.PrimReserve(IdxBuffer.Size, VtxBuffer.Size);

    // Copy vertices (no reordering needed)
    memcpy(draw_list._VtxWritePtr, VtxBuffer.Data, VtxBuffer.Size * sizeof(ImDrawVert));
    unsigned int idx_offset = draw_list._VtxCurrentIdx;
    draw_list._VtxWritePtr += VtxBuffer.Size;
    draw_list._VtxCurrentIdx += (unsigned int)VtxBuffer.Size;

    // Maximum index allowed to not overflow ImDrawIdx
    unsigned int max_index_allowed = MaxIdx() - idx_offset;

    // Copy indices with triangle sorting based on distance from viewer
    ImDrawIdx* idx_out = draw_list._IdxWritePtr;
    ImDrawIdx* idx_in = IdxBuffer.Data;
    int triangles_added = 0;
    for (int i = 0; i < tri_count; i++) {
        int tri_i = tris[i].tri_idx;
        int base_idx = tri_i * 3;
        unsigned int i0 = (unsigned int)idx_in[base_idx + 0];
        unsigned int i1 = (unsigned int)idx_in[base_idx + 1];
        unsigned int i2 = (unsigned int)idx_in[base_idx + 2];

        // Check if after adding offset any of these indices exceed max_index_allowed
        if (i0 > max_index_allowed || i1 > max_index_allowed || i2 > max_index_allowed)
            break;

        idx_out[0] = (ImDrawIdx)(i0 + idx_offset);
        idx_out[1] = (ImDrawIdx)(i1 + idx_offset);
        idx_out[2] = (ImDrawIdx)(i2 + idx_offset);

        idx_out += 3;
        triangles_added++;
    }
    draw_list._IdxWritePtr = idx_out;

    // Clear local buffers since we've moved them
    VtxBuffer.clear();
    IdxBuffer.clear();
    ZBuffer.clear();
    _VtxCurrentIdx = 0;
    _VtxWritePtr = VtxBuffer.Data;
    _IdxWritePtr = IdxBuffer.Data;
    _ZWritePtr = ZBuffer.Data;

    IM_FREE(tris);
}

//-----------------------------------------------------------------------------
// [SECTION] ImPlot3DAxis
//-----------------------------------------------------------------------------

bool ImPlot3DAxis::HasLabel() const { return !Label.empty() && !ImPlot3D::ImHasFlag(Flags, ImPlot3DAxisFlags_NoLabel); }
bool ImPlot3DAxis::HasGridLines() const { return !ImPlot3D::ImHasFlag(Flags, ImPlot3DAxisFlags_NoGridLines); }
bool ImPlot3DAxis::HasTickLabels() const { return !ImPlot3D::ImHasFlag(Flags, ImPlot3DAxisFlags_NoTickLabels); }
bool ImPlot3DAxis::HasTickMarks() const { return !ImPlot3D::ImHasFlag(Flags, ImPlot3DAxisFlags_NoTickMarks); }
bool ImPlot3DAxis::IsAutoFitting() const { return ImPlot3D::ImHasFlag(Flags, ImPlot3DAxisFlags_AutoFit); }

void ImPlot3DAxis::ExtendFit(float value) {
    FitExtents.Min = ImMin(FitExtents.Min, value);
    FitExtents.Max = ImMax(FitExtents.Max, value);
}

void ImPlot3DAxis::ApplyFit() {
    if (!IsLockedMin() && !ImPlot3D::ImNanOrInf(FitExtents.Min))
        Range.Min = FitExtents.Min;
    if (!IsLockedMax() && !ImPlot3D::ImNanOrInf(FitExtents.Max))
        Range.Max = FitExtents.Max;
    if (ImPlot3D::ImAlmostEqual(Range.Min, Range.Max)) {
        Range.Max += 0.5;
        Range.Min -= 0.5;
    }
    FitExtents.Min = HUGE_VAL;
    FitExtents.Max = -HUGE_VAL;
}

float ImPlot3DAxis::PlotToNDC(float value) const {
    return (value - Range.Min) / (Range.Max - Range.Min) - 0.5f;
}

float ImPlot3DAxis::NDCToPlot(float value) const {
    return Range.Min + (value + 0.5f) * (Range.Max - Range.Min);
}

//-----------------------------------------------------------------------------
// [SECTION] ImPlot3DPlot
//-----------------------------------------------------------------------------

void ImPlot3DPlot::ExtendFit(const ImPlot3DPoint& point) {
    for (int i = 0; i < 3; i++) {
        if (!ImPlot3D::ImNanOrInf(point[i]) && Axes[i].FitThisFrame)
            Axes[i].ExtendFit(point[i]);
    }
}

ImPlot3DPoint ImPlot3DPlot::RangeMin() const {
    return ImPlot3DPoint(Axes[0].Range.Min, Axes[1].Range.Min, Axes[2].Range.Min);
}

ImPlot3DPoint ImPlot3DPlot::RangeMax() const {
    return ImPlot3DPoint(Axes[0].Range.Max, Axes[1].Range.Max, Axes[2].Range.Max);
}

ImPlot3DPoint ImPlot3DPlot::RangeCenter() const {
    return ImPlot3DPoint(
        (Axes[0].Range.Min + Axes[0].Range.Max) * 0.5f,
        (Axes[1].Range.Min + Axes[1].Range.Max) * 0.5f,
        (Axes[2].Range.Min + Axes[2].Range.Max) * 0.5f);
}

void ImPlot3DPlot::SetRange(const ImPlot3DPoint& min, const ImPlot3DPoint& max) {
    Axes[0].SetRange(min.x, max.x);
    Axes[1].SetRange(min.y, max.y);
    Axes[2].SetRange(min.z, max.z);
}

//-----------------------------------------------------------------------------
// [SECTION] ImPlot3DStyle
//-----------------------------------------------------------------------------

ImPlot3DStyle::ImPlot3DStyle() {
    // Item style
    LineWeight = 1.0f;
    Marker = ImPlot3DMarker_None;
    MarkerSize = 4.0f;
    MarkerWeight = 1.0f;
    FillAlpha = 1.0f;
    // Plot style
    PlotDefaultSize = ImVec2(400, 400);
    PlotMinSize = ImVec2(200, 200);
    PlotPadding = ImVec2(10, 10);
    LabelPadding = ImVec2(5, 5);
    // Legend style
    LegendPadding = ImVec2(10, 10);
    LegendInnerPadding = ImVec2(5, 5);
    LegendSpacing = ImVec2(5, 0);
    // Colors
    ImPlot3D::StyleColorsAuto(this);
    Colormap = ImPlot3DColormap_Deep;
};

#endif // #ifndef IMGUI_DISABLE
