Source code for pyleecan.GUI.Dxf.DXF_Slot

from logging import getLogger
from os.path import dirname, isfile, basename, splitext

import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import (
    FigureCanvasQTAgg,
    NavigationToolbar2QT as NavigationToolbar,
)
from ezdxf import readfile
from numpy import angle as np_angle
from numpy import argmin, array, exp, angle
from PySide2.QtCore import QSize, Qt, QUrl
from PySide2.QtGui import QIcon, QPixmap, QFont, QDesktopServices
from PySide2.QtWidgets import QComboBox, QDialog, QFileDialog, QMessageBox, QPushButton


from ...Classes.LamSlot import LamSlot
from ...Classes.SlotUD import SlotUD
from ...Classes.Segment import Segment
from ...definitions import config_dict
from ...Functions.Plot.set_plot_gui_icon import set_plot_gui_icon
from ...GUI.Dxf.dxf_to_pyleecan import dxf_to_pyleecan_list, convert_dxf_with_FEMM
from ...GUI.Resources import pixmap_dict
from ...GUI.Tools.MPLCanvas import MPLCanvas
from ...loggers import GUI_LOG_NAME
from .Ui_DXF_Slot import Ui_DXF_Slot
from ...Functions.init_fig import init_fig

# Column index for table
TYPE_COL = 0
DEL_COL = 1
HL_COL = 2
WIND_COLOR = config_dict["PLOT"]["COLOR_DICT"]["BAR_COLOR"]
FONT_SIZE = 12
FONT_NAME = config_dict["PLOT"]["FONT_NAME"]
Z_TOL = 1e-4  # Point comparison tolerance


