flatcam/appGUI/PlotCanvasLegacy.py

1657 lines
61 KiB
Python
Raw Permalink Normal View History

############################################################
# FlatCAM: 2D Post-processing for Manufacturing #
# http://caram.cl/software/flatcam #
# Author: Juan Pablo Caram (c) #
# Date: 2/5/2014 #
# MIT Licence #
# Modified by Marius Stanciu 09/21/2019 #
############################################################
from PyQt5 import QtCore
from PyQt5.QtCore import pyqtSignal
# needed for legacy mode
# Used for solid polygons in Matplotlib
from descartes.patch import PolygonPatch
from shapely.geometry import Polygon, LineString, LinearRing
from copy import deepcopy
import logging
2020-06-02 15:29:45 +00:00
import numpy as np
import gettext
import appTranslation as fcTranslate
import builtins
# Prevent conflict with Qt5 and above.
from matplotlib import use as mpl_use
mpl_use("Qt5Agg")
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.lines import Line2D
2020-06-02 15:29:45 +00:00
from matplotlib.offsetbox import AnchoredText
# from matplotlib.widgets import Cursor
fcTranslate.apply_language('strings')
if '_' not in builtins.__dict__:
_ = gettext.gettext
log = logging.getLogger('base')
class CanvasCache(QtCore.QObject):
"""
Case story #1:
1) No objects in the project.
2020-06-02 15:29:45 +00:00
2) Object is created (app_obj.new_object() emits object_created(obj)).
on_object_created() adds (i) object to collection and emits
2020-06-02 15:29:45 +00:00
(ii) app_obj.new_object_available() then calls (iii) object.plot()
3) object.plot() creates axes if necessary on
app.collection.figure. Then plots on it.
4) Plots on a cache-size canvas (in background).
5) Plot completes. Bitmap is generated.
6) Visible canvas is painted.
"""
# Signals:
# A bitmap is ready to be displayed.
new_screen = QtCore.pyqtSignal()
def __init__(self, plotcanvas, app, dpi=50):
super(CanvasCache, self).__init__()
self.app = app
self.plotcanvas = plotcanvas
self.dpi = dpi
self.figure = Figure(dpi=dpi)
self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
self.axes.set_frame_on(False)
self.axes.set_xticks([])
self.axes.set_yticks([])
if self.app.defaults['global_theme'] == 'white':
self.axes.set_facecolor('#FFFFFF')
else:
self.axes.set_facecolor('#000000')
self.canvas = FigureCanvas(self.figure)
self.cache = None
def run(self):
log.debug("CanvasCache Thread Started!")
self.plotcanvas.update_screen_request.connect(self.on_update_req)
def on_update_req(self, extents):
"""
Event handler for an updated display request.
:param extents: [xmin, xmax, ymin, ymax, zoom(optional)]
"""
# log.debug("Canvas update requested: %s" % str(extents))
# Note: This information below might be out of date. Establish
# a protocol regarding when to change the canvas in the main
# thread and when to check these values here in the background,
# or pass this data in the signal (safer).
# log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize()))
# log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density()))
# Move the requested screen portion to the main thread
# and inform about the update:
self.new_screen.emit()
# Continue to update the cache.
2020-06-02 15:29:45 +00:00
# def on_app_obj.new_object_available(self):
#
# log.debug("A new object is available. Should plot it!")
class PlotCanvasLegacy(QtCore.QObject):
"""
Class handling the plotting area in the application.
"""
# Signals:
# Request for new bitmap to display. The parameter
# is a list with [xmin, xmax, ymin, ymax, zoom(optional)]
update_screen_request = QtCore.pyqtSignal(list)
double_click = QtCore.pyqtSignal(object)
def __init__(self, container, app):
"""
The constructor configures the Matplotlib figure that
will contain all plots, creates the base axes and connects
events to the plotting area.
:param container: The parent container in which to draw plots.
:rtype: PlotCanvas
"""
super(PlotCanvasLegacy, self).__init__()
self.app = app
if self.app.defaults['global_theme'] == 'white':
theme_color = '#FFFFFF'
tick_color = '#000000'
2020-06-02 15:29:45 +00:00
self.rect_hud_color = '#0000FF10'
self.text_hud_color = '#000000'
else:
theme_color = '#000000'
tick_color = '#FFFFFF'
2020-06-02 15:29:45 +00:00
self.rect_hud_color = '#80808040'
self.text_hud_color = '#FFFFFF'
# workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
# which might decrease performance
# self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
self.workspace_line = None
2020-02-29 22:52:24 +00:00
self.pagesize_dict = {}
self.pagesize_dict.update(
{
'A0': (841, 1189),
'A1': (594, 841),
'A2': (420, 594),
'A3': (297, 420),
'A4': (210, 297),
'A5': (148, 210),
'A6': (105, 148),
'A7': (74, 105),
'A8': (52, 74),
'A9': (37, 52),
'A10': (26, 37),
'B0': (1000, 1414),
'B1': (707, 1000),
'B2': (500, 707),
'B3': (353, 500),
'B4': (250, 353),
'B5': (176, 250),
'B6': (125, 176),
'B7': (88, 125),
'B8': (62, 88),
'B9': (44, 62),
'B10': (31, 44),
'C0': (917, 1297),
'C1': (648, 917),
'C2': (458, 648),
'C3': (324, 458),
'C4': (229, 324),
'C5': (162, 229),
'C6': (114, 162),
'C7': (81, 114),
'C8': (57, 81),
'C9': (40, 57),
'C10': (28, 40),
# American paper sizes
'LETTER': (8.5*25.4, 11*25.4),
'LEGAL': (8.5*25.4, 14*25.4),
'ELEVENSEVENTEEN': (11*25.4, 17*25.4),
# From https://en.wikipedia.org/wiki/Paper_size
'JUNIOR_LEGAL': (5*25.4, 8*25.4),
'HALF_LETTER': (5.5*25.4, 8*25.4),
'GOV_LETTER': (8*25.4, 10.5*25.4),
'GOV_LEGAL': (8.5*25.4, 13*25.4),
'LEDGER': (17*25.4, 11*25.4),
}
)
# Options
self.x_margin = 15 # pixels
self.y_margin = 25 # Pixels
# Parent container
self.container = container
# Plots go onto a single matplotlib.figure
self.figure = Figure(dpi=50)
self.figure.patch.set_visible(True)
self.figure.set_facecolor(theme_color)
# These axes show the ticks and grid. No plotting done here.
# New axes must have a label, otherwise mpl returns an existing one.
self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
self.axes.set_aspect(1)
self.axes.grid(True, color='gray')
self.h_line = self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
self.v_line = self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
self.axes.tick_params(axis='x', color=tick_color, labelcolor=tick_color)
self.axes.tick_params(axis='y', color=tick_color, labelcolor=tick_color)
self.axes.spines['bottom'].set_color(tick_color)
self.axes.spines['top'].set_color(tick_color)
self.axes.spines['right'].set_color(tick_color)
self.axes.spines['left'].set_color(tick_color)
self.axes.set_facecolor(theme_color)
self.ch_line = None
self.cv_line = None
# The canvas is the top level container (FigureCanvasQTAgg)
self.canvas = FigureCanvas(self.figure)
self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
self.canvas.setFocus()
self.native = self.canvas
self.adjust_axes(-10, -10, 100, 100)
# self.canvas.set_can_focus(True) # For key press
# Attach to parent
# self.container.attach(self.canvas, 0, 0, 600, 400)
self.container.addWidget(self.canvas) # Qt
# Copy a bitmap of the canvas for quick animation.
# Update every time the canvas is re-drawn.
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
# ################### NOT IMPLEMENTED YET - EXPERIMENTAL #######################
# ## Bitmap Cache
# self.cache = CanvasCache(self, self.app)
# self.cache_thread = QtCore.QThread()
# self.cache.moveToThread(self.cache_thread)
# # super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
# self.cache_thread.started.connect(self.cache.run)
#
# self.cache_thread.start()
# self.cache.new_screen.connect(self.on_new_screen)
# ##############################################################################
# Events
self.mp = self.graph_event_connect('button_press_event', self.on_mouse_press)
self.mr = self.graph_event_connect('button_release_event', self.on_mouse_release)
self.mm = self.graph_event_connect('motion_notify_event', self.on_mouse_move)
# self.canvas.connect('configure-event', self.auto_adjust_axes)
self.aaa = self.graph_event_connect('resize_event', self.auto_adjust_axes)
# self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
# self.canvas.connect("scroll-event", self.on_scroll)
self.osc = self.graph_event_connect('scroll_event', self.on_scroll)
# self.graph_event_connect('key_press_event', self.on_key_down)
# self.graph_event_connect('key_release_event', self.on_key_up)
self.odr = self.graph_event_connect('draw_event', self.on_draw)
self.key = None
self.pan_axes = []
self.panning = False
self.mouse = [0, 0]
self.big_cursor = False
self.big_cursor_isdisabled = None
# signal is the mouse is dragging
self.is_dragging = False
2020-06-02 15:29:45 +00:00
self.mouse_press_pos = None
# signal if there is a doubleclick
self.is_dblclk = False
# HUD Display
2020-06-02 15:29:45 +00:00
self.hud_enabled = False
self.text_hud = self.Thud(plotcanvas=self)
if self.app.defaults['global_hud'] is True:
self.on_toggle_hud(state=True, silent=None)
2020-06-02 15:29:45 +00:00
# enable Grid lines
self.grid_lines_enabled = True
# draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
# all CNC have a limited workspace
if self.app.defaults['global_workspace'] is True:
self.draw_workspace(workspace_size=self.app.defaults["global_workspaceT"])
2020-06-02 15:29:45 +00:00
# Axis Display
self.axis_enabled = True
# enable Axis
self.on_toggle_axis(state=True, silent=True)
self.app.ui.axis_status_label.setStyleSheet("""
QLabel
{
color: black;
background-color: orange;
}
""")
def on_toggle_axis(self, signal=None, state=None, silent=None):
if not state:
2020-06-02 15:29:45 +00:00
state = not self.axis_enabled
if state:
self.axis_enabled = True
self.app.defaults['global_axis'] = True
2020-06-02 15:29:45 +00:00
if self.h_line not in self.axes.lines and self.v_line not in self.axes.lines:
self.h_line = self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
self.v_line = self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
self.app.ui.axis_status_label.setStyleSheet("""
QLabel
{
color: black;
background-color: orange;
}
""")
if silent is None:
self.app.inform[str, bool].emit(_("Axis enabled."), False)
2020-06-02 15:29:45 +00:00
else:
self.axis_enabled = False
self.app.defaults['global_axis'] = False
2020-06-02 15:29:45 +00:00
if self.h_line in self.axes.lines and self.v_line in self.axes.lines:
self.axes.lines.remove(self.h_line)
self.axes.lines.remove(self.v_line)
self.app.ui.axis_status_label.setStyleSheet("")
if silent is None:
self.app.inform[str, bool].emit(_("Axis disabled."), False)
2020-06-02 15:29:45 +00:00
self.canvas.draw()
def on_toggle_hud(self, signal=None, state=None, silent=None):
2020-06-02 15:29:45 +00:00
if state is None:
state = not self.hud_enabled
if state:
self.hud_enabled = True
self.text_hud.add_artist()
self.app.defaults['global_hud'] = True
self.app.ui.hud_label.setStyleSheet("""
QLabel
{
color: black;
background-color: mediumpurple;
}
""")
if silent is None:
self.app.inform[str, bool].emit(_("HUD enabled."), False)
2020-06-02 15:29:45 +00:00
else:
self.hud_enabled = False
self.text_hud.remove_artist()
self.app.defaults['global_hud'] = False
self.app.ui.hud_label.setStyleSheet("")
if silent is None:
self.app.inform[str, bool].emit(_("HUD disabled."), False)
2020-06-02 15:29:45 +00:00
self.canvas.draw()
class Thud(QtCore.QObject):
text_changed = QtCore.pyqtSignal(str)
def __init__(self, plotcanvas):
super().__init__()
self.p = plotcanvas
units = self.p.app.defaults['units']
self._text = 'Dx: %s [%s]\nDy: %s [%s]\n\nX: %s [%s]\nY: %s [%s]' % \
('0.0000', units, '0.0000', units, '0.0000', units, '0.0000', units)
# set font size
qsettings = QtCore.QSettings("Open Source", "FlatCAM")
if qsettings.contains("hud_font_size"):
# I multiply with 2.5 because this seems to be the difference between the value taken by the VisPy (3D)
# and Matplotlib (Legacy2D FlatCAM graphic engine)
fsize = int(qsettings.value('hud_font_size', type=int) * 2.5)
else:
fsize = 20
self.hud_holder = AnchoredText(self._text, prop=dict(size=fsize), frameon=True, loc='upper left')
self.hud_holder.patch.set_boxstyle("round,pad=0.,rounding_size=0.2")
fc_color = self.p.rect_hud_color[:-2]
fc_alpha = int(self.p.rect_hud_color[-2:], 16) / 255
text_color = self.p.text_hud_color
self.hud_holder.patch.set_facecolor(fc_color)
self.hud_holder.patch.set_alpha(fc_alpha)
self.hud_holder.patch.set_edgecolor((0, 0, 0, 0))
self. hud_holder.txt._text.set_color(color=text_color)
self.text_changed.connect(self.on_text_changed)
@property
def text(self):
return self._text
@text.setter
def text(self, val):
self.text_changed.emit(val)
self._text = val
def on_text_changed(self, txt):
try:
txt = txt.replace('\t', ' ')
self.hud_holder.txt.set_text(txt)
self.p.canvas.draw()
except Exception:
pass
def add_artist(self):
if self.hud_holder not in self.p.axes.artists:
self.p.axes.add_artist(self.hud_holder)
def remove_artist(self):
if self.hud_holder in self.p.axes.artists:
self.p.axes.artists.remove(self.hud_holder)
def on_toggle_grid_lines(self, signal=None, silent=None):
2020-06-02 15:29:45 +00:00
state = not self.grid_lines_enabled
if state:
self.app.defaults['global_grid_lines'] = True
2020-06-02 15:29:45 +00:00
self.grid_lines_enabled = True
self.axes.grid(True)
try:
self.canvas.draw()
except IndexError:
pass
if silent is None:
self.app.inform[str, bool].emit(_("Grid enabled."), False)
2020-06-02 15:29:45 +00:00
else:
self.app.defaults['global_grid_lines'] = False
2020-06-02 15:29:45 +00:00
self.grid_lines_enabled = False
self.axes.grid(False)
try:
self.canvas.draw()
except IndexError:
pass
if silent is None:
self.app.inform[str, bool].emit(_("Grid disabled."), False)
2020-06-02 15:29:45 +00:00
def draw_workspace(self, workspace_size):
"""
Draw a rectangular shape on canvas to specify our valid workspace.
:param workspace_size: the workspace size; tuple
:return:
"""
try:
if self.app.defaults['units'].upper() == 'MM':
dims = self.pagesize_dict[workspace_size]
else:
dims = (self.pagesize_dict[workspace_size][0]/25.4, self.pagesize_dict[workspace_size][1]/25.4)
except Exception as e:
log.debug("PlotCanvasLegacy.draw_workspace() --> %s" % str(e))
return
if self.app.defaults['global_workspace_orientation'] == 'l':
dims = (dims[1], dims[0])
xdata = [0, dims[0], dims[0], 0, 0]
ydata = [0, 0, dims[1], dims[1], 0]
if self.workspace_line not in self.axes.lines:
self.workspace_line = Line2D(xdata=xdata, ydata=ydata, linewidth=2, antialiased=True, color='#b34d4d')
self.axes.add_line(self.workspace_line)
self.canvas.draw()
2020-06-02 15:29:45 +00:00
self.app.ui.wplace_label.set_value(workspace_size[:3])
self.app.ui.wplace_label.setToolTip(workspace_size)
self.fcapp.ui.wplace_label.setStyleSheet("""
QLabel
{
color: black;
background-color: olivedrab;
}
""")
def delete_workspace(self):
try:
self.axes.lines.remove(self.workspace_line)
self.canvas.draw()
except Exception:
pass
2020-06-02 15:29:45 +00:00
self.fcapp.ui.wplace_label.setStyleSheet("")
def graph_event_connect(self, event_name, callback):
"""
Attach an event handler to the canvas through the Matplotlib interface.
:param event_name: Name of the event
:type event_name: str
:param callback: Function to call
:type callback: func
:return: Connection id
:rtype: int
"""
if event_name == 'mouse_move':
event_name = 'motion_notify_event'
if event_name == 'mouse_press':
event_name = 'button_press_event'
if event_name == 'mouse_release':
event_name = 'button_release_event'
if event_name == 'mouse_double_click':
return self.double_click.connect(callback)
if event_name == 'key_press':
event_name = 'key_press_event'
return self.canvas.mpl_connect(event_name, callback)
def graph_event_disconnect(self, cid):
"""
Disconnect callback with the give id.
:param cid: Callback id.
:return: None
"""
self.canvas.mpl_disconnect(cid)
def on_new_screen(self):
pass
# log.debug("Cache updated the screen!")
def new_cursor(self, axes=None, big=None):
# if axes is None:
# c = MplCursor(axes=self.axes, color='black', linewidth=1)
# else:
# c = MplCursor(axes=axes, color='black', linewidth=1)
if self.app.defaults["global_cursor_color_enabled"]:
color = self.app.defaults["global_cursor_color"]
else:
if self.app.defaults['global_theme'] == 'white':
color = '#000000'
else:
color = '#FFFFFF'
if big is True:
self.big_cursor = True
self.ch_line = self.axes.axhline(color=color, linewidth=self.app.defaults["global_cursor_width"])
self.cv_line = self.axes.axvline(color=color, linewidth=self.app.defaults["global_cursor_width"])
self.big_cursor_isdisabled = False
else:
self.big_cursor = False
c = FakeCursor()
c.mouse_state_updated.connect(self.clear_cursor)
return c
def draw_cursor(self, x_pos, y_pos, color=None):
"""
Draw a cursor at the mouse grid snapped position
:param x_pos: mouse x position
:param y_pos: mouse y position
:param color: custom color of the mouse
:return:
"""
# there is no point in drawing mouse cursor when panning as it jumps in a confusing way
if self.app.app_cursor.enabled is True and self.panning is False:
if color:
color = color
else:
if self.app.defaults['global_theme'] == 'white':
color = '#000000'
else:
color = '#FFFFFF'
if self.big_cursor is False:
try:
2020-06-02 15:29:45 +00:00
x, y = self.snap(x_pos, y_pos)
# Pointer (snapped)
# The size of the cursor is multiplied by 1.65 because that value made the cursor similar with the
# one in the OpenGL(3D) graphic engine
pointer_size = int(float(self.app.defaults["global_cursor_size"]) * 1.65)
elements = self.axes.plot(x, y, '+', color=color, ms=pointer_size,
mew=self.app.defaults["global_cursor_width"], animated=True)
for el in elements:
self.axes.draw_artist(el)
except Exception as e:
# this happen at app initialization since self.app.geo_editor does not exist yet
# I could reshuffle the object instantiating order but what's the point?
# I could crash something else and that's pythonic, too
log.debug("PlotCanvasLegacy.draw_cursor() big_cursor is False --> %s" % str(e))
else:
try:
self.ch_line.set_markeredgewidth(self.app.defaults["global_cursor_width"])
self.cv_line.set_markeredgewidth(self.app.defaults["global_cursor_width"])
except Exception:
pass
try:
x, y = self.app.geo_editor.snap(x_pos, y_pos)
self.ch_line.set_ydata(y)
self.cv_line.set_xdata(x)
except Exception:
# this happen at app initialization since self.app.geo_editor does not exist yet
# I could reshuffle the object instantiating order but what's the point?
# I could crash something else and that's pythonic, too
pass
self.canvas.draw_idle()
2020-06-02 15:29:45 +00:00
self.canvas.blit(self.axes.bbox)
def clear_cursor(self, state):
if state is True:
if self.big_cursor is True and self.big_cursor_isdisabled is True:
if self.app.defaults["global_cursor_color_enabled"]:
color = self.app.defaults["global_cursor_color"]
else:
if self.app.defaults['global_theme'] == 'white':
color = '#000000'
else:
color = '#FFFFFF'
self.ch_line = self.axes.axhline(color=color, linewidth=self.app.defaults["global_cursor_width"])
self.cv_line = self.axes.axvline(color=color, linewidth=self.app.defaults["global_cursor_width"])
self.big_cursor_isdisabled = False
if self.app.defaults["global_cursor_color_enabled"] is True:
self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1], color=self.app.cursor_color_3D)
else:
self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
else:
if self.big_cursor is True:
self.big_cursor_isdisabled = True
try:
self.ch_line.remove()
self.cv_line.remove()
self.canvas.draw_idle()
except Exception as e:
log.debug("PlotCanvasLegacy.clear_cursor() big_cursor is True --> %s" % str(e))
self.canvas.restore_region(self.background)
self.canvas.blit(self.axes.bbox)
def on_key_down(self, event):
"""
:param event:
:return:
"""
log.debug('on_key_down(): ' + str(event.key))
self.key = event.key
def on_key_up(self, event):
"""
:param event:
:return:
"""
self.key = None
def connect(self, event_name, callback):
"""
Attach an event handler to the canvas through the native Qt interface.
:param event_name: Name of the event
:type event_name: str
:param callback: Function to call
:type callback: function
:return: Nothing
"""
self.canvas.connect(event_name, callback)
def clear(self):
"""
Clears axes and figure.
:return: None
"""
# Clear
self.axes.cla()
try:
self.figure.clf()
except KeyError:
log.warning("KeyError in MPL figure.clf()")
# Re-build
self.figure.add_axes(self.axes)
self.axes.set_aspect(1)
self.axes.grid(True)
self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
self.adjust_axes(-10, -10, 100, 100)
# Re-draw
self.canvas.draw_idle()
def redraw(self):
"""
Created only to serve for compatibility with the VisPy plotcanvas (the other graphic engine, 3D)
:return:
"""
self.clear()
def adjust_axes(self, xmin, ymin, xmax, ymax):
"""
Adjusts all axes while maintaining the use of the whole canvas
and an aspect ratio to 1:1 between x and y axes. The parameters are an original
request that will be modified to fit these restrictions.
:param xmin: Requested minimum value for the X axis.
:type xmin: float
:param ymin: Requested minimum value for the Y axis.
:type ymin: float
:param xmax: Requested maximum value for the X axis.
:type xmax: float
:param ymax: Requested maximum value for the Y axis.
:type ymax: float
:return: None
"""
# FlatCAMApp.App.log.debug("PC.adjust_axes()")
if not self.app.collection.get_list():
xmin = -10
ymin = -10
xmax = 100
ymax = 100
width = xmax - xmin
height = ymax - ymin
try:
r = width / height
except ZeroDivisionError:
log.error("Height is %f" % height)
return
canvas_w, canvas_h = self.canvas.get_width_height()
canvas_r = float(canvas_w) / canvas_h
x_ratio = float(self.x_margin) / canvas_w
y_ratio = float(self.y_margin) / canvas_h
if r > canvas_r:
ycenter = (ymin + ymax) / 2.0
newheight = height * r / canvas_r
ymin = ycenter - newheight / 2.0
ymax = ycenter + newheight / 2.0
else:
xcenter = (xmax + xmin) / 2.0
newwidth = width * canvas_r / r
xmin = xcenter - newwidth / 2.0
xmax = xcenter + newwidth / 2.0
# Adjust axes
for ax in self.figure.get_axes():
if ax._label != 'base':
ax.set_frame_on(False) # No frame
ax.set_xticks([]) # No tick
ax.set_yticks([]) # No ticks
ax.patch.set_visible(False) # No background
ax.set_aspect(1)
ax.set_xlim((xmin, xmax))
ax.set_ylim((ymin, ymax))
ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
# Sync re-draw to proper paint on form resize
self.canvas.draw()
# #### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def auto_adjust_axes(self, *args):
"""
Calls ``adjust_axes()`` using the extents of the base axes.
:rtype : None
:return: None
"""
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
self.adjust_axes(xmin, ymin, xmax, ymax)
def fit_view(self):
self.auto_adjust_axes()
def fit_center(self, loc, rect=None):
x = loc[0]
y = loc[1]
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
half_width = (xmax - xmin) / 2
half_height = (ymax - ymin) / 2
# Adjust axes
for ax in self.figure.get_axes():
ax.set_xlim((x - half_width, x + half_width))
ax.set_ylim((y - half_height, y + half_height))
# Re-draw
self.canvas.draw()
# #### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def zoom(self, factor, center=None):
"""
Zooms the plot by factor around a given
center point. Takes care of re-drawing.
:param factor: Number by which to scale the plot.
:type factor: float
:param center: Coordinates [x, y] of the point around which to scale the plot.
:type center: list
:return: None
"""
factor = 1 / factor
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
width = xmax - xmin
height = ymax - ymin
if center is None or center == [None, None]:
center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
# For keeping the point at the pointer location
relx = (xmax - center[0]) / width
rely = (ymax - center[1]) / height
new_width = width / factor
new_height = height / factor
xmin = center[0] - new_width * (1 - relx)
xmax = center[0] + new_width * relx
ymin = center[1] - new_height * (1 - rely)
ymax = center[1] + new_height * rely
# Adjust axes
for ax in self.figure.get_axes():
ax.set_xlim((xmin, xmax))
ax.set_ylim((ymin, ymax))
# Async re-draw
self.canvas.draw_idle()
# #### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def pan(self, x, y, idle=True):
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
width = xmax - xmin
height = ymax - ymin
# Adjust axes
for ax in self.figure.get_axes():
ax.set_xlim((xmin + x * width, xmax + x * width))
ax.set_ylim((ymin + y * height, ymax + y * height))
# Re-draw
if idle:
self.canvas.draw_idle()
else:
self.canvas.draw()
# #### Temporary place-holder for cached update #####
self.update_screen_request.emit([0, 0, 0, 0, 0])
def new_axes(self, name):
"""
Creates and returns an Axes object attached to this object's Figure.
:param name: Unique label for the axes.
:return: Axes attached to the figure.
:rtype: Axes
"""
new_ax = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
return new_ax
def remove_current_axes(self):
"""
:return: The name of the deleted axes
"""
axes_to_remove = self.figure.axes.gca()
current_axes_name = deepcopy(axes_to_remove._label)
self.figure.axes.remove(axes_to_remove)
return current_axes_name
def on_scroll(self, event):
"""
Scroll event handler.
:param event: Event object containing the event information.
:return: None
"""
# So it can receive key presses
# self.canvas.grab_focus()
self.canvas.setFocus()
# Event info
# z, direction = event.get_scroll_direction()
if self.key is None:
if event.button == 'up':
self.zoom(1 / 1.5, self.mouse)
else:
self.zoom(1.5, self.mouse)
return
if self.key == 'shift':
if event.button == 'up':
self.pan(0.3, 0)
else:
self.pan(-0.3, 0)
return
if self.key == 'control':
if event.button == 'up':
self.pan(0, 0.3)
else:
self.pan(0, -0.3)
return
def on_mouse_press(self, event):
self.is_dragging = True
2020-06-02 15:29:45 +00:00
self.mouse_press_pos = (event.x, event.y)
# Check for middle mouse button press
if self.app.defaults["global_pan_button"] == '2':
pan_button = 3 # right button for Matplotlib
else:
pan_button = 2 # middle button for Matplotlib
if event.button == pan_button:
# Prepare axes for pan (using 'matplotlib' pan function)
self.pan_axes = []
for a in self.figure.get_axes():
if (event.x is not None and event.y is not None and a.in_axes(event) and
a.get_navigate() and a.can_pan()):
a.start_pan(event.x, event.y, 1)
self.pan_axes.append(a)
# Set pan view flag
if len(self.pan_axes) > 0:
self.panning = True
if event.dblclick:
self.double_click.emit(event)
def on_mouse_release(self, event):
2020-06-02 15:29:45 +00:00
mouse_release_pos = (event.x, event.y)
delta = 0.05
if abs(self.distance(self.mouse_press_pos, mouse_release_pos)) < delta:
self.is_dragging = False
# Check for middle mouse button release to complete pan procedure
# Check for middle mouse button press
if self.app.defaults["global_pan_button"] == '2':
pan_button = 3 # right button for Matplotlib
else:
pan_button = 2 # middle button for Matplotlib
if event.button == pan_button:
for a in self.pan_axes:
a.end_pan()
# Clear pan flag
self.panning = False
# And update the cursor
if self.app.defaults["global_cursor_color_enabled"] is True:
self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1], color=self.app.cursor_color_3D)
else:
self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
def on_mouse_move(self, event):
"""
Mouse movement event handler. Stores the coordinates. Updates view on pan.
:param event: Contains information about the event.
:return: None
"""
try:
x = float(event.xdata)
y = float(event.ydata)
except TypeError:
return
self.mouse = [event.xdata, event.ydata]
self.canvas.restore_region(self.background)
# Update pan view on mouse move
if self.panning is True:
for a in self.pan_axes:
a.drag_pan(1, event.key, event.x, event.y)
# x_pan, y_pan = self.app.geo_editor.snap(event.xdata, event.ydata)
# self.draw_cursor(x_pos=x_pan, y_pos=y_pan)
# Async re-draw (redraws only on thread idle state, uses timer on backend)
self.canvas.draw_idle()
# #### Temporary place-holder for cached update #####
2020-06-02 15:29:45 +00:00
# self.update_screen_request.emit([0, 0, 0, 0, 0])
if self.app.defaults["global_cursor_color_enabled"] is True:
self.draw_cursor(x_pos=x, y_pos=y, color=self.app.cursor_color_3D)
else:
self.draw_cursor(x_pos=x, y_pos=y)
# self.canvas.blit(self.axes.bbox)
def translate_coords(self, position):
"""
This does not do much. It's just for code compatibility
:param position: Mouse event position
:return: Tuple with mouse position
"""
return position[0], position[1]
def on_draw(self, renderer):
# Store background on canvas redraw
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
def get_axes_pixelsize(self):
"""
Axes size in pixels.
:return: Pixel width and height
:rtype: tuple
"""
bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width, bbox.height
width *= self.figure.dpi
height *= self.figure.dpi
return width, height
def get_density(self):
"""
Returns unit length per pixel on horizontal
and vertical axes.
:return: X and Y density
:rtype: tuple
"""
xpx, ypx = self.get_axes_pixelsize()
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
width = xmax - xmin
height = ymax - ymin
return width / xpx, height / ypx
2020-06-02 15:29:45 +00:00
def snap(self, x, y):
"""
Adjusts coordinates to snap settings.
:param x: Input coordinate X
:param y: Input coordinate Y
:return: Snapped (x, y)
"""
snap_x, snap_y = (x, y)
snap_distance = np.Inf
# ### Grid snap
if self.app.grid_status():
if self.app.defaults["global_gridx"] != 0:
try:
snap_x_ = round(x / float(self.app.defaults["global_gridx"])) * \
float(self.app.defaults["global_gridx"])
except TypeError:
snap_x_ = x
else:
snap_x_ = x
# If the Grid_gap_linked on Grid Toolbar is checked then the snap distance on GridY entry will be ignored
# and it will use the snap distance from GridX entry
if self.app.ui.grid_gap_link_cb.isChecked():
if self.app.defaults["global_gridx"] != 0:
try:
snap_y_ = round(y / float(self.app.defaults["global_gridx"])) * \
float(self.app.defaults["global_gridx"])
except TypeError:
snap_y_ = y
else:
snap_y_ = y
else:
if self.app.defaults["global_gridy"] != 0:
try:
snap_y_ = round(y / float(self.app.defaults["global_gridy"])) * \
float(self.app.defaults["global_gridy"])
except TypeError:
snap_y_ = y
else:
snap_y_ = y
nearest_grid_distance = self.distance((x, y), (snap_x_, snap_y_))
if nearest_grid_distance < snap_distance:
snap_x, snap_y = (snap_x_, snap_y_)
return snap_x, snap_y
@staticmethod
def distance(pt1, pt2):
return np.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
class FakeCursor(QtCore.QObject):
"""
This is a fake cursor to ensure compatibility with the OpenGL engine (VisPy).
This way I don't have to chane (disable) things related to the cursor all over when
using the low performance Matplotlib 2D graphic engine.
"""
mouse_state_updated = pyqtSignal(bool)
def __init__(self):
super().__init__()
self._enabled = True
@property
def enabled(self):
return True if self._enabled else False
@enabled.setter
def enabled(self, value):
self._enabled = value
self.mouse_state_updated.emit(value)
def set_data(self, pos, **kwargs):
"""Internal event handler to draw the cursor when the mouse moves."""
return
class ShapeCollectionLegacy:
"""
This will create the axes for each collection of shapes and will also
hold the collection of shapes into a dict self._shapes.
This handles the shapes redraw on canvas.
"""
def __init__(self, obj, app, name=None, annotation_job=None, linewidth=1):
"""
:param obj: This is the object to which the shapes collection is attached and for
which it will have to draw shapes
:param app: This is the FLatCAM.App usually, needed because we have to access attributes there
:param name: This is the name given to the Matplotlib axes; it needs to be unique due of
Matplotlib requurements
:param annotation_job: Make this True if the job needed is just for annotation
:param linewidth: THe width of the line (outline where is the case)
"""
self.obj = obj
self.app = app
self.annotation_job = annotation_job
2020-02-29 22:52:24 +00:00
self._shapes = {}
self.shape_dict = {}
self.shape_id = 0
self._color = None
self._face_color = None
self._visible = True
self._update = False
self._alpha = None
self._tool_tolerance = None
self._tooldia = None
self._obj = None
self._gcode_parsed = None
self._linewidth = linewidth
if name is None:
axes_name = self.obj.options['name']
else:
axes_name = name
# Axes must exist and be attached to canvas.
if axes_name not in self.app.plotcanvas.figure.axes:
self.axes = self.app.plotcanvas.new_axes(axes_name)
def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
update=False, layer=1, tolerance=0.01, obj=None, gcode_parsed=None, tool_tolerance=None, tooldia=None,
linewidth=None):
"""
This function will add shapes to the shape collection
:param shape: the Shapely shape to be added to the shape collection
:param color: edge color of the shape, hex value
:param face_color: the body color of the shape, hex value
:param alpha: level of transparency of the shape [0.0 ... 1.0]; Float
:param visible: if True will allow the shapes to be added
:param update: not used; just for compatibility with VIsPy canvas
:param layer: just for compatibility with VIsPy canvas
:param tolerance: just for compatibility with VIsPy canvas
:param obj: not used
:param gcode_parsed: not used; just for compatibility with VIsPy canvas
:param tool_tolerance: just for compatibility with VIsPy canvas
:param tooldia:
:param linewidth: the width of the line
:return:
"""
self._color = color if color is not None else "#006E20"
# self._face_color = face_color if face_color is not None else "#BBF268"
self._face_color = face_color
if linewidth is None:
line_width = self._linewidth
else:
line_width = linewidth
if len(self._color) > 7:
self._color = self._color[:7]
if self._face_color is not None:
if len(self._face_color) > 7:
self._face_color = self._face_color[:7]
# self._alpha = int(self._face_color[-2:], 16) / 255
self._alpha = 0.75
if alpha is not None:
self._alpha = alpha
self._visible = visible
self._update = update
# CNCJob object related arguments
self._obj = obj
self._gcode_parsed = gcode_parsed
self._tool_tolerance = tool_tolerance
self._tooldia = tooldia
# if self._update:
# self.clear()
try:
for sh in shape:
self.shape_id += 1
self.shape_dict.update({
'color': self._color,
'face_color': self._face_color,
'linewidth': line_width,
'alpha': self._alpha,
'visible': self._visible,
'shape': sh
})
self._shapes.update({
self.shape_id: deepcopy(self.shape_dict)
})
except TypeError:
self.shape_id += 1
self.shape_dict.update({
'color': self._color,
'face_color': self._face_color,
'linewidth': line_width,
'alpha': self._alpha,
'visible': self._visible,
'shape': shape
})
self._shapes.update({
self.shape_id: deepcopy(self.shape_dict)
})
return self.shape_id
def remove(self, shape_id, update=None):
for k in list(self._shapes.keys()):
if shape_id == k:
self._shapes.pop(k, None)
if update is True:
self.redraw()
def clear(self, update=None):
"""
Clear the canvas of the shapes.
:param update:
:return: None
"""
self._shapes.clear()
self.shape_id = 0
self.axes.cla()
try:
self.app.plotcanvas.auto_adjust_axes()
except Exception as e:
log.debug("ShapeCollectionLegacy.clear() --> %s" % str(e))
if update is True:
self.redraw()
def redraw(self, update_colors=None):
"""
This draw the shapes in the shapes collection, on canvas
:return: None
"""
path_num = 0
local_shapes = deepcopy(self._shapes)
try:
obj_type = self.obj.kind
except AttributeError:
obj_type = 'utility'
# if we don't use this then when adding each new shape, the old ones will be added again, too
# if obj_type == 'utility':
# self.axes.patches.clear()
self.axes.patches.clear()
for element in local_shapes:
if local_shapes[element]['visible'] is True:
if obj_type == 'excellon':
# Plot excellon (All polygons?)
if self.obj.options["solid"] and isinstance(local_shapes[element]['shape'], Polygon):
try:
patch = PolygonPatch(local_shapes[element]['shape'],
facecolor=local_shapes[element]['face_color'],
edgecolor=local_shapes[element]['color'],
alpha=local_shapes[element]['alpha'],
zorder=3,
linewidth=local_shapes[element]['linewidth']
)
self.axes.add_patch(patch)
except Exception as e:
log.debug("ShapeCollectionLegacy.redraw() excellon poly --> %s" % str(e))
else:
try:
if isinstance(local_shapes[element]['shape'], Polygon):
x, y = local_shapes[element]['shape'].exterior.coords.xy
self.axes.plot(x, y, 'r-', linewidth=local_shapes[element]['linewidth'])
for ints in local_shapes[element]['shape'].interiors:
x, y = ints.coords.xy
self.axes.plot(x, y, 'o-', linewidth=local_shapes[element]['linewidth'])
elif isinstance(local_shapes[element]['shape'], LinearRing):
x, y = local_shapes[element]['shape'].coords.xy
self.axes.plot(x, y, 'r-', linewidth=local_shapes[element]['linewidth'])
except Exception as e:
log.debug("ShapeCollectionLegacy.redraw() excellon no poly --> %s" % str(e))
elif obj_type == 'geometry':
if type(local_shapes[element]['shape']) == Polygon:
try:
x, y = local_shapes[element]['shape'].exterior.coords.xy
self.axes.plot(x, y, local_shapes[element]['color'],
linestyle='-',
linewidth=local_shapes[element]['linewidth'])
for ints in local_shapes[element]['shape'].interiors:
x, y = ints.coords.xy
self.axes.plot(x, y, local_shapes[element]['color'],
linestyle='-',
linewidth=local_shapes[element]['linewidth'])
except Exception as e:
log.debug("ShapeCollectionLegacy.redraw() geometry poly --> %s" % str(e))
elif type(local_shapes[element]['shape']) == LineString or \
type(local_shapes[element]['shape']) == LinearRing:
try:
x, y = local_shapes[element]['shape'].coords.xy
self.axes.plot(x, y, local_shapes[element]['color'],
linestyle='-',
linewidth=local_shapes[element]['linewidth'])
except Exception as e:
log.debug("ShapeCollectionLegacy.redraw() geometry no poly --> %s" % str(e))
elif obj_type == 'gerber':
if self.obj.options["multicolored"]:
linespec = '-'
else:
linespec = 'k-'
if self.obj.options["solid"]:
if update_colors:
gerber_fill_color = update_colors[0]
gerber_outline_color = update_colors[1]
else:
gerber_fill_color = local_shapes[element]['face_color']
gerber_outline_color = local_shapes[element]['color']
try:
patch = PolygonPatch(local_shapes[element]['shape'],
facecolor=gerber_fill_color,
edgecolor=gerber_outline_color,
alpha=local_shapes[element]['alpha'],
zorder=2,
linewidth=local_shapes[element]['linewidth'])
self.axes.add_patch(patch)
except AssertionError:
log.warning("A geometry component was not a polygon:")
log.warning(str(element))
except Exception as e:
log.debug(
"PlotCanvasLegacy.ShepeCollectionLegacy.redraw() gerber 'solid' --> %s" % str(e))
else:
try:
x, y = local_shapes[element]['shape'].exterior.xy
self.axes.plot(x, y, linespec, linewidth=local_shapes[element]['linewidth'])
for ints in local_shapes[element]['shape'].interiors:
x, y = ints.coords.xy
self.axes.plot(x, y, linespec, linewidth=local_shapes[element]['linewidth'])
except Exception as e:
log.debug("ShapeCollectionLegacy.redraw() gerber no 'solid' --> %s" % str(e))
elif obj_type == 'cncjob':
if local_shapes[element]['face_color'] is None:
try:
linespec = '--'
linecolor = local_shapes[element]['color']
# if geo['kind'][0] == 'C':
# linespec = 'k-'
x, y = local_shapes[element]['shape'].coords.xy
self.axes.plot(x, y, linespec, color=linecolor,
linewidth=local_shapes[element]['linewidth'])
except Exception as e:
log.debug("ShapeCollectionLegacy.redraw() cncjob with face_color --> %s" % str(e))
else:
try:
path_num += 1
if self.obj.ui.annotation_cb.get_value():
if isinstance(local_shapes[element]['shape'], Polygon):
self.axes.annotate(
str(path_num),
xy=local_shapes[element]['shape'].exterior.coords[0],
xycoords='data', fontsize=20)
else:
self.axes.annotate(
str(path_num),
xy=local_shapes[element]['shape'].coords[0],
xycoords='data', fontsize=20)
patch = PolygonPatch(local_shapes[element]['shape'],
facecolor=local_shapes[element]['face_color'],
edgecolor=local_shapes[element]['color'],
alpha=local_shapes[element]['alpha'], zorder=2,
linewidth=local_shapes[element]['linewidth'])
self.axes.add_patch(patch)
except Exception as e:
log.debug("ShapeCollectionLegacy.redraw() cncjob no face_color --> %s" % str(e))
elif obj_type == 'utility':
# not a FlatCAM object, must be utility
if local_shapes[element]['face_color']:
try:
patch = PolygonPatch(local_shapes[element]['shape'],
facecolor=local_shapes[element]['face_color'],
edgecolor=local_shapes[element]['color'],
alpha=local_shapes[element]['alpha'],
zorder=2,
linewidth=local_shapes[element]['linewidth'])
self.axes.add_patch(patch)
except Exception as e:
log.debug("ShapeCollectionLegacy.redraw() utility poly with face_color --> %s" % str(e))
else:
if isinstance(local_shapes[element]['shape'], Polygon):
try:
ext_shape = local_shapes[element]['shape'].exterior
if ext_shape is not None:
x, y = ext_shape.xy
self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-',
linewidth=local_shapes[element]['linewidth'])
for ints in local_shapes[element]['shape'].interiors:
if ints is not None:
x, y = ints.coords.xy
self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-',
linewidth=local_shapes[element]['linewidth'])
except Exception as e:
log.debug("ShapeCollectionLegacy.redraw() utility poly no face_color --> %s" % str(e))
else:
try:
if local_shapes[element]['shape'] is not None:
x, y = local_shapes[element]['shape'].coords.xy
self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-',
linewidth=local_shapes[element]['linewidth'])
except Exception as e:
log.debug("ShapeCollectionLegacy.redraw() utility lines no face_color --> %s" % str(e))
self.app.plotcanvas.auto_adjust_axes()
2019-09-20 15:23:54 +00:00
def set(self, text, pos, visible=True, font_size=16, color=None):
"""
This will set annotations on the canvas.
:param text: a list of text elements to be used as annotations
:param pos: a list of positions for showing the text elements above
:param visible: if True will display annotations, if False will clear them on canvas
:param font_size: the font size or the annotations
:param color: color of the annotations
:return: None
"""
if color is None:
color = "#000000FF"
if visible is not True:
self.clear()
return
if len(text) != len(pos):
self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not annotate due of a difference between the number "
"of text elements and the number of text positions."))
return
for idx in range(len(text)):
try:
self.axes.annotate(text[idx], xy=pos[idx], xycoords='data', fontsize=font_size, color=color)
except Exception as e:
log.debug("ShapeCollectionLegacy.set() --> %s" % str(e))
self.app.plotcanvas.auto_adjust_axes()
@property
def visible(self):
return self._visible
@visible.setter
def visible(self, value):
if value is False:
self.axes.cla()
self.app.plotcanvas.auto_adjust_axes()
else:
if self._visible is False:
self.redraw()
self._visible = value
def update_visibility(self, state, indexes=None):
if indexes:
for i in indexes:
if i in self._shapes:
self._shapes[i]['visible'] = state
else:
for i in self._shapes:
self._shapes[i]['visible'] = state
self.redraw()
@property
def enabled(self):
return self._visible
@enabled.setter
def enabled(self, value):
if value is False:
self.axes.cla()
self.app.plotcanvas.auto_adjust_axes()
else:
if self._visible is False:
self.redraw()
self._visible = value
# class MplCursor(Cursor):
# """
# Unfortunately this gets attached to the current axes and if a new axes is added
# it will not be showed until that axes is deleted.
# Not the kind of behavior needed here so I don't use it anymore.
# """
# def __init__(self, axes, color='red', linewidth=1):
#
# super().__init__(ax=axes, useblit=True, color=color, linewidth=linewidth)
# self._enabled = True
#
# self.axes = axes
# self.color = color
# self.linewidth = linewidth
#
# self.x = None
# self.y = None
#
# @property
# def enabled(self):
# return True if self._enabled else False
#
# @enabled.setter
# def enabled(self, value):
# self._enabled = value
# self.visible = self._enabled
# self.canvas.draw()
#
# def onmove(self, event):
# pass
#
# def set_data(self, event, pos):
# """Internal event handler to draw the cursor when the mouse moves."""
# self.x = pos[0]
# self.y = pos[1]
#
# if self.ignore(event):
# return
# if not self.canvas.widgetlock.available(self):
# return
# if event.inaxes != self.ax:
# self.linev.set_visible(False)
# self.lineh.set_visible(False)
#
# if self.needclear:
# self.canvas.draw()
# self.needclear = False
# return
# self.needclear = True
# if not self.visible:
# return
# self.linev.set_xdata((self.x, self.x))
#
# self.lineh.set_ydata((self.y, self.y))
# self.linev.set_visible(self.visible and self.vertOn)
# self.lineh.set_visible(self.visible and self.horizOn)
#
# self._update()