diff --git a/CMakeLists.txt b/CMakeLists.txt index a9c20a6..c7afd45 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,8 @@ set(sources src/config.h src/drawing.c src/drawing.h + src/coordlist_ops.c + src/coordlist_ops.h src/main.c src/main.h src/input.c diff --git a/README.md b/README.md index 2485b6c..db54af7 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ can draw everywhere onto the screen, highlighting some button or area. Key features include: * **Desktop-independent**. Gromit-MPX works with GNOME, KDE, XFCE, ... - under X11 as well as with a Wayland session using XWayland. + under X11 (every desktop environment) as well as with a Wayland session + using XWayland (most desktop environments). * **Hotkey-based**. The fundamental philosophy is that Gromit-MPX does not get into your way of doing things by sticking some UI widget on your desktop, potentially obscuring more important contents. It *does* @@ -203,6 +204,24 @@ A `RECT`-tool draws rectangles. "red Rectangle" = RECT (color = "red"); +A `SMOOTH`-tool that behaves like `PEN` except that it produces smoothed curves. +The degree of smoothing can be specified using `simplify=N`. `N` can +be imagined as approximate pixel range around the resulting line +within which intermediate points are "simplified away". Closed paths +can be drawn using the `snap=N` option where `N` indicates the maximum +distance between start and end point within which these "snap" together. + + "smoothed line" = SMOOTH (color = "red" simplify=10 snap=30); + +A `ORTHOGONAL`-tool that behaves like `SMOOTH` except that it produces +straight line segments that automatically snap to perfectly horizontal +and vertical direction when their direction deviated by a maximum of +`maxangle` degrees. Transitions between straight segments are drawn as +arcs with a certain `radius`, if these segments exceed a length of +`minlen`. + + "ortho line" = ORTHOGONAL (color="red" size=5 simplify=15 radius=20 minlen=50 snap=40); + If you define a tool with the same name as an input-device (see the output of `xinput --list`) this input-device uses this tool: diff --git a/src/callbacks.c b/src/callbacks.c index 2737f7b..1bb0230 100644 --- a/src/callbacks.c +++ b/src/callbacks.c @@ -31,7 +31,7 @@ #include "config.h" #include "drawing.h" #include "build-config.h" - +#include "coordlist_ops.h" gboolean on_expose (GtkWidget *widget, cairo_t* cr, @@ -142,10 +142,10 @@ void on_monitors_changed ( GdkScreen *screen, parse_config(data); // also calls paint_context_new() :-( - data->default_pen = paint_context_new (data, GROMIT_PEN, data->red, 7, - 0, GROMIT_ARROW_END, 1, G_MAXUINT); - data->default_eraser = paint_context_new (data, GROMIT_ERASER, data->red, 75, - 0, GROMIT_ARROW_END, 1, G_MAXUINT); + data->default_pen = paint_context_new (data, GROMIT_PEN, data->red, 7, 0, GROMIT_ARROW_END, + 5, 10, 15, 25, 1, 0, G_MAXUINT); + data->default_eraser = paint_context_new (data, GROMIT_ERASER, data->red, 75, 0, GROMIT_ARROW_END, + 5, 10, 15, 25, 1, 0, G_MAXUINT); if(!data->composited) // set shape { @@ -281,7 +281,7 @@ gboolean on_buttonpress (GtkWidget *win, GromitPaintType type = devdata->cur_context->type; // store original state to have dynamic update of line and rect - if (type == GROMIT_LINE || type == GROMIT_RECT) + if (type == GROMIT_LINE || type == GROMIT_RECT || type == GROMIT_SMOOTH || type == GROMIT_ORTHOGONAL) { copy_surface(data->aux_backbuffer, data->backbuffer); } @@ -398,6 +398,7 @@ gboolean on_motion (GtkWidget *win, } if (type == GROMIT_LINE) { + GromitArrowType atype = devdata->cur_context->arrow_type; draw_line (data, ev->device, devdata->lastx, devdata->lasty, ev->x, ev->y); if (devdata->cur_context->arrowsize > 0) { @@ -444,12 +445,12 @@ gboolean on_buttonrelease (GtkWidget *win, GromitData *data = (GromitData *) user_data; /* get the device data for this event */ GromitDeviceData *devdata = g_hash_table_lookup(data->devdatatable, ev->device); + GromitPaintContext *ctx = devdata->cur_context; gfloat direction = 0; gint width = 0; - if(devdata->cur_context) - width = devdata->cur_context->arrowsize * devdata->cur_context->width / 2; - + if(ctx) + width = ctx->arrowsize * ctx->width / 2; if ((ev->x != devdata->lastx) || (ev->y != devdata->lasty)) @@ -458,11 +459,39 @@ gboolean on_buttonrelease (GtkWidget *win, if (!devdata->is_grabbed) return FALSE; - GromitPaintType type = devdata->cur_context->type; + GromitPaintType type = ctx->type; + + if (type == GROMIT_SMOOTH || type == GROMIT_ORTHOGONAL) + { + gboolean joined = FALSE; + douglas_peucker(devdata->coordlist, ctx->simplify); + if (ctx->snapdist > 0) + joined = snap_ends(devdata->coordlist, ctx->snapdist); + if (type == GROMIT_SMOOTH) { + add_points(devdata->coordlist, 200); + devdata->coordlist = catmull_rom(devdata->coordlist, 5, joined); + } else { + orthogonalize(devdata->coordlist, ctx->maxangle, ctx->minlen); + round_corners(devdata->coordlist, ctx->radius, 6, joined); + } + + copy_surface(data->backbuffer, data->aux_backbuffer); + GdkRectangle rect = {0, 0, data->width, data->height}; + gdk_window_invalidate_rect(gtk_widget_get_window(data->win), &rect, 0); + + GList *ptr = devdata->coordlist; + while (ptr && ptr->next) + { + GromitStrokeCoordinate *c1 = ptr->data; + GromitStrokeCoordinate *c2 = ptr->next->data; + ptr = ptr->next; + draw_line (data, ev->device, c1->x, c1->y, c2->x, c2->y); + } + } - if (devdata->cur_context->arrowsize != 0) + if (ctx->arrowsize != 0) { - GromitArrowType atype = devdata->cur_context->arrow_type; + GromitArrowType atype = ctx->arrow_type; if (type == GROMIT_LINE) { direction = atan2 (ev->y - devdata->lasty, ev->x - devdata->lastx); @@ -615,8 +644,8 @@ void on_mainapp_selection_received (GtkWidget *widget, "Keeping default.\n"); } GromitPaintContext* line_ctx = - paint_context_new(data, GROMIT_PEN, fg_color, thickness, - 0, GROMIT_ARROW_END, thickness, thickness); + paint_context_new(data, GROMIT_PEN, fg_color, thickness, 0, GROMIT_ARROW_END, + 5, 10, 15, 25, 0, thickness, thickness); GdkRectangle rect; rect.x = MIN (startX,endX) - thickness / 2; diff --git a/src/config.c b/src/config.c index 4c2b1fc..a8d9238 100644 --- a/src/config.c +++ b/src/config.c @@ -30,6 +30,7 @@ #include "config.h" #include "main.h" +#include "math.h" #include "build-config.h" #define KEY_DFLT_SHOW_INTRO_ON_STARTUP TRUE @@ -110,6 +111,44 @@ static gchar* parse_name (GScanner *scanner) return name; } + +enum tool_arguments { + SYM_SIZE = 1, + SYM_COLOR, + SYM_ARROWSIZE, + SYM_ARROWTYPE, + SYM_MINSIZE, + SYM_MAXSIZE, + SYM_MINLEN, + SYM_MAXANGLE, + SYM_RADIUS, + SYM_SIMPLIFY, + SYM_SNAP, +}; + +/* + * get "=VALUE", where VALUE is a float + * returns NAN is an error occurs + */ +gfloat parse_get_float(GScanner *scanner, const gchar *msg) +{ + GTokenType token = g_scanner_get_next_token (scanner); + if (token != G_TOKEN_EQUAL_SIGN) + { + g_printerr ("Missing \"=\"... aborting\n"); + return NAN; + } + token = g_scanner_get_next_token (scanner); + if (token != G_TOKEN_FLOAT) + { + g_printerr ("%s", msg); + g_printerr ("... aborting\n"); + return NAN; + } + return scanner->value.v_float; +} + + gboolean parse_config (GromitData *data) { gboolean status = FALSE; @@ -125,6 +164,7 @@ gboolean parse_config (GromitData *data) GromitPaintType type; GdkRGBA *fg_color=NULL; guint width, arrowsize, minwidth, maxwidth; + guint minlen, maxangle, radius, simplify, snapdist; GromitArrowType arrowtype; /* try user config location */ @@ -168,13 +208,15 @@ gboolean parse_config (GromitData *data) scanner->config->numbers_2_int = 1; scanner->config->int_2_float = 1; - g_scanner_scope_add_symbol (scanner, 0, "PEN", (gpointer) GROMIT_PEN); - g_scanner_scope_add_symbol (scanner, 0, "LINE", (gpointer) GROMIT_LINE); - g_scanner_scope_add_symbol (scanner, 0, "RECT", (gpointer) GROMIT_RECT); - g_scanner_scope_add_symbol (scanner, 0, "ERASER", (gpointer) GROMIT_ERASER); - g_scanner_scope_add_symbol (scanner, 0, "RECOLOR",(gpointer) GROMIT_RECOLOR); - g_scanner_scope_add_symbol (scanner, 0, "HOTKEY", HOTKEY_SYMBOL_VALUE); - g_scanner_scope_add_symbol (scanner, 0, "UNDOKEY", UNDOKEY_SYMBOL_VALUE); + g_scanner_scope_add_symbol (scanner, 0, "PEN", (gpointer) GROMIT_PEN); + g_scanner_scope_add_symbol (scanner, 0, "LINE", (gpointer) GROMIT_LINE); + g_scanner_scope_add_symbol (scanner, 0, "RECT", (gpointer) GROMIT_RECT); + g_scanner_scope_add_symbol (scanner, 0, "SMOOTH", (gpointer) GROMIT_SMOOTH); + g_scanner_scope_add_symbol (scanner, 0, "ORTHOGONAL",(gpointer) GROMIT_ORTHOGONAL); + g_scanner_scope_add_symbol (scanner, 0, "ERASER", (gpointer) GROMIT_ERASER); + g_scanner_scope_add_symbol (scanner, 0, "RECOLOR", (gpointer) GROMIT_RECOLOR); + g_scanner_scope_add_symbol (scanner, 0, "HOTKEY", HOTKEY_SYMBOL_VALUE); + g_scanner_scope_add_symbol (scanner, 0, "UNDOKEY", UNDOKEY_SYMBOL_VALUE); g_scanner_scope_add_symbol (scanner, 1, "BUTTON1", (gpointer) 1); g_scanner_scope_add_symbol (scanner, 1, "BUTTON2", (gpointer) 2); @@ -186,12 +228,17 @@ gboolean parse_config (GromitData *data) g_scanner_scope_add_symbol (scanner, 1, "META", (gpointer) 13); g_scanner_scope_add_symbol (scanner, 1, "ALT", (gpointer) 13); - g_scanner_scope_add_symbol (scanner, 2, "size", (gpointer) 1); - g_scanner_scope_add_symbol (scanner, 2, "color", (gpointer) 2); - g_scanner_scope_add_symbol (scanner, 2, "arrowsize", (gpointer) 3); - g_scanner_scope_add_symbol (scanner, 2, "arrowtype", (gpointer) 4); - g_scanner_scope_add_symbol (scanner, 2, "minsize", (gpointer) 5); - g_scanner_scope_add_symbol (scanner, 2, "maxsize", (gpointer) 6); + g_scanner_scope_add_symbol (scanner, 2, "size", (gpointer) SYM_SIZE); + g_scanner_scope_add_symbol (scanner, 2, "color", (gpointer) SYM_COLOR); + g_scanner_scope_add_symbol (scanner, 2, "arrowsize", (gpointer) SYM_ARROWSIZE); + g_scanner_scope_add_symbol (scanner, 2, "arrowtype", (gpointer) SYM_ARROWTYPE); + g_scanner_scope_add_symbol (scanner, 2, "minsize", (gpointer) SYM_MINSIZE); + g_scanner_scope_add_symbol (scanner, 2, "maxsize", (gpointer) SYM_MAXSIZE); + g_scanner_scope_add_symbol (scanner, 2, "radius", (gpointer) SYM_RADIUS); + g_scanner_scope_add_symbol (scanner, 2, "maxangle", (gpointer) SYM_MAXANGLE); + g_scanner_scope_add_symbol (scanner, 2, "minlen", (gpointer) SYM_MINLEN); + g_scanner_scope_add_symbol (scanner, 2, "simplify", (gpointer) SYM_SIMPLIFY); + g_scanner_scope_add_symbol (scanner, 2, "snap", (gpointer) SYM_SNAP); g_scanner_set_scope (scanner, 0); scanner->config->scope_0_fallback = 0; @@ -230,6 +277,11 @@ gboolean parse_config (GromitData *data) arrowtype = GROMIT_ARROW_END; minwidth = 1; maxwidth = G_MAXUINT; + radius = 10; + minlen = 2 * radius + radius / 2; + maxangle = 15; + simplify = 10; + snapdist = 0; fg_color = data->red; if (token == G_TOKEN_SYMBOL) @@ -250,6 +302,11 @@ gboolean parse_config (GromitData *data) width = context_template->width; arrowsize = context_template->arrowsize; arrowtype = context_template->arrow_type; + radius = context_template->radius; + simplify = context_template->simplify; + minlen = context_template->minlen; + maxangle = context_template->maxangle; + snapdist = context_template->snapdist; minwidth = context_template->minwidth; maxwidth = context_template->maxwidth; fg_color = context_template->paint_color; @@ -281,23 +338,13 @@ gboolean parse_config (GromitData *data) { if (token == G_TOKEN_SYMBOL) { - if ((intptr_t) scanner->value.v_symbol == 1) + if ((intptr_t) scanner->value.v_symbol == SYM_SIZE) { - token = g_scanner_get_next_token (scanner); - if (token != G_TOKEN_EQUAL_SIGN) - { - g_printerr ("Missing \"=\"... aborting\n"); - goto cleanup; - } - token = g_scanner_get_next_token (scanner); - if (token != G_TOKEN_FLOAT) - { - g_printerr ("Missing Size (float)... aborting\n"); - goto cleanup; - } - width = (guint) (scanner->value.v_float + 0.5); + gfloat v = parse_get_float(scanner, "Missing Size (float)"); + if (isnan(v)) goto cleanup; + width = (guint) (v + 0.5); } - else if ((intptr_t) scanner->value.v_symbol == 2) + else if ((intptr_t) scanner->value.v_symbol == SYM_COLOR) { token = g_scanner_get_next_token (scanner); if (token != G_TOKEN_EQUAL_SIGN) @@ -325,24 +372,14 @@ gboolean parse_config (GromitData *data) } color = NULL; } - else if ((intptr_t) scanner->value.v_symbol == 3) + else if ((intptr_t) scanner->value.v_symbol == SYM_ARROWSIZE) { - token = g_scanner_get_next_token (scanner); - if (token != G_TOKEN_EQUAL_SIGN) - { - g_printerr ("Missing \"=\"... aborting\n"); - goto cleanup; - } - token = g_scanner_get_next_token (scanner); - if (token != G_TOKEN_FLOAT) - { - g_printerr ("Missing Arrowsize (float)... " - "aborting\n"); - goto cleanup; - } - arrowsize = scanner->value.v_float; + gfloat v = parse_get_float(scanner, "Missing arrowsize (float)"); + if (isnan(v)) goto cleanup; + arrowsize = (guint)(v + 0.5); + arrowtype = GROMIT_ARROW_END; } - else if ((intptr_t) scanner->value.v_symbol == 4) + else if ((intptr_t) scanner->value.v_symbol == SYM_ARROWTYPE) { token = g_scanner_get_next_token (scanner); if (token != G_TOKEN_EQUAL_SIGN) @@ -370,39 +407,47 @@ gboolean parse_config (GromitData *data) goto cleanup; } } - else if ((intptr_t) scanner->value.v_symbol == 5) + else if ((intptr_t) scanner->value.v_symbol == SYM_MINSIZE) { - token = g_scanner_get_next_token (scanner); - if (token != G_TOKEN_EQUAL_SIGN) - { - g_printerr ("Missing \"=\"... aborting\n"); - goto cleanup; - } - token = g_scanner_get_next_token (scanner); - if (token != G_TOKEN_FLOAT) - { - g_printerr ("Missing Minsize (float)... " - "aborting\n"); - goto cleanup; - } - minwidth = scanner->value.v_float; + gfloat v = parse_get_float(scanner, "Missing minsize (float)"); + if (isnan(v)) goto cleanup; + minwidth = v; } - else if ((intptr_t) scanner->value.v_symbol == 6) + else if ((intptr_t) scanner->value.v_symbol == SYM_MAXSIZE) { - token = g_scanner_get_next_token (scanner); - if (token != G_TOKEN_EQUAL_SIGN) - { - g_printerr ("Missing \"=\"... aborting\n"); - goto cleanup; - } - token = g_scanner_get_next_token (scanner); - if (token != G_TOKEN_FLOAT) - { - g_printerr ("Missing Maxsize (float)... " - "aborting\n"); - goto cleanup; - } - maxwidth = scanner->value.v_float; + gfloat v = parse_get_float(scanner, "Missing maxsize (float)"); + if (isnan(v)) goto cleanup; + maxwidth = v; + } + else if ((intptr_t) scanner->value.v_symbol == SYM_RADIUS) + { + gfloat v = parse_get_float(scanner, "Missing radius (float)"); + if (isnan(v)) goto cleanup; + radius = v; + } + else if ((intptr_t) scanner->value.v_symbol == SYM_MAXANGLE) + { + gfloat v = parse_get_float(scanner, "Missing angle (float)"); + if (isnan(v)) goto cleanup; + maxangle = v; + } + else if ((intptr_t) scanner->value.v_symbol == SYM_SIMPLIFY) + { + gfloat v = parse_get_float(scanner, "Missing simplify value (float)"); + if (isnan(v)) goto cleanup; + simplify = v; + } + else if ((intptr_t) scanner->value.v_symbol == SYM_MINLEN) + { + gfloat v = parse_get_float(scanner, "Missing minlen value (float)"); + if (isnan(v)) goto cleanup; + minlen = v; + } + else if ((intptr_t) scanner->value.v_symbol == SYM_SNAP) + { + gfloat v = parse_get_float(scanner, "Missing snap distance (float)"); + if (isnan(v)) goto cleanup; + snapdist = v; } else { @@ -430,7 +475,9 @@ gboolean parse_config (GromitData *data) } context = paint_context_new (data, type, fg_color, width, - arrowsize, arrowtype, minwidth, maxwidth); + arrowsize, arrowtype, + simplify, radius, maxangle, minlen, snapdist, + minwidth, maxwidth); g_hash_table_insert (data->tool_config, name, context); } else if (token == G_TOKEN_SYMBOL && @@ -568,7 +615,7 @@ int parse_args (int argc, char **argv, GromitData *data) wrong_arg = TRUE; } } - else if (strcmp (arg, "-o") == 0 || + else if (strcmp (arg, "-o") == 0 || strcmp (arg, "--opacity") == 0) { if (i+1 < argc && strtod (argv[i+1], NULL) >= 0.0 && strtod (argv[i+1], NULL) <= 1.0) diff --git a/src/coordlist_ops.c b/src/coordlist_ops.c new file mode 100644 index 0000000..012432e --- /dev/null +++ b/src/coordlist_ops.c @@ -0,0 +1,913 @@ +/* + * Gromit-MPX -- a program for painting on the screen + * + * Gromit Copyright (C) 2000 Simon Budig + * + * Gromit-MPX Copyright (C) 2009,2010 Christian Beier + * Copyright (C) 2024 Pascal Niklaus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#include +#include +#include +#include + +#include "main.h" +#include "drawing.h" +#include "coordlist_ops.h" + +// ================== forward definitions and types ================== + +// ------------------ coordinate-related functions ------------------- + +typedef struct { + gfloat x; + gfloat y; +} xy; + +static xy get_xy_from_coord(GList *ptr); +static void set_coord_from_xy(xy *point, GList *ptr); +static xy xy_vec_from_coords(GList *start, GList *end); +static xy xy_vec(xy *start, xy *end); +static xy xy_add(xy *point, xy *translation); +static gfloat xy_length(xy *vec); +static gfloat coord_distance(GList *p1, GList *p2); +static gfloat distance_from_line(GList *p, GList *line_start, GList *line_end); +static gfloat find_coord_vec_rotation(GList *start0, GList *end0, + GList *start1, GList *end1); +static gfloat find_xy_vec_rotation(xy *start0, xy *end0, + xy *start1, xy *end1); +static gfloat direction_of_coord_vector(GList *p1, GList *p2); +static gfloat angle_deg_snap(gfloat angle, gfloat tolerance, gboolean *snapped); + +// -------------------- 2D affine transformations -------------------- + +typedef struct { + union { + gfloat m[6]; + struct { + gfloat m_11; gfloat m_12; gfloat m_13; + gfloat m_21; gfloat m_22; gfloat m_23; + }; + }; +} trans2D; + +static void unity2D(trans2D *m); +static void rotate2D(gfloat angle, trans2D *m); +static void translate2D(gfloat dx, gfloat dy, trans2D *m); +static void scale2D(gfloat kx, gfloat ky, trans2D *m); +static void add2D(trans2D *t, trans2D *m); +static xy apply2D_xy(xy *coord, trans2D *m); +static void apply2D_coords(GList *start, GList *end, trans2D *m); + +// ------------------------ sections-related ------------------------- + +/* + * Section of a path in GList with 'GromitStrokecoordinate's + * + * 'start' and 'end' point into GList if 'GromitStrokecooordinate's + * + * 'xy_start' and 'xy_end' are the coordinates of the first and last + * point; these are intermediate values and not necessarily in sync + * with the GList pointed into. + * + * 'direction' is the orthogonal direction in degrees (0,90,180,270), + * or -999 indicating a non-orthogonal section. + */ +#define NON_ORTHO_ANGLE -999 + +typedef struct { + GList *start; + GList *end; + xy xy_start; + xy xy_end; + gint direction; +} Section; + +static xy section_center(Section *ptr); +static xy section_dxdy(Section *ptr); +static gfloat section_diag_len(Section *ptr); +static gboolean section_is_ortho(Section *ptr); +static gboolean section_is_vertical(Section *ptr); + +static GList * build_section_list(GList *coords, gint max_angular_deviation, gint min_ortho_len); + +// ----------- stuff for douglas-peucker path simplication ----------- + +/* + * a range of a GList of 'GromitStrokeCoordinate's, ranging from + * 'first' up to and including 'last' + */ +typedef struct { + GList *first; + GList *last; +} ListRange; + +static GList *push_list_range(GList *list, GList *first, GList *last); +static GList *pop_list_range(GList *list, GList **first, GList **last); + +// ----------------- stuff for catmull-rom smoothing ----------------- + +static void cr_f_mul_grxy(gfloat f, GList *p1, GList *p2, xy *xy); +static void cr_f_mul_xy(gfloat f, xy *p1, xy *p2, xy *xy); +static gfloat catmull_rom_tj(gfloat ti, + GromitStrokeCoordinate *pi, + GromitStrokeCoordinate *pj); + +// ===================== end forward definitions ===================== + +// ----------------------------- helpers ----------------------------- + +inline static gfloat square(gfloat x) { + return (x * x); +} + +// ------------------ coordinate-related functions ------------------- +// +// In function names, 'xy' refers to the float 'xy' type with just the +// two coordinates, while 'coord' refers to 'GList' elements holding a +// 'GromitStrokeCoordinate'. 'xy' is returned by value -- for simple +// functions, the compiler will likely the call anyway. + +static xy get_xy_from_coord(GList *ptr) { + xy result; + result.x = ((GromitStrokeCoordinate *)ptr->data)->x; + result.y = ((GromitStrokeCoordinate *)ptr->data)->y; + return result; +} + +static void set_coord_from_xy(xy *point, GList *ptr) { + GromitStrokeCoordinate *coord = ptr->data; + coord->x = point->x + 0.5; + coord->y = point->y + 0.5; +} + +static xy xy_vec_from_coords(GList *start, GList *end) { + xy result; + result.x = ((GromitStrokeCoordinate *)end->data)->x - + ((GromitStrokeCoordinate *)start->data)->x; + result.y = ((GromitStrokeCoordinate *)end->data)->y - + ((GromitStrokeCoordinate *)start->data)->y; + return result; +} + +static xy xy_vec(xy *start, xy *end) { + xy result; + result.x = end->x - start->x; + result.y = end->y - start->y; + return result; +} + +static xy xy_add(xy *point, xy *translation) { + xy result = *point; + result.x += translation->x; + result.y += translation->y; + return result; +} + +static gfloat xy_length(xy *vec) { + return sqrtf(vec->x * vec->x + vec->y * vec->y); +} + + gfloat coord_distance(GList *p1, GList *p2) { + GromitStrokeCoordinate *g1 = p1->data; + GromitStrokeCoordinate *g2 = p2->data; + gfloat dx = g1->x - g2->x; + gfloat dy = g1->y - g2->y; + return sqrtf(dx * dx + dy * dy); +} + +/* + * distance of point 'p' from line a line defined by points + * 'line_start' and 'line_end' + */ +static gfloat distance_from_line(GList *p, + GList *line_start, + GList *line_end) { + GromitStrokeCoordinate * gp = p->data; + GromitStrokeCoordinate * gline_start = line_start->data; + GromitStrokeCoordinate * gline_end = line_end->data; + + gfloat a = (gline_start->y - gline_end->y) * gp->x + + (gline_end->x - gline_start->x) * gp->y + + (gline_start->x * gline_end->y - gline_end->x * gline_start->y); + gfloat l = sqrt(square(gline_start->x - gline_end->x) + + square(gline_start->y - gline_end->y)); + return fabs(a / l); +} + +/* + * find rotation that turns vector0 into direction of vector1. All + * coordinates are pointers into element of a GList of + * 'GromitStrokeCooordinate's + */ +static gfloat find_coord_vec_rotation(GList *start0, + GList *end0, + GList *start1, + GList *end1) { + xy v1 = xy_vec_from_coords(start0, end0); + xy v2 = xy_vec_from_coords(start1, end1); + return atan2(v1.x * v2.y - v2.x * v1.y, v1.x * v2.x + v1.y * v2.y); +} + +/* + * find rotation that turns vector0 into direction of vector1. + */ +static gfloat find_xy_vec_rotation(xy *start0, + xy *end0, + xy *start1, + xy *end1) { + xy v1 = xy_vec(start0, end0); + xy v2 = xy_vec(start1, end1); + return atan2(v1.x * v2.y - v2.x * v1.y, v1.x * v2.x + v1.y * v2.y); +} + +/* + * get angle (direction) of vector (p1->p2) + */ +static gfloat direction_of_coord_vector(GList *p1, GList *p2) { + assert(p1 && p2); + xy xy1 = get_xy_from_coord(p1); + xy xy2 = get_xy_from_coord(p2); + return atan2(xy2.y - xy1.y, xy2.x - xy1.x); +} + +/* + * check if angle is close to horizontal or vertical, within + * tolerance, and if they are, "snap" angle to these + */ +static gfloat angle_deg_snap(gfloat angle, + gfloat tolerance, + gboolean *snapped) { + if (angle < 0) angle += 360.0; + if (angle == 360.0) angle = 0.0; + gfloat ortho = round(angle / 90.0) * 90.0; + if (fabs(angle - ortho) < tolerance) { + if (snapped) *snapped = TRUE; + angle = remainderf(ortho, 360.0); + if (angle < 0) angle += 360.0; + } else { + if (snapped) *snapped = FALSE; + } + return angle; +} + +// -------------------- 2D affine transformations -------------------- + +static void unity2D(trans2D *m) { + memcpy(m->m, (gfloat[6]){1.0, 0.0, 0.0, 0.0, 1.0, 0.0}, sizeof(gfloat[6])); +} + +static void rotate2D(gfloat angle, trans2D *m) { + unity2D(m); + m->m_11 = m->m_22 = cos(angle); + m->m_12 = sin(angle); + m->m_21 = -m->m_12; +} + +static void translate2D(gfloat dx, gfloat dy, trans2D *m) { + unity2D(m); + m->m_13 = dx; + m->m_23 = dy; +} + +static void scale2D(gfloat kx, gfloat ky, trans2D *m) { + unity2D(m); + m->m_11 = kx; + m->m_22 = ky; +} + +static void add2D(trans2D *t, trans2D *m) { + trans2D new; + new.m_11 = t->m_11 * m->m_11 + t->m_12 * m->m_21; + new.m_12 = t->m_11 * m->m_12 + t->m_12 * m->m_22; + new.m_13 = t->m_11 * m->m_13 + t->m_12 * m->m_23 + t->m_13; + new.m_21 = t->m_21 * m->m_11 + t->m_22 * m->m_21; + new.m_22 = t->m_21 * m->m_12 + t->m_22 * m->m_22; + new.m_23 = t->m_21 * m->m_13 + t->m_22 * m->m_23 + t->m_23; + *m = new; +} + +/* + * apply a 2d transformation to xy coordinate + */ +static xy apply2D_xy(xy *coord, trans2D *m) { + xy result; + result.x = m->m_11 * coord->x + m->m_12 * coord->y + m->m_13; + result.y = m->m_21 * coord->x + m->m_22 * coord->y + m->m_23; + return result; +} + +/* + * apply a 2d transformation to section of a 'GList' of + * 'Gromitstrokecoordinate's + */ +static void apply2D_coords(GList *start, GList *end, trans2D *m) { + while (start) { + GromitStrokeCoordinate *ptr = start->data; + gfloat newx = m->m_11 * ptr->x + m->m_12 * ptr->y + m->m_13; + ptr->y = m->m_21 * ptr->x + m->m_22 * ptr->y + m->m_23 + 0.5; + ptr->x = newx + 0.5; + if (start == end) break; + start = start->next; + } +} + +// ---------------------- path-section-related ----------------------- + +/* + * get middle point between start and end of section by accessing + * original stroke coordinates + */ +static xy section_center(Section *ptr) { + xy start = get_xy_from_coord(ptr->start); + xy end = get_xy_from_coord(ptr->end); + start.x = 0.5 * (start.x + end.x); + start.y = 0.5 * (start.y + end.y); + return start; +} + +/* + * get vector from start to end of section by accessing original + * stroke coordinates + */ +static xy section_dxdy(Section *ptr) { + xy start = get_xy_from_coord(ptr->start); + xy end = get_xy_from_coord(ptr->end); + start.x = (end.x - start.x); + start.y = (end.y - start.y); + return start; +} + +/* + * get distance between start and end point of section + */ +static gfloat section_diag_len(Section *ptr) { + xy start = get_xy_from_coord(ptr->start); + xy end = get_xy_from_coord(ptr->end); + return sqrt(square(start.x - end.x) + square(start.y - end.y)); +} + +/* + * is section horizontal or vertical ? + */ +static gboolean section_is_ortho(Section *ptr) { + return (ptr->direction % 90) == 0; +} + +/* + * is section vertical ? + */ +static gboolean section_is_vertical(Section *ptr) { + return (ptr->direction % 180 == 90); +} + +/* + * scan GList of 'GromitStrokeCoordinate's and build list with + * 'Sections' that are orthogonal (withing +- max_angular_deviation) or + * 'free' + */ +static GList *build_section_list(GList *const coords, + const gint max_angular_deviation, + const gint min_ortho_len) { + GList *section_list = NULL; + GList *coords_ptr = coords; + + while (coords_ptr && coords_ptr->next) { + Section *new_section = g_malloc(sizeof(Section)); + new_section->start = new_section->end = coords_ptr; + + // check if section is orthogonal + gboolean ortho; + gint angle, angle0; + while (new_section->end->next) { + angle = direction_of_coord_vector(new_section->end, new_section->end->next) * 180 / M_PI; + angle = angle_deg_snap(angle, max_angular_deviation, &ortho); + if (!ortho) + break; + if (new_section->start == new_section->end) + angle0 = angle; + else if (angle != angle0) + break; + new_section->end = new_section->end->next; + } + + // if section exceeds minimum length, add orthogonal section to list + if (section_diag_len(new_section) >= min_ortho_len) { + new_section->direction = angle0; + section_list = g_list_append(section_list, new_section); + // keep only first and last coordinate in section + GList *const ptr = new_section->start; + while (ptr->next != new_section->end) { + g_free(ptr->next->data); + GList *tmp = g_list_delete_link(coords, ptr->next); + assert(tmp == coords); + } + coords_ptr = new_section->end; + continue; + } + + // if not, include it in free (non-orthogonal) section + while (new_section->end->next) { + angle = direction_of_coord_vector(new_section->end, new_section->end->next) * 180 / M_PI; + angle = angle_deg_snap(angle, max_angular_deviation, &ortho); + if (ortho) break; + new_section->end = new_section->end->next; + } + + new_section->direction = NON_ORTHO_ANGLE; + section_list = g_list_append(section_list, new_section); + coords_ptr = new_section->end; + } + + // cleanup: join successive non-orthogonal sections + GList *ptr = section_list; + while (ptr && ptr->next) { + gint dir = ((Section *)ptr->data)->direction; + gint nextdir = ((Section *)ptr->next->data)->direction; + if (dir == nextdir) { + ((Section *)ptr->data)->end = ((Section *)ptr->next->data)->end; + g_free(ptr->next->data); + section_list = g_list_delete_link(section_list, ptr->next); + } else { + ptr = ptr->next; + } + } + + // fill in start and end coordinates + for (ptr = section_list; ptr; ptr = ptr->next) { + Section *sec = ptr->data; + sec->xy_start = get_xy_from_coord(sec->start); + sec->xy_end = get_xy_from_coord(sec->end); + } + + return section_list; +} + +// ----------------------- orthogonalize path ------------------------ + +void orthogonalize(GList *const coords, + const gint max_angular_deviation, + const gint min_ortho_len) { + GList *sec_list = + build_section_list(coords, max_angular_deviation, min_ortho_len); + + if (g_list_length(sec_list) > 1) { + // determine "fixed" coordinate of H and V sections (x for V and y + // for H) and adjust start and end points of path accordingly + for (GList *ptr = sec_list; ptr; ptr = ptr->next) { + Section *const sec = ((Section *)ptr->data); + if (section_is_ortho(sec)) { + xy center; + if (!ptr->prev) + center = sec->xy_start; + else if (!ptr->next) + center = sec->xy_end; + else + center = section_center(sec); + gboolean horiz = (sec->direction == 0 || sec->direction == 180); + if (horiz) { + sec->xy_start.y = sec->xy_end.y = center.y; + } else { + sec->xy_start.x = sec->xy_end.x = center.x; + } + } + } + + // now "join" ends of adjacent sections + for (GList *ptr = sec_list; ptr; ptr = ptr->next) { + Section *const sec = ptr->data; + if (section_is_ortho(sec)) { + if (ptr->next && section_is_ortho(ptr->next->data)) { + Section *const next_sec = ptr->next->data; + // join orthogonal section to other orthogonal section + const gboolean first_v = section_is_vertical(sec); + const gboolean second_v = section_is_vertical(next_sec); + if (first_v != second_v) { + // sections meet at 90 degrees -> join them + if (first_v) { + sec->xy_end.y = next_sec->xy_start.y; + next_sec->xy_start.x = sec->xy_end.x; + } else { + sec->xy_end.x = next_sec->xy_start.x; + next_sec->xy_start.y = sec->xy_end.y; + } + } else { + // sections are parallel -> align ends + if (first_v) { + sec->xy_end.y = next_sec->xy_start.y = + 0.5 * (sec->xy_end.y + next_sec->xy_start.y); + } else { + sec->xy_end.x = next_sec->xy_start.x = + 0.5 * (sec->xy_end.x + next_sec->xy_start.x); + } + } + } else { + if (ptr->prev) { + Section *const prev_sec = ((Section *)ptr->prev->data); + if (section_is_ortho(prev_sec)) { + sec->xy_start = prev_sec->xy_end; + } + } + } + } else { + // section is not orthogonal + trans2D m, t; + if (ptr->prev && ptr->next) { + Section *const next_sec = ((Section *)ptr->next->data); + Section *const prev_sec = ((Section *)ptr->prev->data); + gboolean next_vert = section_is_vertical(next_sec); + gboolean prev_vert = section_is_vertical(prev_sec); + + // one could fine-tune the orientation of the free section, + // based on how skewed the adjacent orthogonal sections are. + // gfloat angle = 0.5 * (remainderf(section_angle(prev_sec), M_PI_2) + + // remainderf(section_angle(next_sec), M_PI_2)); + // rotate2D(angle, &m); // or -angle ?? + // apply2D(sec->start, sec->end, &m); + + xy delta = section_dxdy(sec); + + if (prev_vert != next_vert) { + // prev & next sections are at 90 degrees -> + // adjust their lengths to accomodate intermediate section. + // + // TODO: this could produce artifacts when ends would be moved back + // too much; then, the section could be scaled down to fit or rotated. + // But this does not seem to happen... + if (prev_vert) { + next_sec->xy_start.x = prev_sec->xy_end.x + delta.x; + prev_sec->xy_end.y = next_sec->xy_start.y - delta.y; + } else { + next_sec->xy_start.y = prev_sec->xy_end.y + delta.y; + prev_sec->xy_end.x = next_sec->xy_start.x - delta.x; + } + // update fields in Section + sec->xy_start = prev_sec->xy_end; + sec->xy_end = next_sec->xy_start; + // move section to right place + GromitStrokeCoordinate *point = (sec->start->data); + translate2D(prev_sec->xy_end.x - point->x, + prev_sec->xy_end.y - point->y, + &m); + } else { + // prev & next are somehow parallel -> fit free section between adjacent sections + gfloat x0 = prev_sec->xy_end.x; + gfloat y0 = prev_sec->xy_end.y; + translate2D(-x0, -y0, &m); + gfloat rot = find_xy_vec_rotation(&sec->xy_start, &sec->xy_end, + &prev_sec->xy_end, &next_sec->xy_start); + rotate2D(rot, &t); + add2D(&t, &m); + xy tmpvec = xy_vec(&prev_sec->xy_end, &next_sec->xy_start); + gfloat scale = xy_length(&tmpvec) / section_diag_len(sec); + scale2D(scale, scale, &t); + add2D(&t, &m); + translate2D(x0, y0, &t); + add2D(&t, &m); + } + apply2D_coords(sec->start, sec->end, &m); + } + } + } + + // copy start and end points of orthogonal sections to coords + for (GList *ptr = sec_list; ptr; ptr = ptr->next) { + Section *const sec = ((Section *)ptr->data); + if (section_is_ortho(sec)) { + set_coord_from_xy(&sec->xy_start, sec->start); + set_coord_from_xy(&sec->xy_end, sec->end); + } + } + } + + g_list_free_full(sec_list, g_free); +} + +/* + * insert points into list of 'GromitStrokeCoordinate's so that the + * distance between points becomes smaller than 'max_distance' + */ +void add_points(GList *coords, gfloat max_distance) { + GList *ptr = coords; + while (ptr && ptr->next) { + gfloat d = coord_distance(ptr, ptr->next); + if (d > max_distance) { + gint n_sections = ceilf(d / max_distance); + GromitStrokeCoordinate *const p0 = ptr->data; + GromitStrokeCoordinate *const p1 = ptr->next->data; + + for (gint step = n_sections - 1; step > 0; step--) { + gfloat k = (gfloat)step / n_sections; + + GromitStrokeCoordinate *new_coord = + g_malloc(sizeof(GromitStrokeCoordinate)); + new_coord->width = p0->width; + new_coord->x = p0->x + (p1->x - p0->x) * k + 0.5; + new_coord->y = p0->y + (p1->y - p0->y) * k + 0.5; + GList *tmp = g_list_insert_before(coords, ptr->next, new_coord); + assert(tmp == coords); + } + } + ptr = ptr->next; + } +} + +/* + * add rounded corners between sections + */ +void round_corners(GList *coords, gint radius, gint steps, gboolean circular) { + GList *ptr = coords; + if (ptr && g_list_length(ptr) > 2) { + gfloat prev_len, next_len; + const gint width = ((GromitStrokeCoordinate *)ptr->data)->width; + for (;;) { + gboolean is_last = (ptr->next == NULL); + GList *next_pt = is_last ? coords->next : ptr->next; + if (circular || !is_last) { + prev_len = next_len; + next_len = coord_distance(ptr, next_pt); + if (ptr != coords && next_len > 2 * radius && prev_len > 2 * radius) { + const gfloat rot = + find_coord_vec_rotation(ptr->prev, ptr, ptr, next_pt); + const gfloat beta = rot / steps; + const gfloat a = 2 * radius * tan((M_PI - rot) / 2) * sin(rot / (2 * steps)); + + xy vec = xy_vec_from_coords(ptr->prev, ptr); + // move back point by radius + xy point = get_xy_from_coord(ptr); + gfloat k = radius / xy_length(&vec); + point.x -= (vec.x * k); + point.y -= (vec.y * k); + set_coord_from_xy(&point, ptr); + + // initial step with length a + k *= (a / radius); + vec.x *= k; + vec.y *= k; + trans2D m; + rotate2D(beta / 2.0, &m); + + vec = apply2D_xy(&vec, &m); + rotate2D(-beta, &m); + + for (gint i = 0; i < steps; i++) { + vec = apply2D_xy(&vec, &m); + point = xy_add(&point, &vec); + GromitStrokeCoordinate *new_coord = g_malloc(sizeof(GromitStrokeCoordinate)); + GList *tmp = g_list_insert_before(coords, ptr->next, new_coord); + assert(tmp == coords); + new_coord->width = width; + ptr = ptr->next; + set_coord_from_xy(&point, ptr); + } + if (is_last && circular) { + set_coord_from_xy(&point, coords); + } + } + } + if (is_last) + break; + ptr = ptr->next; + } + } +} + +/* + * join ends of coordinates if their distance does not exceed max_distance + * and if the initial segments are long enough + */ +gboolean snap_ends(GList *coords, gint max_distance) { + if (g_list_length(coords) < 3) return FALSE; + GList *last = g_list_last(coords); + gfloat start_end_dist = coord_distance(coords, last); + gfloat start_seg_len = coord_distance(coords, coords->next); + gfloat end_seg_len = coord_distance(last, last->prev); + + if (start_end_dist <= max_distance && + start_seg_len > 0.7 * start_end_dist && + end_seg_len > 0.7 * start_end_dist) { + xy p0 = get_xy_from_coord(coords); + xy p1 = get_xy_from_coord(last); + p0.x = 0.5 * (p0.x + p1.x); + p0.y = 0.5 * (p0.y + p1.y); + set_coord_from_xy(&p0, coords); + set_coord_from_xy(&p0, last); + return TRUE; + } + return FALSE; +} + +// ----------------- douglas-peucker point reduction ----------------- + +/* + * push a list range (first, last) to the front of a list + */ +static GList *push_list_range(GList *list, GList *first, GList *last) { + ListRange *new_element = g_malloc(sizeof(ListRange)); + new_element->first = first; + new_element->last = last; + return g_list_prepend(list, new_element); +} + +/* + * pop a list range (first, last) from the front of a list + */ +static GList *pop_list_range(GList *list, GList **first, GList **last) { + g_assert(list != NULL); + *first = ((ListRange *)list->data)->first; + *last = ((ListRange *)list->data)->last; + GList *list_new = g_list_remove_link(list, list); + g_free(list->data); + g_list_free_1(list); + return list_new; +} + +/* + * perform Douglas-Peucker smoothing of 'coords' with distance + * threshold 'epsilon'. Based on + * https://namekdev.net/2014/06/iterative-version-of-ramer-douglas-peucker-line-simplification-algorithm/ + */ +void douglas_peucker(GList *coords, gfloat epsilon) { + GList *first = coords; + GList *last = g_list_last(coords); + + if (first != last) { + GList *ranges = push_list_range(NULL, first, last); + while (ranges != NULL) { + ranges = pop_list_range(ranges, &first, &last); + { + int i = 1; + GList *tmp = first; + while (tmp != last) { + i++; + tmp = tmp->next; + } + } + + gfloat dmax = 0.0; + GList *max_element = first; + GList *ptr = first->next; + if (ptr != last) { + // d-p fails when start and end pts near-identical -> use dist from start instead + gboolean too_close = coord_distance(first, last) < 10; + while (ptr && ptr != last) { + gfloat d; + if (too_close) { + d = coord_distance(ptr, first); + } else { + d = distance_from_line(ptr, first, last); + } + if (d > dmax) { + dmax = d; + max_element = ptr; + } + ptr = ptr->next; + } + if (dmax > epsilon) { + ranges = push_list_range(ranges, first, max_element); + ranges = push_list_range(ranges, max_element, last); + } else { + first->next->prev = NULL; + last->prev->next = NULL; + g_list_free_full(first->next, g_free); + first->next = last; + last->prev = first; + } + } + } + } +} + +// ------------------- for catmull_rom_smoothing -------------------- + +/* + * weighted mean of members of GList of 'GromitStrokeCoordinate's + */ +static void cr_f_mul_grxy(gfloat f, GList *p1, GList *p2, xy *xy) { + xy->x = f * ((GromitStrokeCoordinate *)p1->data)->x + + (1.0 - f) * ((GromitStrokeCoordinate *)p2->data)->x; + xy->y = f * ((GromitStrokeCoordinate *)p1->data)->y + + (1.0 - f) * ((GromitStrokeCoordinate *)p2->data)->y; +} + +/* + * weighted mean of members of (x,y) coordinates stored in xy + */ +static void cr_f_mul_xy(gfloat f, xy *p1, xy *p2, xy *xy) { + xy->x = f * p1->x + (1.0 - f) * p2->x; + xy->y = f * p1->y + (1.0 - f) * p2->y; +} +static gfloat catmull_rom_tj(gfloat ti, + GromitStrokeCoordinate *pi, + GromitStrokeCoordinate *pj) { + gfloat dx = pj->x - pi->x; + gfloat dy = pj->y - pi->y; + return ti + sqrt(sqrt(dx * dx + dy * dy)); +} + +/* + * centripetal Catmull-Rom interpolation with 'steps' steps between + * coordinates. Based on Python implementation at + * https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline + */ +GList *catmull_rom(GList *coords, gint steps, gboolean circular) { + gint segments = g_list_length(coords); + + if (segments < 3) // at least 4 points needed + return coords; + + GList *result = NULL; // interpolated coordinated + GList *srcptr = coords; + gint width = ((GromitStrokeCoordinate *)coords->data)->width; + + GList *p0 = NULL; // first segment: p0 = NULL + GList *p1 = srcptr; + GList *p2 = p1->next; + GList *p3 = p2->next; // last segment: p3 = NULL + + for (;;) { + xy a1, a2, a3, b1, b2, pt; + gfloat t0 = 0.0; + gfloat t1 = 0.0; + if (p0 == NULL) { + if (circular) { + t1 = catmull_rom_tj(t0, g_list_last(coords)->prev->data, p1->data); + } else { + t1 = 0.0; + } + } else { + t1 = catmull_rom_tj(t0, p0->data, p1->data); + } + gfloat t2 = catmull_rom_tj(t1, p1->data, p2->data); + gfloat t3 = t2; + if (p3 == NULL) { + if (circular) { + t3 = catmull_rom_tj(t2, p2->data, coords->next->data); + } else { + t3 = t2; + } + } else { + t3 = catmull_rom_tj(t2, p2->data, p3->data); + } + + gfloat k = (t2 - t1) / steps; + for (gint i = 0; i <= steps; i++) { + gfloat t = t1 + k * i; + + if (p0 == NULL) { + if (circular) { + cr_f_mul_grxy((t1 - t) / (t1 - t0), g_list_last(coords)->prev, p1, &a1); + } else { + a1 = get_xy_from_coord(p1); + } + } else { + cr_f_mul_grxy((t1 - t) / (t1 - t0), p0, p1, &a1); + } + cr_f_mul_grxy((t2 - t) / (t2 - t1), p1, p2, &a2); + if (p3 == NULL) { + if (circular) { + cr_f_mul_grxy((t3 - t) / (t3 - t2), p2, coords->next, &a3); + } else { + a3 = get_xy_from_coord(p2); + } + } else { + cr_f_mul_grxy((t3 - t) / (t3 - t2), p2, p3, &a3); + } + + cr_f_mul_xy((t2 - t) / (t2 - t0), &a1, &a2, &b1); + cr_f_mul_xy((t3 - t) / (t3 - t1), &a2, &a3, &b2); + cr_f_mul_xy((t2 - t) / (t2 - t1), &b1, &b2, &pt); + + GromitStrokeCoordinate *coord = g_malloc(sizeof(GromitStrokeCoordinate)); + coord->width = width; + coord->x = pt.x + 0.5; + coord->y = pt.y + 0.5; + + result = g_list_append(result, coord); + } + p0 = p1; + p1 = p1->next; + p2 = p2->next; + if (p3 == NULL) + break; + p3 = p3->next; + } + g_list_free_full(coords, g_free); + return result; +} diff --git a/src/coordlist_ops.h b/src/coordlist_ops.h new file mode 100644 index 0000000..27defd5 --- /dev/null +++ b/src/coordlist_ops.h @@ -0,0 +1,37 @@ +/* + * Gromit-MPX -- a program for painting on the screen + * + * Gromit Copyright (C) 2000 Simon Budig + * + * Gromit-MPX Copyright (C) 2009,2010 Christian Beier + * Copytight (C) 2024 Pascal Niklaus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + */ + +#ifndef SMOOTH_H +#define SMOOTH_H + +#include "main.h" + +gboolean snap_ends(GList *coords, gint max_distance); +void orthogonalize(GList *coords, gint max_angular_deviation, gint min_ortho_len); +void add_points(GList *coords, gfloat max_distance); +void round_corners(GList *coords, gint radius, gint steps, gboolean circular); +void douglas_peucker(GList *coords, gfloat epsilon); +GList *catmull_rom(GList *coords, gint steps, gboolean circular); + +#endif diff --git a/src/drawing.c b/src/drawing.c index adfaf0a..5eefb00 100644 --- a/src/drawing.c +++ b/src/drawing.c @@ -3,14 +3,6 @@ #include "drawing.h" #include "main.h" -typedef struct -{ - gint x; - gint y; - gint width; -} GromitStrokeCoordinate; - - void draw_line (GromitData *data, GdkDevice *dev, gint x1, gint y1, diff --git a/src/drawing.h b/src/drawing.h index d1954a6..794c2f7 100644 --- a/src/drawing.h +++ b/src/drawing.h @@ -8,6 +8,14 @@ #include "main.h" +typedef struct +{ + gint x; + gint y; + gint width; +} GromitStrokeCoordinate; + + void draw_line (GromitData *data, GdkDevice *dev, gint x1, gint y1, gint x2, gint y2); void draw_arrow (GromitData *data, GdkDevice *dev, gint x1, gint y1, gint width, gfloat direction); diff --git a/src/main.c b/src/main.c index bd84215..10c8ae2 100644 --- a/src/main.c +++ b/src/main.c @@ -37,15 +37,21 @@ #include "paint_cursor.xpm" #include "erase_cursor.xpm" +#include "coordlist_ops.h" -GromitPaintContext *paint_context_new (GromitData *data, +GromitPaintContext *paint_context_new (GromitData *data, GromitPaintType type, - GdkRGBA *paint_color, + GdkRGBA *paint_color, guint width, guint arrowsize, GromitArrowType arrowtype, + guint simpilfy, + guint radius, + guint maxangle, + guint minlen, + guint snapdist, guint minwidth, guint maxwidth) { @@ -60,6 +66,11 @@ GromitPaintContext *paint_context_new (GromitData *data, context->minwidth = minwidth; context->maxwidth = maxwidth; context->paint_color = paint_color; + context->radius = radius; + context->maxangle = maxangle; + context->simplify = simpilfy; + context->minlen = minlen; + context->snapdist = snapdist; context->paint_ctx = cairo_create (data->backbuffer); @@ -69,7 +80,7 @@ GromitPaintContext *paint_context_new (GromitData *data, cairo_set_line_width(context->paint_ctx, width); cairo_set_line_cap(context->paint_ctx, CAIRO_LINE_CAP_ROUND); cairo_set_line_join(context->paint_ctx, CAIRO_LINE_JOIN_ROUND); - + if (type == GROMIT_ERASER) cairo_set_operator(context->paint_ctx, CAIRO_OPERATOR_CLEAR); else if (type == GROMIT_RECOLOR) @@ -81,24 +92,28 @@ GromitPaintContext *paint_context_new (GromitData *data, } -void paint_context_print (gchar *name, +void paint_context_print (gchar *name, GromitPaintContext *context) { g_printerr ("Tool name: \"%-20s\": ", name); switch (context->type) { case GROMIT_PEN: - g_printerr ("Pen, "); break; + g_printerr ("Pen, "); break; case GROMIT_LINE: - g_printerr ("Line, "); break; + g_printerr ("Line, "); break; case GROMIT_RECT: - g_printerr ("Rect, "); break; + g_printerr ("Rect, "); break; + case GROMIT_SMOOTH: + g_printerr ("Smooth, "); break; + case GROMIT_ORTHOGONAL: + g_printerr ("Orthogonal, "); break; case GROMIT_ERASER: - g_printerr ("Eraser, "); break; + g_printerr ("Eraser, "); break; case GROMIT_RECOLOR: - g_printerr ("Recolor, "); break; + g_printerr ("Recolor, "); break; default: - g_printerr ("UNKNOWN, "); break; + g_printerr ("UNKNOWN, "); break; } g_printerr ("width: %u, ", context->width); @@ -119,9 +134,18 @@ void paint_context_print (gchar *name, break; } } - gchar *col_name = gdk_rgba_to_string(context->paint_color); - g_printerr ("color: %s\n", col_name); - g_free(col_name); + if (context->type == GROMIT_SMOOTH || context->type == GROMIT_ORTHOGONAL) + { + g_printerr(" simplify: %u, ", context->simplify); + if (context->snapdist > 0) + g_printerr(" snap: %u, ", context->snapdist); + } + if (context->type == GROMIT_ORTHOGONAL) + { + g_printerr(" radius: %u, minlen: %u, maxangle: %u ", + context->radius, context->minlen, context->maxangle); + } + g_printerr ("color: %s\n", gdk_rgba_to_string(context->paint_color)); } @@ -139,9 +163,9 @@ void hide_window (GromitData *data) /* save grab state of each device */ GHashTableIter it; gpointer value; - GromitDeviceData* devdata; + GromitDeviceData* devdata; g_hash_table_iter_init (&it, data->devdatatable); - while (g_hash_table_iter_next (&it, NULL, &value)) + while (g_hash_table_iter_next (&it, NULL, &value)) { devdata = value; devdata->was_grabbed = devdata->is_grabbed; @@ -149,7 +173,7 @@ void hide_window (GromitData *data) data->hidden = 1; release_grab (data, NULL); /* release all */ gtk_widget_hide (data->win); - + if(data->debug) g_printerr ("DEBUG: Hiding window.\n"); } @@ -166,9 +190,9 @@ void show_window (GromitData *data) /* restore grab state of each device */ GHashTableIter it; gpointer value; - GromitDeviceData* devdata; + GromitDeviceData* devdata; g_hash_table_iter_init (&it, data->devdatatable); - while (g_hash_table_iter_next (&it, NULL, &value)) + while (g_hash_table_iter_next (&it, NULL, &value)) { devdata = value; if(devdata->was_grabbed) @@ -201,8 +225,8 @@ void clear_screen (GromitData *data) cairo_destroy(cr); GdkRectangle rect = {0, 0, data->width, data->height}; - gdk_window_invalidate_rect(gtk_widget_get_window(data->win), &rect, 0); - + gdk_window_invalidate_rect(gtk_widget_get_window(data->win), &rect, 0); + if(!data->composited) { cairo_region_t* r = gdk_cairo_region_create_from_surface(data->backbuffer); @@ -255,8 +279,8 @@ gint reshape (gpointer user_data) } -void select_tool (GromitData *data, - GdkDevice *device, +void select_tool (GromitData *data, + GdkDevice *device, GdkDevice *slave_device, guint state) { @@ -270,7 +294,7 @@ void select_tool (GromitData *data, /* get the data for this device */ GromitDeviceData *devdata = g_hash_table_lookup(data->devdatatable, device); - + if (device) { slave_len = strlen (gdk_device_get_name(slave_device)); @@ -279,8 +303,8 @@ void select_tool (GromitData *data, name = (guchar*) g_strndup (gdk_device_get_name(device), len + 3); default_len = strlen(DEFAULT_DEVICE_NAME); default_name = (guchar*) g_strndup (DEFAULT_DEVICE_NAME, default_len + 3); - - + + /* Extract Button/Modifiers from state (see GdkModifierType) */ req_buttons = (state >> 8) & 31; @@ -354,7 +378,7 @@ void select_tool (GromitData *data, devdata->cur_context = context; success = 1; } - + } while (j<=3 && req_modifier >= (1u << j)); } @@ -379,7 +403,7 @@ void select_tool (GromitData *data, GdkCursor *cursor; if(devdata->cur_context && devdata->cur_context->type == GROMIT_ERASER) - cursor = data->erase_cursor; + cursor = data->erase_cursor; else cursor = data->paint_cursor; @@ -621,7 +645,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) gdk_rgba_parse(data->red, "#FF0000"); - /* + /* CURSORS */ GdkPixbuf* paint_cursor_pixbuf = gdk_pixbuf_new_from_xpm_data(paint_cursor_xpm); @@ -644,7 +668,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) */ cairo_surface_destroy(data->backbuffer); data->backbuffer = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, data->width, data->height); - + // original state for LINE and RECT tool cairo_surface_destroy(data->aux_backbuffer); data->aux_backbuffer = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, data->width, data->height); @@ -682,15 +706,15 @@ void setup_main_app (GromitData *data, int argc, char ** argv) G_CALLBACK (on_device_removed), data); g_signal_connect (data->win, "motion_notify_event", G_CALLBACK (on_motion), data); - g_signal_connect (data->win, "button_press_event", + g_signal_connect (data->win, "button_press_event", G_CALLBACK (on_buttonpress), data); g_signal_connect (data->win, "button_release_event", G_CALLBACK (on_buttonrelease), data); /* disconnect previously defined selection handlers */ - g_signal_handlers_disconnect_by_func (data->win, + g_signal_handlers_disconnect_by_func (data->win, G_CALLBACK (on_clientapp_selection_get), data); - g_signal_handlers_disconnect_by_func (data->win, + g_signal_handlers_disconnect_by_func (data->win, G_CALLBACK (on_clientapp_selection_received), data); /* and re-connect them to mainapp functions */ @@ -706,7 +730,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) gtk_widget_shape_combine_region(data->win, r); cairo_region_destroy(r); } - + /* reset settings from client setup */ gtk_selection_remove_all (data->win); @@ -724,6 +748,9 @@ void setup_main_app (GromitData *data, int argc, char ** argv) gtk_selection_add_target (data->win, GA_CONTROL, GA_REDO, 9); gtk_selection_add_target (data->win, GA_CONTROL, GA_LINE, 10); + + + /* * Parse Config file */ @@ -744,8 +771,8 @@ void setup_main_app (GromitData *data, int argc, char ** argv) // might have been in key file gtk_widget_set_opacity(data->win, data->opacity); - /* - FIND HOTKEY KEYCODE + /* + FIND HOTKEY KEYCODE */ if (data->hot_keyval) { @@ -773,7 +800,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) } /* - FIND UNDOKEY KEYCODE + FIND UNDOKEY KEYCODE */ if (data->undo_keyval) { @@ -801,7 +828,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) } - /* + /* INPUT DEVICES */ data->devdatatable = g_hash_table_new(NULL, NULL); @@ -813,15 +840,15 @@ void setup_main_app (GromitData *data, int argc, char ** argv) hide_window (data); data->timeout_id = g_timeout_add (20, reshape, data); - + data->modified = 0; data->default_pen = - paint_context_new (data, GROMIT_PEN, data->red, 7, - 0, GROMIT_ARROW_END, 1, G_MAXUINT); + paint_context_new (data, GROMIT_PEN, data->red, 7, 0, GROMIT_ARROW_END, + 5, 10, 15, 25, 0, 1, G_MAXUINT); data->default_eraser = - paint_context_new (data, GROMIT_ERASER, data->red, 75, - 0, GROMIT_ARROW_END, 1, G_MAXUINT); + paint_context_new (data, GROMIT_ERASER, data->red, 75, 0, GROMIT_ARROW_END, + 5, 10, 15, 25, 0, 1, G_MAXUINT); gdk_event_handler_set ((GdkEventFunc) main_do_event, data, NULL); gtk_key_snooper_install (snoop_key_press, data); @@ -829,7 +856,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) if (activate) acquire_grab (data, NULL); /* grab all */ - /* + /* TRAY ICON */ data->trayicon = app_indicator_new (PACKAGE_NAME, @@ -1076,9 +1103,9 @@ int main_client (int argc, char **argv, GromitData *data) { if (argc - (i+1) == 6) /* this command must have exactly 6 params */ { - + // check if provided values are valid coords on the screen - if (atoi(argv[i+1]) < 0 || atoi(argv[i+1]) > (int)data->width || + if (atoi(argv[i+1]) < 0 || atoi(argv[i+1]) > (int)data->width || atoi(argv[i+2]) < 0 || atoi(argv[i+2]) > (int)data->height || atoi(argv[i+3]) < 0 || atoi(argv[i+3]) > (int)data->width || atoi(argv[i+4]) < 0 || atoi(argv[i+4]) > (int)data->height) @@ -1091,15 +1118,15 @@ int main_client (int argc, char **argv, GromitData *data) g_printerr ("Thickness must be atleast 1\n"); wrong_arg = TRUE; } - else + else { data->clientdata = g_strjoin(" ", argv[i+1], argv[i+2], argv[i+3], argv[i+4], argv[i+5], argv[i+6], NULL); } - + action = GA_LINE; i += 6; } - else + else { g_printerr ("-l requires 6 parameters: startX, startY, endX, endY, color, thickness\n"); wrong_arg = TRUE; diff --git a/src/main.h b/src/main.h index 527ca55..8ebedd1 100644 --- a/src/main.h +++ b/src/main.h @@ -66,6 +66,8 @@ typedef enum GROMIT_PEN, GROMIT_LINE, GROMIT_RECT, + GROMIT_SMOOTH, + GROMIT_ORTHOGONAL, GROMIT_ERASER, GROMIT_RECOLOR } GromitPaintType; @@ -85,6 +87,11 @@ typedef struct GromitArrowType arrow_type; guint minwidth; guint maxwidth; + guint radius; + guint minlen; + guint maxangle; + guint simplify; + guint snapdist; GdkRGBA *paint_color; cairo_t *paint_ctx; gdouble pressure; @@ -188,6 +195,7 @@ void clear_screen (GromitData *data); GromitPaintContext *paint_context_new (GromitData *data, GromitPaintType type, GdkRGBA *fg_color, guint width, guint arrowsize, GromitArrowType arrowtype, + guint simpilfy, guint radius, guint maxangle, guint minlen, guint snapdist, guint minwidth, guint maxwidth); void paint_context_free (GromitPaintContext *context);