[docs]class DXF_Slot(Ui_DXF_Slot, QDialog): """Dialog to create SlotUD objects from DXF files""" convert_dxf_with_FEMM = convert_dxf_with_FEMM def __init__(self, dxf_path=None, Zs=None, lam=None, is_notch=False): """Initialize the Dialog Parameters ---------- self : DXF_Slot a DXF_Slot object dxf_path : str Path to a dxf file to read Zs : int Number of slot/notch lam : Lamination Lamination to add the slot to is_notch : bool True if the DXF is meant for a notch """ # Widget setup QDialog.__init__(self) self.setupUi(self) # Init properties self.line_list = list() # List of line from DXF self.selected_list = list() # List of currently selected lines self.lam = lam self.is_notch = is_notch self.Zcenter = 0 # For offset # Tutorial video link self.url = "https://pyleecan.org/videos.html#feature-tutorials" self.b_tuto.setEnabled(True) # Adapt GUI for notches if self.is_notch: self.g_active.hide() self.in_Zs.setText("Number of notches") self.setWindowTitle("Define Notch from DXF") # Initialize the graph self.init_graph() # Not used yet self.lf_axe_angle.hide() self.in_axe_angle.hide() # Set DXF edit widget self.lf_center_x.setValue(0) self.lf_center_y.setValue(0) self.lf_scaling.validator().setBottom(0) self.lf_scaling.setValue(1) # Set default values if Zs is not None: self.si_Zs.setValue(Zs) # Setup Path selector for DXF files self.dxf_path = dxf_path self.w_path_selector.obj = self self.w_path_selector.param_name = "dxf_path" self.w_path_selector.verbose_name = "DXF File" self.w_path_selector.extension = "DXF file (*.dxf)" self.w_path_selector.set_path_txt(self.dxf_path) self.w_path_selector.update() # Load the DXF file if provided if self.dxf_path is not None and isfile(self.dxf_path): self.open_document() # Set font font = QFont() font.setFamily(FONT_NAME) font.setPointSize(FONT_SIZE) self.textBrowser.setFont(font) # Connect signals to slot self.w_path_selector.pathChanged.connect(self.open_document) self.b_save.pressed.connect(self.save) self.b_plot.pressed.connect(self.plot) self.b_reset.pressed.connect(self.update_graph) self.b_cancel.pressed.connect(self.remove_selection) self.b_tuto.pressed.connect(self.open_tuto) self.is_convert.toggled.connect(self.enable_tolerance) self.lf_center_x.editingFinished.connect(self.set_center) self.lf_center_y.editingFinished.connect(self.set_center) # Display the GUI self.show()
[docs] def enable_tolerance(self): """Enable/Disable tolerance widget""" self.lf_tol.setEnabled(self.is_convert.isChecked()) self.in_tol.setEnabled(self.is_convert.isChecked())
[docs] def open_document(self): """Open a new dxf in the viewer Parameters ---------- self : DXF_Slot a DXF_Slot object """ # Check convertion if self.is_convert.isChecked(): getLogger(GUI_LOG_NAME).info("Converting dxf file: " + self.dxf_path) self.dxf_path = self.convert_dxf_with_FEMM( self.dxf_path, self.lf_tol.value() ) self.w_path_selector.blockSignals(True) self.w_path_selector.set_path_txt(self.dxf_path) self.w_path_selector.blockSignals(False) getLogger(GUI_LOG_NAME).debug("Reading dxf file: " + self.dxf_path) # Read the DXF file try: document = readfile(self.dxf_path) modelspace = document.modelspace() # Convert DXF to pyleecan objects self.line_list = dxf_to_pyleecan_list(modelspace) # Display self.selected_list = [False for line in self.line_list] self.update_graph() except Exception as e: QMessageBox().critical( self, self.tr("Error"), self.tr("Error while reading dxf file:\n" + str(e)), )
[docs] def init_graph(self): """Initialize the viewer Parameters ---------- self : DXF_Slot a DXF_Slot object """ # Init fig fig, axes = plt.subplots(tight_layout=False) self.fig = fig self.axes = axes # Set plot layout canvas = FigureCanvasQTAgg(fig) toolbar = NavigationToolbar(canvas, self) # Remove Subplots button unwanted_buttons = ["Subplots", "Customize", "Save"] for x in toolbar.actions(): if x.text() in unwanted_buttons: toolbar.removeAction(x) # Adding custom icon on mpl toobar icons_buttons = [ "Home", "Pan", "Zoom", "Back", "Forward", ] for action in toolbar.actions(): if action.text() in icons_buttons and "mpl_" + action.text() in pixmap_dict: action.setIcon(QIcon(pixmap_dict["mpl_" + action.text()])) # Change default file name canvas.get_default_filename = "DXF_slot_visu.png" self.layout_plot.insertWidget(1, toolbar) self.layout_plot.insertWidget(2, canvas) self.canvas = canvas axes.set_axis_off() self.toolbar = toolbar self.xlim = self.axes.get_xlim() self.ylim = self.axes.get_ylim() def on_draw(event): self.xlim = self.axes.get_xlim() self.ylim = self.axes.get_ylim() # Setup interaction with graph def select_line(event): """Function to select/unselect the closest line from click""" # Ignore if matplotlib action is clicked is_ignore = False for action in self.toolbar.actions(): if action.isChecked(): is_ignore = True if not is_ignore: X = event.xdata # X position of the click Y = event.ydata # Y position of the click # Get closer pyleecan object Z = X + 1j * Y min_dist = float("inf") closest_id = -1 for ii, line in enumerate(self.line_list): line_dist = line.comp_distance(Z) if line_dist < min_dist: closest_id = ii min_dist = line_dist # Select/unselect line self.selected_list[closest_id] = not self.selected_list[closest_id] # Change line color point_list = array(self.line_list[closest_id].discretize(20)) if self.selected_list[closest_id]: color = "r" else: color = "k" axes.plot(point_list.real, point_list.imag, color, zorder=2) self.axes.set_xlim(self.xlim) self.axes.set_ylim(self.ylim) self.canvas.draw() def zoom(event): """Function to zoom/unzoom according the mouse wheel""" base_scale = 0.8 # Scaling factor # get the current x and y limits ax = self.axes cur_xlim = ax.get_xlim() cur_ylim = ax.get_ylim() cur_xrange = (cur_xlim[1] - cur_xlim[0]) * 0.5 cur_yrange = (cur_ylim[1] - cur_ylim[0]) * 0.5 xdata = event.xdata # get event x location ydata = event.ydata # get event y location if event.button == "down": # deal with zoom in scale_factor = 1 / base_scale elif event.button == "up": # deal with zoom out scale_factor = base_scale else: # deal with something that should never happen scale_factor = 1 # set new limits ax.set_xlim( [xdata - cur_xrange * scale_factor, xdata + cur_xrange * scale_factor] ) ax.set_ylim( [ydata - cur_yrange * scale_factor, ydata + cur_yrange * scale_factor] ) self.canvas.draw() # force re-draw # Connect the function self.canvas.mpl_connect("draw_event", on_draw) self.canvas.mpl_connect("button_press_event", select_line) self.canvas.mpl_connect("scroll_event", zoom) # Axis cleanup axes.axis("equal") axes.set_axis_off()
[docs] def set_center(self): """Update the position of the center""" self.Zcenter = self.lf_center_x.value() + 1j * self.lf_center_y.value() self.update_graph()
[docs] def update_graph(self): """Clean and redraw all the lines in viewer Parameters ---------- self : DXF_Slot a DXF_Slot object """ fig, axes = self.fig, self.axes axes.clear() axes.set_axis_off() # Draw the lines in the correct color for ii, line in enumerate(self.line_list): point_list = array(line.discretize(20)) if self.selected_list[ii]: color = "r" else: color = "k" axes.plot(point_list.real, point_list.imag, color, zorder=1) # Add lamination center axes.plot(self.Zcenter.real, self.Zcenter.imag, "rx", zorder=0) axes.text(self.Zcenter.real, self.Zcenter.imag, "O") self.canvas.draw()
[docs] def check_selection(self): """Check if every line in the selection are connected Parameters ---------- self : DXF_Slot a DXF_Slot object Returns ------- is_line : bool True if it forms a line """ # Create list of begin and end point for all lines point_list = list() for ii, line in enumerate(self.line_list): if self.selected_list[ii]: point_list.append(line.get_begin()) point_list.append(line.get_end()) # Check with a tolerance if every point is twice in the list if len(point_list) == 0: return False # Number of point only 1 time in the list (begin and end) count_1 = 0 for p1 in point_list: count = 0 for p2 in point_list: if abs(p1 - p2) < Z_TOL: count += 1 if count == 1: count_1 += 1 if count_1 > 2: return False elif count != 2: return False return True
[docs] def remove_selection(self): # Remove selection self.selected_list = [False for line in self.line_list] self.update_graph()
[docs] def get_slot(self): """Generate the SlotUD object corresponding to the selected lines Parameters ---------- self : DXF_Slot a DXF_Slot object Returns ------- sot : SlotUD User defined slot according to selected lines """ if self.lf_scaling.value() in [0, None]: # Avoid error self.lf_scaling.setValue(1) # Get all the selected lines at proper scale line_list = list() point_list = list() # List of all begin, end of all the lines for ii, line in enumerate(self.line_list): if self.selected_list[ii]: line_list.append(line.copy()) line_list[-1].scale(self.lf_scaling.value()) point_list.append(line_list[-1].get_begin()) point_list.append(line_list[-1].get_end()) # Find begin point single_list = list() # In point list all point should be present twice # except first and last point for p1 in point_list: count = 0 for p2 in point_list: if abs(p1 - p2) < Z_TOL: count += 1 if count == 1: single_list.append(p1) assert ( len(single_list) == 2 ), "Unable to detect first and last point of the slot" # Lines must be returned in trigo order center on Ox # Begin selection can be wrong, corrected if needed after rotate Zbegin = single_list[argmin(np_angle(array(single_list)))] # Get First line id_list = list() id_list.extend( [ ii for ii, line in enumerate(line_list) if abs(line.get_begin() - Zbegin) < Z_TOL or abs(line.get_end() - Zbegin) < Z_TOL ] ) # Sort the lines (find line with current.end == line.begin) curve_list = list() curve_list.append(line_list.pop(id_list[0])) if abs(curve_list[0].get_end() - Zbegin) < Z_TOL: # Reverse begin line if line end matches with begin point curve_list[0].reverse() while len(line_list) > 0: end = curve_list[-1].get_end() for ii in range(len(line_list)): if abs(line_list[ii].get_begin() - end) < Z_TOL: break if abs(line_list[ii].get_end() - end) < Z_TOL: line_list[ii].reverse() break curve_list.append(line_list.pop(ii)) # Translate if self.Zcenter != 0: for line in curve_list: line.translate(-self.Zcenter * self.lf_scaling.value()) # Check the first and last point are matching Rint Rbo = self.lam.get_Rbo() Zbegin = curve_list[0].get_begin() Zend = curve_list[-1].get_end() if abs(abs(Zbegin) - Rbo) > Z_TOL: QMessageBox().critical( self, self.tr("Error"), self.tr( "First point of the slot is not on the bore radius:\nBore radius=" + format(Rbo, ".6g") + ", abs(First point)=" + format(abs(Zbegin), ".6g") ), ) return None if abs(abs(Zend) - Rbo) > Z_TOL: QMessageBox().critical( self, self.tr("Error"), self.tr( "Last point of the slot is not on the bore radius:\nBore radius=" + format(Rbo, ".6g") + ", abs(Last point)=" + format(abs(Zend), ".6g") ), ) return None # Rotation Z1 = curve_list[0].get_begin() Z2 = curve_list[-1].get_end() alpha = (np_angle(Z2) + np_angle(Z1)) / 2 for line in curve_list: line.rotate(-1 * alpha) # Enforce perfect match with Bore radius by adding Segments in needed Zbegin = curve_list[0].get_begin() Zb2 = Rbo * exp(1j * angle(Zbegin)) if abs(Zb2 - Zbegin) > 1e-9: curve_list.insert(0, Segment(begin=Zb2, end=Zbegin)) Zend = curve_list[-1].get_end() Ze2 = Rbo * exp(1j * angle(Zend)) if abs(Ze2 - Zend) > 1e-9: curve_list.append(Segment(begin=Zend, end=Ze2)) # Check that the lines are in trigo order (can be reversed for inner slot) if angle(curve_list[0].get_begin()) > angle(curve_list[-1].get_end()): curve_list = curve_list[::-1] for line in curve_list: line.reverse() # Create the Slot object slot = SlotUD(line_list=curve_list) slot.type_line_wind = self.c_type_line.currentIndex() begin_id = self.si_wind_begin_index.value() end_id = self.si_wind_end_index.value() if begin_id == 0 and end_id == 0: # Not defined yet slot.wind_begin_index = None slot.wind_end_index = None elif ( begin_id < len(curve_list) and end_id < len(curve_list) # and begin_id < end_id ): slot.wind_begin_index = begin_id slot.wind_end_index = end_id else: # Wrong definition slot.wind_begin_index = None slot.wind_end_index = None if self.is_notch: slot.wind_begin_index = None slot.wind_end_index = None slot.Zs = self.si_Zs.value() return slot
[docs] def plot(self): """Plot the current state of the slot Parameters ---------- self : DXF_Slot a DXF_Slot object """ if self.check_selection(): try: slot = self.get_slot() except Exception as e: err_msg = "Error in DXF slot definition:\n" + str(e) QMessageBox().critical( self, self.tr("Error"), err_msg, ) return if slot is None: return # Uncorrect slot # Lamination definition if self.lam is None: lam = LamSlot(slot=slot) Rbo = abs(slot.line_list[0].get_begin()) else: lam = self.lam.copy() lam.slot = slot try: # Left single slot with point index, Right Lamination with slot fig, (ax1, ax2) = plt.subplots(1, 2) slot.plot(fig=fig, ax=ax1) # Add the winding to slot if defined if not self.is_notch and slot.wind_begin_index is not None: surf_wind = slot.get_surface_active() surf_wind.plot(fig=fig, ax=ax1, color=WIND_COLOR, is_show_fig=False) if not self.is_notch: # Add point index for winding definition index = 0 for line in slot.line_list: Zb = line.get_begin() ax1.plot(Zb.real, Zb.imag, "rx", zorder=0) ax1.text(Zb.real, Zb.imag, str(index)) index += 1 Ze = slot.line_list[-1].get_end() ax1.plot(Ze.real, Ze.imag, "rx", zorder=0) ax1.text(Ze.real, Ze.imag, str(index)) # Plot lamination with slot and winding (if needed) lam.plot(fig=fig, ax=ax2, is_lam_only=self.is_notch) set_plot_gui_icon() except Exception as e: err_msg = "Error while plotting DXF imported slot:\n" + str(e) QMessageBox().critical( self, self.tr("Error"), err_msg, ) return
[docs] def save(self): """Save the SlotUD object in a json file Parameters ---------- self : DXF_Slot a DXF_Slot object """ if self.check_selection(): try: slot = self.get_slot() except Exception as e: err_msg = "Error in DXF slot definition:\n" + str(e) QMessageBox().critical( self, self.tr("Error"), err_msg, ) return if slot is None: return # Uncorrect slot if ( self.si_wind_begin_index.value() == 0 and self.si_wind_end_index.value() == 0 and not self.is_notch # No winding for notch ): QMessageBox().warning( self, self.tr("Warning"), self.tr( "The winding was not defined. Please use plot to see the points indices" ), ) return save_file_path = QFileDialog.getSaveFileName( self, self.tr("Save file"), dirname(self.dxf_path), "Json (*.json)" )[0] if save_file_path not in ["", ".json", None]: self.save_path = save_file_path slot.name = splitext(basename(self.save_path))[0] try: slot.save(save_file_path) self.accept() except Exception as e: err_msg = "Error while saving DXF slot json file:\n" + str(e) QMessageBox().critical( self, self.tr("Error"), err_msg, ) return
[docs] def open_tuto(self): """Open the tutorial video in a web browser""" QDesktopServices.openUrl(QUrl(self.url))