Skip to content
Merged
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
81 changes: 64 additions & 17 deletions gui/CustomCurve.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "CustomCurve.h"

#include <ImGui/imgui.h>
#include <algorithm>
#include <sstream>
#include <string>
Expand Down Expand Up @@ -27,21 +28,63 @@ 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);
float w3 = 3 * (t * t);
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 {
Expand All @@ -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));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand All @@ -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 << ";";
Expand Down Expand Up @@ -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};
Comment on lines +271 to +284

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Is there some sort of safety mechanism for scenarios where the user loads a config with the old formatting? We could have some kind of fallback that parses the old format as well. Or at least exits cleanly without loading any data that might otherwise be invalid. Technically backwards compatibility doesn't have to be maintained, as there are no releases yet.

@anengineer1 anengineer1 Jul 14, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

If the data is invalid, it just cleans the points and you end up with the default curve, take a look at:

    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 % 8 - 2 != 0) {
        points.clear();
        control_points.clear();
        return false;
    }

I also thought about that and because and after thinking, any solution felt like a way to make the code less elegant so I ended up preferring for the curve to go default if the collection doesn't make sense

}


char nextC = ss.peek();
if (nextC == ';' || nextC == ',')
ss.ignore();
Expand All @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions gui/CustomCurve.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

#include <ImGui/implot.h>

#define CURVE_POINTS_MARGIN 0.2f
#define CURVE_POINTS_MARGIN 0.001f
#define BEZIER_FRAG_SEGMENTS 50
#define CURVE_EXPORT_PRECISION 3 // Decimal points precision for exporting Custom Curves

Expand All @@ -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<Ex_Vec2> points{{5, 1}, {50, 2}}; // actual points
std::deque<std::array<ImVec2, 2> > control_points{std::array<ImVec2, 2>({ImVec2{40, 1}, ImVec2{20, 2}})};
std::deque<std::array<ControlPoint_Vec2, 2> > control_points{std::array<ControlPoint_Vec2, 2>({ImVec2{40, 1}, ImVec2{20, 2}})};
std::vector<ImPlotPoint> LUT_points{};

CustomCurve() = default;
Expand Down
Loading
Loading