Compare commits

...

31 Commits

Author SHA1 Message Date
Matthias Clasen
568ae041d8 gsk: Make glyphy optional for now
Enable glyphy by default, but make it possible
to get the previous cairo rendering by setting
GSK_DEBUG=no-glyphy in the environment.

Keeping this optional will make it easier to
debug issues and compare output.
2023-07-19 00:54:49 -04:00
Matthias Clasen
d5a6cface7 Add an animated text example 2023-07-19 00:26:02 -04:00
Matthias Clasen
6c9c9ad6c3 Add a font rendering test 2023-07-19 00:26:02 -04:00
Matthias Clasen
de08441d4d glyphy: Implement synthetic italic
Use the font matrix found in the FcPattern to
transform the quads we use to render the glyphs.

This makes Cantarell Italic come out just like
it does with freetype.
2023-07-19 00:26:02 -04:00
Matthias Clasen
8a438c46ae glyphy: Update for api changes in glyphy
With this, synthetic bold fonts work as well
as they do with freetype.
2023-07-19 00:26:02 -04:00
Matthias Clasen
5fb1a572c6 glyphy: Implement synthetic bold
When the font appears to be synthetic bold (as
indicated by th embolden property in FcPattern),
use glyphys boldness uniform to render the font
bolder.
2023-07-19 00:26:02 -04:00
Matthias Clasen
1ab45331c0 glyphy: Remove glyphy debug code
We don't have a knob to turn this on, and it is
not really useful outside of glyphy itself, so
remove it.
2023-07-19 00:26:02 -04:00
Matthias Clasen
23e36c9245 glyphy: Simplify the path
Use path ops to simplify the path we get from
harfbuzz, since variable fonts often have overlapping
contours, and the glyphy shader can't handle those.
2023-07-19 00:26:02 -04:00
Matthias Clasen
0beb416b53 build: Bump the harfbuzz requirement to 4.0
The hb_font_get_glyph_shape api was introduced
in 4.0.
2023-07-19 00:26:02 -04:00
Matthias Clasen
3d27f67107 glyphy: Pencil in outline rendering 2023-07-19 00:26:02 -04:00
Matthias Clasen
9574e49df6 glyphy: Declare that we are using dFdx
GLES2 does not have these by default :(
2023-07-19 00:26:02 -04:00
Christian Hergert
0347e4cd76 gsk/gl: Fix output color of glyphy fragment shader
This copied more or less what the coloring vertex shader
was doing in that we premultiply alpha. That changes how
we apply alpha in the fragment shader to match.

This fixes a white halo around the fonts.
2023-07-19 00:25:03 -04:00
Matthias Clasen
14a8321b02 glyphy: Make glyphy cache size-independent
Use a hb font at nominal size when generating sdf
contours, and use a cache key that is independent
of the size.
2023-07-19 00:25:03 -04:00
Christian Hergert
5bb4846442 gsk/gl: Start on basic glyphy renderjob integration
This doesn't work correctly yet, as there are lots of
bumps along the way to still smooth out.
2023-07-19 00:25:03 -04:00
Christian Hergert
4715afc822 gsk/gl: Add a texture library for Glyphy
This adds a new texture library that can upload SDF data
from libglyphy into regions of a texture atlas so that it
can be accessed by Glyphy shaders in the appropriate place
and format.

Some of the placement positioning may seem odd in that it
needs to follow a certain format to be decoded from the
Glyphy shaders.
2023-07-19 00:25:03 -04:00
Christian Hergert
9f58740486 gsk/gl: Dispatch text_node to legacy vs glyphy
If the text node has color glyphs, then we need to dispatch
to the legacy form of rendering which uses FreeType/Cairo/etc
to upload glyphs to a rendered glyph cache.

Otherwise, we can dispatch to a new function which will
eventually use Glyphy to shape to SDF content and upload
to an alternate texture atlas.
2023-07-19 00:25:03 -04:00
Matthias Clasen
7088acb88b build: Add a dependency on glyphy
We have a subproject, and we link statically
if we can, to avoid depending on a project
that is not generally packaged in distros.
2023-07-19 00:25:03 -04:00
Matthias Clasen
732a607127 Make path tests build as internal tests 2023-07-19 00:25:03 -04:00
Matthias Clasen
e8ef875a7b Drop all path api 2023-07-19 00:25:03 -04:00
Matthias Clasen
56b17715f1 path: Make GskFillRule available 2023-07-18 21:31:16 -04:00
Matthias Clasen
6775a22cff Implement boolean operations on paths
Implement union, intersection, difference and
symmetric difference of two paths, as well as
simplification of a single path.
2023-07-18 21:31:16 -04:00
Matthias Clasen
380ac5804d curve: Add gsk_curve_intersect
Add a way to find the intersections of two curves.
We can handle some curve-line intersections directly,
the general case is handled via bisecting.

This will be used in stroking and path ops.
2023-07-18 21:02:38 -04:00
Matthias Clasen
241c6811f9 curve: Add utilities for cusps and inflections
Add functions to find cusps and inflection points of cubics.
These will be used for intersections and in the stroker.
2023-07-18 21:02:38 -04:00
Matthias Clasen
ba442fc24e curve: Add gsk_curve_get_bounds
Add getters for bounding boxes of curves.

We have cheap ones, which are just the bounding
box of the control points, and tighter ones, which
require finding the actual extrema.

Bounding boxes are needed to implement intersection
via bisecting.
2023-07-18 21:02:38 -04:00
Matthias Clasen
9d5c23f342 Add a bounding box type
graphene_rect_t does not quite work for this purpose.
2023-07-18 21:02:38 -04:00
Matthias Clasen
568ac6b7f6 Add another curve decomposition test
This one uses GskPathMeasure to check that
our conic approximations look roughly right.
2023-07-18 21:02:38 -04:00
Matthias Clasen
a94cc20067 Add tests for GskPathMeasure 2023-07-18 21:02:38 -04:00
Matthias Clasen
638a91451a Add GskPathMeasure
An object to do measuring operations on paths - determining
their length, cutting off subpaths, things like that.
2023-07-18 21:02:38 -04:00
Matthias Clasen
769679d111 gsk: Add tests for GskPath 2023-07-18 21:02:38 -04:00
Matthias Clasen
306f524e59 gsk: Add tests for GskCurve 2023-07-18 21:02:38 -04:00
Matthias Clasen
e86281d81c Add GskPath and GskPathBuilder
GskPath is a data structure for paths that consists
of contours, which in turn might contain Bézier curves.

The 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.

In addition to the usual contours comprised of Bézier
segments, GskPath supports certain special contours
directly, such as rectangles, rounded rectangles and
circles.
2023-07-18 21:02:37 -04:00
52 changed files with 15476 additions and 10 deletions

View File

@@ -32,6 +32,7 @@
#include "gskglcommandqueueprivate.h"
#include "gskglcompilerprivate.h"
#include "gskglglyphlibraryprivate.h"
#include "gskglglyphylibraryprivate.h"
#include "gskgliconlibraryprivate.h"
#include "gskglprogramprivate.h"
#include "gskglshadowlibraryprivate.h"
@@ -273,6 +274,7 @@ gsk_gl_driver_dispose (GObject *object)
}
g_clear_object (&self->glyphs_library);
g_clear_object (&self->glyphy_library);
g_clear_object (&self->icons_library);
g_clear_object (&self->shadows_library);
@@ -463,6 +465,7 @@ gsk_gl_driver_new (GskGLCommandQueue *command_queue,
}
self->glyphs_library = gsk_gl_glyph_library_new (self);
self->glyphy_library = gsk_gl_glyphy_library_new (self);
self->icons_library = gsk_gl_icon_library_new (self);
self->shadows_library = gsk_gl_shadow_library_new (self);
@@ -573,6 +576,8 @@ gsk_gl_driver_begin_frame (GskGLDriver *self,
self->current_frame_id);
gsk_gl_texture_library_begin_frame (GSK_GL_TEXTURE_LIBRARY (self->glyphs_library),
self->current_frame_id);
gsk_gl_texture_library_begin_frame (GSK_GL_TEXTURE_LIBRARY (self->glyphy_library),
self->current_frame_id);
/* Cleanup old shadows */
gsk_gl_shadow_library_begin_frame (self->shadows_library);

View File

@@ -97,6 +97,7 @@ struct _GskGLDriver
GskGLCommandQueue *command_queue;
GskGLGlyphLibrary *glyphs_library;
GskGLGlyphyLibrary *glyphy_library;
GskGLIconLibrary *icons_library;
GskGLShadowLibrary *shadows_library;

544
gsk/gl/gskglglyphylibrary.c Normal file
View File

@@ -0,0 +1,544 @@
/* gskglglyphylibrary.c
*
* Copyright 2020 Christian Hergert <chergert@redhat.com>
*
* 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 program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
/* Some of the glyphy cache is based upon the original glyphy code.
* It's license is provided below.
*/
/*
* Copyright 2012 Google, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Google Author(s): Behdad Esfahbod
*/
#include "config.h"
#include <gdk/gdkglcontextprivate.h>
#include <gdk/gdkmemoryformatprivate.h>
#include <gdk/gdkprofilerprivate.h>
#include "gskglcommandqueueprivate.h"
#include "gskgldriverprivate.h"
#include "gskglglyphylibraryprivate.h"
#include "gskdebugprivate.h"
#include "gskpathprivate.h"
#include <glyphy.h>
#define TOLERANCE (1/2048.)
#define MIN_FONT_SIZE 14
#define GRID_SIZE 20 /* Per EM */
#define ENLIGHTEN_MAX .01 /* Per EM */
#define EMBOLDEN_MAX .024 /* Per EM */
/* We split the atlas into cells of size 64x8, so the minimum number of
* bytes we store per glyph is 2048, and an atlas of size 2048x1024 can
* hold at most 4096 glyphs. We need 5 and 7 bits to store the position
* of a glyph in the atlas.
*
* We allocate each glyph a column of as many vertically adjacent cells
* as it needs.
*/
#define ITEM_W 64
#define ITEM_H_QUANTUM 8
G_DEFINE_TYPE (GskGLGlyphyLibrary, gsk_gl_glyphy_library, GSK_TYPE_GL_TEXTURE_LIBRARY)
GskGLGlyphyLibrary *
gsk_gl_glyphy_library_new (GskGLDriver *driver)
{
g_return_val_if_fail (GSK_IS_GL_DRIVER (driver), NULL);
return g_object_new (GSK_TYPE_GL_GLYPHY_LIBRARY,
"driver", driver,
NULL);
}
static guint
gsk_gl_glyphy_key_hash (gconstpointer data)
{
const GskGLGlyphyKey *key = data;
/* malloc()'d pointers already guarantee 3 bits from the LSB on 64-bit and
* 2 bits from the LSB on 32-bit. Shift by enough to give us 256 entries
* in our front cache for the glyph since languages will naturally cluster
* for us.
*/
return (key->font << 8) ^ key->glyph;
}
static gboolean
gsk_gl_glyphy_key_equal (gconstpointer v1,
gconstpointer v2)
{
return memcmp (v1, v2, sizeof (GskGLGlyphyKey)) == 0;
}
static void
gsk_gl_glyphy_key_free (gpointer data)
{
GskGLGlyphyKey *key = data;
g_slice_free (GskGLGlyphyKey, key);
}
static void
gsk_gl_glyphy_value_free (gpointer data)
{
g_slice_free (GskGLGlyphyValue, data);
}
static void
gsk_gl_glyphy_library_clear_cache (GskGLTextureLibrary *library)
{
GskGLGlyphyLibrary *self = (GskGLGlyphyLibrary *)library;
g_assert (GSK_IS_GL_GLYPHY_LIBRARY (self));
memset (self->front, 0, sizeof self->front);
}
static void
gsk_gl_glyphy_library_init_atlas (GskGLTextureLibrary *library,
GskGLTextureAtlas *atlas)
{
g_assert (GSK_IS_GL_GLYPHY_LIBRARY (library));
g_assert (atlas != NULL);
atlas->cursor_x = 0;
atlas->cursor_y = 0;
}
static gboolean
gsk_gl_glyphy_library_allocate (GskGLTextureLibrary *library,
GskGLTextureAtlas *atlas,
int width,
int height,
int *out_x,
int *out_y)
{
GskGLGlyphyLibrary *self = (GskGLGlyphyLibrary *)library;
int cursor_save_x;
int cursor_save_y;
g_assert (GSK_IS_GL_GLYPHY_LIBRARY (self));
g_assert (atlas != NULL);
cursor_save_x = atlas->cursor_x;
cursor_save_y = atlas->cursor_y;
if ((height & (self->item_h_q-1)) != 0)
height = (height + self->item_h_q - 1) & ~(self->item_h_q - 1);
/* Require allocations in columns of 64 and rows of 8 */
g_assert (width == self->item_w);
g_assert ((height % self->item_h_q) == 0);
if (atlas->cursor_y + height > atlas->height)
{
/* Go to next column */
atlas->cursor_x += self->item_w;
atlas->cursor_y = 0;
}
if (atlas->cursor_x + width <= atlas->width &&
atlas->cursor_y + height <= atlas->height)
{
*out_x = atlas->cursor_x;
*out_y = atlas->cursor_y;
atlas->cursor_y += height;
return TRUE;
}
atlas->cursor_x = cursor_save_x;
atlas->cursor_y = cursor_save_y;
return FALSE;
}
static void
gsk_gl_glyphy_library_finalize (GObject *object)
{
GskGLGlyphyLibrary *self = (GskGLGlyphyLibrary *)object;
g_clear_pointer (&self->acc, glyphy_arc_accumulator_destroy);
g_clear_pointer (&self->acc_endpoints, g_array_unref);
G_OBJECT_CLASS (gsk_gl_glyphy_library_parent_class)->finalize (object);
}
GQuark quark_glyphy_font_key;
static void
gsk_gl_glyphy_library_class_init (GskGLGlyphyLibraryClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GskGLTextureLibraryClass *library_class = GSK_GL_TEXTURE_LIBRARY_CLASS (klass);
quark_glyphy_font_key = g_quark_from_static_string ("glyphy-font-key");
object_class->finalize = gsk_gl_glyphy_library_finalize;
library_class->allocate = gsk_gl_glyphy_library_allocate;
library_class->clear_cache = gsk_gl_glyphy_library_clear_cache;
library_class->init_atlas = gsk_gl_glyphy_library_init_atlas;
}
static void
gsk_gl_glyphy_library_init (GskGLGlyphyLibrary *self)
{
GskGLTextureLibrary *tl = (GskGLTextureLibrary *)self;
tl->max_entry_size = 0;
tl->max_frame_age = 512;
tl->atlas_width = 2048;
tl->atlas_height = 1024;
gsk_gl_texture_library_set_funcs (tl,
gsk_gl_glyphy_key_hash,
gsk_gl_glyphy_key_equal,
gsk_gl_glyphy_key_free,
gsk_gl_glyphy_value_free);
self->acc = glyphy_arc_accumulator_create ();
self->acc_endpoints = g_array_new (FALSE, FALSE, sizeof (glyphy_arc_endpoint_t));
self->item_w = ITEM_W;
self->item_h_q = ITEM_H_QUANTUM;
}
static glyphy_bool_t
accumulate_endpoint (glyphy_arc_endpoint_t *endpoint,
GArray *endpoints)
{
g_array_append_vals (endpoints, endpoint, 1);
return TRUE;
}
static void
move_to (hb_draw_funcs_t *dfuncs,
GskPathBuilder *builder,
hb_draw_state_t *st,
float x,
float y,
void *data)
{
gsk_path_builder_move_to (builder, x, y);
}
static void
line_to (hb_draw_funcs_t *dfuncs,
GskPathBuilder *builder,
hb_draw_state_t *st,
float x,
float y,
void *data)
{
gsk_path_builder_line_to (builder, x, y);
}
static void
cubic_to (hb_draw_funcs_t *dfuncs,
GskPathBuilder *builder,
hb_draw_state_t *st,
float x1,
float y1,
float x2,
float y2,
float x3,
float y3,
void *data)
{
gsk_path_builder_cubic_to (builder, x1, y1, x2, y2, x3, y3);
}
static void
close_path (hb_draw_funcs_t *dfuncs,
GskPathBuilder *builder,
hb_draw_state_t *st,
void *data)
{
gsk_path_builder_close (builder);
}
static hb_draw_funcs_t *
gsk_path_get_draw_funcs (void)
{
static hb_draw_funcs_t *funcs = NULL;
if (!funcs)
{
funcs = hb_draw_funcs_create ();
hb_draw_funcs_set_move_to_func (funcs, (hb_draw_move_to_func_t) move_to, NULL, NULL);
hb_draw_funcs_set_line_to_func (funcs, (hb_draw_line_to_func_t) line_to, NULL, NULL);
hb_draw_funcs_set_cubic_to_func (funcs, (hb_draw_cubic_to_func_t) cubic_to, NULL, NULL);
hb_draw_funcs_set_close_path_func (funcs, (hb_draw_close_path_func_t) close_path, NULL, NULL);
hb_draw_funcs_make_immutable (funcs);
}
return funcs;
}
static gboolean
acc_callback (GskPathOperation op,
const graphene_point_t *pts,
gsize n_pts,
float weight,
gpointer user_data)
{
glyphy_arc_accumulator_t *acc = user_data;
glyphy_point_t p0, p1, p2, p3;
switch (op)
{
case GSK_PATH_MOVE:
p0.x = pts[0].x; p0.y = pts[0].y;
glyphy_arc_accumulator_move_to (acc, &p0);
break;
case GSK_PATH_CLOSE:
glyphy_arc_accumulator_close_path (acc);
break;
case GSK_PATH_LINE:
p1.x = pts[1].x; p1.y = pts[1].y;
glyphy_arc_accumulator_line_to (acc, &p1);
break;
case GSK_PATH_QUAD:
p1.x = pts[1].x; p1.y = pts[1].y;
p2.x = pts[2].x; p2.y = pts[2].y;
/* This glyphy api is mis-named */
glyphy_arc_accumulator_conic_to (acc, &p1, &p2);
break;
case GSK_PATH_CUBIC:
p1.x = pts[1].x; p1.y = pts[1].y;
p2.x = pts[2].x; p2.y = pts[2].y;
p3.x = pts[3].x; p3.y = pts[3].y;
glyphy_arc_accumulator_cubic_to (acc, &p1, &p2, &p3);
break;
case GSK_PATH_CONIC:
default:
g_assert_not_reached ();
}
return TRUE;
}
static inline gboolean
encode_glyph (GskGLGlyphyLibrary *self,
hb_font_t *font,
unsigned int glyph_index,
double tolerance_per_em,
glyphy_rgba_t *buffer,
guint buffer_len,
guint *output_len,
guint *nominal_width,
guint *nominal_height,
glyphy_extents_t *extents)
{
hb_face_t *face = hb_font_get_face (font);
guint upem = hb_face_get_upem (face);
double tolerance = upem * tolerance_per_em;
double faraway = (double)upem / (MIN_FONT_SIZE * M_SQRT2);
double unit_size = (double) upem / GRID_SIZE;
double enlighten_max = (double) upem * ENLIGHTEN_MAX;
double embolden_max = (double) upem * EMBOLDEN_MAX;
double avg_fetch_achieved;
GskPathBuilder *builder;
GskPath *path, *simplified;
self->acc_endpoints->len = 0;
glyphy_arc_accumulator_reset (self->acc);
glyphy_arc_accumulator_set_tolerance (self->acc, tolerance);
glyphy_arc_accumulator_set_callback (self->acc,
(glyphy_arc_endpoint_accumulator_callback_t)accumulate_endpoint,
self->acc_endpoints);
builder = gsk_path_builder_new ();
#if HB_VERSION_ATLEAST (7, 0, 0)
hb_font_draw_glyph (font, glyph_index, gsk_path_get_draw_funcs (), builder);
#else
hb_font_get_glyph_shape (font, glyph_index, gsk_path_get_draw_funcs (), builder);
#endif
path = gsk_path_builder_free_to_path (builder);
simplified = gsk_path_op (GSK_PATH_OP_SIMPLIFY, GSK_FILL_RULE_WINDING, path, NULL);
gsk_path_foreach (simplified, -1, acc_callback, self->acc);
gsk_path_unref (simplified);
gsk_path_unref (path);
if (!glyphy_arc_accumulator_successful (self->acc))
return FALSE;
g_assert (glyphy_arc_accumulator_get_error (self->acc) <= tolerance);
if (self->acc_endpoints->len > 0)
glyphy_outline_winding_from_even_odd ((gpointer)self->acc_endpoints->data,
self->acc_endpoints->len,
FALSE);
if (!glyphy_arc_list_encode_blob2 ((gpointer)self->acc_endpoints->data,
self->acc_endpoints->len,
buffer,
buffer_len,
faraway,
unit_size,
enlighten_max,
embolden_max,
&avg_fetch_achieved,
output_len,
nominal_width,
nominal_height,
extents))
return FALSE;
glyphy_extents_scale (extents, 1./upem, 1./upem);
return TRUE;
}
static inline hb_font_t *
get_nominal_size_hb_font (PangoFont *font)
{
hb_font_t *hbfont;
const float *coords;
unsigned int length;
hbfont = (hb_font_t *) g_object_get_data ((GObject *)font, "glyph-nominal-size-font");
if (hbfont == NULL)
{
hbfont = hb_font_create (hb_font_get_face (pango_font_get_hb_font (font)));
coords = hb_font_get_var_coords_design (pango_font_get_hb_font (font), &length);
if (length > 0)
hb_font_set_var_coords_design (hbfont, coords, length);
g_object_set_data_full ((GObject *)font, "glyphy-nominal-size-font",
hbfont, (GDestroyNotify)hb_font_destroy);
}
return hbfont;
}
gboolean
gsk_gl_glyphy_library_add (GskGLGlyphyLibrary *self,
GskGLGlyphyKey *key,
PangoFont *font,
const GskGLGlyphyValue **out_value)
{
static glyphy_rgba_t buffer[4096 * 16];
GskGLTextureLibrary *tl = (GskGLTextureLibrary *)self;
GskGLGlyphyValue *value;
glyphy_extents_t extents;
hb_font_t *hbfont;
guint packed_x;
guint packed_y;
guint nominal_w, nominal_h;
guint output_len = 0;
guint texture_id;
guint width, height;
g_assert (GSK_IS_GL_GLYPHY_LIBRARY (self));
g_assert (key != NULL);
g_assert (font != NULL);
g_assert (out_value != NULL);
hbfont = get_nominal_size_hb_font (font);
/* Convert the glyph to a list of arcs */
if (!encode_glyph (self, hbfont, key->glyph, TOLERANCE,
buffer, sizeof buffer, &output_len,
&nominal_w, &nominal_h, &extents))
return FALSE;
/* Allocate space for list within atlas */
width = self->item_w;
height = (output_len + width - 1) / width;
GSK_DEBUG (GLYPH_CACHE, "font %u glyph %u: %u bytes (%u x %u)", key->font, key->glyph, output_len * 4, width, height);
value = gsk_gl_texture_library_pack (tl, key, sizeof *value,
width, height, 0,
&packed_x, &packed_y);
g_assert (packed_x % ITEM_W == 0);
g_assert (packed_y % ITEM_H_QUANTUM == 0);
/* Make sure we found space to pack */
texture_id = GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (value);
if (texture_id == 0)
return FALSE;
if (!glyphy_extents_is_empty (&extents))
{
/* Connect the texture for data upload */
glActiveTexture (GL_TEXTURE0);
glBindTexture (GL_TEXTURE_2D, texture_id);
g_assert (width > 0);
g_assert (height > 0);
/* Upload the arc list */
if (width * height == output_len)
{
glTexSubImage2D (GL_TEXTURE_2D, 0,
packed_x, packed_y,
width, height,
GL_RGBA, GL_UNSIGNED_BYTE,
buffer);
}
else
{
glTexSubImage2D (GL_TEXTURE_2D, 0,
packed_x, packed_y,
width, height - 1,
GL_RGBA, GL_UNSIGNED_BYTE,
buffer);
/* Upload the last row separately */
glTexSubImage2D (GL_TEXTURE_2D, 0,
packed_x, packed_y + height - 1,
output_len - (width * (height - 1)), 1,
GL_RGBA, GL_UNSIGNED_BYTE,
buffer + (width * (height - 1)));
}
}
value->extents.min_x = extents.min_x;
value->extents.min_y = extents.min_y;
value->extents.max_x = extents.max_x;
value->extents.max_y = extents.max_y;
value->nominal_w = nominal_w;
value->nominal_h = nominal_h;
value->atlas_x = packed_x / self->item_w;
value->atlas_y = packed_y / self->item_h_q;
*out_value = value;
return TRUE;
}

