// https://github.com/CedricGuillemet/ImGuizmo // v 1.89 WIP // // The MIT License(MIT) // // Copyright(c) 2021 Cedric Guillemet // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files(the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions : // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #include "ImCurveEdit.h" #include "imgui.h" #include "imgui_internal.h" #include #include #include #if defined(_MSC_VER) || defined(__MINGW32__) #include #endif #if !defined(_MSC_VER) && !defined(__MINGW64_VERSION_MAJOR) #define _malloca(x) alloca(x) #define _freea(x) #endif namespace ImCurveEdit { #ifndef IMGUI_DEFINE_MATH_OPERATORS static ImVec2 operator+(const ImVec2& a, const ImVec2& b) { return ImVec2(a.x + b.x, a.y + b.y); } static ImVec2 operator-(const ImVec2& a, const ImVec2& b) { return ImVec2(a.x - b.x, a.y - b.y); } static ImVec2 operator*(const ImVec2& a, const ImVec2& b) { return ImVec2(a.x * b.x, a.y * b.y); } static ImVec2 operator/(const ImVec2& a, const ImVec2& b) { return ImVec2(a.x / b.x, a.y / b.y); } static ImVec2 operator*(const ImVec2& a, const float b) { return ImVec2(a.x * b, a.y * b); } #endif static float smoothstep(float edge0, float edge1, float x) { x = ImClamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); return x * x * (3 - 2 * x); } static float distance(float x, float y, float x1, float y1, float x2, float y2) { float A = x - x1; float B = y - y1; float C = x2 - x1; float D = y2 - y1; float dot = A * C + B * D; float len_sq = C * C + D * D; float param = -1.f; if (len_sq > FLT_EPSILON) param = dot / len_sq; float xx, yy; if (param < 0.f) { xx = x1; yy = y1; } else if (param > 1.f) { xx = x2; yy = y2; } else { xx = x1 + param * C; yy = y1 + param * D; } float dx = x - xx; float dy = y - yy; return sqrtf(dx * dx + dy * dy); } static int DrawPoint(ImDrawList* draw_list, ImVec2 pos, const ImVec2 size, const ImVec2 offset, bool edited) { int ret = 0; ImGuiIO& io = ImGui::GetIO(); static const ImVec2 localOffsets[4] = { ImVec2(1,0), ImVec2(0,1), ImVec2(-1,0), ImVec2(0,-1) }; ImVec2 offsets[4]; for (int i = 0; i < 4; i++) { offsets[i] = pos * size + localOffsets[i] * 4.5f + offset; } const ImVec2 center = pos * size + offset; const ImRect anchor(center - ImVec2(5, 5), center + ImVec2(5, 5)); draw_list->AddConvexPolyFilled(offsets, 4, 0xFF000000); if (anchor.Contains(io.MousePos)) { ret = 1; if (io.MouseDown[0]) ret = 2; } if (edited) draw_list->AddPolyline(offsets, 4, 0xFFFFFFFF, true, 3.0f); else if (ret) draw_list->AddPolyline(offsets, 4, 0xFF80B0FF, true, 2.0f); else draw_list->AddPolyline(offsets, 4, 0xFF0080FF, true, 2.0f); return ret; } int Edit(Delegate& delegate, const ImVec2& size, unsigned int id, const ImRect* clippingRect, ImVector* selectedPoints) { static bool selectingQuad = false; static ImVec2 quadSelection; static int overCurve = -1; static int movingCurve = -1; static bool scrollingV = false; static std::set selection; static bool overSelectedPoint = false; int ret = 0; ImGuiIO& io = ImGui::GetIO(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); ImGui::PushStyleColor(ImGuiCol_Border, 0); ImGui::BeginChildFrame(id, size); delegate.focused = ImGui::IsWindowFocused(); ImDrawList* draw_list = ImGui::GetWindowDrawList(); if (clippingRect) draw_list->PushClipRect(clippingRect->Min, clippingRect->Max, true); const ImVec2 offset = ImGui::GetCursorScreenPos() + ImVec2(0.f, size.y); const ImVec2 ssize(size.x, -size.y); const ImRect container(offset + ImVec2(0.f, ssize.y), offset + ImVec2(ssize.x, 0.f)); ImVec2& min = delegate.GetMin(); ImVec2& max = delegate.GetMax(); // handle zoom and VScroll if (container.Contains(io.MousePos)) { if (fabsf(io.MouseWheel) > FLT_EPSILON) { const float r = (io.MousePos.y - offset.y) / ssize.y; float ratioY = ImLerp(min.y, max.y, r); auto scaleValue = [&](float v) { v -= ratioY; v *= (1.f - io.MouseWheel * 0.05f); v += ratioY; return v; }; min.y = scaleValue(min.y); max.y = scaleValue(max.y); } if (!scrollingV && ImGui::IsMouseDown(2)) { scrollingV = true; } } ImVec2 range = max - min + ImVec2(1.f, 0.f); // +1 because of inclusive last frame const ImVec2 viewSize(size.x, -size.y); const ImVec2 sizeOfPixel = ImVec2(1.f, 1.f) / viewSize; const size_t curveCount = delegate.GetCurveCount(); if (scrollingV) { float deltaH = io.MouseDelta.y * range.y * sizeOfPixel.y; min.y -= deltaH; max.y -= deltaH; if (!ImGui::IsMouseDown(2)) scrollingV = false; } draw_list->AddRectFilled(offset, offset + ssize, delegate.GetBackgroundColor()); auto pointToRange = [&](ImVec2 pt) { return (pt - min) / range; }; auto rangeToPoint = [&](ImVec2 pt) { return (pt * range) + min; }; draw_list->AddLine(ImVec2(-1.f, -min.y / range.y) * viewSize + offset, ImVec2(1.f, -min.y / range.y) * viewSize + offset, 0xFF000000, 1.5f); bool overCurveOrPoint = false; int localOverCurve = -1; // make sure highlighted curve is rendered last int* curvesIndex = (int*)_malloca(sizeof(int) * curveCount); for (size_t c = 0; c < curveCount; c++) curvesIndex[c] = int(c); int highLightedCurveIndex = -1; if (overCurve != -1 && curveCount) { ImSwap(curvesIndex[overCurve], curvesIndex[curveCount - 1]); highLightedCurveIndex = overCurve; } for (size_t cur = 0; cur < curveCount; cur++) { int c = curvesIndex[cur]; if (!delegate.IsVisible(c)) continue; const size_t ptCount = delegate.GetPointCount(c); if (ptCount < 1) continue; CurveType curveType = delegate.GetCurveType(c); if (curveType == CurveNone) continue; const ImVec2* pts = delegate.GetPoints(c); uint32_t curveColor = delegate.GetCurveColor(c); if ((c == highLightedCurveIndex && selection.empty() && !selectingQuad) || movingCurve == c) curveColor = 0xFFFFFFFF; for (size_t p = 0; p < ptCount - 1; p++) { const ImVec2 p1 = pointToRange(pts[p]); const ImVec2 p2 = pointToRange(pts[p + 1]); if (curveType == CurveSmooth || curveType == CurveLinear) { size_t subStepCount = (curveType == CurveSmooth) ? 20 : 2; float step = 1.f / float(subStepCount - 1); for (size_t substep = 0; substep < subStepCount - 1; substep++) { float t = float(substep) * step; const ImVec2 sp1 = ImLerp(p1, p2, t); const ImVec2 sp2 = ImLerp(p1, p2, t + step); const float rt1 = smoothstep(p1.x, p2.x, sp1.x); const float rt2 = smoothstep(p1.x, p2.x, sp2.x); const ImVec2 pos1 = ImVec2(sp1.x, ImLerp(p1.y, p2.y, rt1)) * viewSize + offset; const ImVec2 pos2 = ImVec2(sp2.x, ImLerp(p1.y, p2.y, rt2)) * viewSize + offset; if (distance(io.MousePos.x, io.MousePos.y, pos1.x, pos1.y, pos2.x, pos2.y) < 8.f && !scrollingV) { localOverCurve = int(c); overCurve = int(c); overCurveOrPoint = true; } draw_list->AddLine(pos1, pos2, curveColor, 1.3f); } // substep } else if (curveType == CurveDiscrete) { ImVec2 dp1 = p1 * viewSize + offset; ImVec2 dp2 = ImVec2(p2.x, p1.y) * viewSize + offset; ImVec2 dp3 = p2 * viewSize + offset; draw_list->AddLine(dp1, dp2, curveColor, 1.3f); draw_list->AddLine(dp2, dp3, curveColor, 1.3f); if ((distance(io.MousePos.x, io.MousePos.y, dp1.x, dp1.y, dp3.x, dp1.y) < 8.f || distance(io.MousePos.x, io.MousePos.y, dp3.x, dp1.y, dp3.x, dp3.y) < 8.f) /*&& localOverCurve == -1*/) { localOverCurve = int(c); overCurve = int(c); overCurveOrPoint = true; } } } // point loop for (size_t p = 0; p < ptCount; p++) { const int drawState = DrawPoint(draw_list, pointToRange(pts[p]), viewSize, offset, (selection.find({ int(c), int(p) }) != selection.end() && movingCurve == -1 && !scrollingV)); if (drawState && movingCurve == -1 && !selectingQuad) { overCurveOrPoint = true; overSelectedPoint = true; overCurve = -1; if (drawState == 2) { if (!io.KeyShift && selection.find({ int(c), int(p) }) == selection.end()) selection.clear(); selection.insert({ int(c), int(p) }); } } } } // curves loop if (localOverCurve == -1) overCurve = -1; // move selection static bool pointsMoved = false; static ImVec2 mousePosOrigin; static std::vector originalPoints; if (overSelectedPoint && io.MouseDown[0]) { if ((fabsf(io.MouseDelta.x) > 0.f || fabsf(io.MouseDelta.y) > 0.f) && !selection.empty()) { if (!pointsMoved) { delegate.BeginEdit(0); mousePosOrigin = io.MousePos; originalPoints.resize(selection.size()); int index = 0; for (auto& sel : selection) { const ImVec2* pts = delegate.GetPoints(sel.curveIndex); originalPoints[index++] = pts[sel.pointIndex]; } } pointsMoved = true; ret = 1; auto prevSelection = selection; int originalIndex = 0; for (auto& sel : prevSelection) { const ImVec2 p = rangeToPoint(pointToRange(originalPoints[originalIndex]) + (io.MousePos - mousePosOrigin) * sizeOfPixel); const int newIndex = delegate.EditPoint(sel.curveIndex, sel.pointIndex, p); if (newIndex != sel.pointIndex) { selection.erase(sel); selection.insert({ sel.curveIndex, newIndex }); } originalIndex++; } } } if (overSelectedPoint && !io.MouseDown[0]) { overSelectedPoint = false; if (pointsMoved) { pointsMoved = false; delegate.EndEdit(); } } // add point if (overCurve != -1 && io.MouseDoubleClicked[0]) { const ImVec2 np = rangeToPoint((io.MousePos - offset) / viewSize); delegate.BeginEdit(overCurve); delegate.AddPoint(overCurve, np); delegate.EndEdit(); ret = 1; } // move curve if (movingCurve != -1) { const size_t ptCount = delegate.GetPointCount(movingCurve); const ImVec2* pts = delegate.GetPoints(movingCurve); if (!pointsMoved) { mousePosOrigin = io.MousePos; pointsMoved = true; originalPoints.resize(ptCount); for (size_t index = 0; index < ptCount; index++) { originalPoints[index] = pts[index]; } } if (ptCount >= 1) { for (size_t p = 0; p < ptCount; p++) { delegate.EditPoint(movingCurve, int(p), rangeToPoint(pointToRange(originalPoints[p]) + (io.MousePos - mousePosOrigin) * sizeOfPixel)); } ret = 1; } if (!io.MouseDown[0]) { movingCurve = -1; pointsMoved = false; delegate.EndEdit(); } } if (movingCurve == -1 && overCurve != -1 && ImGui::IsMouseClicked(0) && selection.empty() && !selectingQuad) { movingCurve = overCurve; delegate.BeginEdit(overCurve); } // quad selection if (selectingQuad) { const ImVec2 bmin = ImMin(quadSelection, io.MousePos); const ImVec2 bmax = ImMax(quadSelection, io.MousePos); draw_list->AddRectFilled(bmin, bmax, 0x40FF0000, 1.f); draw_list->AddRect(bmin, bmax, 0xFFFF0000, 1.f); const ImRect selectionQuad(bmin, bmax); if (!io.MouseDown[0]) { if (!io.KeyShift) selection.clear(); // select everythnig is quad for (size_t c = 0; c < curveCount; c++) { if (!delegate.IsVisible(c)) continue; const size_t ptCount = delegate.GetPointCount(c); if (ptCount < 1) continue; const ImVec2* pts = delegate.GetPoints(c); for (size_t p = 0; p < ptCount; p++) { const ImVec2 center = pointToRange(pts[p]) * viewSize + offset; if (selectionQuad.Contains(center)) selection.insert({ int(c), int(p) }); } } // done selectingQuad = false; } } if (!overCurveOrPoint && ImGui::IsMouseClicked(0) && !selectingQuad && movingCurve == -1 && !overSelectedPoint && container.Contains(io.MousePos)) { selectingQuad = true; quadSelection = io.MousePos; } if (clippingRect) draw_list->PopClipRect(); ImGui::EndChildFrame(); ImGui::PopStyleVar(); ImGui::PopStyleColor(1); if (selectedPoints) { selectedPoints->resize(int(selection.size())); int index = 0; for (auto& point : selection) (*selectedPoints)[index++] = point; } return ret; } }