Fix importing the publisher class
[PoPiPaint.git] / CanvasView.py
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2014  Antonio Ospite <ao2@ao2.it>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 import wx
19
20 from math import *
21
22 import CanvasModel
23
24
25 # Polar coordinate system:
26 # http://en.wikipedia.org/wiki/Polar_coordinate_system
27 # (r, theta)
28
29 def cartesian2polar(x, y, offset_angle=0):
30     """return the polar coordinates relative to a circle
31     centered in (0,0) going counterclockwise.
32
33     returned angle is in degrees
34     """
35     r = sqrt(x*x + y*y)
36     theta = atan2(float(y), float(x)) + offset_angle
37
38     # report theta in the [0,360) range
39     theta = degrees(theta)
40     if theta < 0:
41         theta = 360 + theta
42
43     return r, theta
44
45
46 def polar2cartesian(r, theta, offset_angle=0):
47     """
48     theta expected in degrees
49     """
50     theta = radians(theta) - offset_angle
51     x = r * cos(theta)
52     y = r * sin(theta)
53
54     return x, y
55
56
57 class CanvasView(wx.Window):
58     def __init__(self, *args, **kwargs):
59         self.model = kwargs.pop('model')
60         base_image = kwargs.pop('base_image', None)
61         wx.Window.__init__(self, *args, **kwargs)
62
63         # This eliminates flickering on Windows
64         self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
65
66         w = h = int(self.model.external_radius * 2)
67
68         self.offset_angle = -pi/2.
69
70         self.SetSize((w, h))
71
72         self.SetFocus()
73
74         self.Bind(wx.EVT_PAINT, self.OnPaint)
75
76         # make a DC to draw into...
77         self.buffer = wx.EmptyBitmap(w, h)
78         self.dc = wx.BufferedDC(None, self.buffer)
79
80         # Because we want the position of the mouse pointer
81         # relative to the center of the canvas
82         self.dc.SetDeviceOrigin(w/2., h/2.)
83
84         self.draw_grid = True
85
86         if base_image:
87             self.loadImage(base_image)
88
89         # FIXME: fix the file path.
90         #
91         # Actually it'd be even better to make the grid drawn to a MemoryDC with
92         # drawGrid(), but this is not working with python-wxgtk2.8 because there
93         # are bugs regarding bitmaps with alpha channels and MemoryDC
94         self.grid_buffer = self.loadGrid("res/grid.png")
95
96         self.pixels_buffer = wx.EmptyBitmap(w, h, depth=32)
97         self.drawAllPixels()
98
99     def setPixelCoordinates(self, gc):
100         w, h = self.GetSize()
101         gc.Translate(w/2., h/2.)
102         gc.Rotate(-self.offset_angle)
103
104     def MakePixelsGC(self, dc_buffer):
105         dc = wx.MemoryDC(dc_buffer)
106         gc = wx.GraphicsContext.Create(dc)
107         self.setPixelCoordinates(gc)
108         return gc
109
110     def loadGrid(self, file_path):
111         image = wx.Image(file_path)
112
113         im_w, im_h = image.GetSize()
114         w, h = self.GetSize()
115         if im_w != w or im_h != h:
116             return None
117
118         return wx.BitmapFromImage(image)
119
120     def loadImage(self, file_path):
121         image = wx.Image(file_path)
122
123         w, h = image.GetSize()
124         if w != self.model.width or h != self.model.height:
125             return False
126
127         bitmap = wx.BitmapFromImage(image)
128         bitmap.CopyToBuffer(self.model.pixels_array)
129         return True
130
131     def drawGrid(self, gc):
132         pen_size = 1
133         gc.SetPen(wx.Pen('#555555', pen_size))
134         gc.SetBrush(wx.Brush(wx.BLACK, wx.TRANSPARENT))
135
136         for i in range(0, self.model.height):
137             r, theta = self.model.toPolar(0, i)
138             path = gc.CreatePath()
139             path.AddCircle(0, 0, r)
140             gc.DrawPath(path)
141
142         # draw the outmost circle
143         r, theta = self.model.toPolar(0, self.model.height)
144         path = gc.CreatePath()
145         path.AddCircle(0, 0, r - pen_size)
146         gc.DrawPath(path)
147
148         min_r = self.model.internal_radius
149         max_r = self.model.external_radius - pen_size
150         for i in range(0, self.model.width):
151             r, theta = self.model.toPolar(i, 0)
152             x1, y1 = polar2cartesian(min_r, theta)
153             x2, y2 = polar2cartesian(max_r, theta)
154
155             path = gc.CreatePath()
156             path.MoveToPoint(x1, y1)
157             path.AddLineToPoint(x2, y2)
158             gc.DrawPath(path)
159
160     def _drawPixel(self, gc, x, y, color=None):
161         pen_size = 1
162         if color:
163             gc.SetPen(wx.Pen(color, pen_size))
164             gc.SetBrush(wx.Brush(color))
165         else:
166             gc.SetPen(wx.Pen(wx.RED, pen_size))
167             gc.SetBrush(wx.Brush(wx.BLACK, wx.TRANSPARENT))
168
169         min_r, theta1 = self.model.toPolar(x, y)
170
171         max_r = min_r + self.model.ring_width
172         
173         # prevent the outmost circle to overflow the canvas
174         if y == self.model.height - 1:
175             max_r -= pen_size
176
177         theta2 = theta1 + self.model.sector_width
178
179         # Draw the circular arc
180         path = gc.CreatePath()
181         path.AddArc(0, 0, min_r, radians(theta1), radians(theta2), True)
182         path.AddLineToPoint(polar2cartesian(max_r, theta2))
183         path.AddArc(0, 0, max_r, radians(theta2), radians(theta1), False)
184         path.AddLineToPoint(polar2cartesian(min_r, theta1))
185         path.CloseSubpath()
186         gc.DrawPath(path)
187
188     def drawAllPixels(self):
189         gc = self.MakePixelsGC(self.pixels_buffer)
190         for y in range(0, self.model.height):
191             for x in range(0, self.model.width):
192                 color = self.model.getPixelColor(x, y)
193                 self._drawPixel(gc, x, y, color=color)
194
195     def drawPixel(self, pixel):
196         if not pixel:
197             return
198
199         x, y = pixel
200
201         gc = self.MakePixelsGC(self.pixels_buffer)
202         color = self.model.getPixelColor(x, y)
203         self._drawPixel(gc, x, y, color)
204
205     def drawPixelMarker(self, gc, pixel):
206         if not pixel:
207             return
208
209         x, y = pixel
210         self._drawPixel(gc, x, y)
211
212     def drawImage(self, gc):
213         gc.SetPen(wx.Pen('#CCCCCC', 1))
214         gc.SetBrush(wx.Brush(wx.BLACK, wx.TRANSPARENT))
215
216         w, h = self.GetSize()
217         gc.DrawRectangle(w - self.model.width, 0,
218                          self.model.width, self.model.height)
219
220         bitmap = wx.BitmapFromBuffer(self.model.width, self.model.height,
221                                      self.model.pixels_array)
222         gc.DrawBitmap(bitmap, w - self.model.width, 0,
223                       self.model.width, self.model.height)
224
225     def drawTag(self, gc):
226         font = wx.Font(12, wx.FONTFAMILY_MODERN, wx.FONTSTYLE_NORMAL,
227                        wx.FONTWEIGHT_BOLD)
228         gc.SetFont(font, "#0099FF")
229         w, h = self.GetSize()
230         text = "ao2.it"
231         twidth, theight, descent, externalLeading = gc.GetFullTextExtent(text)
232         gc.DrawText(text, w - twidth - 4, h - theight - 2)
233
234     def OnPaint(self, event):
235         # make a DC to draw into...
236         if 'wxMSW' in wx.PlatformInfo:
237             dc = wx.BufferedPaintDC(self)
238         else:
239             dc = wx.PaintDC(self)
240
241         dc.SetBackground(wx.Brush(wx.BLACK))
242         dc.Clear()
243
244         gc = wx.GraphicsContext.Create(dc)
245         w, h = self.GetSize()
246
247         gc.DrawBitmap(self.pixels_buffer, 0, 0, w, h)
248
249         if self.draw_grid:
250             gc.DrawBitmap(self.grid_buffer, 0, 0, w, h)
251
252         gc.PushState()
253         self.setPixelCoordinates(gc)
254         self.drawPixelMarker(gc, self.model.last_pixel)
255         gc.PopState()
256
257         self.drawImage(gc)
258         self.drawTag(gc)