From d8c2a53cf5a3c3fd193a434c4837802295b94e5e Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 18 Dec 2014 11:11:46 -0500 Subject: [PATCH] Fixed numerous issues relating to opening, closing, newing up, and saving scripts. Also added an example contributed by @emdash. --- CadQuery/Examples/Ex027_Remote_Enclosure.py | 90 ++++++++++++++++++ CadQuery/Gui/Command.py | 100 +++++++++++--------- CadQuery/Gui/ExportCQ.py | 4 +- CadQuery/Gui/ImportCQ.py | 10 +- CadQuery/InitGui.py | 31 +++--- 5 files changed, 165 insertions(+), 70 deletions(-) create mode 100644 CadQuery/Examples/Ex027_Remote_Enclosure.py diff --git a/CadQuery/Examples/Ex027_Remote_Enclosure.py b/CadQuery/Examples/Ex027_Remote_Enclosure.py new file mode 100644 index 0000000..ae014a3 --- /dev/null +++ b/CadQuery/Examples/Ex027_Remote_Enclosure.py @@ -0,0 +1,90 @@ +# This example is meant to be used from within the CadQuery module of FreeCAD. +import cadquery as cq +import Part + +exploded = False # when true, moves the base away from the top so we see +showTop = True # When true, the top is rendered. +showCover = True # When true, the cover is rendered + +width = 2.2 # Nominal x dimension of the part +height = 0.5 # Height from bottom top to the top of the top :P +length = 1.5 # Nominal y dimension of the part +trapezoidFudge = 0.7 # ratio of trapezoid bases. set to 1.0 for cube +xHoleOffset = 0.500 # Holes are distributed symetrically about each axis +yHoleOffset = 0.500 +zFilletRadius = 0.50 # Fillet radius of corners perp. to Z axis. +yFilletRadius = 0.250 # Fillet readius of the top edge of the case +lipHeight = 0.1 # The height of the lip on the inside of the cover +wallThickness = 0.06 # Wall thickness for the case +coverThickness = 0.2 # Thickness of the cover plate +holeRadius = 0.30 # Button hole radius +counterSyncAngle = 100 # Countersink angle. + +xyplane = cq.Workplane("XY") +yzplane = cq.Workplane("YZ") + + +def trapezoid(b1, b2, h): + "Defines a symetrical trapezoid in the XY plane." + + y = h / 2 + x1 = b1 / 2 + x2 = b2 / 2 + return (xyplane + .polyline([(-x1, y), + (x1, y), + (x2, -y), + (-x2, -y), + (-x1, y)])) + + +# Defines our base shape: a box with fillets around the vertical edges. +# This has to be a function because we need to create multiple copies of +# the shape. +def base(h): + global width, trapezoidFudge, length, zFilletRadius + return (trapezoid(width, width * trapezoidFudge, length) + .extrude(h) + .translate((0, 0, height / 2)) + .edges("Z") + .fillet(zFilletRadius)) + +# start with the base shape +top = (base(height) + # then fillet the top edge + .edges(">Z") + .fillet(yFilletRadius) + # shell the solid from the bottom face, with a .060" wall thickness + .faces("-Z") + .shell(-wallThickness) + # cut five button holes into the top face in a cross pattern. + .faces("+Z") + .workplane() + .pushPoints([(0, 0), + (-xHoleOffset, 0), + (0, -yHoleOffset), + (xHoleOffset, 0), + (0, yHoleOffset)]) + .cskHole(diameter=holeRadius, + cskDiameter=holeRadius * 1.5, + cskAngle=counterSyncAngle)) + +# the bottom cover begins with the same basic shape as the top +cover = (base(coverThickness) + # we need to move it upwards into the parent solid slightly. + .translate((0, 0, -coverThickness + lipHeight)) + # now we subtract the top from the cover. This produces a lip on the + # solid NOTE: that this does not account for mechanical tolerances. + # But it looks cool. + .cut(top) + # try to fillet the inner edge of the cover lip. Technically this + # fillets every edge perpendicular to the Z axis. + .edges("#Z") + .fillet(.020) + .translate((0, 0, -0.5 if exploded else 0))) + +# Conditionally render the parts +if showTop: + Part.show(top.toFreecad()) +if showCover: + Part.show(cover.toFreecad()) diff --git a/CadQuery/Gui/Command.py b/CadQuery/Gui/Command.py index 82ca238..7ce7a81 100644 --- a/CadQuery/Gui/Command.py +++ b/CadQuery/Gui/Command.py @@ -1,17 +1,20 @@ """Adds all of the commands that are used for the menus of the CadQuery module""" # (c) 2014 Jeremy Wright LGPL v3 -import tempfile +import imp, os, sys, tempfile import FreeCAD, FreeCADGui from PySide import QtGui -import ExportCQ, ImportCQ, CadQuery_rc +import ExportCQ, ImportCQ +import module_locator #Distinguish python built-in open function from the one declared here if open.__module__ == '__builtin__': pythonopen = open -def clearAll(): +def clearActiveDocument(): + """Clears the currently active 3D view so that we can re-render""" + doc = FreeCAD.ActiveDocument #Make sure we have an active document to work with @@ -29,12 +32,14 @@ class CadQueryCloseScript: def IsActive(self): return True + # if FreeCAD.ActiveDocument is None: + # return False + # else: + # return True def Activated(self): - #Getting the main window will allow us to find the children we need to work with + #Grab our code editor so we can interact with it mw = FreeCADGui.getMainWindow() - - #We need this so we can load the file into it cqCodePane = mw.findChild(QtGui.QPlainTextEdit, "cqCodePane") #If there's nothing open in the code pane, we don't need to close it @@ -61,10 +66,13 @@ class CadQueryCloseScript: if filename is not None: ExportCQ.save(filename) + #Close the matching 3D view if it's open + if cqCodePane.file.path is not None: + docname = os.path.splitext(os.path.basename(cqCodePane.file.path))[0] + FreeCAD.closeDocument(docname) + #Clear our script and whatever was rendered by it out cqCodePane.file.close() - #TODO Close the matching 3D view here instead of clearing it - clearAll() class CadQueryExecuteScript: @@ -78,26 +86,32 @@ class CadQueryExecuteScript: def IsActive(self): return True - # if FreeCAD.ActiveDocument is None: - # return False - # else: - # return True def Activated(self): - #Getting the main window will allow us to find the children we need to work with + #Grab our code editor so we can interact with it mw = FreeCADGui.getMainWindow() - - #We need this so we can load the file into it cqCodePane = mw.findChild(QtGui.QPlainTextEdit, "cqCodePane") - clearAll() + #Clear the old render before re-rendering + clearActiveDocument() #Save our code to a tempfile and render it tempFile = tempfile.NamedTemporaryFile(delete=False) tempFile.write(cqCodePane.toPlainText()) tempFile.close() FreeCAD.Console.PrintMessage("\r\n") - execfile(tempFile.name) + + docname = os.path.splitext(os.path.basename(cqCodePane.file.path))[0] + + #If the matching 3D view has been closed, we need to open a new one + try: + FreeCAD.getDocument(docname) + except: + FreeCAD.newDocument(docname) + + + #We import this way because using execfile() causes non-standard script execution in some situations + imp.load_source('temp.module', tempFile.name) msg = QtGui.QApplication.translate( "cqCodeWidget", @@ -119,8 +133,6 @@ class CadQueryNewScript: return True def Activated(self): - import os, module_locator - #We need to close any file that's already open in the editor window CadQueryCloseScript().Activated() @@ -143,20 +155,14 @@ class CadQueryOpenScript: def IsActive(self): return True - # if FreeCAD.ActiveDocument is None: - # return False - # else: - # return True def Activated(self): - import os, sys - + #So we can open the "Open File" dialog mw = FreeCADGui.getMainWindow() #Try to keep track of the previous path used to open as a convenience to the user if self.previousPath is None: - import os, module_locator - + #Start off defaulting to the Examples directory module_base_path = module_locator.module_path() exs_dir_path = os.path.join(module_base_path, 'Examples') @@ -178,6 +184,16 @@ class CadQueryOpenScript: #We've created a library that FreeCAD can use as well to open CQ files ImportCQ.open(filename[0]) + docname = os.path.splitext(os.path.basename(filename[0]))[0] + FreeCAD.newDocument(docname) + + #Execute the script + CadQueryExecuteScript().Activated() + + #Get a nice view of our model + FreeCADGui.activeDocument().activeView().viewAxometric() + FreeCADGui.SendMsgToActiveView("ViewFit") + class CadQuerySaveScript: """CadQuery's command to save a script file""" @@ -190,21 +206,13 @@ class CadQuerySaveScript: def IsActive(self): return True - # if FreeCAD.ActiveDocument is None: - # return False - # else: - # return True def Activated(self): - import os - - #Getting the main window will allow us to find the children we need to work with + #Grab our code editor so we can interact with it mw = FreeCADGui.getMainWindow() - - #We need this so we can load the file into it cqCodePane = mw.findChild(QtGui.QPlainTextEdit, "cqCodePane") - #If the codepane doesn't have a filename, we need to present the save as dialog + #If the code pane doesn't have a filename, we need to present the save as dialog if len(cqCodePane.file.path) == 0 or os.path.basename(cqCodePane.file.path) == 'script_template.py': FreeCAD.Console.PrintMessage("\r\nYou cannot save a blank file or save over a template file.") @@ -227,13 +235,11 @@ class CadQuerySaveAsScript: def IsActive(self): return True - # if FreeCAD.ActiveDocument is None: - # return False - # else: - # return True def Activated(self): + #So we can open the save-as dialog mw = FreeCADGui.getMainWindow() + cqCodePane = mw.findChild(QtGui.QPlainTextEdit, "cqCodePane") #Try to keep track of the previous path used to open as a convenience to the user if self.previousPath is None: @@ -246,6 +252,14 @@ class CadQuerySaveAsScript: #Make sure the user didn't click cancel if filename[0]: - #Save the file before the re-render + #Close the 3D view for the original script if it's open + try: + docname = os.path.splitext(os.path.basename(cqCodePane.file.path))[0] + FreeCAD.closeDocument(docname) + except: + #Assume that there was no 3D view to close + pass + + #Save the file before closing the original and the re-rendering the new one ExportCQ.save(filename[0]) - execfile(filename[0]) \ No newline at end of file + CadQueryExecuteScript().Activated() diff --git a/CadQuery/Gui/ExportCQ.py b/CadQuery/Gui/ExportCQ.py index 655f51f..dbcbaf1 100644 --- a/CadQuery/Gui/ExportCQ.py +++ b/CadQuery/Gui/ExportCQ.py @@ -13,10 +13,8 @@ def save(filename=None): :param filename: The path and file name to save to. If not provided we try to pull it from the code pane itself """ - #Getting the main window will allow us to find the children we need to work with + #Grab our code editor so we can interact with it mw = FreeCADGui.getMainWindow() - - #We need this so we can load the file into it cqCodePane = mw.findChild(QtGui.QPlainTextEdit, "cqCodePane") #If we weren't provided a file name, we need to find it from the text field diff --git a/CadQuery/Gui/ImportCQ.py b/CadQuery/Gui/ImportCQ.py index e74a56d..a3cd04b 100644 --- a/CadQuery/Gui/ImportCQ.py +++ b/CadQuery/Gui/ImportCQ.py @@ -1,17 +1,15 @@ """Adds the ability to open files from disk to the CadQuery FreeCAD module""" # (c) 2014 Jeremy Wright LGPL v3 -import os, FreeCAD, FreeCADGui +import FreeCAD, FreeCADGui from PySide import QtGui #Distinguish python built-in open function from the one declared here if open.__module__ == '__builtin__': pythonopen = open -def open(filename): - docname = os.path.splitext(os.path.basename(filename))[0] - doc = FreeCAD.newDocument(docname) +def open(filename): #All of the Gui.* calls in the Python console break after opening if we don't do this FreeCADGui.doCommand("import FreeCADGui as Gui") @@ -24,8 +22,6 @@ def open(filename): #Pull the text of the CQ script file into our code pane cqCodePane.file.open(filename) - execfile(filename) - msg = QtGui.QApplication.translate( "cqCodeWidget", "Opened ", @@ -33,4 +29,4 @@ def open(filename): QtGui.QApplication.UnicodeUTF8) FreeCAD.Console.PrintMessage("\r\n" + msg + filename) - return doc \ No newline at end of file + return diff --git a/CadQuery/InitGui.py b/CadQuery/InitGui.py index ec5a7be..eedea2e 100644 --- a/CadQuery/InitGui.py +++ b/CadQuery/InitGui.py @@ -2,9 +2,10 @@ This adds a workbench with a scripting editor to FreeCAD's GUI.""" # (c) 2014 Jeremy Wright LGPL v3 -import FreeCAD -import FreeCADGui +import FreeCAD, FreeCADGui from Gui.Command import * +import CadQuery_rc + class CadQueryWorkbench (Workbench): """CadQuery workbench for FreeCAD""" @@ -34,6 +35,7 @@ class CadQueryWorkbench (Workbench): def Activated(self): import os, sys import module_locator + from Gui import Command, ImportCQ #Set up so that we can import from our embedded packages module_base_path = module_locator.module_path() @@ -53,7 +55,6 @@ class CadQueryWorkbench (Workbench): sys.path.insert(1, fc_bin_path) import cadquery - from Gui import ImportCQ from pyqode.python.widgets import PyCodeEdit from PySide import QtGui, QtCore @@ -87,7 +88,7 @@ class CadQueryWorkbench (Workbench): interpreter = 'python' #Getting the main window will allow us to start setting things up the way we want - mw = Gui.getMainWindow() + mw = FreeCADGui.getMainWindow() #Find all of the docks that are open so we can close them (except the Python console) dockWidgets = mw.findChildren(QtGui.QDockWidget) @@ -109,7 +110,7 @@ class CadQueryWorkbench (Workbench): #Set up the text area for our CQ code server_path = os.path.join(module_base_path, 'cq_server.py') - #Windows needs some exra help with paths + #Windows needs some extra help with paths if sys.platform.startswith('win'): codePane = PyCodeEdit(server_script=server_path, interpreter=interpreter , args=['-s', fc_lib_path, libs_dir_path]) @@ -122,28 +123,24 @@ class CadQueryWorkbench (Workbench): #Add the text area to our dock widget cqCodeWidget.setWidget(codePane) - #Open our introduction example + #Open and execute our introduction example example_path = os.path.join(module_base_path, 'Examples') example_path = os.path.join(example_path, 'Ex000_Introduction.py') ImportCQ.open(example_path) + docname = os.path.splitext(os.path.basename(example_path))[0] + FreeCAD.newDocument(docname) + Command.CadQueryExecuteScript().Activated() + + #Get a nice view of our example + FreeCADGui.activeDocument().activeView().viewAxometric() + FreeCADGui.SendMsgToActiveView("ViewFit") def Deactivated(self): - from Gui import ExportCQ - from Gui import Command - #Put the UI back the way we found it FreeCAD.Console.PrintMessage("\r\nCadQuery Workbench Deactivated\r\n") Command.CadQueryCloseScript().Activated() - #Rely on our export library to help us save the file - #ExportCQ.save() - - #TODO: This won't work for now because the views are destroyed when they are hidden - # for widget in self.closedWidgets: - # FreeCAD.Console.PrintMessage(widget.objectName()) - # widget.setVisible(True) - FreeCADGui.addCommand('CadQueryNewScript', CadQueryNewScript()) FreeCADGui.addCommand('CadQueryOpenScript', CadQueryOpenScript()) FreeCADGui.addCommand('CadQuerySaveScript', CadQuerySaveScript())