From 8ac8bca52a2d85dc80b9bde515af6c410a3800bc Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 16 Sep 2023 10:03:24 -0400 Subject: [PATCH 1/9] Fix an indentation mishap --- gtk/gtkwindow.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gtk/gtkwindow.c b/gtk/gtkwindow.c index bac2a25b89..5841265aac 100644 --- a/gtk/gtkwindow.c +++ b/gtk/gtkwindow.c @@ -4364,16 +4364,16 @@ gtk_window_realize (GtkWidget *widget) { gtk_window_enable_csd (window); - if (priv->title_box == NULL) - { - priv->title_box = gtk_header_bar_new (); - gtk_widget_add_css_class (priv->title_box, "titlebar"); - gtk_widget_add_css_class (priv->title_box, "default-decoration"); + if (priv->title_box == NULL) + { + priv->title_box = gtk_header_bar_new (); + gtk_widget_add_css_class (priv->title_box, "titlebar"); + gtk_widget_add_css_class (priv->title_box, "default-decoration"); - gtk_widget_insert_before (priv->title_box, widget, NULL); - } + gtk_widget_insert_before (priv->title_box, widget, NULL); + } - update_window_actions (window); + update_window_actions (window); } } From 7cd97192e53f519a8af1754809051f4edad5b87a Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 16 Sep 2023 14:32:12 -0400 Subject: [PATCH 2/9] pathbuilder: Simplify degenerate curves When a quadratic or conic has identical points, replace it with a line. --- gsk/gskpathbuilder.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/gsk/gskpathbuilder.c b/gsk/gskpathbuilder.c index 3381ddbf6e..08aa1fd0b9 100644 --- a/gsk/gskpathbuilder.c +++ b/gsk/gskpathbuilder.c @@ -653,12 +653,14 @@ gsk_path_builder_quad_to (GskPathBuilder *self, { g_return_if_fail (self != NULL); - /* skip the quad if it collapses to a point */ if (graphene_point_equal (&self->current_point, - &GRAPHENE_POINT_INIT (x1, y1)) && + &GRAPHENE_POINT_INIT (x1, y1)) || graphene_point_equal (&GRAPHENE_POINT_INIT (x1, y1), &GRAPHENE_POINT_INIT (x2, y2))) - return; + { + gsk_path_builder_line_to (self, x2, y2); + return; + } self->flags &= ~GSK_PATH_FLAT; gsk_path_builder_append_current (self, @@ -836,10 +838,13 @@ gsk_path_builder_conic_to (GskPathBuilder *self, /* skip the conic if it collapses to a point */ if (graphene_point_equal (&self->current_point, - &GRAPHENE_POINT_INIT (x1, y1)) && + &GRAPHENE_POINT_INIT (x1, y1)) || graphene_point_equal (&GRAPHENE_POINT_INIT (x1, y1), &GRAPHENE_POINT_INIT (x2, y2))) - return; + { + gsk_path_builder_line_to (self, x2, y2); + return; + } self->flags &= ~GSK_PATH_FLAT; gsk_path_builder_append_current (self, From a6d2d983b863dc2a85906e7d3c3b37c92820e74e Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 16 Sep 2023 21:56:05 -0400 Subject: [PATCH 3/9] Add gsk_bounding_box_get_corner This will be used in the following commits. --- gsk/gskboundingboxprivate.h | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/gsk/gskboundingboxprivate.h b/gsk/gskboundingboxprivate.h index 629ffd9bfb..687257bc25 100644 --- a/gsk/gskboundingboxprivate.h +++ b/gsk/gskboundingboxprivate.h @@ -112,4 +112,32 @@ gsk_bounding_box_union (const GskBoundingBox *a, gsk_bounding_box_init (res, &min, &max); } -G_END_DECLS +static inline void +gsk_bounding_box_get_corner (const GskBoundingBox *b, + GskCorner c, + graphene_point_t *p) +{ + switch (c) + { + case GSK_CORNER_TOP_LEFT: + *p = b->min; + break; + + case GSK_CORNER_TOP_RIGHT: + p->x = b->max.x; + p->y = b->min.y; + break; + + case GSK_CORNER_BOTTOM_RIGHT: + *p = b->max; + break; + + case GSK_CORNER_BOTTOM_LEFT: + p->x = b->min.x; + p->y = b->max.y; + break; + + default: + g_assert_not_reached (); + } +} From ca63552cf8e6e6f7f58e261a2d714fcfd3f36e48 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 16 Sep 2023 18:58:50 -0400 Subject: [PATCH 4/9] path builder: Handle degenerate quads Quads that have a cusp need to be replaced by two lines. --- gsk/gskpathbuilder.c | 76 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/gsk/gskpathbuilder.c b/gsk/gskpathbuilder.c index 08aa1fd0b9..9c960895e4 100644 --- a/gsk/gskpathbuilder.c +++ b/gsk/gskpathbuilder.c @@ -24,6 +24,7 @@ #include "gskpathbuilder.h" #include "gskpathprivate.h" +#include "gskcurveprivate.h" #include "gskpathpointprivate.h" #include "gskcontourprivate.h" @@ -624,6 +625,40 @@ gsk_path_builder_rel_line_to (GskPathBuilder *self, self->current_point.y + y); } +static inline void +closest_point (const graphene_point_t *p, + const graphene_point_t *a, + const graphene_point_t *b, + graphene_point_t *q) +{ + graphene_vec2_t n; + graphene_vec2_t ap; + float t; + + graphene_vec2_init (&n, b->x - a->x, b->y - a->y); + graphene_vec2_init (&ap, p->x - a->x, p->y - a->y); + + t = graphene_vec2_dot (&ap, &n) / graphene_vec2_dot (&n, &n); + + q->x = a->x + t * (b->x - a->x); + q->y = a->y + t * (b->y - a->y); +} + +static inline gboolean +collinear (const graphene_point_t *p, + const graphene_point_t *a, + const graphene_point_t *b) +{ + graphene_point_t q; + + if (graphene_point_equal (a, b)) + return TRUE; + + closest_point (p, a, b, &q); + + return graphene_point_near (p, &q, 0.001); +} + /** * gsk_path_builder_quad_to: * @self: a #GskPathBuilder @@ -651,24 +686,49 @@ gsk_path_builder_quad_to (GskPathBuilder *self, float x2, float y2) { + graphene_point_t p0 = self->current_point; + graphene_point_t p1 = GRAPHENE_POINT_INIT (x1, y1); + graphene_point_t p2 = GRAPHENE_POINT_INIT (x2, y2); + g_return_if_fail (self != NULL); - if (graphene_point_equal (&self->current_point, - &GRAPHENE_POINT_INIT (x1, y1)) || - graphene_point_equal (&GRAPHENE_POINT_INIT (x1, y1), - &GRAPHENE_POINT_INIT (x2, y2))) + if (collinear (&p0, &p1, &p2)) { + GskBoundingBox bb; + + /* We simplify degenerate quads to one or two lines */ + if (!gsk_bounding_box_contains_point (gsk_bounding_box_init (&bb, &p0, &p2), &p1)) + { + GskCurve c; + + gsk_curve_init_foreach (&c, GSK_PATH_QUAD, + (const graphene_point_t []) { p0, p1, p2 }, + 3, 0.f); + gsk_curve_get_tight_bounds (&c, &bb); + for (int i = 0; i < 4; i++) + { + graphene_point_t q; + + gsk_bounding_box_get_corner (&bb, i, &q); + if (graphene_point_equal (&p0, &q) || + graphene_point_equal (&p2, &q)) + { + gsk_bounding_box_get_corner (&bb, (i + 2) % 4, &q); + gsk_path_builder_line_to (self, q.x, q.y); + break; + } + } + } + gsk_path_builder_line_to (self, x2, y2); + return; } 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) - }); + 2, (graphene_point_t[2]) { p1, p2 }); } /** From 1f3b69e7db1e7ac062a25553629989f2d2a78bd5 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 16 Sep 2023 19:04:19 -0400 Subject: [PATCH 5/9] path builder: Handle degenerate conics Do the same we do for quads here. Also, conics with weight 1 are just quads. --- gsk/gskpathbuilder.c | 45 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/gsk/gskpathbuilder.c b/gsk/gskpathbuilder.c index 9c960895e4..9229a1e001 100644 --- a/gsk/gskpathbuilder.c +++ b/gsk/gskpathbuilder.c @@ -893,16 +893,51 @@ gsk_path_builder_conic_to (GskPathBuilder *self, float y2, float weight) { + graphene_point_t p0 = self->current_point; + graphene_point_t p1 = GRAPHENE_POINT_INIT (x1, y1); + graphene_point_t p2 = GRAPHENE_POINT_INIT (x2, y2); + g_return_if_fail (self != NULL); g_return_if_fail (weight > 0); - /* skip the conic if it collapses to a point */ - if (graphene_point_equal (&self->current_point, - &GRAPHENE_POINT_INIT (x1, y1)) || - graphene_point_equal (&GRAPHENE_POINT_INIT (x1, y1), - &GRAPHENE_POINT_INIT (x2, y2))) + if (weight == 1) { + gsk_path_builder_quad_to (self, x1, y1, x2, y2); + return; + } + + if (collinear (&p0, &p1, &p2)) + { + GskBoundingBox bb; + + /* We simplify degenerate quads to one or two lines + * (two lines are needed if there's a cusp). + */ + if (!gsk_bounding_box_contains_point (gsk_bounding_box_init (&bb, &p0, &p2), &p1)) + { + GskCurve c; + + gsk_curve_init_foreach (&c, GSK_PATH_CONIC, + (const graphene_point_t []) { p0, p1, p2 }, + 3, weight); + gsk_curve_get_tight_bounds (&c, &bb); + for (int i = 0; i < 4; i++) + { + graphene_point_t q; + + gsk_bounding_box_get_corner (&bb, i, &q); + if (graphene_point_equal (&p0, &q) || + graphene_point_equal (&p2, &q)) + { + gsk_bounding_box_get_corner (&bb, (i + 2) % 4, &q); + gsk_path_builder_line_to (self, q.x, q.y); + break; + } + } + } + gsk_path_builder_line_to (self, x2, y2); + return; } From 68b4d9c35e51be233b7d2e14d996b58724bc6794 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sun, 17 Sep 2023 00:13:17 -0400 Subject: [PATCH 6/9] Add gsk_curve_get_cusps --- gsk/gskcurve.c | 51 +++++++++++++++++++++++++++++++++++++++++++ gsk/gskcurveprivate.h | 5 ++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/gsk/gskcurve.c b/gsk/gskcurve.c index 8a76633a4b..237d339ffc 100644 --- a/gsk/gskcurve.c +++ b/gsk/gskcurve.c @@ -2581,6 +2581,57 @@ gsk_curve_get_curvature_points (const GskCurve *curve, return filter_allowable (t, n); } +/* Find cusps inside the open interval from 0 to 1. + * + * According to Stone & deRose, A Geometric Characterization + * of Parametric Cubic curves, a necessary and sufficient + * condition is that the first derivative vanishes. + */ +int +gsk_curve_get_cusps (const GskCurve *curve, + float t[2]) +{ + const graphene_point_t *pts = curve->cubic.points; + graphene_point_t p[3]; + float ax, bx, cx; + float ay, by, cy; + float tx[3]; + int nx; + int n = 0; + + if (curve->op != GSK_PATH_CUBIC) + return 0; + + p[0].x = 3 * (pts[1].x - pts[0].x); + p[0].y = 3 * (pts[1].y - pts[0].y); + p[1].x = 3 * (pts[2].x - pts[1].x); + p[1].y = 3 * (pts[2].y - pts[1].y); + p[2].x = 3 * (pts[3].x - pts[2].x); + p[2].y = 3 * (pts[3].y - pts[2].y); + + ax = p[0].x - 2 * p[1].x + p[2].x; + bx = - 2 * p[0].x + 2 * p[1].x; + cx = p[0].x; + + nx = solve_quadratic (ax, bx, cx, tx); + nx = filter_allowable (tx, nx); + + ay = p[0].y - 2 * p[1].y + p[2].y; + by = - 2 * p[0].y + 2 * p[1].y; + cy = p[0].y; + + for (int i = 0; i < nx; i++) + { + float ti = tx[i]; + + if (0 < ti && ti < 1 && + fabsf (ay * ti * ti + by * ti + cy) < 0.001) + t[n++] = ti; + } + + return n; +} + /* }}} */ /* vim:set foldmethod=marker expandtab: */ diff --git a/gsk/gskcurveprivate.h b/gsk/gskcurveprivate.h index c03d6c9720..1aa82746dc 100644 --- a/gsk/gskcurveprivate.h +++ b/gsk/gskcurveprivate.h @@ -181,9 +181,12 @@ float gsk_curve_at_length (const GskCurve float distance, float epsilon); -int gsk_curve_get_curvature_points (const GskCurve * curve, +int gsk_curve_get_curvature_points (const GskCurve *curve, float t[3]); +int gsk_curve_get_cusps (const GskCurve *curve, + float t[2]); + G_END_DECLS From 7e13cfea91e9d1bdd3464aa3a17b88248463e5b8 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sat, 16 Sep 2023 21:56:54 -0400 Subject: [PATCH 7/9] path builder: Handle degenerate cubics Replace cubics by lines or quadratics when possible, and split at cusps. --- gsk/gskpathbuilder.c | 167 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 155 insertions(+), 12 deletions(-) diff --git a/gsk/gskpathbuilder.c b/gsk/gskpathbuilder.c index 9229a1e001..da28c8a515 100644 --- a/gsk/gskpathbuilder.c +++ b/gsk/gskpathbuilder.c @@ -764,6 +764,38 @@ gsk_path_builder_rel_quad_to (GskPathBuilder *self, self->current_point.y + y2); } +static gboolean +point_is_between (const graphene_point_t *q, + const graphene_point_t *p0, + const graphene_point_t *p1) +{ + return collinear (p0, p1, q) && + fabsf (graphene_point_distance (p0, q, NULL, NULL) + graphene_point_distance (p1, q, NULL, NULL) - graphene_point_distance (p0, p1, NULL, NULL)) < 0.001; +} + +static gboolean +bounding_box_corner_between (const GskBoundingBox *bb, + const graphene_point_t *p0, + const graphene_point_t *p1, + graphene_point_t *p) +{ + for (int i = 0; i < 4; i++) + { + graphene_point_t q; + + gsk_bounding_box_get_corner (bb, i, &q); + + if (point_is_between (&q, p0, p1)) + { + *p = q; + return TRUE; + } + } + + return FALSE; +} + + /** * gsk_path_builder_cubic_to: * @self: a `GskPathBuilder` @@ -796,25 +828,136 @@ gsk_path_builder_cubic_to (GskPathBuilder *self, float x3, float y3) { + graphene_point_t p0 = self->current_point; + graphene_point_t p1 = GRAPHENE_POINT_INIT (x1, y1); + graphene_point_t p2 = GRAPHENE_POINT_INIT (x2, y2); + graphene_point_t p3 = GRAPHENE_POINT_INIT (x3, y3); + graphene_point_t p, q; + gboolean p01, p12, p23; + g_return_if_fail (self != NULL); - /* skip the cubic if it collapses to a point */ - if (graphene_point_equal (&self->current_point, - &GRAPHENE_POINT_INIT (x1, y1)) && - graphene_point_equal (&GRAPHENE_POINT_INIT (x1, y1), - &GRAPHENE_POINT_INIT (x2, y2)) && - graphene_point_equal (&GRAPHENE_POINT_INIT (x2, y2), - &GRAPHENE_POINT_INIT (x3, y3))) + p01 = graphene_point_equal (&p0, &p1); + p12 = graphene_point_equal (&p1, &p2); + p23 = graphene_point_equal (&p2, &p3); + + if (p01 && p12 && p23) return; + if ((p01 && p23) || (p12 && (p01 || p23))) + { + gsk_path_builder_line_to (self, x3, y3); + return; + } + + if (collinear (&p0, &p1, &p2) && + collinear (&p1, &p2, &p3) && + (!p12 || collinear (&p0, &p1, &p3))) + { + GskBoundingBox bb; + gboolean p1in, p2in; + + gsk_bounding_box_init (&bb, &p0, &p3); + p1in = gsk_bounding_box_contains_point (&bb, &p1); + p2in = gsk_bounding_box_contains_point (&bb, &p2); + if (p1in && p2in) + { + gsk_path_builder_line_to (self, x3, y3); + } + else + { + GskCurve c; + + gsk_curve_init_foreach (&c, + GSK_PATH_CUBIC, + (const graphene_point_t[]) { p0, p1, p2, p3 }, + 4, + 0.f); + gsk_curve_get_tight_bounds (&c, &bb); + if (!p1in) + { + /* Find the intersection of bb with p0 - p1. + * It must be a corner + */ + bounding_box_corner_between (&bb, &p0, &p1, &p); + gsk_path_builder_line_to (self, p.x, p.y); + } + if (!p2in) + { + /* Find the intersection of bb with p2 - p3. */ + bounding_box_corner_between (&bb, &p3, &p2, &p); + gsk_path_builder_line_to (self, p.x, p.y); + } + gsk_path_builder_line_to (self, x3, y3); + } + + return; + } + + /* reduce to a quadratic if possible */ + graphene_point_interpolate (&p0, &p1, 1.5, &p); + graphene_point_interpolate (&p3, &p2, 1.5, &q); + if (graphene_point_near (&p, &q, 0.001)) + { + gsk_path_builder_quad_to (self, p.x, p.y, x3, y3); + return; + } + self->flags &= ~GSK_PATH_FLAT; + + /* At this point, we are dealing with a cubic that can't be reduced to + * lines or quadratics. Check for cusps. + */ + { + GskCurve c, c1, c2, c3, c4; + float t[2]; + int n; + + gsk_curve_init_foreach (&c, + GSK_PATH_CUBIC, + (const graphene_point_t[]) { p0, p1, p2, p3 }, + 4, + 0.f); + + n = gsk_curve_get_cusps (&c, t); + if (n == 1) + { + gsk_curve_split (&c, t[0], &c1, &c2); + gsk_path_builder_append_current (self, + GSK_PATH_CUBIC, + 3, &c1.cubic.points[1]); + gsk_path_builder_append_current (self, + GSK_PATH_CUBIC, + 3, &c2.cubic.points[1]); + return; + } + else if (n == 2) + { + if (t[1] < t[0]) + { + float s = t[0]; + t[0] = t[1]; + t[1] = s; + } + + gsk_curve_split (&c, t[0], &c1, &c2); + gsk_curve_split (&c2, (t[1] - t[0]) / (1 - t[0]), &c3, &c4); + gsk_path_builder_append_current (self, + GSK_PATH_CUBIC, + 3, &c1.cubic.points[1]); + gsk_path_builder_append_current (self, + GSK_PATH_CUBIC, + 3, &c3.cubic.points[1]); + gsk_path_builder_append_current (self, + GSK_PATH_CUBIC, + 3, &c4.cubic.points[1]); + return; + } + } + 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) - }); + 3, (graphene_point_t[3]) { p1, p2, p3 }); } /** From ecfc661054f27628db48b6f6bf817da6d9a493d6 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sun, 17 Sep 2023 08:27:02 -0400 Subject: [PATCH 8/9] Adapt tests to new path builder behavior Some tests were expecting to get elevated curves from GskPathBuilder. But they won't, anymore. --- testsuite/gsk/nodeparser/fill2.node | 2 +- testsuite/gsk/nodeparser/fill2.ref.node | 2 +- testsuite/gsk/path-special-cases.c | 15 ++++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/testsuite/gsk/nodeparser/fill2.node b/testsuite/gsk/nodeparser/fill2.node index 4702c0fa79..9674539d3d 100644 --- a/testsuite/gsk/nodeparser/fill2.node +++ b/testsuite/gsk/nodeparser/fill2.node @@ -1,4 +1,4 @@ fill { - path: "M 0 0 O 10 10 20 20 5"; + path: "M 0 0 O 10 0 20 20 5"; fill-rule: even-odd; } diff --git a/testsuite/gsk/nodeparser/fill2.ref.node b/testsuite/gsk/nodeparser/fill2.ref.node index 467a06ede0..6fd83027db 100644 --- a/testsuite/gsk/nodeparser/fill2.ref.node +++ b/testsuite/gsk/nodeparser/fill2.ref.node @@ -4,6 +4,6 @@ fill { color: rgb(255,0,204); } path: "\ -M 0 0 O 10 10, 20 20, 5"; +M 0 0 O 10 0, 20 20, 5"; fill-rule: even-odd; } diff --git a/testsuite/gsk/path-special-cases.c b/testsuite/gsk/path-special-cases.c index 22a99f5b89..3226ff0f92 100644 --- a/testsuite/gsk/path-special-cases.c +++ b/testsuite/gsk/path-special-cases.c @@ -426,10 +426,23 @@ test_foreach (void) path2 = gsk_path_builder_free_to_path (builder); s2 = gsk_path_to_string (path2); - g_assert_cmpstr (sp, ==, s2); + /* We still end up with quads here, since GskPathBuilder aggressively reduces + * curves degrees. + */ + g_assert_cmpstr (s, ==, s2); gsk_path_unref (path2); g_free (s2); + + path2 = gsk_path_parse (sp); + s2 = gsk_path_to_string (path2); + + g_assert_cmpstr (s, ==, s2); + + gsk_path_unref (path2); + g_free (s2); + + gsk_path_unref (path); } static void From 5777587e7af0b2dbca337a1349640f9bf3f9a122 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sun, 17 Sep 2023 09:31:59 -0400 Subject: [PATCH 9/9] docs: Add details for GskPathBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mention that GskPathBuilder will simplify added Bézier curves. --- gsk/gskpathbuilder.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gsk/gskpathbuilder.c b/gsk/gskpathbuilder.c index da28c8a515..ebb5a9dcf7 100644 --- a/gsk/gskpathbuilder.c +++ b/gsk/gskpathbuilder.c @@ -68,6 +68,9 @@ * * This is similar to how paths are drawn in Cairo. * + * Note that `GskPathBuilder` will reduce the degree of added Bézier + * curves as much as possible, to simplify rendering. + * * Since: 4.14 */