diff --git a/testsuite/gsk/curve-special-cases.c b/testsuite/gsk/curve-special-cases.c new file mode 100644 index 0000000000..8c4bd131bc --- /dev/null +++ b/testsuite/gsk/curve-special-cases.c @@ -0,0 +1,159 @@ +/* + * Copyright © 2020 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include + +#include "gsk/gskcurveprivate.h" + +static void +test_curve_tangents (void) +{ + GskCurve c; + graphene_point_t p[4]; + graphene_vec2_t t; + + graphene_point_init (&p[0], 0, 0); + graphene_point_init (&p[1], 100, 0); + gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_LINE, p)); + + gsk_curve_get_start_tangent (&c, &t); + g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001)); + gsk_curve_get_end_tangent (&c, &t); + g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001)); + + graphene_point_init (&p[0], 0, 0); + graphene_point_init (&p[1], 0, 100); + gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_LINE, p)); + + gsk_curve_get_start_tangent (&c, &t); + g_assert_true (graphene_vec2_near (&t, graphene_vec2_y_axis (), 0.0001)); + gsk_curve_get_end_tangent (&c, &t); + g_assert_true (graphene_vec2_near (&t, graphene_vec2_y_axis (), 0.0001)); + + graphene_point_init (&p[0], 0, 0); + graphene_point_init (&p[1], 50, 0); + graphene_point_init (&p[2], 100, 50); + graphene_point_init (&p[3], 100, 100); + gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CUBIC, p)); + + gsk_curve_get_start_tangent (&c, &t); + g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001)); + gsk_curve_get_end_tangent (&c, &t); + g_assert_true (graphene_vec2_near (&t, graphene_vec2_y_axis (), 0.0001)); +} + +static void +test_curve_degenerate_tangents (void) +{ + GskCurve c; + graphene_point_t p[4]; + graphene_vec2_t t; + + graphene_point_init (&p[0], 0, 0); + graphene_point_init (&p[1], 0, 0); + graphene_point_init (&p[2], 100, 0); + graphene_point_init (&p[3], 100, 0); + gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CUBIC, p)); + + gsk_curve_get_start_tangent (&c, &t); + g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001)); + gsk_curve_get_end_tangent (&c, &t); + g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001)); + + graphene_point_init (&p[0], 0, 0); + graphene_point_init (&p[1], 50, 0); + graphene_point_init (&p[2], 50, 0); + graphene_point_init (&p[3], 100, 0); + gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CUBIC, p)); + + gsk_curve_get_start_tangent (&c, &t); + g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001)); + gsk_curve_get_end_tangent (&c, &t); + g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001)); +} + +static gboolean +pathop_cb (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + gpointer user_data) +{ + GskCurve *curve = user_data; + + g_assert (op != GSK_PATH_CLOSE); + + if (op == GSK_PATH_MOVE) + return TRUE; + + gsk_curve_init_foreach (curve, op, pts, n_pts); + return FALSE; +} + +static void +parse_curve (GskCurve *c, + const char *str) +{ + GskPath *path = gsk_path_parse (str); + + gsk_path_foreach (path, -1, pathop_cb, c); + + gsk_path_unref (path); +} + +static void +test_curve_crossing (void) +{ + struct { + const char *c; + const graphene_point_t p; + int crossing; + } tests[] = { + { "M 0 0 L 200 200", { 200, 100 }, 0 }, + { "M 0 0 L 200 200", { 0, 100 }, 1 }, + { "M 0 200 L 200 0", { 0, 100 }, -1 }, + { "M 0 0 C 100 100 200 200 300 300", { 200, 100 }, 0 }, + { "M 0 0 C 100 100 200 200 300 300", { 0, 100 }, 1 }, + { "M 0 300 C 100 200 200 100 300 0", { 0, 100 }, -1 }, + { "M 0 0 C 100 600 200 -300 300 300", { 0, 150 }, 1 }, + { "M 0 0 C 100 600 200 -300 300 300", { 100, 150 }, 0 }, + { "M 0 0 C 100 600 200 -300 300 300", { 200, 150 }, 1 }, + }; + + for (unsigned int i = 0; i < G_N_ELEMENTS (tests); i++) + { + GskCurve c; + + parse_curve (&c, tests[i].c); + + g_assert_true (gsk_curve_get_crossing (&c, &tests[i].p) == tests[i].crossing); + } +} + +int +main (int argc, + char *argv[]) +{ + gtk_test_init (&argc, &argv, NULL); + + g_test_add_func ("/curve/special/tangents", test_curve_tangents); + g_test_add_func ("/curve/special/degenerate-tangents", test_curve_degenerate_tangents); + g_test_add_func ("/curve/special/crossing", test_curve_crossing); + + return g_test_run (); +} diff --git a/testsuite/gsk/curve.c b/testsuite/gsk/curve.c new file mode 100644 index 0000000000..5310324c93 --- /dev/null +++ b/testsuite/gsk/curve.c @@ -0,0 +1,337 @@ +#include +#include "gsk/gskcurveprivate.h" + +static void +init_random_point (graphene_point_t *p) +{ + p->x = g_test_rand_double_range (0, 1000); + p->y = g_test_rand_double_range (0, 1000); +} + +static void +init_random_curve_with_op (GskCurve *curve, + GskPathOperation min_op, + GskPathOperation max_op) +{ + switch (g_test_rand_int_range (min_op, max_op + 1)) + { + case GSK_PATH_LINE: + { + graphene_point_t p[2]; + + init_random_point (&p[0]); + init_random_point (&p[1]); + gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_LINE, p)); + } + break; + + case GSK_PATH_QUAD: + { + graphene_point_t p[3]; + + init_random_point (&p[0]); + init_random_point (&p[1]); + init_random_point (&p[2]); + gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_QUAD, p)); + } + break; + + case GSK_PATH_CUBIC: + { + graphene_point_t p[4]; + + init_random_point (&p[0]); + init_random_point (&p[1]); + init_random_point (&p[2]); + init_random_point (&p[3]); + gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_CUBIC, p)); + } + break; + + default: + g_assert_not_reached (); + } +} + +static void +init_random_curve (GskCurve *curve) +{ + init_random_curve_with_op (curve, GSK_PATH_LINE, GSK_PATH_CUBIC); +} + +static void +test_curve_tangents (void) +{ + for (int i = 0; i < 100; i++) + { + GskCurve c; + graphene_vec2_t vec, exact; + + init_random_curve (&c); + + gsk_curve_get_tangent (&c, 0, &vec); + g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&vec), 1.0f, 0.00001); + gsk_curve_get_start_tangent (&c, &exact); + g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&exact), 1.0f, 0.00001); + g_assert_true (graphene_vec2_near (&vec, &exact, 0.05)); + + gsk_curve_get_tangent (&c, 1, &vec); + g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&vec), 1.0f, 0.00001); + gsk_curve_get_end_tangent (&c, &exact); + g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&exact), 1.0f, 0.00001); + g_assert_true (graphene_vec2_near (&vec, &exact, 0.05)); + } +} + +static void +test_curve_points (void) +{ + for (int i = 0; i < 100; i++) + { + GskCurve c; + graphene_point_t p; + + init_random_curve (&c); + + /* We can assert equality here because evaluating the polynomials with 0 + * has no effect on accuracy. + */ + gsk_curve_get_point (&c, 0, &p); + g_assert_true (graphene_point_equal (gsk_curve_get_start_point (&c), &p)); + /* But here we evaluate the polynomials with 1 which gives the highest possible + * accuracy error. So we'll just be generous here. + */ + gsk_curve_get_point (&c, 1, &p); + g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c), &p, 0.05)); + } +} + +/* at this point the subdivision stops and the decomposer + * violates tolerance rules + */ +#define MIN_PROGRESS (1/1024.f) + +typedef struct +{ + graphene_point_t p; + float t; +} PointOnLine; + +static gboolean +add_line_to_array (const graphene_point_t *from, + const graphene_point_t *to, + float from_progress, + float to_progress, + GskCurveLineReason reason, + gpointer user_data) +{ + GArray *array = user_data; + PointOnLine *last = &g_array_index (array, PointOnLine, array->len - 1); + + g_assert_true (array->len > 0); + g_assert_cmpfloat (from_progress, >=, 0.0f); + g_assert_cmpfloat (from_progress, <, to_progress); + g_assert_cmpfloat (to_progress, <=, 1.0f); + + g_assert_true (graphene_point_equal (&last->p, from)); + g_assert_cmpfloat (last->t, ==, from_progress); + + g_array_append_vals (array, (PointOnLine[1]) { { *to, to_progress } }, 1); + + return TRUE; +} + +static void +test_curve_decompose (void) +{ + static const float tolerance = 0.5; + + for (int i = 0; i < 100; i++) + { + GArray *array; + GskCurve c; + + init_random_curve (&c); + + array = g_array_new (FALSE, FALSE, sizeof (PointOnLine)); + g_array_append_vals (array, (PointOnLine[1]) { { *gsk_curve_get_start_point (&c), 0.f } }, 1); + + g_assert_true (gsk_curve_decompose (&c, tolerance, add_line_to_array, array)); + + g_assert_cmpint (array->len, >=, 2); /* We at least got a line to the end */ + g_assert_cmpfloat (g_array_index (array, PointOnLine, array->len - 1).t, ==, 1.0); + + for (int j = 0; j < array->len; j++) + { + PointOnLine *pol = &g_array_index (array, PointOnLine, j); + graphene_point_t p; + + /* Check that the points we got are actually on the line */ + gsk_curve_get_point (&c, pol->t, &p); + g_assert_true (graphene_point_near (&pol->p, &p, 0.05)); + + /* Check that the mid point is not further than the tolerance */ + if (j > 0) + { + PointOnLine *last = &g_array_index (array, PointOnLine, j - 1); + graphene_point_t mid; + + if (pol->t - last->t > MIN_PROGRESS) + { + graphene_point_interpolate (&last->p, &pol->p, 0.5, &mid); + gsk_curve_get_point (&c, (pol->t + last->t) / 2, &p); + /* The decomposer does this cheaper Manhattan distance test, + * so graphene_point_near() does not work */ + g_assert_cmpfloat (fabs (mid.x - p.x), <=, tolerance); + g_assert_cmpfloat (fabs (mid.y - p.y), <=, tolerance); + } + } + } + + g_array_unref (array); + } +} + +static gboolean +add_curve_to_array (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + gpointer user_data) +{ + GArray *array = user_data; + GskCurve c; + + gsk_curve_init_foreach (&c, op, pts, n_pts); + g_array_append_val (array, c); + + return TRUE; +} + +static void +test_curve_decompose_into (GskPathForeachFlags flags) +{ + for (int i = 0; i < 100; i++) + { + GskCurve c; + GskPathBuilder *builder; + const graphene_point_t *s; + GskPath *path; + GArray *array; + + init_random_curve (&c); + + builder = gsk_path_builder_new (); + + s = gsk_curve_get_start_point (&c); + gsk_path_builder_move_to (builder, s->x, s->y); + gsk_curve_builder_to (&c, builder); + path = gsk_path_builder_free_to_path (builder); + + array = g_array_new (FALSE, FALSE, sizeof (GskCurve)); + + g_assert_true (gsk_curve_decompose_curve (&c, flags, 0.1, add_curve_to_array, array)); + + g_assert_cmpint (array->len, >=, 1); + + for (int j = 0; j < array->len; j++) + { + GskCurve *c2 = &g_array_index (array, GskCurve, j); + + switch (c2->op) + { + case GSK_PATH_MOVE: + case GSK_PATH_CLOSE: + case GSK_PATH_LINE: + break; + case GSK_PATH_QUAD: + g_assert_true (flags & GSK_PATH_FOREACH_ALLOW_QUAD); + break; + case GSK_PATH_CUBIC: + g_assert_true (flags & GSK_PATH_FOREACH_ALLOW_CUBIC); + break; + default: + g_assert_not_reached (); + } + } + + g_array_unref (array); + gsk_path_unref (path); + } +} + +static void +test_curve_decompose_into_line (void) +{ + test_curve_decompose_into (0); +} + +static void +test_curve_decompose_into_quad (void) +{ + test_curve_decompose_into (GSK_PATH_FOREACH_ALLOW_QUAD); +} + +static void +test_curve_decompose_into_cubic (void) +{ + test_curve_decompose_into (GSK_PATH_FOREACH_ALLOW_CUBIC); +} + +/* Some sanity checks for splitting curves. */ +static void +test_curve_split (void) +{ + for (int i = 0; i < 100; i++) + { + GskCurve c; + GskCurve c1, c2; + graphene_point_t p; + graphene_vec2_t t, t1, t2; + + init_random_curve (&c); + + gsk_curve_split (&c, 0.5, &c1, &c2); + + g_assert_true (c1.op == c.op); + g_assert_true (c2.op == c.op); + + g_assert_true (graphene_point_near (gsk_curve_get_start_point (&c), + gsk_curve_get_start_point (&c1), 0.005)); + g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c1), + gsk_curve_get_start_point (&c2), 0.005)); + g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c), + gsk_curve_get_end_point (&c2), 0.005)); + gsk_curve_get_point (&c, 0.5, &p); + gsk_curve_get_tangent (&c, 0.5, &t); + g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c1), &p, 0.005)); + g_assert_true (graphene_point_near (gsk_curve_get_start_point (&c2), &p, 0.005)); + + gsk_curve_get_start_tangent (&c, &t1); + gsk_curve_get_start_tangent (&c1, &t2); + g_assert_true (graphene_vec2_near (&t1, &t2, 0.005)); + gsk_curve_get_end_tangent (&c1, &t1); + gsk_curve_get_start_tangent (&c2, &t2); + g_assert_true (graphene_vec2_near (&t1, &t2, 0.005)); + g_assert_true (graphene_vec2_near (&t, &t1, 0.005)); + g_assert_true (graphene_vec2_near (&t, &t2, 0.005)); + gsk_curve_get_end_tangent (&c, &t1); + gsk_curve_get_end_tangent (&c2, &t2); + g_assert_true (graphene_vec2_near (&t1, &t2, 0.005)); + } +} + +int +main (int argc, char *argv[]) +{ + (g_test_init) (&argc, &argv, NULL); + + g_test_add_func ("/curve/points", test_curve_points); + g_test_add_func ("/curve/tangents", test_curve_tangents); + g_test_add_func ("/curve/decompose", test_curve_decompose); + g_test_add_func ("/curve/decompose/into/line", test_curve_decompose_into_line); + g_test_add_func ("/curve/decompose/into/quad", test_curve_decompose_into_quad); + g_test_add_func ("/curve/decompose/into/cubic", test_curve_decompose_into_cubic); + g_test_add_func ("/curve/split", test_curve_split); + + return g_test_run (); +} diff --git a/testsuite/gsk/meson.build b/testsuite/gsk/meson.build index 9e104e664e..2ae7bbb22f 100644 --- a/testsuite/gsk/meson.build +++ b/testsuite/gsk/meson.build @@ -389,6 +389,8 @@ foreach t : tests endforeach internal_tests = [ + [ 'curve' ], + [ 'curve-special-cases' ], [ 'diff' ], [ 'half-float' ], ['rounded-rect'],