2019-09-20 14:25:32 +00:00
|
|
|
############################################################
|
|
|
|
# FlatCAM: 2D Post-processing for Manufacturing #
|
|
|
|
# http://caram.cl/software/flatcam #
|
|
|
|
# Author: Juan Pablo Caram (c) #
|
|
|
|
# Date: 2/5/2014 #
|
|
|
|
# MIT Licence #
|
2019-09-22 10:12:21 +00:00
|
|
|
# Modified by Marius Stanciu 09/21/2019 #
|
2019-09-20 14:25:32 +00:00
|
|
|
############################################################
|
|
|
|
|
2019-10-15 23:28:18 +00:00
|
|
|
from PyQt5 import QtCore
|
2019-09-24 12:47:33 +00:00
|
|
|
from PyQt5.QtCore import pyqtSignal
|
2019-09-20 14:25:32 +00:00
|
|
|
|
2019-09-21 15:26:03 +00:00
|
|
|
# needed for legacy mode
|
|
|
|
# Used for solid polygons in Matplotlib
|
|
|
|
from descartes.patch import PolygonPatch
|
|
|
|
|
2020-04-20 01:18:12 +00:00
|
|
|
from shapely.geometry import Polygon, LineString, LinearRing
|
2019-09-21 15:26:03 +00:00
|
|
|
|
|
|
|
from copy import deepcopy
|
2019-09-20 14:25:32 +00:00
|
|
|
import logging
|
|
|
|
|
2020-05-11 22:09:00 +00:00
|
|
|
import numpy as np
|
|
|
|
|
2019-09-22 10:12:21 +00:00
|
|
|
import gettext
|
2020-05-18 13:02:41 +00:00
|
|
|
import AppTranslation as fcTranslate
|
2019-09-22 10:12:21 +00:00
|
|
|
import builtins
|
|
|
|
|
2019-09-25 00:26:34 +00:00
|
|
|
# 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
|
2019-11-27 19:40:39 +00:00
|
|
|
from matplotlib.lines import Line2D
|
2020-05-11 04:29:46 +00:00
|
|
|
from matplotlib.offsetbox import AnchoredText
|
2019-09-25 00:26:34 +00:00
|
|
|
# from matplotlib.widgets import Cursor
|
|
|
|
|
2019-09-22 10:12:21 +00:00
|
|
|
fcTranslate.apply_language('strings')
|
|
|
|
if '_' not in builtins.__dict__:
|
|
|
|
_ = gettext.gettext
|
|
|
|
|
2019-09-22 17:32:29 +00:00
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
log = logging.getLogger('base')
|
|
|
|
|
|
|
|
|
|
|
|
class CanvasCache(QtCore.QObject):
|
|
|
|
"""
|
|
|
|
|
|
|
|
Case story #1:
|
|
|
|
|
|
|
|
1) No objects in the project.
|
2020-05-18 14:31:02 +00:00
|
|
|
2) Object is created (app_obj.new_object() emits object_created(obj)).
|
2019-09-20 14:25:32 +00:00
|
|
|
on_object_created() adds (i) object to collection and emits
|
2020-05-18 14:31:02 +00:00
|
|
|
(ii) app_obj.new_object_available() then calls (iii) object.plot()
|
2019-09-20 14:25:32 +00:00
|
|
|
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([])
|
|
|
|
|
2019-10-07 01:06:00 +00:00
|
|
|
if self.app.defaults['global_theme'] == 'white':
|
|
|
|
self.axes.set_facecolor('#FFFFFF')
|
|
|
|
else:
|
|
|
|
self.axes.set_facecolor('#000000')
|
|
|
|
|
2019-09-25 00:26:34 +00:00
|
|
|
self.canvas = FigureCanvas(self.figure)
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
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-05-18 14:31:02 +00:00
|
|
|
# def on_app_obj.new_object_available(self):
|
2019-09-20 14:25:32 +00:00
|
|
|
#
|
|
|
|
# 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)
|
2019-09-25 00:26:34 +00:00
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
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
|
|
|
|
|
2019-10-07 01:06:00 +00:00
|
|
|
if self.app.defaults['global_theme'] == 'white':
|
|
|
|
theme_color = '#FFFFFF'
|
|
|
|
tick_color = '#000000'
|
2020-05-11 04:29:46 +00:00
|
|
|
self.rect_hud_color = '#0000FF10'
|
|
|
|
self.text_hud_color = '#000000'
|
2019-10-07 01:06:00 +00:00
|
|
|
else:
|
|
|
|
theme_color = '#000000'
|
|
|
|
tick_color = '#FFFFFF'
|
2020-05-11 22:09:00 +00:00
|
|
|
self.rect_hud_color = '#80808040'
|
|
|
|
self.text_hud_color = '#FFFFFF'
|
2019-10-07 01:06:00 +00:00
|
|
|
|
2019-11-27 19:40:39 +00:00
|
|
|
# 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 = {}
|
2019-11-27 19:40:39 +00:00
|
|
|
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),
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
# Options
|
|
|
|
self.x_margin = 15 # pixels
|
|
|
|
self.y_margin = 25 # Pixels
|
|
|
|
|
|
|
|
# Parent container
|
|
|
|
self.container = container
|
|
|
|
|
|
|
|
# Plots go onto a single matplotlib.figure
|
2020-04-20 01:18:12 +00:00
|
|
|
self.figure = Figure(dpi=50)
|
2019-10-07 01:06:00 +00:00
|
|
|
self.figure.patch.set_visible(True)
|
|
|
|
self.figure.set_facecolor(theme_color)
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
# 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)
|
2019-10-07 01:06:00 +00:00
|
|
|
self.axes.grid(True, color='gray')
|
2019-11-27 19:40:39 +00:00
|
|
|
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)
|
2019-09-20 14:25:32 +00:00
|
|
|
|
2019-10-07 01:06:00 +00:00
|
|
|
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)
|
|
|
|
|
2019-09-27 01:42:28 +00:00
|
|
|
self.ch_line = None
|
|
|
|
self.cv_line = None
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
# The canvas is the top level container (FigureCanvasQTAgg)
|
|
|
|
self.canvas = FigureCanvas(self.figure)
|
2019-09-25 00:26:34 +00:00
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
|
|
|
|
self.canvas.setFocus()
|
|
|
|
self.native = self.canvas
|
|
|
|
|
2019-09-22 01:34:19 +00:00
|
|
|
self.adjust_axes(-10, -10, 100, 100)
|
2019-09-20 14:25:32 +00:00
|
|
|
# self.canvas.set_can_focus(True) # For key press
|
|
|
|
|
|
|
|
# Attach to parent
|
2020-04-20 01:18:12 +00:00
|
|
|
# self.container.attach(self.canvas, 0, 0, 600, 400)
|
2019-09-20 14:25:32 +00:00
|
|
|
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)
|
|
|
|
|
2019-09-25 00:26:34 +00:00
|
|
|
# ################### NOT IMPLEMENTED YET - EXPERIMENTAL #######################
|
2019-09-20 14:25:32 +00:00
|
|
|
# ## Bitmap Cache
|
2019-09-25 00:26:34 +00:00
|
|
|
# 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)
|
|
|
|
# ##############################################################################
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
# Events
|
2019-09-20 22:10:32 +00:00
|
|
|
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)
|
2019-09-20 14:25:32 +00:00
|
|
|
# self.canvas.connect('configure-event', self.auto_adjust_axes)
|
2019-09-20 22:10:32 +00:00
|
|
|
self.aaa = self.graph_event_connect('resize_event', self.auto_adjust_axes)
|
2019-09-20 14:25:32 +00:00
|
|
|
# self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
|
|
|
|
# self.canvas.connect("scroll-event", self.on_scroll)
|
2019-09-20 22:10:32 +00:00
|
|
|
self.osc = self.graph_event_connect('scroll_event', self.on_scroll)
|
2019-09-20 14:25:32 +00:00
|
|
|
# self.graph_event_connect('key_press_event', self.on_key_down)
|
|
|
|
# self.graph_event_connect('key_release_event', self.on_key_up)
|
2019-09-20 22:10:32 +00:00
|
|
|
self.odr = self.graph_event_connect('draw_event', self.on_draw)
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
self.key = None
|
|
|
|
|
|
|
|
self.pan_axes = []
|
|
|
|
self.panning = False
|
2019-09-25 00:26:34 +00:00
|
|
|
self.mouse = [0, 0]
|
2019-09-27 01:42:28 +00:00
|
|
|
self.big_cursor = False
|
2019-12-28 01:59:05 +00:00
|
|
|
self.big_cursor_isdisabled = None
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
# signal is the mouse is dragging
|
|
|
|
self.is_dragging = False
|
|
|
|
|
2020-05-12 21:59:35 +00:00
|
|
|
self.mouse_press_pos = None
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
# signal if there is a doubleclick
|
|
|
|
self.is_dblclk = False
|
|
|
|
|
2020-05-11 04:29:46 +00:00
|
|
|
self.hud_enabled = False
|
|
|
|
self.text_hud = self.Thud(plotcanvas=self)
|
|
|
|
|
2019-11-27 19:40:39 +00:00
|
|
|
# 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-05-11 04:29:46 +00:00
|
|
|
if self.app.defaults['global_hud'] is True:
|
|
|
|
self.on_toggle_hud(state=True)
|
|
|
|
|
|
|
|
def on_toggle_hud(self, state):
|
|
|
|
if state:
|
|
|
|
self.hud_enabled = True
|
|
|
|
self.text_hud.add_artist()
|
2020-05-12 21:08:56 +00:00
|
|
|
|
|
|
|
self.app.defaults['global_hud'] = True
|
2020-05-11 04:29:46 +00:00
|
|
|
else:
|
|
|
|
self.hud_enabled = False
|
|
|
|
self.text_hud.remove_artist()
|
2020-05-12 21:08:56 +00:00
|
|
|
|
|
|
|
self.app.defaults['global_hud'] = False
|
2020-05-12 21:59:35 +00:00
|
|
|
|
2020-05-11 04:29:46 +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']
|
2020-05-11 22:09:00 +00:00
|
|
|
self._text = 'Dx: %s [%s]\nDy: %s [%s]\n\nX: %s [%s]\nY: %s [%s]' % \
|
2020-05-11 04:29:46 +00:00
|
|
|
('0.0000', units, '0.0000', units, '0.0000', units, '0.0000', units)
|
|
|
|
|
2020-05-16 16:13:05 +00:00
|
|
|
# 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')
|
2020-05-11 04:29:46 +00:00
|
|
|
self.hud_holder.patch.set_boxstyle("round,pad=0.,rounding_size=0.2")
|
|
|
|
|
2020-05-11 22:09:00 +00:00
|
|
|
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)
|
2020-05-11 04:29:46 +00:00
|
|
|
self.hud_holder.patch.set_edgecolor((0, 0, 0, 0))
|
|
|
|
|
2020-05-11 22:09:00 +00:00
|
|
|
self. hud_holder.txt._text.set_color(color=text_color)
|
2020-05-11 04:29:46 +00:00
|
|
|
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)
|
|
|
|
|
2019-11-27 19:40:39 +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-05-17 12:53:13 +00:00
|
|
|
self.app.ui.wplace_label.set_value(workspace_size[:3])
|
|
|
|
self.app.ui.wplace_label.setToolTip(workspace_size)
|
2020-05-18 03:08:43 +00:00
|
|
|
self.fcapp.ui.wplace_label.setStyleSheet("""
|
|
|
|
QLabel
|
|
|
|
{
|
|
|
|
color: black;
|
|
|
|
background-color: lightgreen;
|
|
|
|
}
|
|
|
|
""")
|
2020-05-17 12:53:13 +00:00
|
|
|
|
2019-11-27 19:40:39 +00:00
|
|
|
def delete_workspace(self):
|
|
|
|
try:
|
|
|
|
self.axes.lines.remove(self.workspace_line)
|
|
|
|
self.canvas.draw()
|
|
|
|
except Exception:
|
|
|
|
pass
|
2020-05-18 03:08:43 +00:00
|
|
|
self.fcapp.ui.wplace_label.setStyleSheet("")
|
2019-11-27 19:40:39 +00:00
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
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!")
|
|
|
|
|
2019-12-28 01:59:05 +00:00
|
|
|
def new_cursor(self, axes=None, big=None):
|
2019-09-20 22:10:32 +00:00
|
|
|
# if axes is None:
|
|
|
|
# c = MplCursor(axes=self.axes, color='black', linewidth=1)
|
|
|
|
# else:
|
|
|
|
# c = MplCursor(axes=axes, color='black', linewidth=1)
|
|
|
|
|
2019-12-28 01:59:05 +00:00
|
|
|
if self.app.defaults["global_cursor_color_enabled"]:
|
|
|
|
color = self.app.defaults["global_cursor_color"]
|
2019-10-07 01:06:00 +00:00
|
|
|
else:
|
2019-12-25 15:51:37 +00:00
|
|
|
if self.app.defaults['global_theme'] == 'white':
|
|
|
|
color = '#000000'
|
|
|
|
else:
|
|
|
|
color = '#FFFFFF'
|
2019-10-07 01:06:00 +00:00
|
|
|
|
|
|
|
if big is True:
|
2019-09-27 01:42:28 +00:00
|
|
|
self.big_cursor = True
|
2019-12-28 01:59:05 +00:00
|
|
|
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
|
2019-09-27 14:19:44 +00:00
|
|
|
else:
|
|
|
|
self.big_cursor = False
|
|
|
|
|
2019-09-20 22:10:32 +00:00
|
|
|
c = FakeCursor()
|
2019-09-27 01:42:28 +00:00
|
|
|
c.mouse_state_updated.connect(self.clear_cursor)
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
return c
|
|
|
|
|
2019-12-25 15:51:37 +00:00
|
|
|
def draw_cursor(self, x_pos, y_pos, color=None):
|
2019-09-25 00:26:34 +00:00
|
|
|
"""
|
|
|
|
Draw a cursor at the mouse grid snapped position
|
|
|
|
|
|
|
|
:param x_pos: mouse x position
|
|
|
|
:param y_pos: mouse y position
|
2019-12-26 12:38:23 +00:00
|
|
|
:param color: custom color of the mouse
|
2019-09-25 00:26:34 +00:00
|
|
|
:return:
|
|
|
|
"""
|
2019-12-26 12:38:23 +00:00
|
|
|
|
2019-09-25 00:26:34 +00:00
|
|
|
# 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:
|
2019-12-25 15:51:37 +00:00
|
|
|
if color:
|
|
|
|
color = color
|
2019-10-07 01:06:00 +00:00
|
|
|
else:
|
2019-12-25 15:51:37 +00:00
|
|
|
if self.app.defaults['global_theme'] == 'white':
|
|
|
|
color = '#000000'
|
|
|
|
else:
|
|
|
|
color = '#FFFFFF'
|
2019-10-07 01:06:00 +00:00
|
|
|
|
2019-09-27 01:42:28 +00:00
|
|
|
if self.big_cursor is False:
|
|
|
|
try:
|
2020-05-11 22:09:00 +00:00
|
|
|
x, y = self.snap(x_pos, y_pos)
|
2019-09-27 01:42:28 +00:00
|
|
|
|
|
|
|
# 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
|
2020-04-20 01:18:12 +00:00
|
|
|
pointer_size = int(float(self.app.defaults["global_cursor_size"]) * 1.65)
|
2019-12-25 15:51:37 +00:00
|
|
|
elements = self.axes.plot(x, y, '+', color=color, ms=pointer_size,
|
|
|
|
mew=self.app.defaults["global_cursor_width"], animated=True)
|
2019-09-27 01:42:28 +00:00
|
|
|
for el in elements:
|
|
|
|
self.axes.draw_artist(el)
|
2019-12-28 01:59:05 +00:00
|
|
|
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)
|
2019-12-25 15:51:37 +00:00
|
|
|
except Exception:
|
2019-09-27 01:42:28 +00:00
|
|
|
# this happen at app initialization since self.app.geo_editor does not exist yet
|
2019-10-07 01:06:00 +00:00
|
|
|
# I could reshuffle the object instantiating order but what's the point?
|
|
|
|
# I could crash something else and that's pythonic, too
|
2019-09-27 01:42:28 +00:00
|
|
|
pass
|
|
|
|
self.canvas.draw_idle()
|
2019-09-25 00:26:34 +00:00
|
|
|
|
2020-05-11 22:09:00 +00:00
|
|
|
self.canvas.blit(self.axes.bbox)
|
2019-09-25 00:26:34 +00:00
|
|
|
|
2019-09-24 12:47:33 +00:00
|
|
|
def clear_cursor(self, state):
|
|
|
|
if state is True:
|
2019-12-28 01:59:05 +00:00
|
|
|
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
|
2019-12-26 12:38:23 +00:00
|
|
|
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])
|
2019-09-24 12:47:33 +00:00
|
|
|
else:
|
2019-09-27 14:19:44 +00:00
|
|
|
if self.big_cursor is True:
|
2019-12-28 01:59:05 +00:00
|
|
|
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))
|
2019-09-24 12:47:33 +00:00
|
|
|
self.canvas.restore_region(self.background)
|
|
|
|
self.canvas.blit(self.axes.bbox)
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
def on_key_down(self, event):
|
|
|
|
"""
|
|
|
|
|
|
|
|
:param event:
|
|
|
|
:return:
|
|
|
|
"""
|
2020-04-27 07:03:22 +00:00
|
|
|
log.debug('on_key_down(): ' + str(event.key))
|
2019-09-20 14:25:32 +00:00
|
|
|
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:
|
2020-04-27 07:03:22 +00:00
|
|
|
log.warning("KeyError in MPL figure.clf()")
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
# Re-build
|
|
|
|
self.figure.add_axes(self.axes)
|
|
|
|
self.axes.set_aspect(1)
|
|
|
|
self.axes.grid(True)
|
2019-09-22 22:25:06 +00:00
|
|
|
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)
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
# Re-draw
|
|
|
|
self.canvas.draw_idle()
|
|
|
|
|
2019-09-22 22:25:06 +00:00
|
|
|
def redraw(self):
|
|
|
|
"""
|
|
|
|
Created only to serve for compatibility with the VisPy plotcanvas (the other graphic engine, 3D)
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
self.clear()
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
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()")
|
|
|
|
|
2019-09-22 21:17:45 +00:00
|
|
|
if not self.app.collection.get_list():
|
|
|
|
xmin = -10
|
|
|
|
ymin = -10
|
|
|
|
xmax = 100
|
|
|
|
ymax = 100
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
width = xmax - xmin
|
|
|
|
height = ymax - ymin
|
|
|
|
try:
|
|
|
|
r = width / height
|
|
|
|
except ZeroDivisionError:
|
2020-04-27 07:03:22 +00:00
|
|
|
log.error("Height is %f" % height)
|
2019-09-20 14:25:32 +00:00
|
|
|
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)
|
|
|
|
|
2019-09-21 10:07:30 +00:00
|
|
|
def fit_view(self):
|
|
|
|
self.auto_adjust_axes()
|
|
|
|
|
2019-09-25 12:02:59 +00:00
|
|
|
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():
|
2019-10-07 01:06:00 +00:00
|
|
|
ax.set_xlim((x - half_width, x + half_width))
|
2019-09-25 12:02:59 +00:00
|
|
|
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])
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
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
|
|
|
|
"""
|
|
|
|
|
2019-09-21 23:03:19 +00:00
|
|
|
factor = 1 / factor
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
xmin, xmax = self.axes.get_xlim()
|
|
|
|
ymin, ymax = self.axes.get_ylim()
|
2019-09-23 14:08:46 +00:00
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
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])
|
|
|
|
|
2019-09-25 12:02:59 +00:00
|
|
|
def pan(self, x, y, idle=True):
|
2019-09-20 14:25:32 +00:00
|
|
|
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
|
2019-09-25 12:02:59 +00:00
|
|
|
if idle:
|
|
|
|
self.canvas.draw_idle()
|
|
|
|
else:
|
|
|
|
self.canvas.draw()
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
# #### 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
|
|
|
|
"""
|
2019-09-25 12:02:59 +00:00
|
|
|
new_ax = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
|
|
|
|
return new_ax
|
2019-09-20 14:25:32 +00:00
|
|
|
|
2019-09-23 14:52:34 +00:00
|
|
|
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
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
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)
|
2019-09-21 23:03:19 +00:00
|
|
|
else:
|
|
|
|
self.zoom(1.5, self.mouse)
|
2019-09-20 14:25:32 +00:00
|
|
|
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-05-12 21:59:35 +00:00
|
|
|
self.mouse_press_pos = (event.x, event.y)
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
# 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-05-12 21:59:35 +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
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
# 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
|
|
|
|
|
2019-09-24 12:47:33 +00:00
|
|
|
# And update the cursor
|
2019-12-26 12:38:23 +00:00
|
|
|
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])
|
2019-09-24 12:47:33 +00:00
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
def on_mouse_move(self, event):
|
|
|
|
"""
|
2019-09-24 12:47:33 +00:00
|
|
|
Mouse movement event handler. Stores the coordinates. Updates view on pan.
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
:param event: Contains information about the event.
|
|
|
|
:return: None
|
|
|
|
"""
|
2019-09-20 22:10:32 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
x = float(event.xdata)
|
|
|
|
y = float(event.ydata)
|
|
|
|
except TypeError:
|
|
|
|
return
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
self.mouse = [event.xdata, event.ydata]
|
|
|
|
|
2019-09-20 22:10:32 +00:00
|
|
|
self.canvas.restore_region(self.background)
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
# 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)
|
|
|
|
|
2019-09-24 12:47:33 +00:00
|
|
|
# x_pan, y_pan = self.app.geo_editor.snap(event.xdata, event.ydata)
|
|
|
|
# self.draw_cursor(x_pos=x_pan, y_pos=y_pan)
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
# Async re-draw (redraws only on thread idle state, uses timer on backend)
|
|
|
|
self.canvas.draw_idle()
|
|
|
|
|
|
|
|
# #### Temporary place-holder for cached update #####
|
2020-05-11 22:09:00 +00:00
|
|
|
# self.update_screen_request.emit([0, 0, 0, 0, 0])
|
2019-09-20 14:25:32 +00:00
|
|
|
|
2019-12-26 12:38:23 +00:00
|
|
|
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)
|
2019-09-24 12:47:33 +00:00
|
|
|
# self.canvas.blit(self.axes.bbox)
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
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
|
|
|
|
"""
|
2019-09-22 21:17:45 +00:00
|
|
|
return position[0], position[1]
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
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-05-11 22:09:00 +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)
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
|
2019-09-24 12:47:33 +00:00
|
|
|
class FakeCursor(QtCore.QObject):
|
2019-09-22 10:12:21 +00:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
2019-09-24 12:47:33 +00:00
|
|
|
|
|
|
|
mouse_state_updated = pyqtSignal(bool)
|
|
|
|
|
2019-09-20 22:10:32 +00:00
|
|
|
def __init__(self):
|
2019-09-24 12:47:33 +00:00
|
|
|
super().__init__()
|
2019-09-20 22:10:32 +00:00
|
|
|
self._enabled = True
|
|
|
|
|
|
|
|
@property
|
|
|
|
def enabled(self):
|
|
|
|
return True if self._enabled else False
|
|
|
|
|
|
|
|
@enabled.setter
|
|
|
|
def enabled(self, value):
|
|
|
|
self._enabled = value
|
2019-09-24 12:47:33 +00:00
|
|
|
self.mouse_state_updated.emit(value)
|
2019-09-20 22:10:32 +00:00
|
|
|
|
2019-09-22 10:12:21 +00:00
|
|
|
def set_data(self, pos, **kwargs):
|
|
|
|
"""Internal event handler to draw the cursor when the mouse moves."""
|
2019-12-26 12:38:23 +00:00
|
|
|
return
|
2019-09-22 10:12:21 +00:00
|
|
|
|
2019-09-20 22:10:32 +00:00
|
|
|
|
2019-09-21 23:38:56 +00:00
|
|
|
class ShapeCollectionLegacy:
|
2019-09-22 10:12:21 +00:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
2020-04-20 01:18:12 +00:00
|
|
|
def __init__(self, obj, app, name=None, annotation_job=None, linewidth=1):
|
2019-09-22 10:12:21 +00:00
|
|
|
"""
|
2019-09-20 14:25:32 +00:00
|
|
|
|
2020-04-20 01:18:12 +00:00
|
|
|
:param obj: This is the object to which the shapes collection is attached and for
|
2020-05-05 23:10:18 +00:00
|
|
|
which it will have to draw shapes
|
2020-04-20 01:18:12 +00:00
|
|
|
: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
|
2020-05-05 23:10:18 +00:00
|
|
|
Matplotlib requurements
|
2020-04-20 01:18:12 +00:00
|
|
|
: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)
|
2019-09-22 10:12:21 +00:00
|
|
|
"""
|
2019-09-21 15:26:03 +00:00
|
|
|
self.obj = obj
|
|
|
|
self.app = app
|
2019-09-22 10:12:21 +00:00
|
|
|
self.annotation_job = annotation_job
|
2019-09-21 15:26:03 +00:00
|
|
|
|
2020-02-29 22:52:24 +00:00
|
|
|
self._shapes = {}
|
|
|
|
self.shape_dict = {}
|
2019-09-21 15:26:03 +00:00
|
|
|
self.shape_id = 0
|
|
|
|
|
|
|
|
self._color = None
|
|
|
|
self._face_color = None
|
|
|
|
self._visible = True
|
|
|
|
self._update = False
|
2019-09-21 23:03:19 +00:00
|
|
|
self._alpha = None
|
2019-09-21 23:38:56 +00:00
|
|
|
self._tool_tolerance = None
|
|
|
|
self._tooldia = None
|
2019-09-21 15:26:03 +00:00
|
|
|
|
|
|
|
self._obj = None
|
|
|
|
self._gcode_parsed = None
|
|
|
|
|
2020-04-20 01:18:12 +00:00
|
|
|
self._linewidth = linewidth
|
|
|
|
|
2019-09-21 15:26:03 +00:00
|
|
|
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)
|
2019-09-20 14:25:32 +00:00
|
|
|
|
2019-09-20 22:10:32 +00:00
|
|
|
def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
|
2019-10-05 22:32:21 +00:00
|
|
|
update=False, layer=1, tolerance=0.01, obj=None, gcode_parsed=None, tool_tolerance=None, tooldia=None,
|
|
|
|
linewidth=None):
|
2019-09-22 11:03:20 +00:00
|
|
|
"""
|
|
|
|
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:
|
2019-10-05 22:32:21 +00:00
|
|
|
:param linewidth: the width of the line
|
2019-09-22 11:03:20 +00:00
|
|
|
:return:
|
|
|
|
"""
|
2019-12-23 00:30:56 +00:00
|
|
|
self._color = color if color is not None else "#006E20"
|
2020-04-20 01:18:12 +00:00
|
|
|
# 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
|
2019-12-23 00:30:56 +00:00
|
|
|
|
|
|
|
if len(self._color) > 7:
|
|
|
|
self._color = self._color[:7]
|
|
|
|
|
2020-04-20 01:18:12 +00:00
|
|
|
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
|
2019-12-23 00:30:56 +00:00
|
|
|
|
|
|
|
self._alpha = 0.75
|
2019-09-21 23:03:19 +00:00
|
|
|
|
2019-09-21 19:00:02 +00:00
|
|
|
if alpha is not None:
|
|
|
|
self._alpha = alpha
|
|
|
|
|
2019-09-21 15:26:03 +00:00
|
|
|
self._visible = visible
|
|
|
|
self._update = update
|
|
|
|
|
2019-09-22 10:12:21 +00:00
|
|
|
# CNCJob object related arguments
|
2019-09-21 15:26:03 +00:00
|
|
|
self._obj = obj
|
|
|
|
self._gcode_parsed = gcode_parsed
|
|
|
|
self._tool_tolerance = tool_tolerance
|
|
|
|
self._tooldia = tooldia
|
|
|
|
|
2019-09-21 19:00:02 +00:00
|
|
|
# if self._update:
|
|
|
|
# self.clear()
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
try:
|
|
|
|
for sh in shape:
|
2019-09-21 15:26:03 +00:00
|
|
|
self.shape_id += 1
|
|
|
|
self.shape_dict.update({
|
|
|
|
'color': self._color,
|
|
|
|
'face_color': self._face_color,
|
2020-04-20 01:18:12 +00:00
|
|
|
'linewidth': line_width,
|
2019-09-21 19:00:02 +00:00
|
|
|
'alpha': self._alpha,
|
2019-09-21 15:26:03 +00:00
|
|
|
'shape': sh
|
|
|
|
})
|
|
|
|
|
|
|
|
self._shapes.update({
|
|
|
|
self.shape_id: deepcopy(self.shape_dict)
|
|
|
|
})
|
2019-09-20 14:25:32 +00:00
|
|
|
except TypeError:
|
2019-09-21 15:26:03 +00:00
|
|
|
self.shape_id += 1
|
|
|
|
self.shape_dict.update({
|
|
|
|
'color': self._color,
|
|
|
|
'face_color': self._face_color,
|
2020-04-20 01:18:12 +00:00
|
|
|
'linewidth': line_width,
|
2019-09-21 19:00:02 +00:00
|
|
|
'alpha': self._alpha,
|
2019-09-21 15:26:03 +00:00
|
|
|
'shape': shape
|
|
|
|
})
|
2019-09-20 14:25:32 +00:00
|
|
|
|
2019-09-21 15:26:03 +00:00
|
|
|
self._shapes.update({
|
|
|
|
self.shape_id: deepcopy(self.shape_dict)
|
|
|
|
})
|
|
|
|
|
|
|
|
return self.shape_id
|
2019-09-20 22:10:32 +00:00
|
|
|
|
2019-11-25 19:03:12 +00:00
|
|
|
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()
|
|
|
|
|
2019-09-20 14:25:32 +00:00
|
|
|
def clear(self, update=None):
|
2019-09-22 11:03:20 +00:00
|
|
|
"""
|
|
|
|
Clear the canvas of the shapes.
|
|
|
|
|
|
|
|
:param update:
|
|
|
|
:return: None
|
|
|
|
"""
|
2019-09-21 15:26:03 +00:00
|
|
|
self._shapes.clear()
|
|
|
|
self.shape_id = 0
|
|
|
|
|
|
|
|
self.axes.cla()
|
2019-09-25 00:26:34 +00:00
|
|
|
try:
|
|
|
|
self.app.plotcanvas.auto_adjust_axes()
|
|
|
|
except Exception as e:
|
|
|
|
log.debug("ShapeCollectionLegacy.clear() --> %s" % str(e))
|
2019-09-20 14:25:32 +00:00
|
|
|
|
|
|
|
if update is True:
|
|
|
|
self.redraw()
|
|
|
|
|
2019-12-22 14:24:04 +00:00
|
|
|
def redraw(self, update_colors=None):
|
2019-09-22 11:03:20 +00:00
|
|
|
"""
|
|
|
|
This draw the shapes in the shapes collection, on canvas
|
|
|
|
|
|
|
|
:return: None
|
|
|
|
"""
|
2019-10-28 13:03:21 +00:00
|
|
|
|
2019-09-21 15:26:03 +00:00
|
|
|
path_num = 0
|
2019-09-21 23:03:19 +00:00
|
|
|
local_shapes = deepcopy(self._shapes)
|
2019-09-21 21:05:35 +00:00
|
|
|
|
2019-09-21 19:00:02 +00:00
|
|
|
try:
|
|
|
|
obj_type = self.obj.kind
|
|
|
|
except AttributeError:
|
|
|
|
obj_type = 'utility'
|
2019-09-21 23:38:56 +00:00
|
|
|
|
2019-09-21 15:26:03 +00:00
|
|
|
if self._visible:
|
2019-10-28 13:03:21 +00:00
|
|
|
# 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()
|
|
|
|
|
2019-09-21 23:03:19 +00:00
|
|
|
for element in local_shapes:
|
2019-09-21 19:00:02 +00:00
|
|
|
if obj_type == 'excellon':
|
2019-09-21 15:26:03 +00:00
|
|
|
# Plot excellon (All polygons?)
|
2019-09-21 23:03:19 +00:00
|
|
|
if self.obj.options["solid"] and isinstance(local_shapes[element]['shape'], Polygon):
|
2020-04-20 01:18:12 +00:00
|
|
|
try:
|
|
|
|
patch = PolygonPatch(local_shapes[element]['shape'],
|
|
|
|
facecolor="#C40000",
|
|
|
|
edgecolor="#750000",
|
|
|
|
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))
|
2019-09-21 15:26:03 +00:00
|
|
|
else:
|
2020-04-20 01:18:12 +00:00
|
|
|
try:
|
|
|
|
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'])
|
|
|
|
except Exception as e:
|
|
|
|
log.debug("ShapeCollectionLegacy.redraw() excellon no poly --> %s" % str(e))
|
2019-09-21 23:38:56 +00:00
|
|
|
elif obj_type == 'geometry':
|
2019-09-21 23:03:19 +00:00
|
|
|
if type(local_shapes[element]['shape']) == Polygon:
|
2020-04-20 01:18:12 +00:00
|
|
|
try:
|
|
|
|
x, y = local_shapes[element]['shape'].exterior.coords.xy
|
2019-10-05 22:32:21 +00:00
|
|
|
self.axes.plot(x, y, local_shapes[element]['color'],
|
|
|
|
linestyle='-',
|
|
|
|
linewidth=local_shapes[element]['linewidth'])
|
2020-04-20 01:18:12 +00:00
|
|
|
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))
|
2019-09-21 23:38:56 +00:00
|
|
|
elif type(local_shapes[element]['shape']) == LineString or \
|
|
|
|
type(local_shapes[element]['shape']) == LinearRing:
|
|
|
|
|
2020-04-20 01:18:12 +00:00
|
|
|
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))
|
2019-09-21 19:00:02 +00:00
|
|
|
elif obj_type == 'gerber':
|
2019-09-21 15:26:03 +00:00
|
|
|
if self.obj.options["multicolored"]:
|
|
|
|
linespec = '-'
|
|
|
|
else:
|
|
|
|
linespec = 'k-'
|
|
|
|
|
|
|
|
if self.obj.options["solid"]:
|
2019-12-22 14:24:04 +00:00
|
|
|
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']
|
|
|
|
|
2019-09-21 15:26:03 +00:00
|
|
|
try:
|
2019-09-21 23:03:19 +00:00
|
|
|
patch = PolygonPatch(local_shapes[element]['shape'],
|
2019-12-22 14:24:04 +00:00
|
|
|
facecolor=gerber_fill_color,
|
|
|
|
edgecolor=gerber_outline_color,
|
2019-09-21 23:03:19 +00:00
|
|
|
alpha=local_shapes[element]['alpha'],
|
2020-04-20 01:18:12 +00:00
|
|
|
zorder=2,
|
|
|
|
linewidth=local_shapes[element]['linewidth'])
|
2019-09-21 15:26:03 +00:00
|
|
|
self.axes.add_patch(patch)
|
|
|
|
except AssertionError:
|
2020-04-27 07:03:22 +00:00
|
|
|
log.warning("A geometry component was not a polygon:")
|
|
|
|
log.warning(str(element))
|
2019-12-22 14:24:04 +00:00
|
|
|
except Exception as e:
|
2020-04-27 07:03:22 +00:00
|
|
|
log.debug(
|
2020-04-20 01:18:12 +00:00
|
|
|
"PlotCanvasLegacy.ShepeCollectionLegacy.redraw() gerber 'solid' --> %s" % str(e))
|
2019-09-21 15:26:03 +00:00
|
|
|
else:
|
2020-04-20 01:18:12 +00:00
|
|
|
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))
|
2019-09-21 19:00:02 +00:00
|
|
|
elif obj_type == 'cncjob':
|
2019-09-21 15:26:03 +00:00
|
|
|
|
2019-09-21 23:03:19 +00:00
|
|
|
if local_shapes[element]['face_color'] is None:
|
2020-04-20 01:18:12 +00:00
|
|
|
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))
|
2019-09-21 15:26:03 +00:00
|
|
|
else:
|
2020-04-20 01:18:12 +00:00
|
|
|
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))
|
2019-09-21 19:00:02 +00:00
|
|
|
elif obj_type == 'utility':
|
|
|
|
# not a FlatCAM object, must be utility
|
2019-09-21 23:03:19 +00:00
|
|
|
if local_shapes[element]['face_color']:
|
2019-09-21 19:00:02 +00:00
|
|
|
try:
|
2019-09-21 23:03:19 +00:00
|
|
|
patch = PolygonPatch(local_shapes[element]['shape'],
|
|
|
|
facecolor=local_shapes[element]['face_color'],
|
|
|
|
edgecolor=local_shapes[element]['color'],
|
|
|
|
alpha=local_shapes[element]['alpha'],
|
2020-04-20 01:18:12 +00:00
|
|
|
zorder=2,
|
|
|
|
linewidth=local_shapes[element]['linewidth'])
|
2019-10-28 13:03:21 +00:00
|
|
|
|
2019-09-21 19:00:02 +00:00
|
|
|
self.axes.add_patch(patch)
|
2019-09-21 23:03:19 +00:00
|
|
|
except Exception as e:
|
2020-04-20 01:18:12 +00:00
|
|
|
log.debug("ShapeCollectionLegacy.redraw() utility poly with face_color --> %s" % str(e))
|
2019-09-21 19:00:02 +00:00
|
|
|
else:
|
2019-09-21 23:03:19 +00:00
|
|
|
if isinstance(local_shapes[element]['shape'], Polygon):
|
2020-04-20 01:18:12 +00:00
|
|
|
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))
|
2019-09-21 19:00:02 +00:00
|
|
|
else:
|
2020-04-20 01:18:12 +00:00
|
|
|
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))
|
2019-09-21 15:26:03 +00:00
|
|
|
self.app.plotcanvas.auto_adjust_axes()
|
2019-09-20 15:23:54 +00:00
|
|
|
|
2019-09-22 10:12:21 +00:00
|
|
|
def set(self, text, pos, visible=True, font_size=16, color=None):
|
2019-09-22 11:03:20 +00:00
|
|
|
"""
|
|
|
|
This will set annotations on the canvas.
|
2019-09-22 10:12:21 +00:00
|
|
|
|
2019-09-22 11:03:20 +00:00
|
|
|
: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
|
|
|
|
"""
|
2019-09-22 10:12:21 +00:00
|
|
|
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()
|
|
|
|
|
2019-09-21 15:26:03 +00:00
|
|
|
@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
|
2019-09-22 10:12:21 +00:00
|
|
|
|
|
|
|
@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
|
2019-09-25 00:26:34 +00:00
|
|
|
|
|
|
|
# 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()
|