diff --git a/gsk/gsk.h b/gsk/gsk.h index fc5f3962f9..af1f0987d2 100644 --- a/gsk/gsk.h +++ b/gsk/gsk.h @@ -20,6 +20,9 @@ #define __GSK_H_INSIDE__ #include +#include +#include +#include #include #include #include diff --git a/gsk/gskcontour.c b/gsk/gskcontour.c new file mode 100644 index 0000000000..f28e6115c7 --- /dev/null +++ b/gsk/gskcontour.c @@ -0,0 +1,635 @@ +/* + * 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 "config.h" + +#include "gskcontourprivate.h" + +#include "gskcurveprivate.h" +#include "gskpathbuilder.h" +#include "gskpathprivate.h" +#include "gskpathpointprivate.h" +#include "gsksplineprivate.h" + +typedef struct _GskContourClass GskContourClass; + +struct _GskContour +{ + const GskContourClass *klass; +}; + +struct _GskContourClass +{ + gsize struct_size; + const char *type_name; + + void (* copy) (const GskContour *contour, + GskContour *dest); + gsize (* get_size) (const GskContour *contour); + GskPathFlags (* get_flags) (const GskContour *contour); + void (* print) (const GskContour *contour, + GString *string); + gboolean (* get_bounds) (const GskContour *contour, + GskBoundingBox *bounds); + void (* get_start_end) (const GskContour *self, + graphene_point_t *start, + graphene_point_t *end); + gboolean (* foreach) (const GskContour *contour, + float tolerance, + GskPathForeachFunc func, + gpointer user_data); + GskContour * (* reverse) (const GskContour *contour); + int (* get_winding) (const GskContour *contour, + const graphene_point_t *point); + gboolean (* get_closest_point) (const GskContour *contour, + const graphene_point_t *point, + float threshold, + GskRealPathPoint *result, + float *out_dist); + void (* get_position) (const GskContour *contour, + GskRealPathPoint *point, + graphene_point_t *position); + void (* get_tangent) (const GskContour *contour, + GskRealPathPoint *point, + GskPathDirection direction, + graphene_vec2_t *tangent); + float (* get_curvature) (const GskContour *contour, + GskRealPathPoint *point, + graphene_point_t *center); +}; + +/* {{{ Utilities */ + +#define DEG_TO_RAD(x) ((x) * (G_PI / 180.f)) +#define RAD_TO_DEG(x) ((x) / (G_PI / 180.f)) + +static void +_g_string_append_double (GString *string, + double d) +{ + char buf[G_ASCII_DTOSTR_BUF_SIZE]; + + g_ascii_dtostr (buf, G_ASCII_DTOSTR_BUF_SIZE, d); + g_string_append (string, buf); +} + +static void +_g_string_append_point (GString *string, + const graphene_point_t *pt) +{ + _g_string_append_double (string, pt->x); + g_string_append_c (string, ' '); + _g_string_append_double (string, pt->y); +} + + +/* }}} */ +/* {{{ Standard */ + +typedef struct _GskStandardContour GskStandardContour; +struct _GskStandardContour +{ + GskContour contour; + + GskPathFlags flags; + + gsize n_ops; + gsize n_points; + graphene_point_t *points; + gskpathop ops[]; +}; + +static gsize +gsk_standard_contour_compute_size (gsize n_ops, + gsize n_points) +{ + return sizeof (GskStandardContour) + + sizeof (gskpathop) * n_ops + + sizeof (graphene_point_t) * n_points; +} + +static void +gsk_standard_contour_init (GskContour *contour, + GskPathFlags flags, + const graphene_point_t *points, + gsize n_points, + const gskpathop *ops, + gsize n_ops, + ptrdiff_t offset); + +static void +gsk_standard_contour_copy (const GskContour *contour, + GskContour *dest) +{ + const GskStandardContour *self = (const GskStandardContour *) contour; + + gsk_standard_contour_init (dest, self->flags, self->points, self->n_points, self->ops, self->n_ops, 0); +} + +static gsize +gsk_standard_contour_get_size (const GskContour *contour) +{ + const GskStandardContour *self = (const GskStandardContour *) contour; + + return gsk_standard_contour_compute_size (self->n_ops, self->n_points); +} + +static gboolean +gsk_standard_contour_foreach (const GskContour *contour, + float tolerance, + GskPathForeachFunc func, + gpointer user_data) +{ + const GskStandardContour *self = (const GskStandardContour *) contour; + gsize i; + + for (i = 0; i < self->n_ops; i ++) + { + if (!gsk_pathop_foreach (self->ops[i], func, user_data)) + return FALSE; + } + + return TRUE; +} + +static gboolean +add_reverse (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + gpointer user_data) +{ + GskPathBuilder *builder = user_data; + GskCurve c, r; + + if (op == GSK_PATH_MOVE) + return TRUE; + + if (op == GSK_PATH_CLOSE) + op = GSK_PATH_LINE; + + gsk_curve_init_foreach (&c, op, pts, n_pts); + gsk_curve_reverse (&c, &r); + gsk_curve_builder_to (&r, builder); + + return TRUE; +} + +static GskContour * +gsk_standard_contour_reverse (const GskContour *contour) +{ + const GskStandardContour *self = (const GskStandardContour *) contour; + GskPathBuilder *builder; + GskPath *path; + GskContour *res; + + builder = gsk_path_builder_new (); + + gsk_path_builder_move_to (builder, self->points[self->n_points - 1].x, + self->points[self->n_points - 1].y); + + for (int i = self->n_ops - 1; i >= 0; i--) + gsk_pathop_foreach (self->ops[i], add_reverse, builder); + + if (self->flags & GSK_PATH_CLOSED) + gsk_path_builder_close (builder); + + path = gsk_path_builder_free_to_path (builder); + + g_assert (gsk_path_get_n_contours (path) == 1); + + res = gsk_contour_dup (gsk_path_get_contour (path, 0)); + + gsk_path_unref (path); + + return res; +} + +static GskPathFlags +gsk_standard_contour_get_flags (const GskContour *contour) +{ + const GskStandardContour *self = (const GskStandardContour *) contour; + + return self->flags; +} + +static void +gsk_standard_contour_print (const GskContour *contour, + GString *string) +{ + const GskStandardContour *self = (const GskStandardContour *) contour; + gsize i; + + for (i = 0; i < self->n_ops; i ++) + { + const graphene_point_t *pt = gsk_pathop_points (self->ops[i]); + + switch (gsk_pathop_op (self->ops[i])) + { + case GSK_PATH_MOVE: + g_string_append (string, "M "); + _g_string_append_point (string, &pt[0]); + break; + + case GSK_PATH_CLOSE: + g_string_append (string, " Z"); + break; + + case GSK_PATH_LINE: + g_string_append (string, " L "); + _g_string_append_point (string, &pt[1]); + break; + + case GSK_PATH_QUAD: + g_string_append (string, " Q "); + _g_string_append_point (string, &pt[1]); + g_string_append (string, ", "); + _g_string_append_point (string, &pt[2]); + break; + + case GSK_PATH_CUBIC: + g_string_append (string, " C "); + _g_string_append_point (string, &pt[1]); + g_string_append (string, ", "); + _g_string_append_point (string, &pt[2]); + g_string_append (string, ", "); + _g_string_append_point (string, &pt[3]); + break; + + default: + g_assert_not_reached(); + return; + } + } +} + +static gboolean +gsk_standard_contour_get_bounds (const GskContour *contour, + GskBoundingBox *bounds) +{ + const GskStandardContour *self = (const GskStandardContour *) contour; + gsize i; + + if (self->n_points == 0) + return FALSE; + + gsk_bounding_box_init (bounds, &self->points[0], &self->points[0]); + for (i = 1; i < self->n_points; i ++) + gsk_bounding_box_expand (bounds, &self->points[i]); + + return bounds->max.x > bounds->min.x && bounds->max.y > bounds->min.y; +} + +static void +gsk_standard_contour_get_start_end (const GskContour *contour, + graphene_point_t *start, + graphene_point_t *end) +{ + const GskStandardContour *self = (const GskStandardContour *) contour; + + if (start) + *start = self->points[0]; + + if (end) + *end = self->points[self->n_points - 1]; +} + +static int +gsk_standard_contour_get_winding (const GskContour *contour, + const graphene_point_t *point) +{ + GskStandardContour *self = (GskStandardContour *) contour; + int winding = 0; + + for (gsize i = 0; i < self->n_ops; i ++) + { + GskCurve c; + + if (gsk_pathop_op (self->ops[i]) == GSK_PATH_MOVE) + continue; + + gsk_curve_init (&c, self->ops[i]); + winding += gsk_curve_get_crossing (&c, point); + } + + if ((self->flags & GSK_PATH_CLOSED) == 0) + { + GskCurve c; + + gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CLOSE, + (const graphene_point_t[]) { self->points[self->n_points - 1], + self->points[0] })); + + winding += gsk_curve_get_crossing (&c, point); + } + + return winding; +} + +static gboolean +gsk_standard_contour_get_closest_point (const GskContour *contour, + const graphene_point_t *point, + float threshold, + GskRealPathPoint *result, + float *out_dist) +{ + GskStandardContour *self = (GskStandardContour *) contour; + unsigned int best_idx = G_MAXUINT; + float best_t = 0; + + g_assert (gsk_pathop_op (self->ops[0]) == GSK_PATH_MOVE); + + if (self->n_ops == 1) + { + float dist; + + dist = graphene_point_distance (point, &self->points[0], NULL, NULL); + if (dist <= threshold) + { + *out_dist = dist; + result->data.std.idx = 0; + result->data.std.t = 0; + return TRUE; + } + + return FALSE; + } + + for (gsize i = 0; i < self->n_ops; i ++) + { + GskCurve c; + float distance, t; + + if (gsk_pathop_op (self->ops[i]) == GSK_PATH_MOVE) + continue; + + gsk_curve_init (&c, self->ops[i]); + if (gsk_curve_get_closest_point (&c, point, threshold, &distance, &t)) + { + best_idx = i; + best_t = t; + threshold = distance; + } + } + + if (best_idx != G_MAXUINT) + { + *out_dist = threshold; + result->data.std.idx = best_idx; + result->data.std.t = best_t; + return TRUE; + } + + return FALSE; +} + +static void +gsk_standard_contour_get_position (const GskContour *contour, + GskRealPathPoint *point, + graphene_point_t *position) +{ + GskStandardContour *self = (GskStandardContour *) contour; + GskCurve curve; + + if (G_UNLIKELY (point->data.std.idx == 0)) + { + *position = self->points[0]; + return; + } + + gsk_curve_init (&curve, self->ops[point->data.std.idx]); + gsk_curve_get_point (&curve, point->data.std.t, position); +} + +static void +gsk_standard_contour_get_tangent (const GskContour *contour, + GskRealPathPoint *point, + GskPathDirection direction, + graphene_vec2_t *tangent) +{ + GskStandardContour *self = (GskStandardContour *) contour; + GskCurve curve; + + if (G_UNLIKELY (point->data.std.idx == 0)) + { + graphene_vec2_init (tangent, 1, 0); + return; + } + + gsk_curve_init (&curve, self->ops[point->data.std.idx]); + gsk_curve_get_tangent (&curve, point->data.std.t, tangent); +} + +static float +gsk_standard_contour_get_curvature (const GskContour *contour, + GskRealPathPoint *point, + graphene_point_t *center) +{ + GskStandardContour *self = (GskStandardContour *) contour; + GskCurve curve; + + if (G_UNLIKELY (point->data.std.idx == 0)) + return 0; + + gsk_curve_init (&curve, self->ops[point->data.std.idx]); + return gsk_curve_get_curvature (&curve, point->data.std.t, center); +} + +static const GskContourClass GSK_STANDARD_CONTOUR_CLASS = +{ + sizeof (GskStandardContour), + "GskStandardContour", + gsk_standard_contour_copy, + gsk_standard_contour_get_size, + gsk_standard_contour_get_flags, + gsk_standard_contour_print, + gsk_standard_contour_get_bounds, + gsk_standard_contour_get_start_end, + gsk_standard_contour_foreach, + gsk_standard_contour_reverse, + gsk_standard_contour_get_winding, + gsk_standard_contour_get_closest_point, + gsk_standard_contour_get_position, + gsk_standard_contour_get_tangent, + gsk_standard_contour_get_curvature, +}; + +/* You must ensure the contour has enough size allocated, + * see gsk_standard_contour_compute_size() + */ +static void +gsk_standard_contour_init (GskContour *contour, + GskPathFlags flags, + const graphene_point_t *points, + gsize n_points, + const gskpathop *ops, + gsize n_ops, + gssize offset) + +{ + GskStandardContour *self = (GskStandardContour *) contour; + gsize i; + + self->contour.klass = &GSK_STANDARD_CONTOUR_CLASS; + + self->flags = flags; + self->n_ops = n_ops; + self->n_points = n_points; + self->points = (graphene_point_t *) &self->ops[n_ops]; + memcpy (self->points, points, sizeof (graphene_point_t) * n_points); + + offset += self->points - points; + for (i = 0; i < n_ops; i++) + { + self->ops[i] = gsk_pathop_encode (gsk_pathop_op (ops[i]), + gsk_pathop_points (ops[i]) + offset); + } +} + +GskContour * +gsk_standard_contour_new (GskPathFlags flags, + const graphene_point_t *points, + gsize n_points, + const gskpathop *ops, + gsize n_ops, + gssize offset) +{ + GskContour *contour; + + contour = g_malloc0 (gsk_standard_contour_compute_size (n_ops, n_points)); + + gsk_standard_contour_init (contour, flags, points, n_points, ops, n_ops, offset); + + return contour; +} + +/* }}} */ +/* {{{ API */ + +gsize +gsk_contour_get_size (const GskContour *self) +{ + return self->klass->get_size (self); +} + +void +gsk_contour_copy (GskContour *dest, + const GskContour *src) +{ + src->klass->copy (src, dest); +} + +GskContour * +gsk_contour_dup (const GskContour *src) +{ + GskContour *copy; + + copy = g_malloc0 (gsk_contour_get_size (src)); + gsk_contour_copy (copy, src); + + return copy; +} + +GskContour * +gsk_contour_reverse (const GskContour *src) +{ + return src->klass->reverse (src); +} + +GskPathFlags +gsk_contour_get_flags (const GskContour *self) +{ + return self->klass->get_flags (self); +} + +void +gsk_contour_print (const GskContour *self, + GString *string) +{ + self->klass->print (self, string); +} + +gboolean +gsk_contour_get_bounds (const GskContour *self, + GskBoundingBox *bounds) +{ + return self->klass->get_bounds (self, bounds); +} + +gboolean +gsk_contour_foreach (const GskContour *self, + float tolerance, + GskPathForeachFunc func, + gpointer user_data) +{ + return self->klass->foreach (self, tolerance, func, user_data); +} + +void +gsk_contour_get_start_end (const GskContour *self, + graphene_point_t *start, + graphene_point_t *end) +{ + self->klass->get_start_end (self, start, end); +} + +int +gsk_contour_get_winding (const GskContour *self, + const graphene_point_t *point) +{ + return self->klass->get_winding (self, point); +} + +gboolean +gsk_contour_get_closest_point (const GskContour *self, + const graphene_point_t *point, + float threshold, + GskRealPathPoint *result, + float *out_dist) +{ + return self->klass->get_closest_point (self, point, threshold, result, out_dist); +} + +void +gsk_contour_get_position (const GskContour *self, + GskRealPathPoint *point, + graphene_point_t *pos) +{ + self->klass->get_position (self, point, pos); +} + +void +gsk_contour_get_tangent (const GskContour *self, + GskRealPathPoint *point, + GskPathDirection direction, + graphene_vec2_t *tangent) +{ + self->klass->get_tangent (self, point, direction, tangent); +} + +float +gsk_contour_get_curvature (const GskContour *self, + GskRealPathPoint *point, + graphene_point_t *center) +{ + return self->klass->get_curvature (self, point, center); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/gsk/gskcontourprivate.h b/gsk/gskcontourprivate.h new file mode 100644 index 0000000000..04f8d4239b --- /dev/null +++ b/gsk/gskcontourprivate.h @@ -0,0 +1,73 @@ +/* + * 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 + */ + + +#pragma once + +#include "gskpathprivate.h" +#include "gskpathpointprivate.h" +#include "gskpathopprivate.h" +#include "gskboundingboxprivate.h" + +G_BEGIN_DECLS + +GskContour * gsk_standard_contour_new (GskPathFlags flags, + const graphene_point_t *points, + gsize n_points, + const gskpathop *ops, + gsize n_ops, + gssize offset); + +void gsk_contour_copy (GskContour * dest, + const GskContour *src); +GskContour * gsk_contour_dup (const GskContour *src); +GskContour * gsk_contour_reverse (const GskContour *src); + +gsize gsk_contour_get_size (const GskContour *self); +GskPathFlags gsk_contour_get_flags (const GskContour *self); +void gsk_contour_print (const GskContour *self, + GString *string); +gboolean gsk_contour_get_bounds (const GskContour *self, + GskBoundingBox *bounds); +gboolean gsk_contour_foreach (const GskContour *self, + float tolerance, + GskPathForeachFunc func, + gpointer user_data); +void gsk_contour_get_start_end (const GskContour *self, + graphene_point_t *start, + graphene_point_t *end); +int gsk_contour_get_winding (const GskContour *self, + const graphene_point_t *point); +gboolean gsk_contour_get_closest_point (const GskContour *self, + const graphene_point_t *point, + float threshold, + GskRealPathPoint *result, + float *out_dist); +void gsk_contour_get_position (const GskContour *self, + GskRealPathPoint *point, + graphene_point_t *pos); +void gsk_contour_get_tangent (const GskContour *self, + GskRealPathPoint *point, + GskPathDirection direction, + graphene_vec2_t *tangent); +float gsk_contour_get_curvature (const GskContour *self, + GskRealPathPoint *point, + graphene_point_t *center); + +G_END_DECLS diff --git a/gsk/gskcurve.c b/gsk/gskcurve.c new file mode 100644 index 0000000000..d2c23ef0f4 --- /dev/null +++ b/gsk/gskcurve.c @@ -0,0 +1,1533 @@ +/* + * 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 "config.h" + +#include "gskcurveprivate.h" +#include "gskboundingboxprivate.h" + +/* GskCurve collects all the functionality we need for Bézier segments */ + +#define MIN_PROGRESS (1/1024.f) + +typedef struct _GskCurveClass GskCurveClass; + +struct _GskCurveClass +{ + void (* init) (GskCurve *curve, + gskpathop op); + void (* init_foreach) (GskCurve *curve, + GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts); + void (* print) (const GskCurve *curve, + GString *string); + gskpathop (* pathop) (const GskCurve *curve); + const graphene_point_t * (* get_start_point) (const GskCurve *curve); + const graphene_point_t * (* get_end_point) (const GskCurve *curve); + void (* get_start_tangent) (const GskCurve *curve, + graphene_vec2_t *tangent); + void (* get_end_tangent) (const GskCurve *curve, + graphene_vec2_t *tangent); + void (* get_point) (const GskCurve *curve, + float t, + graphene_point_t *pos); + void (* get_tangent) (const GskCurve *curve, + float t, + graphene_vec2_t *tangent); + void (* reverse) (const GskCurve *curve, + GskCurve *reverse); + float (* get_curvature) (const GskCurve *curve, + float t); + void (* split) (const GskCurve *curve, + float progress, + GskCurve *result1, + GskCurve *result2); + void (* segment) (const GskCurve *curve, + float start, + float end, + GskCurve *segment); + gboolean (* decompose) (const GskCurve *curve, + float tolerance, + GskCurveAddLineFunc add_line_func, + gpointer user_data); + gboolean (* decompose_curve) (const GskCurve *curve, + GskPathForeachFlags flags, + float tolerance, + GskCurveAddCurveFunc add_curve_func, + gpointer user_data); + void (* get_bounds) (const GskCurve *curve, + GskBoundingBox *bounds); + void (* get_tight_bounds) (const GskCurve *curve, + GskBoundingBox *bounds); +}; + +/* {{{ Utilities */ + +static void +get_tangent (const graphene_point_t *p0, + const graphene_point_t *p1, + graphene_vec2_t *t) +{ + graphene_vec2_init (t, p1->x - p0->x, p1->y - p0->y); + graphene_vec2_normalize (t, t); +} + +/* Replace a line by an equivalent quad, + * and a quad by an equivalent cubic. + */ +static void +gsk_curve_elevate (const GskCurve *curve, + GskCurve *elevated) +{ + if (curve->op == GSK_PATH_LINE) + { + graphene_point_t p[3]; + + p[0] = curve->line.points[0]; + graphene_point_interpolate (&curve->line.points[0], + &curve->line.points[1], + 0.5, + &p[1]); + p[2] = curve->line.points[1]; + gsk_curve_init (elevated, gsk_pathop_encode (GSK_PATH_QUAD, p)); + } + else if (curve->op == GSK_PATH_QUAD) + { + graphene_point_t p[4]; + + p[0] = curve->quad.points[0]; + graphene_point_interpolate (&curve->quad.points[0], + &curve->quad.points[1], + 2/3., + &p[1]); + graphene_point_interpolate (&curve->quad.points[2], + &curve->quad.points[1], + 2/3., + &p[2]); + p[3] = curve->quad.points[2]; + gsk_curve_init (elevated, gsk_pathop_encode (GSK_PATH_CUBIC, p)); + } + else + g_assert_not_reached (); +} + +/* }}} */ +/* {{{ Line */ + +static void +gsk_line_curve_init_from_points (GskLineCurve *self, + GskPathOperation op, + const graphene_point_t *start, + const graphene_point_t *end) +{ + self->op = op; + self->points[0] = *start; + self->points[1] = *end; +} + +static void +gsk_line_curve_init (GskCurve *curve, + gskpathop op) +{ + GskLineCurve *self = &curve->line; + const graphene_point_t *pts = gsk_pathop_points (op); + + gsk_line_curve_init_from_points (self, gsk_pathop_op (op), &pts[0], &pts[1]); +} + +static void +gsk_line_curve_init_foreach (GskCurve *curve, + GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts) +{ + GskLineCurve *self = &curve->line; + + g_assert (n_pts == 2); + + gsk_line_curve_init_from_points (self, op, &pts[0], &pts[1]); +} + +static void +gsk_line_curve_print (const GskCurve *curve, + GString *string) +{ + g_string_append_printf (string, + "M %g %g L %g %g", + curve->line.points[0].x, curve->line.points[0].y, + curve->line.points[1].x, curve->line.points[1].y); +} + +static gskpathop +gsk_line_curve_pathop (const GskCurve *curve) +{ + const GskLineCurve *self = &curve->line; + + return gsk_pathop_encode (self->op, self->points); +} + +static const graphene_point_t * +gsk_line_curve_get_start_point (const GskCurve *curve) +{ + const GskLineCurve *self = &curve->line; + + return &self->points[0]; +} + +static const graphene_point_t * +gsk_line_curve_get_end_point (const GskCurve *curve) +{ + const GskLineCurve *self = &curve->line; + + return &self->points[1]; +} + +static void +gsk_line_curve_get_start_end_tangent (const GskCurve *curve, + graphene_vec2_t *tangent) +{ + const GskLineCurve *self = &curve->line; + + get_tangent (&self->points[0], &self->points[1], tangent); +} + +static void +gsk_line_curve_get_point (const GskCurve *curve, + float t, + graphene_point_t *pos) +{ + const GskLineCurve *self = &curve->line; + + graphene_point_interpolate (&self->points[0], &self->points[1], t, pos); +} + +static void +gsk_line_curve_get_tangent (const GskCurve *curve, + float t, + graphene_vec2_t *tangent) +{ + const GskLineCurve *self = &curve->line; + + get_tangent (&self->points[0], &self->points[1], tangent); +} + +static float +gsk_line_curve_get_curvature (const GskCurve *curve, + float t) +{ + return 0; +} + +static void +gsk_line_curve_reverse (const GskCurve *curve, + GskCurve *reverse) +{ + const GskLineCurve *self = &curve->line; + + reverse->op = GSK_PATH_LINE; + reverse->line.points[0] = self->points[1]; + reverse->line.points[1] = self->points[0]; +} + +static void +gsk_line_curve_split (const GskCurve *curve, + float progress, + GskCurve *start, + GskCurve *end) +{ + const GskLineCurve *self = &curve->line; + graphene_point_t point; + + graphene_point_interpolate (&self->points[0], &self->points[1], progress, &point); + + if (start) + gsk_line_curve_init_from_points (&start->line, GSK_PATH_LINE, &self->points[0], &point); + if (end) + gsk_line_curve_init_from_points (&end->line, GSK_PATH_LINE, &point, &self->points[1]); +} + +static void +gsk_line_curve_segment (const GskCurve *curve, + float start, + float end, + GskCurve *segment) +{ + const GskLineCurve *self = &curve->line; + graphene_point_t start_point, end_point; + + graphene_point_interpolate (&self->points[0], &self->points[1], start, &start_point); + graphene_point_interpolate (&self->points[0], &self->points[1], end, &end_point); + + gsk_line_curve_init_from_points (&segment->line, GSK_PATH_LINE, &start_point, &end_point); +} + +static gboolean +gsk_line_curve_decompose (const GskCurve *curve, + float tolerance, + GskCurveAddLineFunc add_line_func, + gpointer user_data) +{ + const GskLineCurve *self = &curve->line; + + return add_line_func (&self->points[0], &self->points[1], 0.0f, 1.0f, GSK_CURVE_LINE_REASON_STRAIGHT, user_data); +} + +static gboolean +gsk_line_curve_decompose_curve (const GskCurve *curve, + GskPathForeachFlags flags, + float tolerance, + GskCurveAddCurveFunc add_curve_func, + gpointer user_data) +{ + const GskLineCurve *self = &curve->line; + + return add_curve_func (GSK_PATH_LINE, self->points, 2, user_data); +} + +static void +gsk_line_curve_get_bounds (const GskCurve *curve, + GskBoundingBox *bounds) +{ + const GskLineCurve *self = &curve->line; + const graphene_point_t *pts = self->points; + + gsk_bounding_box_init (bounds, &pts[0], &pts[1]); +} + +static const GskCurveClass GSK_LINE_CURVE_CLASS = { + gsk_line_curve_init, + gsk_line_curve_init_foreach, + gsk_line_curve_print, + gsk_line_curve_pathop, + gsk_line_curve_get_start_point, + gsk_line_curve_get_end_point, + gsk_line_curve_get_start_end_tangent, + gsk_line_curve_get_start_end_tangent, + gsk_line_curve_get_point, + gsk_line_curve_get_tangent, + gsk_line_curve_reverse, + gsk_line_curve_get_curvature, + gsk_line_curve_split, + gsk_line_curve_segment, + gsk_line_curve_decompose, + gsk_line_curve_decompose_curve, + gsk_line_curve_get_bounds, + gsk_line_curve_get_bounds, +}; + +/* }}} */ +/* {{{ Quadratic */ + +static void +gsk_quad_curve_ensure_coefficients (const GskQuadCurve *curve) +{ + GskQuadCurve *self = (GskQuadCurve *) curve; + const graphene_point_t *pts = self->points; + + if (self->has_coefficients) + return; + + self->coeffs[2] = pts[0]; + self->coeffs[1] = GRAPHENE_POINT_INIT (2 * (pts[1].x - pts[0].x), + 2 * (pts[1].y - pts[0].y)); + self->coeffs[0] = GRAPHENE_POINT_INIT (pts[2].x - 2 * pts[1].x + pts[0].x, + pts[2].y - 2 * pts[1].y + pts[0].y); + + self->has_coefficients = TRUE; +} + +static void +gsk_quad_curve_init_from_points (GskQuadCurve *self, + const graphene_point_t pts[3]) +{ + self->op = GSK_PATH_QUAD; + self->has_coefficients = FALSE; + memcpy (self->points, pts, sizeof (graphene_point_t) * 3); +} + +static void +gsk_quad_curve_init (GskCurve *curve, + gskpathop op) +{ + GskQuadCurve *self = &curve->quad; + + gsk_quad_curve_init_from_points (self, gsk_pathop_points (op)); +} + +static void +gsk_quad_curve_init_foreach (GskCurve *curve, + GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts) +{ + GskQuadCurve *self = &curve->quad; + + g_assert (n_pts == 3); + + gsk_quad_curve_init_from_points (self, pts); +} + +static void +gsk_quad_curve_print (const GskCurve *curve, + GString *string) +{ + g_string_append_printf (string, + "M %g %g Q %g %g %g %g", + curve->quad.points[0].x, curve->quad.points[0].y, + curve->quad.points[1].x, curve->cubic.points[1].y, + curve->quad.points[2].x, curve->cubic.points[2].y); +} + +static gskpathop +gsk_quad_curve_pathop (const GskCurve *curve) +{ + const GskQuadCurve *self = &curve->quad; + + return gsk_pathop_encode (self->op, self->points); +} + +static const graphene_point_t * +gsk_quad_curve_get_start_point (const GskCurve *curve) +{ + const GskQuadCurve *self = &curve->quad; + + return &self->points[0]; +} + +static const graphene_point_t * +gsk_quad_curve_get_end_point (const GskCurve *curve) +{ + const GskQuadCurve *self = &curve->quad; + + return &self->points[2]; +} + +static void +gsk_quad_curve_get_start_tangent (const GskCurve *curve, + graphene_vec2_t *tangent) +{ + const GskQuadCurve *self = &curve->quad; + + get_tangent (&self->points[0], &self->points[1], tangent); +} + +static void +gsk_quad_curve_get_end_tangent (const GskCurve *curve, + graphene_vec2_t *tangent) +{ + const GskQuadCurve *self = &curve->quad; + + get_tangent (&self->points[1], &self->points[2], tangent); +} + +static void +gsk_quad_curve_get_point (const GskCurve *curve, + float t, + graphene_point_t *pos) +{ + GskQuadCurve *self = (GskQuadCurve *) &curve->quad; + const graphene_point_t *c = self->coeffs; + + gsk_quad_curve_ensure_coefficients (self); + + *pos = GRAPHENE_POINT_INIT ((c[0].x * t + c[1].x) * t + c[2].x, + (c[0].y * t + c[1].y) * t + c[2].y); +} + +static void +gsk_quad_curve_get_tangent (const GskCurve *curve, + float t, + graphene_vec2_t *tangent) +{ + GskQuadCurve *self = (GskQuadCurve *) &curve->quad; + const graphene_point_t *c = self->coeffs; + + gsk_quad_curve_ensure_coefficients (self); + + graphene_vec2_init (tangent, + 2.0f * c[0].x * t + c[1].x, + 2.0f * c[0].y * t + c[1].y); + graphene_vec2_normalize (tangent, tangent); +} + + +static float gsk_cubic_curve_get_curvature (const GskCurve *curve, + float t); + +static float +gsk_quad_curve_get_curvature (const GskCurve *curve, + float t) +{ + return gsk_cubic_curve_get_curvature (curve, t); +} + +static void +gsk_quad_curve_reverse (const GskCurve *curve, + GskCurve *reverse) +{ + const GskCubicCurve *self = &curve->cubic; + + reverse->op = GSK_PATH_QUAD; + reverse->cubic.points[0] = self->points[2]; + reverse->cubic.points[1] = self->points[1]; + reverse->cubic.points[2] = self->points[0]; + reverse->cubic.has_coefficients = FALSE; +} + +static void +gsk_quad_curve_split (const GskCurve *curve, + float progress, + GskCurve *start, + GskCurve *end) +{ + GskQuadCurve *self = (GskQuadCurve *) &curve->quad; + const graphene_point_t *pts = self->points; + graphene_point_t ab, bc; + graphene_point_t final; + + graphene_point_interpolate (&pts[0], &pts[1], progress, &ab); + graphene_point_interpolate (&pts[1], &pts[2], progress, &bc); + graphene_point_interpolate (&ab, &bc, progress, &final); + + if (start) + gsk_quad_curve_init_from_points (&start->quad, (graphene_point_t[3]) { pts[0], ab, final }); + if (end) + gsk_quad_curve_init_from_points (&end->quad, (graphene_point_t[3]) { final, bc, pts[2] }); +} + +static void +gsk_quad_curve_segment (const GskCurve *curve, + float start, + float end, + GskCurve *segment) +{ + GskCurve tmp; + + gsk_quad_curve_split (curve, start, NULL, &tmp); + gsk_quad_curve_split (&tmp, (end - start) / (1.0f - start), segment, NULL); +} + +/* taken from Skia, including the very descriptive name */ +static gboolean +gsk_quad_curve_too_curvy (const GskQuadCurve *self, + float tolerance) +{ + const graphene_point_t *pts = self->points; + float dx, dy; + + dx = fabs (pts[1].x / 2 - (pts[0].x + pts[2].x) / 4); + dy = fabs (pts[1].y / 2 - (pts[0].y + pts[2].y) / 4); + + return MAX (dx, dy) > tolerance; +} + +static gboolean +gsk_quad_curve_decompose_step (const GskCurve *curve, + float start_progress, + float end_progress, + float tolerance, + GskCurveAddLineFunc add_line_func, + gpointer user_data) +{ + const GskQuadCurve *self = &curve->quad; + GskCurve left, right; + float mid_progress; + + if (!gsk_quad_curve_too_curvy (self, tolerance)) + return add_line_func (&self->points[0], &self->points[2], start_progress, end_progress, GSK_CURVE_LINE_REASON_STRAIGHT, user_data); + if (end_progress - start_progress <= MIN_PROGRESS) + return add_line_func (&self->points[0], &self->points[2], start_progress, end_progress, GSK_CURVE_LINE_REASON_SHORT, user_data); + + gsk_quad_curve_split ((const GskCurve *) self, 0.5, &left, &right); + mid_progress = (start_progress + end_progress) / 2; + + return gsk_quad_curve_decompose_step (&left, start_progress, mid_progress, tolerance, add_line_func, user_data) + && gsk_quad_curve_decompose_step (&right, mid_progress, end_progress, tolerance, add_line_func, user_data); +} + +static gboolean +gsk_quad_curve_decompose (const GskCurve *curve, + float tolerance, + GskCurveAddLineFunc add_line_func, + gpointer user_data) +{ + return gsk_quad_curve_decompose_step (curve, 0.0, 1.0, tolerance, add_line_func, user_data); +} + +typedef struct +{ + GskCurveAddCurveFunc add_curve; + gpointer user_data; +} AddLineData; + +static gboolean +gsk_curve_add_line_cb (const graphene_point_t *from, + const graphene_point_t *to, + float from_progress, + float to_progress, + GskCurveLineReason reason, + gpointer user_data) +{ + AddLineData *data = user_data; + graphene_point_t p[2] = { *from, *to }; + + return data->add_curve (GSK_PATH_LINE, p, 2, data->user_data); +} + +static gboolean +gsk_quad_curve_decompose_curve (const GskCurve *curve, + GskPathForeachFlags flags, + float tolerance, + GskCurveAddCurveFunc add_curve_func, + gpointer user_data) +{ + const GskQuadCurve *self = &curve->quad; + + if (flags & GSK_PATH_FOREACH_ALLOW_QUAD) + return add_curve_func (GSK_PATH_QUAD, self->points, 3, user_data); + else if (flags & GSK_PATH_FOREACH_ALLOW_CUBIC) + { + GskCurve c; + + gsk_curve_elevate (curve, &c); + return add_curve_func (GSK_PATH_CUBIC, c.cubic.points, 4, user_data); + } + else + { + return gsk_quad_curve_decompose (curve, + tolerance, + gsk_curve_add_line_cb, + &(AddLineData) { add_curve_func, user_data }); + } +} + +static void +gsk_quad_curve_get_bounds (const GskCurve *curve, + GskBoundingBox *bounds) +{ + const GskQuadCurve *self = &curve->quad; + const graphene_point_t *pts = self->points; + + gsk_bounding_box_init (bounds, &pts[0], &pts[2]); + gsk_bounding_box_expand (bounds, &pts[1]); +} + +/* Solve P' = 0 where P is + * P = (1-t)^2*pa + 2*t*(1-t)*pb + t^2*pc + */ +static int +get_quadratic_extrema (float pa, float pb, float pc, float t[1]) +{ + float d = pa - 2 * pb + pc; + + if (fabs (d) > 0.0001) + { + t[0] = (pa - pb) / d; + return 1; + } + + return 0; +} + +static void +gsk_quad_curve_get_tight_bounds (const GskCurve *curve, + GskBoundingBox *bounds) +{ + const GskQuadCurve *self = &curve->quad; + const graphene_point_t *pts = self->points; + float t[4]; + int n; + + gsk_bounding_box_init (bounds, &pts[0], &pts[2]); + + n = 0; + n += get_quadratic_extrema (pts[0].x, pts[1].x, pts[2].x, &t[n]); + n += get_quadratic_extrema (pts[0].y, pts[1].y, pts[2].y, &t[n]); + + for (int i = 0; i < n; i++) + { + graphene_point_t p; + + gsk_quad_curve_get_point (curve, t[i], &p); + gsk_bounding_box_expand (bounds, &p); + } +} + +static const GskCurveClass GSK_QUAD_CURVE_CLASS = { + gsk_quad_curve_init, + gsk_quad_curve_init_foreach, + gsk_quad_curve_print, + gsk_quad_curve_pathop, + gsk_quad_curve_get_start_point, + gsk_quad_curve_get_end_point, + gsk_quad_curve_get_start_tangent, + gsk_quad_curve_get_end_tangent, + gsk_quad_curve_get_point, + gsk_quad_curve_get_tangent, + gsk_quad_curve_reverse, + gsk_quad_curve_get_curvature, + gsk_quad_curve_split, + gsk_quad_curve_segment, + gsk_quad_curve_decompose, + gsk_quad_curve_decompose_curve, + gsk_quad_curve_get_bounds, + gsk_quad_curve_get_tight_bounds, +}; + +/* }}} */ +/* {{{ Cubic */ + +static void +gsk_cubic_curve_ensure_coefficients (const GskCubicCurve *curve) +{ + GskCubicCurve *self = (GskCubicCurve *) curve; + const graphene_point_t *pts = &self->points[0]; + + if (self->has_coefficients) + return; + + self->coeffs[0] = GRAPHENE_POINT_INIT (pts[3].x - 3.0f * pts[2].x + 3.0f * pts[1].x - pts[0].x, + pts[3].y - 3.0f * pts[2].y + 3.0f * pts[1].y - pts[0].y); + self->coeffs[1] = GRAPHENE_POINT_INIT (3.0f * pts[2].x - 6.0f * pts[1].x + 3.0f * pts[0].x, + 3.0f * pts[2].y - 6.0f * pts[1].y + 3.0f * pts[0].y); + self->coeffs[2] = GRAPHENE_POINT_INIT (3.0f * pts[1].x - 3.0f * pts[0].x, + 3.0f * pts[1].y - 3.0f * pts[0].y); + self->coeffs[3] = pts[0]; + + self->has_coefficients = TRUE; +} + +static void +gsk_cubic_curve_init_from_points (GskCubicCurve *self, + const graphene_point_t pts[4]) +{ + self->op = GSK_PATH_CUBIC; + self->has_coefficients = FALSE; + memcpy (self->points, pts, sizeof (graphene_point_t) * 4); +} + +static void +gsk_cubic_curve_init (GskCurve *curve, + gskpathop op) +{ + GskCubicCurve *self = &curve->cubic; + + gsk_cubic_curve_init_from_points (self, gsk_pathop_points (op)); +} + +static void +gsk_cubic_curve_init_foreach (GskCurve *curve, + GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts) +{ + GskCubicCurve *self = &curve->cubic; + + g_assert (n_pts == 4); + + gsk_cubic_curve_init_from_points (self, pts); +} + +static void +gsk_cubic_curve_print (const GskCurve *curve, + GString *string) +{ + g_string_append_printf (string, + "M %f %f C %f %f %f %f %f %f", + curve->cubic.points[0].x, curve->cubic.points[0].y, + curve->cubic.points[1].x, curve->cubic.points[1].y, + curve->cubic.points[2].x, curve->cubic.points[2].y, + curve->cubic.points[3].x, curve->cubic.points[3].y); +} + +static gskpathop +gsk_cubic_curve_pathop (const GskCurve *curve) +{ + const GskCubicCurve *self = &curve->cubic; + + return gsk_pathop_encode (self->op, self->points); +} + +static const graphene_point_t * +gsk_cubic_curve_get_start_point (const GskCurve *curve) +{ + const GskCubicCurve *self = &curve->cubic; + + return &self->points[0]; +} + +static const graphene_point_t * +gsk_cubic_curve_get_end_point (const GskCurve *curve) +{ + const GskCubicCurve *self = &curve->cubic; + + return &self->points[3]; +} + +static void +gsk_cubic_curve_get_start_tangent (const GskCurve *curve, + graphene_vec2_t *tangent) +{ + const GskCubicCurve *self = &curve->cubic; + + if (graphene_point_near (&self->points[0], &self->points[1], 0.0001)) + { + if (graphene_point_near (&self->points[0], &self->points[2], 0.0001)) + get_tangent (&self->points[0], &self->points[3], tangent); + else + get_tangent (&self->points[0], &self->points[2], tangent); + } + else + get_tangent (&self->points[0], &self->points[1], tangent); +} + +static void +gsk_cubic_curve_get_end_tangent (const GskCurve *curve, + graphene_vec2_t *tangent) +{ + const GskCubicCurve *self = &curve->cubic; + + if (graphene_point_near (&self->points[2], &self->points[3], 0.0001)) + { + if (graphene_point_near (&self->points[1], &self->points[3], 0.0001)) + get_tangent (&self->points[0], &self->points[3], tangent); + else + get_tangent (&self->points[1], &self->points[3], tangent); + } + else + get_tangent (&self->points[2], &self->points[3], tangent); +} + +static void +gsk_cubic_curve_get_point (const GskCurve *curve, + float t, + graphene_point_t *pos) +{ + const GskCubicCurve *self = &curve->cubic; + const graphene_point_t *c = self->coeffs; + + gsk_cubic_curve_ensure_coefficients (self); + + *pos = GRAPHENE_POINT_INIT (((c[0].x * t + c[1].x) * t +c[2].x) * t + c[3].x, + ((c[0].y * t + c[1].y) * t +c[2].y) * t + c[3].y); +} + +static void +gsk_cubic_curve_get_tangent (const GskCurve *curve, + float t, + graphene_vec2_t *tangent) +{ + const GskCubicCurve *self = &curve->cubic; + const graphene_point_t *c = self->coeffs; + + gsk_cubic_curve_ensure_coefficients (self); + + graphene_vec2_init (tangent, + (3.0f * c[0].x * t + 2.0f * c[1].x) * t + c[2].x, + (3.0f * c[0].y * t + 2.0f * c[1].y) * t + c[2].y); + graphene_vec2_normalize (tangent, tangent); +} + +static void +gsk_cubic_curve_reverse (const GskCurve *curve, + GskCurve *reverse) +{ + const GskCubicCurve *self = &curve->cubic; + + reverse->op = GSK_PATH_CUBIC; + reverse->cubic.points[0] = self->points[3]; + reverse->cubic.points[1] = self->points[2]; + reverse->cubic.points[2] = self->points[1]; + reverse->cubic.points[3] = self->points[0]; + reverse->cubic.has_coefficients = FALSE; +} + +static void +gsk_curve_get_derivative (const GskCurve *curve, + GskCurve *deriv) +{ + switch (curve->op) + { + case GSK_PATH_LINE: + { + const GskLineCurve *self = &curve->line; + graphene_point_t p; + + p.x = self->points[1].x - self->points[0].x; + p.y = self->points[1].y - self->points[0].y; + + gsk_line_curve_init_from_points (&deriv->line, GSK_PATH_LINE, &p, &p); + } + break; + + case GSK_PATH_QUAD: + { + const GskQuadCurve *self = &curve->quad; + graphene_point_t p[2]; + + p[0].x = 2.f * (self->points[1].x - self->points[0].x); + p[0].y = 2.f * (self->points[1].y - self->points[0].y); + p[1].x = 2.f * (self->points[2].x - self->points[1].x); + p[1].y = 2.f * (self->points[2].y - self->points[1].y); + + gsk_line_curve_init_from_points (&deriv->line, GSK_PATH_LINE, &p[0], &p[1]); + } + break; + + case GSK_PATH_CUBIC: + { + const GskCubicCurve *self = &curve->cubic; + graphene_point_t p[3]; + + p[0].x = 3.f * (self->points[1].x - self->points[0].x); + p[0].y = 3.f * (self->points[1].y - self->points[0].y); + p[1].x = 3.f * (self->points[2].x - self->points[1].x); + p[1].y = 3.f * (self->points[2].y - self->points[1].y); + p[2].x = 3.f * (self->points[3].x - self->points[2].x); + p[2].y = 3.f * (self->points[3].y - self->points[2].y); + + gsk_quad_curve_init_from_points (&deriv->quad, p); + } + break; + + case GSK_PATH_MOVE: + case GSK_PATH_CLOSE: + default: + g_assert_not_reached (); + } +} + +static inline float +cross (const graphene_vec2_t *v1, + const graphene_vec2_t *v2) +{ + return graphene_vec2_get_x (v1) * graphene_vec2_get_y (v2) + - graphene_vec2_get_y (v1) * graphene_vec2_get_x (v2); +} + +static inline float +pow3 (float w) +{ + return w * w * w; +} + +static float +gsk_cubic_curve_get_curvature (const GskCurve *curve, + float t) +{ + GskCurve c1, c2; + graphene_point_t p, pp; + graphene_vec2_t d, dd; + float num, denom; + + gsk_curve_get_derivative (curve, &c1); + gsk_curve_get_derivative (&c1, &c2); + + gsk_curve_get_point (&c1, t, &p); + gsk_curve_get_point (&c2, t, &pp); + + graphene_vec2_init (&d, p.x, p.y); + graphene_vec2_init (&dd, pp.x, pp.y); + + num = cross (&d, &dd); + if (num == 0) + return 0; + + denom = pow3 (graphene_vec2_length (&d)); + if (denom == 0) + return 0; + + return num / denom; +} + +static void +gsk_cubic_curve_split (const GskCurve *curve, + float progress, + GskCurve *start, + GskCurve *end) +{ + const GskCubicCurve *self = &curve->cubic; + const graphene_point_t *pts = self->points; + graphene_point_t ab, bc, cd; + graphene_point_t abbc, bccd; + graphene_point_t final; + + graphene_point_interpolate (&pts[0], &pts[1], progress, &ab); + graphene_point_interpolate (&pts[1], &pts[2], progress, &bc); + graphene_point_interpolate (&pts[2], &pts[3], progress, &cd); + graphene_point_interpolate (&ab, &bc, progress, &abbc); + graphene_point_interpolate (&bc, &cd, progress, &bccd); + graphene_point_interpolate (&abbc, &bccd, progress, &final); + + if (start) + gsk_cubic_curve_init_from_points (&start->cubic, (graphene_point_t[4]) { pts[0], ab, abbc, final }); + if (end) + gsk_cubic_curve_init_from_points (&end->cubic, (graphene_point_t[4]) { final, bccd, cd, pts[3] }); +} + +static void +gsk_cubic_curve_segment (const GskCurve *curve, + float start, + float end, + GskCurve *segment) +{ + GskCurve tmp; + + gsk_cubic_curve_split (curve, start, NULL, &tmp); + gsk_cubic_curve_split (&tmp, (end - start) / (1.0f - start), segment, NULL); +} + +/* taken from Skia, including the very descriptive name */ +static gboolean +gsk_cubic_curve_too_curvy (const GskCubicCurve *self, + float tolerance) +{ + const graphene_point_t *pts = self->points; + graphene_point_t p; + + graphene_point_interpolate (&pts[0], &pts[3], 1.0f / 3, &p); + if (MAX (ABS (p.x - pts[1].x), ABS (p.y - pts[1].y)) > tolerance) + return TRUE; + + graphene_point_interpolate (&pts[0], &pts[3], 2.0f / 3, &p); + if (MAX (ABS (p.x - pts[2].x), ABS (p.y - pts[2].y)) > tolerance) + return TRUE; + + return FALSE; +} + +static gboolean +gsk_cubic_curve_decompose_step (const GskCurve *curve, + float start_progress, + float end_progress, + float tolerance, + GskCurveAddLineFunc add_line_func, + gpointer user_data) +{ + const GskCubicCurve *self = &curve->cubic; + GskCurve left, right; + float mid_progress; + + if (!gsk_cubic_curve_too_curvy (self, tolerance)) + return add_line_func (&self->points[0], &self->points[3], start_progress, end_progress, GSK_CURVE_LINE_REASON_STRAIGHT, user_data); + if (end_progress - start_progress <= MIN_PROGRESS) + return add_line_func (&self->points[0], &self->points[3], start_progress, end_progress, GSK_CURVE_LINE_REASON_SHORT, user_data); + + gsk_cubic_curve_split ((const GskCurve *) self, 0.5, &left, &right); + mid_progress = (start_progress + end_progress) / 2; + + return gsk_cubic_curve_decompose_step (&left, start_progress, mid_progress, tolerance, add_line_func, user_data) + && gsk_cubic_curve_decompose_step (&right, mid_progress, end_progress, tolerance, add_line_func, user_data); +} + +static gboolean +gsk_cubic_curve_decompose (const GskCurve *curve, + float tolerance, + GskCurveAddLineFunc add_line_func, + gpointer user_data) +{ + return gsk_cubic_curve_decompose_step (curve, 0.0, 1.0, tolerance, add_line_func, user_data); +} + +static gboolean +gsk_cubic_curve_decompose_curve (const GskCurve *curve, + GskPathForeachFlags flags, + float tolerance, + GskCurveAddCurveFunc add_curve_func, + gpointer user_data) +{ + const GskCubicCurve *self = &curve->cubic; + + if (flags & GSK_PATH_FOREACH_ALLOW_CUBIC) + return add_curve_func (GSK_PATH_CUBIC, self->points, 4, user_data); + + /* FIXME: Quadratic (or conic?) approximation */ + return gsk_cubic_curve_decompose (curve, + tolerance, + gsk_curve_add_line_cb, + &(AddLineData) { add_curve_func, user_data }); +} + +static void +gsk_cubic_curve_get_bounds (const GskCurve *curve, + GskBoundingBox *bounds) +{ + const GskCubicCurve *self = &curve->cubic; + const graphene_point_t *pts = self->points; + + gsk_bounding_box_init (bounds, &pts[0], &pts[3]); + gsk_bounding_box_expand (bounds, &pts[1]); + gsk_bounding_box_expand (bounds, &pts[2]); +} + +static inline gboolean +acceptable (float t) +{ + return 0 <= t && t <= 1; +} + +/* Solve P' = 0 where P is + * P = (1-t)^3*pa + 3*t*(1-t)^2*pb + 3*t^2*(1-t)*pc + t^3*pd + */ +static int +get_cubic_extrema (float pa, float pb, float pc, float pd, float t[2]) +{ + float a, b, c; + float d, tt; + int n = 0; + + a = 3 * (pd - 3*pc + 3*pb - pa); + b = 6 * (pc - 2*pb + pa); + c = 3 * (pb - pa); + + if (fabs (a) > 0.0001) + { + if (b*b > 4*a*c) + { + d = sqrt (b*b - 4*a*c); + tt = (-b + d)/(2*a); + if (acceptable (tt)) + t[n++] = tt; + tt = (-b - d)/(2*a); + if (acceptable (tt)) + t[n++] = tt; + } + else + { + tt = -b / (2*a); + if (acceptable (tt)) + t[n++] = tt; + } + } + else if (fabs (b) > 0.0001) + { + tt = -c / b; + if (acceptable (tt)) + t[n++] = tt; + } + + return n; +} + +static void +gsk_cubic_curve_get_tight_bounds (const GskCurve *curve, + GskBoundingBox *bounds) +{ + const GskCubicCurve *self = &curve->cubic; + const graphene_point_t *pts = self->points; + float t[4]; + int n; + + gsk_bounding_box_init (bounds, &pts[0], &pts[3]); + + n = 0; + n += get_cubic_extrema (pts[0].x, pts[1].x, pts[2].x, pts[3].x, &t[n]); + n += get_cubic_extrema (pts[0].y, pts[1].y, pts[2].y, pts[3].y, &t[n]); + + for (int i = 0; i < n; i++) + { + graphene_point_t p; + + gsk_cubic_curve_get_point (curve, t[i], &p); + gsk_bounding_box_expand (bounds, &p); + } +} + +static const GskCurveClass GSK_CUBIC_CURVE_CLASS = { + gsk_cubic_curve_init, + gsk_cubic_curve_init_foreach, + gsk_cubic_curve_print, + gsk_cubic_curve_pathop, + gsk_cubic_curve_get_start_point, + gsk_cubic_curve_get_end_point, + gsk_cubic_curve_get_start_tangent, + gsk_cubic_curve_get_end_tangent, + gsk_cubic_curve_get_point, + gsk_cubic_curve_get_tangent, + gsk_cubic_curve_reverse, + gsk_cubic_curve_get_curvature, + gsk_cubic_curve_split, + gsk_cubic_curve_segment, + gsk_cubic_curve_decompose, + gsk_cubic_curve_decompose_curve, + gsk_cubic_curve_get_bounds, + gsk_cubic_curve_get_tight_bounds, +}; + +/* }}} */ +/* {{{ API */ + +static const GskCurveClass * +get_class (GskPathOperation op) +{ + const GskCurveClass *klasses[] = { + [GSK_PATH_CLOSE] = &GSK_LINE_CURVE_CLASS, + [GSK_PATH_LINE] = &GSK_LINE_CURVE_CLASS, + [GSK_PATH_QUAD] = &GSK_QUAD_CURVE_CLASS, + [GSK_PATH_CUBIC] = &GSK_CUBIC_CURVE_CLASS, + }; + + g_assert (op < G_N_ELEMENTS (klasses) && klasses[op] != NULL); + + return klasses[op]; +} + +void +gsk_curve_init (GskCurve *curve, + gskpathop op) +{ + memset (curve, 0, sizeof (GskCurve)); + get_class (gsk_pathop_op (op))->init (curve, op); +} + +void +gsk_curve_init_foreach (GskCurve *curve, + GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts) +{ + memset (curve, 0, sizeof (GskCurve)); + get_class (op)->init_foreach (curve, op, pts, n_pts); +} + +void +gsk_curve_print (const GskCurve *curve, + GString *string) +{ + get_class (curve->op)->print (curve, string); +} + +char * +gsk_curve_to_string (const GskCurve *curve) +{ + GString *s = g_string_new (""); + gsk_curve_print (curve, s); + return g_string_free (s, FALSE); +} + +gskpathop +gsk_curve_pathop (const GskCurve *curve) +{ + return get_class (curve->op)->pathop (curve); +} + +const graphene_point_t * +gsk_curve_get_start_point (const GskCurve *curve) +{ + return get_class (curve->op)->get_start_point (curve); +} + +const graphene_point_t * +gsk_curve_get_end_point (const GskCurve *curve) +{ + return get_class (curve->op)->get_end_point (curve); +} + +void +gsk_curve_get_start_tangent (const GskCurve *curve, + graphene_vec2_t *tangent) +{ + get_class (curve->op)->get_start_tangent (curve, tangent); +} + +void +gsk_curve_get_end_tangent (const GskCurve *curve, + graphene_vec2_t *tangent) +{ + get_class (curve->op)->get_end_tangent (curve, tangent); +} + +void +gsk_curve_get_point (const GskCurve *curve, + float progress, + graphene_point_t *pos) +{ + get_class (curve->op)->get_point (curve, progress, pos); +} + +void +gsk_curve_get_tangent (const GskCurve *curve, + float progress, + graphene_vec2_t *tangent) +{ + get_class (curve->op)->get_tangent (curve, progress, tangent); +} + +float +gsk_curve_get_curvature (const GskCurve *curve, + float t, + graphene_point_t *center) +{ + float k; + + k = get_class (curve->op)->get_curvature (curve, t); + + if (center != NULL && k != 0) + { + graphene_point_t p; + graphene_vec2_t tangent; + float r; + + r = 1/k; + gsk_curve_get_point (curve, t, &p); + gsk_curve_get_tangent (curve, t, &tangent); + center->x = p.x - r * graphene_vec2_get_y (&tangent); + center->y = p.y + r * graphene_vec2_get_x (&tangent); + } + + return k; +} + +void +gsk_curve_reverse (const GskCurve *curve, + GskCurve *reverse) +{ + get_class (curve->op)->reverse (curve, reverse); +} + +void +gsk_curve_split (const GskCurve *curve, + float progress, + GskCurve *start, + GskCurve *end) +{ + get_class (curve->op)->split (curve, progress, start, end); +} + +void +gsk_curve_segment (const GskCurve *curve, + float start, + float end, + GskCurve *segment) +{ + if (start <= 0 && end >= 1) + { + *segment = *curve; + return; + } + + get_class (curve->op)->segment (curve, start, end, segment); +} + +gboolean +gsk_curve_decompose (const GskCurve *curve, + float tolerance, + GskCurveAddLineFunc add_line_func, + gpointer user_data) +{ + return get_class (curve->op)->decompose (curve, tolerance, add_line_func, user_data); +} + +gboolean +gsk_curve_decompose_curve (const GskCurve *curve, + GskPathForeachFlags flags, + float tolerance, + GskCurveAddCurveFunc add_curve_func, + gpointer user_data) +{ + return get_class (curve->op)->decompose_curve (curve, flags, tolerance, add_curve_func, user_data); +} + +void +gsk_curve_get_bounds (const GskCurve *curve, + GskBoundingBox *bounds) +{ + get_class (curve->op)->get_bounds (curve, bounds); +} + +void +gsk_curve_get_tight_bounds (const GskCurve *curve, + GskBoundingBox *bounds) +{ + get_class (curve->op)->get_tight_bounds (curve, bounds); +} + +static inline int +line_get_crossing (const graphene_point_t *p, + const graphene_point_t *p1, + const graphene_point_t *p2) +{ + if (p1->y <= p->y) + { + if (p2->y > p->y) + { + if ((p2->x - p1->x) * (p->y - p1->y) - (p->x - p1->x) * (p2->y - p1->y) > 0) + return 1; + } + } + else if (p2->y <= p->y) + { + if ((p2->x - p1->x) * (p->y - p1->y) - (p->x - p1->x) * (p2->y - p1->y) < 0) + return -1; + } + + return 0; +} + +int +gsk_curve_get_crossing (const GskCurve *curve, + const graphene_point_t *point) +{ + GskBoundingBox bounds; + GskCurve c1, c2; + + if (curve->op == GSK_PATH_LINE || curve->op == GSK_PATH_CLOSE) + return line_get_crossing (point, gsk_curve_get_start_point (curve), gsk_curve_get_end_point (curve)); + + gsk_curve_get_bounds (curve, &bounds); + + if (bounds.max.y < point->y || bounds.min.y > point->y || bounds.max.x < point->x) + return 0; + + if (bounds.min.x > point->x) + return line_get_crossing (point, gsk_curve_get_start_point (curve), gsk_curve_get_end_point (curve)); + + if (graphene_point_distance (&bounds.min, &bounds.max, NULL, NULL) < 0.001) + return line_get_crossing (point, gsk_curve_get_start_point (curve), gsk_curve_get_end_point (curve)); + + gsk_curve_split (curve, 0.5, &c1, &c2); + + return gsk_curve_get_crossing (&c1, point) + gsk_curve_get_crossing (&c2, point); +} + +static gboolean +project_point_onto_line (const GskCurve *curve, + const graphene_point_t *point, + float threshold, + float *out_distance, + float *out_t) +{ + const graphene_point_t *a = gsk_curve_get_start_point (curve); + const graphene_point_t *b = gsk_curve_get_end_point (curve); + graphene_vec2_t n, ap; + graphene_point_t p; + + if (graphene_point_equal (a, b)) + { + *out_t = 0; + *out_distance = graphene_point_distance (point, a, NULL, NULL); + } + else + { + graphene_vec2_init (&n, b->x - a->x, b->y - a->y); + graphene_vec2_init (&ap, point->x - a->x, point->y - a->y); + + *out_t = graphene_vec2_dot (&n, &ap) / graphene_vec2_dot (&n, &n); + *out_t = CLAMP (*out_t, 0, 1); + + graphene_point_interpolate (a, b, *out_t, &p); + *out_distance = graphene_point_distance (point, &p, NULL, NULL); + } + + return *out_distance <= threshold; +} + +static float +get_segment_bounding_sphere (const GskCurve *curve, + float t1, + float t2, + graphene_point_t *center) +{ + GskCurve c; + GskBoundingBox bounds; + + gsk_curve_segment (curve, t1, t2, &c); + gsk_curve_get_tight_bounds (&c, &bounds); + graphene_point_interpolate (&bounds.min, &bounds.max, 0.5, center); + return graphene_point_distance (center, &bounds.min, NULL, NULL); +} + +static gboolean +find_closest_point (const GskCurve *curve, + const graphene_point_t *point, + float threshold, + float t1, + float t2, + float *out_distance, + float *out_t) +{ + graphene_point_t center; + float radius; + float t, d, nt; + + radius = get_segment_bounding_sphere (curve, t1, t2, ¢er); + if (graphene_point_distance (¢er, point, NULL, NULL) > threshold + radius) + return FALSE; + + d = INFINITY; + t = (t1 + t2) / 2; + + if (radius < 1) + { + graphene_point_t p; + gsk_curve_get_point (curve, t, &p); + d = graphene_point_distance (point, &p, NULL, NULL); + nt = t; + } + else + { + float dd, tt; + + dd = INFINITY; + nt = 0; + + if (find_closest_point (curve, point, threshold, t1, t, &dd, &tt)) + { + d = dd; + nt = tt; + } + + if (find_closest_point (curve, point, MIN (dd, threshold), t, t2, &dd, &tt)) + { + d = dd; + nt = tt; + } + } + + if (d < threshold) + { + *out_distance = d; + *out_t = nt; + return TRUE; + } + else + { + *out_distance = INFINITY; + *out_t = 0; + return FALSE; + } +} + +gboolean +gsk_curve_get_closest_point (const GskCurve *curve, + const graphene_point_t *point, + float threshold, + float *out_dist, + float *out_t) +{ + if (curve->op == GSK_PATH_LINE || curve->op == GSK_PATH_CLOSE) + return project_point_onto_line (curve, point, threshold, out_dist, out_t); + else + return find_closest_point (curve, point, threshold, 0, 1, out_dist, out_t); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/gsk/gskcurveprivate.h b/gsk/gskcurveprivate.h new file mode 100644 index 0000000000..ef95231077 --- /dev/null +++ b/gsk/gskcurveprivate.h @@ -0,0 +1,157 @@ +/* + * 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 + */ + + +#pragma once + +#include "gskpathopprivate.h" +#include "gskpath.h" +#include "gskboundingboxprivate.h" + +G_BEGIN_DECLS + +typedef gpointer gskpathop; + +typedef union _GskCurve GskCurve; + +typedef struct _GskLineCurve GskLineCurve; +typedef struct _GskQuadCurve GskQuadCurve; +typedef struct _GskCubicCurve GskCubicCurve; + +struct _GskLineCurve +{ + GskPathOperation op; + + gboolean padding; + + graphene_point_t points[2]; +}; + +struct _GskQuadCurve +{ + GskPathOperation op; + + gboolean has_coefficients; + + graphene_point_t points[3]; + + graphene_point_t coeffs[3]; +}; + +struct _GskCubicCurve +{ + GskPathOperation op; + + gboolean has_coefficients; + + graphene_point_t points[4]; + + graphene_point_t coeffs[4]; +}; + +union _GskCurve +{ + GskPathOperation op; + GskLineCurve line; + GskQuadCurve quad; + GskCubicCurve cubic; +}; + +typedef enum { + GSK_CURVE_LINE_REASON_STRAIGHT, + GSK_CURVE_LINE_REASON_SHORT +} GskCurveLineReason; + +typedef gboolean (* GskCurveAddLineFunc) (const graphene_point_t *from, + const graphene_point_t *to, + float from_progress, + float to_progress, + GskCurveLineReason reason, + gpointer user_data); + +typedef gboolean (* GskCurveAddCurveFunc) (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + gpointer user_data); + +void gsk_curve_init (GskCurve *curve, + gskpathop op); +void gsk_curve_init_foreach (GskCurve *curve, + GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts); + +void gsk_curve_print (const GskCurve *curve, + GString *string); + +char * gsk_curve_to_string (const GskCurve *curve); + +gskpathop gsk_curve_pathop (const GskCurve *curve); +const graphene_point_t *gsk_curve_get_start_point (const GskCurve *curve); +const graphene_point_t *gsk_curve_get_end_point (const GskCurve *curve); +void gsk_curve_get_start_tangent (const GskCurve *curve, + graphene_vec2_t *tangent); +void gsk_curve_get_end_tangent (const GskCurve *curve, + graphene_vec2_t *tangent); +void gsk_curve_get_point (const GskCurve *curve, + float progress, + graphene_point_t *pos); +void gsk_curve_get_tangent (const GskCurve *curve, + float progress, + graphene_vec2_t *tangent); +void gsk_curve_reverse (const GskCurve *curve, + GskCurve *reverse); +void gsk_curve_split (const GskCurve *curve, + float progress, + GskCurve *start, + GskCurve *end); +void gsk_curve_segment (const GskCurve *curve, + float start, + float end, + GskCurve *segment); +gboolean gsk_curve_decompose (const GskCurve *curve, + float tolerance, + GskCurveAddLineFunc add_line_func, + gpointer user_data); +gboolean gsk_curve_decompose_curve (const GskCurve *curve, + GskPathForeachFlags flags, + float tolerance, + GskCurveAddCurveFunc add_curve_func, + gpointer user_data); + +#define gsk_curve_builder_to(curve, builder) gsk_path_builder_pathop_to ((builder), gsk_curve_pathop (curve)) + +float gsk_curve_get_curvature (const GskCurve *curve, + float t, + graphene_point_t *center); +void gsk_curve_get_bounds (const GskCurve *curve, + GskBoundingBox *bounds); +void gsk_curve_get_tight_bounds (const GskCurve *curve, + GskBoundingBox *bounds); + +int gsk_curve_get_crossing (const GskCurve *curve, + const graphene_point_t *point); +gboolean gsk_curve_get_closest_point (const GskCurve *curve, + const graphene_point_t *point, + float threshold, + float *out_dist, + float *out_t); + +G_END_DECLS + diff --git a/gsk/gskenums.h b/gsk/gskenums.h index 5b0ac88193..37553ae81a 100644 --- a/gsk/gskenums.h +++ b/gsk/gskenums.h @@ -170,6 +170,81 @@ typedef enum { GSK_CORNER_BOTTOM_LEFT } GskCorner; +/** + * GskFillRule: + * @GSK_FILL_RULE_WINDING: If the path crosses the ray from + * left-to-right, counts +1. If the path crosses the ray + * from right to left, counts -1. (Left and right are determined + * from the perspective of looking along the ray from the starting + * point.) If the total count is non-zero, the point will be filled. + * @GSK_FILL_RULE_EVEN_ODD: Counts the total number of + * intersections, without regard to the orientation of the contour. If + * the total number of intersections is odd, the point will be + * filled. + * + * `GskFillRule` is used to select how paths are filled. + * + * Whether or not a point is included in the fill is determined by taking + * a ray from that point to infinity and looking at intersections with the + * path. The ray can be in any direction, as long as it doesn't pass through + * the end point of a segment or have a tricky intersection such as + * intersecting tangent to the path. + * + * (Note that filling is not actually implemented in this way. This + * is just a description of the rule that is applied.) + * + * New entries may be added in future versions. + * + * Since: 4.14 + */ +typedef enum { + GSK_FILL_RULE_WINDING, + GSK_FILL_RULE_EVEN_ODD +} GskFillRule; + +/** + * GskPathOperation: + * @GSK_PATH_MOVE: A move-to operation, with 1 point describing the target point. + * @GSK_PATH_CLOSE: A close operation ending the current contour with a line back + * to the starting point. Two points describe the start and end of the line. + * @GSK_PATH_LINE: A line-to operation, with 2 points describing the start and + * end point of a straight line. + * @GSK_PATH_QUAD: A curve-to operation describing a quadratic Bézier curve + * with 3 points describing the start point, the control point and the end + * point of the curve. + * @GSK_PATH_CUBIC: A curve-to operation describing a cubic Bézier curve with 4 + * points describing the start point, the two control points and the end point + * of the curve. + * + * Path operations are used to described segments of a `GskPath`. + * + * More values may be added in the future. + * + * Since: 4.14 + */ +typedef enum { + GSK_PATH_MOVE, + GSK_PATH_CLOSE, + GSK_PATH_LINE, + GSK_PATH_QUAD, + GSK_PATH_CUBIC, +} GskPathOperation; + +/** + * GskPathDirection: + * @GSK_PATH_START: The side that leads to the start of the path + * @GSK_PATH_END: The side that leads to the end of the path + * + * The values of the `GskPathDirection` enum are used to pick one + * of the two sides of the path that at a given point on the path. + * + * Since: 4.14 + */ +typedef enum { + GSK_PATH_START, + GSK_PATH_END +} GskPathDirection; + /** * GskSerializationError: * @GSK_SERIALIZATION_UNSUPPORTED_FORMAT: The format can not be identified @@ -274,4 +349,3 @@ typedef enum GSK_MASK_MODE_LUMINANCE, GSK_MASK_MODE_INVERTED_LUMINANCE } GskMaskMode; - diff --git a/gsk/gskpath.c b/gsk/gskpath.c new file mode 100644 index 0000000000..94f1eb3eb1 --- /dev/null +++ b/gsk/gskpath.c @@ -0,0 +1,1277 @@ +/* + * 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 "config.h" + +#include "gskpathprivate.h" + +#include "gskcurveprivate.h" +#include "gskpathbuilder.h" +#include "gskpathpointprivate.h" +#include "gsksplineprivate.h" + +/** + * GskPath: + * + * A `GskPath` describes lines and curves that are more complex + * than simple rectangles. Paths can used for rendering (filling or + * stroking) and for animations (e.g. as trajectories). + * + * `GskPath` is an immutable, opaque, reference-counted struct. + * After creation, you cannot change the types it represents. + * Instead, new `GskPath` objects have to be created. + * + * The [struct@Gsk.PathBuilder] structure is meant to help in this endeavor. + * + * Conceptually, a path consists of zero or more contours (continous, connected + * curves), each of which may or may not be closed. Contours are typically + * constructed from Bézier segments. + */ + +struct _GskPath +{ + /*< private >*/ + guint ref_count; + + GskPathFlags flags; + + gsize n_contours; + GskContour *contours[]; + /* followed by the contours data */ +}; + +G_DEFINE_BOXED_TYPE (GskPath, gsk_path, gsk_path_ref, gsk_path_unref) + +GskPath * +gsk_path_new_from_contours (const GSList *contours) +{ + GskPath *path; + const GSList *l; + gsize size; + gsize n_contours; + guint8 *contour_data; + GskPathFlags flags; + + flags = GSK_PATH_CLOSED | GSK_PATH_FLAT; + size = 0; + n_contours = 0; + for (l = contours; l; l = l->next) + { + GskContour *contour = l->data; + + n_contours++; + size += sizeof (GskContour *); + size += gsk_contour_get_size (contour); + flags &= gsk_contour_get_flags (contour); + } + + path = g_malloc0 (sizeof (GskPath) + size); + path->ref_count = 1; + path->flags = flags; + path->n_contours = n_contours; + contour_data = (guint8 *) &path->contours[n_contours]; + n_contours = 0; + + for (l = contours; l; l = l->next) + { + GskContour *contour = l->data; + + path->contours[n_contours] = (GskContour *) contour_data; + gsk_contour_copy ((GskContour *) contour_data, contour); + size = gsk_contour_get_size (contour); + contour_data += size; + n_contours++; + } + + return path; +} + +/** + * gsk_path_ref: + * @self: a `GskPath` + * + * Increases the reference count of a `GskPath` by one. + * + * Returns: the passed in `GskPath`. + * + * Since: 4.14 + */ +GskPath * +gsk_path_ref (GskPath *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + self->ref_count++; + + return self; +} + +/** + * gsk_path_unref: + * @self: a `GskPath` + * + * Decreases the reference count of a `GskPath` by one. + * + * If the resulting reference count is zero, frees the path. + * + * Since: 4.14 + */ +void +gsk_path_unref (GskPath *self) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (self->ref_count > 0); + + self->ref_count--; + if (self->ref_count > 0) + return; + + g_free (self); +} + +const GskContour * +gsk_path_get_contour (const GskPath *self, + gsize i) +{ + if (i < self->n_contours) + return self->contours[i]; + else + return NULL; +} + +GskPathFlags +gsk_path_get_flags (const GskPath *self) +{ + return self->flags; +} + +/** + * gsk_path_print: + * @self: a `GskPath` + * @string: The string to print into + * + * Converts @self into a human-readable string representation suitable + * for printing. + * + * The string is compatible with + * [SVG path syntax](https://www.w3.org/TR/SVG11/paths.html#PathData). + * + * Since: 4.14 + */ +void +gsk_path_print (GskPath *self, + GString *string) +{ + gsize i; + + g_return_if_fail (self != NULL); + g_return_if_fail (string != NULL); + + for (i = 0; i < self->n_contours; i++) + { + if (i > 0) + g_string_append_c (string, ' '); + gsk_contour_print (self->contours[i], string); + } +} + +/** + * gsk_path_to_string: + * @self: a `GskPath` + * + * Converts the path into a string that is suitable for printing. + * + * You can use this function in a debugger to get a quick overview + * of the path. + * + * This is a wrapper around [method@Gsk.Path.print], see that function + * for details. + * + * Returns: A new string for @self + * + * Since: 4.14 + */ +char * +gsk_path_to_string (GskPath *self) +{ + GString *string; + + g_return_val_if_fail (self != NULL, NULL); + + string = g_string_new (""); + + gsk_path_print (self, string); + + return g_string_free (string, FALSE); +} + +static gboolean +gsk_path_to_cairo_add_op (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + gpointer cr) +{ + switch (op) + { + case GSK_PATH_MOVE: + cairo_move_to (cr, pts[0].x, pts[0].y); + break; + + case GSK_PATH_CLOSE: + cairo_close_path (cr); + break; + + case GSK_PATH_LINE: + cairo_line_to (cr, pts[1].x, pts[1].y); + break; + + case GSK_PATH_QUAD: + { + double x, y; + cairo_get_current_point (cr, &x, &y); + cairo_curve_to (cr, + 1/3.f * x + 2/3.f * pts[1].x, 1/3.f * y + 2/3.f * pts[1].y, + 2/3.f * pts[1].x + 1/3.f * pts[2].x, 2/3.f * pts[1].y + 1/3.f * pts[2].y, + pts[2].x, pts[2].y); + } + break; + + case GSK_PATH_CUBIC: + cairo_curve_to (cr, pts[1].x, pts[1].y, pts[2].x, pts[2].y, pts[3].x, pts[3].y); + break; + + default: + g_assert_not_reached (); + return FALSE; + } + + return TRUE; +} + +/** + * gsk_path_to_cairo: + * @self: a `GskPath` + * @cr: a cairo context + * + * Appends the given @path to the given cairo context for drawing + * with Cairo. + * + * This may cause some suboptimal conversions to be performed as + * Cairo does not support all features of `GskPath`. + * + * This function does not clear the existing Cairo path. Call + * cairo_new_path() if you want this. + * + * Since: 4.14 + */ +void +gsk_path_to_cairo (GskPath *self, + cairo_t *cr) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (cr != NULL); + + gsk_path_foreach_with_tolerance (self, + GSK_PATH_FOREACH_ALLOW_CUBIC, + cairo_get_tolerance (cr), + gsk_path_to_cairo_add_op, + cr); +} + +/*< private > + * gsk_path_get_n_contours: + * @path: a `GskPath` + * + * Gets the number of contours @path is composed out of. + * + * Returns: the number of contours in @path + */ +gsize +gsk_path_get_n_contours (const GskPath *self) +{ + return self->n_contours; +} + +/** + * gsk_path_is_empty: + * @self: a `GskPath` + * + * Checks if the path is empty, i.e. contains no lines or curves. + * + * Returns: `TRUE` if the path is empty + * + * Since: 4.14 + */ +gboolean +gsk_path_is_empty (GskPath *self) +{ + g_return_val_if_fail (self != NULL, FALSE); + + return self->n_contours == 0; +} + +/** + * gsk_path_is_closed: + * @self: a `GskPath` + * + * Returns if the path represents a single closed + * contour. + * + * Returns: `TRUE` if the path is closed + * + * Since: 4.14 + */ +gboolean +gsk_path_is_closed (GskPath *self) +{ + g_return_val_if_fail (self != NULL, FALSE); + + /* XXX: is the empty path closed? Currently it's not */ + if (self->n_contours != 1) + return FALSE; + + return gsk_contour_get_flags (self->contours[0]) & GSK_PATH_CLOSED ? TRUE : FALSE; +} + +/** + * gsk_path_get_bounds: + * @self: a `GskPath` + * @bounds: (out caller-allocates): the bounds of the given path + * + * Computes the bounds of the given path. + * + * The returned bounds may be larger than necessary, because this + * function aims to be fast, not accurate. The bounds are guaranteed + * to contain the path. + * + * It is possible that the returned rectangle has 0 width and/or height. + * This can happen when the path only describes a point or an + * axis-aligned line. + * + * If the path is empty, `FALSE` is returned and @bounds are set to + * graphene_rect_zero(). This is different from the case where the path + * is a single point at the origin, where the @bounds will also be set to + * the zero rectangle but 0 will be returned. + * + * Returns: `TRUE` if the path has bounds, `FALSE` if the path is known + * to be empty and have no bounds. + * + * Since: 4.14 + */ +gboolean +gsk_path_get_bounds (GskPath *self, + graphene_rect_t *bounds) +{ + GskBoundingBox b; + gsize i; + + g_return_val_if_fail (self != NULL, FALSE); + g_return_val_if_fail (bounds != NULL, FALSE); + + if (self->n_contours == 0) + { + graphene_rect_init_from_rect (bounds, graphene_rect_zero ()); + return FALSE; + } + + gsk_contour_get_bounds (self->contours[0], &b); + + for (i = 1; i < self->n_contours; i++) + { + GskBoundingBox bb; + + gsk_contour_get_bounds (self->contours[i], &bb); + gsk_bounding_box_union (&b, &bb, &b); + } + + gsk_bounding_box_to_rect (&b, bounds); + + return TRUE; +} + +/** + * gsk_path_in_fill: + * @self: a `GskPath` + * @point: the point to test + * @fill_rule: the fill rule to follow + * + * Returns whether the given point is inside the area + * that would be affected if the path was filled according + * to @fill_rule. + * + * Note that this function assumes that filling a contour + * implicitly closes it. + * + * Returns: `TRUE` if @point is inside + * + * Since: 4.14 + */ +gboolean +gsk_path_in_fill (GskPath *self, + const graphene_point_t *point, + GskFillRule fill_rule) +{ + int winding = 0; + + for (int i = 0; i < self->n_contours; i++) + winding += gsk_contour_get_winding (self->contours[i], point); + + switch (fill_rule) + { + case GSK_FILL_RULE_EVEN_ODD: + return winding & 1; + case GSK_FILL_RULE_WINDING: + return winding != 0; + default: + g_assert_not_reached (); + } +} + +/** + * gsk_path_get_closest_point: + * @self: a `GskPath` + * @point: the point + * @threshold: maximum allowed distance + * @result: (out caller-allocates): return location for the closest point + * + * Computes the closest point on the path to the given point + * and sets the @result to it. + * + * If there is no point closer than the given threshold, + * `FALSE` is returned. + * + * Returns: `TRUE` if @point was set to the closest point + * on @self, `FALSE` if no point is closer than @threshold + * + * Since: 4.14 + */ +gboolean +gsk_path_get_closest_point (GskPath *self, + const graphene_point_t *point, + float threshold, + GskPathPoint *result) +{ + GskRealPathPoint *res = (GskRealPathPoint *) result; + gboolean found; + + g_return_val_if_fail (self != NULL, FALSE); + g_return_val_if_fail (point != NULL, FALSE); + g_return_val_if_fail (threshold >= 0, FALSE); + g_return_val_if_fail (result != NULL, FALSE); + + found = FALSE; + + for (int i = 0; i < self->n_contours; i++) + { + float distance; + + if (gsk_contour_get_closest_point (self->contours[i], point, threshold, res, &distance)) + { + found = TRUE; + res->contour = i; + threshold = distance; + } + } + + return found; +} + +/** + * gsk_path_foreach: + * @self: a `GskPath` + * @flags: flags to pass to the foreach function. See [flags@Gsk.PathForeachFlags] + * for details about flags + * @func: (scope call) (closure user_data): the function to call for operations + * @user_data: (nullable): user data passed to @func + * + * Calls @func for every operation of the path. + * + * Note that this only approximates @self, because paths can contain + * optimizations for various specialized contours, and depending on + * the @flags, the path may be decomposed into simpler curves than + * the ones that it contained originally. + * + * Returns: `FALSE` if @func returned FALSE`, `TRUE` otherwise. + * + * Since: 4.14 + */ +gboolean +gsk_path_foreach (GskPath *self, + GskPathForeachFlags flags, + GskPathForeachFunc func, + gpointer user_data) +{ + g_return_val_if_fail (self != NULL, FALSE); + g_return_val_if_fail (func, FALSE); + + return gsk_path_foreach_with_tolerance (self, + flags, + GSK_PATH_TOLERANCE_DEFAULT, + func, + user_data); +} + +typedef struct _GskPathForeachTrampoline GskPathForeachTrampoline; +struct _GskPathForeachTrampoline +{ + GskPathForeachFlags flags; + double tolerance; + GskPathForeachFunc func; + gpointer user_data; +}; + +static gboolean +gsk_path_foreach_trampoline_add_line (const graphene_point_t *from, + const graphene_point_t *to, + float from_progress, + float to_progress, + GskCurveLineReason reason, + gpointer data) +{ + GskPathForeachTrampoline *trampoline = data; + + return trampoline->func (GSK_PATH_LINE, + (graphene_point_t[2]) { *from, *to }, + 2, + trampoline->user_data); +} + +static gboolean +gsk_path_foreach_trampoline_add_curve (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + gpointer data) +{ + GskPathForeachTrampoline *trampoline = data; + + return trampoline->func (op, pts, n_pts, trampoline->user_data); +} + +static gboolean +gsk_path_foreach_trampoline (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + gpointer data) +{ + GskPathForeachTrampoline *trampoline = data; + + switch (op) + { + case GSK_PATH_MOVE: + case GSK_PATH_CLOSE: + case GSK_PATH_LINE: + return trampoline->func (op, pts, n_pts, trampoline->user_data); + + case GSK_PATH_QUAD: + { + GskCurve curve; + + if (trampoline->flags & GSK_PATH_FOREACH_ALLOW_QUAD) + return trampoline->func (op, pts, n_pts, trampoline->user_data); + else if (trampoline->flags & GSK_PATH_FOREACH_ALLOW_CUBIC) + { + return trampoline->func (GSK_PATH_CUBIC, + (graphene_point_t[4]) { + pts[0], + GRAPHENE_POINT_INIT ((pts[0].x + 2 * pts[1].x) / 3, + (pts[0].y + 2 * pts[1].y) / 3), + GRAPHENE_POINT_INIT ((pts[2].x + 2 * pts[1].x) / 3, + (pts[2].y + 2 * pts[1].y) / 3), + pts[2], + }, + 4, + trampoline->user_data); + } + + gsk_curve_init (&curve, gsk_pathop_encode (GSK_PATH_QUAD, pts)); + return gsk_curve_decompose (&curve, + trampoline->tolerance, + gsk_path_foreach_trampoline_add_line, + trampoline); + } + + case GSK_PATH_CUBIC: + { + GskCurve curve; + + if (trampoline->flags & GSK_PATH_FOREACH_ALLOW_CUBIC) + return trampoline->func (op, pts, n_pts, trampoline->user_data); + + gsk_curve_init (&curve, gsk_pathop_encode (GSK_PATH_CUBIC, pts)); + if (trampoline->flags & GSK_PATH_FOREACH_ALLOW_QUAD) + return gsk_curve_decompose_curve (&curve, + trampoline->flags, + trampoline->tolerance, + gsk_path_foreach_trampoline_add_curve, + trampoline); + + return gsk_curve_decompose (&curve, + trampoline->tolerance, + gsk_path_foreach_trampoline_add_line, + trampoline); + } + + default: + g_assert_not_reached (); + return FALSE; + } +} + +#define ALLOW_ANY (GSK_PATH_FOREACH_ALLOW_QUAD| \ + GSK_PATH_FOREACH_ALLOW_CUBIC) + +gboolean +gsk_path_foreach_with_tolerance (GskPath *self, + GskPathForeachFlags flags, + double tolerance, + GskPathForeachFunc func, + gpointer user_data) +{ + GskPathForeachTrampoline trampoline; + gsize i; + + /* If we need to massage the data, set up a trampoline here */ + if ((flags & ALLOW_ANY) != ALLOW_ANY) + { + trampoline = (GskPathForeachTrampoline) { flags, tolerance, func, user_data }; + func = gsk_path_foreach_trampoline; + user_data = &trampoline; + } + + for (i = 0; i < self->n_contours; i++) + { + if (!gsk_contour_foreach (self->contours[i], tolerance, func, user_data)) + return FALSE; + } + + return TRUE; +} + +/* path parser and utilities */ + +static void +skip_whitespace (const char **p) +{ + while (g_ascii_isspace (**p)) + (*p)++; +} + +static void +skip_optional_comma (const char **p) +{ + skip_whitespace (p); + if (**p == ',') + (*p)++; +} + +static gboolean +parse_number (const char **p, + double *c) +{ + char *e; + *c = g_ascii_strtod (*p, &e); + if (e == *p) + return FALSE; + *p = e; + skip_optional_comma (p); + return TRUE; +} + +static gboolean +parse_coordinate (const char **p, + double *c) +{ + return parse_number (p, c); +} + +static gboolean +parse_coordinate_pair (const char **p, + double *x, + double *y) +{ + double xx, yy; + const char *o = *p; + + if (!parse_coordinate (p, &xx)) + { + *p = o; + return FALSE; + } + if (!parse_coordinate (p, &yy)) + { + *p = o; + return FALSE; + } + + *x = xx; + *y = yy; + + return TRUE; +} + +static gboolean +parse_nonnegative_number (const char **p, + double *x) +{ + const char *o = *p; + double n; + + if (!parse_number (p, &n)) + return FALSE; + + if (n < 0) + { + *p = o; + return FALSE; + } + + *x = n; + + return TRUE; +} + +/* This fixes a flaw in our use of strchr() below: + * + * If p already points at the end of the string, + * we misinterpret strchr ("xyz", *p) returning + * non-NULL to mean that we can increment p. + * + * But strchr() will return a pointer to the + * final NUL byte in this case, and we walk off + * the end of the string. Oops + */ +static inline char * +_strchr (const char *str, + int c) +{ + if (c == 0) + return NULL; + else + return strchr (str, c); +} + +static gboolean +parse_flag (const char **p, + gboolean *f) +{ + skip_whitespace (p); + if (_strchr ("01", **p)) + { + *f = **p == '1'; + (*p)++; + skip_optional_comma (p); + return TRUE; + } + + return FALSE; +} + +static gboolean +parse_command (const char **p, + char *cmd) +{ + char *s; + const char *allowed; + + if (*cmd == 'X') + allowed = "mM"; + else + allowed = "mMhHvVzZlLcCsStTqQoOaA"; + + skip_whitespace (p); + s = _strchr (allowed, **p); + if (s) + { + *cmd = *s; + (*p)++; + return TRUE; + } + return FALSE; +} + +static gboolean +parse_string (const char **p, + const char *s) +{ + int len = strlen (s); + if (strncmp (*p, s, len) != 0) + return FALSE; + (*p) += len; + return TRUE; +} + +static gboolean +parse_rectangle (const char **p, + double *x, + double *y, + double *w, + double *h) +{ + const char *o = *p; + double w2; + + /* Check for M%g,%gh%gv%gh%gz without any intervening whitespace */ + if (parse_coordinate_pair (p, x, y) && + parse_string (p, "h") && + parse_coordinate (p, w) && + parse_string (p, "v") && + parse_coordinate (p, h) && + parse_string (p, "h") && + parse_coordinate (p, &w2) && + parse_string (p, "z") && + w2 == - *w) + { + skip_whitespace (p); + + return TRUE; + } + + *p = o; + return FALSE; +} + +static gboolean +parse_circle (const char **p, + double *sx, + double *sy, + double *r) +{ + const char *o = *p; + double r1, r2, r3, mx, my, ex, ey; + + /* Check for M%g,%gA%g,%g,0,1,0,%g,%gA%g,%g,0,1,0,%g,%g + * without any intervening whitespace + */ + if (parse_coordinate_pair (p, sx, sy) && + parse_string (p, "A") && + parse_coordinate_pair (p, r, &r1) && + parse_string (p, "0 0 0") && + parse_coordinate_pair (p, &mx, &my) && + parse_string (p, "A") && + parse_coordinate_pair (p, &r2, &r3) && + parse_string (p, "0 0 0") && + parse_coordinate_pair (p, &ex, &ey) && + parse_string (p, "z") && + *r == r1 && r1 == r2 && r2 == r3 && + *sx == ex && *sy == ey) + { + skip_whitespace (p); + + return TRUE; + } + + *p = o; + return FALSE; +} + +/** + * gsk_path_parse: + * @string: a string + * + * This is a convenience function that constructs a `GskPath` + * from a serialized form. + * + * The string is expected to be in + * [SVG path syntax](https://www.w3.org/TR/SVG11/paths.html#PathData), + * as e.g. produced by [method@Gsk.Path.to_string]. + * + * Returns: (nullable): a new `GskPath`, or `NULL` + * if @string could not be parsed + * + * Since: 4.14 + */ +GskPath * +gsk_path_parse (const char *string) +{ + GskPathBuilder *builder; + double x, y; + double prev_x1, prev_y1; + double path_x, path_y; + const char *p; + char cmd; + char prev_cmd; + gboolean after_comma; + gboolean repeat; + + builder = gsk_path_builder_new (); + + cmd = 'X'; + path_x = path_y = 0; + x = y = 0; + prev_x1 = prev_y1 = 0; + after_comma = FALSE; + + p = string; + while (*p) + { + prev_cmd = cmd; + repeat = !parse_command (&p, &cmd); + + if (after_comma && !repeat) + goto error; + + switch (cmd) + { + case 'X': + goto error; + + case 'Z': + case 'z': + if (repeat) + goto error; + else + { + gsk_path_builder_close (builder); + x = path_x; + y = path_y; + } + break; + + case 'M': + case 'm': + { + double x1, y1, w, h, r; + + if (parse_rectangle (&p, &x1, &y1, &w, &h)) + { + gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (x1, y1, w, h)); + if (_strchr ("zZX", prev_cmd)) + { + path_x = x1; + path_y = y1; + } + x = x1; + y = y1; + } + else if (parse_circle (&p, &x1, &y1, &r)) + { + gsk_path_builder_add_circle (builder, &GRAPHENE_POINT_INIT (x1 - r, y1), r); + if (_strchr ("zZX", prev_cmd)) + { + path_x = x1; + path_y = y1; + } + x = x1; + y = y1; + } + else if (parse_coordinate_pair (&p, &x1, &y1)) + { + if (cmd == 'm') + { + x1 += x; + y1 += y; + } + if (repeat) + gsk_path_builder_line_to (builder, x1, y1); + else + { + gsk_path_builder_move_to (builder, x1, y1); + if (_strchr ("zZX", prev_cmd)) + { + path_x = x1; + path_y = y1; + } + } + x = x1; + y = y1; + } + else + goto error; + } + break; + + case 'L': + case 'l': + { + double x1, y1; + + if (parse_coordinate_pair (&p, &x1, &y1)) + { + if (cmd == 'l') + { + x1 += x; + y1 += y; + } + + if (_strchr ("zZ", prev_cmd)) + { + gsk_path_builder_move_to (builder, x, y); + path_x = x; + path_y = y; + } + gsk_path_builder_line_to (builder, x1, y1); + x = x1; + y = y1; + } + else + goto error; + } + break; + + case 'H': + case 'h': + { + double x1; + + if (parse_coordinate (&p, &x1)) + { + if (cmd == 'h') + x1 += x; + if (_strchr ("zZ", prev_cmd)) + { + gsk_path_builder_move_to (builder, x, y); + path_x = x; + path_y = y; + } + gsk_path_builder_line_to (builder, x1, y); + x = x1; + } + else + goto error; + } + break; + + case 'V': + case 'v': + { + double y1; + + if (parse_coordinate (&p, &y1)) + { + if (cmd == 'v') + y1 += y; + if (_strchr ("zZ", prev_cmd)) + { + gsk_path_builder_move_to (builder, x, y); + path_x = x; + path_y = y; + } + gsk_path_builder_line_to (builder, x, y1); + y = y1; + } + else + goto error; + } + break; + + case 'C': + case 'c': + { + double x0, y0, x1, y1, x2, y2; + + if (parse_coordinate_pair (&p, &x0, &y0) && + parse_coordinate_pair (&p, &x1, &y1) && + parse_coordinate_pair (&p, &x2, &y2)) + { + if (cmd == 'c') + { + x0 += x; + y0 += y; + x1 += x; + y1 += y; + x2 += x; + y2 += y; + } + if (_strchr ("zZ", prev_cmd)) + { + gsk_path_builder_move_to (builder, x, y); + path_x = x; + path_y = y; + } + gsk_path_builder_cubic_to (builder, x0, y0, x1, y1, x2, y2); + prev_x1 = x1; + prev_y1 = y1; + x = x2; + y = y2; + } + else + goto error; + } + break; + + case 'S': + case 's': + { + double x0, y0, x1, y1, x2, y2; + + if (parse_coordinate_pair (&p, &x1, &y1) && + parse_coordinate_pair (&p, &x2, &y2)) + { + if (cmd == 's') + { + x1 += x; + y1 += y; + x2 += x; + y2 += y; + } + if (_strchr ("CcSs", prev_cmd)) + { + x0 = 2 * x - prev_x1; + y0 = 2 * y - prev_y1; + } + else + { + x0 = x; + y0 = y; + } + if (_strchr ("zZ", prev_cmd)) + { + gsk_path_builder_move_to (builder, x, y); + path_x = x; + path_y = y; + } + gsk_path_builder_cubic_to (builder, x0, y0, x1, y1, x2, y2); + prev_x1 = x1; + prev_y1 = y1; + x = x2; + y = y2; + } + else + goto error; + } + break; + + case 'Q': + case 'q': + { + double x1, y1, x2, y2; + + if (parse_coordinate_pair (&p, &x1, &y1) && + parse_coordinate_pair (&p, &x2, &y2)) + { + if (cmd == 'q') + { + x1 += x; + y1 += y; + x2 += x; + y2 += y; + } + if (_strchr ("zZ", prev_cmd)) + { + gsk_path_builder_move_to (builder, x, y); + path_x = x; + path_y = y; + } + gsk_path_builder_quad_to (builder, x1, y1, x2, y2); + prev_x1 = x1; + prev_y1 = y1; + x = x2; + y = y2; + } + else + goto error; + } + break; + + case 'T': + case 't': + { + double x1, y1, x2, y2; + + if (parse_coordinate_pair (&p, &x2, &y2)) + { + if (cmd == 't') + { + x2 += x; + y2 += y; + } + if (_strchr ("QqTt", prev_cmd)) + { + x1 = 2 * x - prev_x1; + y1 = 2 * y - prev_y1; + } + else + { + x1 = x; + y1 = y; + } + if (_strchr ("zZ", prev_cmd)) + { + gsk_path_builder_move_to (builder, x, y); + path_x = x; + path_y = y; + } + gsk_path_builder_quad_to (builder, x1, y1, x2, y2); + prev_x1 = x1; + prev_y1 = y1; + x = x2; + y = y2; + } + else + goto error; + } + break; + + case 'A': + case 'a': + { + double rx, ry; + double x_axis_rotation; + int large_arc, sweep; + double x1, y1; + + if (parse_nonnegative_number (&p, &rx) && + parse_nonnegative_number (&p, &ry) && + parse_number (&p, &x_axis_rotation) && + parse_flag (&p, &large_arc) && + parse_flag (&p, &sweep) && + parse_coordinate_pair (&p, &x1, &y1)) + { + if (cmd == 'a') + { + x1 += x; + y1 += y; + } + + if (_strchr ("zZ", prev_cmd)) + { + gsk_path_builder_move_to (builder, x, y); + path_x = x; + path_y = y; + } + gsk_path_builder_svg_arc_to (builder, + rx, ry, x_axis_rotation, + large_arc, sweep, + x1, y1); + x = x1; + y = y1; + } + else + goto error; + } + break; + + default: + goto error; + } + + after_comma = (p > string) && p[-1] == ','; + } + + if (after_comma) + goto error; + + return gsk_path_builder_free_to_path (builder); + +error: + //g_warning ("Can't parse string '%s' as GskPath, error at %ld", string, p - string); + gsk_path_builder_unref (builder); + + return NULL; +} diff --git a/gsk/gskpath.h b/gsk/gskpath.h new file mode 100644 index 0000000000..31f8c6ef8a --- /dev/null +++ b/gsk/gskpath.h @@ -0,0 +1,122 @@ +/* + * 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 + */ + +#pragma once + +#if !defined (__GSK_H_INSIDE__) && !defined (GTK_COMPILATION) +#error "Only can be included directly." +#endif + + +#include + +G_BEGIN_DECLS + +/** + * GskPathForeachFlags: + * @GSK_PATH_FOREACH_ALLOW_ONLY_LINES: The default behavior, only allow lines. + * @GSK_PATH_FOREACH_ALLOW_QUAD: Allow emission of `GSK_PATH_QUAD` operations + * @GSK_PATH_FOREACH_ALLOW_CUBIC: Allow emission of `GSK_PATH_CUBIC` operations. + * @GSK_PATH_FOREACH_ALLOW_ANY: Allow emission of any kind of operation. + * + * Flags that can be passed to gsk_path_foreach() to enable additional + * features. + * + * By default, [method@Gsk.Path.foreach] will only emit a path with all operations + * flattened to straight lines to allow for maximum compatibility. The only + * operations emitted will be `GSK_PATH_MOVE`, `GSK_PATH_LINE` and `GSK_PATH_CLOSE`. + */ +typedef enum +{ + GSK_PATH_FOREACH_ALLOW_ONLY_LINES = 0, + GSK_PATH_FOREACH_ALLOW_QUAD = (1 << 0), + GSK_PATH_FOREACH_ALLOW_CUBIC = (1 << 1), +} GskPathForeachFlags; + +/** + * GskPathForeachFunc: + * @op: The operation to perform + * @pts: The points of the operation + * @n_pts: The number of points + * @user_data: The user data provided with the function + * + * Prototype of the callback to iterate throught the operations of + * a path. + * + * Returns: %TRUE to continue evaluating the path, %FALSE to + * immediately abort and not call the function again. + */ +typedef gboolean (* GskPathForeachFunc) (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + gpointer user_data); + +#define GSK_TYPE_PATH (gsk_path_get_type ()) + +GDK_AVAILABLE_IN_4_14 +GType gsk_path_get_type (void) G_GNUC_CONST; + +GDK_AVAILABLE_IN_4_14 +GskPath * gsk_path_ref (GskPath *self); +GDK_AVAILABLE_IN_4_14 +void gsk_path_unref (GskPath *self); + +GDK_AVAILABLE_IN_4_14 +void gsk_path_print (GskPath *self, + GString *string); +GDK_AVAILABLE_IN_4_14 +char * gsk_path_to_string (GskPath *self); + +GDK_AVAILABLE_IN_4_14 +GskPath * gsk_path_parse (const char *string); + +GDK_AVAILABLE_IN_4_14 +void gsk_path_to_cairo (GskPath *self, + cairo_t *cr); + +GDK_AVAILABLE_IN_4_14 +gboolean gsk_path_is_empty (GskPath *self); + +GDK_AVAILABLE_IN_4_14 +gboolean gsk_path_is_closed (GskPath *self); + +GDK_AVAILABLE_IN_4_14 +gboolean gsk_path_get_bounds (GskPath *self, + graphene_rect_t *bounds); + +GDK_AVAILABLE_IN_4_14 +gboolean gsk_path_in_fill (GskPath *self, + const graphene_point_t *point, + GskFillRule fill_rule); + +GDK_AVAILABLE_IN_4_14 +gboolean gsk_path_get_closest_point (GskPath *self, + const graphene_point_t *point, + float threshold, + GskPathPoint *result); + +GDK_AVAILABLE_IN_4_14 +gboolean gsk_path_foreach (GskPath *self, + GskPathForeachFlags flags, + GskPathForeachFunc func, + gpointer user_data); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GskPath, gsk_path_unref) + +G_END_DECLS diff --git a/gsk/gskpathbuilder.c b/gsk/gskpathbuilder.c new file mode 100644 index 0000000000..7fc6591d88 --- /dev/null +++ b/gsk/gskpathbuilder.c @@ -0,0 +1,985 @@ +/* + * 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 "config.h" + +#include + +#include "gskpathbuilder.h" + +#include "gskpathprivate.h" +#include "gskcontourprivate.h" +#include "gsksplineprivate.h" + +/** + * GskPathBuilder: + * + * `GskPathBuilder` is an auxiliary object for constructing + * `GskPath` objects. + * + * A path is constructed like this: + * + * |[ + * GskPath * + * construct_path (void) + * { + * GskPathBuilder *builder; + * + * builder = gsk_path_builder_new (); + * + * // add contours to the path here + * + * return gsk_path_builder_free_to_path (builder); + * ]| + * + * Adding contours to the path can be done in two ways. + * The easiest option is to use the `gsk_path_builder_add_*` group + * of functions that add predefined contours to the current path, + * either common shapes like [method@Gsk.PathBuilder.add_circle] + * or by adding from other paths like [method@Gsk.PathBuilder.add_path]. + * + * The other option is to define each line and curve manually with + * the `gsk_path_builder_*_to` group of functions. You start with + * a call to [method@Gsk.PathBuilder.move_to] to set the starting point + * and then use multiple calls to any of the drawing functions to + * move the pen along the plane. Once you are done, you can call + * [method@Gsk.PathBuilder.close] to close the path by connecting it + * back with a line to the starting point. + * + * This is similar for how paths are drawn in Cairo. + */ + +struct _GskPathBuilder +{ + int ref_count; + + GSList *contours; /* (reverse) list of already recorded contours */ + + GskPathFlags flags; /* flags for the current path */ + graphene_point_t current_point; /* the point all drawing ops start from */ + GArray *ops; /* operations for current contour - size == 0 means no current contour */ + GArray *points; /* points for the operations */ +}; + +G_DEFINE_BOXED_TYPE (GskPathBuilder, + gsk_path_builder, + gsk_path_builder_ref, + gsk_path_builder_unref) + + +/** + * gsk_path_builder_new: + * + * Create a new `GskPathBuilder` object. + * + * The resulting builder would create an empty `GskPath`. + * Use addition functions to add types to it. + * + * Returns: a new `GskPathBuilder` + * + * Since: 4.14 + */ +GskPathBuilder * +gsk_path_builder_new (void) +{ + GskPathBuilder *self; + + self = g_slice_new0 (GskPathBuilder); + self->ref_count = 1; + + self->ops = g_array_new (FALSE, FALSE, sizeof (gskpathop)); + self->points = g_array_new (FALSE, FALSE, sizeof (graphene_point_t)); + + /* Be explicit here */ + self->current_point = GRAPHENE_POINT_INIT (0, 0); + + return self; +} + +/** + * gsk_path_builder_ref: + * @self: a `GskPathBuilder` + * + * Acquires a reference on the given builder. + * + * This function is intended primarily for language bindings. + * `GskPathBuilder` objects should not be kept around. + * + * Returns: (transfer none): the given `GskPathBuilder` with + * its reference count increased + * + * Since: 4.14 + */ +GskPathBuilder * +gsk_path_builder_ref (GskPathBuilder *self) +{ + g_return_val_if_fail (self != NULL, NULL); + g_return_val_if_fail (self->ref_count > 0, NULL); + + self->ref_count += 1; + + return self; +} + +/* We're cheating here. Out pathops are relative to the NULL pointer, + * so that we can not care about the points GArray reallocating itself + * until we create the contour. + * This does however mean that we need to not use gsk_pathop_get_points() + * without offsetting the returned pointer. + */ +static inline gskpathop +gsk_pathop_encode_index (GskPathOperation op, + gsize index) +{ + return gsk_pathop_encode (op, ((graphene_point_t *) NULL) + index); +} + +static void +gsk_path_builder_ensure_current (GskPathBuilder *self) +{ + if (self->ops->len != 0) + return; + + self->flags = GSK_PATH_FLAT; + g_array_append_vals (self->ops, (gskpathop[1]) { gsk_pathop_encode_index (GSK_PATH_MOVE, 0) }, 1); + g_array_append_val (self->points, self->current_point); +} + +static void +gsk_path_builder_append_current (GskPathBuilder *self, + GskPathOperation op, + gsize n_points, + const graphene_point_t *points) +{ + gsk_path_builder_ensure_current (self); + + g_array_append_vals (self->ops, (gskpathop[1]) { gsk_pathop_encode_index (op, self->points->len - 1) }, 1); + g_array_append_vals (self->points, points, n_points); + + self->current_point = points[n_points - 1]; +} + +static void +gsk_path_builder_end_current (GskPathBuilder *self) +{ + GskContour *contour; + + if (self->ops->len == 0) + return; + + contour = gsk_standard_contour_new (self->flags, + (graphene_point_t *) self->points->data, + self->points->len, + (gskpathop *) self->ops->data, + self->ops->len, + (graphene_point_t *) self->points->data - (graphene_point_t *) NULL); + + g_array_set_size (self->ops, 0); + g_array_set_size (self->points, 0); + + /* do this at the end to avoid inflooping when add_contour calls back here */ + gsk_path_builder_add_contour (self, contour); +} + +static void +gsk_path_builder_clear (GskPathBuilder *self) +{ + gsk_path_builder_end_current (self); + + g_slist_free_full (self->contours, g_free); + self->contours = NULL; +} + +/** + * gsk_path_builder_unref: + * @self: a `GskPathBuilder` + * + * Releases a reference on the given builder. + * + * Since: 4.14 + */ +void +gsk_path_builder_unref (GskPathBuilder *self) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (self->ref_count > 0); + + self->ref_count -= 1; + + if (self->ref_count > 0) + return; + + gsk_path_builder_clear (self); + g_array_unref (self->ops); + g_array_unref (self->points); + g_slice_free (GskPathBuilder, self); +} + +/** + * gsk_path_builder_free_to_path: (skip) + * @self: a `GskPathBuilder` + * + * Creates a new `GskPath` from the current state of the + * given builder, and frees the @builder instance. + * + * Returns: (transfer full): the newly created `GskPath` + * with all the contours added to the builder + * + * Since: 4.14 + */ +GskPath * +gsk_path_builder_free_to_path (GskPathBuilder *self) +{ + GskPath *res; + + g_return_val_if_fail (self != NULL, NULL); + + res = gsk_path_builder_to_path (self); + + gsk_path_builder_unref (self); + + return res; +} + +/** + * gsk_path_builder_to_path: + * @self: a `GskPathBuilder` + * + * Creates a new `GskPath` from the given builder. + * + * The given `GskPathBuilder` is reset once this function returns; + * you cannot call this function multiple times on the same builder + * instance. + * + * This function is intended primarily for language bindings. + * C code should use [method@Gsk.PathBuilder.free_to_path]. + * + * Returns: (transfer full): the newly created `GskPath` + * with all the contours added to the builder + * + * Since: 4.14 + */ +GskPath * +gsk_path_builder_to_path (GskPathBuilder *self) +{ + GskPath *path; + + g_return_val_if_fail (self != NULL, NULL); + + gsk_path_builder_end_current (self); + + self->contours = g_slist_reverse (self->contours); + + path = gsk_path_new_from_contours (self->contours); + + gsk_path_builder_clear (self); + + return path; +} + +void +gsk_path_builder_add_contour (GskPathBuilder *self, + GskContour *contour) +{ + gsk_path_builder_end_current (self); + + self->contours = g_slist_prepend (self->contours, contour); +} + +/** + * gsk_path_builder_get_current_point: + * @self: a `GskPathBuilder` + * + * Gets the current point. The current point is used for relative + * drawing commands and updated after every operation. + * + * When the builder is created, the default current point is set to (0, 0). + * Note that this is different from cairo, which starts out without + * a current point. + * + * Returns: (transfer none): The current point + * + * Since: 4.14 + */ +const graphene_point_t * +gsk_path_builder_get_current_point (GskPathBuilder *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return &self->current_point; +} + +/** + * gsk_path_builder_add_path: + * @self: a `GskPathBuilder` + * @path: (transfer none): the path to append + * + * Appends all of @path to the builder. + * + * Since: 4.14 + */ +void +gsk_path_builder_add_path (GskPathBuilder *self, + GskPath *path) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (path != NULL); + + for (gsize i = 0; i < gsk_path_get_n_contours (path); i++) + { + const GskContour *contour = gsk_path_get_contour (path, i); + + gsk_path_builder_add_contour (self, gsk_contour_dup (contour)); + } +} + +/** + * gsk_path_builder_add_reverse_path: + * @self: a `GskPathBuilder` + * @path: (transfer none): the path to append + * + * Appends all of @path to the builder, in reverse order. + * + * Since: 4.14 + */ +void +gsk_path_builder_add_reverse_path (GskPathBuilder *self, + GskPath *path) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (path != NULL); + + for (gsize i = gsk_path_get_n_contours (path); i > 0; i--) + { + const GskContour *contour = gsk_path_get_contour (path, i - 1); + + gsk_path_builder_add_contour (self, gsk_contour_reverse (contour)); + } +} + +/** + * gsk_path_builder_add_cairo_path: + * @self: a `GskPathBuilder` + * + * Adds a Cairo path to the builder. + * + * You can use cairo_copy_path() to access the path + * from a Cairo context. + * + * Since: 4.14 + */ +void +gsk_path_builder_add_cairo_path (GskPathBuilder *self, + const cairo_path_t *path) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (path != NULL); + + for (gsize i = 0; i < path->num_data; i += path->data[i].header.length) + { + const cairo_path_data_t *data = &path->data[i]; + + switch (data->header.type) + { + case CAIRO_PATH_MOVE_TO: + gsk_path_builder_move_to (self, data[1].point.x, data[1].point.y); + break; + + case CAIRO_PATH_LINE_TO: + gsk_path_builder_line_to (self, data[1].point.x, data[1].point.y); + break; + + case CAIRO_PATH_CURVE_TO: + gsk_path_builder_cubic_to (self, + data[1].point.x, data[1].point.y, + data[2].point.x, data[2].point.y, + data[3].point.x, data[3].point.y); + break; + + case CAIRO_PATH_CLOSE_PATH: + gsk_path_builder_close (self); + break; + + default: + g_assert_not_reached (); + break; + } + } +} + +/** + * gsk_path_builder_add_rect: + * @self: A `GskPathBuilder` + * @rect: The rectangle to create a path for + * + * Adds @rect as a new contour to the path built by the builder. + * + * If the width or height of the rectangle is negative, the start + * point will be on the right or bottom, respectively. + * + * If the the width or height are 0, the path will be a closed + * horizontal or vertical line. If both are 0, it'll be a closed dot. + * + * Since: 4.14 + */ +void +gsk_path_builder_add_rect (GskPathBuilder *self, + const graphene_rect_t *rect) +{ + g_return_if_fail (self != NULL); + + gsk_path_builder_move_to (self, rect->origin.x, rect->origin.y); + + gsk_path_builder_rel_line_to (self, rect->size.width, 0); + gsk_path_builder_rel_line_to (self, 0, rect->size.height); + gsk_path_builder_rel_line_to (self, - rect->size.width, 0); + + gsk_path_builder_close (self); +} + +static gboolean +circle_contour_curve (const graphene_point_t pts[4], + gpointer data) +{ + GskPathBuilder *self = data; + + gsk_path_builder_cubic_to (self, + pts[1].x, pts[1].y, + pts[2].x, pts[2].y, + pts[3].x, pts[3].y); + + return TRUE; +} + +/** + * gsk_path_builder_add_circle: + * @self: a `GskPathBuilder` + * @center: the center of the circle + * @radius: the radius of the circle + * + * Adds a circle with the @center and @radius. + * + * Since: 4.14 + */ +void +gsk_path_builder_add_circle (GskPathBuilder *self, + const graphene_point_t *center, + float radius) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (center != NULL); + g_return_if_fail (radius > 0); + + gsk_path_builder_move_to (self, center->x + radius, center->y); + gsk_spline_decompose_arc (center, radius, + GSK_PATH_TOLERANCE_DEFAULT, + 0, 2 * M_PI, + circle_contour_curve, self); + gsk_path_builder_close (self); +} + +/** + * gsk_path_builder_move_to: + * @self: a `GskPathBuilder` + * @x: x coordinate + * @y: y coordinate + * + * Starts a new contour by placing the pen at @x, @y. + * + * If this function is called twice in succession, the first + * call will result in a contour made up of a single point. + * The second call will start a new contour. + * + * Since: 4.14 + */ +void +gsk_path_builder_move_to (GskPathBuilder *self, + float x, + float y) +{ + g_return_if_fail (self != NULL); + + gsk_path_builder_end_current (self); + + self->current_point = GRAPHENE_POINT_INIT(x, y); + + gsk_path_builder_ensure_current (self); +} + +/** + * gsk_path_builder_rel_move_to: + * @self: a `GskPathBuilder` + * @x: x offset + * @y: y offset + * + * Starts a new contour by placing the pen at @x, @y relative to the current + * point. + * + * This is the relative version of [method@Gsk.PathBuilder.move_to]. + * + * Since: 4.14 + */ +void +gsk_path_builder_rel_move_to (GskPathBuilder *self, + float x, + float y) +{ + g_return_if_fail (self != NULL); + + gsk_path_builder_move_to (self, + self->current_point.x + x, + self->current_point.y + y); +} + +/** + * gsk_path_builder_line_to: + * @self: a `GskPathBuilder` + * @x: x coordinate + * @y: y coordinate + * + * Draws a line from the current point to @x, @y and makes it + * the new current point. + * + * Since: 4.14 + */ +void +gsk_path_builder_line_to (GskPathBuilder *self, + float x, + float y) +{ + g_return_if_fail (self != NULL); + + /* skip the line if it goes to the same point */ + if (graphene_point_equal (&self->current_point, + &GRAPHENE_POINT_INIT (x, y))) + return; + + gsk_path_builder_append_current (self, + GSK_PATH_LINE, + 1, (graphene_point_t[1]) { + GRAPHENE_POINT_INIT (x, y) + }); +} + +/** + * gsk_path_builder_rel_line_to: + * @self: a `GskPathBuilder` + * @x: x offset + * @y: y offset + * + * Draws a line from the current point to a point offset to it by @x, @y + * and makes it the new current point. + * + * This is the relative version of [method@Gsk.PathBuilder.line_to]. + * + * Since: 4.14 + */ +void +gsk_path_builder_rel_line_to (GskPathBuilder *self, + float x, + float y) +{ + g_return_if_fail (self != NULL); + + gsk_path_builder_line_to (self, + self->current_point.x + x, + self->current_point.y + y); +} + +/** + * gsk_path_builder_quad_to: + * @self: a #GskPathBuilder + * @x1: x coordinate of control point + * @y1: y coordinate of control point + * @x2: x coordinate of the end of the curve + * @y2: y coordinate of the end of the curve + * + * Adds a [quadratic Bézier curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve) + * from the current point to @x2, @y2 with @x1, @y1 as the control point. + * + * After this, @x2, @y2 will be the new current point. + * + * Since: 4.14 + */ +void +gsk_path_builder_quad_to (GskPathBuilder *self, + float x1, + float y1, + float x2, + float y2) +{ + g_return_if_fail (self != NULL); + + self->flags &= ~GSK_PATH_FLAT; + gsk_path_builder_append_current (self, + GSK_PATH_QUAD, + 2, (graphene_point_t[2]) { + GRAPHENE_POINT_INIT (x1, y1), + GRAPHENE_POINT_INIT (x2, y2) + }); +} + +/** + * gsk_path_builder_rel_quad_to: + * @self: a `GskPathBuilder` + * @x1: x offset of control point + * @y1: y offset of control point + * @x2: x offset of the end of the curve + * @y2: y offset of the end of the curve + * + * Adds a [quadratic Bézier curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve) + * from the current point to @x2, @y2 with @x1, @y1 the control point. + * + * All coordinates are given relative to the current point. + * + * This is the relative version of [method@Gsk.PathBuilder.quad_to]. + * + * Since: 4.14 + */ +void +gsk_path_builder_rel_quad_to (GskPathBuilder *self, + float x1, + float y1, + float x2, + float y2) +{ + g_return_if_fail (self != NULL); + + gsk_path_builder_quad_to (self, + self->current_point.x + x1, + self->current_point.y + y1, + self->current_point.x + x2, + self->current_point.y + y2); +} + +/** + * gsk_path_builder_cubic_to: + * @self: a `GskPathBuilder` + * @x1: x coordinate of first control point + * @y1: y coordinate of first control point + * @x2: x coordinate of second control point + * @y2: y coordinate of second control point + * @x3: x coordinate of the end of the curve + * @y3: y coordinate of the end of the curve + * + * Adds a [cubic Bézier curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve) + * from the current point to @x3, @y3 with @x1, @y1 and @x2, @y2 as the control + * points. + * + * After this, @x3, @y3 will be the new current point. + * + * Since: 4.14 + */ +void +gsk_path_builder_cubic_to (GskPathBuilder *self, + float x1, + float y1, + float x2, + float y2, + float x3, + float y3) +{ + g_return_if_fail (self != NULL); + + self->flags &= ~GSK_PATH_FLAT; + gsk_path_builder_append_current (self, + GSK_PATH_CUBIC, + 3, (graphene_point_t[3]) { + GRAPHENE_POINT_INIT (x1, y1), + GRAPHENE_POINT_INIT (x2, y2), + GRAPHENE_POINT_INIT (x3, y3) + }); +} + +/** + * gsk_path_builder_rel_cubic_to: + * @self: a `GskPathBuilder` + * @x1: x offset of first control point + * @y1: y offset of first control point + * @x2: x offset of second control point + * @y2: y offset of second control point + * @x3: x offset of the end of the curve + * @y3: y offset of the end of the curve + * + * Adds a [cubic Bézier curve](https://en.wikipedia.org/wiki/B%C3%A9zier_curve) + * from the current point to @x3, @y3 with @x1, @y1 and @x2, @y2 as the control + * points. All coordinates are given relative to the current point. + * + * This is the relative version of [method@Gsk.PathBuilder.cubic_to]. + * + * Since: 4.14 + */ +void +gsk_path_builder_rel_cubic_to (GskPathBuilder *self, + float x1, + float y1, + float x2, + float y2, + float x3, + float y3) +{ + g_return_if_fail (self != NULL); + + gsk_path_builder_cubic_to (self, + self->current_point.x + x1, + self->current_point.y + y1, + self->current_point.x + x2, + self->current_point.y + y2, + self->current_point.x + x3, + self->current_point.y + y3); +} + +/** + * gsk_path_builder_close: + * @self: a `GskPathBuilder` + * + * Ends the current contour with a line back to the start point. + * + * Note that this is different from calling [method@Gsk.PathBuilder.line_to] + * with the start point in that the contour will be closed. A closed + * contour behaves different from an open one when stroking its start + * and end point are considered connected, so they will be joined + * via the line join, and not ended with line caps. + * + * Since: 4.14 + */ +void +gsk_path_builder_close (GskPathBuilder *self) +{ + g_return_if_fail (self != NULL); + + if (self->ops->len == 0) + return; + + self->flags |= GSK_PATH_CLOSED; + gsk_path_builder_append_current (self, + GSK_PATH_CLOSE, + 1, (graphene_point_t[1]) { + g_array_index (self->points, graphene_point_t, 0) + }); + + gsk_path_builder_end_current (self); +} + +static void +arc_segment (GskPathBuilder *self, + double cx, + double cy, + double rx, + double ry, + double sin_phi, + double cos_phi, + double sin_th0, + double cos_th0, + double sin_th1, + double cos_th1, + double t) +{ + double x1, y1, x2, y2, x3, y3; + + x1 = rx * (cos_th0 - t * sin_th0); + y1 = ry * (sin_th0 + t * cos_th0); + x3 = rx * cos_th1; + y3 = ry * sin_th1; + x2 = x3 + rx * (t * sin_th1); + y2 = y3 + ry * (-t * cos_th1); + + gsk_path_builder_cubic_to (self, + cx + cos_phi * x1 - sin_phi * y1, + cy + sin_phi * x1 + cos_phi * y1, + cx + cos_phi * x2 - sin_phi * y2, + cy + sin_phi * x2 + cos_phi * y2, + cx + cos_phi * x3 - sin_phi * y3, + cy + sin_phi * x3 + cos_phi * y3); +} + +static inline void +_sincos (double angle, + double *y, + double *x) +{ +#ifdef HAVE_SINCOS + sincos (angle, y, x); +#else + *x = cos (angle); + *y = sin (angle); +#endif +} + +void +gsk_path_builder_svg_arc_to (GskPathBuilder *self, + float rx, + float ry, + float x_axis_rotation, + gboolean large_arc, + gboolean positive_sweep, + float x, + float y) +{ + graphene_point_t *current; + double x1, y1, x2, y2; + double phi, sin_phi, cos_phi; + double mid_x, mid_y; + double lambda; + double d; + double k; + double x1_, y1_; + double cx_, cy_; + double cx, cy; + double ux, uy, u_len; + double cos_theta1, theta1; + double vx, vy, v_len; + double dp_uv; + double cos_delta_theta, delta_theta; + int i, n_segs; + double d_theta, theta; + double sin_th0, cos_th0; + double sin_th1, cos_th1; + double th_half; + double t; + + if (self->points->len > 0) + { + current = &g_array_index (self->points, graphene_point_t, self->points->len - 1); + x1 = current->x; + y1 = current->y; + } + else + { + x1 = 0; + y1 = 0; + } + x2 = x; + y2 = y; + + phi = x_axis_rotation * M_PI / 180.0; + _sincos (phi, &sin_phi, &cos_phi); + + rx = fabs (rx); + ry = fabs (ry); + + mid_x = (x1 - x2) / 2; + mid_y = (y1 - y2) / 2; + + x1_ = cos_phi * mid_x + sin_phi * mid_y; + y1_ = - sin_phi * mid_x + cos_phi * mid_y; + + lambda = (x1_ / rx) * (x1_ / rx) + (y1_ / ry) * (y1_ / ry); + if (lambda > 1) + { + lambda = sqrt (lambda); + rx *= lambda; + ry *= lambda; + } + + d = (rx * y1_) * (rx * y1_) + (ry * x1_) * (ry * x1_); + if (d == 0) + return; + + k = sqrt (fabs ((rx * ry) * (rx * ry) / d - 1.0)); + if (positive_sweep == large_arc) + k = -k; + + cx_ = k * rx * y1_ / ry; + cy_ = -k * ry * x1_ / rx; + + cx = cos_phi * cx_ - sin_phi * cy_ + (x1 + x2) / 2; + cy = sin_phi * cx_ + cos_phi * cy_ + (y1 + y2) / 2; + + ux = (x1_ - cx_) / rx; + uy = (y1_ - cy_) / ry; + u_len = sqrt (ux * ux + uy * uy); + if (u_len == 0) + return; + + cos_theta1 = CLAMP (ux / u_len, -1, 1); + theta1 = acos (cos_theta1); + if (uy < 0) + theta1 = - theta1; + + vx = (- x1_ - cx_) / rx; + vy = (- y1_ - cy_) / ry; + v_len = sqrt (vx * vx + vy * vy); + if (v_len == 0) + return; + + dp_uv = ux * vx + uy * vy; + cos_delta_theta = CLAMP (dp_uv / (u_len * v_len), -1, 1); + delta_theta = acos (cos_delta_theta); + if (ux * vy - uy * vx < 0) + delta_theta = - delta_theta; + if (positive_sweep && delta_theta < 0) + delta_theta += 2 * M_PI; + else if (!positive_sweep && delta_theta > 0) + delta_theta -= 2 * M_PI; + + n_segs = ceil (fabs (delta_theta / (M_PI_2 + 0.001))); + d_theta = delta_theta / n_segs; + theta = theta1; + _sincos (theta1, &sin_th1, &cos_th1); + + th_half = d_theta / 2; + t = (8.0 / 3.0) * sin (th_half / 2) * sin (th_half / 2) / sin (th_half); + + for (i = 0; i < n_segs; i++) + { + theta = theta1; + theta1 = theta + d_theta; + sin_th0 = sin_th1; + cos_th0 = cos_th1; + _sincos (theta1, &sin_th1, &cos_th1); + arc_segment (self, + cx, cy, rx, ry, + sin_phi, cos_phi, + sin_th0, cos_th0, + sin_th1, cos_th1, + t); + } +} + +/** + * gsk_path_builder_add_layout: + * @self: a #GskPathBuilder + * @layout: the pango layout to add + * + * Adds the outlines for the glyphs in @layout to + * the builder. + * + * Since: 4.14 + */ +void +gsk_path_builder_add_layout (GskPathBuilder *self, + PangoLayout *layout) +{ + cairo_surface_t *surface; + cairo_t *cr; + cairo_path_t *cairo_path; + + surface = cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA, NULL); + cr = cairo_create (surface); + + pango_cairo_layout_path (cr, layout); + cairo_path = cairo_copy_path (cr); + + gsk_path_builder_add_cairo_path (self, cairo_path); + + cairo_path_destroy (cairo_path); + cairo_destroy (cr); + cairo_surface_destroy (surface); +} diff --git a/gsk/gskpathbuilder.h b/gsk/gskpathbuilder.h new file mode 100644 index 0000000000..8e32564dc3 --- /dev/null +++ b/gsk/gskpathbuilder.h @@ -0,0 +1,121 @@ +/* + * 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 + */ + +#pragma once + +#if !defined (__GSK_H_INSIDE__) && !defined (GTK_COMPILATION) +#error "Only can be included directly." +#endif + + +#include +#include + +G_BEGIN_DECLS + +#define GSK_TYPE_PATH_BUILDER (gsk_path_builder_get_type ()) + +GDK_AVAILABLE_IN_4_14 +GType gsk_path_builder_get_type (void) G_GNUC_CONST; + +GDK_AVAILABLE_IN_4_14 +GskPathBuilder * gsk_path_builder_new (void); +GDK_AVAILABLE_IN_4_14 +GskPathBuilder * gsk_path_builder_ref (GskPathBuilder *self); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_unref (GskPathBuilder *self); +GDK_AVAILABLE_IN_4_14 +GskPath * gsk_path_builder_free_to_path (GskPathBuilder *self) G_GNUC_WARN_UNUSED_RESULT; +GDK_AVAILABLE_IN_4_14 +GskPath * gsk_path_builder_to_path (GskPathBuilder *self) G_GNUC_WARN_UNUSED_RESULT; + +GDK_AVAILABLE_IN_4_14 +const graphene_point_t *gsk_path_builder_get_current_point (GskPathBuilder *self); + +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_add_path (GskPathBuilder *self, + GskPath *path); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_add_reverse_path (GskPathBuilder *self, + GskPath *path); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_add_cairo_path (GskPathBuilder *self, + const cairo_path_t *path); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_add_layout (GskPathBuilder *self, + PangoLayout *layout); + +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_add_rect (GskPathBuilder *self, + const graphene_rect_t *rect); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_add_circle (GskPathBuilder *self, + const graphene_point_t *center, + float radius); + +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_move_to (GskPathBuilder *self, + float x, + float y); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_rel_move_to (GskPathBuilder *self, + float x, + float y); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_line_to (GskPathBuilder *self, + float x, + float y); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_rel_line_to (GskPathBuilder *self, + float x, + float y); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_quad_to (GskPathBuilder *self, + float x1, + float y1, + float x2, + float y2); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_rel_quad_to (GskPathBuilder *self, + float x1, + float y1, + float x2, + float y2); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_cubic_to (GskPathBuilder *self, + float x1, + float y1, + float x2, + float y2, + float x3, + float y3); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_rel_cubic_to (GskPathBuilder *self, + float x1, + float y1, + float x2, + float y2, + float x3, + float y3); +GDK_AVAILABLE_IN_4_14 +void gsk_path_builder_close (GskPathBuilder *self); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GskPathBuilder, gsk_path_builder_unref) + +G_END_DECLS diff --git a/gsk/gskpathopprivate.h b/gsk/gskpathopprivate.h new file mode 100644 index 0000000000..a679f7eb15 --- /dev/null +++ b/gsk/gskpathopprivate.h @@ -0,0 +1,172 @@ +/* + * 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 + */ + + +#pragma once + +#include "gskpath.h" +#include "gskpathbuilder.h" + +G_BEGIN_DECLS + +typedef gpointer gskpathop; + +static inline +gskpathop gsk_pathop_encode (GskPathOperation op, + const graphene_point_t *pts); +static inline +const graphene_point_t *gsk_pathop_points (gskpathop pop); +static inline +GskPathOperation gsk_pathop_op (gskpathop pop); + +static inline +gboolean gsk_pathop_foreach (gskpathop pop, + GskPathForeachFunc func, + gpointer user_data); + +/* included inline so tests can use them */ +static inline +void gsk_path_builder_pathop_to (GskPathBuilder *builder, + gskpathop op); +static inline +void gsk_path_builder_pathop_reverse_to (GskPathBuilder *builder, + gskpathop op); + +/* IMPLEMENTATION */ + +#define GSK_PATHOP_OPERATION_MASK (0x7) + +static inline gskpathop +gsk_pathop_encode (GskPathOperation op, + const graphene_point_t *pts) +{ + /* g_assert (op & GSK_PATHOP_OPERATION_MASK == op); */ + g_assert ((GPOINTER_TO_SIZE (pts) & GSK_PATHOP_OPERATION_MASK) == 0); + + return GSIZE_TO_POINTER (GPOINTER_TO_SIZE (pts) | op); +} + +static inline const graphene_point_t * +gsk_pathop_points (gskpathop pop) +{ + return GSIZE_TO_POINTER (GPOINTER_TO_SIZE (pop) & ~GSK_PATHOP_OPERATION_MASK); +} + +static inline +GskPathOperation gsk_pathop_op (gskpathop pop) +{ + return GPOINTER_TO_SIZE (pop) & GSK_PATHOP_OPERATION_MASK; +} + +static inline gboolean +gsk_pathop_foreach (gskpathop pop, + GskPathForeachFunc func, + gpointer user_data) +{ + switch (gsk_pathop_op (pop)) + { + case GSK_PATH_MOVE: + return func (gsk_pathop_op (pop), gsk_pathop_points (pop), 1, user_data); + + case GSK_PATH_CLOSE: + case GSK_PATH_LINE: + return func (gsk_pathop_op (pop), gsk_pathop_points (pop), 2, user_data); + + case GSK_PATH_QUAD: + return func (gsk_pathop_op (pop), gsk_pathop_points (pop), 3, user_data); + + case GSK_PATH_CUBIC: + return func (gsk_pathop_op (pop), gsk_pathop_points (pop), 4, user_data); + + default: + g_assert_not_reached (); + return TRUE; + } +} + +static inline void +gsk_path_builder_pathop_to (GskPathBuilder *builder, + gskpathop op) +{ + const graphene_point_t *pts = gsk_pathop_points (op); + + switch (gsk_pathop_op (op)) + { + case GSK_PATH_MOVE: + gsk_path_builder_move_to (builder, pts[0].x, pts[0].y); + break; + + case GSK_PATH_CLOSE: + gsk_path_builder_close (builder); + break; + + case GSK_PATH_LINE: + gsk_path_builder_line_to (builder, pts[1].x, pts[1].y); + break; + + case GSK_PATH_QUAD: + gsk_path_builder_quad_to (builder, pts[1].x, pts[1].y, pts[2].x, pts[2].y); + break; + + case GSK_PATH_CUBIC: + gsk_path_builder_cubic_to (builder, pts[1].x, pts[1].y, pts[2].x, pts[2].y, pts[3].x, pts[3].y); + break; + + default: + g_assert_not_reached (); + break; + } +} + +static inline void +gsk_path_builder_pathop_reverse_to (GskPathBuilder *builder, + gskpathop op) +{ + const graphene_point_t *pts = gsk_pathop_points (op); + + switch (gsk_pathop_op (op)) + { + case GSK_PATH_MOVE: + gsk_path_builder_move_to (builder, pts[0].x, pts[0].y); + break; + + case GSK_PATH_CLOSE: + gsk_path_builder_line_to (builder, pts[0].x, pts[0].y); + break; + + case GSK_PATH_LINE: + gsk_path_builder_line_to (builder, pts[1].x, pts[1].y); + break; + + case GSK_PATH_QUAD: + gsk_path_builder_quad_to (builder, pts[1].x, pts[1].y, pts[0].x, pts[0].y); + break; + + case GSK_PATH_CUBIC: + gsk_path_builder_cubic_to (builder, pts[2].x, pts[2].y, pts[1].x, pts[1].y, pts[0].x, pts[0].y); + break; + + default: + g_assert_not_reached (); + break; + } +} + + +G_END_DECLS diff --git a/gsk/gskpathpoint.c b/gsk/gskpathpoint.c new file mode 100644 index 0000000000..adc72bf2ee --- /dev/null +++ b/gsk/gskpathpoint.c @@ -0,0 +1,155 @@ +/* + * Copyright © 2023 Red Hat, Inc. + * + * 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: Matthias Clasen + */ + +#include "config.h" + +#include "gskpathpointprivate.h" + +#include "gskcontourprivate.h" + +#include "gdk/gdkprivate.h" + +/** + * GskPathPoint: + * + * `GskPathPoint` is an opaque type representing a point on a path. + * + * It can be queried for properties of the path at that point, such as its + * tangent or its curvature. + * + * To obtain a `GskPathPoint`, use [method@Gsk.Path.get_closest_point]. + * + * Note that `GskPathPoint` structs are meant to be stack-allocated, and + * don't a reference to the path object they are obtained from. It is the + * callers responsibility to keep a reference to the path as long as the + * `GskPathPoint` is used. + */ + +G_DEFINE_BOXED_TYPE (GskPathPoint, gsk_path_point, + gsk_path_point_copy, + gsk_path_point_free) + + +GskPathPoint * +gsk_path_point_copy (GskPathPoint *point) +{ + GskPathPoint *copy; + + copy = g_new0 (GskPathPoint, 1); + + memcpy (copy, point, sizeof (GskRealPathPoint)); + + return copy; +} + +void +gsk_path_point_free (GskPathPoint *point) +{ + g_free (point); +} + +/** + * gsk_path_point_get_position: + * @path: a `GskPath` + * @point: a `GskPathPoint` on @path + * @position: (out caller-allocates): Return location for + * the coordinates of the point + * + * Gets the position of the point. + * + * Since: 4.14 + */ +void +gsk_path_point_get_position (GskPath *path, + const GskPathPoint *point, + graphene_point_t *position) +{ + GskRealPathPoint *self = (GskRealPathPoint *) point; + const GskContour *contour = gsk_path_get_contour (path, self->contour); + + g_return_if_fail (path == self->path); + g_return_if_fail (contour == NULL); + + gsk_contour_get_position (contour, self, position); +} + +/** + * gsk_path_point_get_tangent: + * @path: a `GskPath` + * @point: a `GskPathPoint` on @path + * @direction: the direction for which to return the tangent + * @tangent: (out caller-allocates): Return location for + * the tangent at the point + * + * Gets the tangent of the path at the point. + * + * Note that certain points on a path may not have a single + * tangent, such as sharp turns. At such points, there are + * two tangents -- the direction of the path going into the + * point, and the direction coming out of it. The @direction + * argument lets you choose which one to get. + * + * Since: 4.14 + */ +void +gsk_path_point_get_tangent (GskPath *path, + const GskPathPoint *point, + GskPathDirection direction, + graphene_vec2_t *tangent) +{ + GskRealPathPoint *self = (GskRealPathPoint *) point; + const GskContour *contour = gsk_path_get_contour (path, self->contour); + + g_return_if_fail (path == self->path); + g_return_if_fail (contour == NULL); + + gsk_contour_get_tangent (contour, self, direction, tangent); +} + +/** + * gsk_path_point_get_curvature: + * @path: a `GskPath` + * @point: a `GskPathPoint` on @path + * @center: (out caller-allocates): Return location for + * the center of the osculating circle + * + * Calculates the curvature of the path at the point. + * + * Optionally, returns the center of the osculating circle as well. + * + * If the curvature is infinite (at line segments), zero is returned, + * and @center is not modified. + * + * Returns: The curvature of the path at the given point + * + * Since: 4.14 + */ +float +gsk_path_point_get_curvature (GskPath *path, + const GskPathPoint *point, + graphene_point_t *center) +{ + GskRealPathPoint *self = (GskRealPathPoint *) point; + const GskContour *contour = gsk_path_get_contour (path, self->contour); + + g_return_val_if_fail (path == self->path, 0); + g_return_val_if_fail (contour == NULL, 0); + + return gsk_contour_get_curvature (contour, self, center); +} diff --git a/gsk/gskpathpoint.h b/gsk/gskpathpoint.h new file mode 100644 index 0000000000..daf66f6a49 --- /dev/null +++ b/gsk/gskpathpoint.h @@ -0,0 +1,67 @@ +/* + * Copyright © 2023 Red Hat, Inc. + * + * 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: Matthias Clasen + */ + +#pragma once + +#if !defined (__GSK_H_INSIDE__) && !defined (GTK_COMPILATION) +#error "Only can be included directly." +#endif + + +#include + +G_BEGIN_DECLS + +#define GSK_TYPE_PATH_POINT (gsk_path_point_get_type ()) + +typedef struct _GskPathPoint GskPathPoint; +struct _GskPathPoint { + /*< private >*/ + union { + float f[8]; + gpointer p[8]; + } data; +}; + +GDK_AVAILABLE_IN_4_14 +GType gsk_path_point_get_type (void) G_GNUC_CONST; + +GDK_AVAILABLE_IN_4_14 +GskPathPoint * gsk_path_point_copy (GskPathPoint *point); + +GDK_AVAILABLE_IN_4_14 +void gsk_path_point_free (GskPathPoint *point); + +GDK_AVAILABLE_IN_4_14 +void gsk_path_point_get_position (GskPath *path, + const GskPathPoint *point, + graphene_point_t *position); + +GDK_AVAILABLE_IN_4_14 +void gsk_path_point_get_tangent (GskPath *path, + const GskPathPoint *point, + GskPathDirection direction, + graphene_vec2_t *tangent); + +GDK_AVAILABLE_IN_4_14 +float gsk_path_point_get_curvature (GskPath *path, + const GskPathPoint *point, + graphene_point_t *center); + +G_END_DECLS diff --git a/gsk/gskpathpointprivate.h b/gsk/gskpathpointprivate.h new file mode 100644 index 0000000000..4d7d08a14e --- /dev/null +++ b/gsk/gskpathpointprivate.h @@ -0,0 +1,24 @@ +#pragma once + +#include "gskpathpoint.h" +#include "gskcontourprivate.h" + +G_BEGIN_DECLS + +struct _GskRealPathPoint +{ + GskPath *path; + gsize contour; + + union { + struct { + unsigned int idx; + float t; + } std; + } data; +}; + +G_STATIC_ASSERT (sizeof (GskRealPathPoint) <= sizeof (GskPathPoint)); + +G_END_DECLS + diff --git a/gsk/gskpathprivate.h b/gsk/gskpathprivate.h new file mode 100644 index 0000000000..9a889ba0a1 --- /dev/null +++ b/gsk/gskpathprivate.h @@ -0,0 +1,70 @@ +/* + * 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 + */ + + +#pragma once + +#include "gskpath.h" + +#include "gskpathopprivate.h" + +G_BEGIN_DECLS + +typedef enum +{ + GSK_PATH_FLAT, + GSK_PATH_CLOSED +} GskPathFlags; + +typedef struct _GskContour GskContour; +typedef struct _GskRealPathPoint GskRealPathPoint; + +/* Same as Skia, so looks like a good value. ¯\_(ツ)_/¯ */ +#define GSK_PATH_TOLERANCE_DEFAULT (0.5) + +GskPath * gsk_path_new_from_contours (const GSList *contours); + +gsize gsk_path_get_n_contours (const GskPath *self); +const GskContour * gsk_path_get_contour (const GskPath *self, + gsize i); + +GskPathFlags gsk_path_get_flags (const GskPath *self); + +gboolean gsk_path_foreach_with_tolerance (GskPath *self, + GskPathForeachFlags flags, + double tolerance, + GskPathForeachFunc func, + gpointer user_data); + + +void gsk_path_builder_add_contour (GskPathBuilder *builder, + GskContour *contour); + +void gsk_path_builder_svg_arc_to (GskPathBuilder *builder, + float rx, + float ry, + float x_axis_rotation, + gboolean large_arc, + gboolean positive_sweep, + float x, + float y); + + +G_END_DECLS + diff --git a/gsk/gskpointprivate.h b/gsk/gskpointprivate.h new file mode 100644 index 0000000000..b72c768876 --- /dev/null +++ b/gsk/gskpointprivate.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +static inline void G_GNUC_PURE +gsk_point_interpolate (const graphene_point_t *p1, + const graphene_point_t *p2, + float t, + graphene_point_t *p) +{ + p->x = p1->x * (1 - t) + p2->x * t; + p->Y = p1->y * (1 - t) + p2->y * t; +} + +static inline float G_GNUC_PURE +gsk_point_distance (const graphene_point_t *p1, + const graphene_point_t *p2) +{ + return sqrtf ((p1->x - p2->x)*(p1->x - p2->x) + (p1->y - p2->y)*(p1->y - p2->y)); +} diff --git a/gsk/gskspline.c b/gsk/gskspline.c new file mode 100644 index 0000000000..5590c7f7e0 --- /dev/null +++ b/gsk/gskspline.c @@ -0,0 +1,208 @@ +/* + * Copyright © 2002 University of Southern California + * 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 + * Carl D. Worth + */ + +#include "config.h" + +#include "gsksplineprivate.h" + +#include + +/* Spline deviation from the circle in radius would be given by: + + error = sqrt (x**2 + y**2) - 1 + + A simpler error function to work with is: + + e = x**2 + y**2 - 1 + + From "Good approximation of circles by curvature-continuous Bezier + curves", Tor Dokken and Morten Daehlen, Computer Aided Geometric + Design 8 (1990) 22-41, we learn: + + abs (max(e)) = 4/27 * sin**6(angle/4) / cos**2(angle/4) + + and + abs (error) =~ 1/2 * e + + Of course, this error value applies only for the particular spline + approximation that is used in _cairo_gstate_arc_segment. +*/ +static float +arc_error_normalized (float angle) +{ + return 2.0/27.0 * pow (sin (angle / 4), 6) / pow (cos (angle / 4), 2); +} + +static float +arc_max_angle_for_tolerance_normalized (float tolerance) +{ + float angle, error; + guint i; + + /* Use table lookup to reduce search time in most cases. */ + struct { + float angle; + float error; + } table[] = { + { G_PI / 1.0, 0.0185185185185185036127 }, + { G_PI / 2.0, 0.000272567143730179811158 }, + { G_PI / 3.0, 2.38647043651461047433e-05 }, + { G_PI / 4.0, 4.2455377443222443279e-06 }, + { G_PI / 5.0, 1.11281001494389081528e-06 }, + { G_PI / 6.0, 3.72662000942734705475e-07 }, + { G_PI / 7.0, 1.47783685574284411325e-07 }, + { G_PI / 8.0, 6.63240432022601149057e-08 }, + { G_PI / 9.0, 3.2715520137536980553e-08 }, + { G_PI / 10.0, 1.73863223499021216974e-08 }, + { G_PI / 11.0, 9.81410988043554039085e-09 }, + }; + + for (i = 0; i < G_N_ELEMENTS (table); i++) + { + if (table[i].error < tolerance) + return table[i].angle; + } + + i++; + do { + angle = G_PI / i++; + error = arc_error_normalized (angle); + } while (error > tolerance); + + return angle; +} + +static guint +arc_segments_needed (float angle, + float radius, + float tolerance) +{ + float max_angle; + + /* the error is amplified by at most the length of the + * major axis of the circle; see cairo-pen.c for a more detailed analysis + * of this. */ + max_angle = arc_max_angle_for_tolerance_normalized (tolerance / radius); + + return ceil (fabs (angle) / max_angle); +} + +/* We want to draw a single spline approximating a circular arc radius + R from angle A to angle B. Since we want a symmetric spline that + matches the endpoints of the arc in position and slope, we know + that the spline control points must be: + + (R * cos(A), R * sin(A)) + (R * cos(A) - h * sin(A), R * sin(A) + h * cos (A)) + (R * cos(B) + h * sin(B), R * sin(B) - h * cos (B)) + (R * cos(B), R * sin(B)) + + for some value of h. + + "Approximation of circular arcs by cubic polynomials", Michael + Goldapp, Computer Aided Geometric Design 8 (1991) 227-238, provides + various values of h along with error analysis for each. + + From that paper, a very practical value of h is: + + h = 4/3 * R * tan(angle/4) + + This value does not give the spline with minimal error, but it does + provide a very good approximation, (6th-order convergence), and the + error expression is quite simple, (see the comment for + _arc_error_normalized). +*/ +static gboolean +gsk_spline_decompose_arc_segment (const graphene_point_t *center, + float radius, + float angle_A, + float angle_B, + GskSplineAddCurveFunc curve_func, + gpointer user_data) +{ + float r_sin_A, r_cos_A; + float r_sin_B, r_cos_B; + float h; + + r_sin_A = radius * sin (angle_A); + r_cos_A = radius * cos (angle_A); + r_sin_B = radius * sin (angle_B); + r_cos_B = radius * cos (angle_B); + + h = 4.0/3.0 * tan ((angle_B - angle_A) / 4.0); + + return curve_func ((graphene_point_t[4]) { + GRAPHENE_POINT_INIT ( + center->x + r_cos_A, + center->y + r_sin_A + ), + GRAPHENE_POINT_INIT ( + center->x + r_cos_A - h * r_sin_A, + center->y + r_sin_A + h * r_cos_A + ), + GRAPHENE_POINT_INIT ( + center->x + r_cos_B + h * r_sin_B, + center->y + r_sin_B - h * r_cos_B + ), + GRAPHENE_POINT_INIT ( + center->x + r_cos_B, + center->y + r_sin_B + ) + }, + user_data); +} + +gboolean +gsk_spline_decompose_arc (const graphene_point_t *center, + float radius, + float tolerance, + float start_angle, + float end_angle, + GskSplineAddCurveFunc curve_func, + gpointer user_data) +{ + float step = start_angle - end_angle; + guint i, n_segments; + + /* Recurse if drawing arc larger than pi */ + if (ABS (step) > G_PI) + { + float mid_angle = (start_angle + end_angle) / 2.0; + + return gsk_spline_decompose_arc (center, radius, tolerance, start_angle, mid_angle, curve_func, user_data) + && gsk_spline_decompose_arc (center, radius, tolerance, mid_angle, end_angle, curve_func, user_data); + } + else if (ABS (step) < tolerance) + { + return gsk_spline_decompose_arc_segment (center, radius, start_angle, end_angle, curve_func, user_data); + } + + n_segments = arc_segments_needed (ABS (step), radius, tolerance); + step = (end_angle - start_angle) / n_segments; + + for (i = 0; i < n_segments - 1; i++, start_angle += step) + { + if (!gsk_spline_decompose_arc_segment (center, radius, start_angle, start_angle + step, curve_func, user_data)) + return FALSE; + } + return gsk_spline_decompose_arc_segment (center, radius, start_angle, end_angle, curve_func, user_data); +} + diff --git a/gsk/gsksplineprivate.h b/gsk/gsksplineprivate.h new file mode 100644 index 0000000000..c9da32734c --- /dev/null +++ b/gsk/gsksplineprivate.h @@ -0,0 +1,41 @@ +/* + * 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 + */ + + +#ifndef __GSK_SPLINE_PRIVATE_H__ +#define __GSK_SPLINE_PRIVATE_H__ + +#include "gskpath.h" + +G_BEGIN_DECLS + +typedef gboolean (* GskSplineAddCurveFunc) (const graphene_point_t curve[4], + gpointer user_data); +gboolean gsk_spline_decompose_arc (const graphene_point_t *center, + float radius, + float tolerance, + float start_angle, + float end_angle, + GskSplineAddCurveFunc curve_func, + gpointer user_data); + +G_END_DECLS + +#endif /* __GSK_SPLINE_PRIVATE_H__ */ + diff --git a/gsk/gsktypes.h b/gsk/gsktypes.h index acc8bc841b..656513c39c 100644 --- a/gsk/gsktypes.h +++ b/gsk/gsktypes.h @@ -25,6 +25,9 @@ #include #include +typedef struct _GskPath GskPath; +typedef struct _GskPathBuilder GskPathBuilder; +typedef struct _GskPathPoint GskPathPoint; typedef struct _GskRenderer GskRenderer; typedef struct _GskTransform GskTransform; diff --git a/gsk/meson.build b/gsk/meson.build index 489024572a..7072fc8b5a 100644 --- a/gsk/meson.build +++ b/gsk/meson.build @@ -23,9 +23,12 @@ gsk_private_gl_shaders = [ ] gsk_public_sources = files([ - 'gskdiff.c', 'gskcairorenderer.c', + 'gskdiff.c', 'gskglshader.c', + 'gskpath.c', + 'gskpathbuilder.c', + 'gskpathpoint.c', 'gskrenderer.c', 'gskrendernode.c', 'gskrendernodeimpl.c', @@ -37,9 +40,12 @@ gsk_public_sources = files([ gsk_private_sources = files([ 'gskcairoblur.c', + 'gskcontour.c', + 'gskcurve.c', 'gskdebug.c', 'gskprivate.c', 'gskprofiler.c', + 'gskspline.c', 'gl/gskglattachmentstate.c', 'gl/gskglbuffer.c', 'gl/gskglcommandqueue.c', @@ -66,6 +72,9 @@ gsk_public_headers = files([ 'gskcairorenderer.h', 'gskenums.h', 'gskglshader.h', + 'gskpath.h', + 'gskpathbuilder.h', + 'gskpathpoint.h', 'gskrenderer.h', 'gskrendernode.h', 'gskroundedrect.h',