gsk: Add tests for GskCurve

This commit is contained in:
Matthias Clasen 2023-07-08 11:31:29 -04:00
parent 1b5dfcba7e
commit a4cbabb80f
3 changed files with 498 additions and 0 deletions

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include <gtk/gtk.h>
#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 ();
}

337
testsuite/gsk/curve.c Normal file
View File

@ -0,0 +1,337 @@
#include <gtk/gtk.h>
#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 ();
}

View File

@ -389,6 +389,8 @@ foreach t : tests
endforeach
internal_tests = [
[ 'curve' ],
[ 'curve-special-cases' ],
[ 'diff' ],
[ 'half-float' ],
['rounded-rect'],