class MainWindow(wx.Frame): # Constructor def __init__(self): wx.Frame.__init__(self, parent = None, title = "PyStrEmbed-1") self.SetBackgroundColour('white') ### MENU BAR menuBar = wx.MenuBar() fileMenu = wx.Menu() menuBar.Append(fileMenu, "&File") fileOpen = fileMenu.Append(wx.ID_OPEN, "&Open", "Open file") fileSave = fileMenu.Append(wx.ID_SAVE, "&Save", "Save file") fileSaveAs = fileMenu.Append(wx.ID_SAVEAS, "&Save as", "Save file as") fileClose = fileMenu.Append(wx.ID_CLOSE, "&Close", "Close file") fileExit = fileMenu.Append(wx.ID_EXIT, "&Exit", "Exit program") partMenu = wx.Menu() menuBar.Append(partMenu, "&Parts") geomMenu = wx.Menu() menuBar.Append(geomMenu, "&Geometry") lattMenu = wx.Menu() menuBar.Append(lattMenu, "&Lattice") abtMenu = wx.Menu() menuBar.Append(abtMenu, "&About") menuAbout = abtMenu.Append(wx.ID_ABOUT,"&About", "About PyStrEmbed-1") self.SetMenuBar(menuBar) # Bindings for menu items self.Bind(wx.EVT_MENU, self.OnFileOpen, fileOpen) self.Bind(wx.EVT_MENU, self.DoNothingDialog, fileSave) self.Bind(wx.EVT_MENU, self.DoNothingDialog, fileSaveAs) self.Bind(wx.EVT_MENU, self.OnExit, fileClose) self.Bind(wx.EVT_MENU, self.OnExit, fileExit) self.Bind(wx.EVT_MENU, self.OnAbout, menuAbout) ### TOOLBAR # Main window toolbar with assembly operations self.tb = wx.ToolBar(self, style = wx.TB_NODIVIDER | wx.TB_FLAT) self.SetToolBar(self.tb) self.tb.SetToolBitmapSize((40,40)) self.tb.SetBackgroundColour('white') # File tools self.fileOpenTool = self.tb.AddTool(wx.ID_ANY, 'Open', wx.Bitmap("Images/fileopen.bmp"), bmpDisabled = wx.NullBitmap, shortHelp = 'File open', longHelp = 'File open') self.exitTool = self.tb.AddTool(wx.ID_ANY, 'Exit', wx.Bitmap("Images/fileclose.bmp"), bmpDisabled = wx.NullBitmap, shortHelp = 'Exit', longHelp = 'Exit') self.tb.AddSeparator() # Assembly tools self.insertLeftTool = self.tb.AddTool(wx.ID_ANY, 'Insert node to left', wx.Bitmap("Images/insertleft1.bmp"), bmpDisabled = wx.NullBitmap, shortHelp = 'Insert left', longHelp = 'Insert left') self.insertRightTool = self.tb.AddTool(wx.ID_ANY, 'Insert node to right', wx.Bitmap("Images/insertright1.bmp"), bmpDisabled = wx.NullBitmap, shortHelp = 'Insert right', longHelp = 'Insert right') self.adoptTool = self.tb.AddTool(wx.ID_ANY, 'Adopt node', wx.Bitmap("Images/adopt1.bmp"), bmpDisabled = wx.NullBitmap, shortHelp = 'Adopt', longHelp = 'Adopt') self.aggregateTool = self.tb.AddTool(wx.ID_ANY, 'Aggregate nodes', wx.Bitmap("Images/aggregate1.bmp"), bmpDisabled = wx.NullBitmap, shortHelp = 'Aggregate', longHelp = 'Aggregate') self.disaggregateTool = self.tb.AddTool(wx.ID_ANY, 'Disaggregate nodes', wx.Bitmap("Images/disaggregate1.bmp"), bmpDisabled = wx.NullBitmap, shortHelp = 'Disaggregate', longHelp = 'Disaggregate') self.tb.Realize() # Bind toolbar tools to actions self.Bind(wx.EVT_TOOL, self.OnFileOpen, self.fileOpenTool) self.Bind(wx.EVT_TOOL, self.OnExit, self.exitTool) self.Bind(wx.EVT_TOOL, self.DoNothingDialog, self.insertLeftTool) self.Bind(wx.EVT_TOOL, self.DoNothingDialog, self.insertRightTool) self.Bind(wx.EVT_TOOL, self.DoNothingDialog, self.adoptTool) self.Bind(wx.EVT_TOOL, self.DoNothingDialog, self.aggregateTool) self.Bind(wx.EVT_TOOL, self.DoNothingDialog, self.disaggregateTool) ### STATUS BAR # Status bar self.statbar = self.CreateStatusBar() self.statbar.SetBackgroundColour('white') # Update status bar with window size on (a) first showing and (b) resizing self.Bind(wx.EVT_SIZE, self.OnResize, self) # Create main panel self.InitMainPanel() def InitMainPanel(self): ### MAIN PANEL # # Create main panel to contain everything self.panel = wx.Panel(self) self.box = wx.BoxSizer(wx.VERTICAL) # Create FlexGridSizer to have 3 panes # 2nd and 3rd arguments are hgap and vgap b/t panes (cosmetic) self.grid = wx.FlexGridSizer(cols = 3, rows = 2, hgap = 10, vgap = 10) self.part_header = wx.StaticText(self.panel, label = "Parts view") self.geom_header = wx.StaticText(self.panel, label = "Geometry view") self.latt_header = wx.StaticText(self.panel, label = "Lattice view") self.panel_style = wx.SIMPLE_BORDER self.part_panel = wx.Panel(self.panel, style = self.panel_style) self.geom_panel = wx.Panel(self.panel, style = self.panel_style) self.latt_panel = wx.Panel(self.panel, style = self.panel_style) self.part_sizer = wx.BoxSizer(wx.VERTICAL) self.latt_sizer = wx.BoxSizer(wx.VERTICAL) # Some special setup for geometry sizer (grid) self.image_cols = 2 self.geom_sizer = wx.GridSizer(cols = self.image_cols, rows = 0, hgap = 5, vgap = 5) # Defines tightness of images in grid (i.e. produces blank border) self.geom_tight = 0.7 # PARTS VIEW SETUP # Custom tree ctrl implementation self.treeStyle = (ctc.TR_MULTIPLE | ctc.TR_EDIT_LABELS | ctc.TR_HAS_BUTTONS) self.partTree_ctc = ctc.CustomTreeCtrl(self.part_panel, agwStyle = self.treeStyle) self.partTree_ctc.SetBackgroundColour('white') self.part_sizer.Add(self.partTree_ctc, 1, wx.EXPAND) # GEOMETRY VIEW SETUP # Set up image-view grid, where "rows = 0" means the sizer updates dynamically # according to the number of elements it holds # self.geom_sizer.Add(self.image_grid, 1, wx.EXPAND) # Binding for toggling of part/assembly images # though toggle buttons not yet realised self.Bind(wx.EVT_TOGGLEBUTTON, self.ImageToggled) self.no_image_ass = 'Images/noimage_ass.png' self.no_image_part = 'Images/noimage_part.png' # LATTICE VIEW SETUP # Set up matplotlib FigureCanvas with toolbar for zooming and movement self.latt_figure = mpl.figure.Figure() self.latt_canvas = FigureCanvas(self.latt_panel, -1, self.latt_figure) self.latt_axes = self.latt_figure.add_subplot(111) self.latt_canvas.Hide() # Realize but hide, to be shown later when file loaded/data updated self.latt_tb = NavigationToolbar(self.latt_canvas) # self.latt_tb.Realize() self.latt_tb.Hide() self.latt_sizer.Add(self.latt_canvas, 1, wx.EXPAND | wx.ALIGN_BOTTOM | wx.ALL, border = 5) self.latt_sizer.Add(self.latt_tb, 0, wx.EXPAND) self.selected_colour = 'blue' # OVERALL SIZERS SETUP self.part_panel.SetSizer(self.part_sizer) self.geom_panel.SetSizer(self.geom_sizer) self.latt_panel.SetSizer(self.latt_sizer) self.grid.AddMany([(self.part_header), (self.geom_header), (self.latt_header), (self.part_panel, 1, wx.EXPAND), (self.geom_panel, 1, wx.EXPAND), (self.latt_panel, 1, wx.EXPAND)]) # Set all grid elements to "growable" upon resizing # Flags (second argument is proportional size) self.grid.AddGrowableRow(1,0) self.grid.AddGrowableCol(0,3) self.grid.AddGrowableCol(1,2) self.grid.AddGrowableCol(2,3) # Set sizer for/update main panel self.box.Add(self.grid, 1, wx.ALL | wx.EXPAND, 5) self.panel.SetSizer(self.box) def GetFilename(self, dialog_text = "Open file", starter = None, ender = None): ### General file-open method; takes list of file extensions as argument ### and can be used for specific file names ("starter", string) ### or types ("ender", string or list) # Convert "ender" to list if only one element if isinstance(ender, str): ender = [ender] # Check that only one kwarg is present # Create text for file dialog if starter is not None and ender is None: file_open_text = starter.upper() + " files (" + starter.lower() + "*)|" + starter.lower() + "*" elif starter is None and ender is not None: file_open_text = [el.upper() + " files (*." + el.lower() + ")|*." + el.lower() for el in ender] file_open_text = "|".join(file_open_text) else: raise ValueError("Requires starter or ender only") # Create file dialog fileDialog = wx.FileDialog(self, dialog_text, "", "", file_open_text, wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) fileDialog.ShowModal() filename = fileDialog.GetPath() fileDialog.Destroy() # Return file name, ignoring rest of path return filename def DisplayPartsList(self): # Create root node... root_id = self.assembly.tree.root root_tag = self.assembly.tree.get_node(root_id).tag # Exception handler if file loaded previously try: self.partTree_ctc.DeleteAllItems() except: pass ctc_root_item = self.partTree_ctc.AddRoot(text = root_tag, ct_type = 1) self.ctc_dict[root_id] = ctc_root_item self.ctc_dict_inv[ctc_root_item] = root_id # ...then all others # Assumes treelib ordering ensures parents are defined before children for el in self.assembly.tree_dict: if el != root_id: parent_id = self.assembly.tree.parent(el).identifier ctc_parent = self.ctc_dict[parent_id] ctc_text = self.assembly.part_dict[self.assembly.tree_dict[el]] ctc_item = self.partTree_ctc.AppendItem(ctc_parent, text = ctc_text, ct_type = 1) self.ctc_dict[el] = ctc_item self.ctc_dict_inv[ctc_item] = el # Binding for checking of list items self.Bind(ctc.EVT_TREE_ITEM_CHECKED, self.TreeItemChecked) self.Bind(ctc.EVT_TREE_SEL_CHANGED, self.TreeItemSelected) self.partTree_ctc.ExpandAll() def TreeItemChecked(self, event): def ScaleImage(img): # Resize image to geometry panel # NEEDS IMPROVEMENT TO BE MORE GENERAL (IN TERMS OF ASPECT RATIO) p_w, p_h = self.geom_panel.GetSize() h = img.GetHeight() w = img.GetWidth() w_new = p_w*self.geom_tight/self.image_cols h_new = w_new*h/w img = img.Scale(w_new, h_new) return img # Get checked item and search for corresponding image # item = event.GetItem() id_ = self.ctc_dict_inv[item] self.selected_items = self.partTree_ctc.GetSelections() if item.IsChecked(): # Get image if id_ in self.assembly.leaf_ids: img = self.assembly.tree_dict[id_] img = os.path.join('Images', img + '.png') if os.path.isfile(img): img = wx.Image(img, wx.BITMAP_TYPE_ANY) else: img = wx.Image(self.no_image_part, wx.BITMAP_TYPE_ANY) else: img = wx.Image(self.no_image_ass, wx.BITMAP_TYPE_ANY) # Create/add button in geom_panel # # Includes rescaling to panel img = ScaleImage(img) button = wx.BitmapToggleButton(self.geom_panel, id_, wx.Bitmap(img)) button.SetBackgroundColour('white') self.geom_sizer.Add(button, 0, wx.EXPAND) # Update global list and dict # # Data is list, i.e. same format as "selected_items" # but ctc lacks "get selections" method for checked items self.checked_items.append(item) self.button_dict[id_] = button self.button_dict_inv[button] = id_ # Toggle if already selected elsewhere if self.ctc_dict[id_] in self.selected_items: button.SetValue(True) else: pass else: # Remove button from geom_panel obj = self.button_dict[id_] obj.Destroy() # Update global list and dict self.checked_items.remove(item) self.button_dict.pop(id_) self.button_dict_inv.pop(obj) # Update image sizer self.geom_sizer.Layout() def TreeItemSelected(self, event): # Get selected item and update global list of items # # Using GetSelection rather than maintaining list of items # as with checked items b/c releasing ctrl key during multiple # selection means not all selections are tracked easily self.selected_items = self.partTree_ctc.GetSelections() self.UpdateToggledImages() self.UpdateLatticeSelections() def ImageToggled(self, event): id_ = event.GetId() self.UpdateListSelections(id_) self.UpdateLatticeSelections() def UpdateListSelections(self, id_): # Select/deselect parts list item item = self.ctc_dict[id_] if item in self.selected_items: self.selected_items.remove(item) else: self.selected_items.append(item) # With "select = True", SelectItem toggles state if multiple selections enabled self.partTree_ctc.SelectItem(self.ctc_dict[id_], select = True) def UpdateLatticeSelections(self): # Update colour of selected items # # Set all back to default colour first for node in self.assembly.g.nodes(): self.assembly.g.nodes[node]['colour'] = self.assembly.default_colour # Then selected nodes for item in self.selected_items: id_ = self.ctc_dict_inv[item] self.assembly.g.nodes[id_]['colour'] = self.selected_colour # Redraw lattice self.DisplayLattice() def UpdateToggledImages(self): for id_, button in self.button_dict.items(): button.SetValue(False) for item in self.selected_items: id_ = self.ctc_dict_inv[item] if id_ in self.button_dict: button = self.button_dict[id_] button.SetValue(True) else: pass def DisplayLattice(self): # Get node positions, colour map, labels pos = nx.get_node_attributes(self.assembly.g, 'pos') colour_map = [self.assembly.g.nodes[el]['colour'] for el in self.assembly.g.nodes] # node_labels = nx.get_node_attributes(self.assembly.g, 'label') # Draw to lattice panel figure try: self.latt_axes.clear() except: pass nx.draw(self.assembly.g, pos, node_color = colour_map, with_labels = True, ax = self.latt_axes) # nx.draw_networkx_labels(self.assembly.g, pos, labels = node_labels, ax = self.latt_axes) # Minimise white space around plot in panel self.latt_figure.subplots_adjust(left = 0.01, bottom = 0.01, right = 0.99, top = 0.99) # Show lattice figure self.latt_canvas.draw() self.latt_canvas.Show() self.latt_tb.Show() # Update lattice panel layout self.latt_panel.Layout() def DoNothingDialog(self, event): nowt = wx.MessageDialog(self, "Functionality to be added", "Do nothing dialog", wx.OK) # Create modal dialogue that stops process nowt.ShowModal() nowt.Destroy() def OnFileOpen(self, event): # # Delete if exists from previous file load # # TO BE COMPLETED FOR VERSION 1-2 # try: # del self.assembly # except AttributeError: # pass # Get STEP filename self.open_filename = self.GetFilename(ender = ["stp", "step"]).split("\\")[-1] # Load data, create nodes and edges, etc. self.assembly = StepParse() self.assembly.load_step(self.open_filename) self.assembly.create_tree() # Checked and selected items lists, shared b/t all views self.checked_items = [] self.selected_items = [] # Toggle buttons self.button_dict = {} self.button_dict_inv = {} # Write interactive parts list using WX customtreectrl, from treelib nodes self.ctc_dict = {} self.ctc_dict_inv = {} # Show parts list and lattice self.DisplayPartsList() # Clear geometry window if necessary try: self.geom_sizer.Clear(True) except: pass # Clear lattice plot if necessary try: self.latt_axes.clear() except: pass # Display lattice self.DisplayLattice() def OnInsertLeft(self, event): pass def OnInsertRight(self, event): pass def OnAdopt(self, event): pass def OnAggregate(self, event): pass def OnDisaggregate(self, event): pass def OnAbout(self, event): # Show program info abt_text = """StrEmbed-5-1: A user interface for manipulation of design configurations\n Copyright (C) 2019 Hugh Patrick Rice\n This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\n This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.""" abt = wx.MessageDialog(self, abt_text, 'About StrEmbed-5-1', wx.OK) abt.ShowModal() # Shows dialogue that stops process (modal) abt.Destroy() def OnExit(self, event): self.Close(True) # Close program def OnResize(self, event): # Display window size in status bar self.statbar.SetStatusText("Window size = " + format(self.GetSize())) event.Skip()
class GraphPanel(wx.Panel): r"""wx Panel embedding a Matplotlib plot Parameters ---------- parent : wx parent model : Model The data model """ # sequence of matplotlib included colors. # Used for the display of the reference foils color_sequence = ['dimgrey', 'maroon', 'darkolivegreen', 'darkred', 'tomato'] parsec_lower_color = 'orange' parsec_upper_color = 'red' parsec_lower_name = 'PARSEC l' parsec_upper_name = 'PARSEC u' def __init__(self, parent, model): super(GraphPanel, self).__init__(parent, wx.ID_ANY) self.model = model self.figure = plt.figure(facecolor='white') sizer = wx.BoxSizer(wx.VERTICAL) self.canvas = FigureCanvas(self, -1, self.figure) self.toolbar = NavigationToolbar(self.canvas) self.toolbar.Show() sizer.Add(self.canvas, 1, wx.EXPAND) sizer.Add(self.toolbar, 0) self.SetSizer(sizer) self.model.observe("parsec_params_changed", self.on_model_changed) self.model.observe("ref_foils_changed", self.on_model_changed) self.on_model_changed(dict()) def on_model_changed(self, change): r"""Handler for model changes, be it a parsec_params_changed or a ref_foils_changed change Parameters ---------- change : dict Not used but must be present in function signature """ colors = list() names = list() xs = list() ys = list() x_l, y_l, x_u, y_u = self.model.points[:4] xs.append(x_l) ys.append(y_l) colors.append(GraphPanel.parsec_lower_color) names.append(GraphPanel.parsec_lower_name) xs.append(x_u) ys.append(y_u) colors.append(GraphPanel.parsec_upper_color) names.append(GraphPanel.parsec_upper_name) for i, foil in enumerate(self.model.ref_foils): ref_foil_xs = [x for x, y in foil.points] ref_foil_ys = [y for x, y in foil.points] xs.append(ref_foil_xs) ys.append(ref_foil_ys) colors.append(GraphPanel.color_sequence[i % len(GraphPanel.color_sequence)]) # ht* are the Drela foils. # Apart from ht22 and ht23, they are almost symmetrical if foil.is_symmetrical or 'ht' in foil.name: names.append("%s - th:%f -mx:%f - ler:%f" % (foil.name, foil.y_spread, foil.max_y_x, foil.pseudo_leading_edge_radius)) else: names.append(foil.name) self.plot(xs=xs, ys=ys, colors=colors, names=names) def plot(self, xs, ys, colors, names): r"""Plot values with an associated color and an associated name Parameters ---------- xs : list of list of x values ys : list of list of y values, same order as xs colors : list of colors, same order as xs names : list of names, same order as xs """ self.figure.clear() ax = self.figure.add_subplot(111, aspect='equal') plt.subplots_adjust(left=0.05, right=0.95, top=0.99, bottom=0.01) # can be hardcoded since x values of foil sections are bound to 0 -> 1 ax.set_xlim(left=-.1, right=1.1) # can be hardcoded since y values of foils # will never exceed these bounds ax.set_ylim(bottom=-.25, top=.25) ax.set_yticks([-0.1, -0.08, -0.06, -0.04, -0.02, 0., 0.02, 0.04, 0.06, 0.08, 0.1]) ax.set_frame_on(False) # outer frame # for x, y, color, name in reversed(list(zip(xs, ys, colors, names))): ax.plot(x, y, marker='+', color=color, label=name) ax.grid() legend = ax.legend(loc='upper left', shadow=True) if legend is not None: for label in legend.get_texts(): label.set_fontsize('small') legend.draggable() self.canvas.draw()