[gimp] Issue #4326 - Add visual tab to spyrogimp plugin
- From: Ell <ell src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gimp] Issue #4326 - Add visual tab to spyrogimp plugin
- Date: Tue, 5 May 2020 10:30:16 +0000 (UTC)
commit 93602a3973120da248af06e3f5a4bb7bbc895db4
Author: Elad Shahar <dawn ever gmail com>
Date: Tue May 5 00:11:12 2020 +0300
Issue #4326 - Add visual tab to spyrogimp plugin
Add visual tab to spyrogimp plugin for a more intuitive, visual
way of specifying the spyrograph pattern.
In addition, fix using the selection as the fixed gear, and add
option to save the pattern to a path.
plug-ins/python/spyro-plus.py | 730 +++++++++++++++++++++++++++++++++---------
1 file changed, 587 insertions(+), 143 deletions(-)
---
diff --git a/plug-ins/python/spyro-plus.py b/plug-ins/python/spyro-plus.py
index bb60647e8a..1710e559c7 100644
--- a/plug-ins/python/spyro-plus.py
+++ b/plug-ins/python/spyro-plus.py
@@ -32,17 +32,46 @@ import gettext
_ = gettext.gettext
def N_(message): return message
-from math import pi, sin, cos, atan, atan2, fmod, radians
+from math import pi, sin, cos, atan, atan2, fmod, radians, sqrt
import gettext
import math
import time
+def pdb_call(proc_name, *args):
+ if len(args) % 2 == 1:
+ raise ValueError("The number of arguments after proc_name needs to be even. ")
+
+ num_args = len(args) // 2
+ proc_args = Gimp.ValueArray.new(num_args)
+ for i in range(num_args):
+ proc_args.append(GObject.Value(args[2 * i], args[2 * i + 1]))
+
+ return Gimp.get_pdb().run_procedure(proc_name, proc_args)
+
+
+def result_success():
+ return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
+
+
+PROC_NAME = "plug-in-spyrogimp"
+
two_pi, half_pi = 2 * pi, pi / 2
layer_name = _("Spyro Layer")
+path_name = _("Spyro Path")
# "Enums"
-GEAR_NOTATION, TOY_KIT_NOTATION = range(2) # Pattern notations
+GEAR_NOTATION, TOY_KIT_NOTATION, VISUAL_NOTATION = range(3) # Pattern notations
+
+RESPONSE_REDRAW, RESPONSE_RESET_PARAMS = range(2) # Button responses in dialog.
+
+# Save options of the dialog
+SAVE_AS_NEW_LAYER, SAVE_BY_REDRAW, SAVE_AS_PATH = range(3)
+save_options = [
+ _("As New Layer"),
+ _("Redraw on last active layer"),
+ _("As Path")
+]
# Mapping of pattern notation to the corresponding tab in the pattern notation notebook.
pattern_notation_page = {}
@@ -60,6 +89,10 @@ wheel = [
wheel_teeth = [wh[0] for wh in wheel]
+def lcm(a, b):
+ """ Least common multiplier """
+ return a * b // math.gcd(a, b)
+
### Shapes
@@ -68,7 +101,7 @@ class CanRotateShape:
class Shape:
- def configure(self, img, pp, cp, drawing_no):
+ def configure(self, img, pp, cp):
self.image, self.pp, self.cp = img, pp, cp
def can_equal_w_h(self):
@@ -102,8 +135,8 @@ class CircleShape(Shape):
class SidedShape(CanRotateShape, Shape):
- def configure(self, img, pp, cp, drawing_no):
- Shape.configure(self, img, pp, cp, drawing_no)
+ def configure(self, img, pp, cp):
+ Shape.configure(self, img, pp, cp)
self.angle_of_each_side = two_pi / pp.sides
self.half_angle = self.angle_of_each_side / 2.0
self.cos_half_angle = cos(self.half_angle)
@@ -254,8 +287,8 @@ class AbstractShapeFromParts(Shape):
class RackShape(CanRotateShape, AbstractShapeFromParts):
name = _("Rack")
- def configure(self, img, pp, cp, drawing_no):
- Shape.configure(self, img, pp, cp, drawing_no)
+ def configure(self, img, pp, cp):
+ Shape.configure(self, img, pp, cp)
round_teeth = 12
side_teeth = (cp.fixed_gear_teeth - 2 * round_teeth) / 2
@@ -298,8 +331,8 @@ class RackShape(CanRotateShape, AbstractShapeFromParts):
class FrameShape(AbstractShapeFromParts):
name = _("Frame")
- def configure(self, img, pp, cp, drawing_no):
- Shape.configure(self, img, pp, cp, drawing_no)
+ def configure(self, img, pp, cp):
+ Shape.configure(self, img, pp, cp)
x1, x2 = cp.x1 + cp.moving_gear_radius, cp.x2 - cp.moving_gear_radius
y1, y2 = cp.y1 + cp.moving_gear_radius, cp.y2 - cp.moving_gear_radius
@@ -314,6 +347,26 @@ class FrameShape(AbstractShapeFromParts):
self.parts.finish()
+# Naive hash that gets only a limited amount of points from the selection.
+# We use this hash to detect whether the selection has changed or not.
+def naive_hash(img):
+ selection = img.get_selection()
+
+ # Get bounds of selection.
+ flag, non_empty, x1, y1, x2, y2 = selection.bounds(img)
+
+ # We want to compute a hash of the selection, but getting all the points in the selection
+ # will take too long. We will get at most 25 points in each axis, so at most 25**2 points.
+ step_x = 1 if (x2 - x1) <= 25 else (x2 - x1) // 25 + 1
+ step_y = 1 if (y2 - y1) <= 25 else (y2 - y1) // 25 + 1
+ hash = x1 * y1 + x2 * y2
+
+ for x in range(x1, x2, step_x):
+ for y in range(y1, y2, step_y):
+ hash += selection.get_pixel(x, y)[0] * x * y
+ return hash
+
+
class SelectionToPath:
""" Converts a selection to a path """
@@ -333,25 +386,27 @@ class SelectionToPath:
else:
selection_was_empty = False
- pdb.plug_in_sel2path(self.image, self.image.active_layer)
-
- self.path = self.image.vectors[0]
+ result = pdb_call('plug-in-sel2path',
+ Gimp.RunMode, Gimp.RunMode.NONINTERACTIVE,
+ Gimp.Image, self.image,
+ Gimp.Drawable, self.image.get_active_layer()
+ )
- self.num_strokes, self.stroke_ids = self.path.get_strokes(self.path)
- self.stroke_ids = list(self.stroke_ids)
+ self.path = self.image.get_vectors()[0]
+ self.stroke_ids = self.path.get_strokes()
# A path may contain several strokes. If so lets throw away a stroke that
# simply describes the borders of the image, if one exists.
- if self.num_strokes > 1:
+ if len(self.stroke_ids) > 1:
# Lets compute what a stroke of the image borders should look like.
w, h = float(self.image.width()), float(self.image.height())
frame_strokes = [0.0] * 6 + [0.0, h] * 3 + [w, h] * 3 + [w, 0.0] * 3
- for stroke in range(self.num_strokes):
- strokes = self.path.strokes[stroke].points[0]
- if strokes == frame_strokes:
+ for stroke in range(len(self.stroke_ids)):
+ stroke_id = self.stroke_ids[stroke]
+ vectors_stroke_type, control_points, closed = self.path.stroke_get_points(stroke_id)
+ if control_points == frame_strokes:
del self.stroke_ids[stroke]
- self.num_strokes -= 1
break
self.set_current_stroke(0)
@@ -361,8 +416,10 @@ class SelectionToPath:
Gimp.Selection.none(self.image)
def compute_selection_hash(self):
- px = self.image.selection.get_pixel_rgn(0, 0, self.image.width(), self.image.height())
- return px[0:self.image.width(), 0:self.image.height()].__hash__()
+ return naive_hash(self.image)
+ # In gimp 2 we used this:
+ #px = self.image.get_selection(). get_pixel_rgn(0, 0, self.image.width(), self.image.height())
+ #return px[0:self.image.width(), 0:self.image.height()].__hash__()
def regenerate_path_if_selection_changed(self):
current_selection_hash = self.compute_selection_hash()
@@ -371,7 +428,7 @@ class SelectionToPath:
self.convert_selection_to_path()
def get_num_strokes(self):
- return self.num_strokes
+ return len(self.stroke_ids)
def set_current_stroke(self, stroke_id=0):
# Compute path length.
@@ -396,11 +453,11 @@ class SelectionShape(Shape):
else:
self.path.regenerate_path_if_selection_changed()
- def configure(self, img, pp, cp, drawing_no):
+ def configure(self, img, pp, cp):
""" Set bounds of pattern """
- Shape.configure(self, img, pp, cp, drawing_no)
- self.drawing_no = drawing_no
- self.path.set_current_stroke(drawing_no)
+ Shape.configure(self, img, pp, cp)
+ self.drawing_no = cp.current_drawing
+ self.path.set_current_stroke(self.drawing_no)
def get_num_drawings(self):
return self.path.get_num_strokes()
@@ -417,7 +474,7 @@ class SelectionShape(Shape):
cp = self.cp
if dist is None:
dist = cp.moving_gear_radius
- x, y, slope, valid = self.path.point_at_angle(oangle)
+ another_bool, x, y, slope, valid = self.path.point_at_angle(oangle)
slope_angle = atan(slope)
# We want to find an angle perpendicular to the slope, but in which direction?
# Lets try both sides and see which of them is inside the selection.
@@ -579,6 +636,25 @@ class StrokePaintTool(AbstractStrokeTool):
Gimp.context_set_paint_method(self.paint_method)
+class SaveToPathTool():
+ """ This tool cannot be chosen by the user from the tools menu.
+ We dont add this to the list of tools. """
+
+ def __init__(self, img):
+ self.path = Gimp.Vectors.new(img, path_name)
+ img.insert_vectors(self.path, None, 0)
+
+ def draw(self, layer, strokes, color=None):
+ # We need to multiply every point by 3, because we are creating a path,
+ # where each point has two additional control points.
+ control_points = []
+ for i, k in zip(strokes[0::2], strokes[1::2]):
+ control_points += [i, k] * 3
+
+ self.path.stroke_new_from_points(Gimp.VectorsStrokeType.BEZIER,
+ control_points, False)
+
+
tools = [
PreviewTool(),
StrokePaintTool(_("PaintBrush"), "gimp-paintbrush"),
@@ -615,6 +691,7 @@ class PatternParameters:
# A value of 100 means the edge of the wheel.
if not hasattr(self, 'hole_percent'):
self.hole_percent = 100.0
+
# Toy Kit parameters
# Hole number in Toy Kit notation. Hole #1 is at the edge of the wheel, and the last hole is
# near the center of the wheel, but not exactly at the center.
@@ -625,6 +702,16 @@ class PatternParameters:
if not hasattr(self, 'kit_moving_gear_index'):
self.kit_moving_gear_index = 1
+ # Visual notation parameters
+ if not hasattr(self, 'petals'):
+ self.petals = 5
+ if not hasattr(self, 'petal_skip'):
+ self.petal_skip = 2
+ if not hasattr(self, 'doughnut_hole'):
+ self.doughnut_hole = 50.0
+ if not hasattr(self, 'doughnut_width'):
+ self.doughnut_width = 50.0
+
# Shape
if not hasattr(self, 'shape_index'):
self.shape_index = 0 # Index in the shapes array
@@ -646,8 +733,8 @@ class PatternParameters:
if not hasattr(self, 'long_gradient'):
self.long_gradient = False
- if not hasattr(self, 'keep_separate_layer'):
- self.keep_separate_layer = True
+ if not hasattr(self, 'save_option'):
+ self.save_option = SAVE_AS_NEW_LAYER
def kit_max_hole_number(self):
return wheel[self.kit_moving_gear_index][1]
@@ -678,12 +765,11 @@ class ComputedParameters:
The results of these computations are used to perform the drawing.
Having all these computations in one place makes it convenient to pass
around as a parameter.
- """
- def __init__(self, pp, x1, y1, x2, y2):
- def lcm(a, b):
- """ Least common multiplier """
- return a * b // math.gcd(a, b)
+ If the pattern parameters should result in multiple patterns to be drawn, the
+ compute parameters also stores which one is currently being drawn.
+ """
+ def __init__(self, pp, img):
def compute_gradients():
self.use_gradient = self.pp.long_gradient and tools[self.pp.tool_index].can_color
@@ -732,30 +818,79 @@ class ComputedParameters:
# Find the distance between the hole and the center of the inner circle.
# To do this, we compute the size of the gears, by the number of teeth.
# The circumference of the outer ring is 2 * pi * outer_R = #fixed_gear_teeth * tooth size.
- self.outer_R = min(self.x_half_size, self.y_half_size)
- size_of_tooth_in_pixels = two_pi * self.outer_R / self.fixed_gear_teeth
- self.moving_gear_radius = size_of_tooth_in_pixels * self.moving_gear_teeth / two_pi
+ outer_R = min(self.x_half_size, self.y_half_size)
+ if self.pp.pattern_notation == VISUAL_NOTATION:
+ doughnut_width = self.pp.doughnut_width
+ if doughnut_width + self.pp.doughnut_hole > 100:
+ doughnut_width = 100.0 - self.pp.doughnut_hole
+
+ # Let R, r be the radius of fixed and moving gear, and let hp be the hole percent.
+ # Let dwp, dhp be the doughnut width and hole in percents of R.
+ # The two sides of the following equation calculate how to reach the center of the moving
+ # gear from the center of the fixed gear:
+ # I) R * (dhp/100 + dwp/100/2) = R - r
+ # The following equation expresses which r and hp would generate a doughnut of width dw.
+ # II) R * dw/100 = 2 * r * hp/100
+ # We solve the two above equations to calculate hp and r:
+ self.hole_percent = doughnut_width / (2.0 * (1 - (self.pp.doughnut_hole + doughnut_width /
2.0) / 100.0))
+ self.moving_gear_radius = outer_R * doughnut_width / (2 * self.hole_percent)
+ else:
+ size_of_tooth_in_pixels = two_pi * outer_R / self.fixed_gear_teeth
+ self.moving_gear_radius = size_of_tooth_in_pixels * self.moving_gear_teeth / two_pi
+
self.hole_dist_from_center = self.hole_percent / 100.0 * self.moving_gear_radius
self.pp = pp
+ # Check if the shape is made of multiple shapes, as in using Selection as fixed gear.
+ if (
+ isinstance(shapes[self.pp.shape_index], SelectionShape) and
+ curve_types[self.pp.curve_type].supports_shapes()
+ ):
+ shapes[self.pp.shape_index].process_selection(img)
+ Gimp.displays_flush()
+ self.num_drawings = shapes[self.pp.shape_index].get_num_drawings()
+ else:
+ self.num_drawings = 1
+ self.current_drawing = 0
+
+ # Get bounds. We don't care weather a selection exists or not.
+ success, exists, x1, y1, x2, y2 = Gimp.Selection.bounds(img)
+
# Combine different ways to specify patterns, into a unified set of computed parameters.
+ self.num_notation_drawings = 1
+ self.current_notation_drawing = 0
if self.pp.pattern_notation == GEAR_NOTATION:
self.fixed_gear_teeth = int(round(pp.outer_teeth))
self.moving_gear_teeth = int(round(pp.inner_teeth))
+ self.petals = self.num_petals()
self.hole_percent = pp.hole_percent
elif self.pp.pattern_notation == TOY_KIT_NOTATION:
self.fixed_gear_teeth = ring_teeth[pp.kit_fixed_gear_index]
self.moving_gear_teeth = wheel[pp.kit_moving_gear_index][0]
+ self.petals = self.num_petals()
# We want to map hole #1 to 100% and hole of max_hole_number to 2.5%
# We don't want 0% because that would be the exact center of the moving gear,
# and that would create a boring pattern.
max_hole_number = wheel[pp.kit_moving_gear_index][1]
self.hole_percent = (max_hole_number - pp.hole_number) / float(max_hole_number - 1) * 97.5 + 2.5
+ elif self.pp.pattern_notation == VISUAL_NOTATION:
+ self.petals = pp.petals
+ self.fixed_gear_teeth = pp.petals
+ self.moving_gear_teeth = pp.petals - pp.petal_skip
+ if self.moving_gear_teeth < 20:
+ self.fixed_gear_teeth *= 10
+ self.moving_gear_teeth *= 10
+ self.hole_percent = 100.0
+ self.num_notation_drawings = math.gcd(pp.petals, pp.petal_skip)
+ self.notation_drawings_rotation = two_pi / pp.petals
# Rotations
self.shape_rotation_radians = self.radians_from_degrees(pp.shape_rotation)
- self.pattern_rotation_radians = self.radians_from_degrees(pp.pattern_rotation)
+ self.pattern_rotation_start_radians = self.radians_from_degrees(pp.pattern_rotation)
+ self.pattern_rotation_radians = self.pattern_rotation_start_radians
+ # Additional fixed pattern rotation for lissajous.
+ self.lissajous_rotation = two_pi / self.petals / 4.0
# Compute the total number of teeth we have to go over.
# Another way to view it is the total of lines we are going to draw.
@@ -785,6 +920,10 @@ class ComputedParameters:
compute_sizes()
+ def num_petals(self):
+ """ The number of 'petals' (or points) that will be produced by a spirograph drawing. """
+ return lcm(self.fixed_gear_teeth, self.moving_gear_teeth) / self.moving_gear_teeth
+
def radians_from_degrees(self, degrees):
positive_degrees = degrees if degrees >= 0 else degrees + 360
return radians(positive_degrees)
@@ -798,6 +937,22 @@ class ComputedParameters:
color.a = colors[3]
return color
+ def next_drawing(self):
+ """ Multiple drawings can be drawn either when the selection is used as a fixed
+ gear, and/or the visual tab is used, which causes multiple drawings
+ to be drawn at different rotations. """
+ if self.current_notation_drawing < self.num_notation_drawings - 1:
+ self.current_notation_drawing += 1
+ self.pattern_rotation_radians = self.pattern_rotation_start_radians + (
+ self.current_notation_drawing * self.notation_drawings_rotation)
+ else:
+ self.current_drawing += 1
+ self.current_notation_drawing = 0
+ self.pattern_rotation_radians = self.pattern_rotation_start_radians
+
+ def has_more_drawings(self):
+ return (self.current_notation_drawing < self.num_notation_drawings - 1 or
+ self.current_drawing < self.num_drawings - 1)
### Curve types
@@ -807,12 +962,13 @@ class CurveType:
def supports_shapes(self):
return True
+
class RouletteCurveType(CurveType):
def get_strokes(self, p, cp):
strokes = []
for curr_tooth in range(cp.num_points):
- iangle = curr_tooth * cp.iangle_factor
+ iangle = fmod(curr_tooth * cp.iangle_factor + cp.pattern_rotation_radians, two_pi)
oangle = fmod(curr_tooth * cp.oangle_factor + cp.pattern_rotation_radians, two_pi)
x, y = shapes[p.shape_index].get_center_of_moving_gear(oangle)
@@ -866,7 +1022,10 @@ class LissaCurveType:
strokes = []
for curr_tooth in range(cp.num_points):
iangle = curr_tooth * cp.iangle_factor
- oangle = fmod(curr_tooth * cp.oangle_factor + cp.pattern_rotation_radians, two_pi)
+ # Adding the cp.lissajous_rotation rotation makes the pattern have the same number of curves
+ # as the other curve types. Without it, many lissajous patterns would redraw the same lines
twice,
+ # and thus look less dense than the other curves.
+ oangle = fmod(curr_tooth * cp.oangle_factor + cp.pattern_rotation_radians +
cp.lissajous_rotation, two_pi)
strokes.append(cp.x_center + cp.x_half_size * cos(oangle))
strokes.append(cp.y_center + cp.y_half_size * cos(iangle))
@@ -887,12 +1046,10 @@ class DrawingEngine:
def __init__(self, img, p):
self.img, self.p = img, p
self.cp = None
- self.num_drawings = 0
# For incremental drawing
self.strokes = []
self.start = 0
- self.current_drawing = 0
self.chunk_size_lines = 600
self.chunk_no = 0
# We are aiming for the drawing time of a chunk to be no longer than max_time.
@@ -902,20 +1059,7 @@ class DrawingEngine:
def pre_draw(self):
""" Needs to be called before starting to draw a pattern. """
-
- self.current_drawing = 0
-
- if isinstance(shapes[self.p.shape_index], SelectionShape) and
curve_types[self.p.curve_type].supports_shapes():
- shapes[self.p.shape_index].process_selection(self.img)
- Gimp.displays_flush()
- self.num_drawings = shapes[self.p.shape_index].get_num_drawings()
- else:
- self.num_drawings = 1
-
- # Get bounds. We don't care weather a selection exists or not.
- success, exists, x1, y1, x2, y2 = Gimp.Selection.bounds(self.img)
-
- self.cp = ComputedParameters(self.p, x1, y1, x2, y2)
+ self.cp = ComputedParameters(self.p, self.img)
def draw_full(self, layer):
""" Non incremental drawing. """
@@ -923,8 +1067,7 @@ class DrawingEngine:
self.pre_draw()
self.img.undo_group_start()
- for drawing_no in range(self.num_drawings):
- self.current_drawing = drawing_no
+ while True:
self.set_strokes()
if self.cp.use_gradient:
@@ -933,21 +1076,28 @@ class DrawingEngine:
else:
tools[self.p.tool_index].draw(layer, self.strokes)
+ if self.cp.has_more_drawings():
+ self.cp.next_drawing()
+ else:
+ break
+
self.img.undo_group_end()
Gimp.displays_flush()
# Methods for incremental drawing.
- def draw_next_chunk(self, layer, fetch_next_drawing=True):
+ def draw_next_chunk(self, layer, fetch_next_drawing=True, tool=None):
stroke_chunk, color = self.next_chunk(fetch_next_drawing)
- tools[self.p.tool_index].draw(layer, stroke_chunk, color)
+ if not tool:
+ tool = tools[self.p.tool_index]
+ tool.draw(layer, stroke_chunk, color)
return len(stroke_chunk)
def set_strokes(self):
""" Compute the strokes of the current pattern. The heart of the plugin. """
- shapes[self.p.shape_index].configure(self.img, self.p, self.cp, drawing_no=self.current_drawing)
+ shapes[self.p.shape_index].configure(self.img, self.p, self.cp)
self.strokes = curve_types[self.p.curve_type].get_strokes(self.p, self.cp)
@@ -980,8 +1130,8 @@ class DrawingEngine:
# If self.strokes has ended, lets fetch strokes for the next drawing.
if fetch_next_drawing and not self.has_more_strokes():
- self.current_drawing += 1
- if self.current_drawing < self.num_drawings:
+ if self.cp.has_more_drawings():
+ self.cp.next_drawing()
self.set_strokes()
return result, color
@@ -1006,12 +1156,187 @@ class DrawingEngine:
self.chunk_size_lines = min(1000, self.chunk_size_lines)
-class SpyroWindow(Gtk.Window):
+# Constants for DoughnutWidget
+
+# Enum - When the mouse is pressed, which target value is being changed.
+TARGET_NONE, TARGET_HOLE, TARGET_WIDTH = range(3)
+
+CIRCLE_CENTER_X = 4
+RIGHT_MARGIN = 2
+TOTAL_MARGIN = CIRCLE_CENTER_X + RIGHT_MARGIN
+
+# A widget for displaying and setting the pattern of a spirograph, using a "doughnut" as
+# a visual metaphore. This widget replaces two scale widgets.
+class DoughnutWidget(Gtk.DrawingArea):
+ __gtype_name__ = 'DoughnutWidget'
+
+ def __init__(self, *args, **kwds):
+ super().__init__(*args, **kwds)
+ self.set_size_request(80, 40)
+ self.set_margin_start(2)
+ self.set_margin_end(2)
+ self.set_margin_top(2)
+ self.set_margin_bottom(2)
+
+ self.add_events(
+ Gdk.EventMask.BUTTON1_MOTION_MASK |
+ Gdk.EventMask.BUTTON_PRESS_MASK |
+ Gdk.EventMask.BUTTON_RELEASE_MASK |
+ Gdk.EventMask.POINTER_MOTION_MASK
+ )
+
+ self.resize_cursor = Gdk.Cursor.new_for_display(self.get_display(),
+ Gdk.CursorType.SB_H_DOUBLE_ARROW)
+
+ self.button_pressed = False
+ self.target = TARGET_NONE
+
+ self.hole_radius = 30
+ self.doughnut_width = 30
+
+ def set_hole_radius(self, hole_radius):
+ self.hole_radius = hole_radius
+ self.queue_draw()
+
+ def get_hole_radius(self):
+ return self.hole_radius
+
+ def set_width(self, width):
+ self.doughnut_width = width
+ self.queue_draw()
+
+ def get_width(self):
+ return self.doughnut_width
+
+ def compute_doughnut(self):
+ """ Compute the location of the doughnut circles.
+ Returns (circle center x, circle center y, radius of inner circle, radius of outer circle) """
+ allocation = self.get_allocation()
+ alloc_width = allocation.width - TOTAL_MARGIN
+ return (
+ CIRCLE_CENTER_X, allocation.height/2,
+ alloc_width * self.hole_radius/ 100.0,
+ alloc_width * min(self.hole_radius + self.doughnut_width, 100.0)/ 100.0
+ )
+
+ def set_cursor_h_resize(self, flag):
+ """ Set the mouse to be a double arrow, if flag is true.
+ Otherwise, use the cursor of the parent window. """
+ gdk_window = self.get_window()
+ gdk_window.set_cursor(self.resize_cursor if flag else None)
+
+ def get_target(self, x, y):
+ # Find out if x, y is over one of the circle edges.
+
+ center_x, center_y, hole_radius, outer_radius = self.compute_doughnut()
+
+ # Compute distance from circle center to point
+ dist = math.sqrt((center_x - x) ** 2 + (center_y - y) ** 2)
+
+ if abs(dist - hole_radius) <= 3:
+ return TARGET_HOLE
+ if abs(dist - outer_radius) <= 3:
+ return TARGET_WIDTH
+
+ return TARGET_NONE
+
+ def do_draw(self, cr):
+
+ allocation = self.get_allocation()
+ center_x, center_y, hole_radius, outer_radius = self.compute_doughnut()
+
+ # Paint background
+ Gtk.render_background(self.get_style_context(), cr,
+ 0, 0, allocation.width, allocation.height)
+
+ fg_color = self.get_style_context().get_color(Gtk.StateFlags.NORMAL)
+
+ # Draw doughnut interior
+ arc = math.pi*3/2.0
+ cr.set_source_rgba(fg_color.red, fg_color.green, fg_color.blue, fg_color.alpha/2);
+ cr.arc(center_x, center_y, hole_radius, -arc, arc)
+ cr.arc_negative(center_x, center_y, outer_radius, arc, -arc)
+ cr.close_path()
+ cr.fill()
+
+ # Draw doughnut border.
+ cr.set_source_rgba(*list(fg_color));
+ cr.set_line_width(3)
+ cr.arc_negative(center_x, center_y, outer_radius, arc, -arc)
+ cr.stroke()
+ if hole_radius < 1.0:
+ # If the radius is too small, nothing will be drawn.
+ # So draw a small cross marker instead.
+ cr.set_line_width(2)
+ cr.move_to(center_x-4, center_y)
+ cr.line_to(center_x+4, center_y)
+ cr.move_to(center_x, center_y-4)
+ cr.line_to(center_x, center_y+4)
+ else:
+ cr.arc(center_x, center_y, hole_radius, -arc, arc)
+ cr.stroke()
+
+ def compute_new_radius(self, x):
+ """ This method is called during mouse dragging of the widget.
+ Compute the new radius based on
+ the current x location of the mouse pointer. """
+ allocation = self.get_allocation()
+
+ # How much does a single pixel difference in x, change the radius?
+ # Note that: allocation.width - TOTAL_MARGIN = 100 radius units,
+ radius_per_pixel = 100.0 / (allocation.width - TOTAL_MARGIN)
+ new_radius = self.start_radius + (x - self.start_x) * radius_per_pixel
+
+ if self.target == TARGET_HOLE:
+ self.hole_radius = max(min(new_radius, 99.0), 0.0)
+ else:
+ self.doughnut_width = max(min(new_radius, 100.0), 1.0)
+
+ self.queue_draw()
+
+ def do_button_press_event(self, event):
+ self.button_pressed = True
+
+ # If we clicked on one of the doughnut borders, remember which
+ # border we clicked on, and setup variable to start dragging it.
+ target = self.get_target(event.x, event.y)
+ if target == TARGET_HOLE or target == TARGET_WIDTH:
+ self.target = target
+ self.start_x = event.x
+ self.start_radius = (
+ self.hole_radius if target == TARGET_HOLE else
+ self.doughnut_width
+ )
+
+ def do_button_release_event(self, event):
+ # If one the doughnut borders was being dragged, recompute doughnut size.
+ if self.target != TARGET_NONE:
+ self.compute_new_radius(event.x)
+ # Clip the width, if it is too large to fit.
+ if self.hole_radius + self.doughnut_width > 100:
+ self.doughnut_width = 100 - self.hole_radius
+ self.emit("values_changed", self.hole_radius, self.doughnut_width)
+
+ self.button_pressed = False
+ self.target = TARGET_NONE
+
+ def do_motion_notify_event(self, event):
+ if self.button_pressed:
+ # We are dragging one of the doughnut borders; recompute its size.
+ if self.target != TARGET_NONE:
+ self.compute_new_radius(event.x)
+ else:
+ # Set cursor according to whether we are over one of the doughnut borders.
+ target = self.get_target(event.x, event.y)
+ self.set_cursor_h_resize(target != TARGET_NONE)
+
+# Create signal that returns change parameters.
+GObject.type_register(DoughnutWidget)
+GObject.signal_new("values_changed", DoughnutWidget, GObject.SignalFlags.RUN_LAST,
+ GObject.TYPE_NONE, (GObject.TYPE_INT, GObject.TYPE_INT))
- def do_key_press_event(self, event):
- # Quit the window on Escape key.
- if event.keyval == Gdk.KEY_Escape:
- self.cancel_window(self)
+
+class SpyroWindow():
class MyScale():
""" Combintation of scale and spin that control the same adjuster. """
@@ -1047,17 +1372,28 @@ class SpyroWindow(Gtk.Window):
table.set_row_spacing(10)
return table
- def label_in_table(label_text, table, row, tooltip_text=None):
+ def label_in_table(label_text, table, row, tooltip_text=None, col=0):
""" Create a label and set it in first col of table. """
label = Gtk.Label(label=label_text)
label.set_xalign(0.0)
label.set_yalign(0.5)
if tooltip_text:
label.set_tooltip_text(tooltip_text)
- table.attach(label, 0, row, 1, 1)
+ table.attach(label, col, row, 1, 1)
label.show()
- def hscale_in_table(adj, table, row, callback, digits=0):
+ def spin_in_table(adj, table, row, callback, digits=0, col=0):
+ spin = Gtk.SpinButton.new(adj, climb_rate=0.5, digits=digits)
+ spin.set_numeric(True)
+ spin.set_snap_to_ticks(True)
+ spin.set_max_length(5)
+ spin.set_width_chars(5)
+ table.attach(spin, col, row, 1, 1)
+ spin.show()
+ adj.connect("value_changed", callback)
+ return spin
+
+ def hscale_in_table(adj, table, row, callback, digits=0, col=1, cols=1):
""" Create an hscale and a spinner using the same Adjustment, and set it in table. """
scale = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, adj)
scale.set_size_request(150, -1)
@@ -1068,21 +1404,15 @@ class SpyroWindow(Gtk.Window):
#scale.set_update_policy(Gtk.UPDATE_DISCONTINUOUS)
scale.set_hexpand(True)
scale.set_halign(Gtk.Align.FILL)
- table.attach(scale, 1, row, 1, 1)
+ table.attach(scale, col, row, cols, 1)
scale.show()
spin = Gtk.SpinButton.new(adj, climb_rate=0.5, digits=digits)
spin.set_numeric(True)
- # TODO:
- # For some reason, spinbutton does not allow editing the text directly.
- # None of the following solve this issue:
- # spin.set_editable(True)
- # spin.set_overwrite_mode(True)
- # spin.set_update_policy(Gtk.SpinButtonUpdatePolicy.ALWAYS)
spin.set_snap_to_ticks(True)
spin.set_max_length(5)
spin.set_width_chars(5)
- table.attach(spin, 2, row, 1, 1)
+ table.attach(spin, col + cols, row, 1, 1)
spin.show()
adj.connect("value_changed", callback)
@@ -1163,7 +1493,7 @@ class SpyroWindow(Gtk.Window):
alignment.show()
self.pattern_notebook = Gtk.Notebook()
- self.pattern_notebook.set_border_width(0)
+ self.pattern_notebook.set_border_width(10)
self.pattern_notebook.connect('switch-page', self.pattern_notation_tab_changed)
# "Gear" pattern notation.
@@ -1222,7 +1552,61 @@ class SpyroWindow(Gtk.Window):
self.kit_hole_adj = Gtk.Adjustment.new(self.p.hole_number, 1, self.p.kit_max_hole_number(), 1,
10, 0)
self.kit_hole_myscale = hscale_in_table(self.kit_hole_adj, kit_table, row, self.kit_hole_changed)
- # Add tables as childs of the pattern notebook
+ # "Visual" pattern notation.
+
+ visual_table = create_table(5)
+
+ row = 0
+ label_in_table(_("Flower Petals"), visual_table, row,
+ _("The number of petals in the pattern."))
+ self.petals_adj = Gtk.Adjustment.new(self.p.petals, 2, 100, 1, 5, 0)
+ hscale_in_table(self.petals_adj, visual_table, row, self.petals_changed, cols=3)
+
+ row += 1
+ label_in_table(_("Petal Skip"), visual_table, row,
+ _( "The number of petals to advance for drawing the next petal."))
+ self.petal_skip_adj = Gtk.Adjustment.new(self.p.petal_skip, 1, 50, 1, 5, 0)
+ hscale_in_table(self.petal_skip_adj, visual_table, row, self.petal_skip_changed, cols=3)
+
+ row += 1
+ label_in_table(_("Hole Radius(%)"), visual_table, row,
+ _("The radius of the hole in the center of the pattern "
+ "where nothing will be drawn. Given as a percentage of the "
+ "size of the pattern. A value of 0 will produce no hole. "
+ "A Value of 99 will produce a thin line on the edge."))
+ self.doughnut_hole_adj = Gtk.Adjustment.new(self.p.doughnut_hole, 0.0, 99.0, 0.1, 5.0, 0.0)
+ self.doughnut_hole_myscale = spin_in_table(self.doughnut_hole_adj, visual_table,
+ row, self.doughnut_hole_changed, 1, 1)
+
+ self.doughnut = DoughnutWidget()
+ frame = Gtk.Frame()
+ frame.add(self.doughnut)
+ visual_table.attach(frame, 2, row, 1, 1)
+ self.doughnut.set_hexpand(True)
+ self.doughnut.set_halign(Gtk.Align.FILL)
+ frame.set_hexpand(True)
+ frame.set_halign(Gtk.Align.FILL)
+
+ self.doughnut.connect('values_changed', self.doughnut_changed)
+ frame.show()
+ self.doughnut.show()
+
+ label_in_table(_("Width(%)"), visual_table, row,
+ _("The width of the pattern as a percentage of the "
+ "size of the pattern. A Value of 1 will just draw a thin pattern. "
+ "A Value of 100 will fill the entire fixed gear."),
+ 3)
+ self.doughnut_width_adj = Gtk.Adjustment.new(self.p.doughnut_width, 1.0, 100.0, 0.1, 5.0, 0.0)
+ self.doughnut_width_myscale = spin_in_table(self.doughnut_width_adj, visual_table,
+ row, self.doughnut_width_changed, 1, 4)
+
+ # Add tables to the pattern notebook
+
+ pattern_notation_page[VISUAL_NOTATION] = self.pattern_notebook.append_page(visual_table)
+ self.pattern_notebook.set_tab_label_text(visual_table, _("Visual"))
+ self.pattern_notebook.child_set_property(visual_table, 'tab-expand', False)
+ self.pattern_notebook.child_set_property(visual_table, 'tab-fill', False)
+ visual_table.show()
pattern_notation_page[TOY_KIT_NOTATION] = self.pattern_notebook.append_page(kit_table)
self.pattern_notebook.set_tab_label_text(kit_table, _("Toy Kit"))
@@ -1318,53 +1702,49 @@ class SpyroWindow(Gtk.Window):
self.equal_w_h_checkbox.show()
self.equal_w_h_checkbox.connect("toggled", self.equal_w_h_checkbox_changed)
-
add_to_box(vbox, table)
return vbox
- def add_button_to_box(box, text, callback, tooltip_text=None):
- btn = Gtk.Button(label=text)
- if tooltip_text:
- btn.set_tooltip_text(tooltip_text)
- box.add(btn)
- btn.show()
- btn.connect("clicked", callback)
- return btn
-
def dialog_button_box():
- hbox = Gtk.HBox(homogeneous=True, spacing=20)
- add_button_to_box(hbox, _("Re_draw"), self.redraw,
- _("If you change the settings of a tool, change color, or change the
selection, "
- "press this to preview how the pattern looks."))
- add_button_to_box(hbox, _("_Reset"), self.reset_params)
- add_button_to_box(hbox, _("_Cancel"), self.cancel_window)
- self.ok_btn = add_button_to_box(hbox, _("_OK"), self.ok_window)
-
- self.keep_separate_layer_checkbox = Gtk.CheckButton(label=_("Keep\nLayer"))
- self.keep_separate_layer_checkbox.set_tooltip_text(
- _("If checked, then once OK is pressed, the spyro layer is kept, and the plugin exits
quickly. "
- "If unchecked, the spyro layer is deleted, and the pattern is redrawn on the layer that
was "
- "active when the plugin was launched.")
+ self.dialog.add_button("_Cancel", Gtk.ResponseType.CANCEL)
+ self.ok_btn = self.dialog.add_button("_OK", Gtk.ResponseType.OK)
+ btn = self.dialog.add_button(_("Re_draw"), RESPONSE_REDRAW)
+ btn.set_tooltip_text(
+ _("If you change the settings of a tool, change color, or change the selection, "
+ "press this to preview how the pattern looks.")
)
- hbox.add(self.keep_separate_layer_checkbox)
- self.keep_separate_layer_checkbox.show()
- self.keep_separate_layer_checkbox.connect("toggled", self.keep_separate_layer_checkbox_changed)
+ self.dialog.add_button(_("_Reset"), RESPONSE_RESET_PARAMS)
+
+ hbox = Gtk.HBox(homogeneous=True, spacing=20)
+ hbox.set_border_width(10)
+
+ table = create_table(5)
+
+ row = 0
+ label_in_table(_("Save"), table, row,
+ _("Choose whether to save as new layer, redraw on last active layer, or save to
path"))
+ self.save_option_combo = set_combo_in_table(save_options, table, row,
+ self.save_option_changed)
+ self.save_option_combo.show()
+
+ hbox.add(table)
+ table.show()
return hbox
def create_ui():
- # Create the dialog
- Gtk.Window.__init__(self)
- self.set_title(_("Spyrogimp"))
- self.set_default_size(350, -1)
- self.set_border_width(10)
- # self.set_keep_above(True) # keep the window on top
+ use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
+ self.dialog = Gimp.Dialog(use_header_bar=use_header_bar,
+ title=_("Spyrogimp"))
+ #self.set_default_size(350, -1)
+ #self.set_border_width(10)
- # Vertical box in which we will add all the UI elements.
- vbox = Gtk.VBox(spacing=10, homogeneous=False)
- self.add(vbox)
+ vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
+ homogeneous=False, spacing=10)
+ self.dialog.get_content_area().add(vbox)
+ vbox.show()
box = Gimp.HintBox.new(_("Draw spyrographs using current tool settings and selection."))
vbox.pack_start(box, False, False, 0)
@@ -1391,17 +1771,16 @@ class SpyroWindow(Gtk.Window):
vbox.add(self.main_notebook)
self.main_notebook.show()
- add_horizontal_separator(vbox)
+ # add_horizontal_separator(vbox)
+
+ add_to_box(vbox, dialog_button_box())
self.progress_bar = Gtk.ProgressBar() # gimpui.ProgressBar() - causes gimppdbprogress error
message.
- self.progress_bar.set_size_request(-1, 30)
+ self.progress_bar.set_show_text(True)
vbox.add(self.progress_bar)
self.progress_bar.show()
- add_to_box(vbox, dialog_button_box())
-
- vbox.show()
- self.show()
+ self.dialog.show()
self.enable_incremental_drawing = False
@@ -1414,17 +1793,18 @@ class SpyroWindow(Gtk.Window):
self.engine = DrawingEngine(img, self.p)
# Make a new GIMP layer to draw on
- self.spyro_layer = Gimp.Layer.new(img, layer_name, img.width(), img.height(),
Gimp.ImageType.RGBA_IMAGE, 100, Gimp.LayerMode.NORMAL)
+ self.spyro_layer = Gimp.Layer.new(img, layer_name, img.width(), img.height(),
+ layer.type(), 100, Gimp.LayerMode.NORMAL)
img.insert_layer(self.spyro_layer, None, 0)
-
self.drawing_layer = self.spyro_layer
+ # Create the UI.
Gimp.ui_init(sys.argv[0])
create_ui()
- self.update_view()
+ self.update_view() # Update UI to reflect the parameter values.
- # Obey the window manager quit signal
- self.connect("destroy", self.cancel_window)
+ # Map button responses to callback this method
+ self.dialog.connect('response', self.handle_response)
# Setup for Handling incremental/interactive drawing of pattern
self.idle_task = None
@@ -1433,6 +1813,20 @@ class SpyroWindow(Gtk.Window):
# Draw pattern of the current settings.
self.start_new_incremental_drawing()
+ def handle_response(self, dialog, response_id):
+ if response_id in [Gtk.ResponseType.CANCEL, Gtk.ResponseType.CLOSE, Gtk.ResponseType.DELETE_EVENT]:
+ self.cancel_window(self.dialog)
+ elif response_id == Gtk.ResponseType.OK:
+ self.ok_window(self.dialog)
+ elif response_id == RESPONSE_REDRAW:
+ self.redraw()
+ elif response_id == RESPONSE_RESET_PARAMS:
+ self.reset_params()
+ else:
+ print("Unhandled response: " + str(response_id))
+ #GTK_RESPONSE_APPLY
+ #GTK_RESPONSE_HELP
+
# Callbacks for closing the plugin
def ok_window(self, widget):
@@ -1442,7 +1836,7 @@ class SpyroWindow(Gtk.Window):
shelf_parameters(self.p)
- if self.p.keep_separate_layer:
+ if self.p.save_option == SAVE_AS_NEW_LAYER:
if self.spyro_layer in self.img.list_layers():
self.img.active_layer = self.spyro_layer
@@ -1457,7 +1851,7 @@ class SpyroWindow(Gtk.Window):
yield False
task = quit_dialog_on_completion()
- GObject.idle_add(task.__next__)
+ GLib.idle_add(task.__next__)
else:
Gtk.main_quit()
else:
@@ -1471,7 +1865,7 @@ class SpyroWindow(Gtk.Window):
self.drawing_layer = self.active_layer
- def draw_full():
+ def draw_full(tool):
self.progress_start()
yield True
@@ -1481,7 +1875,7 @@ class SpyroWindow(Gtk.Window):
while self.engine.has_more_strokes():
yield True
- self.draw_next_chunk(undo_group=False)
+ self.draw_next_chunk(undo_group=False, tool=tool)
self.img.undo_group_end()
@@ -1490,8 +1884,10 @@ class SpyroWindow(Gtk.Window):
Gtk.main_quit()
yield False
- task = draw_full()
- GObject.idle_add(task.__next__)
+ tool = SaveToPathTool(self.img) if self.p.save_option == SAVE_AS_PATH else None
+
+ task = draw_full(tool)
+ GLib.idle_add(task.__next__)
def cancel_window(self, widget, what=None):
@@ -1521,6 +1917,14 @@ class SpyroWindow(Gtk.Window):
self.kit_hole_adj.set_value(self.p.hole_number)
self.kit_inner_teeth_combo_side_effects()
+ self.petals_adj.set_value(self.p.petals)
+ self.petal_skip_adj.set_value(self.p.petal_skip)
+ self.doughnut_hole_adj.set_value(self.p.doughnut_hole)
+ self.doughnut.set_hole_radius(self.p.doughnut_hole)
+ self.doughnut_width_adj.set_value(self.p.doughnut_width)
+ self.doughnut.set_width(self.p.doughnut_width)
+ self.petals_changed_side_effects()
+
self.shape_combo.set_active(self.p.shape_index)
self.shape_combo_side_effects()
self.sides_adj.set_value(self.p.sides)
@@ -1531,9 +1935,9 @@ class SpyroWindow(Gtk.Window):
self.margin_adj.set_value(self.p.margin_pixels)
self.tool_combo.set_active(self.p.tool_index)
self.long_gradient_checkbox.set_active(self.p.long_gradient)
- self.keep_separate_layer_checkbox.set_active(self.p.keep_separate_layer)
+ self.save_option_combo.set_active(self.p.save_option)
- def reset_params(self, widget):
+ def reset_params(self, widget=None):
self.engine.p = self.p = PatternParameters()
self.update_view()
@@ -1549,6 +1953,9 @@ class SpyroWindow(Gtk.Window):
self.hole_percent_myscale.set_sensitive(True)
self.kit_hole_myscale.set_sensitive(True)
+
+ self.doughnut_hole_myscale.set_sensitive(True)
+ self.doughnut_width_myscale.set_sensitive(True)
else:
# Lissajous curves do not have shapes, or holes for moving gear
self.shape_combo.set_sensitive(False)
@@ -1560,6 +1967,9 @@ class SpyroWindow(Gtk.Window):
self.hole_percent_myscale.set_sensitive(False)
self.kit_hole_myscale.set_sensitive(False)
+ self.doughnut_hole_myscale.set_sensitive(False)
+ self.doughnut_width_myscale.set_sensitive(False)
+
def curve_type_changed(self, val):
self.p.curve_type = val.get_active()
self.curve_type_side_effects()
@@ -1615,6 +2025,41 @@ class SpyroWindow(Gtk.Window):
self.p.pattern_rotation = val.get_value()
self.redraw()
+ # Callbacks: pattern changes using the Visual notation.
+
+ def petals_changed_side_effects(self):
+ max_petal_skip = int(self.p.petals / 2)
+ if self.p.petal_skip > max_petal_skip:
+ self.p.petal_skip = max_petal_skip
+ self.petal_skip_adj.set_value(max_petal_skip)
+ self.petal_skip_adj.set_upper(max_petal_skip)
+
+ def petals_changed(self, val):
+ self.p.petals = int(val.get_value())
+ self.petals_changed_side_effects()
+ self.redraw()
+
+ def petal_skip_changed(self, val):
+ self.p.petal_skip = int(val.get_value())
+ self.redraw()
+
+ def doughnut_hole_changed(self, val):
+ self.p.doughnut_hole = val.get_value()
+
+ self.doughnut.set_hole_radius(val.get_value())
+ self.redraw()
+
+ def doughnut_width_changed(self, val):
+ self.p.doughnut_width = val.get_value()
+ self.doughnut.set_width(val.get_value())
+ self.redraw()
+
+ def doughnut_changed(self, widget, hole, width):
+ self.doughnut_hole_adj.set_value(hole)
+ self.doughnut_width_adj.set_value(width)
+ # We don't need to redraw, because the callbacks of the doughnut hole and
+ # width spinners will be triggered by the above lines.
+
# Callbacks: Fixed gear
def shape_combo_side_effects(self):
@@ -1662,8 +2107,8 @@ class SpyroWindow(Gtk.Window):
self.p.long_gradient = val.get_active()
self.redraw()
- def keep_separate_layer_checkbox_changed(self, val):
- self.p.keep_separate_layer = self.keep_separate_layer_checkbox.get_active()
+ def save_option_changed(self, val):
+ self.p.save_option = self.save_option_combo.get_active()
# Progress bar of plugin window.
@@ -1686,14 +2131,14 @@ class SpyroWindow(Gtk.Window):
# Incremental drawing.
- def draw_next_chunk(self, undo_group=True):
+ def draw_next_chunk(self, undo_group=True, tool=None):
""" Incremental drawing """
t = time.time()
if undo_group:
self.img.undo_group_start()
- chunk_size = self.engine.draw_next_chunk(self.drawing_layer)
+ chunk_size = self.engine.draw_next_chunk(self.drawing_layer, tool=tool)
if undo_group:
self.img.undo_group_end()
@@ -1746,7 +2191,6 @@ class SpyroWindow(Gtk.Window):
class SpyrogimpPlusPlugin(Gimp.PlugIn):
- plugin_name = "plug-in-spyrogimp"
## Parameters ##
__gproperties__ = {
@@ -1827,14 +2271,14 @@ class SpyrogimpPlusPlugin(Gimp.PlugIn):
self.set_translation_domain("gimp30-python",
Gio.file_new_for_path(Gimp.locale_directory()))
- return [ self.plugin_name ]
+ return [PROC_NAME]
def do_create_procedure(self, name):
- if name == self.plugin_name:
+ if name == PROC_NAME:
procedure = Gimp.ImageProcedure.new(self, name,
Gimp.PDBProcType.PLUGIN,
self.plug_in_spyrogimp, None)
- procedure.set_image_types("*");
+ procedure.set_image_types("*")
procedure.set_documentation (N_("Draw spyrographs using current tool settings and selection."),
"Uses current tool settings to draw Spyrograph patterns. "
"The size and location of the pattern is based on the current
selection.",
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]