View File

@@ -0,0 +1,142 @@
/* gskglglyphylibraryprivate.h
*
* Copyright 2020-2022 Christian Hergert <chergert@redhat.com>
*
* 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 program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef __GSK_GL_GLYPHY_LIBRARY_PRIVATE_H__
#define __GSK_GL_GLYPHY_LIBRARY_PRIVATE_H__
#include <glyphy.h>
#include <pango/pango.h>
#include "gskgltexturelibraryprivate.h"
G_BEGIN_DECLS
#define GSK_TYPE_GL_GLYPHY_LIBRARY (gsk_gl_glyphy_library_get_type())
typedef guint FontKey;
extern GQuark quark_glyphy_font_key;
static inline FontKey
gsk_gl_glyphy_library_get_font_key (PangoFont *font)
{
FontKey key;
key = (FontKey) GPOINTER_TO_UINT (g_object_get_qdata ((GObject *)font, quark_glyphy_font_key));
if (key == 0)
{
PangoFontDescription *desc = pango_font_describe (font);
pango_font_description_set_size (desc, 10 * PANGO_SCALE);
key = (FontKey) pango_font_description_hash (desc);
pango_font_description_free (desc);
g_object_set_qdata ((GObject *)font, quark_glyphy_font_key, GUINT_TO_POINTER (key));
}
return key;
}
static inline float
gsk_gl_glyphy_library_get_font_scale (PangoFont *font)
{
hb_font_t *hbfont;
int x_scale, y_scale;
hbfont = pango_font_get_hb_font (font);
hb_font_get_scale (hbfont, &x_scale, &y_scale);
return MAX (x_scale, y_scale) / 1000.0;
}
typedef struct _GskGLGlyphyKey
{
FontKey font;
PangoGlyph glyph;
} GskGLGlyphyKey;
typedef struct _GskGLGlyphyValue
{
GskGLTextureAtlasEntry entry;
struct {
float min_x;
float min_y;
float max_x;
float max_y;
} extents;
guint nominal_w;
guint nominal_h;
guint atlas_x;
guint atlas_y;
} GskGLGlyphyValue;
G_DECLARE_FINAL_TYPE (GskGLGlyphyLibrary, gsk_gl_glyphy_library, GSK, GL_GLYPHY_LIBRARY, GskGLTextureLibrary)
struct _GskGLGlyphyLibrary
{
GskGLTextureLibrary parent_instance;
glyphy_arc_accumulator_t *acc;
GArray *acc_endpoints;
guint item_w;
guint item_h_q;
struct {
GskGLGlyphyKey key;
const GskGLGlyphyValue *value;
} front[256];
};
GskGLGlyphyLibrary *gsk_gl_glyphy_library_new (GskGLDriver *driver);
gboolean gsk_gl_glyphy_library_add (GskGLGlyphyLibrary *self,
GskGLGlyphyKey *key,
PangoFont *font,
const GskGLGlyphyValue **out_value);
static inline guint
gsk_gl_glyphy_library_lookup_or_add (GskGLGlyphyLibrary *self,
const GskGLGlyphyKey *key,
PangoFont *font,
const GskGLGlyphyValue **out_value)
{
GskGLTextureAtlasEntry *entry;
guint front_index = key->glyph & 0xFF;
if (memcmp (key, &self->front[front_index], sizeof *key) == 0)
{
*out_value = self->front[front_index].value;
}
else if (gsk_gl_texture_library_lookup ((GskGLTextureLibrary *)self, key, &entry))
{
*out_value = (GskGLGlyphyValue *)entry;
self->front[front_index].key = *key;
self->front[front_index].value = *out_value;
}
else
{
GskGLGlyphyKey *k = g_slice_copy (sizeof *key, key);
gsk_gl_glyphy_library_add (self, k, font, out_value);
self->front[front_index].key = *key;
self->front[front_index].value = *out_value;
}
return GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (*out_value);
}
G_END_DECLS
#endif /* __GSK_GL_GLYPHY_LIBRARY_PRIVATE_H__ */

View File

@@ -87,3 +87,21 @@ GSK_GL_DEFINE_PROGRAM (unblurred_outset_shadow,
GSK_GL_ADD_UNIFORM (1, UNBLURRED_OUTSET_SHADOW_SPREAD, u_spread)
GSK_GL_ADD_UNIFORM (2, UNBLURRED_OUTSET_SHADOW_OFFSET, u_offset)
GSK_GL_ADD_UNIFORM (3, UNBLURRED_OUTSET_SHADOW_OUTLINE_RECT, u_outline_rect))
GSK_GL_DEFINE_PROGRAM (glyphy,
GSK_GL_SHADER_JOINED (VERTEX,
GSK_GL_SHADER_RESOURCE ("glyphy.vs.glsl"),
NULL)
GSK_GL_SHADER_JOINED (FRAGMENT,
GSK_GL_SHADER_RESOURCE ("glyphy.atlas.glsl"),
GSK_GL_SHADER_STRING (glyphy_common_shader_source ()),
GSK_GL_SHADER_STRING ("#define GLYPHY_SDF_PSEUDO_DISTANCE 1\n"),
GSK_GL_SHADER_STRING (glyphy_sdf_shader_source ()),
GSK_GL_SHADER_RESOURCE ("glyphy.fs.glsl"),
NULL),
GSK_GL_ADD_UNIFORM (0, GLYPHY_CONTRAST, u_contrast)
GSK_GL_ADD_UNIFORM (1, GLYPHY_GAMMA_ADJUST, u_gamma_adjust)
GSK_GL_ADD_UNIFORM (2, GLYPHY_OUTLINE_THICKNESS, u_outline_thickness)
GSK_GL_ADD_UNIFORM (3, GLYPHY_OUTLINE, u_outline)
GSK_GL_ADD_UNIFORM (4, GLYPHY_BOLDNESS, u_boldness)
GSK_GL_ADD_UNIFORM (6, GLYPHY_ATLAS_INFO, u_atlas_info))

View File

@@ -150,6 +150,13 @@ gsk_gl_renderer_realize (GskRenderer *renderer,
gsk_gl_command_queue_set_profiler (self->command_queue,
gsk_renderer_get_profiler (renderer));
#ifdef G_ENABLE_DEBUG
if (gsk_renderer_get_debug_flags (renderer) & GSK_DEBUG_NO_GLYPHY)
GSK_RENDERER_DEBUG (renderer, RENDERER, "GL Renderer will use cairo for glyph rendering");
else
GSK_RENDERER_DEBUG (renderer, RENDERER, "GL Renderer will use glyphy for glyph rendering");
#endif
ret = TRUE;
failure:
@@ -307,10 +314,15 @@ gsk_gl_renderer_render (GskRenderer *renderer,
gsk_gl_driver_begin_frame (self->driver, self->command_queue);
job = gsk_gl_render_job_new (self->driver, &viewport, scale, render_region, 0, clear_framebuffer);
if ((gsk_renderer_get_debug_flags (renderer) & GSK_DEBUG_NO_GLYPHY) == 0)
gsk_gl_render_job_set_use_glyphy (job, TRUE);
#ifdef G_ENABLE_DEBUG
if (GSK_RENDERER_DEBUG_CHECK (GSK_RENDERER (self), FALLBACK))
gsk_gl_render_job_set_debug_fallback (job, TRUE);
#endif
gsk_gl_render_job_render (job, root);
gsk_gl_driver_end_frame (self->driver);
gsk_gl_render_job_free (job);

View File

@@ -34,10 +34,14 @@
#include <gsk/gskroundedrectprivate.h>
#include <math.h>
#include <string.h>
#ifdef HAVE_PANGOFT
#include <pango/pangofc-font.h>
#endif
#include "gskglcommandqueueprivate.h"
#include "gskgldriverprivate.h"
#include "gskglglyphlibraryprivate.h"
#include "gskglglyphylibraryprivate.h"
#include "gskgliconlibraryprivate.h"
#include "gskglprogramprivate.h"
#include "gskglrenderjobprivate.h"
@@ -149,6 +153,9 @@ struct _GskGLRenderJob
*/
guint clear_framebuffer : 1;
/* Allow experimental glyph rendering with glyphy */
guint use_glyphy : 1;
/* Format we want to use for intermediate textures, determined by
* looking at the format of the framebuffer we are rendering on.
*/
@@ -2959,10 +2966,10 @@ compute_phase_and_pos (float value, float *pos)
}
static inline void
gsk_gl_render_job_visit_text_node (GskGLRenderJob *job,
const GskRenderNode *node,
const GdkRGBA *color,
gboolean force_color)
gsk_gl_render_job_visit_text_node_legacy (GskGLRenderJob *job,
const GskRenderNode *node,
const GdkRGBA *color,
gboolean force_color)
{
const PangoFont *font = gsk_text_node_get_font (node);
const PangoGlyphInfo *glyphs = gsk_text_node_get_glyphs (node, NULL);
@@ -3098,6 +3105,263 @@ gsk_gl_render_job_visit_text_node (GskGLRenderJob *job,
}
}
/* Keep this in sync with glyph_vertex_transcode in glyphy.vs.glsl */
typedef struct
{
float x;
float y;
float g16hi;
float g16lo;
} EncodedGlyph;
static inline unsigned int
glyph_encode (guint atlas_x , /* 7 bits */
guint atlas_y, /* 7 bits */
guint corner_x, /* 1 bit */
guint corner_y, /* 1 bit */
guint nominal_w, /* 6 bits */
guint nominal_h) /* 6 bits */
{
guint x, y;
g_assert (0 == (atlas_x & ~0x7F));
g_assert (0 == (atlas_y & ~0x7F));
g_assert (0 == (corner_x & ~1));
g_assert (0 == (corner_y & ~1));
g_assert (0 == (nominal_w & ~0x3F));
g_assert (0 == (nominal_h & ~0x3F));
x = (((atlas_x << 6) | nominal_w) << 1) | corner_x;
y = (((atlas_y << 6) | nominal_h) << 1) | corner_y;
return (x << 16) | y;
}
static inline void
encoded_glyph_init (EncodedGlyph *eg,
float x,
float y,
guint corner_x,
guint corner_y,
const GskGLGlyphyValue *gi)
{
guint encoded = glyph_encode (gi->atlas_x, gi->atlas_y, corner_x, corner_y, gi->nominal_w, gi->nominal_h);
eg->x = x;
eg->y = y;
eg->g16hi = encoded >> 16;
eg->g16lo = encoded & 0xFFFF;
}
static inline void
add_encoded_glyph (GskGLDrawVertex *vertices,
const EncodedGlyph *eg,
const guint16 c[4])
{
*vertices = (GskGLDrawVertex) { .position = { eg->x, eg->y}, .uv = { eg->g16hi, eg->g16lo}, .color = { c[0], c[1], c[2], c[3] } };
}
static void
get_synthetic_font_params (PangoFont *font,
gboolean *embolden,
PangoMatrix *matrix)
{
*embolden = FALSE;
*matrix = (PangoMatrix) PANGO_MATRIX_INIT;
#ifdef HAVE_PANGOFT
if (PANGO_IS_FC_FONT (font))
{
FcPattern *pattern = pango_fc_font_get_pattern (PANGO_FC_FONT (font));
FcBool b;
FcMatrix mat;
FcMatrix *m;
if (FcPatternGetBool (pattern, FC_EMBOLDEN, 0, &b) == FcResultMatch)
*embolden = b;
FcMatrixInit (&mat);
for (int i = 0; FcPatternGetMatrix (pattern, FC_MATRIX, i, &m) == FcResultMatch; i++)
FcMatrixMultiply (&mat, &mat, m);
matrix->xx = mat.xx;
matrix->xy = mat.xy;
matrix->yx = mat.yx;
matrix->yy = mat.yy;
}
#endif
}
static inline void
gsk_gl_render_job_visit_text_node_glyphy (GskGLRenderJob *job,
const GskRenderNode *node,
const GdkRGBA *color)
{
const graphene_point_t *offset;
const PangoGlyphInfo *glyphs;
const PangoGlyphInfo *gi;
GskGLGlyphyLibrary *library;
GskGLCommandBatch *batch;
PangoFont *font;
GskGLDrawVertex *vertices;
const guint16 *c;
GskGLGlyphyKey lookup;
guint16 cc[4];
float x;
float y;
guint last_texture = 0;
guint num_glyphs;
guint used = 0;
guint i;
int x_position = 0;
float font_scale;
gboolean embolden;
PangoMatrix matrix = PANGO_MATRIX_INIT;
#define GRID_SIZE 20
g_assert (!gsk_text_node_has_color_glyphs (node));
if (!(num_glyphs = gsk_text_node_get_num_glyphs (node)))
return;
if (RGBA_IS_CLEAR (color))
return;
font = (PangoFont *)gsk_text_node_get_font (node);
get_synthetic_font_params (font, &embolden, &matrix);
glyphs = gsk_text_node_get_glyphs (node, NULL);
library = job->driver->glyphy_library;
offset = gsk_text_node_get_offset (node);
x = offset->x + job->offset_x;
y = offset->y + job->offset_y;
rgba_to_half (color, cc);
c = cc;
gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, glyphy));
batch = gsk_gl_command_queue_get_batch (job->command_queue);
vertices = gsk_gl_command_queue_add_n_vertices (job->command_queue, num_glyphs);
lookup.font = gsk_gl_glyphy_library_get_font_key (font);
font_scale = gsk_gl_glyphy_library_get_font_scale (font);
for (i = 0, gi = glyphs; i < num_glyphs; i++, gi++)
{
const GskGLGlyphyValue *glyph;
float cx = 0, cy = 0;
guint texture_id;
lookup.glyph = gi->glyph;
texture_id = gsk_gl_glyphy_library_lookup_or_add (library, &lookup, font, &glyph);
if G_UNLIKELY (texture_id == 0)
continue;
if G_UNLIKELY (last_texture != texture_id || batch->draw.vbo_count + GSK_GL_N_VERTICES > 0xffff)
{
if G_LIKELY (last_texture != 0)
{
guint vbo_offset = batch->draw.vbo_offset + batch->draw.vbo_count;
/* Since we have batched added our VBO vertices to avoid repeated
* calls to the buffer, we need to manually tweak the vbo offset
* of the new batch as otherwise it will point at the end of our
* vbo array.
*/
gsk_gl_render_job_split_draw (job);
batch = gsk_gl_command_queue_get_batch (job->command_queue);
batch->draw.vbo_offset = vbo_offset;
}
gsk_gl_program_set_uniform4i (job->current_program,
UNIFORM_GLYPHY_ATLAS_INFO, 0,
GSK_GL_TEXTURE_LIBRARY (library)->atlas_width,
GSK_GL_TEXTURE_LIBRARY (library)->atlas_height,
library->item_w,
library->item_h_q);
gsk_gl_program_set_uniform_texture (job->current_program,
UNIFORM_SHARED_SOURCE, 0,
GL_TEXTURE_2D,
GL_TEXTURE0,
texture_id);
gsk_gl_program_set_uniform1f (job->current_program,
UNIFORM_GLYPHY_GAMMA_ADJUST, 0,
1.0);
gsk_gl_program_set_uniform1f (job->current_program,
UNIFORM_GLYPHY_CONTRAST, 0,
1.0);
/* 0.0208 is the value used by freetype for synthetic emboldening */
gsk_gl_program_set_uniform1f (job->current_program,
UNIFORM_GLYPHY_BOLDNESS, 0,
embolden ? 0.0208 * GRID_SIZE : 0.0);
#if 0
gsk_gl_program_set_uniform1f (job->current_program,
UNIFORM_GLYPHY_OUTLINE_THICKNESS, 0,
1.0);
gsk_gl_program_set_uniform1f (job->current_program,
UNIFORM_GLYPHY_OUTLINE, 0,
1.0);
#endif
last_texture = texture_id;
}
cx = (float)(x_position + gi->geometry.x_offset) / PANGO_SCALE;
if G_UNLIKELY (gi->geometry.y_offset != 0)
cy = (float)(gi->geometry.y_offset) / PANGO_SCALE;
x_position += gi->geometry.width;
EncodedGlyph encoded[4];
#define ENCODE_CORNER(_cx, _cy) \
G_STMT_START { \
float _dx = _cx * (glyph->extents.max_x - glyph->extents.min_x); \
float _dy = _cy * (glyph->extents.max_y - glyph->extents.min_y); \
float _vx = x + cx + font_scale * (glyph->extents.min_x + matrix.xx * _dx + matrix.xy * _dy); \
float _vy = y + cy - font_scale * (glyph->extents.min_y + matrix.yx * _dx + matrix.yy * _dy); \
encoded_glyph_init (&encoded[_cx * 2 + _cy], _vx, _vy, _cx, _cy, glyph); \
} G_STMT_END
ENCODE_CORNER (0, 0);
ENCODE_CORNER (0, 1);
ENCODE_CORNER (1, 0);
ENCODE_CORNER (1, 1);
#undef ENCODE_CORNER
add_encoded_glyph (vertices++, &encoded[0], c);
add_encoded_glyph (vertices++, &encoded[1], c);
add_encoded_glyph (vertices++, &encoded[2], c);
add_encoded_glyph (vertices++, &encoded[1], c);
add_encoded_glyph (vertices++, &encoded[2], c);
add_encoded_glyph (vertices++, &encoded[3], c);
batch->draw.vbo_count += GSK_GL_N_VERTICES;
used++;
}
if (used != num_glyphs)
gsk_gl_command_queue_retract_n_vertices (job->command_queue, num_glyphs - used);
gsk_gl_render_job_end_draw (job);
}
static inline void
gsk_gl_render_job_visit_text_node (GskGLRenderJob *job,
const GskRenderNode *node,
const GdkRGBA *color,
gboolean force_color)
{
if (job->use_glyphy && !gsk_text_node_has_color_glyphs (node))
gsk_gl_render_job_visit_text_node_glyphy (job, node, color);
else
gsk_gl_render_job_visit_text_node_legacy (job, node, color, force_color);
}
static inline void
gsk_gl_render_job_visit_shadow_node (GskGLRenderJob *job,
const GskRenderNode *node)
@@ -4463,6 +4727,15 @@ gsk_gl_render_job_set_debug_fallback (GskGLRenderJob *job,
job->debug_fallback = !!debug_fallback;
}
void
gsk_gl_render_job_set_use_glyphy (GskGLRenderJob *job,
gboolean use_glyphy)
{
g_return_if_fail (job != NULL);
job->use_glyphy = !!use_glyphy;
}
static int
get_framebuffer_format (GdkGLContext *context,
guint framebuffer)

View File

@@ -35,4 +35,5 @@ void gsk_gl_render_job_render_flipped (GskGLRenderJob *job
GskRenderNode *root);
void gsk_gl_render_job_set_debug_fallback (GskGLRenderJob *job,
gboolean debug_fallback);
void gsk_gl_render_job_set_use_glyphy (GskGLRenderJob *job,
gboolean use_glyphy);

View File

@@ -36,9 +36,14 @@ G_BEGIN_DECLS
typedef struct _GskGLTextureAtlas
{
/* Used by Glyph/Icons */
struct stbrp_context context;
struct stbrp_node *nodes;
/* Used by Glyphy */
int cursor_x;
int cursor_y;
int width;
int height;
@@ -48,7 +53,6 @@ typedef struct _GskGLTextureAtlas
* But are now unused.
*/
int unused_pixels;
} GskGLTextureAtlas;
typedef struct _GskGLTextureAtlasEntry

View File

@@ -36,6 +36,7 @@ typedef struct _GskGLCompiler GskGLCompiler;
typedef struct _GskGLDrawVertex GskGLDrawVertex;
typedef struct _GskGLRenderTarget GskGLRenderTarget;
typedef struct _GskGLGlyphLibrary GskGLGlyphLibrary;
typedef struct _GskGLGlyphyLibrary GskGLGlyphyLibrary;
typedef struct _GskGLIconLibrary GskGLIconLibrary;
typedef struct _GskGLProgram GskGLProgram;
typedef struct _GskGLRenderJob GskGLRenderJob;

View File

@@ -0,0 +1,15 @@
uniform ivec4 u_atlas_info;
#define GLYPHY_TEXTURE1D_EXTRA_DECLS , sampler2D _tex, ivec4 _atlas_info, ivec2 _atlas_pos
#define GLYPHY_TEXTURE1D_EXTRA_ARGS , _tex, _atlas_info, _atlas_pos
#define GLYPHY_DEMO_EXTRA_ARGS , u_source, u_atlas_info, gi.atlas_pos
vec4
glyphy_texture1D_func (int offset GLYPHY_TEXTURE1D_EXTRA_DECLS)
{
ivec2 item_geom = _atlas_info.zw;
vec2 pos = (vec2 (_atlas_pos.xy * item_geom +
ivec2 (mod (float (offset), float (item_geom.x)), offset / item_geom.x)) +
+ vec2 (.5, .5)) / vec2(_atlas_info.xy);
return GskTexture (_tex, pos);
}

View File

@@ -0,0 +1,62 @@
// FRAGMENT_SHADER:
// glyphy.fs.glsl
uniform float u_contrast;
uniform float u_gamma_adjust;
uniform float u_outline_thickness;
uniform bool u_outline;
uniform float u_boldness;
_IN_ vec4 v_glyph;
_IN_ vec4 final_color;
#define SQRT2 1.4142135623730951
#define SQRT2_INV 0.70710678118654757 /* 1 / sqrt(2.) */
struct glyph_info_t {
ivec2 nominal_size;
ivec2 atlas_pos;
};
glyph_info_t
glyph_info_decode (vec4 v)
{
glyph_info_t gi;
gi.nominal_size = (ivec2 (mod (v.zw, 256.)) + 2) / 4;
gi.atlas_pos = ivec2 (v_glyph.zw) / 256;
return gi;
}
float
antialias (float d)
{
return smoothstep (-.75, +.75, d);
}
void
main()
{
vec2 p = v_glyph.xy;
glyph_info_t gi = glyph_info_decode (v_glyph);
/* isotropic antialiasing */
vec2 dpdx = dFdx (p);
vec2 dpdy = dFdy (p);
float m = length (vec2 (length (dpdx), length (dpdy))) * SQRT2_INV;
float gsdist = glyphy_sdf (p, gi.nominal_size GLYPHY_DEMO_EXTRA_ARGS);
gsdist -= u_boldness;
float sdist = gsdist / m * u_contrast;
if (u_outline)
sdist = abs (sdist) - u_outline_thickness * .5;
if (sdist > 1.)
discard;
float alpha = antialias (-sdist);
if (u_gamma_adjust != 1.)
alpha = pow (alpha, 1./u_gamma_adjust);
gskSetOutputColor(final_color * alpha);
}

