From 1b5dfcba7e4bdeae4ac183e1d0cd08c01261632b Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 8 Jul 2023 10:15:22 -0400 Subject: [PATCH] gsk: Add GskPath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the basic infrastructure for paths. The public APIs consists of GskPath, GskPathPoint and GskPathBuilder. GskPath is a data structure for paths that consists of contours, which in turn might contain Bézier curves. The Bezier data structure is inspired by Skia, with separate arrays for points and operations. One advantage of this arrangement is that start and end points are shared between adjacent curves. A GskPathPoint represents a point on a path, which can be queried for various properties. GskPathBuilder is an auxiliary builder object for paths. --- gsk/gsk.h | 3 + gsk/gskcontour.c | 635 +++++++++++++++ gsk/gskcontourprivate.h | 73 ++ gsk/gskcurve.c | 1533 +++++++++++++++++++++++++++++++++++++ gsk/gskcurveprivate.h | 157 ++++ gsk/gskenums.h | 76 +- gsk/gskpath.c | 1277 ++++++++++++++++++++++++++++++ gsk/gskpath.h | 122 +++ gsk/gskpathbuilder.c | 985 ++++++++++++++++++++++++ gsk/gskpathbuilder.h | 121 +++ gsk/gskpathopprivate.h | 172 +++++ gsk/gskpathpoint.c | 155 ++++ gsk/gskpathpoint.h | 67 ++ gsk/gskpathpointprivate.h | 24 + gsk/gskpathprivate.h | 70 ++ gsk/gskpointprivate.h | 20 + gsk/gskspline.c | 208 +++++ gsk/gsksplineprivate.h | 41 + gsk/gsktypes.h | 3 + gsk/meson.build | 11 +- 20 files changed, 5751 insertions(+), 2 deletions(-) create mode 100644 gsk/gskcontour.c create mode 100644 gsk/gskcontourprivate.h create mode 100644 gsk/gskcurve.c create mode 100644 gsk/gskcurveprivate.h create mode 100644 gsk/gskpath.c create mode 100644 gsk/gskpath.h create mode 100644 gsk/gskpathbuilder.c create mode 100644 gsk/gskpathbuilder.h create mode 100644 gsk/gskpathopprivate.h create mode 100644 gsk/gskpathpoint.c create mode 100644 gsk/gskpathpoint.h create mode 100644 gsk/gskpathpointprivate.h create mode 100644 gsk/gskpathprivate.h create mode 100644 gsk/gskpointprivate.h create mode 100644 gsk/gskspline.c create mode 100644 gsk/gsksplineprivate.h 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',