diff --git a/gui/CustomCurve.cpp b/gui/CustomCurve.cpp index 70a8f30..cdd253e 100644 --- a/gui/CustomCurve.cpp +++ b/gui/CustomCurve.cpp @@ -1,5 +1,6 @@ #include "CustomCurve.h" +#include #include #include #include @@ -27,7 +28,7 @@ void CustomCurve::ApplyCurveConstraints() { } // Cubic Bezier derivatives -static ImVec2 BezierFirstOrderDerivative(ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, float t) { +static ImVec2 CubicBezierFirstOrderDerivative(ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, float t) { float u = (1 - t); float w1 = 3 * (u * u); float w2 = 6 * (u * t); @@ -35,13 +36,55 @@ static ImVec2 BezierFirstOrderDerivative(ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 return ((p1 - p0) * w1) + ((p2 - p1) * w2) + ((p3 - p2) * w3); } -static ImVec2 BezierSecondOrderDerivative(ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, float t) { +static ImVec2 CubicBezierSecondOrderDerivative(ImVec2 p0, ImVec2 p1, ImVec2 p2, ImVec2 p3, float t) { float u = (1 - t); float w1 = 6 * u; float w2 = 6 * t; return (p2 - p1 * 2 + p0) * w1 + (p3 - p2 * 2 + p1) * w2; } +static ImVec2 QuadraticBezierFirstOrderDerivative(ImVec2 p0, ImVec2 p1, ImVec2 p2, float t) { + float u = (1 - t); + float w1 = 2 * u; + float w2 = 2 * t; + return (p0 - p1) * w1 + (p2 - p1) * w2; +} + +static ImVec2 QuadraticBezierSecondOrderDerivative(ImVec2 p0, ImVec2 p1, ImVec2 p2, float t) { + return ( p2 - p1 *2.f + p0 ) * 2; +} + +static ImVec2 BezierFirstOrderDerivative(ImVec2 p0, ControlPoint_Vec2 p1, ControlPoint_Vec2 p2, ControlPoint_Vec2 p3,float t) { + if (p1.enabled && p2.enabled) + return CubicBezierFirstOrderDerivative(p0, p1, p2, p3, t); + + if (p1.enabled || p2.enabled) + return QuadraticBezierFirstOrderDerivative(p0, p1.enabled ? p1 : p2, p3, t); + + return p0 - p3; +} + +static ImVec2 BezierSecondOrderDerivative(ImVec2 p0, ControlPoint_Vec2 p1, ControlPoint_Vec2 p2, ControlPoint_Vec2 p3, float t) { + if (p1.enabled && p2.enabled) + return CubicBezierSecondOrderDerivative(p0, p1, p2, p3, t); + if (p1.enabled || p2.enabled) + return QuadraticBezierSecondOrderDerivative(p0, p1.enabled ? p1 : p2, p3, t); + return ImVec2(0, 0); +} + +static ImVec2 BezierCalc(ImVec2 startPoint, ControlPoint_Vec2 controlPoint1, ControlPoint_Vec2 controlPoint2, + ImVec2 endPoint, float t) { + if (controlPoint1.enabled && controlPoint2.enabled) { + return ImBezierCubicCalc(startPoint, controlPoint1, controlPoint2, endPoint, t); + } + + if (controlPoint1.enabled || controlPoint2.enabled) { + return ImBezierQuadraticCalc(startPoint, controlPoint1.enabled ? controlPoint1 : controlPoint2, endPoint, t); + } + + return ImLerp(startPoint, endPoint, t); +} + // Spreads points based on the rate of change and other things // Sorry for the shear number of magic numbers in this function, there is just a lot to configure int CustomCurve::ExportCurveToLUT(double *LUT_data_x, double *LUT_data_y) const { @@ -66,7 +109,7 @@ int CustomCurve::ExportCurveToLUT(double *LUT_data_x, double *LUT_data_y) const curve_arc_len[idx++] = 0; for (int j = 1; j < PRE_LUT_ARRAY_SIZE; j++) { const double t = j / (PRE_LUT_ARRAY_SIZE - 1.0); - ImVec2 p = ImBezierCubicCalc(points[i], control_points[i][0], control_points[i][1], points[i + 1], t); + ImVec2 p = BezierCalc(points[i], control_points[i][0], control_points[i][1], points[i + 1], t); ImVec2 dist = p - last_p; dist.y *= 30; // The graph is stretched horizontally, this accounts for that curve_arc_len[idx] = curve_arc_len[idx - 1] + std::sqrt(ImLengthSqr(dist)); @@ -127,7 +170,7 @@ int CustomCurve::ExportCurveToLUT(double *LUT_data_x, double *LUT_data_y) const int start = edge_idx * PRE_LUT_ARRAY_SIZE; t = linear_map_t(tg_arc_len, start); - auto p = ImBezierCubicCalc(points[edge_idx], control_points[edge_idx][0], control_points[edge_idx][1], + auto p = BezierCalc(points[edge_idx], control_points[edge_idx][0], control_points[edge_idx][1], points[edge_idx + 1], t); auto dp = BezierFirstOrderDerivative(points[edge_idx], control_points[edge_idx][0], control_points[edge_idx][1], points[edge_idx + 1], t); @@ -180,7 +223,7 @@ int CustomCurve::ExportCurveToLUT(double *LUT_data_x, double *LUT_data_y) const } // ctrl_p -> control_point -// Format: point[i].x;point[i].y;ctrl_p[i][0].x,ctrl_p[i][0].y;ctrl_p[i][1].x;ctrl_p[i][1].y;point[i+1].x;point[i+1].y;ctrl_p[i][0].x;ctrl_p[i][0].y;ctrl_p[i][1].x;ctrl_p[i][1].y;point[i+1].x.y;...point[n-1].x;point[n-1]; +// Format: point[i].x;point[i].y;ctrl_p[i][0].x,ctrl_p[i][0].y;ctrl_p[i][0].enabled;ctrl_p[i][1].x;ctrl_p[i][1].y;ctrl_p[i][1].enabled;point[i+1].x;point[i+1].y;ctrl_p[i][0].x;ctrl_p[i][0].y;ctrl_p[i][0].enabled;ctrl_p[i][1].x;ctrl_p[i][1].y;ctrl_p[i][1].enabled;point[i+1].x.y;...point[n-1].x;point[n-1]; std::string CustomCurve::ExportCustomCurve() const { std::stringstream out_stream; @@ -196,7 +239,7 @@ std::string CustomCurve::ExportCustomCurve() const { // Print control points if (i < points.size() - 1) { for (auto & cp : control_points[i]) { - out_stream << cp.x << ";" << cp.y << ";"; + out_stream << cp.x << ";" << cp.y << ";" << cp.enabled << ";"; } out_stream.seekp(-1, std::ios_base::cur); // remove the last comma out_stream << ";"; @@ -225,22 +268,22 @@ bool CustomCurve::ImportCustomCurve(const std::string& data) { float p_x = 0, p_y = 0; double p = 0; while (idx < MAX_LUT_ARRAY_SIZE && ss >> p) { - if (idx % 2 == 0) + if ((idx % 8 == 0) || (idx % 8 == 2) || (idx % 8 == 5)) p_x = p; - else { + else if ((idx % 8 == 1) || (idx % 8 == 3) || (idx % 8 == 6)) { p_y = p; - if (idx % 6 == 1) { + if (idx % 8 == 1) { points.emplace_back(p_x, p_y); } - else { - if (idx % 6 == 3) - control_points.emplace_back(); - auto j = idx % 6 - 2; - control_points.back()[j / 2] = {p_x, p_y}; + } + else if ((idx % 8 == 4) || (idx % 8 == 7)) { + if (idx % 8 == 4) { + control_points.emplace_back(); } + auto j = (idx % 8) % 2; + control_points.back()[j] = {p_x, p_y, (bool)p}; } - char nextC = ss.peek(); if (nextC == ';' || nextC == ',') ss.ignore(); @@ -249,11 +292,15 @@ bool CustomCurve::ImportCustomCurve(const std::string& data) { } } catch (std::exception& exception) { + points.clear(); + control_points.clear(); return false; } // 1 element is not enough for a linear interpolation, and n = 3(i - 2) + 4 (where i is the number of nodes), idx 2x because of x/y coordinate - if (idx <= 2 || idx % 6 - 2 != 0) { + if (idx <= 2 || idx % 8 - 2 != 0) { + points.clear(); + control_points.clear(); return false; } @@ -347,7 +394,7 @@ void CustomCurve::UpdateLUT() { for (int i = 0; i < points.size() - 1; i++) { for (int j = 0; j < BEZIER_FRAG_SEGMENTS; ++j) { float t = (float) j / (BEZIER_FRAG_SEGMENTS - 1); - ImVec2 p = ImBezierCubicCalc(points[i], control_points[i][0], control_points[i][1], points[i + 1], t); + ImVec2 p = BezierCalc(points[i], control_points[i][0], control_points[i][1], points[i + 1], t); LUT_points[i * BEZIER_FRAG_SEGMENTS + j] = p; if (last_point.x > p.x) { // Bad Curve diff --git a/gui/CustomCurve.h b/gui/CustomCurve.h index 6be6c8e..f99383e 100644 --- a/gui/CustomCurve.h +++ b/gui/CustomCurve.h @@ -22,10 +22,19 @@ struct Ex_Vec2 : ImVec2 { Ex_Vec2() : ImVec2(0, 0) {} }; +struct ControlPoint_Vec2 : ImVec2 { + bool enabled = true; + + ControlPoint_Vec2(float x, float y) : ImVec2(x, y) {} + ControlPoint_Vec2(float x, float y, bool enabled) : ImVec2(x, y), enabled(enabled) {} + ControlPoint_Vec2(ImVec2 vec) : ImVec2(vec) {} + ControlPoint_Vec2() : ImVec2(0, 0) {} +}; + class CustomCurve { public: std::deque points{{5, 1}, {50, 2}}; // actual points - std::deque > control_points{std::array({ImVec2{40, 1}, ImVec2{20, 2}})}; + std::deque > control_points{std::array({ImVec2{40, 1}, ImVec2{20, 2}})}; std::vector LUT_points{}; CustomCurve() = default; diff --git a/gui/main.cpp b/gui/main.cpp index c274119..834b2a1 100644 --- a/gui/main.cpp +++ b/gui/main.cpp @@ -1,5 +1,6 @@ +#include #include -#include +#include "CustomCurve.h" #include "gui.h" #include #include "DriverHelper.h" @@ -459,6 +460,7 @@ static int OnGui() { change |= ImGui::DragFloat("##pos2x", &p1.x, 0.5, p_min, p_max, "%.3f x"); ImGui::SameLine(0, g.Style.ItemInnerSpacing.x); change |= ImGui::DragFloat("##pos2y", &p1.y, 0.01, 0, 10, "%.3f y"); + change |= ImGui::Checkbox("Enable", &p1.enabled); ImGui::PopItemWidth(); ImGui::PopItemWidth(); ImGui::EndGroup(); @@ -608,14 +610,22 @@ static int OnGui() { if (show_custom_curve_control_points) { ImPlot::PushPlotClipRect(); for (int i = 0; i < points.size() - 1; ++i) { - auto &p = points[i]; - ImVec2 p1 = ImPlot::PlotToPixels(p); - ImVec2 p2 = ImPlot::PlotToPixels(points[(i + 1) % points.size()]); - ImVec2 pc1 = ImPlot::PlotToPixels(control_points[i][0]); - ImVec2 pc2 = ImPlot::PlotToPixels(control_points[i][1]); - - ImPlot::GetPlotDrawList()->AddLine(p1, pc1, ImColor(0.7, 0.1f, 0.8, 0.8)); - ImPlot::GetPlotDrawList()->AddLine(p2, pc2, ImColor(0.7, 0.1f, 0.8, 0.8)); + // we will only + if (!control_points[i][0].enabled && + !control_points[i][1].enabled) { + continue; + } + auto &p = points[i]; + ImVec2 p1 = ImPlot::PlotToPixels(p); + ImVec2 p2 = ImPlot::PlotToPixels(points[(i + 1) % points.size()]); + + ImVec2 pc1 = control_points[i][0].enabled ? ImPlot::PlotToPixels(control_points[i][0]) : ImPlot::PlotToPixels(control_points[i][1]); + ImVec2 pc2 = control_points[i][1].enabled ? ImPlot::PlotToPixels(control_points[i][1]) : ImPlot::PlotToPixels(control_points[i][0]); + + ImPlot::GetPlotDrawList()->AddLine(p1, pc1, ImColor(0.7, 0.1f, 0.8, 0.8)); + ImPlot::GetPlotDrawList()->AddLine(p2, pc2, ImColor(0.7, 0.1f, 0.8, 0.8)); + + } ImPlot::PopPlotClipRect(); } @@ -673,23 +683,34 @@ static int OnGui() { ImVec2 drag = p - p_before; //printf("held drag = (%.2f, %.2f)\n", drag.x, drag.y); - // Apply the Bezier point drag to it's control points - if (i == 0) { - control_points[0][0] += drag; - } else if (i == points.size() - 1) { - control_points[i - 1][1] += drag; - } else { - control_points[i][0] += drag; - control_points[i - 1][1] += drag; + // with only one point, we don't have control points + if (points.size() > 1) { + // Apply the Bezier point drag to it's control points + if (i == 0) { + control_points[0][0] += drag; + } else if (i == points.size() - 1) { + control_points[i - 1][1] += drag; + } else { + control_points[i][0] += drag; + control_points[i - 1][1] += drag; + } } } - if (points.size() > 1) { + + bool more_than_one_point = points.size() > 1; + if (more_than_one_point) { + bool is_start_point = i == 0; + bool is_end_point = i == points.size() - 1; + bool is_start_or_end_point = is_start_point || is_end_point; + bool all_left_disabled = false; + bool all_right_disabled = false; + all_right_disabled = !is_end_point && (!control_points[i][0].enabled && !control_points[i][1].enabled); + all_left_disabled = !is_start_point && (!control_points[i - 1][0].enabled && !control_points[i - 1][1].enabled); ImGui::SeparatorText("Control points"); ImGui::Checkbox("Polar coordinates", &p.use_polar_coordinates); ImGui::SetItemTooltip("Use polar coordinates for the control points"); // Begin grouping to from a grid of widgets - bool is_start_or_end_point = (i == 0 || (i == points.size() - 1)); ImGui::BeginGroup(); // The ternary operator is to handle the start and // end point of the curve because there is no align-button @@ -712,10 +733,29 @@ static int OnGui() { ImGui::Text("y"); } ImGui::TableNextRow(); - for (int j = (i == points.size() - 1 || i == 0) ? 0 : 1; j >= 0; j--) { + for (int j = is_start_or_end_point ? 0 : 1; j >= 0; j--) { + int new_i = is_end_point ? (i - 1) : (i - j); + int new_j = is_start_point ? 0 : is_end_point ? 1 : j; + + // The idea is that if no control point is enabled, we control the next point. + ImVec2 *aux = control_points[new_i][new_j].enabled ? &control_points[new_i][new_j] : &control_points[new_i][new_j ^ 1]; + p_max = i < points.size() - 1 ? points[i + (1 - j)].x - CURVE_POINTS_MARGIN : points[i].x - CURVE_POINTS_MARGIN; + p_min = is_start_point ? points[i].x + CURVE_POINTS_MARGIN : points[i - j].x + CURVE_POINTS_MARGIN; + if (all_right_disabled && !j) { + aux = &points[i + 1]; + p_max = i < points.size() - 2 ? points[i + 2].x - 0.5f : 1000; + p_min = is_start_point ? points[i].x + 0.5f : points[i - j].x + 0.5f; + } + else if (all_left_disabled && (j || is_end_point)) { + aux = &points[i - 1]; + p_max = i < points.size() - 1 ? points[i].x - 0.5f : 1000; + p_min = i == 1 ? 0 : points[i - 2].x + 0.5f; + } + ImVec2 &p1 = *aux; + + // That means there are no control points + // if (!p1.enabled) continue; ImGui::PushID(j); - auto &p1 = control_points[i == points.size() - 1 ? (i - 1) : (i - j)][ - i == 0 ? 0 : i == points.size() - 1 ? 1 : j]; // p.x = std::clamp(p.x, i > 0 ? points[i-1].x + 0.5f : 0, i < points.size() - 1 ? points[i+1].x - 0.5f : 1000); ImGui::TableNextColumn(); if (p.use_polar_coordinates) { @@ -731,7 +771,7 @@ static int OnGui() { const int last_idx = (int) points.size() - 1; const bool is_outgoing = - (i == 0) ? true : (i == last_idx) ? false : (j == 0); + (i == 0) ? true : (i == last_idx) ? false : (j == 0); // Bound L (magnitude) so that p1.x stays within the allowed X interval. auto MaxMagnitudeForHandle = [&](float theta_rad) -> float { @@ -804,7 +844,16 @@ static int OnGui() { // the beginning and the end in the logic // in the condition for this block int new_j = j == 0 ? 1 : 0; - auto &p2 = control_points[i - new_j][new_j]; + // We want to align the control point that is available, that is why we flip the value + ImVec2 *aux = control_points[i - new_j][new_j].enabled ? &control_points[i - new_j][new_j] : &control_points[i - new_j][new_j ^ 1]; + // if every control point is disabled, then we pick the next point + if (all_right_disabled && !new_j) { + aux = &points[i + 1]; + } + else if (all_left_disabled && new_j) { + aux = &points[i - 1]; + } + ImVec2 &p2 = *aux; // We want to preserve the current length auto length = std::sqrt(ImLengthSqr(p1 - p)); @@ -856,12 +905,12 @@ static int OnGui() { if (move_control_points_along) { // Apply the Bezier point drag to it's control points if (held_point == 0) { - control_points[0][0] += drag; + control_points[0][control_points[0][0].enabled ? 0 : 1] += drag; } else if (held_point == points.size() - 1) { - control_points[held_point - 1][1] += drag; + control_points[held_point - 1][control_points[held_point - 1][1].enabled ? 1 : 0] += drag; } else { - control_points[held_point][0] += drag; - control_points[held_point - 1][1] += drag; + control_points[held_point][control_points[held_point][0].enabled ? 0 : 1] += drag; + control_points[held_point - 1][control_points[held_point - 1][1].enabled ? 1 : 0] += drag; } } @@ -889,6 +938,10 @@ static int OnGui() { if (show_custom_curve_control_points && points.size() > 1) { for (int i = 0; i < control_points.size(); ++i) { for (int j = 0; j < 2; j++) { + // Skip rendering if this specific control point is disabled + if (!control_points[i][j].enabled) { + continue; + } bool is_point_hovered = hovered_point != -1 && ( (i == hovered_point && j == 0) || (i == hovered_point - 1 && j == 1)); diff --git a/tools/yeetmousectl/main.cpp b/tools/yeetmousectl/main.cpp index 894a14d..b87d4ad 100644 --- a/tools/yeetmousectl/main.cpp +++ b/tools/yeetmousectl/main.cpp @@ -103,3 +103,6 @@ namespace ImGui { float ImBezierCubicCalc(ImVec2 const&, ImVec2 const&, ImVec2 const&, ImVec2 const&, float) { throw std::logic_error("NOT YET IMPLEMENTED!"); } +float ImBezierQuadraticCalc(const ImVec2 &p1, const ImVec2 &p2, const ImVec2 &p3, float t) { + throw std::logic_error("NOT YET IMPLEMENTED!"); +}