View File

@@ -0,0 +1,25 @@
// VERTEX_SHADER:
// glyphy.vs.glsl
_OUT_ vec4 v_glyph;
_OUT_ vec4 final_color;
// Keep this in sync with glyph_encode in gskglrenderjob.c
vec4
glyph_vertex_transcode (vec2 v)
{
ivec2 g = ivec2 (v);
ivec2 corner = ivec2 (mod (v, 2.));
g /= 2;
ivec2 nominal_size = ivec2 (mod (vec2(g), 64.));
return vec4 (corner * nominal_size, g * 4);
}
void
main()
{
v_glyph = glyph_vertex_transcode(aUv);
vUv = v_glyph.zw;
gl_Position = u_projection * u_modelview * vec4(aPosition, 0.0, 1.0);
final_color = gsk_scaled_premultiply(aColor, u_alpha);
}

View File

@@ -1,3 +1,5 @@
#extension GL_OES_standard_derivatives : enable
#ifndef GSK_LEGACY
precision highp float;
#endif

100
gsk/gskboundingboxprivate.h Normal file
View File

@@ -0,0 +1,100 @@
#pragma once
#include <gsk/gsktypes.h>
G_BEGIN_DECLS
typedef struct _GskBoundingBox GskBoundingBox;
struct _GskBoundingBox {
graphene_point_t min;
graphene_point_t max;
};
static inline GskBoundingBox *
gsk_bounding_box_init (GskBoundingBox *self,
const graphene_point_t *a,
const graphene_point_t *b)
{
self->min.x = MIN (a->x, b->x);
self->min.y = MIN (a->y, b->y);
self->max.x = MAX (a->x, b->x);
self->max.y = MAX (a->y, b->y);
return self;
}
static inline GskBoundingBox *
gsk_bounding_box_init_copy (GskBoundingBox *self,
const GskBoundingBox *src)
{
self->min = src->min;
self->max = src->max;
return self;
}
static inline GskBoundingBox *
gsk_bounding_box_init_from_rect (GskBoundingBox *self,
const graphene_rect_t *bounds)
{
self->min = bounds->origin;
self->max.x = bounds->origin.x + bounds->size.width;
self->max.y = bounds->origin.y + bounds->size.height;
return self;
}
static inline void
gsk_bounding_box_expand (GskBoundingBox *self,
const graphene_point_t *p)
{
self->min.x = MIN (self->min.x, p->x);
self->min.y = MIN (self->min.y, p->y);
self->max.x = MAX (self->max.x, p->x);
self->max.y = MAX (self->max.y, p->y);
}
static inline graphene_rect_t *
gsk_bounding_box_to_rect (const GskBoundingBox *self,
graphene_rect_t *rect)
{
rect->origin = self->min;
rect->size.width = self->max.x - self->min.x;
rect->size.height = self->max.y - self->min.y;
return rect;
}
static inline gboolean
gsk_bounding_box_contains_point (const GskBoundingBox *self,
const graphene_point_t *p)
{
return self->min.x <= p->x && p->x <= self->max.x &&
self->min.y <= p->y && p->y <= self->max.y;
}
static inline gboolean
gsk_bounding_box_contains_point_with_epsilon (const GskBoundingBox *self,
const graphene_point_t *p,
float epsilon)
{
return self->min.x - epsilon <= p->x && p->x <= self->max.x + epsilon &&
self->min.y - epsilon <= p->y && p->y <= self->max.y + epsilon;
}
static inline gboolean
gsk_bounding_box_intersection (const GskBoundingBox *a,
const GskBoundingBox *b,
GskBoundingBox *res)
{
graphene_point_t min, max;
min.x = MAX (a->min.x, b->min.x);
min.y = MAX (a->min.y, b->min.y);
max.x = MIN (a->max.x, b->max.x);
max.y = MIN (a->max.y, b->max.y);
if (res)
gsk_bounding_box_init (res, &min, &max);
return min.x <= max.x && min.y <= max.y;
}
G_END_DECLS

2436
gsk/gskcontour.c Normal file

File diff suppressed because it is too large Load Diff

103
gsk/gskcontourprivate.h Normal file
View File

@@ -0,0 +1,103 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#pragma once
#include "gskpath.h"
#include "gskpathopprivate.h"
G_BEGIN_DECLS
typedef enum
{
GSK_PATH_FLAT,
GSK_PATH_CLOSED
} GskPathFlags;
typedef struct _GskContour GskContour;
GskContour * gsk_rect_contour_new (const graphene_rect_t *rect);
GskContour * gsk_rounded_rect_contour_new (const GskRoundedRect *rounded_rect);
GskContour * gsk_circle_contour_new (const graphene_point_t *center,
float radius,
float start_angle,
float end_angle);
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,
graphene_rect_t *bounds);
gpointer gsk_contour_init_measure (const GskContour *self,
float tolerance,
float *out_length);
void gsk_contour_free_measure (const GskContour *self,
gpointer data);
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);
void gsk_contour_get_point (const GskContour *self,
gpointer measure_data,
float distance,
GskPathDirection direction,
graphene_point_t *pos,
graphene_vec2_t *tangent);
gboolean gsk_contour_get_closest_point (const GskContour *self,
gpointer measure_data,
float tolerance,
const graphene_point_t *point,
float threshold,
float *out_distance,
graphene_point_t *out_pos,
float *out_offset,
graphene_vec2_t *out_tangent);
int gsk_contour_get_winding (const GskContour *self,
gpointer measure_data,
const graphene_point_t *point);
void gsk_contour_add_segment (const GskContour *self,
GskPathBuilder *builder,
gpointer measure_data,
gboolean emit_move_to,
float start,
float end);
float gsk_contour_get_curvature (const GskContour *self,
gpointer measure_data,
float distance,
graphene_point_t *center);
G_END_DECLS

2034
gsk/gskcurve.c Normal file

File diff suppressed because it is too large Load Diff

879
gsk/gskcurveintersect.c Normal file
View File

@@ -0,0 +1,879 @@
/*
* Copyright © 2020 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 <http://www.gnu.org/licenses/>.
*
* Authors: Matthias Clasen <mclasen@redhat.com>
*/
#include "config.h"
#include <math.h>
#include "gskcurveprivate.h"
/* {{{ 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);
}
static inline gboolean
acceptable (float t)
{
return 0 - FLT_EPSILON <= t && t <= 1 + FLT_EPSILON;
}
static inline void
_sincosf (float angle,
float *out_s,
float *out_c)
{
#ifdef HAVE_SINCOSF
sincosf (angle, out_s, out_c);
#else
*out_s = sinf (angle);
*out_c = cosf (angle);
#endif
}
static void
align_points (const graphene_point_t *p,
const graphene_point_t *a,
const graphene_point_t *b,
graphene_point_t *q,
int n)
{
graphene_vec2_t n1;
float angle;
float s, c;
get_tangent (a, b, &n1);
angle = - atan2 (graphene_vec2_get_y (&n1), graphene_vec2_get_x (&n1));
_sincosf (angle, &s, &c);
for (int i = 0; i < n; i++)
{
q[i].x = (p[i].x - a->x) * c - (p[i].y - a->y) * s;
q[i].y = (p[i].x - a->x) * s + (p[i].y - a->y) * c;
}
}
static void
find_point_on_line (const graphene_point_t *p1,
const graphene_point_t *p2,
const graphene_point_t *q,
float *t)
{
if (p2->x != p1->x)
*t = (q->x - p1->x) / (p2->x - p1->x);
else
*t = (q->y - p1->y) / (p2->y - p1->y);
}
/* find solutions for at^2 + bt + c = 0 */
static int
solve_quadratic (float a, float b, float c, float t[2])
{
float d;
int n = 0;
if (fabs (a) > 0.0001)
{
if (b*b > 4*a*c)
{
d = sqrt (b*b - 4*a*c);
t[n++] = (-b + d)/(2*a);
t[n++] = (-b - d)/(2*a);
}
else
{
t[n++] = -b / (2*a);
}
}
else if (fabs (b) > 0.0001)
{
t[n++] = -c / b;
}
return n;
}
static int
filter_allowable (float t[3],
int n)
{
float g[3];
int j = 0;
for (int i = 0; i < n; i++)
if (0 < t[i] && t[i] < 1)
g[j++] = t[i];
for (int i = 0; i < j; i++)
t[i] = g[i];
return j;
}
/* Solve P = 0 where P is
* P = (1-t)^2*pa + 2*t*(1-t)*pb + t^2*pc
*/
static int
get_quadratic_roots (float pa, float pb, float pc, float roots[2])
{
float a, b, c, d;
int n_roots = 0;
a = pa - 2 * pb + pc;
b = 2 * (pb - pa);
c = pa;
d = b*b - 4*a*c;
if (d > 0.0001)
{
float q = sqrt (d);
roots[n_roots] = (-b + q) / (2 * a);
if (acceptable (roots[n_roots]))
n_roots++;
roots[n_roots] = (-b - q) / (2 * a);
if (acceptable (roots[n_roots]))
n_roots++;
}
else if (fabs (d) < 0.0001)
{
roots[n_roots] = -b / (2 * a);
if (acceptable (roots[n_roots]))
n_roots++;
}
return n_roots;
}
static float
cuberoot (float v)
{
if (v < 0)
return -pow (-v, 1.f / 3);
return pow (v, 1.f / 3);
}
/* 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_roots (float pa, float pb, float pc, float pd, float roots[3])
{
float a, b, c, d;
float q, q2;
float p, p3;
float discriminant;
float u1, v1, sd;
int n_roots = 0;
d = -pa + 3*pb - 3*pc + pd;
a = 3*pa - 6*pb + 3*pc;
b = -3*pa + 3*pb;
c = pa;
if (fabs (d) < 0.0001)
{
if (fabs (a) < 0.0001)
{
if (fabs (b) < 0.0001)
return 0;
if (acceptable (-c / b))
{
roots[0] = -c / b;
return 1;
}
return 0;
}
q = sqrt (b*b - 4*a*c);
roots[n_roots] = (-b + q) / (2 * a);
if (acceptable (roots[n_roots]))
n_roots++;
roots[n_roots] = (-b - q) / (2 * a);
if (acceptable (roots[n_roots]))
n_roots++;
return n_roots;
}
a /= d;
b /= d;
c /= d;
p = (3*b - a*a)/3;
p3 = p/3;
q = (2*a*a*a - 9*a*b + 27*c)/27;
q2 = q/2;
discriminant = q2*q2 + p3*p3*p3;
if (discriminant < 0)
{
float mp3 = -p/3;
float mp33 = mp3*mp3*mp3;
float r = sqrt (mp33);
float t = -q / (2*r);
float cosphi = t < -1 ? -1 : (t > 1 ? 1 : t);
float phi = acos (cosphi);
float crtr = cuberoot (r);
float t1 = 2*crtr;
roots[n_roots] = t1 * cos (phi/3) - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
roots[n_roots] = t1 * cos ((phi + 2*M_PI) / 3) - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
roots[n_roots] = t1 * cos ((phi + 4*M_PI) / 3) - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
return n_roots;
}
if (discriminant == 0)
{
u1 = q2 < 0 ? cuberoot (-q2) : -cuberoot (q2);
roots[n_roots] = 2*u1 - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
roots[n_roots] = -u1 - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
return n_roots;
}
sd = sqrt (discriminant);
u1 = cuberoot (sd - q2);
v1 = cuberoot (sd + q2);
roots[n_roots] = u1 - v1 - a/3;
if (acceptable (roots[n_roots]))
n_roots++;
return n_roots;
}
/* }}} */
/* {{{ Cusps and inflections */
/* Get the points where the curvature of curve is
* zero, or a maximum or minimum, inside the open
* interval from 0 to 1.
*/
int
gsk_curve_get_curvature_points (const GskCurve *curve,
float t[3])
{
const graphene_point_t *pts = curve->cubic.points;
graphene_point_t p[4];
float a, b, c, d;
float x, y, z;
int n;
if (curve->op != GSK_PATH_CUBIC)
return 0;
align_points (pts, &pts[0], &pts[3], p, 4);
a = p[2].x * p[1].y;
b = p[3].x * p[1].y;
c = p[1].x * p[2].y;
d = p[3].x * p[2].y;
x = - 3*a + 2*b + 3*c - d;
y = 3*a - b - 3*c;
z = c - a;
n = solve_quadratic (x, y, z, t);
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 &&
fabs (ay * ti * ti + by * ti + cy) < 0.001)
t[n++] = ti;
}
return n;
}
/* }}} */
/* {{{ Intersection */
static int
line_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
const graphene_point_t *pts1 = curve1->line.points;
const graphene_point_t *pts2 = curve2->line.points;
float a1 = pts1[0].x - pts1[1].x;
float b1 = pts1[0].y - pts1[1].y;
float a2 = pts2[0].x - pts2[1].x;
float b2 = pts2[0].y - pts2[1].y;
float det = a1 * b2 - b1 * a2;
if (fabs(det) > 0.01)
{
float tt = ((pts1[0].x - pts2[0].x) * b2 - (pts1[0].y - pts2[0].y) * a2) / det;
float ss = - ((pts1[0].y - pts2[0].y) * a1 - (pts1[0].x - pts2[0].x) * b1) / det;
if (acceptable (tt) && acceptable (ss))
{
p[0].x = pts1[0].x + tt * (pts1[1].x - pts1[0].x);
p[0].y = pts1[0].y + tt * (pts1[1].y - pts1[0].y);
t1[0] = tt;
t2[0] = ss;
return 1;
}
}
else /* parallel lines */
{
float r = a1 * (pts1[1].y - pts2[0].y) - (pts1[1].x - pts2[0].x) * b1;
float dist = (r * r) / (a1 * a1 + b1 * b1);
float t, s, tt, ss;
if (dist > 0.01)
return 0;
if (pts1[1].x != pts1[0].x)
{
t = (pts2[0].x - pts1[0].x) / (pts1[1].x - pts1[0].x);
s = (pts2[1].x - pts1[0].x) / (pts1[1].x - pts1[0].x);
}
else
{
t = (pts2[0].y - pts1[0].y) / (pts1[1].y - pts1[0].y);
s = (pts2[1].y - pts1[0].y) / (pts1[1].y - pts1[0].y);
}
if ((t < 0 && s < 0) || (t > 1 && s > 1))
return 0;
if (acceptable (t))
{
t1[0] = t;
t2[0] = 0;
p[0] = pts2[0];
}
else if (t < 0)
{
if (pts2[1].x != pts2[0].x)
tt = (pts1[0].x - pts2[0].x) / (pts2[1].x - pts2[0].x);
else
tt = (pts1[0].y - pts2[0].y) / (pts2[1].y - pts2[0].y);
t1[0] = 0;
t2[0] = tt;
p[0] = pts1[0];
}
else
{
if (pts2[1].x != pts2[0].x)
tt = (pts1[1].x - pts2[0].x) / (pts2[1].x - pts2[0].x);
else
tt = (pts1[1].y - pts2[0].y) / (pts2[1].y - pts2[0].y);
t1[0] = 1;
t2[0] = tt;
p[0] = pts1[1];
}
if (acceptable (s))
{
if (t2[0] == 1)
return 1;
t1[1] = s;
t2[1] = 1;
p[1] = pts2[1];
}
else if (s < 0)
{
if (t1[0] == 0)
return 1;
if (pts2[1].x != pts2[0].x)
ss = (pts1[0].x - pts2[0].x) / (pts2[1].x - pts2[0].x);
else
ss = (pts1[0].y - pts2[0].y) / (pts2[1].y - pts2[0].y);
t1[1] = 0;
t2[1] = ss;
p[1] = pts1[0];
}
else
{
if (t1[0] == 1)
return 1;
if (pts2[1].x != pts2[0].x)
ss = (pts1[1].x - pts2[0].x) / (pts2[1].x - pts2[0].x);
else
ss = (pts1[1].y - pts2[0].y) / (pts2[1].y - pts2[0].y);
t1[1] = 1;
t2[1] = ss;
p[1] = pts1[1];
}
return 2;
}
return 0;
}
static int
line_quad_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
const graphene_point_t *a = &curve1->line.points[0];
const graphene_point_t *b = &curve1->line.points[1];
graphene_point_t pts[4];
float t[2];
int m, i, j;
/* Rotate things to place curve1 on the x axis,
* then solve curve2 for y == 0.
*/
align_points (curve2->quad.points, a, b, pts, 3);
m = get_quadratic_roots (pts[0].y, pts[1].y, pts[2].y, t);
j = 0;
for (i = 0; i < m; i++)
{
t2[j] = t[i];
gsk_curve_get_point (curve2, t2[j], &p[j]);
find_point_on_line (a, b, &p[j], &t1[j]);
if (acceptable (t1[j]))
j++;
if (j == n)
break;
}
return j;
}
static int
line_cubic_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
const graphene_point_t *a = &curve1->line.points[0];
const graphene_point_t *b = &curve1->line.points[1];
graphene_point_t pts[4];
float t[3];
int m, i, j;
/* Rotate things to place curve1 on the x axis,
* then solve curve2 for y == 0.
*/
align_points (curve2->cubic.points, a, b, pts, 4);
m = get_cubic_roots (pts[0].y, pts[1].y, pts[2].y, pts[3].y, t);
j = 0;
for (i = 0; i < m; i++)
{
t2[j] = t[i];
gsk_curve_get_point (curve2, t2[j], &p[j]);
find_point_on_line (a, b, &p[j], &t1[j]);
if (acceptable (t1[j]))
j++;
if (j == n)
break;
}
return j;
}
#define MAX_LEVEL 25
#define TOLERANCE 0.001
static void
cubic_intersect_recurse (const GskCurve *curve1,
const GskCurve *curve2,
float t1l,
float t1r,
float t2l,
float t2r,
float *t1,
float *t2,
graphene_point_t *p,
int n,
int *pos,
int level)
{
GskCurve p11, p12, p21, p22;
GskBoundingBox b1, b2;
float d1, d2;
if (*pos == n)
return;
if (level == MAX_LEVEL)
return;
gsk_curve_get_bounds (curve1, &b1);
gsk_curve_get_bounds (curve2, &b2);
if (!gsk_bounding_box_intersection (&b1, &b2, NULL))
return;
gsk_curve_get_tight_bounds (curve1, &b1);
if (!gsk_bounding_box_intersection (&b1, &b2, NULL))
return;
gsk_curve_get_tight_bounds (curve2, &b2);
if (!gsk_bounding_box_intersection (&b1, &b2, NULL))
return;
d1 = (t1r - t1l) / 2;
d2 = (t2r - t2l) / 2;
if (b1.max.x - b1.min.x < TOLERANCE && b1.max.y - b1.min.y < TOLERANCE &&
b2.max.x - b2.min.x < TOLERANCE && b2.max.y - b2.min.y < TOLERANCE)
{
graphene_point_t c;
t1[*pos] = t1l + d1;
t2[*pos] = t2l + d2;
gsk_curve_get_point (curve1, 0.5, &c);
for (int i = 0; i < *pos; i++)
{
if (graphene_point_near (&c, &p[i], 0.1))
return;
}
p[*pos] = c;
(*pos)++;
return;
}
gsk_curve_split (curve1, 0.5, &p11, &p12);
gsk_curve_split (curve2, 0.5, &p21, &p22);
cubic_intersect_recurse (&p11, &p21, t1l, t1l + d1, t2l, t2l + d2, t1, t2, p, n, pos, level + 1);
cubic_intersect_recurse (&p11, &p22, t1l, t1l + d1, t2l + d2, t2r, t1, t2, p, n, pos, level + 1);
cubic_intersect_recurse (&p12, &p21, t1l + d1, t1r, t2l, t2l + d2, t1, t2, p, n, pos, level + 1);
cubic_intersect_recurse (&p12, &p22, t1l + d1, t1r, t2l + d2, t2r, t1, t2, p, n, pos, level + 1);
}
static int
cubic_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
int pos = 0;
cubic_intersect_recurse (curve1, curve2, 0, 1, 0, 1, t1, t2, p, n, &pos, 0);
return pos;
}
static void
get_bounds (const GskCurve *curve,
float tl,
float tr,
GskBoundingBox *bounds)
{
GskCurve c;
gsk_curve_segment (curve, tl, tr, &c);
gsk_curve_get_tight_bounds (&c, bounds);
}
static void
general_intersect_recurse (const GskCurve *curve1,
const GskCurve *curve2,
float t1l,
float t1r,
float t2l,
float t2r,
float *t1,
float *t2,
graphene_point_t *p,
int n,
int *pos,
int level)
{
GskBoundingBox b1, b2;
float d1, d2;
if (*pos == n)
return;
if (level == MAX_LEVEL)
return;
get_bounds (curve1, t1l, t1r, &b1);
get_bounds (curve2, t2l, t2r, &b2);
if (!gsk_bounding_box_intersection (&b1, &b2, NULL))
return;
d1 = (t1r - t1l) / 2;
d2 = (t2r - t2l) / 2;
if (b1.max.x - b1.min.x < TOLERANCE && b1.max.y - b1.min.y < TOLERANCE &&
b2.max.x - b2.min.x < TOLERANCE && b2.max.y - b2.min.y < TOLERANCE)
{
graphene_point_t c;
t1[*pos] = t1l + d1;
t2[*pos] = t2l + d2;
gsk_curve_get_point (curve1, t1[*pos], &c);
for (int i = 0; i < *pos; i++)
{
if (graphene_point_near (&c, &p[i], 0.1))
return;
}
p[*pos] = c;
(*pos)++;
return;
}
/* Note that in the conic case, we cannot just split the curves and
* pass the two halves down, since splitting changes the parametrization,
* and we need the t's to be valid parameters wrt to the original curve.
*
* So, instead, we determine the bounding boxes above by always starting
* from the original curve. That is a bit less efficient, but also works
* for conics.
*/
general_intersect_recurse (curve1, curve2, t1l, t1l + d1, t2l, t2l + d2, t1, t2, p, n, pos, level + 1);
general_intersect_recurse (curve1, curve2, t1l, t1l + d1, t2l + d2, t2r, t1, t2, p, n, pos, level + 1);
general_intersect_recurse (curve1, curve2, t1l + d1, t1r, t2l, t2l + d2, t1, t2, p, n, pos, level + 1);
general_intersect_recurse (curve1, curve2, t1l + d1, t1r, t2l + d2, t2r, t1, t2, p, n, pos, level + 1);
}
static int
general_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
int pos = 0;
general_intersect_recurse (curve1, curve2, 0, 1, 0, 1, t1, t2, p, n, &pos, 0);
return pos;
}
static int
curve_self_intersect (const GskCurve *curve,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
float tt[3], ss[3], s;
graphene_point_t pp[3];
int m;
GskCurve cs, ce;
if (curve->op != GSK_PATH_CUBIC)
return 0;
s = 0.5;
m = gsk_curve_get_curvature_points (curve, tt);
for (int i = 0; i < m; i++)
{
if (gsk_curve_get_curvature (curve, tt[i], NULL) == 0)
{
s = tt[i];
break;
}
}
gsk_curve_split (curve, s, &cs, &ce);
m = cubic_intersect (&cs, &ce, tt, ss, pp, 3);
if (m > 1)
{
/* One of the (at most 2) intersections we found
* must be the common point where we split the curve.
* It will have a t value of 1 and an s value of 0.
*/
if (fabs (tt[0] - 1) > 1e-3)
{
t1[0] = t2[0] = tt[0] * s;
p[0] = pp[0];
}
else if (fabs (tt[1] - 1) > 1e-3)
{
t1[0] = t2[0] = tt[1] * s;
p[0] = pp[1];
}
if (n == 1)
return 1;
if (fabs (ss[0]) > 1e-3)
{
t1[1] = t2[1] = s + ss[0] * (1 - s);
p[1] = pp[0];
}
else if (fabs (ss[1]) > 1e-3)
{
t1[1] = t2[1] = s + ss[1] * (1 - s);
p[1] = pp[1];
}
return 2;
}
return 0;
}
static inline gboolean
curve_equal (const GskCurve *c1,
const GskCurve *c2)
{
gsize curve_size[] = {
sizeof (GskLineCurve),
sizeof (GskLineCurve),
sizeof (GskLineCurve),
sizeof (GskQuadCurve),
sizeof (GskCubicCurve),
sizeof (GskConicCurve)
};
return c1->op == c2->op && memcmp (c1, c2, curve_size[c1->op]) == 0;
}
/* Place intersections between the curves in p, and their Bezier positions
* in t1 and t2, up to n. Return the number of intersections found.
*
* Note that two cubic Beziers can have up to 9 intersections.
*/
int
gsk_curve_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n)
{
GskPathOperation op1 = curve1->op;
GskPathOperation op2 = curve2->op;
if (op1 == GSK_PATH_CLOSE)
op1 = GSK_PATH_LINE;
if (op2 == GSK_PATH_CLOSE)
op2 = GSK_PATH_LINE;
if (curve_equal (curve1, curve2))
return curve_self_intersect (curve1, t1, t2, p, n);
/* We special-case line-line and line-cubic intersections,
* since we can solve them directly.
* Everything else is done via bisection.
*/
if (op1 == GSK_PATH_LINE && op2 == GSK_PATH_LINE)
return line_intersect (curve1, curve2, t1, t2, p, n);
else if (op1 == GSK_PATH_LINE && op2 == GSK_PATH_QUAD)
return line_quad_intersect (curve1, curve2, t1, t2, p, n);
else if (op1 == GSK_PATH_QUAD && op2 == GSK_PATH_LINE)
return line_quad_intersect (curve2, curve1, t2, t1, p, n);
else if (op1 == GSK_PATH_LINE && op2 == GSK_PATH_CUBIC)
return line_cubic_intersect (curve1, curve2, t1, t2, p, n);
else if (op1 == GSK_PATH_CUBIC && op2 == GSK_PATH_LINE)
return line_cubic_intersect (curve2, curve1, t2, t1, p, n);
else if ((op1 == GSK_PATH_QUAD || op1 == GSK_PATH_CUBIC) &&
(op2 == GSK_PATH_QUAD || op2 == GSK_PATH_CUBIC))
return cubic_intersect (curve1, curve2, t1, t2, p, n);
else
return general_intersect (curve1, curve2, t1, t2, p, n);
}
/* }}} */
/* vim:set foldmethod=marker expandtab: */

