From 4d58439fafa4e7affba16ff7743fdb9b668d41ff Mon Sep 17 00:00:00 2001 From: pascal Date: Thu, 14 Mar 2024 11:25:11 +0100 Subject: [PATCH] ortho and smooth rebased on current master --- src/callbacks.c | 33 +- src/config.c | 4 +- src/drawing.c | 8 - src/drawing.h | 8 + src/main.c | 83 ++--- src/main.h | 2 +- src/smooth.c | 766 ++++++++++++++++++++++++++++++++++++++++++++++ src/smooth.h | 35 +++ src/smooth_priv.h | 133 ++++++++ 9 files changed, 1020 insertions(+), 52 deletions(-) create mode 100644 src/smooth_priv.h diff --git a/src/callbacks.c b/src/callbacks.c index 2737f7b..742c263 100644 --- a/src/callbacks.c +++ b/src/callbacks.c @@ -31,7 +31,7 @@ #include "config.h" #include "drawing.h" #include "build-config.h" - +#include "smooth.h" gboolean on_expose (GtkWidget *widget, cairo_t* cr, @@ -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) { 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) { @@ -460,6 +461,34 @@ gboolean on_buttonrelease (GtkWidget *win, GromitPaintType type = devdata->cur_context->type; + if (type == GROMIT_SMOOTH) + { + if (0) { + // smooth pen + douglas_peucker(devdata->coordlist, 10); // was 15 + add_points(devdata->coordlist, 200); + devdata->coordlist = catmull_rom(devdata->coordlist, 5); + } else { + // smart rect pen + douglas_peucker(devdata->coordlist, 15); // was 15 + orthogonalize(devdata->coordlist, 20, 40); + round_corners(devdata->coordlist, 20, 6); + } + + 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->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) { GromitArrowType atype = devdata->cur_context->arrow_type; diff --git a/src/config.c b/src/config.c index 87ea016..61bc89f 100644 --- a/src/config.c +++ b/src/config.c @@ -171,6 +171,7 @@ gboolean parse_config (GromitData *data) 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, "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); @@ -340,6 +341,7 @@ gboolean parse_config (GromitData *data) goto cleanup; } arrowsize = scanner->value.v_float; + arrowtype = GROMIT_ARROW_END; } else if ((intptr_t) scanner->value.v_symbol == 4) { @@ -567,7 +569,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/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 b54ccb7..6fb1c97 100644 --- a/src/main.c +++ b/src/main.c @@ -32,12 +32,13 @@ #include "paint_cursor.xpm" #include "erase_cursor.xpm" +#include "smooth.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, @@ -56,7 +57,7 @@ GromitPaintContext *paint_context_new (GromitData *data, context->maxwidth = maxwidth; context->paint_color = paint_color; - + context->paint_ctx = cairo_create (data->backbuffer); gdk_cairo_set_source_rgba(context->paint_ctx, paint_color); @@ -65,7 +66,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 @@ -78,7 +79,7 @@ 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); @@ -90,6 +91,8 @@ void paint_context_print (gchar *name, g_printerr ("Line, "); break; case GROMIT_RECT: g_printerr ("Rect, "); break; + case GROMIT_SMOOTH: + g_printerr ("Smooth, "); break; case GROMIT_ERASER: g_printerr ("Eraser, "); break; case GROMIT_RECOLOR: @@ -134,9 +137,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; @@ -144,7 +147,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"); } @@ -161,9 +164,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) @@ -196,8 +199,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); @@ -250,8 +253,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) { @@ -265,7 +268,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)); @@ -274,8 +277,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; @@ -349,7 +352,7 @@ void select_tool (GromitData *data, devdata->cur_context = context; success = 1; } - + } while (j<=3 && req_modifier >= (1u << j)); } @@ -374,7 +377,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; @@ -461,7 +464,7 @@ void undo_drawing (GromitData *data) swap_surfaces(data->backbuffer, data->undobuffer[data->undo_head]); 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); data->modified = 1; @@ -483,9 +486,9 @@ void redo_drawing (GromitData *data) data->undo_head++; if(data->undo_head >= GROMIT_MAX_UNDO) data->undo_head -= GROMIT_MAX_UNDO; - + 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); data->modified = 1; @@ -575,7 +578,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); @@ -599,7 +602,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) /* SHAPE SURFACE*/ cairo_surface_destroy(data->backbuffer); data->backbuffer = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, data->width, data->height); - + /* UNDO STATE */ @@ -635,15 +638,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 */ @@ -659,7 +662,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); @@ -678,7 +681,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) gtk_selection_add_target (data->win, GA_CONTROL, GA_LINE, 10); - + /* * Parse Config file @@ -700,8 +703,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) { @@ -729,7 +732,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) } /* - FIND UNDOKEY KEYCODE + FIND UNDOKEY KEYCODE */ if (data->undo_keyval) { @@ -757,7 +760,7 @@ void setup_main_app (GromitData *data, int argc, char ** argv) } - /* + /* INPUT DEVICES */ data->devdatatable = g_hash_table_new(NULL, NULL); @@ -771,7 +774,7 @@ 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 = @@ -787,7 +790,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, @@ -1016,9 +1019,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) @@ -1031,15 +1034,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 8fea9ba..31dddc9 100644 --- a/src/main.h +++ b/src/main.h @@ -66,6 +66,7 @@ typedef enum GROMIT_PEN, GROMIT_LINE, GROMIT_RECT, + GROMIT_SMOOTH, GROMIT_ERASER, GROMIT_RECOLOR } GromitPaintType; @@ -156,7 +157,6 @@ typedef struct cairo_surface_t *undobuffer[GROMIT_MAX_UNDO]; gint undo_head, undo_depth, redo_depth; - gboolean show_intro_on_startup; } GromitData; diff --git a/src/smooth.c b/src/smooth.c index e69de29..f3d872d 100644 --- a/src/smooth.c +++ b/src/smooth.c @@ -0,0 +1,766 @@ +/* + * Gromit-MPX -- a program for painting on the screen + * + * Gromit Copyright (C) 2000 Simon Budig + * + * Gromit-MPX Copyright (C) 2009,2010 Christian Beier + * + * 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 "drawing.h" +#include "main.h" +#include "smooth.h" +#include "smooth_priv.h" + +// ----------------------------- helpers ----------------------------- + +inline 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 inline the call anyway. + +inline xy get_xy_from_coord(GList *ptr) { + xy result; + result.x = ((GromitStrokeCoordinate *)ptr->data)->x; + result.y = ((GromitStrokeCoordinate *)ptr->data)->y; + return result; +} + +inline 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; +} + +inline 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; +} + +inline xy xy_vec(xy *start, xy *end) { + xy result; + result.x = end->x - start->x; + result.y = end->y - start->y; + return result; +} + +inline xy xy_add(xy *point, xy *translation) { + xy result = *point; + result.x += translation->x; + result.y += translation->y; + return result; +} + +inline gfloat xy_length(xy *vec) { + return sqrtf(vec->x * vec->x + vec->y * vec->y); +} + +inline 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); +} + +/* + * find rotation that turns vector0 into direction of vector1. All + * coordinates are pointers into element of a GList of + * 'GromitStrokeCooordinate's + */ +inline 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. + */ +inline 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) + */ +inline 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 + */ +inline 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 -------------------- + +inline void unity2D(trans2D *m) { + memcpy(m->m, (gfloat[6]){1.0, 0.0, 0.0, 0.0, 1.0, 0.0}, sizeof(gfloat[6])); +} + +inline 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; +} + +inline void translate2D(gfloat dx, gfloat dy, trans2D *m) { + unity2D(m); + m->m_13 = dx; + m->m_23 = dy; +} + +inline 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 ? + */ +inline gboolean section_is_ortho(Section *ptr) { + return (ptr->direction % 90) == 0; +} + +/* + * is section vertical ? + */ +inline 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 ------------------------ + +#if 0 +void dump_section_list(GList *secs) { + gint i=0; + while (secs) { + Section *s = secs->data; + + g_printerr("%2d: dir=%4d\n xy:(%.1f,%.1f)--(%.1f,%.1f)\n", + i++, + s->direction, + s->xy_start.x, + s->xy_start.y, + s->xy_end.x, + s->xy_end.y); + secs =secs->next; + } +} +#endif + +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); + g_printerr("Adding %d points...\n", n_sections-1); + 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) { + GList *ptr = coords; + + gfloat prev_len, next_len; + if (ptr) { + const gint width = ((GromitStrokeCoordinate *)ptr->data)->width; + while (ptr->next) { + prev_len = next_len; + next_len = coord_distance(ptr, ptr->next); + if (ptr != coords && next_len > 2 * radius && prev_len > 2 * radius) { + const gfloat rot = + find_coord_vec_rotation(ptr->prev, ptr, ptr, ptr->next); + 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); + } + } + ptr = ptr->next; + } + } +} + +// ----------------- 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; +} + +/* + * distance of point 'p' from line a line defined by points + * 'line_start' and 'line_end' + */ +static gfloat distance_from_line(GromitStrokeCoordinate *p, + GromitStrokeCoordinate *line_start, + GromitStrokeCoordinate *line_end) { + gfloat a = (line_start->y - line_end->y) * p->x + + (line_end->x - line_start->x) * p->y + + (line_start->x * line_end->y - line_end->x * line_start->y); + gfloat l = sqrt(square(line_start->x - line_end->x) + + square(line_start->y - line_end->y)); + return fabs(a / l); +} + +/* + * 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) { + while (ptr && ptr != last) { + gfloat d = distance_from_line(ptr->data, first->data, last->data); + 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) { + 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) + 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) + 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) + 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) + 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/smooth.h b/src/smooth.h index e69de29..dafd38d 100644 --- a/src/smooth.h +++ b/src/smooth.h @@ -0,0 +1,35 @@ +/* + * Gromit-MPX -- a program for painting on the screen + * + * Gromit Copyright (C) 2000 Simon Budig + * + * Gromit-MPX Copyright (C) 2009,2010 Christian Beier + * + * 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" + +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); +void douglas_peucker(GList *coords, gfloat epsilon); +GList *catmull_rom(GList *coords, gint steps); + +#endif diff --git a/src/smooth_priv.h b/src/smooth_priv.h new file mode 100644 index 0000000..1c7578c --- /dev/null +++ b/src/smooth_priv.h @@ -0,0 +1,133 @@ +/* + * Gromit-MPX -- a program for painting on the screen + * + * Gromit Copyright (C) 2000 Simon Budig + * + * Gromit-MPX Copyright (C) 2009,2010 Christian Beier + * + * 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_PRIV_H +#define SMOOTH_PRIV_H + +#include +#include "drawing.h" + +// ----------------------------- helpers ----------------------------- + +extern inline float square(gfloat x); + +// ------------------ coordinate-related functions ------------------- + +typedef struct { + gfloat x; + gfloat y; +} xy; + +extern inline xy get_xy_from_coord(GList *ptr); +extern inline void set_coord_from_xy(xy *point, GList *ptr); +extern inline xy xy_vec_from_coords(GList *start, GList *end); +extern inline xy xy_vec(xy *start, xy *end); +extern inline xy xy_add(xy *point, xy *translation); +extern inline gfloat xy_length(xy *vec); +extern inline gfloat coord_distance(GList *p1, GList *p2); +extern inline gfloat find_coord_vec_rotation(GList *start0, GList *end0, + GList *start1, GList *end1); +extern inline gfloat find_xy_vec_rotation(xy *start0, xy *end0, + xy *start1, xy *end1); +extern inline gfloat direction_of_coord_vector(GList *p1, GList *p2); +extern inline 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; + +extern inline void unity2D(trans2D *m); +extern inline void rotate2D(gfloat angle, trans2D *m); +extern inline void translate2D(gfloat dx, gfloat dy, trans2D *m); +extern inline 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); +extern inline gboolean section_is_ortho(Section *ptr); +extern inline 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); +static gfloat distance_from_line(GromitStrokeCoordinate *p, + GromitStrokeCoordinate *line_start, + GromitStrokeCoordinate *line_end); + +// ----------------- 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); + +#endif