Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 55 additions & 14 deletions implot3d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
// [SECTION] Setup Utils
// [SECTION] Miscellaneous
// [SECTION] Styles
// [SECTION] Input Mapping
// [SECTION] Colormaps
// [SECTION] Context Utils
// [SECTION] Style Utils
Expand All @@ -39,6 +40,7 @@
// [SECTION] ImPlot3DAxis
// [SECTION] ImPlot3DPlot
// [SECTION] ImPlot3DStyle
// [SECTION] ImPlot3DInputMap
// [SECTION] Metrics

//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -1934,6 +1936,7 @@ ImPlot3DRay NDCRayToPlotRay(const ImPlot3DRay& ray) {
static const float MOUSE_CURSOR_DRAG_THRESHOLD = 5.0f;

void HandleInput(ImPlot3DPlot& plot) {
ImPlot3DContext& gp = *GImPlot3D;
ImGuiIO& IO = ImGui::GetIO();

// clang-format off
Expand All @@ -1950,7 +1953,7 @@ void HandleInput(ImPlot3DPlot& plot) {
#endif

// State
const ImVec2 rot_drag = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right);
const ImVec2 rot_drag = ImPlot3D::ImHasFlag(IO.KeyMods, gp.InputMap.RotateMod) ? ImGui::GetMouseDragDelta(gp.InputMap.Rotate) : ImVec2(0, 0);
const bool rotating = ImLengthSqr(rot_drag) > MOUSE_CURSOR_DRAG_THRESHOLD;

// Check if any axis/plane is hovered
Expand All @@ -1974,7 +1977,8 @@ void HandleInput(ImPlot3DPlot& plot) {
}

// 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)) {
if (!(ImPlot3D::ImHasFlag(IO.KeyMods, gp.InputMap.PanMod) && ImGui::IsMouseDown(gp.InputMap.Pan)) &&
!(ImPlot3D::ImHasFlag(IO.KeyMods, gp.InputMap.ZoomMod) && ImGui::IsMouseDown(ImGuiMouseButton_Middle))) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Should gp.InputMap.ZoomMod also be applied here and later on for the ImGui::IsMouseDown(ImGuiMouseButton_Middle) and ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Middle) checks?
  2. Should we add ImGuiMouseButton_Middle as a option for Zoom together with ZoomMod or should we just keep it as it since it is previous behaviour? Zoom might not make a lot of sense though since it does not use it for zoom ad is for fit(already have option for Fit though), maybe call it something else?