181
gsk/gskcurveprivate.h Normal file
View File

@@ -0,0 +1,181 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#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;
typedef struct _GskConicCurve GskConicCurve;
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];
};
struct _GskConicCurve
{
GskPathOperation op;
gboolean has_coefficients;
/* points[0], points[1], points[3] are the control points,
* points[2].x is the weight
*/
graphene_point_t points[4];
graphene_point_t num[3];
graphene_point_t denom[3];
};
union _GskCurve
{
GskPathOperation op;
GskLineCurve line;
GskQuadCurve quad;
GskCubicCurve cubic;
GskConicCurve conic;
};
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,
float weight,
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,
float weight);
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_curvature_points (const GskCurve *curve,
float t[3]);
int gsk_curve_get_cusps (const GskCurve *curve,
float t[2]);
int gsk_curve_intersect (const GskCurve *curve1,
const GskCurve *curve2,
float *t1,
float *t2,
graphene_point_t *p,
int n);
G_END_DECLS

View File

@@ -17,6 +17,7 @@ static const GdkDebugKey gsk_debug_keys[] = {
{ "full-redraw", GSK_DEBUG_FULL_REDRAW, "Force full redraws" },
{ "sync", GSK_DEBUG_SYNC, "Sync after each frame" },
{ "staging", GSK_DEBUG_STAGING, "Use a staging image for texture upload (Vulkan only)" },
{ "no-glyphy", GSK_DEBUG_NO_GLYPHY, "Don't use GPU for glyph rendering (OpenGL only)", TRUE },
};
static guint gsk_debug_flags;

View File

@@ -19,7 +19,8 @@ typedef enum {
GSK_DEBUG_GEOMETRY = 1 << 9,
GSK_DEBUG_FULL_REDRAW = 1 << 10,
GSK_DEBUG_SYNC = 1 << 11,
GSK_DEBUG_STAGING = 1 << 12
GSK_DEBUG_STAGING = 1 << 12,
GSK_DEBUG_NO_GLYPHY = 1 << 13
} GskDebugFlags;
#define GSK_DEBUG_ANY ((1 << 13) - 1)

View File

@@ -170,6 +170,49 @@ typedef enum {
GSK_CORNER_BOTTOM_LEFT
} GskCorner;
/**
* 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.
* @GSK_PATH_CONIC: A weighted quadratic Bézier curve with 3 points describing
* the start point, control point and end point of the curve. A weight for the
* curve will be passed, too.
*
* Path operations can be used to approximate a `GskPath`.
*
* More values may be added in the future.
**/
typedef enum {
GSK_PATH_MOVE,
GSK_PATH_CLOSE,
GSK_PATH_LINE,
GSK_PATH_QUAD,
GSK_PATH_CUBIC,
GSK_PATH_CONIC,
} 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.
*/
typedef enum {
GSK_PATH_START,
GSK_PATH_END
} GskPathDirection;
/**
* GskSerializationError:
* @GSK_SERIALIZATION_UNSUPPORTED_FORMAT: The format can not be identified
@@ -274,4 +317,3 @@ typedef enum
GSK_MASK_MODE_LUMINANCE,
GSK_MASK_MODE_INVERTED_LUMINANCE
} GskMaskMode;

1187
gsk/gskpath.c Normal file

File diff suppressed because it is too large Load Diff

98
gsk/gskpath.h Normal file
View File

@@ -0,0 +1,98 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#pragma once
#if !defined (__GSK_H_INSIDE__) && !defined (GTK_COMPILATION)
#error "Only <gsk/gsk.h> can be included directly."
#endif
#include <gsk/gsktypes.h>
G_BEGIN_DECLS
typedef enum {
GSK_FILL_RULE_WINDING,
GSK_FILL_RULE_EVEN_ODD
} GskFillRule;
/**
* GskPathForeachFlags:
* @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_CONIC: Allow emission of `GSK_PATH_CONIC` operations.
*
* 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_QUAD = (1 << 0),
GSK_PATH_FOREACH_ALLOW_CUBIC = (1 << 1),
GSK_PATH_FOREACH_ALLOW_CONIC = (1 << 2),
} GskPathForeachFlags;
/**
* GskPathForeachFunc:
* @op: The operation to perform
* @pts: The points of the operation
* @n_pts: The number of points
* @weight: The weight for conic curves, or unused if not a conic curve.
* @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,
float weight,
gpointer user_data);
#define GSK_TYPE_PATH (gsk_path_get_type ())
GType gsk_path_get_type (void) G_GNUC_CONST;
GskPath * gsk_path_ref (GskPath *self);
void gsk_path_unref (GskPath *self);
void gsk_path_print (GskPath *self,
GString *string);
char * gsk_path_to_string (GskPath *self);
GskPath * gsk_path_parse (const char *string);
void gsk_path_to_cairo (GskPath *self,
cairo_t *cr);
gboolean gsk_path_is_empty (GskPath *self);
gboolean gsk_path_get_bounds (GskPath *self,
graphene_rect_t *bounds);
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

1062
gsk/gskpathbuilder.c Normal file

File diff suppressed because it is too large Load Diff

118
gsk/gskpathbuilder.h Normal file
View File

@@ -0,0 +1,118 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#pragma once
#if !defined (__GSK_H_INSIDE__) && !defined (GTK_COMPILATION)
#error "Only <gsk/gsk.h> can be included directly."
#endif
#include <gsk/gskroundedrect.h>
#include <gsk/gsktypes.h>
G_BEGIN_DECLS
#define GSK_TYPE_PATH_BUILDER (gsk_path_builder_get_type ())
GType gsk_path_builder_get_type (void) G_GNUC_CONST;
GskPathBuilder * gsk_path_builder_new (void);
GskPathBuilder * gsk_path_builder_ref (GskPathBuilder *self);
void gsk_path_builder_unref (GskPathBuilder *self);
GskPath * gsk_path_builder_free_to_path (GskPathBuilder *self) G_GNUC_WARN_UNUSED_RESULT;
GskPath * gsk_path_builder_to_path (GskPathBuilder *self) G_GNUC_WARN_UNUSED_RESULT;
const graphene_point_t *gsk_path_builder_get_current_point (GskPathBuilder *self);
void gsk_path_builder_add_path (GskPathBuilder *self,
GskPath *path);
void gsk_path_builder_add_reverse_path (GskPathBuilder *self,
GskPath *path);
void gsk_path_builder_add_cairo_path (GskPathBuilder *self,
const cairo_path_t *path);
void gsk_path_builder_add_layout (GskPathBuilder *self,
PangoLayout *layout);
void gsk_path_builder_add_rect (GskPathBuilder *self,
const graphene_rect_t *rect);
void gsk_path_builder_add_rounded_rect (GskPathBuilder *self,
const GskRoundedRect *rect);
void gsk_path_builder_add_circle (GskPathBuilder *self,
const graphene_point_t *center,
float radius);
void gsk_path_builder_add_ellipse (GskPathBuilder *self,
const graphene_point_t *center,
const graphene_size_t *radius);
void gsk_path_builder_add_segment (GskPathBuilder *self,
GskPathMeasure *measure,
float start,
float end);
void gsk_path_builder_move_to (GskPathBuilder *self,
float x,
float y);
void gsk_path_builder_rel_move_to (GskPathBuilder *self,
float x,
float y);
void gsk_path_builder_line_to (GskPathBuilder *self,
float x,
float y);
void gsk_path_builder_rel_line_to (GskPathBuilder *self,
float x,
float y);
void gsk_path_builder_quad_to (GskPathBuilder *self,
float x1,
float y1,
float x2,
float y2);
void gsk_path_builder_rel_quad_to (GskPathBuilder *self,
float x1,
float y1,
float x2,
float y2);
void gsk_path_builder_cubic_to (GskPathBuilder *self,
float x1,
float y1,
float x2,
float y2,
float x3,
float y3);
void gsk_path_builder_rel_cubic_to (GskPathBuilder *self,
float x1,
float y1,
float x2,
float y2,
float x3,
float y3);
void gsk_path_builder_conic_to (GskPathBuilder *self,
float x1,
float y1,
float x2,
float y2,
float weight);
void gsk_path_builder_rel_conic_to (GskPathBuilder *self,
float x1,
float y1,
float x2,
float y2,
float weight);
void gsk_path_builder_close (GskPathBuilder *self);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GskPathBuilder, gsk_path_builder_unref)
G_END_DECLS

489
gsk/gskpathmeasure.c Normal file
View File

@@ -0,0 +1,489 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include "config.h"
#include "gskpathmeasure.h"
#include "gskpathbuilder.h"
#include "gskpathpointprivate.h"
#include "gskpathprivate.h"
/*< private >
* `GskPathMeasure` is an object that allows measurements
* on `GskPath`s such as determining the length of the path.
*
* Many measuring operations require approximating the path
* with simpler shapes. Therefore, a `GskPathMeasure` has
* a tolerance that determines what amount is required
* for such approximations.
*
* A `GskPathMeasure` struct is a reference counted struct
* and should be treated as opaque.
*/
typedef struct _GskContourMeasure GskContourMeasure;
struct _GskContourMeasure
{
float length;
gpointer contour_data;
};
struct _GskPathMeasure
{
/*< private >*/
guint ref_count;
GskPath *path;
float tolerance;
float length;
gsize n_contours;
GskContourMeasure measures[];
};
G_DEFINE_BOXED_TYPE (GskPathMeasure, gsk_path_measure,
gsk_path_measure_ref,
gsk_path_measure_unref)
/*< private >
* gsk_path_measure_new:
* @path: the path to measure
*
* Creates a measure object for the given @path.
*
* Returns: a new `GskPathMeasure` representing @path
*/
GskPathMeasure *
gsk_path_measure_new (GskPath *path)
{
return gsk_path_measure_new_with_tolerance (path, GSK_PATH_TOLERANCE_DEFAULT);
}
/*< private >
* gsk_path_measure_new_with_tolerance:
* @path: the path to measure
* @tolerance: the tolerance for measuring operations
*
* Creates a measure object for the given @path and @tolerance.
*
* Returns: a new `GskPathMeasure` representing @path
*/
GskPathMeasure *
gsk_path_measure_new_with_tolerance (GskPath *path,
float tolerance)
{
GskPathMeasure *self;
gsize i, n_contours;
g_return_val_if_fail (path != NULL, NULL);
g_return_val_if_fail (tolerance > 0, NULL);
n_contours = gsk_path_get_n_contours (path);
self = g_malloc0 (sizeof (GskPathMeasure) + n_contours * sizeof (GskContourMeasure));
self->ref_count = 1;
self->path = gsk_path_ref (path);
self->tolerance = tolerance;
self->n_contours = n_contours;
for (i = 0; i < n_contours; i++)
{
self->measures[i].contour_data = gsk_contour_init_measure (gsk_path_get_contour (path, i),
self->tolerance,
&self->measures[i].length);
self->length += self->measures[i].length;
}
return self;
}
/*< private >
* gsk_path_measure_ref:
* @self: a `GskPathMeasure`
*
* Increases the reference count of a `GskPathMeasure` by one.
*
* Returns: the passed in `GskPathMeasure`.
*/
GskPathMeasure *
gsk_path_measure_ref (GskPathMeasure *self)
{
g_return_val_if_fail (self != NULL, NULL);
self->ref_count++;
return self;
}
/*< private >
* gsk_path_measure_unref:
* @self: a `GskPathMeasure`
*
* Decreases the reference count of a `GskPathMeasure` by one.
*
* If the resulting reference count is zero, frees the object.
*/
void
gsk_path_measure_unref (GskPathMeasure *self)
{
gsize i;
g_return_if_fail (self != NULL);
g_return_if_fail (self->ref_count > 0);
self->ref_count--;
if (self->ref_count > 0)
return;
for (i = 0; i < self->n_contours; i++)
{
gsk_contour_free_measure (gsk_path_get_contour (self->path, i),
self->measures[i].contour_data);
}
gsk_path_unref (self->path);
g_free (self);
}
/*< private >
* gsk_path_measure_get_path:
* @self: a `GskPathMeasure`
*
* Returns the path that the measure was created for.
*
* Returns: (transfer none): the path of @self
*/
GskPath *
gsk_path_measure_get_path (GskPathMeasure *self)
{
return self->path;
}
/*< private >
* gsk_path_measure_get_tolerance:
* @self: a `GskPathMeasure`
*
* Returns the tolerance that the measure was created with.
*
* Returns: the tolerance of @self
*/
float
gsk_path_measure_get_tolerance (GskPathMeasure *self)
{
return self->tolerance;
}
/*< private >
* gsk_path_measure_get_length:
* @self: a `GskPathMeasure`
*
* Gets the length of the path being measured.
*
* The length is cached, so this function does not do any work.
*
* Returns: The length of the path measured by @self
*/
float
gsk_path_measure_get_length (GskPathMeasure *self)
{
g_return_val_if_fail (self != NULL, 0);
return self->length;
}
/*< private >
* gsk_path_measure_is_closed:
* @self: a `GskPathMeasure`
*
* Returns if the path being measured represents a single closed
* contour.
*
* Returns: `TRUE` if the current path is closed
*/
gboolean
gsk_path_measure_is_closed (GskPathMeasure *self)
{
const GskContour *contour;
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;
contour = gsk_path_get_contour (self->path, 0);
return gsk_contour_get_flags (contour) & GSK_PATH_CLOSED ? TRUE : FALSE;
}
static float
gsk_path_measure_clamp_distance (GskPathMeasure *self,
float distance)
{
if (isnan (distance))
return 0;
return CLAMP (distance, 0, self->length);
}
/*< private >
* gsk_path_measure_in_fill:
* @self: a `GskPathMeasure`
* @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.
*
* Returns: `TRUE` if @point is inside
*/
gboolean
gsk_path_measure_in_fill (GskPathMeasure *self,
const graphene_point_t *point,
GskFillRule fill_rule)
{
int winding = 0;
int i;
for (i = 0; i < self->n_contours; i++)
winding += gsk_contour_get_winding (gsk_path_get_contour (self->path, i),
self->measures[i].contour_data,
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 ();
}
}
static void
gsk_path_builder_add_segment_chunk (GskPathBuilder *self,
GskPathMeasure *measure,
gboolean emit_move_to,
float start,
float end)
{
g_assert (start < end);
for (gsize i = 0; i < measure->n_contours; i++)
{
if (measure->measures[i].length < start)
{
start -= measure->measures[i].length;
end -= measure->measures[i].length;
}
else if (start > 0 || end < measure->measures[i].length)
{
float len = MIN (end, measure->measures[i].length);
gsk_contour_add_segment (gsk_path_get_contour (measure->path, i),
self,
measure->measures[i].contour_data,
emit_move_to,
start,
len);
end -= len;
start = 0;
if (end <= 0)
break;
}
else
{
end -= measure->measures[i].length;
gsk_path_builder_add_contour (self, gsk_contour_dup (gsk_path_get_contour (measure->path, i)));
}
emit_move_to = TRUE;
}
}
/*< private >
* gsk_path_builder_add_segment:
* @self: a `GskPathBuilder`
* @measure: the `GskPathMeasure` to take the segment to
* @start: start distance into the path
* @end: end distance into the path
*
* Adds to @self the segment of @measure from @start to @end.
*
* The distances are given relative to the length of @measure's path,
* from 0 for the beginning of the path to its length for the end
* of the path. The values will be clamped to that range. The length
* can be obtained with [method@Gsk.PathMeasure.get_length].
*
* If @start >= @end after clamping, the path will first add the segment
* from @start to the end of the path, and then add the segment from
* the beginning to @end. If the path is closed, these segments will
* be connected.
*/
void
gsk_path_builder_add_segment (GskPathBuilder *self,
GskPathMeasure *measure,
float start,
float end)
{
g_return_if_fail (self != NULL);
g_return_if_fail (measure != NULL);
start = gsk_path_measure_clamp_distance (measure, start);
end = gsk_path_measure_clamp_distance (measure, end);
if (start < end)
{
gsk_path_builder_add_segment_chunk (self, measure, TRUE, start, end);
}
else
{
/* If the path is closed, we can connect the 2 subpaths. */
gboolean closed = gsk_path_measure_is_closed (measure);
gboolean need_move_to = !closed;
if (start < measure->length)
gsk_path_builder_add_segment_chunk (self, measure,
TRUE,
start, measure->length);
else
need_move_to = TRUE;
if (end > 0)
gsk_path_builder_add_segment_chunk (self, measure,
need_move_to,
0, end);
if (start == end && closed)
gsk_path_builder_close (self);
}
}
/*< private >
* gsk_path_measure_get_point:
* @self: a `GskPathMeasure`
* @distance: the distance
*
* Returns a `GskPathPoint` representing the point at the given
* distance into the path.
*
* An empty path has no points, so `NULL` is returned in that case.
*
* Returns: (transfer full) (nullable): a newly allocated `GskPathPoint`
*/
GskPathPoint *
gsk_path_measure_get_point (GskPathMeasure *self,
float distance)
{
gsize i;
float contour_offset;
float offset;
const GskContour *contour;
g_return_val_if_fail (self != NULL, NULL);
if (self->n_contours == 0)
return NULL;
contour_offset = 0;
offset = gsk_path_measure_clamp_distance (self, distance);
for (i = 0; i < self->n_contours - 1; i++)
{
if (offset < self->measures[i].length)
break;
contour_offset += self->measures[i].length;
offset -= self->measures[i].length;
}
g_assert (0 <= i && i < self->n_contours);
offset = CLAMP (offset, 0, self->measures[i].length);
contour = gsk_path_get_contour (self->path, i);
return gsk_path_point_new (self,
contour, self->measures[i].contour_data,
contour_offset, offset);
}
/*< private >
* gsk_path_measure_get_closest_point:
* @self: a `GskPathMeasure`
* @point: the point to fond the closest point to
* @threshold: The maximum allowed distance between the path and @point.
* Use `INFINITY` to look for any point.
*
* Returns a `GskPathPoint` representing the point on the path
* that is closest to the given point.
*
* If no point on the path is closer than @threshold, `NULL` is returned.
*
* Returns: (transfer full) (nullable): a newly allocated `GskPathPoint`
*/
GskPathPoint *
gsk_path_measure_get_closest_point (GskPathMeasure *self,
const graphene_point_t *point,
float threshold)
{
gssize best_idx;
float best_offset;
float best_contour_offset;
float contour_offset;
contour_offset = 0;
best_idx = -1;
for (gsize i = 0; i < self->n_contours; i++)
{
float distance, offset;
if (gsk_contour_get_closest_point (gsk_path_get_contour (self->path, i),
self->measures[i].contour_data,
self->tolerance,
point,
threshold,
&distance,
NULL,
&offset,
NULL))
{
best_idx = i;
best_offset = offset;
best_contour_offset = contour_offset;
if (distance < self->tolerance)
break;
threshold = distance - self->tolerance;
}
contour_offset += self->measures[i].length;
}
if (best_idx != -1)
return gsk_path_point_new (self,
gsk_path_get_contour (self->path, best_idx),
self->measures[best_idx].contour_data,
best_contour_offset, best_offset);
return NULL;
}

55
gsk/gskpathmeasure.h Normal file
View File

@@ -0,0 +1,55 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#pragma once
#if !defined (__GSK_H_INSIDE__) && !defined (GTK_COMPILATION)
#error "Only <gsk/gsk.h> can be included directly."
#endif
#include <gsk/gskpath.h>
#include <gsk/gskpathpoint.h>
G_BEGIN_DECLS
#define GSK_TYPE_PATH_MEASURE (gsk_path_measure_get_type ())
GType gsk_path_measure_get_type (void) G_GNUC_CONST;
GskPathMeasure * gsk_path_measure_new (GskPath *path);
GskPathMeasure * gsk_path_measure_new_with_tolerance (GskPath *path,
float tolerance);
GskPathMeasure * gsk_path_measure_ref (GskPathMeasure *self);
void gsk_path_measure_unref (GskPathMeasure *self);
GskPath * gsk_path_measure_get_path (GskPathMeasure *self) G_GNUC_PURE;
float gsk_path_measure_get_tolerance (GskPathMeasure *self) G_GNUC_PURE;
float gsk_path_measure_get_length (GskPathMeasure *self);
gboolean gsk_path_measure_is_closed (GskPathMeasure *self);
gboolean gsk_path_measure_in_fill (GskPathMeasure *self,
const graphene_point_t *point,
GskFillRule fill_rule);
GskPathPoint * gsk_path_measure_get_point (GskPathMeasure *self,
float distance);
GskPathPoint * gsk_path_measure_get_closest_point (GskPathMeasure *self,
const graphene_point_t *point,
float threshold);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GskPathMeasure, gsk_path_measure_unref)
G_END_DECLS

