Improve button mapping, spring-loaded modes
[mypaint:maxy-experimental.git] / gui / linemode.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2012 by Richard Jones
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8
9 import math
10
11 import pygtkcompat
12 import gtk
13 from gtk import gdk
14 from gettext import gettext as _
15 import gobject
16
17 import canvasevent
18
19 # internal name, displayed name, constant, minimum, default, maximum, tooltip
20 line_mode_settings_list = [
21     ['entry_pressure', _('Entrance Pressure'), False, 0.0001, 0.3, 1.0, _("Stroke entrance pressure for line tools")],
22     ['midpoint_pressure', _('Midpoint Pressure'), False, 0.0001, 1.0, 1.0, _("Mid-Stroke pressure for line tools")],
23     ['exit_pressure', _('Exit Pressure'), False, 0.0001, 0.3, 1.0, _("Stroke exit pressure for line tools")],
24     ['line_head', _('Head'), False, 0.0001, 0.25, 1.0, _("Stroke lead-in end")],
25     ['line_tail', _('Tail'), False, 0.0001, 0.75, 1.0, _("Stroke trail-off beginning")],
26     ]
27
28
29 class LineModeSettings:
30     """Manage GtkAdjustments for tweaking LineMode settings.
31
32     An instance resides in the main application singleton. Changes to the
33     adjustments are reflected into the app preferences.
34
35     """
36
37     def __init__(self, app):
38         """Initializer; initial settings are loaded from the app prefs.
39         """
40
41         self.app = app
42         self.adjustments = {}  #: Dictionary of GtkAdjustments
43         self.observers = []  #: List of callbacks
44         self._idle_srcid = None
45         self._changed_settings = set()
46         for line_list in line_mode_settings_list:
47             cname, name, const, min_, default, max_, tooltip = line_list
48             prefs_key = "linemode.%s" % cname
49             value = float(self.app.preferences.get(prefs_key, default))
50             adj = gtk.Adjustment(value=value, lower=min_, upper=max_,
51                                  step_incr=0.01, page_incr=0.1)
52             adj.connect("value-changed", self._value_changed_cb, prefs_key)
53             self.adjustments[cname] = adj
54
55
56     def _value_changed_cb(self, adj, prefs_key):
57         # Direct GtkAdjustment callback for a single adjustment being changed.
58         value = float(adj.get_value())
59         self.app.preferences[prefs_key] = value
60         self._changed_settings.add(prefs_key)
61         if self._idle_srcid is None:
62             self._idle_srcid = gobject.idle_add(self._values_changed_idle_cb)
63
64
65     def _values_changed_idle_cb(self):
66         # Aggregate, idle-state callback for multiple adjustments being changed
67         # in a single event. Queues redraws, and runs observers. The curve sets
68         # multiple settings at once, and we might as well not queue too many
69         # redraws.
70         if self._idle_srcid is not None:
71             current_mode = self.app.doc.modes.top
72             if isinstance(current_mode, LineModeBase):
73                 # Redraw last_line when settings are adjusted in the adjustment Curve
74                 gobject.idle_add(current_mode.redraw_line_cb)
75             for func in self.observers:
76                 func(self._changed_settings)
77             self._changed_settings = set()
78             self._idle_srcid = None
79         return False
80
81
82 class LineModeBase (canvasevent.SpringLoadedModeMixin, canvasevent.DragMode,
83                     canvasevent.ScrollableModeMixin):
84     """Draws geometric lines.
85
86     """
87
88     ##
89     ## Class configuration.
90     ##
91
92     cursor = gdk.Cursor(gdk.CROSS)
93
94     # FIXME: all of the logic resides in the base class, for historical
95     # reasons, and is decided by line_mode. The differences should be
96     # factored out to the user-facing mode subclasses at some point.
97     line_mode = None
98
99
100     def __init__(self, initial_modifiers=0, **kwds):
101         """Initialize, possibly as a oneshot mode
102
103         :param initial_modifiers: Modifiers which are being held down as this
104                     mode is being constructed and entered.
105         :type initial_modifiers: a GDK modifiers mask.
106
107         Line modes can be initialized with an optional bitmask of keyboard
108         modifiers which are held as the mode is entered/pushed onto the mode
109         stack. If modifiers are held, the mode is a oneshot mode and is popped
110         from the mode stack automatically at the end of the drag. Currently
111         this style is only used by ``SwitchableFreehandMode``. Without
112         modifiers, line modes may be continued, and some subclasses offer
113         additional options for adjusting control points.
114
115         """
116         super(LineModeBase, self).__init__(**kwds)
117         self.app = None
118         self.last_line_data = None
119         self.idle_srcid = None
120         self.initial_modifiers = initial_modifiers
121
122
123     ##
124     ## InteractionMode/DragMode implementation
125     ##
126
127     def enter(self, **kwds):
128         super(LineModeBase, self).enter(**kwds)
129         self.app = self.doc.app
130         display = gdk.display_get_default()
131         screen, x, y, modifiers = display.get_pointer()
132         modifiers &= gtk.accelerator_get_default_mod_mask()
133         self.initial_modifiers = modifiers
134
135
136     def drag_start_cb(self, tdw, event):
137         canvasevent.SpringLoadedModeMixin.drag_start_cb(self, tdw, event)
138         self.start_command(self.initial_modifiers)
139         return super(LineModeBase, self).drag_start_cb(tdw, event)
140
141
142     def drag_update_cb(self, tdw, event, dx, dy):
143         self.update_position(event.x, event.y)
144         if self.idle_srcid is None:
145             self.idle_srcid = gobject.idle_add(self._drag_idle_cb)
146         return super(LineModeBase, self).drag_update_cb(tdw, event, dx, dy)
147
148
149     def drag_stop_cb(self):
150         self.idle_srcid = None
151         self.stop_command()
152         if self.initial_modifiers != 0:
153             self.doc.modes.pop()
154         return super(LineModeBase, self).drag_stop_cb()
155
156
157     def _drag_idle_cb(self):
158         # Updates the on-screen line during drags.
159         if self.idle_srcid is not None:
160             self.idle_srcid = None
161             self.process_line()
162
163
164     ###
165     ### Draw dynamic Line, Curve, or Ellipse
166     ###
167
168     def start_command(self, modifier):
169         # :param modifier: the keyboard modifiers which ere in place
170         #                   when the mode was created
171
172         active_tdw = self.app.doc.tdw.__class__.get_active_tdw()
173         assert active_tdw is not None
174         if active_tdw is self.app.scratchpad_doc.tdw:
175             self.model = self.app.scratchpad_doc.model
176             self.tdw = self.app.scratchpad_doc.tdw
177         else:
178             # unconditionally
179             self.model = self.app.doc.model
180             self.tdw = self.app.doc.tdw
181
182         self.done = False
183         self.model.split_stroke() # split stroke here
184         self.snapshot = self.model.layer.save_snapshot()
185
186         x, y, kbmods = self.local_mouse_state()
187         # ignore the modifier used to start this action (don't make it change the action)
188         self.invert_kbmods = modifier
189         kbmods ^= self.invert_kbmods # invert using bitwise xor
190         ctrl = kbmods & gdk.CONTROL_MASK
191         shift = kbmods & gdk.SHIFT_MASK
192
193         # line_mode is the type of line to be drawn eg. "EllipseMode"
194         self.mode = self.line_mode
195         assert self.mode is not None
196
197         self.undo = False
198         # Ignore slow_tracking. There are some other sttings that interfere
199         # with the workings of the Line Tools, but slowtracking is the main one.
200         self.adj = self.app.brush_adjustment['slow_tracking']
201         self.slow_tracking = self.adj.get_value()
202         self.adj.set_value(0)
203
204         # Throughout this module these conventions are used:
205         # sx, sy = starting point
206         # ex, ey = end point
207         # kx, ky = curve point from last line
208         # lx, ly = last point from InteractionMode update
209         self.sx, self.sy = x, y
210         self.lx, self.ly = x, y
211
212         if self.mode == "EllipseMode":
213             # Rotation angle of ellipse.
214             self.angle = 90
215             # Vector to measure any rotation from. Assigned when ratation begins.
216             self.ellipse_vec = None
217             return
218         # If not Ellipse, command must be Straight Line or Sequence
219         # First check if the user intends to Curve an existing Line
220         if shift:
221             last_line = self.last_line_data
222             last_stroke = self.model.layer.get_last_stroke_info()
223             if last_line is not None:
224                 if last_line[1] == last_stroke:
225                     self.mode = last_line[0]
226                     self.sx, self.sy = last_line[2], last_line[3]
227                     self.ex, self.ey = last_line[4], last_line[5]
228                     if self.mode == "CurveLine2":
229                         length_a = distance(x, y, self.sx, self.sy)
230                         length_b = distance(x, y, self.ex, self.ey)
231                         self.flip = length_a > length_b
232                         if self.flip:
233                             self.kx, self.ky = last_line[6], last_line[7]
234                         else:
235                             self.kx, self.ky = last_line[8], last_line[9]
236                     self.model.undo()
237                     self.snapshot = self.model.layer.save_snapshot()
238                     self.process_line()
239                     return
240
241         if self.mode == "SequenceMode":
242             if not self.tdw.last_painting_pos:
243                 return
244             else:
245                 self.sx, self.sy = self.tdw.last_painting_pos
246
247     def update_position(self, x, y):
248         self.lx, self.ly = self.tdw.display_to_model(x, y)
249
250     def stop_command(self):
251     # End mode
252         self.done = True
253         x, y = self.process_line()
254         self.model.split_stroke()
255         cmd = self.mode
256         self.record_last_stroke(cmd, x, y)
257
258
259     def record_last_stroke(self, cmd, x, y):
260         last_line = None
261         self.tdw.last_painting_pos = x, y # FIXME: should probably not set that from here
262         last_stroke = self.model.layer.get_last_stroke_info()
263         sx, sy = self.sx, self.sy
264
265         if cmd == "CurveLine1":
266             last_line = ["CurveLine2", last_stroke, sx, sy, self.ex, self.ey, x, y, x, y]
267             self.tdw.last_painting_pos = self.ex, self.ey
268
269         if cmd == "CurveLine2":
270             if self.flip:
271                 last_line = [cmd, last_stroke, sx, sy, self.ex, self.ey, self.kx, self.ky, x, y]
272             else:
273                 last_line = [cmd, last_stroke, sx, sy, self.ex, self.ey, x, y, self.kx, self.ky]
274             self.tdw.last_painting_pos = self.ex, self.ey
275
276         if cmd == "StraightMode" or cmd == "SequenceMode":
277             last_line = ["CurveLine1", last_stroke, sx, sy, x, y]
278
279         if cmd == "EllipseMode":
280             last_line = [cmd, last_stroke, sx, sy, x, y, self.angle]
281             self.tdw.last_painting_pos = sx, sy
282
283         self.last_line_data = last_line
284         self.adj.set_value(self.slow_tracking)
285         self.model.brush.reset()
286
287     def local_mouse_state(self, last_update=False):
288         tdw_win = self.tdw.renderer.get_window()
289         if pygtkcompat.USE_GTK3:
290             ptr_win, x, y, kbmods = tdw_win.get_pointer()
291         else:
292             x, y, kbmods = tdw_win.get_pointer()
293         if last_update:
294             return self.lx, self.ly, kbmods
295         x, y = self.tdw.display_to_model(x, y)
296         return x, y, kbmods
297
298     def process_line(self):
299         sx, sy = self.sx, self.sy
300         x, y, kbmods = self.local_mouse_state(last_update=True)
301         kbmods ^= self.invert_kbmods # invert using bitwise xor
302         ctrl = kbmods & gdk.CONTROL_MASK
303         shift = kbmods & gdk.SHIFT_MASK
304
305         if self.mode == "CurveLine1":
306             self.dynamic_curve_1(x, y, sx, sy, self.ex, self.ey)
307
308         elif self.mode == "CurveLine2":
309             ex, ey = self.ex, self.ey
310             kx, ky = self.kx, self.ky
311             if not self.flip:
312                 self.dynamic_curve_2(x, y, sx, sy, ex, ey, kx, ky)
313             else:
314                 self.dynamic_curve_2(kx, ky, sx, sy, ex, ey, x, y)
315
316         elif self.mode == "EllipseMode":
317             constrain = False
318             if ctrl:
319                 x, y = constrain_to_angle(x, y, sx, sy)
320                 constrain = True
321             if shift:
322                 self.ellipse_rotation_angle(x, y, sx, sy, constrain)
323             else:
324                 self.ellipse_vec = None
325             self.dynamic_ellipse(x, y, sx, sy)
326
327         else: # if "StraightMode" or "SequenceMode"
328             if ctrl or shift:
329                 x, y = constrain_to_angle(x, y, sx, sy)
330             self.dynamic_straight_line(x, y, sx, sy)
331         return x, y
332
333     def ellipse_rotation_angle(self, x, y, sx, sy, constrain):
334         x1, y1 = normal(sx, sy, x, y)
335         if self.ellipse_vec is None:
336             self.ellipse_vec = x1, y1
337             self.last_angle = self.angle
338         x2, y2 = self.ellipse_vec
339         px, py = perpendicular(x2, y2)
340         pangle = get_angle(x1, y1, px, py)
341         angle = get_angle(x1, y1, x2, y2)
342         if pangle > 90.0:
343             angle = 360 - angle
344         angle += self.last_angle
345         if constrain:
346             angle = constraint_angle(angle)
347         self.angle = angle
348
349     ###
350     ### Line Functions
351     ###
352
353     # Straight Line
354     def dynamic_straight_line(self, x, y, sx, sy):
355         self.brush_prep(sx, sy)
356         entry_p, midpoint_p, junk, prange2, head, tail = self.line_settings()
357         # Beginning
358         length, nx, ny = length_and_normal(sx, sy, x, y)
359         mx, my = multiply_add(sx, sy, nx, ny, 0.25)
360         self.stroke_to(mx, my, entry_p)
361         # Middle start
362         #length = length/2
363         mx, my = multiply_add(sx, sy, nx, ny, head * length)
364         self.stroke_to(mx, my, midpoint_p)
365         # Middle end
366         mx, my = multiply_add(sx, sy, nx, ny, tail * length)
367         self.stroke_to(mx, my, midpoint_p)
368         # End
369         self.stroke_to(x, y, self.exit_pressure)
370
371     # Ellipse
372     def dynamic_ellipse(self, x, y, sx, sy):
373         points_in_curve = 360
374         x1, y1 = difference(sx, sy, x, y)
375         x1, y1, sin, cos = starting_point_for_ellipse(x1, y1, self.angle)
376         rx, ry = point_in_ellipse(x1, y1, sin, cos, 0)
377         self.brush_prep(sx+rx, sy+ry)
378         entry_p, midpoint_p, prange1, prange2, h, t = self.line_settings()
379         head = points_in_curve * h
380         head_range = int(head)+1
381         tail = points_in_curve * t
382         tail_range = int(tail)+1
383         tail_length = points_in_curve - tail
384         # Beginning
385         px, py = point_in_ellipse(x1, y1, sin, cos, 1)
386         length, nx, ny = length_and_normal(rx, ry, px, py)
387         mx, my = multiply_add(rx, ry, nx, ny, 0.25)
388         self.stroke_to(sx+mx, sy+my, entry_p)
389         pressure = abs(1/head * prange1 + entry_p)
390         self.stroke_to(sx+px, sy+py, pressure)
391         for degree in xrange(2, head_range):
392             px, py = point_in_ellipse(x1, y1, sin, cos, degree)
393             pressure = abs(degree/head * prange1 + entry_p)
394             self.stroke_to(sx+px, sy+py, pressure)
395         # Middle
396         for degree in xrange(head_range, tail_range):
397             px, py = point_in_ellipse(x1, y1, sin, cos, degree)
398             self.stroke_to(sx+px, sy+py, midpoint_p)
399         # End
400         for degree in xrange(tail_range, points_in_curve+1):
401             px, py = point_in_ellipse(x1, y1, sin, cos, degree)
402             pressure = abs((degree-tail)/tail_length * prange2 + midpoint_p)
403             self.stroke_to(sx+px, sy+py, pressure)
404
405     def dynamic_curve_1(self, cx, cy, sx, sy, ex, ey):
406         self.brush_prep(sx, sy)
407         self.draw_curve_1(cx, cy, sx, sy, ex, ey)
408
409     def dynamic_curve_2(self, cx, cy, sx, sy, ex, ey, kx, ky):
410         self.brush_prep(sx, sy)
411         self.draw_curve_2(cx, cy, sx, sy, ex, ey, kx, ky)
412
413     # Curve Straight Line
414     # Found this page helpful:
415     # http://www.caffeineowl.com/graphics/2d/vectorial/bezierintro.html
416     def draw_curve_1(self, cx, cy, sx, sy, ex, ey):
417         points_in_curve = 100
418         entry_p, midpoint_p, prange1, prange2, h, t = self.line_settings()
419         mx, my = midpoint(sx, sy, ex, ey)
420         length, nx, ny = length_and_normal(mx, my, cx, cy)
421         cx, cy = multiply_add(mx, my, nx, ny, length*2)
422         x1, y1 = difference(sx, sy, cx, cy)
423         x2, y2 = difference(cx, cy, ex, ey)
424         head = points_in_curve * h
425         head_range = int(head)+1
426         tail = points_in_curve * t
427         tail_range = int(tail)+1
428         tail_length = points_in_curve - tail
429         # Beginning
430         px, py = point_on_curve_1(1, cx, cy, sx, sy, x1, y1, x2, y2)
431         length, nx, ny = length_and_normal(sx, sy, px, py)
432         bx, by = multiply_add(sx, sy, nx, ny, 0.25)
433         self.stroke_to(bx, by, entry_p)
434         pressure = abs(1/head * prange1 + entry_p)
435         self.stroke_to(px, py, pressure)
436         for i in xrange(2, head_range):
437             px, py = point_on_curve_1(i, cx, cy, sx, sy, x1, y1, x2, y2)
438             pressure = abs(i/head * prange1 + entry_p)
439             self.stroke_to(px, py, pressure)
440         # Middle
441         for i in xrange(head_range, tail_range):
442             px, py = point_on_curve_1(i, cx, cy, sx, sy, x1, y1, x2, y2)
443             self.stroke_to(px, py, midpoint_p)
444         # End
445         for i in xrange(tail_range, points_in_curve+1):
446             px, py = point_on_curve_1(i, cx, cy, sx, sy, x1, y1, x2, y2)
447             pressure = abs((i-tail)/tail_length * prange2 + midpoint_p)
448             self.stroke_to(px, py, pressure)
449
450     def draw_curve_2(self, cx, cy, sx, sy, ex, ey, kx, ky):
451         points_in_curve = 100
452         self.brush_prep(sx, sy)
453         entry_p, midpoint_p, prange1, prange2, h, t = self.line_settings()
454         mx, my = (cx+sx+ex+kx)/4.0, (cy+sy+ey+ky)/4.0
455         length, nx, ny = length_and_normal(mx, my, cx, cy)
456         cx, cy = multiply_add(mx, my, nx, ny, length*2)
457         length, nx, ny = length_and_normal(mx, my, kx, ky)
458         kx, ky = multiply_add(mx, my, nx, ny, length*2)
459         x1, y1 = difference(sx, sy, cx, cy)
460         x2, y2 = difference(cx, cy, kx, ky)
461         x3, y3 = difference(kx, ky, ex, ey)
462         head = points_in_curve * h
463         head_range = int(head)+1
464         tail = points_in_curve * t
465         tail_range = int(tail)+1
466         tail_length = points_in_curve - tail
467         # Beginning
468         px, py = point_on_curve_2(1, cx, cy, sx, sy, kx, ky, x1, y1, x2, y2, x3, y3)
469         length, nx, ny = length_and_normal(sx, sy, px, py)
470         bx, by = multiply_add(sx, sy, nx, ny, 0.25)
471         self.stroke_to(bx, by, entry_p)
472         pressure = abs(1/head * prange1 + entry_p)
473         self.stroke_to(px, py, pressure)
474         for i in xrange(2, head_range):
475             px, py = point_on_curve_2(i, cx, cy, sx, sy, kx, ky, x1, y1, x2, y2, x3, y3)
476             pressure = abs(i/head * prange1 + entry_p)
477             self.stroke_to(px, py, pressure)
478         # Middle
479         for i in xrange(head_range, tail_range):
480             px, py = point_on_curve_2(i, cx, cy, sx, sy, kx, ky, x1, y1, x2, y2, x3, y3)
481             self.stroke_to(px, py, midpoint_p)
482         # End
483         for i in xrange(tail_range, points_in_curve+1):
484             px, py = point_on_curve_2(i, cx, cy, sx, sy, kx, ky, x1, y1, x2, y2, x3, y3)
485             pressure = abs((i-tail)/tail_length * prange2 + midpoint_p)
486             self.stroke_to(px, py, pressure)
487
488     def stroke_to(self, x, y, pressure):
489         duration = 0.001
490         brush = self.model.brush
491         if not self.done:
492             # stroke without setting undo
493             self.model.layer.stroke_to(brush, x, y, pressure, 0.0, 0.0, duration)
494         else:
495             self.model.stroke_to(duration, x, y, pressure, 0.0, 0.0)
496
497     def brush_prep(self, sx, sy):
498         # Send brush to where the stroke will begin
499         self.model.brush.reset()
500         brush = self.model.brush
501         self.model.layer.stroke_to(brush, sx, sy, 0.0, 0.0, 0.0, 10.0)
502         self.model.layer.load_snapshot(self.snapshot)
503
504
505     ##
506     ## Line mode settings
507     ##
508
509     @property
510     def entry_pressure(self):
511         adj = self.app.line_mode_settings.adjustments["entry_pressure"]
512         return adj.get_value()
513
514     @property
515     def midpoint_pressure(self):
516         adj = self.app.line_mode_settings.adjustments["midpoint_pressure"]
517         return adj.get_value()
518
519     @property
520     def exit_pressure(self):
521         adj = self.app.line_mode_settings.adjustments["exit_pressure"]
522         return adj.get_value()
523
524     @property
525     def head(self):
526         adj = self.app.line_mode_settings.adjustments["line_head"]
527         return adj.get_value()
528
529     @property
530     def tail(self):
531         adj = self.app.line_mode_settings.adjustments["line_tail"]
532         return adj.get_value()
533
534
535     def line_settings(self):
536         p1 = self.entry_pressure
537         p2 = self.midpoint_pressure
538         p3 = self.exit_pressure
539         if self.head == 0.0001:
540             p1 = p2
541         prange1 = p2 - p1
542         prange2 = p3 - p2
543         return p1, p2, prange1, prange2, self.head, self.tail
544
545
546     def redraw_line_cb(self):
547         # Redraws the line when the line_mode_settings change
548         last_line = self.last_line_data
549         if last_line is not None:
550             last_stroke = self.model.layer.get_last_stroke_info()
551             if last_line[1] is last_stroke:
552                 # ignore slow_tracking
553                 self.done = True
554                 self.adj = self.app.brush_adjustment['slow_tracking']
555                 self.slow_tracking = self.adj.get_value()
556                 self.adj.set_value(0)
557                 self.model.undo()
558                 command = last_line[0]
559                 self.sx, self.sy = last_line[2], last_line[3]
560                 self.ex, self.ey = last_line[4], last_line[5]
561                 x, y = self.ex, self.ey
562                 if command == "EllipseMode":
563                     self.angle = last_line[6]
564                     self.dynamic_ellipse(self.ex, self.ey,
565                                          self.sx, self.sy)
566                 if command == "CurveLine1":
567                     self.dynamic_straight_line(self.ex, self.ey,
568                                                self.sx, self.sy)
569                     command = "StraightMode"
570                 if command == "CurveLine2":
571                     x, y = last_line[6], last_line[7]
572                     self.kx, self.ky = last_line[8], last_line[9]
573                     if (x, y) == (self.kx, self.ky):
574                         self.dynamic_curve_1(x, y, self.sx, self.sy,
575                                              self.ex, self.ey)
576                         command = "CurveLine1"
577                     else:
578                         self.flip = False
579                         self.dynamic_curve_2(x, y, self.sx, self.sy,
580                                              self.ex, self.ey,
581                                              self.kx, self.ky)
582                 self.model.split_stroke()
583                 self.record_last_stroke(command, x, y)
584
585
586
587 ## User-facing modes
588
589
590 class StraightMode (LineModeBase):
591     __action_name__ = "StraightMode"
592     line_mode = "StraightMode"
593
594
595 class SequenceMode (LineModeBase):
596     __action_name__ = "SequenceMode"
597     line_mode = "SequenceMode"
598
599
600 class EllipseMode (LineModeBase):
601     __action_name__ = "EllipseMode"
602     line_mode = "EllipseMode"
603
604
605
606 ### Curve Math
607 def point_on_curve_1(t, cx, cy, sx, sy, x1, y1, x2, y2):
608     ratio = t/100.0
609     x3, y3 = multiply_add(sx, sy, x1, y1, ratio)
610     x4, y4 = multiply_add(cx, cy, x2, y2, ratio)
611     x5, y5 = difference(x3, y3, x4, y4)
612     x, y = multiply_add(x3, y3, x5, y5, ratio)
613     return x, y
614
615 def point_on_curve_2(t, cx, cy, sx, sy, kx, ky, x1, y1, x2, y2, x3, y3):
616     ratio = t/100.0
617     x4, y4 = multiply_add(sx, sy, x1, y1, ratio)
618     x5, y5 = multiply_add(cx, cy, x2, y2, ratio)
619     x6, y6 = multiply_add(kx, ky, x3, y3, ratio)
620     x1, y1 = difference(x4, y4, x5, y5)
621     x2, y2 = difference(x5, y5, x6, y6)
622     x4, y4 = multiply_add(x4, y4, x1, y1, ratio)
623     x5, y5 = multiply_add(x5, y5, x2, y2, ratio)
624     x1, y1 = difference(x4, y4, x5, y5)
625     x, y = multiply_add(x4, y4, x1, y1, ratio)
626     return x, y
627
628
629 ### Ellipse Math
630 def starting_point_for_ellipse(x, y, rotate):
631     # Rotate starting point
632     r = math.radians(rotate)
633     sin = math.sin(r)
634     cos = math.cos(r)
635     x, y = rotate_ellipse(x, y, cos, sin)
636     return x, y, sin, cos
637
638 def point_in_ellipse(x, y, r_sin, r_cos, degree):
639     # Find point in ellipse
640     r2 = math.radians(degree)
641     cos = math.cos(r2)
642     sin = math.sin(r2)
643     x = x * cos
644     y = y * sin
645     # Rotate Ellipse
646     x, y = rotate_ellipse(y, x, r_sin, r_cos)
647     return x, y
648
649 def rotate_ellipse(x, y, sin, cos):
650     x1, y1 = multiply(x, y, sin)
651     x2, y2 = multiply(x, y, cos)
652     x = x2 - y1
653     y = y2 + x1
654     return x, y
655
656
657 ### Vector Math
658 def get_angle(x1, y1, x2, y2):
659     dot = dot_product(x1, y1, x2, y2)
660     if abs(dot) < 1.0:
661         angle = math.acos(dot) * 180/math.pi
662     else:
663         angle = 0.0
664     return angle
665
666 def constrain_to_angle(x, y, sx, sy):
667     length, nx, ny = length_and_normal(sx, sy, x, y)
668     # dot = nx*1 + ny*0 therefore nx
669     angle = math.acos(nx) * 180/math.pi
670     angle = constraint_angle(angle)
671     ax, ay = angle_normal(ny, angle)
672     x = sx + ax*length
673     y = sy + ay*length
674     return x, y
675
676 def constraint_angle(angle):
677     n = angle//15
678     n1 = n*15
679     rem = angle - n1
680     if rem < 7.5:
681         angle = n*15.0
682     else:
683         angle = (n+1)*15.0
684     return angle
685
686 def angle_normal(ny, angle):
687     if ny < 0.0:
688         angle = 360.0 - angle
689     radians = math.radians(angle)
690     x = math.cos(radians)
691     y = math.sin(radians)
692     return x, y
693
694 def length_and_normal(x1, y1, x2, y2):
695     x, y = difference(x1, y1, x2, y2)
696     length = vector_length(x, y)
697     if length == 0.0:
698         x, y = 0.0, 0.0
699     else:
700         x, y = x/length, y/length
701     return length, x, y
702
703 def normal(x1, y1, x2, y2):
704     junk, x, y = length_and_normal(x1, y1, x2, y2)
705     return x, y
706
707 def vector_length(x, y):
708     length = math.sqrt(x*x + y*y)
709     return length
710
711 def distance(x1, y1, x2, y2):
712     x, y = difference(x1, y1, x2, y2)
713     length = vector_length(x, y)
714     return length
715
716 def dot_product(x1, y1, x2, y2):
717     return x1*x2 + y1*y2
718
719 def multiply_add(x1, y1, x2, y2, d):
720     x3, y3 = multiply(x2, y2, d)
721     x, y = add(x1, y1, x3, y3)
722     return x, y
723
724 def multiply(x, y, d):
725     # Multiply vector
726     x = x*d
727     y = y*d
728     return x, y
729
730 def add(x1, y1, x2, y2):
731     # Add vectors
732     x = x1+x2
733     y = y1+y2
734     return x, y
735
736 def difference(x1, y1, x2, y2):
737     # Difference in x and y between two points
738     x = x2-x1
739     y = y2-y1
740     return x, y
741
742 def midpoint(x1, y1, x2, y2):
743     # Midpoint between to points
744     x = (x1+x2)/2.0
745     y = (y1+y2)/2.0
746     return x, y
747
748 def perpendicular(x1, y1):
749     # Swap x and y, then flip one sign to give vector at 90 degree
750     x = -y1
751     y = x1
752     return x, y