for (int i = 0; i < 3; i++)
plot.Axes[i].Held = false;
}
Expand Down Expand Up @@ -2014,7 +2018,8 @@ void HandleInput(ImPlot3DPlot& plot) {
ImPlot3DPoint mouse_pos_plot = PixelsToPlotPlane(mouse_pos, mouse_plane, false);

// Handle translation/zoom fit with double click
if (plot_clicked && (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) || ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Middle))) {
if (plot_clicked && (ImGui::IsMouseDoubleClicked(gp.InputMap.Fit) ||
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add support for FitMod? This is not in ImPlot and I think it might be because it is only triggered on double click which does not happen very often.

(ImPlot3D::ImHasFlag(IO.KeyMods, gp.InputMap.ZoomMod) && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Middle)))) {
plot.FitThisFrame = true;
for (int i = 0; i < 3; i++)
plot.Axes[i].FitThisFrame = plot.Axes[i].Hovered;
Expand All @@ -2027,8 +2032,12 @@ void HandleInput(ImPlot3DPlot& plot) {
plot.Axes[i].FitThisFrame = true;
}

// cancel due to DND activity
if (GImGui->DragDropActive || ImPlot3D::ImHasFlag(IO.KeyMods, gp.InputMap.OverrideMod))
Copy link
Contributor Author

@ACvanWyk ACvanWyk Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure where to put this? I put it here after the FitThisFrame has been set for the plot which means that the auto fit can be applied to the plot. ImPlot has similar behavior. Should the data be fitted to the plot if OverrideMod is valid?

return;

// Handle translation with right mouse button
if (plot.Held && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
if (plot.Held && ImPlot3D::ImHasFlag(IO.KeyMods, gp.InputMap.PanMod) && ImGui::IsMouseDown(gp.InputMap.Pan)) {
ImVec2 delta(IO.MouseDelta.x, IO.MouseDelta.y);

if (plot.Axes[0].Hovered && plot.Axes[1].Hovered && plot.Axes[2].Hovered) {
Expand All @@ -2046,7 +2055,7 @@ void HandleInput(ImPlot3DPlot& plot) {

// Adjust delta for inverted axes
for (int i = 0; i < 3; i++) {
if (ImHasFlag(plot.Axes[i].Flags, ImPlot3DAxisFlags_Invert))
if (ImPlot3D::ImHasFlag(plot.Axes[i].Flags, ImPlot3DAxisFlags_Invert))
delta_plot[i] *= -1;
}

Expand Down Expand Up @@ -2094,13 +2103,14 @@ void HandleInput(ImPlot3DPlot& plot) {
}

// Handle context click with right mouse button
if (plot.Held && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !ImPlot3D::ImHasFlag(plot.Flags, ImPlot3DFlags_NoMenus))
if (plot.Held && ImGui::IsMouseClicked(gp.InputMap.Menu) && !ImPlot3D::ImHasFlag(plot.Flags, ImPlot3DFlags_NoMenus))
plot.ContextClick = true;
if (rotating || ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right))
if (rotating || (ImPlot3D::ImHasFlag(IO.KeyMods, gp.InputMap.RotateMod) && ImGui::IsMouseDoubleClicked(gp.InputMap.Rotate)))
plot.ContextClick = false;

// Handle reset rotation with left mouse double click
if (plot.Held && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Right) && !plot.IsRotationLocked()) {
if (plot.Held && ImPlot3D::ImHasFlag(IO.KeyMods, gp.InputMap.RotateMod) && ImGui::IsMouseDoubleClicked(gp.InputMap.ResetRotate) &&
!plot.IsRotationLocked()) {
plot.RotationAnimationEnd = plot.Rotation;

// Calculate rotation to align the z-axis with the camera direction
Expand Down Expand Up @@ -2161,7 +2171,7 @@ void HandleInput(ImPlot3DPlot& plot) {
}

// Handle rotation with left mouse dragging
if (plot.Held && ImGui::IsMouseDown(ImGuiMouseButton_Right) && !plot.IsRotationLocked()) {
if (plot.Held && ImPlot3D::ImHasFlag(IO.KeyMods, gp.InputMap.RotateMod) && ImGui::IsMouseDown(gp.InputMap.Rotate) && !plot.IsRotationLocked()) {
ImVec2 delta(IO.MouseDelta.x, IO.MouseDelta.y);

// Map delta to rotation angles (in radians)
Expand All @@ -2178,10 +2188,11 @@ void HandleInput(ImPlot3DPlot& plot) {
}

// Handle zoom with mouse wheel
if (plot.Hovered) {
if (plot.Hovered && ImPlot3D::ImHasFlag(IO.KeyMods, gp.InputMap.ZoomMod)) {
ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, plot.ID);
if (ImGui::IsMouseDown(ImGuiMouseButton_Middle) || IO.MouseWheel != 0.0f) {
float delta = ImGui::IsMouseDown(ImGuiMouseButton_Middle) ? (-0.01f * IO.MouseDelta.y) : (-0.1f * IO.MouseWheel);
const bool middle_mouse_button_down = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
if (middle_mouse_button_down || IO.MouseWheel != 0.0f) {
float delta = middle_mouse_button_down ? (-0.01f * IO.MouseDelta.y) : (-gp.InputMap.ZoomRate * IO.MouseWheel);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When middle_mouse_button_down is true should the delta be a function of the ZoomRate? Maybe (-gp.InputMap.ZoomRate * IO.MouseDelta.y / 10.0f) instead of (-0.01f * IO.MouseDelta.y)?
Can always create a new variable specifically just for zoom rate with button press(MouseDelta.y) which is separate from the ZoomRate(MouseWheel) ?

float zoom = 1.0f + delta;
for (int i = 0; i < 3; i++) {
ImPlot3DAxis& axis = plot.Axes[i];
Expand Down Expand Up @@ -2226,8 +2237,8 @@ void HandleInput(ImPlot3DPlot& plot) {
}

// 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)) {
bool not_double_click = (float)(ImGui::GetTime() - IO.MouseClickedTime[gp.InputMap.Menu]) > IO.MouseDoubleClickTime;
if (plot.Hovered && plot.ContextClick && not_double_click && !ImGui::IsMouseDown(gp.InputMap.Menu)) {
plot.ContextClick = false;
plot.OpenContextThisFrame = true;
}
Expand Down Expand Up @@ -2546,6 +2557,30 @@ ImVec4 GetStyleColorVec4(ImPlot3DCol idx) { return IsColorAuto(idx) ? GetAutoCol

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

//-----------------------------------------------------------------------------
// [Section] Input Mapping
//-----------------------------------------------------------------------------

ImPlot3DInputMap& GetInputMap() {
IMPLOT3D_CHECK_CTX();
ImPlot3DContext& gp = *GImPlot3D;
return gp.InputMap;
}

void MapInputDefault(ImPlot3DInputMap* dst) {
ImPlot3DInputMap& map = dst ? *dst : GetInputMap();
map.Pan = ImGuiMouseButton_Left;
map.PanMod = ImGuiMod_None;
map.Fit = ImGuiMouseButton_Left;
map.ResetRotate = ImGuiMouseButton_Right;
map.Rotate = ImGuiMouseButton_Right;
map.RotateMod = ImGuiMod_None;
map.Menu = ImGuiMouseButton_Right;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add MenuMod and FitMod as well?
Add small overhead for some more control.

map.OverrideMod = ImGuiMod_Ctrl;
map.ZoomMod = ImGuiMod_None;
map.ZoomRate = 0.1f;
}

//------------------------------------------------------------------------------
// [SECTION] Colormaps
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -3412,6 +3447,12 @@ ImPlot3DStyle::ImPlot3DStyle() {
Colormap = ImPlot3DColormap_Deep;
};

//-----------------------------------------------------------------------------
// [SECTION] ImPlot3DInputMap
//-----------------------------------------------------------------------------

ImPlot3DInputMap::ImPlot3DInputMap() { ImPlot3D::MapInputDefault(this); }

//-----------------------------------------------------------------------------
// [SECTION] Metrics
//-----------------------------------------------------------------------------
Expand Down
30 changes: 30 additions & 0 deletions implot3d.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
// Forward declarations
struct ImPlot3DContext;
struct ImPlot3DStyle;
struct ImPlot3DInputMap;
struct ImPlot3DPoint;
struct ImPlot3DRay;
struct ImPlot3DPlane;
Expand Down Expand Up @@ -544,6 +545,16 @@ IMPLOT3D_API void SetNextMarkerStyle(ImPlot3DMarker marker = IMPLOT3D_AUTO, floa
IMPLOT3D_API ImVec4 GetStyleColorVec4(ImPlot3DCol idx);
IMPLOT3D_API ImU32 GetStyleColorU32(ImPlot3DCol idx);

//-----------------------------------------------------------------------------
// [SECTION] Input Mapping
//-----------------------------------------------------------------------------

// Provides access to input mapping structure for permanant modifications to controls for pan, select, etc.
IMPLOT3D_API ImPlot3DInputMap& GetInputMap();

// Default input mapping: pan = LMB drag, box select = RMB drag, fit = LMB double click, context menu = RMB click, zoom = scroll.
IMPLOT3D_API void MapInputDefault(ImPlot3DInputMap* dst = nullptr);

//-----------------------------------------------------------------------------
// [SECTION] Colormaps
//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -829,6 +840,25 @@ struct ImPlot3DStyle {
ImPlot3DStyle(const ImPlot3DStyle& other) = default;
};

//-----------------------------------------------------------------------------
// [SECTION] ImPlot3DInputMap
//-----------------------------------------------------------------------------

// Input mapping structure. Default values listed. See also MapInputDefault, MapInputReverse.
struct ImPlot3DInputMap {
ImGuiMouseButton Pan; // LMB enables panning when held,
int PanMod; // none optional modifier that must be held for panning/fitting
ImGuiMouseButton Fit; // LMB initiates fit when double clicked
ImGuiMouseButton ResetRotate; // RMB initiates reset of the rotate when double clicked. When double clicked over the axis change to 2D view of the axis
Copy link
Contributor Author

@ACvanWyk ACvanWyk Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure of the name ResetRotate here. Open to suggestions for a better name?

ImGuiMouseButton Rotate; // RMB rotate the plot
int RotateMod; // none optional modifier that must be held when rotating
ImGuiMouseButton Menu; // RMB opens context menus (if enabled) when clicked
int OverrideMod; // Ctrl when held, all input is ignored; used to enable axis/plots as DND sources
int ZoomMod; // none optional modifier that must be held for scroll wheel zooming
float ZoomRate; // 0.1f zoom rate for scroll (e.g. 0.1f = 10% plot range every scroll click); make negative to invert
IMPLOT3D_API ImPlot3DInputMap();
};

//-----------------------------------------------------------------------------
// [SECTION] Meshes
//-----------------------------------------------------------------------------
Expand Down
52 changes: 52 additions & 0 deletions implot3d_demo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,57 @@ void DemoCustomRendering() {
}
}

void ButtonSelector(const char* label, ImGuiMouseButton* b) {
ImGui::PushID(label);
if (ImGui::RadioButton("LMB", *b == ImGuiMouseButton_Left))
*b = ImGuiMouseButton_Left;
ImGui::SameLine();
if (ImGui::RadioButton("RMB", *b == ImGuiMouseButton_Right))
*b = ImGuiMouseButton_Right;
ImGui::SameLine();
if (ImGui::RadioButton("MMB", *b == ImGuiMouseButton_Middle))
*b = ImGuiMouseButton_Middle;
ImGui::PopID();
}

void ModSelector(const char* label, int* k) {
ImGui::PushID(label);
ImGui::CheckboxFlags("Ctrl", (unsigned int*)k, ImGuiMod_Ctrl);
ImGui::SameLine();
ImGui::CheckboxFlags("Shift", (unsigned int*)k, ImGuiMod_Shift);
ImGui::SameLine();
ImGui::CheckboxFlags("Alt", (unsigned int*)k, ImGuiMod_Alt);
ImGui::SameLine();
ImGui::CheckboxFlags("Super", (unsigned int*)k, ImGuiMod_Super);
ImGui::PopID();
}

void InputMapping(const char* label, ImGuiMouseButton* b, int* k) {
ImGui::LabelText("##", "%s", label);
if (b != nullptr) {
ImGui::SameLine(130);
ButtonSelector(label, b);
}
if (k != nullptr) {
ImGui::SameLine(300);
ModSelector(label, k);
}
}

void ShowInputMapping() {
ImPlot3DInputMap& map = ImPlot3D::GetInputMap();
if (ImGui::Button("Reset"))
ImPlot3D::MapInputDefault(&map);
InputMapping("Pan", &map.Pan, &map.PanMod);
InputMapping("Fit", &map.Fit, nullptr);
InputMapping("Reset Rotate", &map.ResetRotate, nullptr);
InputMapping("Rotate", &map.Rotate, &map.RotateMod);
InputMapping("Menu", &map.Menu, nullptr);
InputMapping("OverrideMod", nullptr, &map.OverrideMod);
InputMapping("ZoomMod", nullptr, &map.ZoomMod);
ImGui::SliderFloat("ZoomRate", &map.ZoomRate, -1, 1);
}

//-----------------------------------------------------------------------------
// [SECTION] Demo Window
//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -890,6 +941,7 @@ void ShowAllDemos() {
if (ImGui::BeginTabItem("Custom")) {
DemoHeader("Custom Styles", DemoCustomStyles);
DemoHeader("Custom Rendering", DemoCustomRendering);
DemoHeader("Show Input Mapping", ShowInputMapping);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure where to put this? I put it here for now and this will affect all the plots. Open to moving/changing this

ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Help")) {
Expand Down
1 change: 1 addition & 0 deletions implot3d_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@ struct ImPlot3DContext {
ImPlot3DItem* CurrentItem;
ImPlot3DNextItemData NextItemData;
ImPlot3DStyle Style;
ImPlot3DInputMap InputMap;
ImVector<ImGuiColorMod> ColorModifiers;
ImVector<ImGuiStyleMod> StyleModifiers;
ImVector<ImPlot3DColormap> ColormapModifiers;
Expand Down
Loading