186
gsk/gskpathopprivate.h Normal file
View File

@@ -0,0 +1,186 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#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, 0, user_data);
case GSK_PATH_CLOSE:
case GSK_PATH_LINE:
return func (gsk_pathop_op (pop), gsk_pathop_points (pop), 2, 0, user_data);
case GSK_PATH_QUAD:
return func (gsk_pathop_op (pop), gsk_pathop_points (pop), 3, 0, user_data);
case GSK_PATH_CUBIC:
return func (gsk_pathop_op (pop), gsk_pathop_points (pop), 4, 0, user_data);
case GSK_PATH_CONIC:
{
const graphene_point_t *pts = gsk_pathop_points (pop);
return func (gsk_pathop_op (pop), (graphene_point_t[3]) { pts[0], pts[1], pts[3] }, 3, pts[2].x, 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;
case GSK_PATH_CONIC:
gsk_path_builder_conic_to (builder, pts[1].x, pts[1].y, pts[3].x, pts[3].y, pts[2].x);
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;
case GSK_PATH_CONIC:
gsk_path_builder_conic_to (builder, pts[1].x, pts[1].y, pts[0].x, pts[0].y, pts[2].x);
break;
default:
g_assert_not_reached ();
break;
}
}
G_END_DECLS

1726
gsk/gskpathops.c Normal file

File diff suppressed because it is too large Load Diff

195
gsk/gskpathpoint.c Normal file
View File

@@ -0,0 +1,195 @@
#include "config.h"
#include "gskpathpointprivate.h"
#include "gskcontourprivate.h"
#include "gskpathmeasure.h"
#include "gdk/gdkprivate.h"
/**
* GskPathPoint:
*
* `GskPathPoint` is an opaque, immutable 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.PathMeasure.get_path_point]
* or [method@Gsk.PathMeasure.get_closest_point].
*/
struct _GskPathPoint
{
guint ref_count;
GskPathMeasure *measure;
const GskContour *contour;
gpointer measure_data;
float contour_offset; /* distance from beginning of path to contour */
float offset; /* offset of point inside contour */
};
G_DEFINE_BOXED_TYPE (GskPathPoint, gsk_path_point,
gsk_path_point_ref,
gsk_path_point_unref)
GskPathPoint *
gsk_path_point_new (GskPathMeasure *measure,
const GskContour *contour,
gpointer measure_data,
float contour_offset,
float offset)
{
GskPathPoint *self;
self = g_new0 (GskPathPoint, 1);
self->ref_count = 1;
self->measure = gsk_path_measure_ref (measure);
self->contour = contour;
self->measure_data = measure_data;
self->contour_offset = contour_offset;
self->offset = offset;
return self;
}
/**
* gsk_path_point_ref:
* @self: a `GskPathPoint`
*
* Increases the reference count of a `GskPathPoint` by one.
*
* Returns: the passed in `GskPathPoint`
*/
GskPathPoint *
gsk_path_point_ref (GskPathPoint *self)
{
g_return_val_if_fail (self != NULL, NULL);
self->ref_count++;
return self;
}
/**
* gsk_path_point_unref:
* @self: a `GskPathPoint`
*
* Decreases the reference count of a `GskPathPoint` by one.
*
* If the resulting reference count is zero, frees the path_measure.
*/
void
gsk_path_point_unref (GskPathPoint *self)
{
g_return_if_fail (self != NULL);
g_return_if_fail (self->ref_count > 0);
self->ref_count--;
if (self->ref_count > 0)
return;
gsk_path_measure_unref (self->measure);
g_free (self);
}
GskPathMeasure *
gsk_path_point_get_measure (GskPathPoint *self)
{
return self->measure;
}
/**
* gsk_path_point_get_distance:
* @self: a `GskPathPoint`
*
* Returns the distance of the given point from the start of the path.
*
* This is the length of the contour from the beginning of the path
* to the point.
*
* Returns: The offset of point in path
*/
float
gsk_path_point_get_distance (GskPathPoint *self)
{
return self->contour_offset + self->offset;
}
/**
* gsk_path_point_get_position:
* @self: a `GskPathPoint`
* @position: (out caller-allocates): Return location for
* the coordinates of the point
*
* Gets the position of the point.
*/
void
gsk_path_point_get_position (GskPathPoint *self,
graphene_point_t *position)
{
gsk_contour_get_point (self->contour,
self->measure_data,
self->offset,
GSK_PATH_END,
position, NULL);
}
/**
* gsk_path_point_get_tangent:
* @self: a `GskPathPoint`
* @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.
*/
void
gsk_path_point_get_tangent (GskPathPoint *self,
GskPathDirection direction,
graphene_vec2_t *tangent)
{
gsk_contour_get_point (self->contour,
self->measure_data,
self->offset,
direction,
NULL, tangent);
}
/**
* gsk_path_point_get_curvature:
* @self: a `GskPathPoint`
* @center: (out caller-allocates): Return location for
* the center of the osculating circle
*
* Calculates the curvature at the point @distance units into
* the path.
*
* 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
*/
float
gsk_path_point_get_curvature (GskPathPoint *self,
graphene_point_t *center)
{
return gsk_contour_get_curvature (self->contour,
self->measure_data,
self->offset,
center);
}

29
gsk/gskpathpoint.h Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#if !defined (__GSK_H_INSIDE__) && !defined (GTK_COMPILATION)
#error "Only <gsk/gsk.h> can be included directly."
#endif
#include <gsk/gsktypes.h>
G_BEGIN_DECLS
#define GSK_TYPE_PATH_POINT (gsk_path_point_get_type ())
GType gsk_path_point_get_type (void) G_GNUC_CONST;
GskPathPoint * gsk_path_point_ref (GskPathPoint *self);
void gsk_path_point_unref (GskPathPoint *self);
GskPathMeasure * gsk_path_point_get_measure (GskPathPoint *self);
float gsk_path_point_get_distance (GskPathPoint *self);
void gsk_path_point_get_position (GskPathPoint *self,
graphene_point_t *position);
void gsk_path_point_get_tangent (GskPathPoint *self,
GskPathDirection direction,
graphene_vec2_t *tangent);
float gsk_path_point_get_curvature (GskPathPoint *self,
graphene_point_t *center);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GskPathPoint, gsk_path_point_unref)
G_END_DECLS

16
gsk/gskpathpointprivate.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include "gskpathpoint.h"
#include "gskcontourprivate.h"
#include "gskpathmeasure.h"
G_BEGIN_DECLS
GskPathPoint * gsk_path_point_new (GskPathMeasure *measure,
const GskContour *contour,
gpointer measure_data,
float contour_offset,
float offset);
G_END_DECLS

77
gsk/gskpathprivate.h Normal file
View File

@@ -0,0 +1,77 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#pragma once
#include "gskpath.h"
#include "gskcontourprivate.h"
#include "gskpathopprivate.h"
G_BEGIN_DECLS
/* 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 (GskPath *path);
const GskContour * gsk_path_get_contour (GskPath *path,
gsize i);
GskPathFlags gsk_path_get_flags (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);
typedef enum
{
GSK_PATH_OP_SIMPLIFY,
GSK_PATH_OP_UNION,
GSK_PATH_OP_INTERSECTION,
GSK_PATH_OP_DIFFERENCE,
GSK_PATH_OP_XOR
} GskPathOp;
GskPath * gsk_path_op (GskPathOp operation,
GskFillRule fill_rule,
GskPath *first,
GskPath *second);
G_END_DECLS

208
gsk/gskspline.c Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
* Carl D. Worth <cworth@cworth.org>
*/
#include "config.h"
#include "gsksplineprivate.h"
#include <math.h>
/* 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);
}

41
gsk/gsksplineprivate.h Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#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__ */

View File

@@ -25,6 +25,10 @@
#include <gdk/gdk.h>
#include <gsk/gskenums.h>
typedef struct _GskPath GskPath;
typedef struct _GskPathBuilder GskPathBuilder;
typedef struct _GskPathMeasure GskPathMeasure;
typedef struct _GskPathPoint GskPathPoint;
typedef struct _GskRenderer GskRenderer;
typedef struct _GskTransform GskTransform;

View File

@@ -20,11 +20,14 @@ gsk_private_gl_shaders = [
'gl/resources/custom.glsl',
'gl/resources/filled_border.glsl',
'gl/resources/mask.glsl',
'gl/resources/glyphy.atlas.glsl',
'gl/resources/glyphy.fs.glsl',
'gl/resources/glyphy.vs.glsl',
]
gsk_public_sources = files([
'gskdiff.c',
'gskcairorenderer.c',
'gskdiff.c',
'gskglshader.c',
'gskrenderer.c',
'gskrendernode.c',
@@ -37,15 +40,25 @@ gsk_public_sources = files([
gsk_private_sources = files([
'gskcairoblur.c',
'gskcontour.c',
'gskcurve.c',
'gskcurveintersect.c',
'gskdebug.c',
'gskpath.c',
'gskpathbuilder.c',
'gskpathmeasure.c',
'gskpathops.c',
'gskpathpoint.c',
'gskprivate.c',
'gskprofiler.c',
'gskspline.c',
'gl/gskglattachmentstate.c',
'gl/gskglbuffer.c',
'gl/gskglcommandqueue.c',
'gl/gskglcompiler.c',
'gl/gskgldriver.c',
'gl/gskglglyphlibrary.c',
'gl/gskglglyphylibrary.c',
'gl/gskgliconlibrary.c',
'gl/gskglprogram.c',
'gl/gskglrenderjob.c',
@@ -185,6 +198,7 @@ gsk_deps = [
cairo_csi_dep,
pixbuf_dep,
libgdk_dep,
libglyphy_dep
]
libgsk_f16c = static_library('gsk_f16c',

View File

@@ -1015,6 +1015,7 @@ gtk_deps = [
epoxy_dep,
libm,
graphene_dep,
libglyphy_dep,
]
if x11_enabled

View File

@@ -14,7 +14,7 @@ project('gtk', 'c',
glib_req = '>= 2.76.0'
introspection_req = '>= 1.76.0' # keep this in sync with glib
pango_req = '>= 1.50.0' # keep this in sync with .gitlab-ci/test-msys.sh
harfbuzz_req = '>= 2.6.0'
harfbuzz_req = '>= 4.0.0'
fribidi_req = '>= 1.0.6'
cairo_req = '>= 1.14.0'
gdk_pixbuf_req = '>= 2.30.0'
@@ -25,6 +25,7 @@ epoxy_req = '>= 1.4'
cloudproviders_req = '>= 0.3.1'
xkbcommon_req = '>= 0.2.0'
sysprof_req = '>= 3.38.0'
libglyphy_req = '>= 0.2.0'
fs = import('fs')
gnome = import('gnome')
@@ -378,6 +379,9 @@ fribidi_dep = dependency('fribidi', version: fribidi_req,
default_options: ['docs=false'])
harfbuzz_dep = dependency('harfbuzz', version: harfbuzz_req,
default_options: ['coretext=enabled'])
libglyphy_dep = dependency('glyphy', version: libglyphy_req,
default_options: ['default_library=static', 'demo=disabled'],
fallback : ['glyphy', 'libglyphy_dep'])
# Require PangoFT2 if on X11 or wayland
pangoft_dep = dependency('pangoft2', version: pango_req,

6
subprojects/glyphy.wrap Normal file
View File

@@ -0,0 +1,6 @@
[wrap-git]
directory=glyphy
url=https://github.com/behdad/glyphy.git
revision=master
depth=1

123
tests/bigfont.c Normal file
View File

@@ -0,0 +1,123 @@
#include <gtk/gtk.h>
#define DEMO_TYPE_WIDGET (demo_widget_get_type ())
G_DECLARE_FINAL_TYPE (DemoWidget, demo_widget, DEMO, WIDGET, GtkWidget)
struct _DemoWidget
{
GtkWidget parent_instance;
};
struct _DemoWidgetClass
{
GtkWidgetClass parent_class;
};
G_DEFINE_TYPE (DemoWidget, demo_widget, GTK_TYPE_WIDGET)
static void
demo_widget_init (DemoWidget *self)
{
}
static void
demo_widget_snapshot (GtkWidget *widget,
GtkSnapshot *snapshot)
{
DemoWidget *self = DEMO_WIDGET (widget);
PangoLayout *layout;
int width, height;
int pwidth, pheight;
PangoFontDescription *desc;
int size;
double scale;
int x, y;
GdkRGBA color;
width = gtk_widget_get_width (widget);
height = gtk_widget_get_height (widget);
gtk_widget_get_color (widget, &color);
layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), "Best Aa");
pango_layout_get_pixel_size (layout, &pwidth, &pheight);
desc = pango_font_description_copy_static (pango_context_get_font_description (pango_layout_get_context (layout)));
size = pango_font_description_get_size (desc);
scale = MIN (width / (double)pwidth, height / (double)pheight);
pango_font_description_set_size (desc, size * scale * 0.5);
pango_layout_set_font_description (layout, desc);
pango_font_description_free (desc);
pango_layout_get_pixel_size (layout, &pwidth, &pheight);
x = floor ((width - pwidth) / 2);
y = floor ((height - pheight) / 2);
gtk_snapshot_save (snapshot);
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (x, y));
gtk_snapshot_append_layout (snapshot, layout, &color);
gtk_snapshot_restore (snapshot);
g_object_unref (layout);
}
static void
demo_widget_dispose (GObject *object)
{
G_OBJECT_CLASS (demo_widget_parent_class)->dispose (object);
}
static void
demo_widget_class_init (DemoWidgetClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS (class);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
object_class->dispose = demo_widget_dispose;
widget_class->snapshot = demo_widget_snapshot;
}
static GtkWidget *
demo_widget_new (void)
{
return g_object_new (DEMO_TYPE_WIDGET, NULL);
}
static const char css[] =
"* {\n"
" font-family: Cantarell;\n"
" font-weight: 520;\n"
"}";
int
main (int argc, char *argv[])
{
GtkCssProvider *style;
GtkWidget *window;
gtk_init ();
style = gtk_css_provider_new ();
gtk_css_provider_load_from_string (style, css);
gtk_style_context_add_provider_for_display (gdk_display_get_default (),
GTK_STYLE_PROVIDER (style),
800);
window = gtk_window_new ();
gtk_window_set_child (GTK_WINDOW (window), demo_widget_new ());
gtk_window_present (GTK_WINDOW (window));
while (g_list_model_get_n_items (gtk_window_get_toplevels ()) > 0)
g_main_context_iteration (NULL, TRUE);
return 0;
}

View File

@@ -1,5 +1,7 @@
gtk_tests = [
# testname, optional extra sources
['movingtext'],
['bigfont'],
['testsections'],
['testfilelauncher'],
['input'],

210
tests/movingtext.c Normal file
View File

@@ -0,0 +1,210 @@
#include <gtk/gtk.h>
#define DEMO_TYPE_WIDGET (demo_widget_get_type ())
G_DECLARE_FINAL_TYPE (DemoWidget, demo_widget, DEMO, WIDGET, GtkWidget)
struct _DemoWidget
{
GtkWidget parent_instance;
guint tick_cb;
guint64 start_time;
guint64 stop_time;
char *text;
float angle;
float size;
};
struct _DemoWidgetClass
{
GtkWidgetClass parent_class;
};
G_DEFINE_TYPE (DemoWidget, demo_widget, GTK_TYPE_WIDGET)
static gboolean
tick_cb (GtkWidget *widget,
GdkFrameClock *frame_clock,
gpointer user_data)
{
DemoWidget *self = DEMO_WIDGET (widget);
guint64 now;
now = gdk_frame_clock_get_frame_time (frame_clock);
if (self->start_time == 0)
self->start_time = now;
self->angle = 360 * (now - self->start_time) / (double)(G_TIME_SPAN_SECOND * 10);
self->size = 150 + 130 * sin (2 * G_PI * (now - self->start_time) / (double)(G_TIME_SPAN_SECOND * 5));
gtk_widget_queue_draw (widget);
return G_SOURCE_CONTINUE;
}
static gboolean
pressed_cb (GtkEventController *controller,
guint keyval,
guint keycode,
GdkModifierType state,
gpointer data)
{
DemoWidget *self = (DemoWidget *)gtk_event_controller_get_widget (controller);
if (keyval == GDK_KEY_space)
{
GdkFrameClock *frame_clock;
guint64 now;
frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
now = gdk_frame_clock_get_frame_time (frame_clock);
if (self->tick_cb)
{
gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb);
self->tick_cb = 0;
self->stop_time = now;
}
else
{
self->start_time += now - self->stop_time;
self->tick_cb = gtk_widget_add_tick_callback (GTK_WIDGET (self), tick_cb, NULL, NULL);
}
}
return TRUE;
}
static void
demo_widget_init (DemoWidget *self)
{
GtkEventController *controller;
self->start_time = 0;
self->tick_cb = gtk_widget_add_tick_callback (GTK_WIDGET (self), tick_cb, NULL, NULL);
controller = gtk_event_controller_key_new ();
g_signal_connect (controller, "key-pressed", G_CALLBACK (pressed_cb), NULL);
gtk_widget_add_controller (GTK_WIDGET (self), controller);
gtk_widget_set_focusable (GTK_WIDGET (self), TRUE);
}
static void
demo_widget_snapshot (GtkWidget *widget,
GtkSnapshot *snapshot)
{
DemoWidget *self = DEMO_WIDGET (widget);
PangoLayout *layout;
int width, height;
int pwidth, pheight;
PangoFontDescription *desc;
GdkRGBA color;
width = gtk_widget_get_width (widget);
height = gtk_widget_get_height (widget);
gtk_widget_get_color (widget, &color);
layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), self->text);
desc = pango_font_description_new ();
pango_font_description_set_family (desc, "Cantarell");
pango_font_description_set_weight (desc, 520);
pango_font_description_set_size (desc, self->size * PANGO_SCALE);
pango_layout_set_font_description (layout, desc);
pango_font_description_free (desc);
pango_layout_get_pixel_size (layout, &pwidth, &pheight);
gtk_snapshot_save (snapshot);
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (0.5 * width, 0.5 * height));
gtk_snapshot_rotate (snapshot, self->angle);
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (- 0.5 * pwidth, - 0.5 * pheight));
gtk_snapshot_append_layout (snapshot, layout, &color);
gtk_snapshot_restore (snapshot);
g_object_unref (layout);
}
static void
demo_widget_dispose (GObject *object)
{
DemoWidget *self = DEMO_WIDGET (object);
g_free (self->text);
G_OBJECT_CLASS (demo_widget_parent_class)->dispose (object);
}
static void
demo_widget_class_init (DemoWidgetClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS (class);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
object_class->dispose = demo_widget_dispose;
widget_class->snapshot = demo_widget_snapshot;
}
static GtkWidget *
demo_widget_new (const char *text,
gsize length)
{
DemoWidget *demo;
demo = g_object_new (DEMO_TYPE_WIDGET, NULL);
demo->text = g_strndup (text, length);
return GTK_WIDGET (demo);
}
int
main (int argc, char *argv[])
{
GtkWidget *window;
GtkWidget *demo;
char *text = NULL;
gsize length;
gtk_init ();
window = gtk_window_new ();
if (argc > 1)
{
GError *error = NULL;
if (!g_file_get_contents (argv[1], &text, &length, &error))
{
g_warning ("%s", error->message);
g_error_free (error);
text = NULL;
}
}
if (!text)
{
text = g_strdup ("Best Aa");
length = strlen (text);
}
demo = demo_widget_new (text, length);
gtk_window_set_child (GTK_WINDOW (window), demo);
g_free (text);
gtk_window_present (GTK_WINDOW (window));
gtk_widget_grab_focus (demo);
while (g_list_model_get_n_items (gtk_window_get_toplevels ()) > 0)
g_main_context_iteration (NULL, TRUE);
return 0;
}

View File

@@ -0,0 +1,164 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include <gtk/gtk.h>
#include "gsk/gskcurveprivate.h"
static gboolean
measure_segment (const graphene_point_t *from,
const graphene_point_t *to,
float from_t,
float to_t,
GskCurveLineReason reason,
gpointer data)
{
float *length = data;
*length += graphene_point_distance (from, to, NULL, NULL);
return TRUE;
}
static float
measure_length (const GskCurve *curve)
{
float result = 0;
gsk_curve_decompose (curve, 0.5, measure_segment, &result);
return result;
}
/* This is a pretty nasty conic that makes it obvious that split()
* does not respect the progress values, so split() twice with
* scaled factor won't work.
*/
static void
test_conic_segment (void)
{
GskCurve c, s, e, m;
graphene_point_t pts[4] = {
GRAPHENE_POINT_INIT (-1856.131591796875, 46.217609405517578125),
GRAPHENE_POINT_INIT (-1555.9866943359375, 966.0810546875),
GRAPHENE_POINT_INIT (98.94945526123046875, 0),
GRAPHENE_POINT_INIT (-1471.33154296875, 526.701171875)
};
float start = 0.02222645096480846405029296875;
float end = 0.982032716274261474609375;
gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CONIC, pts));
gsk_curve_split (&c, start, &s, NULL);
gsk_curve_segment (&c, start, end, &m);
gsk_curve_split (&c, end, NULL, &e);
g_assert_cmpfloat_with_epsilon (measure_length (&c), measure_length (&s) + measure_length (&m) + measure_length (&e), 0.03125);
}
static void
test_curve_tangents (void)
{
GskCurve c;
graphene_point_t p[4];
graphene_vec2_t t;
graphene_point_init (&p[0], 0, 0);
graphene_point_init (&p[1], 100, 0);
gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_LINE, p));
gsk_curve_get_start_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
gsk_curve_get_end_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
graphene_point_init (&p[0], 0, 0);
graphene_point_init (&p[1], 0, 100);
gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_LINE, p));
gsk_curve_get_start_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_y_axis (), 0.0001));
gsk_curve_get_end_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_y_axis (), 0.0001));
graphene_point_init (&p[0], 0, 0);
graphene_point_init (&p[1], 100, 0);
p[2] = GRAPHENE_POINT_INIT (g_test_rand_double_range (0, 20), 0);
graphene_point_init (&p[3], 100, 100);
gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CONIC, p));
gsk_curve_get_start_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
gsk_curve_get_end_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_y_axis (), 0.0001));
graphene_point_init (&p[0], 0, 0);
graphene_point_init (&p[1], 50, 0);
graphene_point_init (&p[2], 100, 50);
graphene_point_init (&p[3], 100, 100);
gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CUBIC, p));
gsk_curve_get_start_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
gsk_curve_get_end_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_y_axis (), 0.0001));
}
static void
test_curve_degenerate_tangents (void)
{
GskCurve c;
graphene_point_t p[4];
graphene_vec2_t t;
graphene_point_init (&p[0], 0, 0);
graphene_point_init (&p[1], 0, 0);
graphene_point_init (&p[2], 100, 0);
graphene_point_init (&p[3], 100, 0);
gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CUBIC, p));
gsk_curve_get_start_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
gsk_curve_get_end_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
graphene_point_init (&p[0], 0, 0);
graphene_point_init (&p[1], 50, 0);
graphene_point_init (&p[2], 50, 0);
graphene_point_init (&p[3], 100, 0);
gsk_curve_init (&c, gsk_pathop_encode (GSK_PATH_CUBIC, p));
gsk_curve_get_start_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
gsk_curve_get_end_tangent (&c, &t);
g_assert_true (graphene_vec2_near (&t, graphene_vec2_x_axis (), 0.0001));
}
int
main (int argc,
char *argv[])
{
gtk_test_init (&argc, &argv, NULL);
g_test_add_func ("/curve/special/conic-segment", test_conic_segment);
g_test_add_func ("/curve/special/tangents", test_curve_tangents);
g_test_add_func ("/curve/special/degenerate-tangents", test_curve_degenerate_tangents);
return g_test_run ();
}

