diff --git a/gsk/gskcontourprivate.h b/gsk/gskcontourprivate.h index 77680a4d9a..a03a023204 100644 --- a/gsk/gskcontourprivate.h +++ b/gsk/gskcontourprivate.h @@ -101,6 +101,11 @@ float gsk_contour_get_curvature (const GskContou gpointer measure_data, float distance, graphene_point_t *center); +gboolean gsk_contour_dash (const GskContour *contour, + GskStroke *stroke, + float tolerance, + GskPathForeachFunc func, + gpointer user_data); G_END_DECLS diff --git a/gsk/gskpath.h b/gsk/gskpath.h index 180dc56c69..9e931074aa 100644 --- a/gsk/gskpath.h +++ b/gsk/gskpath.h @@ -104,6 +104,13 @@ gboolean gsk_path_foreach (GskPath GskPathForeachFunc func, gpointer user_data); +GDK_AVAILABLE_IN_ALL +gboolean gsk_path_dash (GskPath *path, + GskStroke *stroke, + float tolerance, + GskPathForeachFunc func, + gpointer user_data); + G_DEFINE_AUTOPTR_CLEANUP_FUNC(GskPath, gsk_path_unref) G_END_DECLS diff --git a/gsk/gskpathdash.c b/gsk/gskpathdash.c new file mode 100644 index 0000000000..86f9baeb47 --- /dev/null +++ b/gsk/gskpathdash.c @@ -0,0 +1,304 @@ +/* + * 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 "gskpathprivate.h" +#include "gskstrokeprivate.h" + +typedef struct +{ + float offset; /* how much of the current dash we've spent */ + gsize dash_index; /* goes from 0 to n_dash * 2, so we don't have to care about on/off + for uneven dashes */ + gboolean on; /* If we're currently dashing or not */ + gboolean may_close; /* TRUE if we haven't turned the dash off in this contour */ + gboolean needs_move_to; /* If we have emitted the initial move_to() yet */ + enum { + NORMAL, /* no special behavior required */ + SKIP, /* skip the next dash */ + ONLY, /* only do the first dash */ + DONE /* done with the first dash */ + } first_dash_behavior; /* How to handle the first dash in the loop. We loop closed contours + twice to make sure the first dash and the last dash can get joined */ + + GskCurve curve; /* Curve we are currently processing */ + + float collect_start; /* We're collecting multiple line segments when decomposing. */ + float collect_length; /* No need to emit a curve for every line segment when the dash is long enough. */ + + /* from the stroke */ + float *dash; + gsize n_dash; + float dash_length; + float dash_offset; + + float tolerance; + GskPathForeachFunc func; + gpointer user_data; +} GskPathDash; + +static void +gsk_path_dash_setup (GskPathDash *self) +{ + self->offset = fmodf (self->dash_offset, 2 * self->dash_length); + + self->dash_index = 0; + self->on = TRUE; + self->may_close = TRUE; + while (self->offset > self->dash[self->dash_index % self->n_dash]) + { + self->offset -= self->dash[self->dash_index % self->n_dash]; + self->dash_index++; + self->on = !self->on; + } + if (self->first_dash_behavior != ONLY) + self->needs_move_to = TRUE; +} + +static gboolean +gsk_path_dash_ensure_move_to (GskPathDash *self, + const graphene_point_t *pt) +{ + if (!self->needs_move_to) + return TRUE; + + if (!self->func (GSK_PATH_MOVE, pt, 1, 0, self->user_data)) + return FALSE; + + self->needs_move_to = FALSE; + return TRUE; +} + +static gboolean +gsk_path_dash_add_line_segment (const graphene_point_t *start, + const graphene_point_t *end, + float t_start, + float t_end, + GskCurveLineReason reason, + gpointer user_data) +{ + GskPathDash *self = user_data; + float remaining, length, t_step; + + length = graphene_point_distance (start, end, NULL, NULL); + if (self->collect_length) + { + t_start = self->collect_start; + length += self->collect_length; + self->collect_length = 0; + } + + t_step = t_end - t_start; + remaining = length; + + while (remaining) + { + float piece; + + if (self->offset + remaining <= self->dash[self->dash_index % self->n_dash]) + { + /* try collecting multiple line segments */ + if (t_end < 1.0) + { + self->collect_start = t_start + t_step * (length - remaining) / length; + self->collect_length = remaining; + return TRUE; + } + + piece = remaining; + } + else + piece = self->dash[self->dash_index % self->n_dash] - self->offset; + + if (self->on) + { + if (self->first_dash_behavior != SKIP) + { + GskCurve segment; + + if (piece) + { + gsk_curve_segment (&self->curve, + t_start + t_step * (length - remaining) / length, + t_start + t_step * (length - (remaining - piece)) / length, + &segment); + if (!gsk_path_dash_ensure_move_to (self, gsk_curve_get_start_point (&segment))) + return FALSE; + + if (!gsk_pathop_foreach (gsk_curve_pathop (&segment), self->func, self->user_data)) + return FALSE; + } + else + { + graphene_point_t p; + + gsk_curve_get_point (&self->curve, t_start + t_step * (length - remaining) / length, &p); + if (!gsk_path_dash_ensure_move_to (self, &p)) + return FALSE; + } + } + } + else + { + self->may_close = FALSE; + if (self->first_dash_behavior == ONLY) + { + self->first_dash_behavior = DONE; + return FALSE; + } + self->first_dash_behavior = NORMAL; + } + + if (self->offset + remaining <= self->dash[self->dash_index % self->n_dash]) + { + self->offset += remaining; + remaining = 0; + } + else + { + remaining -= piece; + self->offset = 0; + self->dash_index++; + self->dash_index %= 2 * self->n_dash; + self->on = !self->on; + self->needs_move_to = TRUE; + } + } + + return TRUE; +} + +static gboolean +gsk_path_dash_foreach (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + float weight, + gpointer user_data) +{ + GskPathDash *self = user_data; + + switch (op) + { + case GSK_PATH_MOVE: + gsk_path_dash_setup (self); + break; + + case GSK_PATH_CLOSE: + if (self->may_close) + { + if (graphene_point_equal (&pts[0], &pts[1])) + return self->func (GSK_PATH_CLOSE, pts, 2, 0, self->user_data); + } + else + op = GSK_PATH_LINE; + G_GNUC_FALLTHROUGH; + + case GSK_PATH_LINE: + case GSK_PATH_QUAD: + case GSK_PATH_CUBIC: + case GSK_PATH_CONIC: + gsk_curve_init_foreach (&self->curve, op, pts, n_pts, weight); + if (!gsk_curve_decompose (&self->curve, self->tolerance, gsk_path_dash_add_line_segment, self)) + return FALSE; + break; + + default: + g_assert_not_reached (); + break; + } + + return TRUE; +} + +gboolean +gsk_contour_dash (const GskContour *contour, + GskStroke *stroke, + float tolerance, + GskPathForeachFunc func, + gpointer user_data) +{ + GskPathDash self = { + .offset = 0, + .dash = stroke->dash, + .n_dash = stroke->n_dash, + .dash_length = stroke->dash_length, + .dash_offset = stroke->dash_offset, + .tolerance = tolerance, + .func = func, + .user_data = user_data + }; + gboolean is_closed = gsk_contour_get_flags (contour) & GSK_PATH_CLOSED ? TRUE : FALSE; + + self.first_dash_behavior = is_closed ? SKIP : NORMAL; + if (!gsk_contour_foreach (contour, tolerance, gsk_path_dash_foreach, &self)) + return FALSE; + + if (is_closed) + { + if (self.first_dash_behavior == NORMAL) + self.first_dash_behavior = ONLY; + else + self.first_dash_behavior = NORMAL; + self.needs_move_to = !self.on; + if (!gsk_contour_foreach (contour, tolerance, gsk_path_dash_foreach, &self) && + self.first_dash_behavior != DONE) + return FALSE; + } + + return TRUE; +} + +/** + * gsk_path_dash: + * @path: the `GskPath` to dash + * @stroke: the stroke containing the dash parameters + * @tolerance: tolerance to use while dashing + * @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 that is the result + * of dashing @path with the dash pattern from @stroke. + * + * Returns: `FALSE` if @func returned FALSE`, `TRUE` otherwise. + */ +gboolean +gsk_path_dash (GskPath *path, + GskStroke *stroke, + float tolerance, + GskPathForeachFunc func, + gpointer user_data) +{ + gsize i; + + /* Dashing disabled, no need to do any work */ + if (stroke->dash_length <= 0) + return gsk_path_foreach (path, -1, func, user_data); + + for (i = 0; i < gsk_path_get_n_contours (path); i++) + { + if (!gsk_contour_dash (gsk_path_get_contour (path, i), stroke, tolerance, func, user_data)) + return FALSE; + } + + return TRUE; +} + diff --git a/gsk/meson.build b/gsk/meson.build index ccf3b98e62..33da0380b1 100644 --- a/gsk/meson.build +++ b/gsk/meson.build @@ -45,6 +45,7 @@ gsk_private_sources = files([ 'gskcontour.c', 'gskcurve.c', 'gskdebug.c', + 'gskpathdash.c', 'gskprivate.c', 'gskprofiler.c', 'gskspline.c',