448 lines
14 KiB
C
448 lines
14 KiB
C
/* 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 <jehan@gimp.org>
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
#include <gegl.h>
|
|
#include <gdk-pixbuf/gdk-pixbuf.h>
|
|
|
|
#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);
|
|
}
|
|
}
|