423
testsuite/gsk/curve.c Normal file
View File

@@ -0,0 +1,423 @@
#include <gtk/gtk.h>
#include "gsk/gskcurveprivate.h"
#include "gsk/gskpath.h"
#include "gsk/gskpathmeasure.h"
static void
init_random_point (graphene_point_t *p)
{
p->x = g_test_rand_double_range (0, 1000);
p->y = g_test_rand_double_range (0, 1000);
}
static float
random_weight (void)
{
if (g_test_rand_bit ())
return g_test_rand_double_range (1, 20);
else
return 1.0 / g_test_rand_double_range (1, 20);
}
static void
init_random_curve_with_op (GskCurve *curve,
GskPathOperation min_op,
GskPathOperation max_op)
{
switch (g_test_rand_int_range (min_op, max_op + 1))
{
case GSK_PATH_LINE:
{
graphene_point_t p[2];
init_random_point (&p[0]);
init_random_point (&p[1]);
gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_LINE, p));
}
break;
case GSK_PATH_QUAD:
{
graphene_point_t p[3];
init_random_point (&p[0]);
init_random_point (&p[1]);
init_random_point (&p[2]);
gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_QUAD, p));
}
break;
case GSK_PATH_CUBIC:
{
graphene_point_t p[4];
init_random_point (&p[0]);
init_random_point (&p[1]);
init_random_point (&p[2]);
init_random_point (&p[3]);
gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_CUBIC, p));
}
break;
case GSK_PATH_CONIC:
{
graphene_point_t p[4];
init_random_point (&p[0]);
init_random_point (&p[1]);
p[2] = GRAPHENE_POINT_INIT (random_weight(), 0);
init_random_point (&p[3]);
gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_CONIC, p));
}
break;
default:
g_assert_not_reached ();
}
}
static void
init_random_curve (GskCurve *curve)
{
init_random_curve_with_op (curve, GSK_PATH_LINE, GSK_PATH_CONIC);
}
static void
test_curve_tangents (void)
{
for (int i = 0; i < 100; i++)
{
GskCurve c;
graphene_vec2_t vec, exact;
init_random_curve (&c);
gsk_curve_get_tangent (&c, 0, &vec);
g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&vec), 1.0f, 0.00001);
gsk_curve_get_start_tangent (&c, &exact);
g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&exact), 1.0f, 0.00001);
g_assert_true (graphene_vec2_near (&vec, &exact, 0.05));
gsk_curve_get_tangent (&c, 1, &vec);
g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&vec), 1.0f, 0.00001);
gsk_curve_get_end_tangent (&c, &exact);
g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&exact), 1.0f, 0.00001);
g_assert_true (graphene_vec2_near (&vec, &exact, 0.05));
}
}
static void
test_curve_points (void)
{
for (int i = 0; i < 100; i++)
{
GskCurve c;
graphene_point_t p;
init_random_curve (&c);
/* We can assert equality here because evaluating the polynomials with 0
* has no effect on accuracy.
*/
gsk_curve_get_point (&c, 0, &p);
g_assert_true (graphene_point_equal (gsk_curve_get_start_point (&c), &p));
/* But here we evaluate the polynomials with 1 which gives the highest possible
* accuracy error. So we'll just be generous here.
*/
gsk_curve_get_point (&c, 1, &p);
g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c), &p, 0.05));
}
}
/* at this point the subdivision stops and the decomposer
* violates tolerance rules
*/
#define MIN_PROGRESS (1/1024.f)
typedef struct
{
graphene_point_t p;
float t;
} PointOnLine;
static gboolean
add_line_to_array (const graphene_point_t *from,
const graphene_point_t *to,
float from_progress,
float to_progress,
GskCurveLineReason reason,
gpointer user_data)
{
GArray *array = user_data;
PointOnLine *last = &g_array_index (array, PointOnLine, array->len - 1);
g_assert_true (array->len > 0);
g_assert_cmpfloat (from_progress, >=, 0.0f);
g_assert_cmpfloat (from_progress, <, to_progress);
g_assert_cmpfloat (to_progress, <=, 1.0f);
g_assert_true (graphene_point_equal (&last->p, from));
g_assert_cmpfloat (last->t, ==, from_progress);
g_array_append_vals (array, (PointOnLine[1]) { { *to, to_progress } }, 1);
return TRUE;
}
static void
test_curve_decompose (void)
{
static const float tolerance = 0.5;
for (int i = 0; i < 100; i++)
{
GArray *array;
GskCurve c;
init_random_curve (&c);
array = g_array_new (FALSE, FALSE, sizeof (PointOnLine));
g_array_append_vals (array, (PointOnLine[1]) { { *gsk_curve_get_start_point (&c), 0.f } }, 1);
g_assert_true (gsk_curve_decompose (&c, tolerance, add_line_to_array, array));
g_assert_cmpint (array->len, >=, 2); /* We at least got a line to the end */
g_assert_cmpfloat (g_array_index (array, PointOnLine, array->len - 1).t, ==, 1.0);
for (int j = 0; j < array->len; j++)
{
PointOnLine *pol = &g_array_index (array, PointOnLine, j);
graphene_point_t p;
/* Check that the points we got are actually on the line */
gsk_curve_get_point (&c, pol->t, &p);
g_assert_true (graphene_point_near (&pol->p, &p, 0.05));
/* Check that the mid point is not further than the tolerance */
if (j > 0)
{
PointOnLine *last = &g_array_index (array, PointOnLine, j - 1);
graphene_point_t mid;
if (pol->t - last->t > MIN_PROGRESS)
{
graphene_point_interpolate (&last->p, &pol->p, 0.5, &mid);
gsk_curve_get_point (&c, (pol->t + last->t) / 2, &p);
/* The decomposer does this cheaper Manhattan distance test,
* so graphene_point_near() does not work */
g_assert_cmpfloat (fabs (mid.x - p.x), <=, tolerance);
g_assert_cmpfloat (fabs (mid.y - p.y), <=, tolerance);
}
}
}
g_array_unref (array);
}
}
static gboolean
add_curve_to_array (GskPathOperation op,
const graphene_point_t *pts,
gsize n_pts,
float weight,
gpointer user_data)
{
GArray *array = user_data;
GskCurve c;
gsk_curve_init_foreach (&c, op, pts, n_pts, weight);
g_array_append_val (array, c);
return TRUE;
}
static void
test_curve_decompose_conic (void)
{
g_test_skip ("No good error bounds for decomposing conics");
return;
for (int i = 0; i < 100; i++)
{
GArray *array;
GskCurve c;
GskPathBuilder *builder;
GskPath *path;
GskPathMeasure *measure;
const graphene_point_t *s;
init_random_curve_with_op (&c, GSK_PATH_CONIC, GSK_PATH_CONIC);
builder = gsk_path_builder_new ();
s = gsk_curve_get_start_point (&c);
gsk_path_builder_move_to (builder, s->x, s->y);
gsk_curve_builder_to (&c, builder);
path = gsk_path_builder_free_to_path (builder);
measure = gsk_path_measure_new_with_tolerance (path, 0.1);
array = g_array_new (FALSE, FALSE, sizeof (GskCurve));
g_assert_true (gsk_curve_decompose_curve (&c, GSK_PATH_FOREACH_ALLOW_CUBIC, 0.1, add_curve_to_array, array));
g_assert_cmpint (array->len, >=, 1);
for (int j = 0; j < array->len; j++)
{
GskCurve *c2 = &g_array_index (array, GskCurve, j);
g_assert_true (c2->op == GSK_PATH_CUBIC);
/* Check that the curves we got are approximating the conic */
for (int k = 0; k < 11; k++)
{
GskPathPoint *point;
graphene_point_t p, q;
gsk_curve_get_point (c2, k/10.0, &p);
point = gsk_path_measure_get_closest_point (measure, &p, INFINITY);
gsk_path_point_get_position (point, &q);
g_assert_true (graphene_point_near (&p, &q, 0.5));
gsk_path_point_unref (point);
}
}
g_array_unref (array);
gsk_path_measure_unref (measure);
gsk_path_unref (path);
}
}
static void
test_curve_decompose_into (GskPathForeachFlags flags)
{
for (int i = 0; i < 100; i++)
{
GskCurve c;
GskPathBuilder *builder;
const graphene_point_t *s;
GskPath *path;
GArray *array;
init_random_curve (&c);
builder = gsk_path_builder_new ();
s = gsk_curve_get_start_point (&c);
gsk_path_builder_move_to (builder, s->x, s->y);
gsk_curve_builder_to (&c, builder);
path = gsk_path_builder_free_to_path (builder);
array = g_array_new (FALSE, FALSE, sizeof (GskCurve));
g_assert_true (gsk_curve_decompose_curve (&c, flags, 0.1, add_curve_to_array, array));
g_assert_cmpint (array->len, >=, 1);
for (int j = 0; j < array->len; j++)
{
GskCurve *c2 = &g_array_index (array, GskCurve, j);
switch (c2->op)
{
case GSK_PATH_MOVE:
case GSK_PATH_CLOSE:
case GSK_PATH_LINE:
break;
case GSK_PATH_QUAD:
g_assert_true (flags & GSK_PATH_FOREACH_ALLOW_QUAD);
break;
case GSK_PATH_CUBIC:
g_assert_true (flags & GSK_PATH_FOREACH_ALLOW_CUBIC);
break;
case GSK_PATH_CONIC:
g_assert_true (flags & GSK_PATH_FOREACH_ALLOW_CONIC);
break;
default:
g_assert_not_reached ();
}
}
g_array_unref (array);
gsk_path_unref (path);
}
}
static void
test_curve_decompose_into_line (void)
{
test_curve_decompose_into (0);
}
static void
test_curve_decompose_into_quad (void)
{
test_curve_decompose_into (GSK_PATH_FOREACH_ALLOW_QUAD);
}
static void
test_curve_decompose_into_cubic (void)
{
test_curve_decompose_into (GSK_PATH_FOREACH_ALLOW_CUBIC);
}
/* Some sanity checks for splitting curves. */
static void
test_curve_split (void)
{
for (int i = 0; i < 100; i++)
{
GskCurve c;
GskCurve c1, c2;
graphene_point_t p;
graphene_vec2_t t, t1, t2;
init_random_curve (&c);
gsk_curve_split (&c, 0.5, &c1, &c2);
g_assert_true (c1.op == c.op);
g_assert_true (c2.op == c.op);
g_assert_true (graphene_point_near (gsk_curve_get_start_point (&c),
gsk_curve_get_start_point (&c1), 0.005));
g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c1),
gsk_curve_get_start_point (&c2), 0.005));
g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c),
gsk_curve_get_end_point (&c2), 0.005));
gsk_curve_get_point (&c, 0.5, &p);
gsk_curve_get_tangent (&c, 0.5, &t);
g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c1), &p, 0.005));
g_assert_true (graphene_point_near (gsk_curve_get_start_point (&c2), &p, 0.005));
gsk_curve_get_start_tangent (&c, &t1);
gsk_curve_get_start_tangent (&c1, &t2);
g_assert_true (graphene_vec2_near (&t1, &t2, 0.005));
gsk_curve_get_end_tangent (&c1, &t1);
gsk_curve_get_start_tangent (&c2, &t2);
g_assert_true (graphene_vec2_near (&t1, &t2, 0.005));
g_assert_true (graphene_vec2_near (&t, &t1, 0.005));
g_assert_true (graphene_vec2_near (&t, &t2, 0.005));
gsk_curve_get_end_tangent (&c, &t1);
gsk_curve_get_end_tangent (&c2, &t2);
g_assert_true (graphene_vec2_near (&t1, &t2, 0.005));
}
}
int
main (int argc, char *argv[])
{
(g_test_init) (&argc, &argv, NULL);
g_test_add_func ("/curve/points", test_curve_points);
g_test_add_func ("/curve/tangents", test_curve_tangents);
g_test_add_func ("/curve/decompose", test_curve_decompose);
g_test_add_func ("/curve/decompose/conic", test_curve_decompose_conic);
g_test_add_func ("/curve/decompose/into/line", test_curve_decompose_into_line);
g_test_add_func ("/curve/decompose/into/quad", test_curve_decompose_into_quad);
g_test_add_func ("/curve/decompose/into/cubic", test_curve_decompose_into_cubic);
g_test_add_func ("/curve/split", test_curve_split);
return g_test_run ();
}

View File

@@ -0,0 +1,252 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include <gtk/gtk.h>
#include <gtk/gtk.h>
#include "gsk/gskpath.h"
#include "gsk/gskpathbuilder.h"
#include "gsk/gskpathmeasure.h"
#include "gsk/gskpathpoint.h"
static void
test_bad_split (void)
{
GskPath *path, *path1;
GskPathMeasure *measure, *measure1;
GskPathBuilder *builder;
float split, length, epsilon;
/* An example that was isolated from the /path/segment test path.c
* It shows how uneven parametrization of cubics can lead to bad
* lengths reported by the measure.
*/
path = gsk_path_parse ("M 0 0 C 2 0 4 0 4 0");
measure = gsk_path_measure_new (path);
split = 2.962588;
length = gsk_path_measure_get_length (measure);
epsilon = MAX (length / 256, 1.f / 1024);
builder = gsk_path_builder_new ();
gsk_path_builder_add_segment (builder, measure, 0, split);
path1 = gsk_path_builder_free_to_path (builder);
measure1 = gsk_path_measure_new (path1);
g_assert_cmpfloat_with_epsilon (split, gsk_path_measure_get_length (measure1), epsilon);
gsk_path_measure_unref (measure1);
gsk_path_unref (path1);
gsk_path_measure_unref (measure);
gsk_path_unref (path);
}
static void
test_bad_in_fill (void)
{
GskPath *path;
GskPathMeasure *measure;
gboolean inside;
/* A fat Cantarell W */
path = gsk_path_parse ("M -2 694 M 206.1748046875 704 L 390.9371337890625 704 L 551.1888427734375 99.5035400390625 L 473.0489501953125 99.5035400390625 L 649.1048583984375 704 L 828.965087890625 704 L 1028.3077392578125 10 L 857.8111572265625 10 L 710.0489501953125 621.251708984375 L 775.9720458984375 598.426513671875 L 614.5245361328125 14.0489501953125 L 430.2237548828125 14.0489501953125 L 278.6783447265625 602.230712890625 L 330.0909423828125 602.230712890625 L 195.88818359375 10 L 5.7342529296875 10 L 206.1748046875 704 Z");
measure = gsk_path_measure_new (path);
/* The midpoint of the right foot of a fat Cantarell X */
inside = gsk_path_measure_in_fill (measure, &GRAPHENE_POINT_INIT (552.360107, 704.000000), GSK_FILL_RULE_WINDING);
g_assert_false (inside);
gsk_path_measure_unref (measure);
gsk_path_unref (path);
}
static void
test_rect (void)
{
GskPathBuilder *builder;
GskPath *path;
GskPathMeasure *measure;
GskPathPoint *point;
graphene_point_t p;
graphene_vec2_t tangent, expected_tangent;
float length;
builder = gsk_path_builder_new ();
gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (0, 0, 100, 50));
path = gsk_path_builder_free_to_path (builder);
measure = gsk_path_measure_new (path);
length = gsk_path_measure_get_length (measure);
g_assert_true (length == 300);
#define TEST_POS_AT(distance, X, Y) \
point = gsk_path_measure_get_point (measure, distance); \
gsk_path_point_get_position (point, &p); \
g_assert_true (graphene_point_near (&p, &GRAPHENE_POINT_INIT (X, Y), 0.01)); \
gsk_path_point_unref (point); \
point = gsk_path_measure_get_closest_point (measure, &GRAPHENE_POINT_INIT (X, Y), INFINITY); \
if (distance < length) \
g_assert_true (fabs (gsk_path_point_get_distance (point) - distance) < 0.01); \
else \
g_assert_true (fabs (gsk_path_point_get_distance (point)) < 0.01); \
gsk_path_point_get_position (point, &p); \
g_assert_true (graphene_point_near (&p, &GRAPHENE_POINT_INIT (X, Y), 0.01)); \
gsk_path_point_unref (point);
#define TEST_TANGENT_AT(distance, x1, y1, x2, y2) \
point = gsk_path_measure_get_point (measure, distance); \
gsk_path_point_get_tangent (point, GSK_PATH_START, &tangent); \
g_assert_true (graphene_vec2_near (&tangent, graphene_vec2_init (&expected_tangent, x1, y1), 0.01)); \
gsk_path_point_get_tangent (point, GSK_PATH_END, &tangent); \
g_assert_true (graphene_vec2_near (&tangent, graphene_vec2_init (&expected_tangent, x2, y2), 0.01)); \
gsk_path_point_unref (point); \
#define TEST_POS_AT2(distance, X, Y, expected_distance) \
point = gsk_path_measure_get_point (measure, distance); \
gsk_path_point_get_position (point, &p); \
g_assert_true (graphene_point_near (&p, &GRAPHENE_POINT_INIT (X, Y), 0.01)); \
gsk_path_point_unref (point); \
point = gsk_path_measure_get_closest_point (measure, &GRAPHENE_POINT_INIT (X, Y), INFINITY); \
g_assert_true (fabs (gsk_path_point_get_distance (point) - expected_distance) < 0.01); \
gsk_path_point_get_position (point, &p); \
g_assert_true (graphene_point_near (&p, &GRAPHENE_POINT_INIT (X, Y), 0.01)); \
gsk_path_point_unref (point);
TEST_POS_AT (0, 0, 0)
TEST_POS_AT (25, 25, 0)
TEST_POS_AT (100, 100, 0)
TEST_POS_AT (110, 100, 10)
TEST_POS_AT (150, 100, 50)
TEST_POS_AT (175, 75, 50)
TEST_POS_AT (250, 0, 50)
TEST_POS_AT (260, 0, 40)
TEST_POS_AT (300, 0, 0)
TEST_TANGENT_AT (0, 0, -1, 1, 0)
TEST_TANGENT_AT (50, 1, 0, 1, 0)
TEST_TANGENT_AT (100, 1, 0, 0, 1)
TEST_TANGENT_AT (125, 0, 1, 0, 1)
TEST_TANGENT_AT (150, 0, 1, -1, 0)
TEST_TANGENT_AT (200, -1, 0, -1, 0)
TEST_TANGENT_AT (250, -1, 0, 0, -1)
TEST_TANGENT_AT (275, 0, -1, 0, -1)
gsk_path_measure_unref (measure);
gsk_path_unref (path);
builder = gsk_path_builder_new ();
gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (100, 50, -100, -50));
path = gsk_path_builder_free_to_path (builder);
measure = gsk_path_measure_new (path);
length = gsk_path_measure_get_length (measure);
g_assert_true (length == 300);
TEST_POS_AT (0, 100, 50)
TEST_POS_AT (25, 75, 50)
TEST_POS_AT (100, 0, 50)
TEST_POS_AT (110, 0, 40)
TEST_POS_AT (150, 0, 0)
TEST_POS_AT (175, 25, 0)
TEST_POS_AT (250, 100, 0)
TEST_POS_AT (260, 100, 10)
TEST_POS_AT (300, 100, 50)
builder = gsk_path_builder_new ();
gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (100, 0, -100, 50));
path = gsk_path_builder_free_to_path (builder);
measure = gsk_path_measure_new (path);
length = gsk_path_measure_get_length (measure);
g_assert_true (length == 300);
TEST_POS_AT (0, 100, 0)
TEST_POS_AT (25, 75, 0)
TEST_POS_AT (100, 0, 0)
TEST_POS_AT (110, 0, 10)
TEST_POS_AT (150, 0, 50)
TEST_POS_AT (175, 25, 50)
TEST_POS_AT (250, 100, 50)
TEST_POS_AT (260, 100, 40)
TEST_POS_AT (300, 100, 0)
builder = gsk_path_builder_new ();
gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (0, 0, 100, 0));
path = gsk_path_builder_free_to_path (builder);
measure = gsk_path_measure_new (path);
length = gsk_path_measure_get_length (measure);
g_assert_true (length == 200);
TEST_POS_AT2 (0, 0, 0, 0)
TEST_POS_AT2 (25, 25, 0, 25)
TEST_POS_AT2 (100, 100, 0, 100)
TEST_POS_AT2 (110, 90, 0, 90)
TEST_POS_AT2 (200, 0, 0, 0)
builder = gsk_path_builder_new ();
gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (100, 0, -100, 0));
path = gsk_path_builder_free_to_path (builder);
measure = gsk_path_measure_new (path);
length = gsk_path_measure_get_length (measure);
g_assert_true (length == 200);
TEST_POS_AT2 (0, 100, 0, 0)
TEST_POS_AT2 (25, 75, 0, 25)
TEST_POS_AT2 (100, 0, 0, 100)
TEST_POS_AT2 (110, 10, 0, 90)
TEST_POS_AT2 (200, 100, 0, 0)
builder = gsk_path_builder_new ();
gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (0, 100, 0, -100));
path = gsk_path_builder_free_to_path (builder);
measure = gsk_path_measure_new (path);
length = gsk_path_measure_get_length (measure);
g_assert_true (length == 200);
TEST_POS_AT2 (0, 0, 100, 0)
TEST_POS_AT2 (25, 0, 75, 25)
TEST_POS_AT2 (100, 0, 0, 100)
TEST_POS_AT2 (110, 0, 10, 90)
TEST_POS_AT2 (200, 0, 100, 0)
#undef TEST_POS_AT
#undef TEST_POS_AT2
#undef TEST_TANGENT_AT
gsk_path_measure_unref (measure);
gsk_path_unref (path);
}
int
main (int argc,
char *argv[])
{
gtk_test_init (&argc, &argv, NULL);
g_test_add_func ("/measure/bad-split", test_bad_split);
g_test_add_func ("/measure/bad-in-fill", test_bad_in_fill);
g_test_add_func ("/measure/rect", test_rect);
return g_test_run ();
}

910
testsuite/gsk/measure.c Normal file
View File

