Programarq: Funcionalidad Cad. Equidistancia en Python y GTK3

Llevo algunos años programando y desde hace casi dos estoy desarrollando un proyecto en el que se maneja hardware y software. El lenguaje de esta última parte es Python 3.
 
Algunas de las funcionalidades que voy diseñando pueden ser interesantes y aquí comparto esta en la que dadas unas coordenadas de una polilinea, la dibuja y traza para una distancia dada su equidistante.
 
La explicación de esta equidistancia u offset (en blender hay algo parecido llamado inset) es la siguiente:
1. En primer lugar es necesario dividir las polilineas en cerradas o abiertas, ya que la manera de encontrar los puntos variará. Siendo la lista de coordenadas de la polilinea polyline.coords:
  1. if polyline.coords[0] == polyline.coords[max_index]:
  2. ...
  3. else:
  4. ...

 

2. En el primer caso la polilinea esta cerrada.
2a. Se haya los dos vectores que corresponden a los segmentos anterior y posterior al vértice.
2b. Se haya el angulo de la bisectiz entre los dos vectores.
2c. Se haya la distancia a colocar en la dirección de la bisectriz. Esa distancia se haya por trigonometría:
2d. Esa distancia se multiplica por el versor (vector unitario) de la bisectiz y se obtiene el bisector:
  1. vector_pos = Vector (self.coords[x], self.coords[x+1]).get_versor()
  2. vector_pre = Vector (self.coords[x], self.coords[x-2]).get_versor()
  3. bisector_angle = (vector_pos.get_angle_vectors(vector_pre)) / 2
  4. dist_bisector = offset_dist / math.sin(math.radians(bisector_angle))
  5. bisector = (vector_pos + vector_pre).get_versor() * dist_bisector
2e. Se suma el vector bisector al propio vértice (punto) y el resultado es el nuevo punto equidistante:
new_point = point_x.plus_vector(bisector)
2f. Existen dos casos particulares:
a. El primer vértice debe considerar el penúltimo punto de la lista de coordenadas, ya que el último es el mismo que el primero (polilinea cerrada) y por tanto daría un vector 0.
b. El último vértice no debe ser calculado, ya que si es una polilinea cerrada, el último vértice de la equidistancia será el primer vértice calculado.
 
3. En el caso de que la polilinea sea abierta cambia la manera de hayar los puntos particulares descritos en el anterior punto, los vértices primero y último. En este caso no se puede tomar la bisectriz del ángulo entre las dos aristas adyacentes, ya que sólo existe una. En este caso se los vértices equidistantes se colocarán en la perpendicular.
3b. Además en el primer vértice, para conseguir que el primer punto equidistante este del mismo lado que el resto, se calcula que el vector perpendicular y el siguiente vector bisector no difieran en más de 90º, en cuyo caso, se invierte el primero.
3b. Además en el último vértice, y para asegurar la equidistancia se compara la nueva arista equidistante con la arista última de la polilinea existente. Si no son paralelas, se tomará el vector perpendicular inverso para obtener el último punto equidistante.
 
