/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * pikasymmetry-tiling.c * Copyright (C) 2015 Jehan * * 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 3 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, see . */ #include "config.h" #include #include #include #include #include "libpikabase/pikabase.h" #include "libpikaconfig/pikaconfig.h" #include "core-types.h" #include "pika.h" #include "pikadrawable.h" #include "pikaimage.h" #include "pikaitem.h" #include "pikasymmetry-tiling.h" #include "pika-intl.h" /* Using same epsilon as in GLIB. */ #define G_DOUBLE_EPSILON (1e-90) enum { PROP_0, PROP_INTERVAL_X, PROP_INTERVAL_Y, PROP_SHIFT, PROP_MAX_X, PROP_MAX_Y }; /* Local function prototypes */ static void pika_tiling_constructed (GObject *object); static void pika_tiling_finalize (GObject *object); static void pika_tiling_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); static void pika_tiling_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); static void pika_tiling_update_strokes (PikaSymmetry *tiling, PikaDrawable *drawable, PikaCoords *origin); static void pika_tiling_image_size_changed_cb (PikaImage *image, gint previous_origin_x, gint previous_origin_y, gint previous_width, gint previous_height, PikaSymmetry *sym); G_DEFINE_TYPE (PikaTiling, pika_tiling, PIKA_TYPE_SYMMETRY) #define parent_class pika_tiling_parent_class static void pika_tiling_class_init (PikaTilingClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); PikaSymmetryClass *symmetry_class = PIKA_SYMMETRY_CLASS (klass); GParamSpec *pspec; object_class->constructed = pika_tiling_constructed; object_class->finalize = pika_tiling_finalize; object_class->set_property = pika_tiling_set_property; object_class->get_property = pika_tiling_get_property; symmetry_class->label = _("Tiling"); symmetry_class->update_strokes = pika_tiling_update_strokes; PIKA_CONFIG_PROP_DOUBLE (object_class, PROP_INTERVAL_X, "interval-x", _("Interval X"), _("Interval on the X axis (pixels)"), 0.0, G_MAXDOUBLE, 0.0, PIKA_PARAM_STATIC_STRINGS | PIKA_SYMMETRY_PARAM_GUI); pspec = g_object_class_find_property (object_class, "interval-x"); gegl_param_spec_set_property_key (pspec, "unit", "pixel-distance"); gegl_param_spec_set_property_key (pspec, "axis", "x"); PIKA_CONFIG_PROP_DOUBLE (object_class, PROP_INTERVAL_Y, "interval-y", _("Interval Y"), _("Interval on the Y axis (pixels)"), 0.0, G_MAXDOUBLE, 0.0, PIKA_PARAM_STATIC_STRINGS | PIKA_SYMMETRY_PARAM_GUI); pspec = g_object_class_find_property (object_class, "interval-y"); gegl_param_spec_set_property_key (pspec, "unit", "pixel-distance"); gegl_param_spec_set_property_key (pspec, "axis", "y"); PIKA_CONFIG_PROP_DOUBLE (object_class, PROP_SHIFT, "shift", _("Shift"), _("X-shift between lines (pixels)"), 0.0, G_MAXDOUBLE, 0.0, PIKA_PARAM_STATIC_STRINGS | PIKA_SYMMETRY_PARAM_GUI); pspec = g_object_class_find_property (object_class, "shift"); gegl_param_spec_set_property_key (pspec, "unit", "pixel-distance"); gegl_param_spec_set_property_key (pspec, "axis", "x"); PIKA_CONFIG_PROP_INT (object_class, PROP_MAX_X, "max-x", _("Max strokes X"), _("Maximum number of strokes on the X axis"), 0, 100, 0, PIKA_PARAM_STATIC_STRINGS | PIKA_SYMMETRY_PARAM_GUI); PIKA_CONFIG_PROP_INT (object_class, PROP_MAX_Y, "max-y", _("Max strokes Y"), _("Maximum number of strokes on the Y axis"), 0, 100, 0, PIKA_PARAM_STATIC_STRINGS | PIKA_SYMMETRY_PARAM_GUI); } static void pika_tiling_init (PikaTiling *tiling) { } static void pika_tiling_constructed (GObject *object) { PikaSymmetry *sym = PIKA_SYMMETRY (object); PikaTiling *tiling = PIKA_TILING (object); g_signal_connect_object (sym->image, "size-changed-detailed", G_CALLBACK (pika_tiling_image_size_changed_cb), sym, 0); /* Set reasonable defaults. */ tiling->interval_x = pika_image_get_width (sym->image) / 2; tiling->interval_y = pika_image_get_height (sym->image) / 2; } static void pika_tiling_finalize (GObject *object) { G_OBJECT_CLASS (parent_class)->finalize (object); } static void pika_tiling_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { PikaTiling *tiling = PIKA_TILING (object); PikaSymmetry *sym = PIKA_SYMMETRY (tiling); switch (property_id) { case PROP_INTERVAL_X: if (sym->image) { gdouble new_x = g_value_get_double (value); if (new_x < pika_image_get_width (sym->image)) { tiling->interval_x = new_x; if (tiling->interval_x <= tiling->shift + G_DOUBLE_EPSILON) { GValue val = G_VALUE_INIT; g_value_init (&val, G_TYPE_DOUBLE); g_value_set_double (&val, 0.0); g_object_set_property (G_OBJECT (object), "shift", &val); } if (sym->drawable) pika_tiling_update_strokes (sym, sym->drawable, sym->origin); } } break; case PROP_INTERVAL_Y: { gdouble new_y = g_value_get_double (value); if (new_y < pika_image_get_height (sym->image)) { tiling->interval_y = new_y; if (tiling->interval_y <= G_DOUBLE_EPSILON) { GValue val = G_VALUE_INIT; g_value_init (&val, G_TYPE_DOUBLE); g_value_set_double (&val, 0.0); g_object_set_property (G_OBJECT (object), "shift", &val); } if (sym->drawable) pika_tiling_update_strokes (sym, sym->drawable, sym->origin); } } break; case PROP_SHIFT: { gdouble new_shift = g_value_get_double (value); if (new_shift == 0.0 || (tiling->interval_y != 0.0 && new_shift < tiling->interval_x)) { tiling->shift = new_shift; if (sym->drawable) pika_tiling_update_strokes (sym, sym->drawable, sym->origin); } } break; case PROP_MAX_X: tiling->max_x = g_value_get_int (value); if (sym->drawable) pika_tiling_update_strokes (sym, sym->drawable, sym->origin); break; case PROP_MAX_Y: tiling->max_y = g_value_get_int (value); if (sym->drawable) pika_tiling_update_strokes (sym, sym->drawable, sym->origin); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void pika_tiling_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { PikaTiling *tiling = PIKA_TILING (object); switch (property_id) { case PROP_INTERVAL_X: g_value_set_double (value, tiling->interval_x); break; case PROP_INTERVAL_Y: g_value_set_double (value, tiling->interval_y); break; case PROP_SHIFT: g_value_set_double (value, tiling->shift); break; case PROP_MAX_X: g_value_set_int (value, tiling->max_x); break; case PROP_MAX_Y: g_value_set_int (value, tiling->max_y); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void pika_tiling_update_strokes (PikaSymmetry *sym, PikaDrawable *drawable, PikaCoords *origin) { PikaTiling *tiling = PIKA_TILING (sym); GList *strokes = NULL; PikaCoords *coords; gdouble width; gdouble height; gdouble startx = origin->x; gdouble starty = origin->y; gdouble x; gdouble y; gint x_count; gint y_count; g_list_free_full (sym->strokes, g_free); sym->strokes = NULL; width = pika_item_get_width (PIKA_ITEM (drawable)); height = pika_item_get_height (PIKA_ITEM (drawable)); if (sym->stateful) { /* While I can compute exactly the right number of strokes to * paint on-canvas for stateless tools, stateful tools need to * always have the same number and order of strokes. For this * reason, I compute strokes to fill 2 times the width and height. * This makes the symmetry less efficient with stateful tools, but * also weird behavior may happen if you decide to paint out of * canvas and expect tiling to work in-canvas since it won't * actually be infinite (as no new strokes can be added while * painting since we are stateful). */ gint i, j; if (tiling->interval_x < 1.0) { x_count = 1; } else if (tiling->max_x == 0) { x_count = (gint) ceil (width / tiling->interval_x); startx -= tiling->interval_x * (gdouble) x_count; x_count = 2 * x_count + 1; } else { x_count = tiling->max_x; } if (tiling->interval_y < 1.0) { y_count = 1; } else if (tiling->max_y == 0) { y_count = (gint) ceil (height / tiling->interval_y); starty -= tiling->interval_y * (gdouble) y_count; y_count = 2 * y_count + 1; } else { y_count = tiling->max_y; } for (i = 0, x = startx; i < x_count; i++) { for (j = 0, y = starty; j < y_count; j++) { coords = g_memdup2 (origin, sizeof (PikaCoords)); coords->x = x; coords->y = y; strokes = g_list_prepend (strokes, coords); y += tiling->interval_y; } x += tiling->interval_x; } } else { if (origin->x > 0 && tiling->max_x == 0 && tiling->interval_x >= 1.0) startx = fmod (origin->x, tiling->interval_x) - tiling->interval_x; if (origin->y > 0 && tiling->max_y == 0 && tiling->interval_y >= 1.0) { starty = fmod (origin->y, tiling->interval_y) - tiling->interval_y; if (tiling->shift > 0.0) startx -= tiling->shift * floor (origin->y / tiling->interval_y + 1); } for (y_count = 0, y = starty; y < height + tiling->interval_y; y_count++, y += tiling->interval_y) { if (tiling->max_y && y_count >= tiling->max_y) break; for (x_count = 0, x = startx; x < width + tiling->interval_x; x_count++, x += tiling->interval_x) { if (tiling->max_x && x_count >= tiling->max_x) break; coords = g_memdup2 (origin, sizeof (PikaCoords)); coords->x = x; coords->y = y; strokes = g_list_prepend (strokes, coords); if (tiling->interval_x < 1.0) break; } if (tiling->max_x || startx + tiling->shift <= 0.0) startx = startx + tiling->shift; else startx = startx - tiling->interval_x + tiling->shift; if (tiling->interval_y < 1.0) break; } } sym->strokes = strokes; g_signal_emit_by_name (sym, "strokes-updated", sym->image); } static void pika_tiling_image_size_changed_cb (PikaImage *image, gint previous_origin_x, gint previous_origin_y, gint previous_width, gint previous_height, PikaSymmetry *sym) { if (previous_width != pika_image_get_width (image) || previous_height != pika_image_get_height (image)) { g_signal_emit_by_name (sym, "gui-param-changed", sym->image); } }