@@ -0,0 +1,910 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include <gtk/gtk.h>
#include "gsk/gskpath.h"
#include "gsk/gskpathbuilder.h"
#include "gsk/gskpathmeasure.h"
static float
random_weight (void)
{
if (g_test_rand_bit ())
return g_test_rand_double_range (1, 20);
else
return 1.0 / g_test_rand_double_range (1, 20);
}
static GskPath *
create_random_degenerate_path (guint max_contours)
{
#define N_DEGENERATE_PATHS 15
GskPathBuilder *builder;
guint i;
builder = gsk_path_builder_new ();
switch (g_test_rand_int_range (0, N_DEGENERATE_PATHS))
{
case 0:
/* empty path */
break;
case 1:
/* a single point */
gsk_path_builder_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 2:
/* N points */
for (i = 0; i < MIN (10, max_contours); i++)
{
gsk_path_builder_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
}
break;
case 3:
/* 1 closed point */
gsk_path_builder_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
gsk_path_builder_close (builder);
break;
case 4:
/* the same point closed N times */
gsk_path_builder_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
for (i = 0; i < MIN (10, max_contours); i++)
{
gsk_path_builder_close (builder);
}
break;
case 5:
/* a zero-width and zero-height rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
0, 0));
break;
case 6:
/* a zero-width rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
0,
g_test_rand_double_range (-1000, 1000)));
break;
case 7:
/* a zero-height rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
0));
break;
case 8:
/* a negative-size rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 0),
g_test_rand_double_range (-1000, 0)));
break;
case 9:
/* an absolutely random rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000)));
break;
case 10:
/* an absolutely random rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000)));
break;
case 11:
/* an absolutely random circle */
gsk_path_builder_add_circle (builder,
&GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000)),
g_test_rand_double_range (1, 1000));
break;
case 12:
/* a zero-length line */
{
graphene_point_t point = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
gsk_path_builder_move_to (builder, point.x, point.y);
gsk_path_builder_line_to (builder, point.x, point.y);
}
break;
case 13:
/* a curve with start == end */
{
graphene_point_t point = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
gsk_path_builder_move_to (builder, point.x, point.y);
gsk_path_builder_cubic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
point.x, point.y);
}
break;
case 14:
/* a conic with start == end */
{
graphene_point_t point = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
gsk_path_builder_move_to (builder, point.x, point.y);
gsk_path_builder_conic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
point.x, point.y,
random_weight ());
}
break;
case N_DEGENERATE_PATHS:
default:
g_assert_not_reached ();
}
return gsk_path_builder_free_to_path (builder);
}
static GskPath *
create_random_path (guint max_contours);
static void
add_shape_contour (GskPathBuilder *builder)
{
#define N_SHAPE_CONTOURS 3
switch (g_test_rand_int_range (0, N_SHAPE_CONTOURS))
{
case 0:
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (1, 1000),
g_test_rand_double_range (1, 1000)));
break;
case 1:
gsk_path_builder_add_circle (builder,
&GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000)),
g_test_rand_double_range (1, 1000));
break;
case 2:
{
GskPath *path = create_random_path (1);
gsk_path_builder_add_path (builder, path);
gsk_path_unref (path);
}
break;
case N_SHAPE_CONTOURS:
default:
g_assert_not_reached ();
break;
}
}
static void
add_standard_contour (GskPathBuilder *builder)
{
guint i, n;
if (g_test_rand_bit ())
{
if (g_test_rand_bit ())
gsk_path_builder_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
else
gsk_path_builder_rel_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
}
/* that 20 is random, but should be enough to get some
* crazy self-intersecting shapes */
n = g_test_rand_int_range (1, 20);
for (i = 0; i < n; i++)
{
switch (g_test_rand_int_range (0, 8))
{
case 0:
gsk_path_builder_line_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 1:
gsk_path_builder_rel_line_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 2:
gsk_path_builder_quad_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 3:
gsk_path_builder_rel_quad_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 4:
gsk_path_builder_cubic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 5:
gsk_path_builder_rel_cubic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 6:
gsk_path_builder_conic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
random_weight ());
break;
case 7:
gsk_path_builder_rel_conic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
random_weight ());
break;
default:
g_assert_not_reached();
break;
}
}
if (g_test_rand_bit ())
gsk_path_builder_close (builder);
}
static GskPath *
create_random_path (guint max_contours)
{
GskPathBuilder *builder;
guint i, n;
/* 5% chance for a weird shape */
if (!g_test_rand_int_range (0, 20))
return create_random_degenerate_path (max_contours);
builder = gsk_path_builder_new ();
n = g_test_rand_int_range (1, 10);
n = MIN (n, max_contours);
for (i = 0; i < n; i++)
{
/* 2/3 of shapes are standard contours */
if (g_test_rand_int_range (0, 3))
add_standard_contour (builder);
else
add_shape_contour (builder);
}
return gsk_path_builder_free_to_path (builder);
}
typedef struct {
GskPathOperation op;
graphene_point_t pts[4];
float weight;
} PathOperation;
static void
test_segment_start (void)
{
GskPath *path, *path1;
GskPathMeasure *measure, *measure1;
GskPathBuilder *builder;
float epsilon, length;
guint i;
path = create_random_path (G_MAXUINT);
measure = gsk_path_measure_new (path);
length = gsk_path_measure_get_length (measure);
epsilon = MAX (length / 1024, G_MINFLOAT);
for (i = 0; i < 100; i++)
{
float seg_length = length * i / 100.0f;
builder = gsk_path_builder_new ();
gsk_path_builder_add_segment (builder, measure, 0, seg_length);
path1 = gsk_path_builder_free_to_path (builder);
measure1 = gsk_path_measure_new (path1);
if (seg_length == 0)
g_assert_cmpfloat_with_epsilon (gsk_path_measure_get_length (measure), gsk_path_measure_get_length (measure1), epsilon);
else
g_assert_cmpfloat_with_epsilon (seg_length, gsk_path_measure_get_length (measure1), epsilon);
gsk_path_measure_unref (measure1);
gsk_path_unref (path1);
}
gsk_path_measure_unref (measure);
gsk_path_unref (path);
}
static void
test_segment_end (void)
{
GskPath *path, *path1;
GskPathMeasure *measure, *measure1;
GskPathBuilder *builder;
float epsilon, length;
guint i;
path = create_random_path (G_MAXUINT);
measure = gsk_path_measure_new (path);
length = gsk_path_measure_get_length (measure);
epsilon = MAX (length / 1024, G_MINFLOAT);
for (i = 0; i < 100; i++)
{
float seg_length = length * i / 100.0f;
builder = gsk_path_builder_new ();
gsk_path_builder_add_segment (builder, measure, length - seg_length, length);
path1 = gsk_path_builder_free_to_path (builder);
measure1 = gsk_path_measure_new (path1);
if (seg_length == 0)
g_assert_cmpfloat_with_epsilon (gsk_path_measure_get_length (measure), gsk_path_measure_get_length (measure1), epsilon);
else
g_assert_cmpfloat_with_epsilon (seg_length, gsk_path_measure_get_length (measure1), epsilon);
gsk_path_measure_unref (measure1);
gsk_path_unref (path1);
}
gsk_path_measure_unref (measure);
gsk_path_unref (path);
}
static void
test_segment_chunk (void)
{
GskPath *path, *path1, *path2;
GskPathMeasure *measure, *measure1, *measure2;
GskPathBuilder *builder;
float epsilon, length;
guint i;
path = create_random_path (G_MAXUINT);
measure = gsk_path_measure_new (path);
length = gsk_path_measure_get_length (measure);
epsilon = MAX (length / 1024, G_MINFLOAT);
for (i = 0; i <= 100; i++)
{
float seg_start = length * i / 200.0f;
builder = gsk_path_builder_new ();
gsk_path_builder_add_segment (builder, measure, seg_start, seg_start + length / 2);
path1 = gsk_path_builder_free_to_path (builder);
measure1 = gsk_path_measure_new (path1);
g_assert_cmpfloat_with_epsilon (length / 2, gsk_path_measure_get_length (measure1), epsilon);
builder = gsk_path_builder_new ();
gsk_path_builder_add_segment (builder, measure, seg_start + length / 2, seg_start);
path2 = gsk_path_builder_free_to_path (builder);
measure2 = gsk_path_measure_new (path2);
g_assert_cmpfloat_with_epsilon (length / 2, gsk_path_measure_get_length (measure2), epsilon);
gsk_path_measure_unref (measure2);
gsk_path_unref (path2);
gsk_path_measure_unref (measure1);
gsk_path_unref (path1);
}
gsk_path_measure_unref (measure);
gsk_path_unref (path);
}
static void
test_segment (void)
{
GskPath *path, *path1, *path2, *path3;
GskPathMeasure *measure, *measure1, *measure2, *measure3;
GskPathBuilder *builder;
guint i;
float split1, split2, epsilon, length;
for (i = 0; i < 1000; i++)
{
path = create_random_path (G_MAXUINT);
measure = gsk_path_measure_new (path);
length = gsk_path_measure_get_length (measure);
/* chosen high enough to stop the testsuite from failing */
epsilon = MAX (length / 64, 1.f / 1024);
split1 = g_test_rand_double_range (0, length);
split2 = g_test_rand_double_range (split1, length);
builder = gsk_path_builder_new ();
gsk_path_builder_add_segment (builder, measure, 0, split1);
path1 = gsk_path_builder_free_to_path (builder);
measure1 = gsk_path_measure_new (path1);
builder = gsk_path_builder_new ();
gsk_path_builder_add_segment (builder, measure, split1, split2);
path2 = gsk_path_builder_free_to_path (builder);
measure2 = gsk_path_measure_new (path2);
builder = gsk_path_builder_new ();
gsk_path_builder_add_segment (builder, measure, split2, length);
path3 = gsk_path_builder_free_to_path (builder);
measure3 = gsk_path_measure_new (path3);
g_assert_cmpfloat_with_epsilon (split1, gsk_path_measure_get_length (measure1), epsilon);
g_assert_cmpfloat_with_epsilon (split2 - split1, gsk_path_measure_get_length (measure2), epsilon);
g_assert_cmpfloat_with_epsilon (length - split2, gsk_path_measure_get_length (measure3), epsilon);
gsk_path_measure_unref (measure2);
gsk_path_measure_unref (measure1);
gsk_path_measure_unref (measure);
gsk_path_unref (path2);
gsk_path_unref (path1);
gsk_path_unref (path);
}
}
static void
test_get_point (void)
{
static const guint max_contours = 5;
static const float tolerance = 1.0;
GskPath *path;
GskPathMeasure *measure;
GskPathPoint *point;
guint n_discontinuities;
float length, offset, last_offset;
graphene_point_t p, last_point;
guint i, j;
for (i = 0; i < 10; i++)
{
path = create_random_path (max_contours);
measure = gsk_path_measure_new_with_tolerance (path, tolerance);
length = gsk_path_measure_get_length (measure);
n_discontinuities = 0;
point = gsk_path_measure_get_point (measure, 0);
if (point == NULL)
{
g_assert_true (gsk_path_is_empty (path));
continue;
}
gsk_path_point_get_position (point, &last_point);
gsk_path_point_unref (point);
/* FIXME: anything we can test with tangents here? */
last_offset = 0;
for (j = 1; j <= 1024; j++)
{
offset = length * j / 1024;
point = gsk_path_measure_get_point (measure, offset);
gsk_path_point_get_position (point, &p);
gsk_path_point_unref (point);
if (graphene_point_distance (&last_point, &p, NULL, NULL) > 2 * (offset - last_offset))
{
n_discontinuities++;
g_assert_cmpint (n_discontinuities, <, max_contours);
}
last_offset = offset;
last_point = p;
}
gsk_path_measure_unref (measure);
gsk_path_unref (path);
}
}
static void
test_closest_point (void)
{
static const float tolerance = 0.5;
GskPath *path, *path1, *path2;
GskPathMeasure *measure, *measure1, *measure2;
GskPathBuilder *builder;
GskPathPoint *point;
guint i, j;
for (i = 0; i < 10; i++)
{
path1 = create_random_path (G_MAXUINT);
measure1 = gsk_path_measure_new_with_tolerance (path1, tolerance);
path2 = create_random_path (G_MAXUINT);
measure2 = gsk_path_measure_new_with_tolerance (path2, tolerance);
builder = gsk_path_builder_new ();
gsk_path_builder_add_path (builder, path1);
gsk_path_builder_add_path (builder, path2);
path = gsk_path_builder_free_to_path (builder);
measure = gsk_path_measure_new_with_tolerance (path, tolerance);
for (j = 0; j < 100; j++)
{
graphene_point_t test = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
graphene_point_t p1, p2, p;
graphene_vec2_t t1, t2, t;
float offset1, offset2, offset;
float distance1, distance2, distance;
offset1 = offset2 = offset = 0;
distance1 = distance2 = distance = 0;
point = gsk_path_measure_get_closest_point (measure1, &test, INFINITY);
g_assert_true (point != NULL);
gsk_path_point_get_position (point, &p1);
gsk_path_point_get_tangent (point, GSK_PATH_END, &t1);
offset1 = gsk_path_point_get_distance (point);
distance1 = graphene_point_distance (&p1, &test, NULL, NULL);
gsk_path_point_unref (point);
point = gsk_path_measure_get_closest_point (measure2, &test, INFINITY);
g_assert_true (point != NULL);
gsk_path_point_get_position (point, &p2);
gsk_path_point_get_tangent (point, GSK_PATH_END, &t2);
offset2 = gsk_path_point_get_distance (point);
distance2 = graphene_point_distance (&p2, &test, NULL, NULL);
gsk_path_point_unref (point);
point = gsk_path_measure_get_closest_point (measure, &test, INFINITY);
g_assert_true (point != NULL);
gsk_path_point_get_position (point, &p);
gsk_path_point_get_tangent (point, GSK_PATH_END, &t);
offset = gsk_path_point_get_distance (point);
distance = graphene_point_distance (&p, &test, NULL, NULL);
gsk_path_point_unref (point);
if (distance1 < distance2 + tolerance)
{
g_assert_cmpfloat (distance1, ==, distance);
g_assert_cmpfloat (p1.x, ==, p.x);
g_assert_cmpfloat (p1.y, ==, p.y);
g_assert_cmpfloat (offset1, ==, offset);
g_assert_true (graphene_vec2_equal (&t1, &t));
}
else
{
g_assert_cmpfloat (distance2, ==, distance);
g_assert_cmpfloat (p2.x, ==, p.x);
g_assert_cmpfloat (p2.y, ==, p.y);
g_assert_cmpfloat_with_epsilon (offset2 + gsk_path_measure_get_length (measure1), offset, MAX (G_MINFLOAT, offset / 1024));
g_assert_true (graphene_vec2_equal (&t2, &t));
}
}
gsk_path_measure_unref (measure2);
gsk_path_measure_unref (measure1);
gsk_path_measure_unref (measure);
gsk_path_unref (path2);
gsk_path_unref (path1);
gsk_path_unref (path);
}
}
static void
test_closest_point_for_point (void)
{
static const float tolerance = 0.5;
GskPath *path;
GskPathMeasure *measure;
GskPathPoint *point;
float length, offset, closest_offset, distance;
graphene_point_t p, closest_point;
guint i, j;
for (i = 0; i < 100; i++)
{
path = create_random_path (G_MAXUINT);
if (gsk_path_is_empty (path))
{
/* empty paths have no closest point to anything */
gsk_path_unref (path);
continue;
}
measure = gsk_path_measure_new_with_tolerance (path, tolerance);
length = gsk_path_measure_get_length (measure);
for (j = 0; j < 100; j++)
{
offset = g_test_rand_double_range (0, length);
point = gsk_path_measure_get_point (measure, offset);
gsk_path_point_get_position (point, &p);
gsk_path_point_unref (point);
point = gsk_path_measure_get_closest_point (measure, &p, tolerance);
g_assert_nonnull (point);
gsk_path_point_get_position (point, &closest_point);
closest_offset = gsk_path_point_get_distance (point);
distance = graphene_point_distance (&p, &closest_point, NULL, NULL);
gsk_path_point_unref (point);
/* should be given due to the TRUE return above, but who knows... */
g_assert_cmpfloat (distance, <=, tolerance);
g_assert_cmpfloat (graphene_point_distance (&p, &closest_point, NULL, NULL), <=, tolerance);
/* can't do == here because points may overlap if we're unlucky */
g_assert_cmpfloat (closest_offset, <, offset + tolerance);
}
gsk_path_measure_unref (measure);
gsk_path_unref (path);
}
}
#define N_PATHS 3
static void
test_in_fill_union (void)
{
GskPath *path;
GskPathMeasure *measure, *measures[N_PATHS];
GskPathBuilder *builder;
guint i, j, k;
for (i = 0; i < 100; i++)
{
builder = gsk_path_builder_new ();
for (k = 0; k < N_PATHS; k++)
{
path = create_random_path (G_MAXUINT);
measures[k] = gsk_path_measure_new (path);
gsk_path_builder_add_path (builder, path);
gsk_path_unref (path);
}
path = gsk_path_builder_free_to_path (builder);
measure = gsk_path_measure_new (path);
gsk_path_unref (path);
for (j = 0; j < 100; j++)
{
graphene_point_t test = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
GskFillRule fill_rule;
for (fill_rule = GSK_FILL_RULE_WINDING; fill_rule <= GSK_FILL_RULE_EVEN_ODD; fill_rule++)
{
guint n_in_fill = 0;
gboolean in_fill;
for (k = 0; k < N_PATHS; k++)
{
if (gsk_path_measure_in_fill (measures[k], &test, GSK_FILL_RULE_EVEN_ODD))
n_in_fill++;
}
in_fill = gsk_path_measure_in_fill (measure, &test, GSK_FILL_RULE_EVEN_ODD);
switch (fill_rule)
{
case GSK_FILL_RULE_WINDING:
if (n_in_fill == 0)
g_assert_false (in_fill);
else if (n_in_fill == 1)
g_assert_true (in_fill);
/* else we can't say anything because the winding rule doesn't give enough info */
break;
case GSK_FILL_RULE_EVEN_ODD:
g_assert_cmpint (in_fill, ==, n_in_fill & 1);
break;
default:
g_assert_not_reached ();
break;
}
}
}
gsk_path_measure_unref (measure);
for (k = 0; k < N_PATHS; k++)
{
gsk_path_measure_unref (measures[k]);
}
}
}
#undef N_PATHS
/* This is somewhat sucky because using foreach breaks up the contours
* (like rects and circles) and replaces everything with the standard
* contour.
* But at least it extensively tests the standard contour.
*/
static gboolean
rotate_path_cb (GskPathOperation op,
const graphene_point_t *pts,
gsize n_pts,
float weight,
gpointer user_data)
{
GskPathBuilder **builders = user_data;
switch (op)
{
case GSK_PATH_MOVE:
gsk_path_builder_move_to (builders[0], pts[0].x, pts[0].y);
gsk_path_builder_move_to (builders[1], pts[0].y, -pts[0].x);
break;
case GSK_PATH_CLOSE:
gsk_path_builder_close (builders[0]);
gsk_path_builder_close (builders[1]);
break;
case GSK_PATH_LINE:
gsk_path_builder_line_to (builders[0], pts[1].x, pts[1].y);
gsk_path_builder_line_to (builders[1], pts[1].y, -pts[1].x);
break;
case GSK_PATH_QUAD:
gsk_path_builder_quad_to (builders[0], pts[1].x, pts[1].y, pts[2].x, pts[2].y);
gsk_path_builder_quad_to (builders[1], pts[1].y, -pts[1].x, pts[2].y, -pts[2].x);
break;
case GSK_PATH_CUBIC:
gsk_path_builder_cubic_to (builders[0], pts[1].x, pts[1].y, pts[2].x, pts[2].y, pts[3].x, pts[3].y);
gsk_path_builder_cubic_to (builders[1], pts[1].y, -pts[1].x, pts[2].y, -pts[2].x, pts[3].y, -pts[3].x);
break;
case GSK_PATH_CONIC:
gsk_path_builder_conic_to (builders[0], pts[1].x, pts[1].y, pts[2].x, pts[2].y, weight);
gsk_path_builder_conic_to (builders[1], pts[1].y, -pts[1].x, pts[2].y, -pts[2].x, weight);
break;
default:
g_assert_not_reached ();
return FALSE;
}
return TRUE;
}
static void
test_in_fill_rotated (void)
{
GskPath *path;
GskPathBuilder *builders[2];
GskPathMeasure *measures[2];
guint i, j;
#define N_FILL_RULES 2
/* if this triggers, you added a new enum value to GskFillRule, so the define above needs
* an update */
//g_assert_null (g_enum_get_value (g_type_class_ref (GSK_TYPE_FILL_RULE), N_FILL_RULES));
for (i = 0; i < 100; i++)
{
path = create_random_path (G_MAXUINT);
builders[0] = gsk_path_builder_new ();
builders[1] = gsk_path_builder_new ();
/* Use -1 here because we want all the flags, even future additions */
gsk_path_foreach (path, -1, rotate_path_cb, builders);
gsk_path_unref (path);
path = gsk_path_builder_free_to_path (builders[0]);
measures[0] = gsk_path_measure_new (path);
gsk_path_unref (path);
path = gsk_path_builder_free_to_path (builders[1]);
measures[1] = gsk_path_measure_new (path);
gsk_path_unref (path);
for (j = 0; j < 100; j++)
{
GskFillRule fill_rule = g_random_int_range (0, N_FILL_RULES);
float x = g_test_rand_double_range (-1000, 1000);
float y = g_test_rand_double_range (-1000, 1000);
g_assert_cmpint (gsk_path_measure_in_fill (measures[0], &GRAPHENE_POINT_INIT (x, y), fill_rule),
==,
gsk_path_measure_in_fill (measures[1], &GRAPHENE_POINT_INIT (y, -x), fill_rule));
g_assert_cmpint (gsk_path_measure_in_fill (measures[0], &GRAPHENE_POINT_INIT (y, x), fill_rule),
==,
gsk_path_measure_in_fill (measures[1], &GRAPHENE_POINT_INIT (x, -y), fill_rule));
}
gsk_path_measure_unref (measures[0]);
gsk_path_measure_unref (measures[1]);
}
#undef N_FILL_RULES
}
int
main (int argc,
char *argv[])
{
gtk_test_init (&argc, &argv, NULL);
g_test_add_func ("/measure/segment_start", test_segment_start);
g_test_add_func ("/measure/segment_end", test_segment_end);
g_test_add_func ("/measure/segment_chunk", test_segment_chunk);
g_test_add_func ("/measure/segment", test_segment);
g_test_add_func ("/measure/get_point", test_get_point);
g_test_add_func ("/measure/closest_point", test_closest_point);
g_test_add_func ("/measure/closest_point_for_point", test_closest_point_for_point);
g_test_add_func ("/measure/in-fill-union", test_in_fill_union);
g_test_add_func ("/measure/in-fill-rotated", test_in_fill_rotated);
return g_test_run ();
}

View File