Aquí dejo el código que carga una ventana para introducir coordenadas en modo lista y las dibuja en el canvas. Después se puede introducir una distancia para la equidistancia (positiva o negativa). Adjunto el archivo comprimido con el código, la Gui y la hoja de estilo al final de la página. Para que funcione, además de tener python 3 instalado es necesario también tener PyGobject. Código:
  1. '''
  2. Created on 17/10/2014
  3.  
  4. @author: fer
  5. http://www.asimply.com
  6. '''
  7.  
  8. import sys
  9. import os
  10.  
  11. from gi.repository import Gtk, GdkPixbuf, Gdk
  12. import cairo
  13. import math
  14.  
  15. # Variable for decimal accurancy
  16. precision = 4
  17.  
  18. # Style for the GUI
  19. screen = Gdk.Screen.get_default()
  20. css_provider = Gtk.CssProvider()
  21. css_provider.load_from_path('style.css')
  22. context = Gtk.StyleContext()
  23. context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
  24.  
  25. # Canvas class
  26. class Canvas(Gtk.DrawingArea):
  27. # The canvas
  28. def __init__(self):
  29. Gtk.DrawingArea.__init__(self)
  30.  
  31. # Create buffer
  32. self.double_buffer = None
  33. self.scale = 1
  34.  
  35. self.connect("draw", self.on_draw)
  36. self.connect("configure-event", self.on_configure_event)
  37.  
  38. self.show()
  39.  
  40.  
  41. def on_draw(self, widget, cr):
  42. #Throw double buffer into widget drawable
  43.  
  44. if self.double_buffer is not None:
  45. cr.set_source_surface(self.double_buffer, 0.0, 0.0)
  46. cr.paint()
  47. else:
  48. print('Invalid double buffer')
  49.  
  50. return False
  51.  
  52. def on_configure_event(self, widget, event, data=None):
  53. self.on_configure()
  54.  
  55. def on_configure(self):
  56. # Destroy previous buffer
  57. if self.double_buffer is not None:
  58. self.double_buffer.finish()
  59. self.double_buffer = None
  60.  
  61. # Create a new buffer
  62. self.double_buffer = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.get_allocated_width(), self.get_allocated_height())
  63.  
  64. db = self.double_buffer
  65. if db is not None:
  66. # Create cairo context with double buffer as is DESTINATION
  67. self.cairo_ctx = cairo.Context(db)
  68.  
  69. else:
  70. print('Invalid double buffer')
  71.  
  72. return False
  73.  
  74.  
  75. # Deletes the buffer and the context
  76. def empty_canvas(self):
  77. #Configure the double buffer based on size of the widget
  78. # Destroy previous buffer
  79. if self.double_buffer is not None:
  80. self.double_buffer.finish()
  81. self.double_buffer = None
  82. self.cairo_ctx = None
  83.  
  84. #self.queue_draw()
  85. return
  86.  
  87. # Draws a polyline in the canvas from a coordinate list
  88. def draw_polyline(self, coordinates_list, line_width = 1, color_line = (0,1,1)):
  89.  
  90. for coordinate in coordinates_list:
  91. self.cairo_ctx.line_to(coordinate[0], coordinate[1])
  92.  
  93. self.cairo_ctx.set_source_rgb(color_line[0], color_line[1], color_line[2])
  94. self.cairo_ctx.set_line_width(line_width)
  95.  
  96. self.cairo_ctx.stroke()
  97.  
  98. self.double_buffer.flush()
  99.  
  100. def draw_space (self, entity):
  101. #if entity.has_child() and (entity.type == "Space" or entity.type == "Polyline"):
  102. if entity.type == "Space" or entity.type == "Polyline":
  103. for entity_inside in entity.list_entities:
  104. if entity_inside.type == "Polyline":
  105. self.draw_polyline(entity_inside.coords)
  106. if entity_inside.has_child():
  107. self.draw_space(entity_inside)
  108.  
  109. def OnDraw(self, widget, cr):
  110.  
  111. if self.double_buffer is not None:
  112. cr.set_source_surface(self.double_buffer, 0.0, 0.0)
  113. cr.paint()
  114. else:
  115. print('Invalid double buffer')
  116.  
  117. return False
  118.  
  119.  
  120. # Main GUI
  121. class WindowGui(Gtk.ApplicationWindow):
  122. # a window
  123. def __init__(self, app):
  124. Gtk.Window.__init__(self, title="StatusBar", application=app)
  125. #self.set_default_size(200, 100)
  126.  
  127. self.gladefile = "canvas_offset_web.glade"
  128. self.builder = Gtk.Builder()
  129. self.builder.add_from_file(self.gladefile)
  130. self.builder.connect_signals(self)
  131. #self.builder.connect_signals({ "on_window_destroy" : Gtk.main_quit })
  132. self.window = self.builder.get_object("applicationwindow1")
  133. self.frame_canvas = self.builder.get_object("box1_canvas")
  134. self.canvas = Canvas()
  135. self.frame_canvas.pack_start(self.canvas, True, True, 0)
  136.  
  137. self.window.show()
  138.  
  139. """
  140. def on_draw(self, widget):
  141. self.canvas.draw_space(new_space)
  142. self.canvas.queue_draw()
  143. """
  144.  
  145. def getLabelValue(self, labelGui):
  146. self.label = self.builder.get_object(labelGui)
  147. return self.label.get_text()
  148.  
  149. def putMessageLabel(self, labelGui, message):
  150. self.label = self.builder.get_object(labelGui)
  151. self.label.set_text(str(message))
  152.  
  153. def get_values_in_list(self, string):
  154. coords = []
  155. for i in string:
  156.  
  157. if i == "[" or i == "(":
  158. coord = []
  159. elif i == "]" or i == ")":
  160. coord = []
  161. elif i == 0:
  162. pass
  163.  
  164. coord.append
  165.  
  166. list.append(int(string[i]))
  167.  
  168. def on_draw(self, widget):
  169. rect_coords = [[10,10,0], [50,10,0], [50,50,0], [10,50,0] , [10,10,0]] # [10,10,0], [50,10,0], [50,50,0], [10,50,0] , [10,10,0]
  170. #polyline2 = new_space.create_entity("Polyline", rect_coords_open)
  171.  
  172. self.space = Space()
  173.  
  174. try:
  175. value = self.getLabelValue("coords_list")
  176. # Deletes the first "[" and last "]" with: value[1:len(value)-1]
  177. #value = value[0:len(value)]
  178. # Removes whitespaces
  179. value = value.replace(' ', '')
  180. # Get the string between '],['
  181. list = [s for s in (value[1:len(value)-1]).split('],[')]
  182. coords = []
  183. for item in list:
  184. coord = [int (s) for s in item.split(",")]
  185. coords.append (coord)
  186. self.space.create_entity("Polyline", coords)
  187. self.canvas.draw_space(self.space)
  188. self.canvas.queue_draw()
  189. except:
  190. error_message = "Incorrect coordinates list: " + str(value)
  191. print (error_message)
  192. self.putMessageLabel("coords_list", error_message)
  193. return
  194.  
  195.  
  196. def on_draw_clear(self, widget):
  197. self.canvas.on_configure()
  198. self.canvas.queue_draw()
  199.  
  200. def on_offset(self, widget):
  201. try:
  202. offset_distance = float (self.getLabelValue("offset_dist"))
  203. for polyline in self.space.list_entities:
  204. new_polyline = self.space.get_offset(polyline, offset_distance)
  205. self.canvas.draw_polyline(new_polyline.coords, line_width = 1, color_line= (1,0,0))
  206. self.canvas.queue_draw()
  207. except:
  208. error_message = "Incorrect offset distance: " + str(self.getLabelValue("offset_dist"))
  209. print (error_message)
  210. self.putMessageLabel("offset_dist", error_message)
  211.  
  212.  
  213. # Main Class with the basic entity properties
  214. class Entity (object):
  215. def __init__(self):
  216. self.type = ""
  217. self.list_entities = []
  218.  
  219. def has_child(self):
  220. if self.list_entities == []:
  221. return False
  222. else:
  223. return True
  224.  
  225. def create_entity(self, type, *args):
  226. if type == "Point" or type == "point":
  227. new_entity = Point (*args) # args[0], args[1])
  228. elif type == "Vector":
  229. new_entity = Vector (args[0], args[1])
  230. elif type == "Polyline":
  231. new_entity = Polyline (args[0])
  232. else:
  233. print ("Unknown type")
  234. return
  235. self.list_entities.append(new_entity)
  236. return new_entity
  237.  
  238. def print_all_entities(self):
  239. for entity in self.list_entities:
  240. print (entity.type, " ", entity, "Has child?: ", entity.has_child())
  241. if entity.has_child():
  242. entity.print_all_entities()
  243.  
  244.  
  245. # Point Class
  246. class Point (Entity):
  247. def __init__(self, x = 0, y = 0, z = 0):
  248. Entity.__init__(self)
  249. self.type = "Point"
  250. self.x = x
  251. self.y = y
  252. self.z = z
  253. self.coords = [x, y, z]
  254.  
  255. def __str__(self):
  256. return str(self.coords)
  257.  
  258. def __add__(self, point):
  259. return Point(self.x + point.x, self.y + point.y) #, self.z + point.z)
  260.  
  261. def __sub__(self, point):
  262. return Point(self.x-point.x, self.y-point.y) #, self.z-point.z)
  263.  
  264. # Vector Class
  265. class Vector(Entity):
  266. def __init__(self, value1, value2):
  267. Entity.__init__(self)
  268. self.type = "Vector"
  269. #### Two points are given
  270. if type(value1).__name__ == "Point" and type(value2).__name__ == "Point":
  271. self.x = value2.x - value1.x
  272. self.y = value2.y - value1.y
  273. #### Two list of coordinates are given
  274. elif isinstance(value1, list) and isinstance(value2, list):
  275. self.x = value2[0] - value1[0]
  276. self.y = value2[1] - value1[1]
  277. #### Two coordinates are given
  278. elif (isinstance(value1, float) or isinstance(value1, int)) and (isinstance(value2, float) or isinstance(value2, int)):
  279. self.x = value1
  280. self.y = value2
  281. #### The given parameters aren't correct
  282. else:
  283. print ("The given parameters don't fit with requirements. Vector not created.")
  284. return None
  285.  
  286. self.get_module()
  287.  
  288. def __str__(self):
  289. return str([self.x, self.y])
  290.  
  291. def __add__(self, vector):
  292. return Vector(self.x+vector.x, self.y+vector.y)
  293.  
  294. def __sub__(self, vector):
  295. return Vector(self.x-vector.x, self.y-vector.y)
  296.  
  297. def __mul__(self, scalar):
  298. return Vector(self.x*scalar, self.y*scalar)
  299.  
  300. # Producto escalar, interno o punto entre vectores
  301. def mul_point (self, vector):
  302. return self.x * vector.x + self.y * vector.y
  303.  
  304. def get_slope (self):
  305. if self.x == 0 and self.y > 0:
  306. self.slope = float("inf")
  307. elif self.x == 0 and self.y < 0:
  308. self.slope = float("-inf")
  309. else:
  310. self.slope = round((self.y / self.x), precision)
  311. return self.slope
  312.  
  313. def get_from_coordinates(self, x1, y1, x2, y2):
  314. self.x = x2 - x1
  315. self.y = y2 - y1
  316.  
  317. def get_from_two_points(self, point1, point2):
  318. self.x = point2[0] - point1[0]
  319. self.y = point2[1] - point1[1]
  320.  
  321. def get_module (self):
  322. self.module = math.sqrt(self.x**2 + self.y**2)
  323.  
  324. def get_versor (self): # vector with the same direction but module equals to 1
  325. if self.module != 0:
  326. vx = self.x / self.module
  327. vy = self.y / self.module
  328. return Vector(vx, vy)
  329. else:
  330. print ("The module of the vector is equals to zero")
  331. return None
  332.  
  333. def get_perpendicular_vector(self):
  334. x = -self.y
  335. y = self.x
  336. perp_vector = Vector(x, y)
  337. return perp_vector
  338.  
  339. def get_reversed (self):
  340. return Vector(-self.x, -self.y)
  341.  
  342. # Get distance from vector to point
  343. def get_distance_to_point(self, point):
  344. # No es correcto, no funciona para vectores libres.
  345. vector_3 = Vector(Point (self.x, self.y))
  346. x_scalar = self.mul_point(vector_3)
  347. vector_proj = Vector(self.get_versor() * x_scalar)
  348. # Da el vector paralelo de dimensión la proyección
  349. return vector_proj
  350.  
  351. def get_angle_vectors (self, vector):
  352. cos_angle = self.mul_point(vector) / (self.module * vector.module)
  353. return math.degrees(math.acos(cos_angle))
  354.  
  355. class Polyline (Entity):
  356. def __init__(self, coords = []):
  357. Entity.__init__(self)
  358. self.type = "Polyline"
  359.  
  360. if coords == []:
  361. self.coords = coords
  362. return
  363. else:
  364. #### A list of Points are given
  365. if type(coords[0]).__name__ == "Point":
  366. self.coords = []
  367. for point in coords:
  368. self.list_entities.append(point)
  369. self.coords.append(point.coords)
  370. #### A list of coords are given
  371. elif isinstance(coords[0], list):
  372. for coord in coords:
  373. self.create_entity("Point", coord[0], coords[1], coords[2])
  374. self.coords = coords
  375. #### The given parameters aren't correct
  376. else:
  377. print ("The given parameters don't fit with requirements. Polyline not created.")
  378. return None
  379.  
  380. self.coords = coords
  381.  
  382. def add_point_to_coords (self, point):
  383. self.coords.append(point.coords)
  384.  
  385. def coords_update(self):
  386. self.coords = []
  387. for point in self.list_entities:
  388. self.coords.append(point.coords)
  389.  
  390.  
  391. # The Class for the entity Space where all the entities has sense
  392. class Space (Entity):
  393. def __init__(self):
  394. Entity.__init__(self)
  395. self.type = "Space"
  396. self.list_points = []
  397. self.list_vectors = []
  398. self.list_lines = []
  399. self.list_polylines = []
  400. self.entities = []
  401.  
  402. def get_perpendicular_vector(self, vector):
  403. x = -vector.y
  404. y = vector.x
  405. perp_vector = Vector(x, y)
  406. return perp_vector
  407.  
  408. def get_point_plus_vector (self, point, vector):
  409. newpoint_x = point.x + vector.x
  410. newpoint_y = point.y + vector.y
  411. return Point (newpoint_x, newpoint_x)
  412.  
  413. def dist_point_line(self, point, line):
  414. if line.slope == float("inf"):
  415. print ("recta paralela a ordenadas")
  416. distance = point.x
  417. else:
  418. distance = ((line.slope * point.x) - point.y + line.y_intersection) / (((line.slope)**2+1)**(1/2))
  419. return distance
  420.  
  421. ##HALLAR LA INTERSECCION ENTRE RECTAS
  422. def lines_intersection (self, line1, line2):
  423. delta_slope = line1.slope - line2.slope
  424. #if (line1.slope - line2.slope == 0 and line1.slope != line2.slope and line1.ecuation[1]==line2.ecuation[1]):
  425. if (delta_slope == 0 or delta_slope == "nan" and line1.ecuation[1]==line2.ecuation[1]):
  426. print ("Son la misma recta o paralelas, no se cruzan")
  427. return "Same"
  428. elif (delta_slope == 0 or delta_slope == "nan" and line1.ecuation[1]!=line2.ecuation[1]):
  429. print ("Son paralelas, no se cruzan")
  430. return "Parallel"
  431. elif (line1.slope == float ('inf')): ##La primera recta es paralela al eje y
  432. xinterseccion = float(line1.point1.x)
  433. yinterseccion = line2.slope*xinterseccion + line2.ecuation[1]
  434. return Point(round(xinterseccion, precision), round(yinterseccion, precision))
  435. elif (line2.slope == float ('inf')): ##La segunda recta es paralela al eje y
  436. xinterseccion = float(line1.point2.x)
  437. yinterseccion = line1.slope*xinterseccion + line1.ecuation[1]
  438. return Point(round(xinterseccion, precision), round(yinterseccion, precision))
  439. else:
  440. xinterseccion = (line2.ecuation[1]-line1.ecuation[1])/(line1.slope - line2.slope)
  441. yinterseccion = line1.slope*xinterseccion + line1.ecuation[1]
  442. return Point(round(xinterseccion, precision), round(yinterseccion,precision))
  443.  
  444. ## Hallar la bisectriz de dos lineas
  445. def get_bisector(self, line1, line2):
  446. intersection_point = self.lines_intersection(line1, line2)
  447. if intersection_point == "Same":
  448. intersection_point = [self.point1, self.point2]
  449. elif intersection_point == "Parallel":
  450. pass
  451. #get_distance_between_lines
  452. else:
  453. # There must be an intersection
  454. pass
  455.  
  456. angle1 = math.degrees(math.atan(line1.slope))
  457. angle2 = math.degrees(math.atan(line2.slope))
  458. bisector_angle = (angle1 + angle2) / 2
  459. bisector_slope = math.tan(math.radians(bisector_angle))
  460.  
  461. return self.create_entity("Line", intersection_point, self.get_bisector_vector(line1.vector, line2.vector))
  462.  
  463. def get_bisector_vector (self, vector1, vector2):
  464. vector_suma = vector1 + vector2
  465. vector_bisector = vector_suma.get_versor()
  466. return vector_bisector
  467.  
  468. def get_offset(self, polyline, offset_dist):
  469. coords_amount = len(polyline.coords)
  470. max_index = coords_amount-1
  471.  
  472. list_new_points = []
  473. # Check if the polyline is closed (first check) or opened (second check)
  474. # If the polyline is closed:
  475. if polyline.coords[0] == polyline.coords[max_index]:
  476. for x in range(0, coords_amount):
  477. point_x = Point(polyline.coords[x][0], polyline.coords[x][1])
  478. if x == 0:
  479. vector_pos = Vector (polyline.coords[x], polyline.coords[x+1])
  480. vector_pre = Vector (polyline.coords[x], polyline.coords[x-2]) # This is beacuse it is a closed polyline
  481. bisector_angle = vector_pos.get_angle_vectors(vector_pre) / 2
  482. dist_bisector = math.sin(math.radians(bisector_angle)) * offset_dist * 2
  483. bisector = self.get_bisector_vector(vector_pos, vector_pre) * dist_bisector
  484. elif x == max_index:
  485. list_new_points.append(list_new_points[0])
  486. break
  487. else:
  488. vector_pos = Vector (polyline.coords[x], polyline.coords[x+1])
  489. vector_pre = Vector (polyline.coords[x], polyline.coords[x-1])
  490. bisector_angle = vector_pos.get_angle_vectors(vector_pre) / 2
  491. dist_bisector = math.sin(math.radians(bisector_angle)) * offset_dist * 2
  492. bisector = self.get_bisector_vector(vector_pos, vector_pre) * dist_bisector
  493. new_point = Point (*((point_x + bisector).coords))
  494. list_new_points.append(new_point)
  495.  
  496. # Polyline is opened
  497. else:
  498. sign = offset_dist/abs (offset_dist)
  499. offset_dist = abs (offset_dist)
  500. for x in range(0, coords_amount):
  501. point_x = Point(polyline.coords[x][0], polyline.coords[x][1])
  502. if x == 0:
  503. vector_pos = Vector (polyline.coords[x], polyline.coords[x+1])
  504. vector_perp = vector_pos.get_perpendicular_vector()
  505. vector_perp = vector_perp.get_versor()
  506. vector_pre_next = Vector (polyline.coords[x+1], polyline.coords[x])
  507. vector_pos_next = Vector (polyline.coords[x+1], polyline.coords[x+2])
  508. next_bisector = vector_pre_next + vector_pos_next
  509. test_angle = vector_perp.get_angle_vectors(next_bisector) * sign
  510. if test_angle > 0 and test_angle < 90:
  511. bisector = vector_perp * offset_dist
  512. else:
  513. bisector = vector_perp.get_reversed() * offset_dist
  514. elif x == max_index:
  515. vector_pre = Vector (polyline.coords[x], polyline.coords[x-1])
  516. vector_perp = vector_pre.get_perpendicular_vector()
  517. vector_perp = vector_perp.get_versor()
  518. test_angle = vector_perp.get_angle_vectors(next_bisector) * sign
  519. bisector = vector_perp * offset_dist
  520. vector_pre_offset = Vector ((point_x + bisector), new_point) # new_point here is the previous and last new_point created.
  521. angle_pre_vectors = vector_pre.get_angle_vectors(vector_pre_offset)
  522. # Checks if the two pre_vectors are parallel or not
  523. if angle_pre_vectors != 0:
  524. bisector = vector_perp.get_reversed() * offset_dist
  525. else:
  526. vector_pos = Vector (polyline.coords[x], polyline.coords[x+1])
  527. vector_pre = Vector (polyline.coords[x], polyline.coords[x-1])
  528. bisector_angle = vector_pos.get_angle_vectors(vector_pre) / 2
  529. dist_bisector = math.sin(math.radians(bisector_angle)) * offset_dist * 2
  530. bisector = self.get_bisector_vector(vector_pos, vector_pre) * dist_bisector * sign
  531. new_point = Point (*((point_x + bisector).coords))
  532. list_new_points.append(new_point)
  533. new_polyline = polyline.create_entity("Polyline", list_new_points) # The type could be the same of origin type
  534. new_polyline.coords_update()
  535.  
  536. return new_polyline
  537.  
  538.  
  539. # Class for running the app
  540. class MyApplication(Gtk.Application):
  541. def __init__(self):
  542. Gtk.Application.__init__(self)
  543.  
  544. def do_activate(self):
  545. self.win = WindowGui(self)
  546. #win.show_all()
  547.  
  548. def do_startup(self):
  549. Gtk.Application.do_startup(self)
  550.  
  551. if __name__ == "__main__":
  552. app = MyApplication()
  553. app.run(sys.argv)
 
archivo: 
AttachmentSize
Binary Data canvas_offset.tar_.gz6.08 KB
Package icon canvas_offset.zip6.1 KB