Skip to content

Commit

Permalink
Refactor CurveEdit to use consistent world/view transforms
Browse files Browse the repository at this point in the history
Currently, `CurveEdit` uses a couple of different ways to ensure that
points and lines are drawn in the right space. This makes the code
harder to understand and maintain.

This PR makes the curve be drawn in view coordinates by transforming
every point from world/curve-space into view-space. By drawing in
view-space, we can keep enable anti-aliasing and use a single way to
transform coordinates from the two different spaces.
  • Loading branch information
anvilfolk committed Sep 29, 2024
1 parent b3d0f47 commit f7484c4
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 73 deletions.
104 changes: 41 additions & 63 deletions editor/plugins/curve_editor_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ void CurveEdit::_curve_changed() {
}
}

int CurveEdit::get_point_at(Vector2 p_pos) const {
int CurveEdit::get_point_at(const Vector2 &p_pos) const {
if (curve.is_null()) {
return -1;
}
Expand All @@ -432,7 +432,7 @@ int CurveEdit::get_point_at(Vector2 p_pos) const {
return closest_idx;
}

CurveEdit::TangentIndex CurveEdit::get_tangent_at(Vector2 p_pos) const {
CurveEdit::TangentIndex CurveEdit::get_tangent_at(const Vector2 &p_pos) const {
if (curve.is_null() || selected_index < 0) {
return TANGENT_NONE;
}
Expand Down Expand Up @@ -491,7 +491,7 @@ float CurveEdit::get_offset_without_collision(int p_current_index, float p_offse
return safe_offset;
}

void CurveEdit::add_point(Vector2 p_pos) {
void CurveEdit::add_point(const Vector2 &p_pos) {
ERR_FAIL_COND(curve.is_null());

// Add a point to get its index, then remove it immediately. Trick to feed the UndoRedo.
Expand Down Expand Up @@ -531,7 +531,7 @@ void CurveEdit::remove_point(int p_index) {
undo_redo->commit_action();
}

void CurveEdit::set_point_position(int p_index, Vector2 p_pos) {
void CurveEdit::set_point_position(int p_index, const Vector2 &p_pos) {
ERR_FAIL_COND(curve.is_null());
ERR_FAIL_INDEX_MSG(p_index, curve->get_point_count(), "Curve point is out of bounds.");

Expand Down Expand Up @@ -707,70 +707,54 @@ Vector2 CurveEdit::get_tangent_view_pos(int p_index, TangentIndex p_tangent) con
return tangent_view_pos;
}

Vector2 CurveEdit::get_view_pos(Vector2 p_world_pos) const {
Vector2 CurveEdit::get_view_pos(const Vector2 &p_world_pos) const {
return _world_to_view.xform(p_world_pos);
}

Vector2 CurveEdit::get_world_pos(Vector2 p_view_pos) const {
Vector2 CurveEdit::get_world_pos(const Vector2 &p_view_pos) const {
return _world_to_view.affine_inverse().xform(p_view_pos);
}

// Uses non-baked points, but takes advantage of ordered iteration to be faster.
template <typename T>
static void plot_curve_accurate(const Curve &curve, float step, Vector2 scaling, T plot_func) {
if (curve.get_point_count() <= 1) {
// Not enough points to make a curve, so it's just a straight line.
// The added tiny vectors make the drawn line stay exactly within the bounds in practice.
float y = curve.sample(0);
plot_func(Vector2(0, y) * scaling + Vector2(0.5, 0), Vector2(1.f, y) * scaling - Vector2(1.5, 0), true);
void CurveEdit::plot_curve_accurate(float p_step, const Color &p_line_color, const Color &p_edge_line_color) {
if (curve->get_point_count() <= 1) { // Draw single line through entire plot
float y = curve->sample(0);
draw_line(get_view_pos(Vector2(0.f, y)) + Vector2(0.5, 0), get_view_pos(Vector2(1.f, y)) - Vector2(1.5, 0), p_line_color, LINE_WIDTH, true);
return;
}

} else {
Vector2 first_point = curve.get_point_position(0);
Vector2 last_point = curve.get_point_position(curve.get_point_count() - 1);

// Edge lines
plot_func(Vector2(0, first_point.y) * scaling + Vector2(0.5, 0), first_point * scaling, false);
plot_func(Vector2(Curve::MAX_X, last_point.y) * scaling - Vector2(1.5, 0), last_point * scaling, false);

// Draw section by section, so that we get maximum precision near points.
// It's an accurate representation, but slower than using the baked one.
for (int i = 1; i < curve.get_point_count(); ++i) {
Vector2 a = curve.get_point_position(i - 1);
Vector2 b = curve.get_point_position(i);

Vector2 pos = a;
Vector2 prev_pos = a;

float scaled_step = step / scaling.x;
float samples = (b.x - a.x) / scaled_step;

for (int j = 1; j < samples; j++) {
float x = j * scaled_step;
pos.x = a.x + x;
pos.y = curve.sample_local_nocheck(i - 1, x);
plot_func(prev_pos * scaling, pos * scaling, true);
prev_pos = pos;
}
Vector2 first_point = curve->get_point_position(0);
Vector2 last_point = curve->get_point_position(curve->get_point_count() - 1);

plot_func(prev_pos * scaling, b * scaling, true);
}
}
}
// Transform pixels-per-step into curve domain. Only works for non-rotated transforms.
const float world_step_size = p_step / _world_to_view.get_scale().x;

struct CanvasItemPlotCurve {
CanvasItem &ci;
Color color1;
Color color2;
// Edge lines
draw_line(get_view_pos(Vector2(0, first_point.y)) + Vector2(0.5, 0), get_view_pos(first_point), p_edge_line_color, LINE_WIDTH, true);
draw_line(get_view_pos(last_point), get_view_pos(Vector2(Curve::MAX_X, last_point.y)) - Vector2(1.5, 0), p_edge_line_color, LINE_WIDTH, true);

CanvasItemPlotCurve(CanvasItem &p_ci, Color p_color1, Color p_color2) :
ci(p_ci),
color1(p_color1),
color2(p_color2) {}
// Draw section by section, so that we get maximum precision near points.
// It's an accurate representation, but slower than using the baked one.
for (int i = 1; i < curve->get_point_count(); ++i) {
Vector2 a = curve->get_point_position(i - 1);
Vector2 b = curve->get_point_position(i);

void operator()(Vector2 pos0, Vector2 pos1, bool in_definition) {
ci.draw_line(pos0, pos1, in_definition ? color1 : color2, 0.5, true);
Vector2 pos = a;
Vector2 prev_pos = a;

float samples = (b.x - a.x) / world_step_size;

for (int j = 1; j < samples; j++) {
float x = j * world_step_size;
pos.x = a.x + x;
pos.y = curve->sample_local_nocheck(i - 1, x);
draw_line(get_view_pos(prev_pos), get_view_pos(pos), p_line_color, LINE_WIDTH, true);
prev_pos = pos;
}

draw_line(get_view_pos(prev_pos), get_view_pos(b), p_line_color, LINE_WIDTH, true);
}
};
}

void CurveEdit::_redraw() {
if (curve.is_null()) {
Expand Down Expand Up @@ -829,20 +813,14 @@ void CurveEdit::_redraw() {
draw_string(font, get_view_pos(Vector2(0, y)) + Vector2(2, -2), String::num(y, 2), HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, text_color);
}

// Draw curve.

// An unusual transform so we can offset the curve before scaling it up, allowing the curve to be antialiased.
// The scaling up ensures that the curve rendering doesn't break when we use a quad line to draw it.
draw_set_transform_matrix(Transform2D(0, get_view_pos(Vector2(0, 0))));
// Draw curve in view coordinates. Curve world-to-view point conversion happens in plot_curve_accurate().

const Color line_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
const Color edge_line_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor)) * Color(1, 1, 1, 0.75);

CanvasItemPlotCurve plot_func(*this, line_color, edge_line_color);
plot_curve_accurate(**curve, 2.f, (get_view_pos(Vector2(1, curve->get_max_value())) - get_view_pos(Vector2(0, curve->get_min_value()))) / Vector2(1, curve->get_range()), plot_func);
plot_curve_accurate(STEP_SIZE, line_color, edge_line_color);

// Draw points, except for the selected one.
draw_set_transform_matrix(Transform2D());

bool shift_pressed = Input::get_singleton()->is_key_pressed(Key::SHIFT);

Expand Down
23 changes: 13 additions & 10 deletions editor/plugins/curve_editor_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ class CurveEdit : public Control {
virtual void gui_input(const Ref<InputEvent> &p_event) override;
void _curve_changed();

int get_point_at(Vector2 p_pos) const;
TangentIndex get_tangent_at(Vector2 p_pos) const;
int get_point_at(const Vector2 &p_pos) const;
TangentIndex get_tangent_at(const Vector2 &p_pos) const;

float get_offset_without_collision(int p_current_index, float p_offset, bool p_prioritize_right = true);

void add_point(Vector2 p_pos);
void add_point(const Vector2 &p_pos);
void remove_point(int p_index);
void set_point_position(int p_index, Vector2 p_pos);
void set_point_position(int p_index, const Vector2 &p_pos);

void set_point_tangents(int p_index, float p_left, float p_right);
void set_point_left_tangent(int p_index, float p_tangent);
Expand All @@ -94,17 +94,20 @@ class CurveEdit : public Control {

void update_view_transform();

void plot_curve_accurate(float p_step, const Color &p_line_color, const Color &p_edge_line_color);

void set_selected_index(int p_index);
void set_selected_tangent_index(TangentIndex p_tangent);

Vector2 get_tangent_view_pos(int p_index, TangentIndex p_tangent) const;
Vector2 get_view_pos(Vector2 p_world_pos) const;
Vector2 get_world_pos(Vector2 p_view_pos) const;
Vector2 get_view_pos(const Vector2 &p_world_pos) const;
Vector2 get_world_pos(const Vector2 &p_view_pos) const;

void _redraw();

private:
const float ASPECT_RATIO = 6.f / 13.f;
const float LINE_WIDTH = 0.5f;
const int STEP_SIZE = 2; // Number of pixels between plot points

Transform2D _world_to_view;

Expand Down Expand Up @@ -136,9 +139,9 @@ class CurveEdit : public Control {
};
GrabMode grabbing = GRAB_NONE;
Vector2 initial_grab_pos;
int initial_grab_index;
float initial_grab_left_tangent;
float initial_grab_right_tangent;
int initial_grab_index = -1;
float initial_grab_left_tangent = 0;
float initial_grab_right_tangent = 0;

bool snap_enabled = false;
int snap_count = 10;
Expand Down

0 comments on commit f7484c4

Please sign in to comment.