From 70634880083dc5c59566fd088214caa6be396ac4 Mon Sep 17 00:00:00 2001 From: "Enrico Di Lorenzo - FastFieldSolvers S.R.L" Date: Thu, 15 Nov 2018 20:23:05 +0100 Subject: [PATCH] * First beta working workbench supporting FastHenry.py Still missing: - FHSegments based on paths - .equiv statement - FHPlanes --- EM.py | 48 ++++ EM_FHInputFile.py | 152 +++++++++++ EM_FHNode.py | 249 +++++++++++++++++ EM_FHPort.py | 245 +++++++++++++++++ EM_FHSegment.py | 421 +++++++++++++++++++++++++++++ EM_FHSolver.py | 232 ++++++++++++++++ Export_mesh.py | 21 +- Init.py | 25 ++ InitGui.py | 75 ++++++ README.md | 28 ++ README.txt | 13 - Resources/EMWorkbench.svg | 504 +++++++++++++++++++++++++++++++++++ Resources/inputfile_icon.svg | 216 +++++++++++++++ Resources/node_icon.svg | 73 +++++ Resources/port_icon.svg | 192 +++++++++++++ Resources/segment_icon.svg | 479 +++++++++++++++++++++++++++++++++ Resources/solver_icon.svg | 121 +++++++++ export_to_FastHenry.py | 469 ++++++++++++++++++++++++++++++++ import_fastercap.py | 362 ++++++++++++++++++++++++- launch_fastercap.py | 42 +++ launch_fasthenry.py | 43 +++ 21 files changed, 3981 insertions(+), 29 deletions(-) create mode 100644 EM.py create mode 100644 EM_FHInputFile.py create mode 100644 EM_FHNode.py create mode 100644 EM_FHPort.py create mode 100644 EM_FHSegment.py create mode 100644 EM_FHSolver.py create mode 100644 Init.py create mode 100644 InitGui.py create mode 100644 README.md delete mode 100644 README.txt create mode 100644 Resources/EMWorkbench.svg create mode 100644 Resources/inputfile_icon.svg create mode 100644 Resources/node_icon.svg create mode 100644 Resources/port_icon.svg create mode 100644 Resources/segment_icon.svg create mode 100644 Resources/solver_icon.svg create mode 100644 export_to_FastHenry.py create mode 100644 launch_fastercap.py create mode 100644 launch_fasthenry.py diff --git a/EM.py b/EM.py new file mode 100644 index 0000000..b3424bb --- /dev/null +++ b/EM.py @@ -0,0 +1,48 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +__title__="FreeCAD E.M. Workbench API" +__author__ = "FastFieldSolvers S.R.L." +__url__ = "http://www.fastfieldsolvers.com" + +## \defgroup EM E.M. +# \ingroup PYTHONWORKBENCHES +# \brief ElectroMagnetic tools +# +# This module provides tools for ElectroMagnetic analysis, +# enabling to create suitable geometries, launching field solvers, +# and post-processing the results + +'''The E.M. module provides tools for ElectroMagnetic analysis''' + +import FreeCAD +if FreeCAD.GuiUp: + import FreeCADGui + FreeCADGui.updateLocale() + +from EM_FHNode import * +from EM_FHSegment import * +from EM_FHPort import * +from EM_FHSolver import * +from EM_FHInputFile import * + diff --git a/EM_FHInputFile.py b/EM_FHInputFile.py new file mode 100644 index 0000000..96c4cf1 --- /dev/null +++ b/EM_FHInputFile.py @@ -0,0 +1,152 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +__title__="FreeCAD E.M. Workbench FastHenry create input file command" +__author__ = "FastFieldSolvers S.R.L." +__url__ = "http://www.fastfieldsolvers.com" + +# defines +# +EMFHINPUTFILE_DEF_FILENAME = "fasthenry_input_file.inp" + +import FreeCAD, FreeCADGui, Mesh, Part, MeshPart, Draft, DraftGeomUtils, os +import EM +from FreeCAD import Vector + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore, QtGui + from DraftTools import translate + from PySide.QtCore import QT_TRANSLATE_NOOP +else: + # \cond + def translate(ctxt,txt, utf8_decode=False): + return txt + def QT_TRANSLATE_NOOP(ctxt,txt): + return txt + # \endcond + +__dir__ = os.path.dirname(__file__) +iconPath = os.path.join( __dir__, 'Resources' ) + +def makeFHInputFile(doc=None,filename=None,folder=None): + '''Creates a FastHenry input file + + 'doc' is the Document object that must contain at least one + EM_FHSolver object and the relevant geometry. + If no 'doc' is given, the active document is used, if any. + + Example: + TBD +''' + if not doc: + doc = App.ActiveDocument + if not doc: + FreeCAD.Console.PrintWarning(translate("EM","No active document available. Aborting.")) + return + # get the solver object, if any + solver = [obj for obj in doc.Objects if Draft.getType(obj) == "FHSolver"] + if solver == []: + # error + FreeCAD.Console.PrintWarning(translate("EM","FHSolver object not found in the document. Aborting.")) + return + else: + # TBC warning: may warn the user if more that one solver is present per document + solver = solver[0] + if not filename: + # if 'filename' was not passed as an argument, retrieve it from the 'solver' object + # (this should be the standard way) + if solver.Filename == "": + # build a filename concatenating the document name + solver.Filename = doc.Name + EMFHSOLVER_DEF_FILENAME + filename = solver.Filename + else: + # otherwise, if the user passed a filename to the function, update it in the 'solver' object + solver.Filename = filename + if not folder: + # if not specified, default to the user's home path + # (e.g. in Windows "C:\Documents and Settings\username\My Documents", in Linux "/home/username") + folder = FreeCAD.ConfigGet("UserHomePath") + if not os.path.isdir(folder): + os.mkdir(folder) + # check if exists + if os.path.isfile(folder + os.sep + filename): + # filename already exists! Do not overwrite + FreeCAD.Console.PrintWarning(translate("EM","Filename already exists") + " '" + str(folder) + str(os.sep) + str(filename) + "'\n") + return + FreeCAD.Console.PrintMessage(QT_TRANSLATE_NOOP("EM","Exporting to FastHenry file ") + "'" + folder + os.sep + filename + "'\n") + with open(folder + os.sep + filename, 'w') as fid: + # serialize the header + solver.Proxy.serialize(fid,"head") + # now the nodes + fid.write("* Nodes\n") + nodes = [obj for obj in doc.Objects if Draft.getType(obj) == "FHNode"] + for node in nodes: + node.Proxy.serialize(fid) + # then the segments + fid.write("\n") + fid.write("* Segments\n") + segments = [obj for obj in doc.Objects if Draft.getType(obj) == "FHSegment"] + for segment in segments: + segment.Proxy.serialize(fid) + # then the .equiv + # TBC + # then the ports + fid.write("\n") + fid.write("* Ports\n") + ports = [obj for obj in doc.Objects if Draft.getType(obj) == "FHPort"] + for port in ports: + port.Proxy.serialize(fid) + # and finally the tail + fid.write("\n") + solver.Proxy.serialize(fid,"tail") + FreeCAD.Console.PrintMessage(QT_TRANSLATE_NOOP("EM","Finished exporting")+"\n") + +class _CommandFHInputFile: + ''' The EM FastHenry create input file command definition +''' + def GetResources(self): + return {'Pixmap' : os.path.join(iconPath, 'inputfile_icon.svg') , + 'MenuText': QT_TRANSLATE_NOOP("EM_FHInputFile","FHInputFile"), + 'Accel': "E, I", + 'ToolTip': QT_TRANSLATE_NOOP("EM_FHInputFile","Creates a FastHenry input file")} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + # init properties (future) + #self.Length = None + # preferences + #p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/EM") + #self.Width = p.GetFloat("Width",200) + FreeCAD.ActiveDocument.openTransaction(translate("EM","Create a FastHenry file")) + FreeCADGui.addModule("EM") + FreeCADGui.doCommand('obj=EM.makeFHInputFile(App.ActiveDocument)') + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('EM_FHInputFile',_CommandFHInputFile()) + +#pts = [obj for obj in FreeCAD.ActiveDocument.Objects if Draft.getType(obj) == "Point"] diff --git a/EM_FHNode.py b/EM_FHNode.py new file mode 100644 index 0000000..1b6fea6 --- /dev/null +++ b/EM_FHNode.py @@ -0,0 +1,249 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +__title__="FreeCAD E.M. Workbench FastHenry Node Class" +__author__ = "FastFieldSolvers S.R.L." +__url__ = "http://www.fastfieldsolvers.com" + +# defines +# +# default node color +EMFHNODE_DEF_NODECOLOR = (1.0,0.0,0.0) +EMFHNODE_DEF_NODESIZE = 10 + +import FreeCAD, FreeCADGui, Mesh, Part, MeshPart, Draft, DraftGeomUtils, os +from FreeCAD import Vector + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore, QtGui + from DraftTools import translate + from PySide.QtCore import QT_TRANSLATE_NOOP +else: + # \cond + def translate(ctxt,txt, utf8_decode=False): + return txt + def QT_TRANSLATE_NOOP(ctxt,txt): + return txt + # \endcond + +__dir__ = os.path.dirname(__file__) +iconPath = os.path.join( __dir__, 'Resources' ) + +def makeFHNode(baseobj=None,color=None,size=None,name='FHNode'): + '''Creates a FastHenry node ('N' statement in FastHenry) + + 'baseobj' is the point object on which the node is based. + If no 'baseobj' is given, the user must assign a base + object later on, to be able to use this object. + + Example: + TBD +''' + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name) + obj.Label = translate("EM", name) + # this adds the relevant properties to the object + #'obj' (e.g. 'Base' property) making it a _FHNode + _FHNode(obj) + # manage ViewProvider object + if FreeCAD.GuiUp: + _ViewProviderFHNode(obj.ViewObject) + # set base ViewObject properties to user-selected values (if any) + if color: + obj.ViewObject.PointColor = color + else: + obj.ViewObject.PointColor = EMFHNODE_DEF_NODECOLOR + if size: + obj.ViewObject.PointSize = size + else: + obj.ViewObject.PointSize = EMFHNODE_DEF_NODESIZE + # check if 'baseobj' is a point (only base object allowed) + if baseobj: + if Draft.getType(baseobj) == "Point": + obj.Base = baseobj + else: + FreeCAD.Console.PrintWarning(translate("EM","FHNodes can only be based on Point objects")) + # hide the base object + if obj.Base and FreeCAD.GuiUp: + obj.Base.ViewObject.hide() + # force recompute to show the Python object + FreeCAD.ActiveDocument.recompute() + # return the newly created Python object + return obj + +class _FHNode: + '''The EM FastHenry Node object''' + def __init__(self, obj): + ''' Add properties ''' + obj.addProperty("App::PropertyLink", "Base", "EM", QT_TRANSLATE_NOOP("App::Property","The base object this component is built upon")) + obj.Proxy = self + self.Type = "FHNode" + # save the object in the class, to store or retrieve specific data from it + # from within the class + self.Object = obj + + def execute(self, obj): + ''' this method is mandatory. It is called on Document.recompute() +''' + # check if we have a 'Base' object + if obj.Base: + # computing a shape from a base object + if obj.Base.isDerivedFrom("Part::Feature"): + # check validity + if obj.Base.Shape.isNull(): + return + if not obj.Base.Shape.isValid(): + return + # ok, it's valid. Let's verify if this is a Point. + if Draft.getType(obj.Base) == "Point": + shape = Part.Vertex(obj.Base.Shape) + obj.Shape = shape + + def onChanged(self, obj, prop): + ''' take action if an object property 'prop' changed +''' + #FreeCAD.Console.PrintWarning("\n_FHNode onChanged(" + str(prop)+")\n") #debug + if not hasattr(self,"Object"): + # on restore, self.Object is not there anymore + self.Object = obj + # if the user changed the base object + if prop == "Base": + # check if we have a 'Base' object + if obj.Base: + # computing a shape from a base object + if obj.Base.isDerivedFrom("Part::Feature"): + # check validity + if obj.Base.Shape.isNull(): + return + if not obj.Base.Shape.isValid(): + return + # ok, it's valid. Let's verify if this is a Point. + if Draft.getType(obj.Base) == "Point": + shape = Part.Vertex(obj.Base.Shape) + obj.Shape = shape + + def serialize(self,fid): + ''' Serialize the object to the 'fid' file descriptor +''' + # check if we have a 'Base' object + if self.Object.Base: + # ok, it's valid. Let's verify if this is a Point. + if Draft.getType(self.Object.Base) == "Point": + fid.write("N" + self.Object.Label) + fid.write(" x=" + str(self.Object.Base.X.Value) + " y=" + str(self.Object.Base.Y.Value) + " z=" + str(self.Object.Base.Z.Value)) + fid.write("\n") + +class _ViewProviderFHNode: + def __init__(self, obj): + ''' Set this object to the proxy object of the actual view provider ''' + obj.Proxy = self + self.Object = obj.Object + + def attach(self, obj): + ''' Setup the scene sub-graph of the view provider, this method is mandatory ''' + return + + def updateData(self, fp, prop): + ''' Print the name of the property that has changed ''' + #FreeCAD.Console.PrintMessage("ViewProvider updateData(), property: " + str(prop) + "\n") + ''' If a property of the handled feature has changed we have the chance to handle this here ''' + return + + def getDefaultDisplayMode(self): + ''' Return the name of the default display mode. It must be defined in getDisplayModes. ''' + return "Flat Lines" + + def onChanged(self, vp, prop): + ''' Print the name of the property that has changed ''' + #FreeCAD.Console.PrintMessage("ViewProvider onChanged(), property: " + str(prop) + "\n") + + def claimChildren(self): + ''' Used to place other objects as childrens in the tree''' + c = [] + if hasattr(self,"Object"): + if hasattr(self.Object,"Base"): + c = [self.Object.Base] + return c + + def getIcon(self): + ''' Return the icon in XMP format which will appear in the tree view. This method is optional + and if not defined a default icon is shown. + ''' + return os.path.join(iconPath, 'node_icon.svg') + +class _CommandFHNode: + ''' The EM FastHenry Node (FHNode) command definition +''' + def GetResources(self): + return {'Pixmap' : os.path.join(iconPath, 'node_icon.svg') , + 'MenuText': QT_TRANSLATE_NOOP("EM_FHNode","FHNode"), + 'Accel': "E, N", + 'ToolTip': QT_TRANSLATE_NOOP("EM_FHNode","Creates a FastHenry Node object from scratch or from a selected object (point)")} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + # get the selected object(s) + sel = FreeCADGui.Selection.getSelectionEx() + done = False + # if selection is not empty + if sel: + # automatic mode + import Draft + if Draft.getType(sel[0].Object) != "FHNode": + FreeCAD.ActiveDocument.openTransaction(translate("EM","Create FHNode")) + FreeCADGui.addModule("EM") + for selobj in sel: + FreeCADGui.doCommand('obj=EM.makeFHNode(FreeCAD.ActiveDocument.'+selobj.Object.Name+')') + # autogrouping, for later on + #FreeCADGui.addModule("Draft") + #FreeCADGui.doCommand("Draft.autogroup(obj)") + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + done = True + # if no selection, or nothing good in the selected objects + if not done: + FreeCAD.DraftWorkingPlane.setup() + # get a 3D point via Snapper, setting the callback functions + FreeCADGui.Snapper.getPoint(callback=self.getPoint) + + def getPoint(self,point=None,obj=None): + "this function is called by the snapper when it has a 3D point" + if point == None: + return + coord = FreeCAD.DraftWorkingPlane.getLocalCoords(point) + FreeCAD.ActiveDocument.openTransaction(translate("EM","Create FHNode")) + FreeCADGui.addModule("EM") + FreeCADGui.doCommand('import Draft') + FreeCADGui.doCommand('base=Draft.makePoint('+str(coord.x)+','+str(coord.y)+','+str(coord.z)+')') + FreeCADGui.doCommand('obj=EM.makeFHNode(base)') + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + # might improve in the future with continue command + #if self.continueCmd: + # self.Activated() + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('EM_FHNode',_CommandFHNode()) + diff --git a/EM_FHPort.py b/EM_FHPort.py new file mode 100644 index 0000000..6062db0 --- /dev/null +++ b/EM_FHPort.py @@ -0,0 +1,245 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + + +__title__="FreeCAD E.M. Workbench FastHenry Port Class" +__author__ = "FastFieldSolvers S.R.L." +__url__ = "http://www.fastfieldsolvers.com" + +# defines +# +# tolerance in distance between nodes to define a port +EMFHPORT_LENTOL = 1e-12 + +import FreeCAD, FreeCADGui, Mesh, Part, MeshPart, Draft, DraftGeomUtils, os +import EM +from FreeCAD import Vector + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore, QtGui + from DraftTools import translate + from PySide.QtCore import QT_TRANSLATE_NOOP +else: + # \cond + def translate(ctxt,txt, utf8_decode=False): + return txt + def QT_TRANSLATE_NOOP(ctxt,txt): + return txt + # \endcond + +__dir__ = os.path.dirname(__file__) +iconPath = os.path.join( __dir__, 'Resources' ) + +def makeFHPort(nodeStart=None,nodeEnd=None,name='FHPort'): + ''' Creates a FastHenry port ('.external' statement in FastHenry) + + 'nodeStart' is the positive node + 'nodeEnd' is the negative node + + Example: + TBD +''' + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name) + obj.Label = translate("EM", name) + # this adds the relevant properties to the object + #'obj' (e.g. 'Base' property) making it a _FHPort + _FHPort(obj) + # manage ViewProvider object + if FreeCAD.GuiUp: + _ViewProviderFHPort(obj.ViewObject) + # set base ViewObject properties to user-selected values (if any) + # check if 'nodeStart' is a FHNode, and if so, assign it as port start (positive) node + if nodeStart: + if Draft.getType(nodeStart) == "FHNode": + obj.NodeStart = nodeStart + # check if 'nodeEnd' is a FHNode, and if so, assign it as port end (negative) node + if nodeEnd: + if Draft.getType(nodeEnd) == "FHNode": + obj.NodeEnd = nodeEnd + # return the newly created Python object + return obj + +class _FHPort: + '''The EM FastHenry Port object''' + def __init__(self, obj): + ''' Add properties ''' + obj.addProperty("App::PropertyLink","NodeStart","EM",QT_TRANSLATE_NOOP("App::Property","Starting FHNode")) + obj.addProperty("App::PropertyLink","NodeEnd","EM",QT_TRANSLATE_NOOP("App::Property","Ending FHNode")) + obj.Proxy = self + self.Type = "FHPort" + # save the object in the class, to store or retrieve specific data from it + # from within the class + self.Object = obj + + def execute(self, obj): + ''' this method is mandatory. It is called on Document.recompute() + ''' + if obj.NodeStart == None: + return + elif Draft.getType(obj.NodeStart) <> "FHNode": + FreeCAD.Console.PrintWarning(translate("EM","NodeStart is not a FHNode")) + return + if obj.NodeEnd == None: + return + elif Draft.getType(obj.NodeEnd) <> "FHNode": + FreeCAD.Console.PrintWarning(translate("EM","NodeEnd is not a FHNode")) + return + if obj.NodeStart == obj.NodeEnd: + FreeCAD.Console.PrintWarning(translate("EM","NodeStart and NodeEnd coincide. Cannot create a port.")) + return + # and finally, if everything is ok, make and assing the shape + self.assignShape(obj) + + def assignShape(self, obj): + ''' Compute and assign the shape to the object 'obj' ''' + n1 = obj.NodeStart.Shape.Point + n2 = obj.NodeEnd.Shape.Point + shape = self.makePortShape(n1,n2) + # shape may be None, e.g. if endpoints coincide. Do not assign in this case. + # Port is still valid, but not visible. + if shape: + obj.Shape = shape + + def makePortShape(self,n1,n2): + ''' Compute a port shape given: + + 'n1': start node position (Vector) + 'n2': end node position (Vector) + ''' + # do not accept coincident nodes + if (n2-n1).Length < EMFHPORT_LENTOL: + return None + line = Part.makeLine(n1, n2) + # calculate arrow head base + direction = n1 - n2 + length = direction.Length + base = Vector(direction) + base.normalize() + base.multiply(length * 0.8) + base = n2 + base + # radius2 is calculated for a fixed arrow head angle tan(15deg)=0.27 + cone = Part.makeCone(0.2 * length * 0.27, 0.0, 0.2 * length, base, direction, 360) + # add the compound representing the arrow + arrow = Part.makeCompound([line, cone]) + return arrow + + def onChanged(self, obj, prop): + ''' take action if an object property 'prop' changed + ''' + #FreeCAD.Console.PrintWarning("\n_FHSegment onChanged(" + str(prop)+")\n") #debug + if not hasattr(self,"Object"): + # on restore, self.Object is not there anymore + self.Object = obj + return + + def serialize(self,fid): + ''' Serialize the object to the 'fid' file descriptor + ''' + fid.write(".external N" + self.Object.NodeStart.Label + " N" + self.Object.NodeEnd.Label + "\n") + +class _ViewProviderFHPort: + def __init__(self, obj): + ''' Set this object to the proxy object of the actual view provider ''' + obj.Proxy = self + self.Object = obj.Object + + def attach(self, obj): + ''' Setup the scene sub-graph of the view provider, this method is mandatory ''' + return + + def updateData(self, fp, prop): + ''' Print the name of the property that has changed ''' + #FreeCAD.Console.PrintMessage("ViewProvider updateData(), property: " + str(prop) + "\n") + ''' If a property of the handled feature has changed we have the chance to handle this here ''' + return + + def getDefaultDisplayMode(self): + ''' Return the name of the default display mode. It must be defined in getDisplayModes. ''' + return "Flat Lines" + + def onChanged(self, vp, prop): + ''' Print the name of the property that has changed ''' + #FreeCAD.Console.PrintMessage("ViewProvider onChanged(), property: " + str(prop) + "\n") + + def claimChildren(self): + ''' Used to place other objects as childrens in the tree''' + c = [] + if hasattr(self,"Object"): + if hasattr(self.Object,"NodeStart"): + c.append(self.Object.NodeStart) + if hasattr(self.Object,"NodeEnd"): + c.append(self.Object.NodeEnd) + return c + + def getIcon(self): + ''' Return the icon in XMP format which will appear in the tree view. This method is optional + and if not defined a default icon is shown. + ''' + return os.path.join(iconPath, 'port_icon.svg') + +class _CommandFHPort: + ''' The EM FastHenry Port (FHPort) command definition +''' + def GetResources(self): + return {'Pixmap' : os.path.join(iconPath, 'port_icon.svg') , + 'MenuText': QT_TRANSLATE_NOOP("EM_FHPort","FHPort"), + 'Accel': "E, P", + 'ToolTip': QT_TRANSLATE_NOOP("EM_FHPort","Creates a FastHenry Port object from two FHNodes")} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + # init properties (future) + #self.Length = None + # preferences + #p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/EM") + #self.Width = p.GetFloat("Width",200) + # get the selected object(s) + selection = FreeCADGui.Selection.getSelectionEx() + startNode = None + endNode = None + # if selection is not empty + for selobj in selection: + if Draft.getType(selobj.Object) == "FHNode": + if startNode == None: + startNode = selobj.Object + elif endNode == None: + endNode = selobj.Object + else: + FreeCAD.Console.PrintWarning(translate("EM","More than two FHNodes selected when creating a FHPort. Using only the first two.")) + if startNode <> None and endNode <> None: + FreeCAD.ActiveDocument.openTransaction(translate("EM","Create FHPort")) + FreeCADGui.addModule("EM") + FreeCADGui.doCommand('obj=EM.makeFHPort(nodeStart=FreeCAD.ActiveDocument.'+startNode.Name+',nodeEnd=FreeCAD.ActiveDocument.'+endNode.Name+')') + # autogrouping, for later on + #FreeCADGui.addModule("Draft") + #FreeCADGui.doCommand("Draft.autogroup(obj)") + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + else: + FreeCAD.Console.PrintWarning(translate("EM","Select two FHNodes for creating a FHPort")) + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('EM_FHPort',_CommandFHPort()) diff --git a/EM_FHSegment.py b/EM_FHSegment.py new file mode 100644 index 0000000..d17fc54 --- /dev/null +++ b/EM_FHSegment.py @@ -0,0 +1,421 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +__title__="FreeCAD E.M. Workbench FastHenry Segment Class" +__author__ = "FastFieldSolvers S.R.L." +__url__ = "http://www.fastfieldsolvers.com" + +# defines +# +EMFHSEGMENT_DEF_SEGWIDTH = 1.0 +EMFHSEGMENT_DEF_SEGHEIGHT = 1.0 +# tolerance in degrees when verifying if vectors are parallel +EMFHSEGMENT_PARTOL = 0.01 +# tolerance in length +EMFHSEGMENT_LENTOL = 1e-12 + +import FreeCAD, FreeCADGui, Mesh, Part, MeshPart, Draft, DraftGeomUtils, os +import EM +from FreeCAD import Vector + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore, QtGui + from DraftTools import translate + from PySide.QtCore import QT_TRANSLATE_NOOP +else: + # \cond + def translate(ctxt,txt, utf8_decode=False): + return txt + def QT_TRANSLATE_NOOP(ctxt,txt): + return txt + # \endcond + +__dir__ = os.path.dirname(__file__) +iconPath = os.path.join( __dir__, 'Resources' ) + +def makeFHSegment(baseobj=None,nodeStart=None,nodeEnd=None,name='FHSegment'): + '''Creates a FastHenry segment ('E' statement in FastHenry) + + 'baseobj' is the line object on which the node is based. + If no 'baseobj' is given, the user must assign a base + object later on, to be able to use this object. + + Example: + TBD +''' + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name) + obj.Label = translate("EM", name) + # this adds the relevant properties to the object + #'obj' (e.g. 'Base' property) making it a _FHSegment + _FHSegment(obj) + # manage ViewProvider object + if FreeCAD.GuiUp: + _ViewProviderFHSegment(obj.ViewObject) + # set base ViewObject properties to user-selected values (if any) + # check if 'nodeStart' is a FHNode, and if so, assign it as segment start node + if nodeStart: + if Draft.getType(nodeStart) == "FHNode": + obj.NodeStart = nodeStart + # check if 'nodeEnd' is a FHNode, and if so, assign it as segment end node + if nodeEnd: + if Draft.getType(nodeEnd) == "FHNode": + obj.NodeEnd = nodeEnd + # check if 'baseobj' is a wire (only base object allowed) + if baseobj: + if Draft.getType(baseobj) == "Wire": + if len(baseobj.Shape.Vertexes) == 2: + obj.Base = baseobj + else: + FreeCAD.Console.PrintWarning(translate("EM","FHSegments can only be based on Line objects (not multi-segment wires)")) + else: + FreeCAD.Console.PrintWarning(translate("EM","FHSegments can only be based on Line objects")) + # hide the base object + if obj.Base and FreeCAD.GuiUp: + obj.Base.ViewObject.hide() + # return the newly created Python object + return obj + +class _FHSegment: + '''The EM FastHenry Segment object''' + def __init__(self, obj): + ''' Add properties ''' + obj.addProperty("App::PropertyLink", "Base", "EM", QT_TRANSLATE_NOOP("App::Property","The base object this component is built upon")) + obj.addProperty("App::PropertyLink","NodeStart","EM",QT_TRANSLATE_NOOP("App::Property","Starting FHNode")) + obj.addProperty("App::PropertyLink","NodeEnd","EM",QT_TRANSLATE_NOOP("App::Property","Ending FHNode")) + obj.addProperty("App::PropertyLength","Width","EM",QT_TRANSLATE_NOOP("App::Property","Segment width ('w' segment parameter)")) + obj.addProperty("App::PropertyLength","Height","EM",QT_TRANSLATE_NOOP("App::Property","Segment height ('h' segment parameter)")) + obj.addProperty("App::PropertyFloat","Sigma","EM",QT_TRANSLATE_NOOP("App::Property","Segment conductivity ('sigma' segment parameter)")) + obj.addProperty("App::PropertyVector","ww","EM",QT_TRANSLATE_NOOP("App::Property","Segment cross-section direction along width ('wx', 'wy', 'wz' segment parameter)")) + obj.addProperty("App::PropertyInteger","nhinc","EM",QT_TRANSLATE_NOOP("App::Property","Number of filaments in the height direction ('nhinc' segment parameter)")) + obj.addProperty("App::PropertyInteger","nwinc","EM",QT_TRANSLATE_NOOP("App::Property","Number of filaments in the width direction ('nwinc' segment parameter)")) + obj.addProperty("App::PropertyInteger","rh","EM",QT_TRANSLATE_NOOP("App::Property","Ratio of adjacent filaments in the height direction ('rh' segment parameter)")) + obj.addProperty("App::PropertyInteger","rw","EM",QT_TRANSLATE_NOOP("App::Property","Ratio of adjacent filaments in the width direction ('rw' segment parameter)")) + obj.Proxy = self + self.Type = "FHSegment" + # save the object in the class, to store or retrieve specific data from it + # from within the class + self.Object = obj + + def execute(self, obj): + ''' this method is mandatory. It is called on Document.recompute() + ''' + # check if we have a 'Base' object; if so, if segment end nodes + # were already defined, re-set them according to the 'Base' object; + # this means that the user cannot move freely the end nodes, if + # there is a base object + if obj.Base: + # if right type of base + if obj.Base.isDerivedFrom("Part::Feature"): + # check validity + if obj.Base.Shape.isNull(): + return + if not obj.Base.Shape.isValid(): + return + # ok, it's valid. Let's verify if this is a Wire. + if Draft.getType(obj.Base) == "Wire": + if obj.NodeStart <> None: + # TBC warning. Should use a method of _FHNode class. Shortcut. + obj.NodeStart.Base.X = obj.Base.Start.x + obj.NodeStart.Base.Y = obj.Base.Start.y + obj.NodeStart.Base.Z = obj.Base.Start.z + #obj.NodeStart.Base.Shape = Part.Vertex(obj.Base.Start) + if obj.NodeEnd <> None: + # TBC warning. Should use a method of _FHNode class. Shortcut. + obj.NodeEnd.Base.X = obj.Base.End.x + obj.NodeEnd.Base.Y = obj.Base.End.y + obj.NodeEnd.Base.Z = obj.Base.End.z + #obj.NodeEnd.Base.Shape = Part.Vertex(obj.Base.End) + if obj.NodeStart == None: + return + elif Draft.getType(obj.NodeStart) <> "FHNode": + FreeCAD.Console.PrintWarning(translate("EM","NodeStart is not a FHNode")) + return + if obj.NodeEnd == None: + return + elif Draft.getType(obj.NodeEnd) <> "FHNode": + FreeCAD.Console.PrintWarning(translate("EM","NodeEnd is not a FHNode")) + return + if obj.Width == None or obj.Width <= 0: + obj.Width = EMFHSEGMENT_DEF_SEGWIDTH + if obj.Height == None or obj.Height <= 0: + obj.Height = EMFHSEGMENT_DEF_SEGHEIGHT + # and finally, if everything is ok, make and assing the shape + self.assignShape(obj) + + def assignShape(self, obj): + ''' Compute and assign the shape to the object 'obj' ''' + n1 = obj.NodeStart.Shape.Point + n2 = obj.NodeEnd.Shape.Point + shape = self.makeSegShape(n1,n2,obj.Width,obj.Height,obj.ww) + # shape may be None, e.g. if endpoints coincide. Do not assign in this case + if shape: + obj.Shape = shape + + def makeSegShape(self,n1,n2,width,height,ww): + ''' Compute a segment shape given: + + 'n1': start node position (Vector) + 'n2': end node position (Vector) + 'width': segment width + 'height': segment height + 'ww': cross-section direction (along width) + ''' + # do not accept coincident nodes + if (n2-n1).Length < EMFHSEGMENT_LENTOL: + return None + # vector along length + wl = n2-n1; + # calculate the vector along the height + wh = (ww.cross(wl)) + # if cross-section is not defined, by default the width vector + # is assumed to lie in x-y plane perpendicular to the length. + # If the length direction is parallel to the z-axis, then + # the width is assumed along the x-axis. + # The same is done if 'ww' has been defined parallel to 'wl' + if ww.Length < EMFHSEGMENT_LENTOL or wh.Length < EMFHSEGMENT_LENTOL: + # if length parallel to the z-axis + if wl.getAngle(Vector(0,0,1))*FreeCAD.Units.Radian < EMFHSEGMENT_PARTOL: + ww = Vector(1,0,0) + else: + ww = (wl.cross(Vector(0,0,1))).normalize() + # and re-calculate 'wh' since we changed 'ww' + wh = (ww.cross(wl)) + # normalize the freshly calculated 'wh' + wh.normalize() + # copy ww as the multiply() method changes the vector on which is called + wwHalf = Vector(ww) + # must normalize. We don't want to touch 'ww', as this is user's defined + wwHalf.normalize() + wwHalf.multiply(width / 2) + # copy wh as the multiply() method changes the vector on which is called + whHalf = Vector(wh) + whHalf.multiply(height / 2) + # calculate the vertexes + v11 = n1 - wwHalf - whHalf + v12 = n1 + wwHalf - whHalf + v13 = n1 + wwHalf + whHalf + v14 = n1 - wwHalf + whHalf + v21 = n2 - wwHalf - whHalf + v22 = n2 + wwHalf - whHalf + v23 = n2 + wwHalf + whHalf + v24 = n2 - wwHalf + whHalf + # now make faces + # front + poly = Part.makePolygon( [v11,v12,v13,v14,v11]) + face1 = Part.Face(poly) + # back + poly = Part.makePolygon( [v21,v24,v23,v22,v21]) + face2 = Part.Face(poly) + # left + poly = Part.makePolygon( [v11,v14,v24,v21,v11]) + face3 = Part.Face(poly) + # right + poly = Part.makePolygon( [v12,v22,v23,v13,v12]) + face4 = Part.Face(poly) + # top + poly = Part.makePolygon( [v14,v13,v23,v24,v14]) + face5 = Part.Face(poly) + # bottom + poly = Part.makePolygon( [v11,v21,v22,v12,v11]) + face6 = Part.Face(poly) + # create a shell. Does not need to be solid. + segShell = Part.makeShell([face1,face2,face3,face4,face5,face6]) + return segShell + + def onChanged(self, obj, prop): + ''' take action if an object property 'prop' changed + ''' + #FreeCAD.Console.PrintWarning("\n_FHSegment onChanged(" + str(prop)+")\n") #debug + if not hasattr(self,"Object"): + # on restore, self.Object is not there anymore + self.Object = obj + # if the user changed the base object. Remark: as makePoint() calls Document.recompute(), + # we cannot have the node creation in the class execute() method, since execute() is called + # upon recompute(), so we'd have recursive recompute() calls + if prop == "Base": + # check if we have a 'Base' object + if obj.Base: + # computing a shape from a base object + if obj.Base.isDerivedFrom("Part::Feature"): + # check validity + if obj.Base.Shape.isNull(): + return + if not obj.Base.Shape.isValid(): + return + # ok, it's valid. Let's verify if this is a Wire. + if Draft.getType(obj.Base) == "Wire": + p1 = Draft.makePoint(obj.Base.Start) + obj.NodeStart = EM.makeFHNode(p1) + p2 = Draft.makePoint(obj.Base.End) + obj.NodeEnd = EM.makeFHNode(p2) + + def serialize(self,fid): + ''' Serialize the object to the 'fid' file descriptor + ''' + fid.write("E" + self.Object.Label + " N" + self.Object.NodeStart.Label + " N" + self.Object.NodeEnd.Label) + fid.write(" w=" + str(self.Object.Width.Value) + " h=" + str(self.Object.Height.Value)) + if self.Object.Sigma > 0: + fid.write(" sigma=" + str(self.Object.Sigma)) + if self.Object.ww.Length >= EMFHSEGMENT_LENTOL: + fid.write(" wx=" + str(self.Object.ww.x) + " wy=" + str(self.Object.ww.y) + " wz=" + str(self.Object.ww.z)) + if self.Object.nhinc > 0: + fid.write(" nhinc=" + str(self.Object.nhinc)) + if self.Object.nwinc > 0: + fid.write(" nwinc=" + str(self.Object.nwinc)) + if self.Object.rh > 0: + fid.write(" rh=" + str(self.Object.rh)) + if self.Object.rw > 0: + fid.write(" rw=" + str(self.Object.rw)) + fid.write("\n") + +class _ViewProviderFHSegment: + def __init__(self, obj): + ''' Set this object to the proxy object of the actual view provider ''' + obj.Proxy = self + self.Object = obj.Object + + def attach(self, obj): + ''' Setup the scene sub-graph of the view provider, this method is mandatory ''' + return + + def updateData(self, fp, prop): + ''' Print the name of the property that has changed ''' + #FreeCAD.Console.PrintMessage("ViewProvider updateData(), property: " + str(prop) + "\n") + ''' If a property of the handled feature has changed we have the chance to handle this here ''' + return + + def getDefaultDisplayMode(self): + ''' Return the name of the default display mode. It must be defined in getDisplayModes. ''' + return "Flat Lines" + + def onChanged(self, vp, prop): + ''' Print the name of the property that has changed ''' + #FreeCAD.Console.PrintMessage("ViewProvider onChanged(), property: " + str(prop) + "\n") + + def claimChildren(self): + ''' Used to place other objects as childrens in the tree''' + c = [] + if hasattr(self,"Object"): + if hasattr(self.Object,"Base"): + c.append(self.Object.Base) + if hasattr(self.Object,"NodeStart"): + c.append(self.Object.NodeStart) + if hasattr(self.Object,"NodeEnd"): + c.append(self.Object.NodeEnd) + return c + + def getIcon(self): + ''' Return the icon in XMP format which will appear in the tree view. This method is optional + and if not defined a default icon is shown. + ''' + return os.path.join(iconPath, 'segment_icon.svg') + +class _CommandFHSegment: + ''' The EM FastHenry Segment (FHSegment) command definition +''' + def GetResources(self): + return {'Pixmap' : os.path.join(iconPath, 'segment_icon.svg') , + 'MenuText': QT_TRANSLATE_NOOP("EM_FHSegment","FHSegment"), + 'Accel': "E, S", + 'ToolTip': QT_TRANSLATE_NOOP("EM_FHSegment","Creates a FastHenry Segment object from scratch, from a selected base object (wire), or from two FHNodes")} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + # init properties (future) + #self.Length = None + # preferences + #p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/EM") + #self.Width = p.GetFloat("Width",200) + # get the selected object(s) + selection = FreeCADGui.Selection.getSelectionEx() + done = False + startNode = None + endNode = None + # if selection is not empty + for selobj in selection: + # automatic mode + if Draft.getType(selobj.Object) == "Wire": + FreeCAD.ActiveDocument.openTransaction(translate("EM","Create FHSegment")) + FreeCADGui.addModule("EM") + FreeCADGui.doCommand('obj=EM.makeFHSegment(FreeCAD.ActiveDocument.'+selobj.Object.Name+')') + # autogrouping, for later on + #FreeCADGui.addModule("Draft") + #FreeCADGui.doCommand("Draft.autogroup(obj)") + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + done = True + if Draft.getType(selobj.Object) == "FHNode": + if startNode == None: + startNode = selobj.Object + elif endNode == None: + endNode = selobj.Object + else: + FreeCAD.Console.PrintWarning(translate("EM","More than two FHNodes selected when creating a FHSegment. Using only the first two.")) + if startNode <> None and endNode <> None: + FreeCAD.ActiveDocument.openTransaction(translate("EM","Create FHSegment")) + FreeCADGui.addModule("EM") + FreeCADGui.doCommand('obj=EM.makeFHSegment(nodeStart=FreeCAD.ActiveDocument.'+startNode.Name+',nodeEnd=FreeCAD.ActiveDocument.'+endNode.Name+')') + # autogrouping, for later on + #FreeCADGui.addModule("Draft") + #FreeCADGui.doCommand("Draft.autogroup(obj)") + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + done = True + # if no selection, or nothing good in the selected objects + if not done: + FreeCAD.DraftWorkingPlane.setup() + # get two 3D point via Snapper, setting the callback functions + self.points = [] + FreeCADGui.Snapper.getPoint(callback=self.getPoint) + + def getPoint(self,point=None,obj=None): + "this function is called by the snapper when it has a 3D point" + if point == None: + return + self.points.append(point) + if len(self.points) == 1: + # get the second point + FreeCADGui.Snapper.getPoint(last=self.points[0],callback=self.getPoint) + elif len(self.points) >= 2: + coord1 = FreeCAD.DraftWorkingPlane.getLocalCoords(self.points[0]) + coord2 = FreeCAD.DraftWorkingPlane.getLocalCoords(self.points[1]) + FreeCAD.ActiveDocument.openTransaction(translate("EM","Create FHNode")) + FreeCADGui.addModule("EM") + FreeCADGui.doCommand('import Draft') + FreeCADGui.doCommand('from FreeCAD import Vector') + FreeCADGui.doCommand('v1 = Vector('+str(coord1.x)+','+str(coord1.y)+','+str(coord1.z)+')') + FreeCADGui.doCommand('v2 = Vector('+str(coord2.x)+','+str(coord2.y)+','+str(coord2.z)+')') + FreeCADGui.doCommand('base=Draft.makeLine(v1,v2)') + FreeCADGui.doCommand('obj=EM.makeFHSegment(base)') + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + # might improve in the future with continue command + #if self.continueCmd: + # self.Activated() + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('EM_FHSegment',_CommandFHSegment()) + diff --git a/EM_FHSolver.py b/EM_FHSolver.py new file mode 100644 index 0000000..c10ebdc --- /dev/null +++ b/EM_FHSolver.py @@ -0,0 +1,232 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +__title__="FreeCAD E.M. Workbench FastHenry Solver Class" +__author__ = "FastFieldSolvers S.R.L." +__url__ = "http://www.fastfieldsolvers.com" + +# defines +# +# copper conductivity 1/(m*Ohms) +EMFHSOLVER_DEF_SEGSIGMA = 5.8e7 +# allowed .units +EMFHSOLVER_UNITS = ["km", "m", "cm", "mm", "um", "in", "mils"] +EMFHSOLVER_UNITS_VALS = [1e3, 1, 1e-2, 1e-3, 1e-6, 2.54e-2, 1e-3] +EMFHSOLVER_DEFUNITS = "mm" +EMFHSOLVER_DEFNHINC = 1 +EMFHSOLVER_DEFNWINC = 1 +EMFHSOLVER_DEFRW = 2 +EMFHSOLVER_DEFRH = 2 +EMFHSOLVER_DEFFMIN = 1 +EMFHSOLVER_DEFFMAX = 1e9 +EMFHSOLVER_DEFNDEC = 1 +# default input file name +EMFHSOLVER_DEF_FILENAME = "fasthenry_input_file.inp" + +import FreeCAD, FreeCADGui, Mesh, Part, MeshPart, Draft, DraftGeomUtils, os +import EM +from FreeCAD import Vector + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore, QtGui + from DraftTools import translate + from PySide.QtCore import QT_TRANSLATE_NOOP +else: + # \cond + def translate(ctxt,txt, utf8_decode=False): + return txt + def QT_TRANSLATE_NOOP(ctxt,txt): + return txt + # \endcond + +__dir__ = os.path.dirname(__file__) +iconPath = os.path.join( __dir__, 'Resources' ) + +def makeFHSolver(units=None,sigma=None,nhinc=None,nwinc=None,rh=None,rw=None,fmin=None,fmax=None,ndec=None,filename=None,name='FHSolver'): + '''Creates a FastHenry Solver object (all statements needed for the simulation) + + Example: + TBD +''' + obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", name) + obj.Label = translate("EM", name) + # this adds the relevant properties to the object + #'obj' (e.g. 'Base' property) making it a _FHSegment + _FHSolver(obj) + # manage ViewProvider object + if FreeCAD.GuiUp: + _ViewProviderFHSolver(obj.ViewObject) + # set base ViewObject properties to user-selected values (if any) + if units in EMFHSOLVER_UNITS: + obj.Units = units + else: + obj.Units = EMFHSOLVER_DEFUNITS + if sigma: + obj.Sigma = sigma + else: + # use default sigma, but scale it according to the chosen units of measurement + mylist = EMFHSOLVER_UNITS + unitindex = mylist.index('mm') + unitindex = EMFHSOLVER_UNITS.index("mm") + obj.Sigma = EMFHSOLVER_DEF_SEGSIGMA * EMFHSOLVER_UNITS_VALS[EMFHSOLVER_UNITS.index(obj.Units)] + if nhinc: + obj.nhinc = nhinc + else: + obj.nhinc = EMFHSOLVER_DEFNHINC + if nwinc: + obj.nwinc = nwinc + else: + obj.nwinc = EMFHSOLVER_DEFNWINC + if rh: + obj.rh = rh + else: + obj.rh = EMFHSOLVER_DEFRH + if rw: + obj.rw = rw + else: + obj.rw = EMFHSOLVER_DEFRW + if fmin: + obj.fmin = fmin + else: + obj.fmin = EMFHSOLVER_DEFFMIN + if fmax: + obj.fmax = rw + else: + obj.fmax = EMFHSOLVER_DEFFMAX + if ndec: + obj.ndec = ndec + else: + obj.ndec = EMFHSOLVER_DEFNDEC + if filename: + obj.Filename = filename + else: + obj.Filename = EMFHSOLVER_DEF_FILENAME + # return the newly created Python object + return obj + +class _FHSolver: + '''The EM FastHenry Solver object''' + def __init__(self, obj): + ''' Add properties ''' + obj.addProperty("App::PropertyEnumeration","Units","EM",QT_TRANSLATE_NOOP("App::Property","The FastHenry '.units'")) + obj.addProperty("App::PropertyFloat","Sigma","EM",QT_TRANSLATE_NOOP("App::Property","Default Segment conductivity ('sigma' segment parameter in '.default')")) + obj.addProperty("App::PropertyInteger","nhinc","EM",QT_TRANSLATE_NOOP("App::Property","Default number of filaments in the height direction ('nhinc' segment parameter in '.default')")) + obj.addProperty("App::PropertyInteger","nwinc","EM",QT_TRANSLATE_NOOP("App::Property","Default number of filaments in the width direction ('nwinc' segment parameter in '.default')")) + obj.addProperty("App::PropertyInteger","rh","EM",QT_TRANSLATE_NOOP("App::Property","Default ratio of adjacent filaments in the height direction ('rh' segment parameter in '.default')")) + obj.addProperty("App::PropertyInteger","rw","EM",QT_TRANSLATE_NOOP("App::Property","Default ratio of adjacent filaments in the width direction ('rw' segment parameter in '.default')")) + obj.addProperty("App::PropertyFloat","fmin","EM",QT_TRANSLATE_NOOP("App::Property","Lowest simulation frequency ('fmin' parameter in '.freq')")) + obj.addProperty("App::PropertyFloat","fmax","EM",QT_TRANSLATE_NOOP("App::Property","Highest simulation frequency ('fmzx' parameter in '.freq')")) + obj.addProperty("App::PropertyFloat","ndec","EM",QT_TRANSLATE_NOOP("App::Property","Number of desired frequency points per decade ('ndec' parameter in '.freq')")) + obj.addProperty("App::PropertyFile","Filename","EM",QT_TRANSLATE_NOOP("App::Property","Simulation filename when exporting to FastHenry input file format")) + obj.Proxy = self + self.Type = "FHSolver" + obj.Units = EMFHSOLVER_UNITS + + def execute(self, obj): + ''' this method is mandatory. It is called on Document.recompute() + ''' + # but nothing to do + return + + def onChanged(self, obj, prop): + ''' take action if an object property 'prop' changed + ''' + #FreeCAD.Console.PrintWarning("\n_FHSolver onChanged(" + str(prop)+")\n") #debug + if not hasattr(self,"Object"): + # on restore, self.Object is not there anymore + self.Object = obj + + def serialize(self,fid,headOrTail): + ''' Serialize the object to the 'fid' file descriptor + ''' + if headOrTail == "head": + fid.write("* FastHenry input file created using FreeCAD's ElectroMagnetic Workbench\n") + fid.write("* See http://www.freecad.org and http://www.fastfieldsolvers.com\n") + fid.write("\n") + fid.write(".units " + self.Object.Units + "\n") + fid.write("\n") + fid.write(".default sigma=" + str(self.Object.Sigma) + " nhinc=" + str(self.Object.nhinc) + " nwinc=" + str(self.Object.nwinc)) + fid.write(" rh=" + str(self.Object.rh) + " rw=" + str(self.Object.rw) + "\n") + fid.write("\n") + else: + fid.write(".freq fmin=" + str(self.Object.fmin) + " fmax=" + str(self.Object.fmax) + " ndec=" + str(self.Object.ndec) + "\n") + fid.write("\n") + fid.write(".end\n") + +class _ViewProviderFHSolver: + def __init__(self, obj): + ''' Set this object to the proxy object of the actual view provider ''' + obj.Proxy = self + self.Object = obj.Object + + def attach(self, obj): + ''' Setup the scene sub-graph of the view provider, this method is mandatory ''' + return + + def updateData(self, fp, prop): + ''' Print the name of the property that has changed ''' + #FreeCAD.Console.PrintMessage("ViewProvider updateData(), property: " + str(prop) + "\n") + ''' If a property of the handled feature has changed we have the chance to handle this here ''' + return + + def getDefaultDisplayMode(self): + ''' Return the name of the default display mode. It must be defined in getDisplayModes. ''' + return "Flat Lines" + + def onChanged(self, vp, prop): + ''' Print the name of the property that has changed ''' + #FreeCAD.Console.PrintMessage("ViewProvider onChanged(), property: " + str(prop) + "\n") + + def getIcon(self): + ''' Return the icon in XMP format which will appear in the tree view. This method is optional + and if not defined a default icon is shown. + ''' + return os.path.join(iconPath, 'solver_icon.svg') + +class _CommandFHSolver: + ''' The EM FastHenry Solver command definition +''' + def GetResources(self): + return {'Pixmap' : os.path.join(iconPath, 'solver_icon.svg') , + 'MenuText': QT_TRANSLATE_NOOP("EM_FHSolver","FHSolver"), + 'Accel': "E, X", + 'ToolTip': QT_TRANSLATE_NOOP("EM_FHSolver","Creates a FastHenry Solver object")} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + # init properties (future) + #self.Length = None + # preferences + #p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/EM") + #self.Width = p.GetFloat("Width",200) + FreeCAD.ActiveDocument.openTransaction(translate("EM","Create FHSolver")) + FreeCADGui.addModule("EM") + FreeCADGui.doCommand('obj=EM.makeFHSolver()') + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('EM_FHSolver',_CommandFHSolver()) diff --git a/Export_mesh.py b/Export_mesh.py index fb816cc..55dbd1a 100644 --- a/Export_mesh.py +++ b/Export_mesh.py @@ -1,6 +1,6 @@ #*************************************************************************** #* * -#* Copyright (c) 2014 * +#* Copyright (c) 2018 * #* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * #* * #* This program is free software; you can redistribute it and/or modify * @@ -124,7 +124,7 @@ def export_mesh(filename, meshobj=None, isDiel=False, showNormals=False, folder= fid.closed def make_arrow(startpoint, endpoint): - '''create an arrow + '''Create an arrow 'startpoint' is a Vector specifying the start position 'endpoint' is a Vector specifying the end position @@ -146,8 +146,8 @@ def make_arrow(startpoint, endpoint): return arrow -def export_faces(filename, isDiel=False, name="", showNormals=False, folder=DEF_FOLDER): - '''export faces in FasterCap format as conductor or dielectric interface +def export_faces(filename, isDiel=False, name="", showNormals=False, forceMesh=False, folder=DEF_FOLDER): + '''Export faces in FasterCap format as conductor or dielectric interface The function operates on the selection. The selection can be a face, a compound or a solid. 'filename' is the name of the export file @@ -164,7 +164,7 @@ def export_faces(filename, isDiel=False, name="", showNormals=False, folder=DEF_ ''' # get selection sel = FreeCADGui.Selection.getSelection() - # if no valid mesh was passed + # if no valid selection was passed if sel == None: return @@ -186,8 +186,12 @@ def export_faces(filename, isDiel=False, name="", showNormals=False, folder=DEF_ faces.extend(obj.Shape.Faces) # scan faces and find out which faces have more than 4 vertexes # TBD warning: should mesh also curve faces - facesComplex = [x for x in faces if len(x.Vertexes) >= 5] - facesSimple = [x for x in faces if len(x.Vertexes) < 5] + if forceMesh == False: + facesComplex = [x for x in faces if len(x.Vertexes) >= 5] + facesSimple = [x for x in faces if len(x.Vertexes) < 5] + else: + facesComplex = faces + facesSimple = [] # mesh complex faces doc = FreeCAD.ActiveDocument for face in facesComplex: @@ -197,7 +201,7 @@ def export_faces(filename, isDiel=False, name="", showNormals=False, folder=DEF_ # now we have faces and facets. Uniform all panels = [] for face in facesSimple: - sortEdges = DraftGeomUtils.sortEdges(face.Edges) + sortEdges = Part.__sortEdges__(face.Edges) # Point of a Vertex is a Vector, as well as Face.normalAt() points = [x.Vertexes[0].Point for x in sortEdges] panels.append( [points, face.normalAt(0,0)] ) @@ -269,4 +273,3 @@ def export_faces(filename, isDiel=False, name="", showNormals=False, folder=DEF_ normalobj.Shape = normals - diff --git a/Init.py b/Init.py new file mode 100644 index 0000000..5a1bd4e --- /dev/null +++ b/Init.py @@ -0,0 +1,25 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +# add import/export types +FreeCAD.addExportType("FastHenry file format (*.inp)","exportFH") diff --git a/InitGui.py b/InitGui.py new file mode 100644 index 0000000..83b6ed5 --- /dev/null +++ b/InitGui.py @@ -0,0 +1,75 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +__title__="FreeCAD E.M. Workbench GUI" +__author__ = "FastFieldSolvers S.R.L." +__url__ = "http://www.fastfieldsolvers.com" + +class EMWorkbench(Workbench): + "E.M. workbench object" + def __init__(self): + self.__class__.Icon = FreeCAD.getUserAppDataDir()+ "Mod/EM/Resources/EMWorkbench.svg" + self.__class__.MenuText = "E.M." + self.__class__.ToolTip = "ElectroMagnetic workbench" + + def Initialize(self): + import DraftTools,DraftGui + from DraftTools import translate + + # import the EM module (and therefore all commands makeXXX) + import EM + + # E.M. tools + self.emtools = ["EM_FHSolver", "EM_FHNode", "EM_FHSegment", "EM_FHPort", "EM_FHInputFile"] + + def QT_TRANSLATE_NOOP(scope, text): return text + self.appendToolbar(QT_TRANSLATE_NOOP("Workbench","E.M. tools"),self.emtools) + self.appendMenu(QT_TRANSLATE_NOOP("EM","&EM"),self.emtools) + #FreeCADGui.addIconPath(":/icons") + #FreeCADGui.addLanguagePath(":/translations") + #FreeCADGui.addPreferencePage(":/ui/preferences-EM.ui","EM") + #FreeCADGui.addPreferencePage(":/ui/preferences-aEMdefaults.ui","EM") + + Log ('Loading EM module... done\n') + + def Activated(self): + Log("EM workbench activated\n") + + def Deactivated(self): + Log("EM workbench deactivated\n") + +# def ContextMenu(self, recipient): +# self.appendContextMenu("Utilities",self.EMcontexttools) + + # needed if this is a pure Python workbench + def GetClassName(self): + return "Gui::PythonWorkbench" + +FreeCADGui.addWorkbench(EMWorkbench) + +# File format pref pages are independent and can be loaded at startup +#import EM_rc +#FreeCADGui.addPreferencePage(":/ui/preferences-inp.ui","Import-Export") + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a62279 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# ElectroMagnetic workbench for FreeCAD +Copyright (c) 2018 +FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com + +## Description + +This project is dedicated to building an ElectroMagnetic workbench for [FreeCAD](https://www.freecadweb.org). FreeCAD is a free 3D parametric CAD. +FreeCAD is used as pre-processor interfacing to the electromagnetic field solvers. + +At present, the workbench supports: + +- [FastHenry](https://www.fastfieldsolvers.com/fasthenry2.htm) inductance solver: ongoing development including GUI support +- [FasterCap](https://www.fastfieldsolvers.com/fastercap.htm) capacitance solver: ongoing development, today at the stage of Python macro only, for creating an input file + + +## Installing + +The ElectroMagnetic workbench is managed as a FreeCAD addon. This addon can be downloaded by clicking the **Download ZIP** button found on top of the page, or using **Git**. The addon must be placed in your user's FreeCAD/Mod folder. + +**Note**: Your user's FreeCAD folder location is obtained by typing in FreeCAD's python console: `FreeCAD.ConfigGet("UserAppData")` + +## Additional information + +For any additional information please visit [FastFieldSolvers](https://www.fastfieldsolvers.com/), write on the [FastFieldSolvers Forum](https://www.fastfieldsolvers.com/forum) or on the [FreeCAD Forum](https://forum.freecadweb.org/viewforum.php?f=18) under the FEM topic. + +See LICENCE.txt for the license conditions. + +Access to the binary and source code download pages on [FastFieldSolvers](https://www.fastfieldsolvers.com/) is free, and you may access anonymously if you want. diff --git a/README.txt b/README.txt deleted file mode 100644 index 4a5727e..0000000 --- a/README.txt +++ /dev/null @@ -1,13 +0,0 @@ -========================================= - ElectroMagnetic workbench for FreeCAD -========================================= - - -Project for building an ElectroMagnetic workbench for FreeCAD. FreeCAD is a free 3D parametric CAD. -FreeCAD is used as pre-processor interfacing to the electromagnetic field solver. -At present, this is just a macro for creating an input file for the FasterCap capacitance solver by FastFieldSolvers S.R.L. - -The macro must be loaded into FreeCAD, see www.freecadweb.org - -For FasterCap and additional information please visit http://www.fastfieldsolvers.com/ -Access to the download pages is free, and you may access anonymously if you want. diff --git a/Resources/EMWorkbench.svg b/Resources/EMWorkbench.svg new file mode 100644 index 0000000..f4a1131 --- /dev/null +++ b/Resources/EMWorkbench.svg @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + [triplus] + + + ArchWorkbench + 2016-02-26 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Arch/Resources/icons/ArchWorkbench.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + - + + + + + + + + + + diff --git a/Resources/inputfile_icon.svg b/Resources/inputfile_icon.svg new file mode 100644 index 0000000..d5aa732 --- /dev/null +++ b/Resources/inputfile_icon.svg @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/Resources/node_icon.svg b/Resources/node_icon.svg new file mode 100644 index 0000000..1f1ba15 --- /dev/null +++ b/Resources/node_icon.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + [wmayer] + + + Sketcher_CreatePoint + 2011-10-10 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Sketcher/Gui/Resources/icons/Sketcher_CreatePoint.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + diff --git a/Resources/port_icon.svg b/Resources/port_icon.svg new file mode 100644 index 0000000..98a65b6 --- /dev/null +++ b/Resources/port_icon.svg @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Draft_Upgrade + + Mon Oct 10 13:44:52 2011 +0000 + + + [wmayer] + + + + + FreeCAD LGPL2+ + + + + + FreeCAD + + + FreeCAD/src/Mod/Draft/Resources/icons/Draft_Upgrade.svg + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + [agryson] Alexander Gryson + + + + + arrow + up + + + A large blue arrow pointing upwards + + + + diff --git a/Resources/segment_icon.svg b/Resources/segment_icon.svg new file mode 100644 index 0000000..e69a86f --- /dev/null +++ b/Resources/segment_icon.svg @@ -0,0 +1,479 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + [triplus] + + + ArchWorkbench + 2016-02-26 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Arch/Resources/icons/ArchWorkbench.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + diff --git a/Resources/solver_icon.svg b/Resources/solver_icon.svg new file mode 100644 index 0000000..79be343 --- /dev/null +++ b/Resources/solver_icon.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + A + + diff --git a/export_to_FastHenry.py b/export_to_FastHenry.py new file mode 100644 index 0000000..60d52c4 --- /dev/null +++ b/export_to_FastHenry.py @@ -0,0 +1,469 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +import FreeCAD, Mesh, Part, MeshPart, DraftGeomUtils, os +from FreeCAD import Vector + +if FreeCAD.GuiUp: + import FreeCADGui + from PySide import QtCore, QtGui +else: + def translate(ctxt,txt): + return txt + +__title__="FreeCAD E.M. FastHenry2 Macros" +__author__ = "FastFieldSolvers S.R.L." +__url__ = "http://www.fastfieldsolvers.com" + +DEF_FOLDER = "." + + +def export_segs(filename="", disc=3, custDot="", FHbug=False, w=0, h=0, nhinc=0, nwinc=0, folder=DEF_FOLDER): + '''Export segments in FastHenry format + + The function operates on the selection. The selection must be a sketch, a wire or an edge. + 'filename' is the name of the export file + 'disc' is the maximum number of segments into which curves will be discretized + 'custDot' is a custom directive added in the output file (a string added as it is on top of the file) + 'FHbug' works around a FastHenry bug happening for some very exact values of diagonal parallel segments, + giving rise to 'uh oh segments don't seem parallel' kind of errors + 'w', 'h', 'nhinc', 'nwinc' are the FastHenry parameters; + if zero, they are ignored (FastHenry will use the .default values). + If 'w' is not zero, no segment shorter than abs(w)*3 will be output. Note that the end point of + the previous segment will be the starting point of the *next* segment (skipping the short one). + This might cause misalignments if there are many consecutive short segments. + If 'w' is negative, it assures that no curve will be discretized if the radius is less than w*3, + to avoid short thick (overlapping) segments. + 'folder' is the folder in which 'filename' will be saved + + Example: + export_segs("mysegs.inp", folder="C:/temp") +''' + # get selection + sel = FreeCADGui.Selection.getSelection() + # if no valid selection was passed + if sel == None: + return + + if filename == "": + filename = sel[0].Label.replace(" ","_") + ".txt" + + if not os.path.isdir(folder): + os.mkdir(folder) + + with open(folder + os.sep + filename, 'w') as fid: + + fid.write("* Conductor definition file for the following objects\n") + for obj in sel: + fid.write("* - " + obj.Label + "\n") + fid.write("* created using FreeCAD's ElectroMagnetic Workbench\n") + fid.write("* see http://www.freecad.org and http://www.fastfieldsolvers.com\n") + fid.write("\n") + + # scan objects in selection and export to FastHenry one by one + + for obj in sel: + + edges_raw = [] + # checking TypeId; cannot check type(obj), too generic + if obj.TypeId == "Sketcher::SketchObject": + if obj.Shape.ShapeType == "Wire": + edges_raw.extend(obj.Shape.Edges) + # compound + elif obj.TypeId == "Part::Compound": + edges_raw.extend(obj.Shape.Edges) + # line or DWire (Draft Wire) + elif obj.TypeId == "Part::Part2DObjectPython": + if obj.Shape.ShapeType == "Wire": + edges_raw.extend(obj.Shape.Edges) + # wire created by upgrading a set of (connected) edges + elif obj.TypeId == "Part::Feature": + if obj.Shape.ShapeType == "Wire": + edges_raw.extend(obj.Shape.Edges) + # any other part, provided it has a 'Shape' attribute + else: + if hasattr(obj, "Shape"): + if obj.Shape.ShapeType == "Wire": + edges_raw.extend(obj.Shape.Edges) + else: + # to be implemented? + FreeCAD.Console.PrintMessage("Unsupported object type for '" + obj.Label + "', skipping\n") + continue + + # sort the edges. If the selected path is disconnected, the path will be broken! + edges = Part.__sortEdges__(edges_raw) + # TBC: join parts with additional edges, or .equiv-ing them, using distToShape between the obj.Shape + # Can happen with a compound containing different edges / wires / stetches + #edge = Part.Edge(Part.Line(Vector(154.0002, -62.6872,0), Vector(154.0002,-53.1876,0))) + #v = Part.Vertex(edges[0].Curve.StartPoint) + #v.Tolerance + #App.ActiveDocument.Shape.Shape.Vertexes[1].distToShape(App.ActiveDocument.Shape001.Shape.Vertexes[0]) + + # scan edges and derive nodes + nodes = [] + for edge in edges: + if type(edge.Curve) == Part.Circle: + # discretize + if edge.Curve.Radius < -w*3 and w < 0: + ddisc = 1 + else: + ddisc = disc + for i in range(0, ddisc): + step = (edge.LastParameter - edge.FirstParameter) / ddisc + # always skip last vertex, as the next edge will start where this finishes + nodes.append(edge.valueAt(edge.FirstParameter + i*step)) + # quick & dirty trick + lastvertex = edge.valueAt(edge.LastParameter) + elif type(edge.Curve) == Part.Ellipse: + # discretize + if (edge.Curve.MajorRadius < -w*3 or edge.Curve.MinorRadius < -w*3) and w < 0: + ddisc = 1 + else: + ddisc = disc + for i in range(0, ddisc): + step = (edge.LastParameter - edge.FirstParameter) / ddisc + # always skip last vertex, as the next edge will start where this finishes + nodes.append(edge.valueAt(edge.FirstParameter + i*step)) + # quick & dirty trick + lastvertex = edge.valueAt(edge.LastParameter) + elif type(edge.Curve) == Part.Line: + # if w=0, the following condition is always true + if edge.Length > abs(w)*3: + nodes.append(edge.Curve.StartPoint) + # quick & dirty trick + lastvertex = edge.Curve.EndPoint + else: + FreeCAD.Console.PrintMessage("Unknown edge: " + str(type(edge.Curve)) + " in '" + obj.Label + "',, skipping\n") + # now add the very last vertex + nodes.append(lastvertex) + + if len(nodes) < 2: + FreeCAD.Console.PrintMessage("Less than two nodes found in '" + obj.Label + "', skipping\n") + continue + + # start actual object output in FastHenry format + fid.write("* " + obj.Label + "\n") + if custDot != "": + fid.write(custDot + "\n") + baseName = obj.Label.replace(" ","_") + "_" + # now create nodes + for i, node in enumerate(nodes): + # extension in the node name must be "S" for the Start node + # and "E" for the End node + if i == 0: + ext = "S" + elif i == len(nodes)-1: + ext = "E" + else: + ext = str(i) + + if FHbug == True: + fid.write("N" + baseName + ext + " x=" + str(node.x) + " y=" + str(int(node.y)) + " z=" + str(node.z) + "\n") + else: + fid.write("N" + baseName + ext + " x=" + str(node.x) + " y=" + str(node.y) + " z=" + str(node.z) + "\n") + + # and finally segments + for i in range(0, len(nodes)-1): + # extension in the node name must be "S" for the Start node + # and "E" for the End node + # + # start node + if i == 0: + ext1 = "S" + else: + ext1 = str(i) + # end node + if i >= len(nodes)-2: + ext2 = "E" + else: + ext2 = str(i+1) + + fid.write("E" + baseName + "N" + ext1 + "N" + ext2 + " ") + fid.write("N" + baseName + ext1 + " " + "N" + baseName + ext2) + if w > 0: + fid.write(" w=" + str(w)) + if h > 0: + fid.write(" h=" + str(w)) + if nhinc > 0: + fid.write(" nhinc=" + str(w)) + if nwinc > 0: + fid.write(" nwinc=" + str(w)) + fid.write("\n") + # blank lines before next object + fid.write("\n\n") + + fid.closed + +def export_segs2(filename="", disc=3, custDot="", FHbug=False, breakSeg=False, w=0, h=0, nhinc=0, nwinc=0, folder=DEF_FOLDER): + '''Export segments in FastHenry format + + The function operates on the selection. The selection must be a sketch, a wire or an edge. + Version 2 means it discretizes both curved and straight parts of a path. It also dumps nodes of an underlying GND plane. + 'filename' is the name of the export file + 'disc' is the maximum number of segments into which curves will be discretized + 'custDot' is a custom directive added in the output file (a string added as it is on top of the file) + 'FHbug' works around a FastHenry bug happening for some very exact values of diagonal parallel segments, + giving rise to 'uh oh segments don't seem parallel' kind of errors + 'breakSeg' if true breaks also straight segments into 'disc' parts + 'w', 'h', 'nhinc', 'nwinc' are the FastHenry parameters; + if zero, they are ignored (FastHenry will use the .default values). + If 'w' is not zero, no segment shorter than abs(w)*3 will be output. Note that the end point of + the previous segment will be the starting point of the *next* segment (skipping the short one). + This might cause misalignments if there are many consecutive short segments. + If 'w' is negative, it assures that no curve will be discretized if the radius is less than w*3, + to avoid short thick (overlapping) segments. + 'folder' is the folder in which 'filename' will be saved + + Example: + export_segs2("mysegs.inp", folder="C:/temp") +''' + # get selection + sel = FreeCADGui.Selection.getSelection() + # if no valid selection was passed + if sel == None: + return + + if filename == "": + filename = sel[0].Label.replace(" ","_") + ".txt" + + if not os.path.isdir(folder): + os.mkdir(folder) + + with open(folder + os.sep + filename, 'w') as fid: + + fid.write("* Conductor definition file for the following objects\n") + for obj in sel: + fid.write("* - " + obj.Label + "\n") + fid.write("* created using FreeCAD's ElectroMagnetic Workbench\n") + fid.write("* see http://www.freecad.org and http://www.fastfieldsolvers.com\n") + fid.write("\n") + + + # scan objects in selection and export to FastHenry one by one + gndplane_nodes = [] + for obj in sel: + + edges_raw = [] + # checking TypeId; cannot check type(obj), too generic + if obj.TypeId == "Sketcher::SketchObject": + if obj.Shape.ShapeType == "Wire": + edges_raw.extend(obj.Shape.Edges) + # compound + elif obj.TypeId == "Part::Compound": + edges_raw.extend(obj.Shape.Edges) + # line or DWire (Draft Wire) + elif obj.TypeId == "Part::Part2DObjectPython": + if obj.Shape.ShapeType == "Wire": + edges_raw.extend(obj.Shape.Edges) + # wire created by upgrading a set of (connected) edges + elif obj.TypeId == "Part::Feature": + if obj.Shape.ShapeType == "Wire": + edges_raw.extend(obj.Shape.Edges) + # any other part, provided it has a 'Shape' attribute + else: + if hasattr(obj, "Shape"): + if obj.Shape.ShapeType == "Wire": + edges_raw.extend(obj.Shape.Edges) + else: + # to be implemented? + FreeCAD.Console.PrintMessage("Unsupported object type for '" + obj.Label + "', skipping\n") + continue + + # sort the edges. If the selected path is disconnected, the path will be broken! + edges = Part.__sortEdges__(edges_raw) + # TBC: join parts with additional edges, or .equiv-ing them, using distToShape between the obj.Shape + # Can happen with a compound containing different edges / wires / stetches + #edge = Part.Edge(Part.Line(Vector(154.0002, -62.6872,0), Vector(154.0002,-53.1876,0))) + #v = Part.Vertex(edges[0].Curve.StartPoint) + #v.Tolerance + #App.ActiveDocument.Shape.Shape.Vertexes[1].distToShape(App.ActiveDocument.Shape001.Shape.Vertexes[0]) + + # scan edges and derive nodes + nodes = [] + for edge in edges: + if type(edge.Curve) == Part.Circle: + # discretize + if edge.Curve.Radius < -w*3 and w < 0: + ddisc = 1 + else: + ddisc = disc + for i in range(0, ddisc): + step = (edge.LastParameter - edge.FirstParameter) / ddisc + # always skip last vertex, as the next edge will start where this finishes + nodes.append(edge.valueAt(edge.FirstParameter + i*step)) + # quick & dirty trick + lastvertex = edge.valueAt(edge.LastParameter) + elif type(edge.Curve) == Part.Ellipse: + # discretize + if (edge.Curve.MajorRadius < -w*3 or edge.Curve.MinorRadius < -w*3) and w < 0: + ddisc = 1 + else: + ddisc = disc + for i in range(0, ddisc): + step = (edge.LastParameter - edge.FirstParameter) / ddisc + # always skip last vertex, as the next edge will start where this finishes + nodes.append(edge.valueAt(edge.FirstParameter + i*step)) + # quick & dirty trick + lastvertex = edge.valueAt(edge.LastParameter) + elif type(edge.Curve) == Part.Line: + # if w=0, the following condition is always true + if edge.Length > abs(w)*3: + if breakSeg == False: + ddisc = 1 + else: + ddisc = disc + for i in range(0, ddisc): + step = (edge.LastParameter - edge.FirstParameter) / ddisc + # always skip last vertex, as the next edge will start where this finishes + nodes.append(edge.valueAt(edge.FirstParameter + i*step)) + # quick & dirty trick + lastvertex = edge.valueAt(edge.LastParameter) + else: + FreeCAD.Console.PrintMessage("Unknown edge: " + str(type(edge.Curve)) + " in '" + obj.Label + "',, skipping\n") + # now add the very last vertex + nodes.append(lastvertex) + + if len(nodes) < 2: + FreeCAD.Console.PrintMessage("Less than two nodes found in '" + obj.Label + "', skipping\n") + continue + + # start actual object output in FastHenry format + fid.write("* " + obj.Label + "\n") + if custDot != "": + fid.write(custDot + "\n") + baseName = obj.Label.replace(" ","_") + "_" + # now create nodes + for i, node in enumerate(nodes): + # extension in the node name must be "S" for the Start node + # and "E" for the End node + if i == 0: + ext = "S" + elif i == len(nodes)-1: + ext = "E" + else: + ext = str(i) + + if FHbug == True: + fid.write("N" + baseName + ext + " x=" + str(node.x) + " y=" + str(int(node.y)) + " z=" + str(node.z) + "\n") + gndplane_nodes.append( (baseName+ext, str(node.x), str(int(node.y)), str(node.z)) ) + else: + fid.write("N" + baseName + ext + " x=" + str(node.x) + " y=" + str(node.y) + " z=" + str(node.z) + "\n") + gndplane_nodes.append( (baseName+ext, str(node.x), str(int(node.y)), str(node.z)) ) + + # and finally segments + for i in range(0, len(nodes)-1): + # extension in the node name must be "S" for the Start node + # and "E" for the End node + # + # start node + if i == 0: + ext1 = "S" + else: + ext1 = str(i) + # end node + if i >= len(nodes)-2: + ext2 = "E" + else: + ext2 = str(i+1) + + fid.write("E" + baseName + "N" + ext1 + "N" + ext2 + " ") + fid.write("N" + baseName + ext1 + " " + "N" + baseName + ext2) + if w > 0: + fid.write(" w=" + str(w)) + if h > 0: + fid.write(" h=" + str(w)) + if nhinc > 0: + fid.write(" nhinc=" + str(w)) + if nwinc > 0: + fid.write(" nwinc=" + str(w)) + fid.write("\n") + + # blank lines before next object + fid.write("\n\n") + + # create GND plane nodes + for gndplane_node in gndplane_nodes: + fid.write("+ Nplane" + gndplane_node[0] + " (" + gndplane_node[1] + "," + + gndplane_node[2] + "," + "-1.5" + ")\n" ) + + # blank lines before next object + fid.write("\n\n") + + # create .equiv plane nodes statements + for gndplane_node in gndplane_nodes: + fid.write(".equiv Nplane" + gndplane_node[0] + " N" + gndplane_node[0] + "\n") + + fid.closed + + +def create_FH_plane(filename="", seg1=10, seg2=10, wx=10, wy=10, name="", custDot="", thick=1.0, folder=DEF_FOLDER): + '''Create a conductive plane using primitive FastHenry segments + + 'filename' is the name of the export file + 'seg1' is the number of segments along x + 'seg2' is the number of segments along y + 'wx', 'wy' are the plane dimensions along x and y + 'name' is the node extension name (e.g. Nname_1_2) + 'folder' is the folder in which 'filename' will be saved + + Example: + create_FH_plane("plane.inp", seg1=5, seg2=3, folder="C:/temp") +''' + + if filename == "": + filename = sel[0].Label.replace(" ","_") + ".txt" + + if not os.path.isdir(folder): + os.mkdir(folder) + + with open(folder + os.sep + filename, 'w') as fid: + + fid.write("* Conductive plane built using primitive FastHenry segments\n") + fid.write("* created using FreeCAD's ElectroMagnetic Workbench\n") + fid.write("* see http://www.freecad.org and http://www.fastfieldsolvers.com\n") + fid.write("\n") + + stepx = wx / seg1 + stepy = wy / seg2 + + # lay down nodes + + for i in range(0, seg1+1): + for j in range(0, seg2+1): + fid.write("N" + name + "_" + str(i) + "_" + str(j) + " x=" + str(i*stepx) + " y=" + str(j*stepy) + " z=0 \n") + + # lay down segments + # + # along y + for i in range(0, seg1+1): + for j in range(0, seg2): + fid.write("E2"+ name + "_" + str(i) + "_" + str(j) + " N" + name + "_" + str(i) + "_" + str(j) + " N" + name + "_" + str(i) + "_" + str(j+1) + " w=" + str(stepx) + " h=" + str(thick) + " \n") + # along x + for j in range(0, seg2+1): + for i in range(0, seg1): + fid.write("E2"+ name + "_" + str(i) + "_" + str(j) + " N" + name + "_" + str(i) + "_" + str(j) + " N" + name + "_" + str(i+1) + "_" + str(j) + " w=" + str(stepy) + " h=" + str(thick) + " \n") + + fid.write("\n") + + fid.closed + diff --git a/import_fastercap.py b/import_fastercap.py index c740c0c..3cb283e 100644 --- a/import_fastercap.py +++ b/import_fastercap.py @@ -22,7 +22,8 @@ #*************************************************************************** import FreeCAD, Mesh, Draft, Part, os -#from FreeCAD import Vector +from collections import namedtuple +from FreeCAD import Vector if FreeCAD.GuiUp: import FreeCADGui @@ -37,7 +38,358 @@ __url__ = "http://www.fastfieldsolvers.com" DEF_FOLDER = "." COLORMAP_LEN = 256 +AUTOREFINE_MAX_PARSE_LEVEL = 32 +# filePosMap members +filePosData = namedtuple('filePosData', ['lineNum', 'filePos']) + +# global vars +# +m_lDielNum = 0 +m_lCondNum = 0 +m_iParseLevel = -1 +m_iGroupNum = [1] +m_lGroupDielNum = 0 +m_bUseMesh = True +# number of input panels +m_ulInputPanelNum = 0 + + +def read_fastcap_file(filename, folder=DEF_FOLDER, usePartType='compound'): + '''Import file in FasterCap format as Mesh or Part.compound + + 'filename' is the name of the export file + 'folder' is the folder where the file resides + + Example: + fastercapObj = read_fastcap_file('cube.txt') +''' + + # + # this function is a Python-converted version of the FasterCap C++ + # ReadFastCapFile() import function (and associated functions) + # + + global m_lDielNum + global m_lCondNum + global m_iParseLevel + global m_iGroupNum + global m_lGroupDielNum + global m_sUsePartType + global m_ulInputPanelNum + + # init global vars + m_lDielNum = 0 + m_lCondNum = 0 + m_iParseLevel = -1 + m_iGroupNum = [1 for x in range(0,AUTOREFINE_MAX_PARSE_LEVEL)] + m_lGroupDielNum = 0 + m_sUsePartType = usePartType + # init number of input panels + m_ulInputPanelNum = 0 + + + if not os.path.isdir(folder): + FreeCAD.Console.PrintMessage("Error: '" + folder + "' is not a valid folder\n") + return False + + if not os.path.exists(folder + os.sep + filename): + FreeCAD.Console.PrintMessage("Error: '" + filename + "' is not a valid file in the directory " + folder + "\n") + return False + + # understand the type of input file (2D or 3D) + fileinname =fol + der + os.sep + filename + line = '' + try: + with open(fileinname, 'r') as fid: + line = fid.readline() + fid.closed + except OSError as err: + FreeCAD.Console.PrintMessage("OS error: " + format(err) + "\n") + return False + + # clear filePosMap dictionary + filePosMap = {} + if '2d' in line or '2D' in line: + # passing dummy 'fid' and 'filePosMap' (that must be empty) since + # there is no parent file + ret = parse_2D_input_file(fileinname, fid, filePosMap); + else: + # passing dummy 'fid' and 'filePosMap' (that must be empty) since + # there is no parent file + ret = parse_3D_input_file(fileinname, fid, filePosMap); + + return ret + +def parse_2D_input_file(fileinname, fid, filePosMap, use_mesh): + + FreeCAD.Console.PrintMessage("Parse 2D\n") + return True + + +def parse_3D_input_file(fileinname, parentFid, parentFilePosMap, isdiel = False, offset = Vector(0.0, 0.0, 0.0), + outperm = complex(1.0), groupname = '', inperm = complex(1.0), dielrefpoint = Vector(0.0, 0.0, 0.0)): + + global m_iParseLevel + global m_iGroupNum + global m_lGroupDielNum + global m_sUsePartType + global m_ulInputPanelNum + + # increment the recursion level counter + m_iParseLevel = m_iParseLevel + 1 + + if m_iParseLevel >= AUTOREFINE_MAX_PARSE_LEVEL: + FreeCAD.Console.PrintMessage("Warning: maxumum number (" + format(AUTOREFINE_MAX_PARSE_LEVEL) + + ") of recursive files exceeded, skipping file " + fileinname + "\n") + return True + + # reset group number for current parse level + m_iGroupNum[m_iParseLevel] = 1; + + # init filePosMap + filePosMap = {} + + # check if the conductor file is a sub-file + if fileinname in parentFilePosMap: + # if it is a sub-file, copy parent data + filePosMap = parentFilePosMap + fid = parentFid + try: + # store current file position to restore it at the end + # Remark: tell() in Python under Windows must be used with files opened as 'rb' + # as Unix-style endings may cause tell() to return illegal values + startPos = fid.tell() + # and get linenum and position + linenum = parentFilePosMap[fileinname].linenum + fid.seek(parentFilePosMap[fileinname].filePos) + except IOError as err: + FreeCAD.Console.PrintMessage("OS error: " + format(err) + "\n") + fid.closed + return False + else: + try: + # open the sub file id + fid = open(fileinname, 'rb') + linenum = 1 + fid.closed + except OSError as err: + FreeCAD.Console.PrintMessage("OS error: " + format(err) + "\n") + return False + + # build the file map (for single input file) + ret = create_file_map(fileinname, fid, filePosMap) + if ret <> True: + return ret + + panelVertexes = [] + chargeDensity = [] + panelColors = [] + + for line in fid: + # if subfile definitions (starting or ending), stop here + if line[0] in ('E', 'e', 'F', 'f'): + break + # now check for actual statements + # + # first split the line into the components + splitLine = line.split() + # if the line was actually composed only by separators, continue + if len(splitLine) == 0: + continue + # if conductor file + if splitLine[0] == 'C': + try: + # read file name + name = splitline[1] + + # read outer permittivity + + if 'j' in splitline[2]: + # as the complex format in FasterCap is 'a-jb' and not 'a-bj' as the Python 'complex' class + # would like, this trick modifies the string to be parsable by 'complex' + # Remark: we assume that the complex number has no spaces; FasterCap would accept + # also syntax like 'a - jb', here it would cause errors + localOutPerm = complex(splitline[2].replace('j', '') + 'j') + else: + localOutPerm = complex(splitline[2]) + + # read offset coordinates + localOffset = Vector(float(splitLine[3]), float(splitLine[4]), float(splitLine[5])) + localOffset = localOffset + offset + + # compute group name (to distinguish between panels with the same + # conductor name because in the same file called more than once) + if m_iParseLevel == 0: + localGroupname = "g" + else + localGroupname = groupname + localGroupname = localGroupname + str(m_iGroupNum[m_iParseLevel]) + '_' + + # read optional values + if len(splitLine) >= 7: + # read optional '+'. If not a '+', increment the group + if splitLine[6] <> '+': + # increase group name + m_iGroupNum[m_iParseLevel] = m_iGroupNum[m_iParseLevel] + 1 + + # read optional color; if present, it is the last element of the line + if splitLine[-1][0:2] in ("0x", "0X"): + groupcolor = splitLine[5] + + # recurse into new conductor file + m_iGroupNum[m_iParseLevel+1] = 1 + + except (IndexError, ValueError): + FreeCAD.Console.PrintMessage("Error in file " + fileinname + " at line " + format(linenum) + " : " + line + "\n") + + ret = Parse3DInputFile(name, fid, filePosMap, False, localOffset, localOutperm, localGroupname) + + if ret == False: + break + + # if dielectric file + if splitLine[0] == 'D': + try: + # read file name + name = splitline[1] + + # read outer permittivity + + if 'j' in splitline[2]: + # as the complex format in FasterCap is 'a-jb' and not 'a-bj' as the Python 'complex' class + # would like, this trick modifies the string to be parsable by 'complex' + # Remark: we assume that the complex number has no spaces; FasterCap would accept + # also syntax like 'a - jb', here it would cause errors + localOutPerm = complex(splitline[2].replace('j', '') + 'j') + else: + localOutPerm = complex(splitline[2]) + + # read inner permittivity + + if 'j' in splitline[3]: + # as the complex format in FasterCap is 'a-jb' and not 'a-bj' as the Python 'complex' class + # would like, this trick modifies the string to be parsable by 'complex' + # Remark: we assume that the complex number has no spaces; FasterCap would accept + # also syntax like 'a - jb', here it would cause errors + localOutPerm = complex(splitline[3].replace('j', '') + 'j') + else: + localOutPerm = complex(splitline[3]) + + # read offset coordinates + localOffset = Vector(float(splitLine[4]), float(splitLine[5]), float(splitLine[6])) + localOffset = localOffset + offset + + # read dielectric reference point coordinates + localDielrefpoint = Vector(float(splitLine[7]), float(splitLine[8]), float(splitLine[9])) + localDielrefpoint = localDielrefpoint + offset + + # read optional values + if len(splitLine) >= 11: + # read optional '-' + # if '-', reverse outperm and inperm; + # in this way, the reference point is always on the outperm side + if splitLine[10] == '-': + localInperm, localOutperm = localOutperm, localInperm + + # read optional color; if present, it is the last element of the line + if splitLine[-1][0:2] in ("0x", "0X"): + groupcolor = splitLine[5] + + # compute dielectric name (to distinguish between panels + # in the same file called more than once) + localGroupname = "diel" + str(m_lGroupDielNum) + sprintf(localGroupname, "diel%ld", m_lGroupDielNum) + # increase group name + m_lGroupDielNum = m_lGroupDielNum + 1 + + except (IndexError, ValueError): + FreeCAD.Console.PrintMessage("Error in file " + fileinname + " at line " + format(linenum) + " : " + line + "\n") + + # recurse into new dielectric file + ret = Parse3DInputFile(name, fid, filePosMap, True, localOffset, localOutperm, localGroupname, localInperm, localDielrefpoint) + + if ret == False: + break + + # if triangle + if splitLine[0] == 'T': + try: + # read conductor name to which the patch belongs + tmpname = splitline[1] + + # read panel coordinates + # + + # if using mesh, we need a flat list of vertexes, that will be used in triplets + # to build the triangular-only mesh faces + if m_sUsePartType == 'mesh': + panelVertexes.extend( [ [float(splitLine[2]), float(splitLine[3]), float(splitLine[4])], + [float(splitLine[5]), float(splitLine[6]), float(splitLine[7])], + [float(splitLine[8]), float(splitLine[9]), float(splitLine[10])] ]) + # if using faces, we need FreeCAD.Vector or tuple of three floats for each vertex, in a vector + # with as many elements as the vertexes of the polygon supporting the face + else: + panelVertexes.append( [ (float(splitLine[2]), float(splitLine[3]), float(splitLine[4])), + (float(splitLine[5]), float(splitLine[6]), float(splitLine[7])), + (float(splitLine[8]), float(splitLine[9]), float(splitLine[10])) ]) + + # read optional reference point + if len(splitLine) >= 14: + localDielrefpoint = Vector(float(splitLine[11]), float(splitLine[12]), float(splitLine[13])) + localDielrefpoint = localDielrefpoint + offset + uselocaldiel = True + else + uselocaldiel = False + + # read optional trailing charge density information, or color + # Note that charge density is alternative to color (cannot have both), but charge density could + # be confused with the last coordinate of the optional reference point. So if there are three + # additional optional float values, this is the reference point, and if there is something else still, + # this must be charge density or color; if there are not three additional optional float values, + # but there is something else, again this must be charge density or color, so look at the last value + # on the line + if (uselocaldiel == True and len(splitLine) >= 15) or (uselocaldiel == false and len(splitline) >= 12): + # if color, read it + if splitLine[-1][0:2] in ("0x", "0X"): + panelColors.append(splitLine[-1]) + else + chargeDensity.append(float(splitLine[11])) + + except (IndexError, ValueError): + FreeCAD.Console.PrintMessage("Error on line " + format(i) + " : " + line + "\n") + + name = groupname + # if this is a conductor panel, compose the actual name; otherwise, for dielectric interfaces, + # we can ignore specific conductor names + if isdiel == False: + # concat name with group name + name = name + tmpname + + ret = GetConductor(&(itc), &dielIndex, name, isdiel, outpermRe, outpermIm, inpermRe, inpermIm, dielrefpoint) + if(ret == False) + break + # ret = (long)CreatePanel(vertex, tmpname, dielIndex, &itc, fileinname, linenum, AUTOREFINE_SIMPLE_CREATE_PANEL, globalVars, uselocaldiel, localDielrefpoint); + + + + # counting panels (i.e. the panel # in the input file, no input refinement, + # e.g. Q panels split in two triangles) + if isdiel == False: + m_ulInputPanelNum = m_ulInputPanelNum + 1 + else: + m_ulInputPanelNum = m_ulInputPanelNum + 1 + + + + + + return True + +def create_file_map(fileinname, fid, filePosMap) + return False + def import_fastercap(filename, folder=DEF_FOLDER, use_mesh=True): '''Import file in FasterCap format as Mesh or Part.compound @@ -47,10 +399,6 @@ def import_fastercap(filename, folder=DEF_FOLDER, use_mesh=True): Example: fastercapObj = import_fastercap('cube.txt') ''' - - # - # this importer is a Python converted version of the FasterCap C++ import function - # if not os.path.isdir(folder): FreeCAD.Console.PrintMessage("Error: '" + folder + "' is not a valid folder\n") @@ -61,12 +409,12 @@ def import_fastercap(filename, folder=DEF_FOLDER, use_mesh=True): return try: - with open(folder + os.sep + filename, 'r') as fid: + with open(folder + os.sep + filename, 'rb') as fid: # reset the list of triangle vertexes panelVertexes = [] chargeDensity = [] # and scan all the file - for i, line in enumerate(fid.readlines()): + for i, line in enumerate(fid): # if first line, or empty line, skip if i == 0 or line in ['', '\n', '\r\n']: continue diff --git a/launch_fastercap.py b/launch_fastercap.py new file mode 100644 index 0000000..c503f4d --- /dev/null +++ b/launch_fastercap.py @@ -0,0 +1,42 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +import subprocess +from time import sleep +import FreeCAD, FreeCADGui + +simfile = "C:/Users/Public/Documents/FastFieldSolvers/FasterCap/3D/array_of_5_spheres.lst" +simengine = 'C:/Program Files (x86)/FastFieldSolvers/FasterCap/fastercap.exe' + +p=subprocess.Popen([simengine, "-b", "-a0.001", "-ap", simfile],stdin=subprocess.PIPE,stdout=subprocess.PIPE) + +while True: + myline = p.stdout.readline() + if myline: + App.Console.PrintMessage(myline) + if not myline: + break + +lastout = p.communicate() +App.Console.PrintMessage(lastout) + diff --git a/launch_fasthenry.py b/launch_fasthenry.py new file mode 100644 index 0000000..e98fe01 --- /dev/null +++ b/launch_fasthenry.py @@ -0,0 +1,43 @@ +#*************************************************************************** +#* * +#* Copyright (c) 2018 * +#* FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com * +#* * +#* This program is free software; you can redistribute it and/or modify * +#* it under the terms of the GNU Lesser General Public License (LGPL) * +#* as published by the Free Software Foundation; either version 2 of * +#* the License, or (at your option) any later version. * +#* for detail see the LICENCE text file. * +#* * +#* 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 Library General Public License for more details. * +#* * +#* You should have received a copy of the GNU Library General Public * +#* License along with this program; if not, write to the Free Software * +#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +#* USA * +#* * +#*************************************************************************** + +import subprocess +from time import sleep +import FreeCAD, FreeCADGui + +simfile = "C:/Users/Public/Documents/FastFieldSolvers/FastHenry2/pin-con7.inp" +simengine = 'C:/Program Files (x86)/FastFieldSolvers/FastHenry2/FastHenry2.exe' + +p=subprocess.Popen([simengine, "-b", "-a0.001", "-ap", simfile],stdin=subprocess.PIPE,stdout=subprocess.PIPE) + +while True: + myline = p.stdout.readline() + if myline: + App.Console.PrintMessage(myline) + if not myline: + break + +# get what is left in the buffer +lastout = p.communicate() +App.Console.PrintMessage(lastout) +