@@ -389,6 +389,12 @@ foreach t : tests
endforeach
internal_tests = [
['measure'],
['measure-special-cases'],
['path'],
['path-special-cases'],
[ 'curve' ],
[ 'curve-special-cases' ],
[ 'diff' ],
[ 'half-float' ],
['rounded-rect'],

View File

@@ -0,0 +1,322 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include <gtk/gtk.h>
#include "gsk/gskpath.h"
#include "gsk/gskpathbuilder.h"
/* testcases from path_parser.rs in librsvg */
static void
test_rsvg_parse (void)
{
struct {
const char *in;
const char *out;
} tests[] = {
{ "", "" },
// numbers
{ "M 10 20", "M 10 20" },
{ "M -10 -20", "M -10 -20" },
{ "M .10 0.20", "M 0.1 0.2" },
{ "M -.10 -0.20", "M -0.1 -0.2" },
{ "M-.10-0.20", "M -0.1 -0.2" },
{ "M10.5.50", "M 10.5 0.5" },
{ "M.10.20", "M 0.1 0.2" },
{ "M .10E1 .20e-4", "M 1 2e-05" },
{ "M-.10E1-.20", "M -1 -0.2" },
{ "M10.10E2 -0.20e3", "M 1010 -200" },
{ "M-10.10E2-0.20e-3", "M -1010 -0.0002" },
{ "M1e2.5", "M 100 0.5" },
{ "M1e-2.5", "M 0.01 0.5" },
{ "M1e+2.5", "M 100 0.5" },
// bogus numbers
{ "M+", NULL },
{ "M-", NULL },
{ "M+x", NULL },
{ "M10e", NULL },
{ "M10ex", NULL },
{ "M10e-", NULL },
{ "M10e+x", NULL },
// numbers with comma
{ "M 10, 20", "M 10 20" },
{ "M -10,-20", "M -10 -20" },
{ "M.10 , 0.20", "M 0.1 0.2" },
{ "M -.10, -0.20 ", "M -0.1 -0.2" },
{ "M-.10-0.20", "M -0.1 -0.2" },
{ "M.10.20", "M 0.1 0.2" },
{ "M .10E1,.20e-4", "M 1 2e-05" },
{ "M-.10E-2,-.20", "M -0.001 -0.2" },
{ "M10.10E2,-0.20e3", "M 1010 -200" },
{ "M-10.10E2,-0.20e-3", "M -1010 -0.0002" },
// single moveto
{ "M 10 20 ", "M 10 20" },
{ "M10,20 ", "M 10 20" },
{ "M10 20 ", "M 10 20" },
{ " M10,20 ", "M 10 20" },
// relative moveto
{ "m10 20", "M 10 20" },
// absolute moveto with implicit lineto
{ "M10 20 30 40", "M 10 20 L 30 40" },
{ "M10,20,30,40", "M 10 20 L 30 40" },
{ "M.1-2,3E2-4", "M 0.1 -2 L 300 -4" },
// relative moveto with implicit lineto
{ "m10 20 30 40", "M 10 20 L 40 60" },
// relative moveto with relative lineto sequence
{ "m 46,447 l 0,0.5 -1,0 -1,0 0,1 0,12",
"M 46 447 L 46 447.5 L 45 447.5 L 44 447.5 L 44 448.5 L 44 460.5" },
// absolute moveto with implicit linetos
{ "M10,20 30,40,50 60", "M 10 20 L 30 40 L 50 60" },
// relative moveto with implicit linetos
{ "m10 20 30 40 50 60", "M 10 20 L 40 60 L 90 120" },
// absolute moveto moveto
{ "M10 20 M 30 40", "M 10 20 M 30 40" },
// relative moveto moveto
{ "m10 20 m 30 40", "M 10 20 M 40 60" },
// relative moveto lineto moveto
{ "m10 20 30 40 m 50 60", "M 10 20 L 40 60 M 90 120" },
// absolute moveto lineto
{ "M10 20 L30,40", "M 10 20 L 30 40" },
// relative moveto lineto
{ "m10 20 l30,40", "M 10 20 L 40 60" },
// relative moveto lineto lineto abs lineto
{ "m10 20 30 40l30,40,50 60L200,300",
"M 10 20 L 40 60 L 70 100 L 120 160 L 200 300" },
// horizontal lineto
{ "M10 20 H30", "M 10 20 L 30 20" },
{ "M 10 20 H 30 40", "M 10 20 L 30 20 L 40 20" },
{ "M10 20 H30,40-50", "M 10 20 L 30 20 L 40 20 L -50 20" },
{ "m10 20 h30,40-50", "M 10 20 L 40 20 L 80 20 L 30 20" },
// vertical lineto
{ "M10 20 V30", "M 10 20 L 10 30" },
{ "M10 20 V30 40", "M 10 20 L 10 30 L 10 40" },
{ "M10 20 V30,40-50", "M 10 20 L 10 30 L 10 40 L 10 -50" },
{ "m10 20 v30,40-50", "M 10 20 L 10 50 L 10 90 L 10 40" },
// curveto
{ "M10 20 C 30,40 50 60-70,80", "M 10 20 C 30 40, 50 60, -70 80" },
{ "M10 20 C 30,40 50 60-70,80,90 100,110 120,130,140",
"M 10 20 C 30 40, 50 60, -70 80 C 90 100, 110 120, 130 140" },
{ "m10 20 c 30,40 50 60-70,80,90 100,110 120,130,140",
"M 10 20 C 40 60, 60 80, -60 100 C 30 200, 50 220, 70 240" },
{ "m10 20 c 30,40 50 60-70,80 90 100,110 120,130,140",
"M 10 20 C 40 60, 60 80, -60 100 C 30 200, 50 220, 70 240" },
// smooth curveto
{ "M10 20 S 30,40-50,60", "M 10 20 C 10 20, 30 40, -50 60" },
{ "M10 20 S 30,40 50 60-70,80,90 100",
"M 10 20 C 10 20, 30 40, 50 60 C 70 80, -70 80, 90 100" },
// quadratic curveto
{ "M10 20 Q30 40 50 60", "M 10 20 Q 30 40, 50 60" },
{ "M10 20 Q30 40 50 60,70,80-90 100",
"M 10 20 Q 30 40, 50 60 Q 70 80, -90 100" },
{ "m10 20 q 30,40 50 60-70,80 90 100",
"M 10 20 Q 40 60, 60 80 Q -10 160, 150 180" },
// smooth quadratic curveto
{ "M10 20 T30 40", "M 10 20 Q 10 20, 30 40" },
{ "M10 20 Q30 40 50 60 T70 80", "M 10 20 Q 30 40, 50 60 Q 70 80, 70 80" },
{ "m10 20 q 30,40 50 60t-70,80",
"M 10 20 Q 40 60, 60 80 Q 80 100, -10 160" },
// elliptical arc. Exact numbers depend on too much math, so just verify
// that these parse successfully
{ "M 1 3 A 1 2 3 00 6 7", "path" },
{ "M 1 2 A 1 2 3 016 7", "path" },
{ "M 1 2 A 1 2 3 10,6 7", "path" },
{ "M 1 2 A 1 2 3 1,1 6 7", "path" },
{ "M 1 2 A 1 2 3 1 1 6 7", "path" },
{ "M 1 2 A 1 2 3 1 16 7", "path" },
// close path
{ "M10 20 Z", "M 10 20 Z" },
{ "m10 20 30 40 m 50 60 70 80 90 100z", "M 10 20 L 40 60 M 90 120 L 160 200 L 250 300 Z" },
// must start with moveto
{ " L10 20", NULL },
// moveto args
{ "M", NULL },
{ "M,", NULL },
{ "M10", NULL },
{ "M10,", NULL },
{ "M10x", NULL },
{ "M10,x", NULL },
{ "M10-20,", NULL },
{ "M10-20-30", NULL },
{ "M10-20-30 x", NULL },
// closepath args
{ "M10-20z10", NULL },
{ "M10-20z,", NULL },
// lineto args
{ "M10-20L10", NULL },
{ "M 10,10 L 20,20,30", NULL },
{ "M 10,10 L 20,20,", NULL },
// horizontal lineto args
{ "M10-20H", NULL },
{ "M10-20H,", NULL },
{ "M10-20H30,", NULL },
// vertical lineto args
{ "M10-20v", NULL },
{ "M10-20v,", NULL },
{ "M10-20v30,", NULL },
// curveto args
{ "M10-20C1", NULL },
{ "M10-20C1,", NULL },
{ "M10-20C1 2", NULL },
{ "M10-20C1,2,", NULL },
{ "M10-20C1 2 3", NULL },
{ "M10-20C1,2,3", NULL },
{ "M10-20C1,2,3,", NULL },
{ "M10-20C1 2 3 4", NULL },
{ "M10-20C1,2,3,4", NULL },
{ "M10-20C1,2,3,4,", NULL },
{ "M10-20C1 2 3 4 5", NULL },
{ "M10-20C1,2,3,4,5", NULL },
{ "M10-20C1,2,3,4,5,", NULL },
{ "M10-20C1,2,3,4,5,6,", NULL },
// smooth curveto args
{ "M10-20S1", NULL },
{ "M10-20S1,", NULL },
{ "M10-20S1 2", NULL },
{ "M10-20S1,2,", NULL },
{ "M10-20S1 2 3", NULL },
{ "M10-20S1,2,3,", NULL },
{ "M10-20S1,2,3,4,", NULL },
// quadratic curveto args
{ "M10-20Q1", NULL },
{ "M10-20Q1,", NULL },
{ "M10-20Q1 2", NULL },
{ "M10-20Q1,2,", NULL },
{ "M10-20Q1 2 3", NULL },
{ "M10-20Q1,2,3", NULL },
{ "M10-20Q1,2,3,", NULL },
{ "M10 20 Q30 40 50 60,", NULL },
// smooth quadratic curveto args
{ "M10-20T1", NULL },
{ "M10-20T1,", NULL },
{ "M10 20 T 30 40,", NULL },
// elliptical arc args
{ "M10-20A1", NULL },
{ "M10-20A1,", NULL },
{ "M10-20A1 2", NULL },
{ "M10-20A1 2,", NULL },
{ "M10-20A1 2 3", NULL },
{ "M10-20A1 2 3,", NULL },
{ "M10-20A1 2 3 4", NULL },
{ "M10-20A1 2 3 1", NULL },
{ "M10-20A1 2 3,1,", NULL },
{ "M10-20A1 2 3 1 5", NULL },
{ "M10-20A1 2 3 1 1", NULL },
{ "M10-20A1 2 3,1,1,", NULL },
{ "M10-20A1 2 3 1 1 6", NULL },
{ "M10-20A1 2 3,1,1,6,", NULL },
{ "M 1 2 A 1 2 3 1.0 0.0 6 7", NULL },
{ "M10-20A1 2 3,1,1,6,7,", NULL },
// misc
{ "M.. 1,0 0,100000", NULL },
{ "M 10 20,M 10 20", NULL },
{ "M 10 20, M 10 20", NULL },
{ "M 10 20, M 10 20", NULL },
{ "M 10 20, ", NULL },
};
int i;
for (i = 0; i < G_N_ELEMENTS (tests); i++)
{
GskPath *path;
char *string;
char *string2;
if (g_test_verbose ())
g_print ("%d: %s\n", i, tests[i].in);
path = gsk_path_parse (tests[i].in);
if (tests[i].out)
{
g_assert_nonnull (path);
string = gsk_path_to_string (path);
gsk_path_unref (path);
if (strcmp (tests[i].out, "path") != 0)
{
/* Preferred, but doesn't work, because gsk_path_print()
* prints numbers with insane accuracy
*/
/* g_assert_cmpstr (tests[i].out, ==, string); */
path = gsk_path_parse (tests[i].out);
g_assert_nonnull (path);
string2 = gsk_path_to_string (path);
gsk_path_unref (path);
g_assert_cmpstr (string, ==, string2);
g_free (string2);
}
path = gsk_path_parse (string);
g_assert_nonnull (path);
string2 = gsk_path_to_string (path);
gsk_path_unref (path);
g_assert_cmpstr (string, ==, string2);
g_free (string);
g_free (string2);
}
else
g_assert_null (path);
}
}
/* Test that circles and rectangles serialize as expected and can be
* round-tripped through strings.
*/
static void
test_serialize_custom_contours (void)
{
GskPathBuilder *builder;
GskPath *path;
GskPath *path1;
char *string;
char *string1;
builder = gsk_path_builder_new ();
gsk_path_builder_add_circle (builder, &GRAPHENE_POINT_INIT (100, 100), 50);
gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (111, 222, 333, 444));
path = gsk_path_builder_free_to_path (builder);
string = gsk_path_to_string (path);
g_assert_cmpstr ("M 150 100 A 50 50 0 0 0 50 100 A 50 50 0 0 0 150 100 z M 111 222 h 333 v 444 h -333 z", ==, string);
path1 = gsk_path_parse (string);
string1 = gsk_path_to_string (path1);
g_assert_cmpstr (string, ==, string1);
g_free (string);
g_free (string1);
gsk_path_unref (path);
gsk_path_unref (path1);
}
int
main (int argc,
char *argv[])
{
gtk_test_init (&argc, &argv, NULL);
g_test_add_func ("/path/rsvg-parse", test_rsvg_parse);
g_test_add_func ("/path/serialize-custom-contours", test_serialize_custom_contours);
return g_test_run ();
}

656
testsuite/gsk/path.c Normal file
View File

@@ -0,0 +1,656 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include <gtk/gtk.h>
#include "gsk/gskpath.h"
#include "gsk/gskpathbuilder.h"
static float
random_weight (void)
{
if (g_test_rand_bit ())
return g_test_rand_double_range (1, 20);
else
return 1.0 / g_test_rand_double_range (1, 20);
}
static GskPath *
create_random_degenerate_path (guint max_contours)
{
#define N_DEGENERATE_PATHS 15
GskPathBuilder *builder;
guint i;
builder = gsk_path_builder_new ();
switch (g_test_rand_int_range (0, N_DEGENERATE_PATHS))
{
case 0:
/* empty path */
break;
case 1:
/* a single point */
gsk_path_builder_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 2:
/* N points */
for (i = 0; i < MIN (10, max_contours); i++)
{
gsk_path_builder_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
}
break;
case 3:
/* 1 closed point */
gsk_path_builder_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
gsk_path_builder_close (builder);
break;
case 4:
/* the same point closed N times */
gsk_path_builder_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
for (i = 0; i < MIN (10, max_contours); i++)
{
gsk_path_builder_close (builder);
}
break;
case 5:
/* a zero-width and zero-height rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
0, 0));
break;
case 6:
/* a zero-width rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
0,
g_test_rand_double_range (-1000, 1000)));
break;
case 7:
/* a zero-height rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
0));
break;
case 8:
/* a negative-size rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 0),
g_test_rand_double_range (-1000, 0)));
break;
case 9:
/* an absolutely random rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000)));
break;
case 10:
/* an absolutely random rect */
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000)));
break;
case 11:
/* an absolutely random circle */
gsk_path_builder_add_circle (builder,
&GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000)),
g_test_rand_double_range (1, 1000));
break;
case 12:
/* a zero-length line */
{
graphene_point_t point = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
gsk_path_builder_move_to (builder, point.x, point.y);
gsk_path_builder_line_to (builder, point.x, point.y);
}
break;
case 13:
/* a curve with start == end */
{
graphene_point_t point = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
gsk_path_builder_move_to (builder, point.x, point.y);
gsk_path_builder_cubic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
point.x, point.y);
}
break;
case 14:
/* a conic with start == end */
{
graphene_point_t point = GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
gsk_path_builder_move_to (builder, point.x, point.y);
gsk_path_builder_conic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
point.x, point.y,
random_weight ());
}
break;
case N_DEGENERATE_PATHS:
default:
g_assert_not_reached ();
}
return gsk_path_builder_free_to_path (builder);
}
static GskPath *
create_random_path (guint max_contours);
static void
add_shape_contour (GskPathBuilder *builder)
{
#define N_SHAPE_CONTOURS 3
switch (g_test_rand_int_range (0, N_SHAPE_CONTOURS))
{
case 0:
gsk_path_builder_add_rect (builder,
&GRAPHENE_RECT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (1, 1000),
g_test_rand_double_range (1, 1000)));
break;
case 1:
gsk_path_builder_add_circle (builder,
&GRAPHENE_POINT_INIT (g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000)),
g_test_rand_double_range (1, 1000));
break;
case 2:
{
GskPath *path = create_random_path (1);
gsk_path_builder_add_path (builder, path);
gsk_path_unref (path);
}
break;
case N_SHAPE_CONTOURS:
default:
g_assert_not_reached ();
break;
}
}
static void
add_standard_contour (GskPathBuilder *builder)
{
guint i, n;
if (g_test_rand_bit ())
{
if (g_test_rand_bit ())
gsk_path_builder_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
else
gsk_path_builder_rel_move_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
}
/* that 20 is random, but should be enough to get some
* crazy self-intersecting shapes */
n = g_test_rand_int_range (1, 20);
for (i = 0; i < n; i++)
{
switch (g_test_rand_int_range (0, 8))
{
case 0:
gsk_path_builder_line_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 1:
gsk_path_builder_rel_line_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 2:
gsk_path_builder_quad_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 3:
gsk_path_builder_rel_quad_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 4:
gsk_path_builder_cubic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 5:
gsk_path_builder_rel_cubic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000));
break;
case 6:
gsk_path_builder_conic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
random_weight ());
break;
case 7:
gsk_path_builder_rel_conic_to (builder,
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
g_test_rand_double_range (-1000, 1000),
random_weight ());
break;
default:
g_assert_not_reached();
break;
}
}
if (g_test_rand_bit ())
gsk_path_builder_close (builder);
}
static GskPath *
create_random_path (guint max_contours)
{
GskPathBuilder *builder;
guint i, n;
/* 5% chance for a weird shape */
if (!g_test_rand_int_range (0, 20))
return create_random_degenerate_path (max_contours);
builder = gsk_path_builder_new ();
n = g_test_rand_int_range (1, 10);
n = MIN (n, max_contours);
for (i = 0; i < n; i++)
{
/* 2/3 of shapes are standard contours */
if (g_test_rand_int_range (0, 3))
add_standard_contour (builder);
else
add_shape_contour (builder);
}
return gsk_path_builder_free_to_path (builder);
}
typedef struct {
GskPathOperation op;
graphene_point_t pts[4];
float weight;
} PathOperation;
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);
}
static void
path_operation_print (const PathOperation *p,
GString *string)
{
switch (p->op)
{
case GSK_PATH_MOVE:
g_string_append (string, "M ");
_g_string_append_point (string, &p->pts[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, &p->pts[1]);
break;
case GSK_PATH_QUAD:
g_string_append (string, " Q ");
_g_string_append_point (string, &p->pts[1]);
g_string_append (string, ", ");
_g_string_append_point (string, &p->pts[2]);
break;
case GSK_PATH_CUBIC:
g_string_append (string, " C ");
_g_string_append_point (string, &p->pts[1]);
g_string_append (string, ", ");
_g_string_append_point (string, &p->pts[2]);
g_string_append (string, ", ");
_g_string_append_point (string, &p->pts[3]);
break;
case GSK_PATH_CONIC:
/* This is not valid SVG */
g_string_append (string, " O ");
_g_string_append_point (string, &p->pts[1]);
g_string_append (string, ", ");
_g_string_append_point (string, &p->pts[2]);
g_string_append (string, ", ");
_g_string_append_double (string, p->weight);
break;
default:
g_assert_not_reached();
return;
}
}
static gboolean
path_operation_equal (const PathOperation *p1,
const PathOperation *p2,
float epsilon)
{
if (p1->op != p2->op)
return FALSE;
/* No need to compare pts[0] for most ops, that's just
* duplicate work. */
switch (p1->op)
{
case GSK_PATH_MOVE:
return graphene_point_near (&p1->pts[0], &p2->pts[0], epsilon);
case GSK_PATH_LINE:
case GSK_PATH_CLOSE:
return graphene_point_near (&p1->pts[1], &p2->pts[1], epsilon);
case GSK_PATH_QUAD:
return graphene_point_near (&p1->pts[1], &p2->pts[1], epsilon)
&& graphene_point_near (&p1->pts[2], &p2->pts[2], epsilon);
case GSK_PATH_CUBIC:
return graphene_point_near (&p1->pts[1], &p2->pts[1], epsilon)
&& graphene_point_near (&p1->pts[2], &p2->pts[2], epsilon)
&& graphene_point_near (&p1->pts[3], &p2->pts[3], epsilon);
case GSK_PATH_CONIC:
return graphene_point_near (&p1->pts[1], &p2->pts[1], epsilon)
&& graphene_point_near (&p1->pts[2], &p2->pts[2], epsilon)
&& G_APPROX_VALUE (p1->weight, p2->weight, epsilon);
default:
g_return_val_if_reached (FALSE);
}
}
static gboolean
collect_path_operation_cb (GskPathOperation op,
const graphene_point_t *pts,
gsize n_pts,
float weight,
gpointer user_data)
{
g_array_append_vals (user_data,
(PathOperation[1]) { {
op,
{
GRAPHENE_POINT_INIT(pts[0].x, pts[0].y),
GRAPHENE_POINT_INIT(n_pts > 1 ? pts[1].x : 0,
n_pts > 1 ? pts[1].y : 0),
GRAPHENE_POINT_INIT(n_pts > 2 ? pts[2].x : 0,
n_pts > 2 ? pts[2].y : 0),
GRAPHENE_POINT_INIT(n_pts > 3 ? pts[3].x : 0,
n_pts > 3 ? pts[3].y : 0)
},
weight
} },
1);
return TRUE;
}
static GArray *
collect_path (GskPath *path)
{
GArray *array = g_array_new (FALSE, FALSE, sizeof (PathOperation));
/* Use -1 here because we want all the flags, even future additions */
gsk_path_foreach (path, -1, collect_path_operation_cb, array);
return array;
}
static void
assert_path_equal_func (const char *domain,
const char *file,
int line,
const char *func,
GskPath *path1,
GskPath *path2,
float epsilon)
{
GArray *ops1, *ops2;
guint i;
ops1 = collect_path (path1);
ops2 = collect_path (path2);
for (i = 0; i < MAX (ops1->len, ops2->len); i++)
{
PathOperation *op1 = i < ops1->len ? &g_array_index (ops1, PathOperation, i) : NULL;
PathOperation *op2 = i < ops2->len ? &g_array_index (ops2, PathOperation, i) : NULL;
if (op1 == NULL || op2 == NULL || !path_operation_equal (op1, op2, epsilon))
{
GString *string;
guint j;
/* Find the operation we start to print */
for (j = i; j-- > 0; )
{
PathOperation *op = &g_array_index (ops1, PathOperation, j);
if (op->op == GSK_PATH_MOVE)
break;
if (j + 3 == i)
{
j = i - 1;
break;
}
}
string = g_string_new (j == 0 ? "" : "... ");
for (; j < i; j++)
{
PathOperation *op = &g_array_index (ops1, PathOperation, j);
path_operation_print (op, string);
g_string_append_c (string, ' ');
}
g_string_append (string, "\\\n ");
if (op1)
{
path_operation_print (op1, string);
if (ops1->len > i + 1)
g_string_append (string, " ...");
}
g_string_append (string, "\n ");
if (op1)
{
path_operation_print (op2, string);
if (ops2->len > i + 1)
g_string_append (string, " ...");
}
g_assertion_message (domain, file, line, func, string->str);
g_string_free (string, TRUE);
}
}
g_array_free (ops1, TRUE);
g_array_free (ops2, TRUE);
}
#define assert_path_equal(p1,p2) assert_path_equal_func(G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (p1),(p2), FLOAT_EPSILON)
#define assert_path_equal_with_epsilon(p1,p2, epsilon) \
assert_path_equal_func(G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (p1),(p2), (epsilon))
static void
test_create (void)
{
GskPath *path1, *path2, *built;
GskPathBuilder *builder;
guint i;
char *s;
GString *str;
for (i = 0; i < 1000; i++)
{
builder = gsk_path_builder_new ();
path1 = create_random_path (G_MAXUINT);
gsk_path_builder_add_path (builder, path1);
path2 = create_random_path (G_MAXUINT);
gsk_path_builder_add_path (builder, path2);
built = gsk_path_builder_free_to_path (builder);
str = g_string_new (NULL);
gsk_path_print (path1, str);
if (!gsk_path_is_empty (path1) && !gsk_path_is_empty (path2))
g_string_append_c (str, ' ');
gsk_path_print (path2, str);
s = gsk_path_to_string (built);
g_assert_cmpstr (s, ==, str->str);
g_string_free (str, TRUE);
g_free (s);
gsk_path_unref (built);
gsk_path_unref (path2);
gsk_path_unref (path1);
}
}
static void
test_parse (void)
{
int i;
for (i = 0; i < 1000; i++)
{
GskPath *path1, *path2;
char *string1, *string2;
path1 = create_random_path (G_MAXUINT);
string1 = gsk_path_to_string (path1);
g_assert_nonnull (string1);
path2 = gsk_path_parse (string1);
g_assert_nonnull (path2);
string2 = gsk_path_to_string (path2);
g_assert_nonnull (string2);
assert_path_equal_with_epsilon (path1, path2, 1.f / 1024);
gsk_path_unref (path2);
gsk_path_unref (path1);
g_free (string2);
g_free (string1);
}
}
int
main (int argc,
char *argv[])
{
gtk_test_init (&argc, &argv, NULL);
g_test_add_func ("/path/create", test_create);
g_test_add_func ("/path/parse", test_parse);
return g_test_run ();
}