From e01b168fa871b327c29099cbb32a776f5d105827 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 24 Jun 2023 08:22:50 -0400 Subject: [PATCH] Implement offsetting Implement offsetting of paths by reusing the infrastructure of the stroker. --- gsk/gskcontour.c | 3 + gsk/gskcontourprivate.h | 6 + gsk/gskpathstroke.c | 356 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 365 insertions(+) diff --git a/gsk/gskcontour.c b/gsk/gskcontour.c index 4d8c209ee3..8496e650bd 100644 --- a/gsk/gskcontour.c +++ b/gsk/gskcontour.c @@ -566,6 +566,7 @@ gsk_rect_contour_offset (const GskContour *contour, GskLineJoin line_join, float miter_limit) { + gsk_contour_default_offset (contour, builder, distance, line_join, miter_limit); } static const GskContourClass GSK_RECT_CONTOUR_CLASS = @@ -1029,6 +1030,7 @@ gsk_circle_contour_offset (const GskContour *contour, GskLineJoin line_join, float miter_limit) { + gsk_contour_default_offset (contour, builder, distance, line_join, miter_limit); } static const GskContourClass GSK_CIRCLE_CONTOUR_CLASS = @@ -1905,6 +1907,7 @@ gsk_standard_contour_offset (const GskContour *contour, GskLineJoin line_join, float miter_limit) { + gsk_contour_default_offset (contour, builder, distance, line_join, miter_limit); } static const GskContourClass GSK_STANDARD_CONTOUR_CLASS = diff --git a/gsk/gskcontourprivate.h b/gsk/gskcontourprivate.h index 93e9102bb3..59ba981909 100644 --- a/gsk/gskcontourprivate.h +++ b/gsk/gskcontourprivate.h @@ -113,6 +113,12 @@ void gsk_contour_offset (const GskContou GskLineJoin line_join, float miter_limit); +void gsk_contour_default_offset (const GskContour *contour, + GskPathBuilder *builder, + float distance, + GskLineJoin line_join, + float miter_limit); + G_END_DECLS #endif /* __GSK_CONTOUR_PRIVATE_H__ */ diff --git a/gsk/gskpathstroke.c b/gsk/gskpathstroke.c index ba175a1bc3..f4871d05c9 100644 --- a/gsk/gskpathstroke.c +++ b/gsk/gskpathstroke.c @@ -448,6 +448,32 @@ path_builder_add_reverse_path (GskPathBuilder *builder, g_list_free_full (ops, g_free); } +static gboolean +add_op (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + float weight, + gpointer user_data) +{ + GskCurve c; + GskPathBuilder *builder = user_data; + + if (op == GSK_PATH_MOVE) + return TRUE; + + gsk_curve_init_foreach (&c, op, pts, n_pts, weight); + path_builder_add_curve (builder, &c); + + return TRUE; +} + +static void +path_builder_append_path (GskPathBuilder *builder, + GskPath *path) +{ + gsk_path_foreach (path, -1, add_op, builder); +} + /* Draw a circular arc from the current point to e, * around c */ @@ -2250,6 +2276,336 @@ gsk_contour_default_add_stroke (const GskContour *contour, stroke_data_clear (&stroke_data); } +/* }}} */ +/* {{{ Offset implementation */ + +typedef struct +{ + GskPathBuilder *builder; + float distance; + GskLineJoin line_join; + float miter_limit; + + GskPathBuilder *offset; + + gboolean has_current_point; + gboolean has_current_curve; + gboolean is_first_curve; + + Segment o; + Segment o0; +} OffsetData; + +static void +offset_data_init (OffsetData *offset_data, + GskPathBuilder *builder, + float distance, + GskLineJoin line_join, + float miter_limit) +{ + memset (offset_data, 0, sizeof (OffsetData)); + offset_data->builder = builder; + offset_data->distance = distance; + offset_data->line_join = line_join; + offset_data->miter_limit = miter_limit; + segment_init (&offset_data->o, NULL); + segment_init (&offset_data->o0, NULL); +} + +static void +offset_data_clear (OffsetData *offset_data) +{ + segment_clear (&offset_data->o); + segment_clear (&offset_data->o0); +} + +static void +append_offset (OffsetData *offset_data) +{ + if (offset_data->is_first_curve) + { + const GskCurve *o = segment_get_last_curve (&offset_data->o); + path_builder_move_to_point (offset_data->offset, gsk_curve_get_end_point (o)); + } + else + { + path_builder_add_curves (offset_data->offset, offset_data->o.c); + } +} + +static void +add_offset (OffsetData *offset_data, + Segment *offset) +{ + const GskCurve *c1, *c2; + const GskCurve *o1, *o2; + float angle; + graphene_vec2_t tangent1, tangent2; + GskLineJoin line_join; + + if (!offset_data->has_current_curve) + { + const GskCurve *o = segment_get_first_curve (offset); + + path_builder_move_to_point (offset_data->offset, gsk_curve_get_start_point (o)); + + segment_copy (offset, &offset_data->o0); + segment_copy (offset, &offset_data->o); + + offset_data->has_current_curve = TRUE; + offset_data->is_first_curve = TRUE; + return; + } + + c1 = &offset_data->o.curve; + o1 = segment_get_last_curve (&offset_data->o); + c2 = &offset->curve; + o2 = segment_get_first_curve (offset); + + gsk_curve_get_end_tangent (c1, &tangent1); + gsk_curve_get_start_tangent (c2, &tangent2); + angle = angle_between (&tangent1, &tangent2); + + if (fabs (angle) < DEG_TO_RAD (5)) + line_join = GSK_LINE_JOIN_ROUND; + else + line_join = offset_data->line_join; + + if (fabs (angle) < DEG_TO_RAD (1)) + { + /* Close enough to a straight line */ + append_offset (offset_data); + } + else + { + float maxdist; + + if (fabs (M_PI - fabs (angle)) < DEG_TO_RAD (1)) + { + /* For 180 turns, we look at the whole curves to + * decide if they are left or right turns + */ + get_tangent (gsk_curve_get_start_point (c1), + gsk_curve_get_end_point (c1), + &tangent1); + get_tangent (gsk_curve_get_start_point (c2), + gsk_curve_get_end_point (c2), + &tangent2); + angle = angle_between (&tangent1, &tangent2); + } + + maxdist = find_closest_intersection (c1, c2, NULL); + + if ((angle > 0) == (offset_data->distance > 0)) + { + int i, j; + float t1, t2; + graphene_point_t p; + + /* Turn towards the offset */ + if (segments_intersect (offset_data->o.c, offset->c, FALSE, &i, &j, &t1, &t2, &p, 1) > 0 && + graphene_point_distance (&p, gsk_curve_get_start_point (c2), NULL, NULL) < maxdist) + { + GskCurve c; + GskCurve *r; + + g_array_set_size (offset_data->o.c, i + 1); + r = &g_array_index (offset_data->o.c, GskCurve, i); + gsk_curve_split (r, t1, &c, NULL); + g_array_index (offset_data->o.c, GskCurve, i) = c; + + g_array_remove_range (offset->c, 0, j); + r = &g_array_index (offset->c, GskCurve, 0); + gsk_curve_split (r, t2, NULL, &c); + g_array_index (offset->c, GskCurve, 0) = c; + + if (offset_data->is_first_curve) + { + g_array_set_size (offset_data->o0.c, 0); + g_array_append_vals (offset_data->o0.c, offset_data->o.c->data, offset_data->o.c->len); + } + + append_offset (offset_data); + } + else + { + append_offset (offset_data); + path_builder_line_to_point (offset_data->offset, gsk_curve_get_start_point (o2)); + } + } + else + { + /* Turn away */ + append_offset (offset_data); + add_line_join (offset_data->offset, + line_join, + offset_data->distance, + offset_data->miter_limit, + gsk_curve_get_start_point (c2), + o1, + o2, + angle); + + } + } + + segment_copy (offset, &offset_data->o); + + offset_data->is_first_curve = FALSE; +} + +static void +finish_offset_contour (OffsetData *offset_data) +{ + GskPath *path; + + if (offset_data->has_current_curve) + { + if (!offset_data->is_first_curve) + path_builder_add_curves (offset_data->offset, offset_data->o.c); + + path_builder_move_to_point (offset_data->builder, gsk_curve_get_start_point (segment_get_first_curve (&offset_data->o0))); + path_builder_add_curves (offset_data->builder, offset_data->o0.c); + + path = gsk_path_builder_free_to_path (offset_data->offset); + path_builder_append_path (offset_data->builder, path); + gsk_path_unref (path); + } + else + { + gsk_path_builder_unref (offset_data->offset); + } + + offset_data->offset = NULL; +} + +static void +close_offset_contour (OffsetData *offset_data) +{ + GskPath *path; + + if (offset_data->has_current_curve) + { + /* add final join and first segment */ + add_offset (offset_data, &offset_data->o0); + path_builder_add_curves (offset_data->offset, offset_data->o.c); + } + + gsk_path_builder_close (offset_data->offset); + + path = gsk_path_builder_free_to_path (offset_data->offset); + gsk_path_builder_add_path (offset_data->builder, path); + gsk_path_unref (path); + + offset_data->offset = NULL; +} + +static void +add_offset_for_curve (const GskCurve *curve, + OffsetData *offset_data) +{ + Segment offset = { 0, }; + + segment_init (&offset, curve); + segment_subdivide (&offset); + segment_offset (&offset, offset_data->distance); + + elide_self_intersections (&offset); + + add_offset (offset_data, &offset); + + segment_clear (&offset); +} + +static gboolean +offset_op (GskPathOperation op, + const graphene_point_t *pts, + gsize n_pts, + float weight, + gpointer user_data) +{ + OffsetData *offset_data = user_data; + Segment offset = { 0, }; + GskCurve curve; + + switch (op) + { + case GSK_PATH_MOVE: + if (offset_data->has_current_point) + finish_offset_contour (offset_data); + + gsk_curve_init_foreach (&curve, + GSK_PATH_LINE, + (const graphene_point_t[]) { pts[0], GRAPHENE_POINT_INIT (pts[0].x + 1, pts[0].y) }, + 2, 0.f); + + segment_init (&offset, &curve); + segment_subdivide (&offset); + + segment_offset (&offset, offset_data->distance); + segment_copy (&offset, &offset_data->o0); + segment_clear (&offset); + + offset_data->offset = gsk_path_builder_new (); + + offset_data->has_current_point = TRUE; + offset_data->has_current_curve = FALSE; + break; + + case GSK_PATH_CLOSE: + if (offset_data->has_current_point) + { + if (!graphene_point_near (&pts[0], &pts[1], 0.001)) + { + gsk_curve_init_foreach (&curve, GSK_PATH_LINE, pts, n_pts, weight); + add_offset_for_curve (&curve, offset_data); + } + + close_offset_contour (offset_data); + } + + offset_data->has_current_point = FALSE; + offset_data->has_current_curve = FALSE; + break; + + case GSK_PATH_LINE: + case GSK_PATH_QUAD: + case GSK_PATH_CONIC: + gsk_curve_init_foreach (&curve, op, pts, n_pts, weight); + add_offset_for_curve (&curve, offset_data); + break; + + case GSK_PATH_CUBIC: + gsk_curve_init_foreach (&curve, op, pts, n_pts, weight); + split_curve_by_cusps (&curve, (AddCurveCallback)add_offset_for_curve, offset_data); + break; + + default: + g_assert_not_reached (); + } + + return TRUE; +} + +void +gsk_contour_default_offset (const GskContour *contour, + GskPathBuilder *builder, + float distance, + GskLineJoin line_join, + float miter_limit) +{ + OffsetData offset_data; + + offset_data_init (&offset_data, builder, distance, line_join, miter_limit); + + gsk_contour_foreach (contour, GSK_PATH_TOLERANCE_DEFAULT, offset_op, &offset_data); + + if (offset_data.has_current_point) + finish_offset_contour (&offset_data); + + offset_data_clear (&offset_data); +} + /* }}} */ /* vim:set foldmethod=marker expandtab: */