diff --git a/EM.py b/EM.py index 5a2035a..5bed8e6 100644 --- a/EM.py +++ b/EM.py @@ -38,12 +38,22 @@ __url__ = "http://www.fastfieldsolvers.com" '''The E.M. module provides tools for ElectroMagnetic analysis''' +import sys + +# Python3 compatibility +if sys.version_info >= (3, 4): + from importlib import reload +elif sys.version_info >= (3, 0): + from imp import reload + import FreeCAD if FreeCAD.GuiUp: import FreeCADGui FreeCADGui.updateLocale() from EM_Globals import * +from EM_About import * +# FastHenry specific from EM_FHNode import * from EM_FHSegment import * from EM_FHPath import * @@ -53,4 +63,43 @@ from EM_FHPort import * from EM_FHEquiv import * from EM_FHSolver import * from EM_FHInputFile import * +## VoxHenry specific +#from EM_VHSolver import * +# for debugging +#import EM_Globals +#reload(EM_Globals) +#from EM_Globals import * +#import EM_About +#reload(EM_About) +#from EM_About import * +#import EM_FHNode +#reload(EM_FHNode) +#from EM_FHNode import * +#import EM_FHSegment +#reload(EM_FHSegment) +#from EM_FHSegment import * +#import EM_FHPath +#reload(EM_FHPath) +#from EM_FHPath import * +#import EM_FHPlaneHole +#reload(EM_FHPlaneHole) +#from EM_FHPlaneHole import * +#import EM_FHPlane +#reload(EM_FHPlane) +#from EM_FHPlane import * +#import EM_FHPort +#reload(EM_FHPort) +#from EM_FHPort import * +#import EM_FHEquiv +#reload(EM_FHEquiv) +#from EM_FHEquiv import * +#import EM_FHSolver +#reload(EM_FHSolver) +#from EM_FHSolver import * +#import EM_FHInputFile +#reload(EM_FHInputFile) +#from EM_FHInputFile import * +#import EM_VHSolver +#reload(EM_VHSolver) +#from EM_VHSolver import * diff --git a/EM_About.py b/EM_About.py new file mode 100644 index 0000000..c1036ff --- /dev/null +++ b/EM_About.py @@ -0,0 +1,82 @@ +#*************************************************************************** +#* * +#* 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 About class" +__author__ = "FastFieldSolvers S.R.L." +__url__ = "http://www.fastfieldsolvers.com" + +# imported defines +from EM_Globals import EM_VERSION + +# defines +# about information +EM_AUTHOR = 'Copyright 2019 FastFieldSolvers S.R.L. and Efficient Power Conversion Inc.\nhttp://www.fastfieldsolvers.com, http://epc-co.com\nPartially developed by FastFieldSolvers S.R.L. under contract by EPC Inc.\n' +EM_LICENSE = 'Licensed under GNU Lesser General Public License (LGPL) version 2\n' + +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' ) + + +class _CommandAbout: + ''' The EM About command definition +''' + def GetResources(self): + # no need of icon or accelerator + return {'MenuText': QT_TRANSLATE_NOOP("EM_About","About"), + 'ToolTip': QT_TRANSLATE_NOOP("EM_About","About the ElectroMagnetic Workbench")} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + msg = translate("EM","ElectroMagnetic Workbench version ") + EM_VERSION + "\n\n" + EM_AUTHOR + "\n" + EM_LICENSE + + if FreeCAD.GuiUp: + # Simple QMessageBox providing "about" informaiton + diag = QtGui.QMessageBox(QtGui.QMessageBox.Information, translate("EM_About","About ElectroMagnetic workbench"), msg) + diag.setWindowModality(QtCore.Qt.ApplicationModal) + diag.exec_() + else: + FreeCAD.Console.PrintWarning(msg) + +if FreeCAD.GuiUp: + FreeCADGui.addCommand('EM_About',_CommandAbout()) diff --git a/EM_FHEquiv.py b/EM_FHEquiv.py index d32c661..1130362 100644 --- a/EM_FHEquiv.py +++ b/EM_FHEquiv.py @@ -101,12 +101,12 @@ class _FHEquiv: ''' if obj.Node1 == None: return - elif Draft.getType(obj.Node1) <> "FHNode": + elif Draft.getType(obj.Node1) != "FHNode": FreeCAD.Console.PrintWarning(translate("EM","Node1 is not a FHNode")) return if obj.Node2 == None: return - elif Draft.getType(obj.Node2) <> "FHNode": + elif Draft.getType(obj.Node2) != "FHNode": FreeCAD.Console.PrintWarning(translate("EM","Node2 is not a FHNode")) return # and finally, if everything is ok, make and assign the shape diff --git a/EM_FHPath.py b/EM_FHPath.py index 25e992f..900c07e 100644 --- a/EM_FHPath.py +++ b/EM_FHPath.py @@ -161,7 +161,7 @@ class _FHPath: # of the vertexes, and the segments cross-section orientation will be # calculated in absolute coordinates from the Positions rotations. # This last part is different from FHSegment. - if obj.Placement <> FreeCAD.Placement(): + if obj.Placement != FreeCAD.Placement(): obj.Placement = FreeCAD.Placement() # define nodes and segments edges_raw = [] diff --git a/EM_FHPlane.py b/EM_FHPlane.py index 6f49f47..c444990 100644 --- a/EM_FHPlane.py +++ b/EM_FHPlane.py @@ -164,8 +164,8 @@ class _FHPlane: obj.addProperty("App::PropertyLength","segwid2","EM",QT_TRANSLATE_NOOP("App::Property","Width of segments along the plane width direction ('segwid2' plane parameter)")) obj.addProperty("App::PropertyLinkList","Nodes","EM",QT_TRANSLATE_NOOP("App::Property","Nodes for connections to the plane")) obj.addProperty("App::PropertyLinkList","Holes","EM",QT_TRANSLATE_NOOP("App::Property","Holes in the plane")) - obj.addProperty("App::PropertyBool","FineMesh","Component",QT_TRANSLATE_NOOP("App::Property","Specifies if this the plane fine mesh is shown (i.e. composing segments)")) - obj.addProperty("App::PropertyBool","ShowNodes","Component",QT_TRANSLATE_NOOP("App::Property","Show the internal node grid supporting the plane")) + obj.addProperty("App::PropertyBool","FineMesh","EM",QT_TRANSLATE_NOOP("App::Property","Specifies if this the plane fine mesh is shown (i.e. composing segments)")) + obj.addProperty("App::PropertyBool","ShowNodes","EM",QT_TRANSLATE_NOOP("App::Property","Show the internal node grid supporting the plane")) obj.Proxy = self self.Type = "FHPlane" self.FineMesh = False @@ -232,7 +232,7 @@ class _FHPlane: obj.segwid2 = segwid2 FreeCAD.Console.PrintWarning(translate("EM","Plane segwid2 would cause segments overlap, re-setting segwid2 to the maximum possible")) # if needed, apply the same Placement of the Base object to the FHPlane object - if obj.Placement <> obj.Base.Placement: + if obj.Placement != obj.Base.Placement: obj.Placement = obj.Base.Placement # Start node and hole repositioning in relative coordinate system of the conductive plane # @@ -241,14 +241,14 @@ class _FHPlane: # that once a node is child of a plane, it cannot be moved independently by changing # its placement for node in obj.Nodes: - if node.Placement <> obj.Placement: + if node.Placement != obj.Placement: node.Placement = obj.Placement # These holes have already been adopted by the plane, if they are in the obj.Holes list; # therefore, must just make sure they track the plane placement. Also, this assures # that once a hole is child of a plane, it cannot be moved independently by changing # its placement for hole in obj.Holes: - if hole.Placement <> obj.Placement: + if hole.Placement != obj.Placement: hole.Placement = obj.Placement # Check if the user selected a coarse or a fine mesh. if obj.FineMesh == False: diff --git a/EM_FHPort.py b/EM_FHPort.py index 0276739..af8b4c3 100644 --- a/EM_FHPort.py +++ b/EM_FHPort.py @@ -99,12 +99,12 @@ class _FHPort: ''' if obj.NodePos == None: return - elif Draft.getType(obj.NodePos) <> "FHNode": + elif Draft.getType(obj.NodePos) != "FHNode": FreeCAD.Console.PrintWarning(translate("EM","NodePos is not a FHNode")) return if obj.NodeNeg == None: return - elif Draft.getType(obj.NodeNeg) <> "FHNode": + elif Draft.getType(obj.NodeNeg) != "FHNode": FreeCAD.Console.PrintWarning(translate("EM","NodeNeg is not a FHNode")) return if obj.NodePos == obj.NodeNeg: @@ -246,7 +246,7 @@ class _CommandFHPort: 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: + if startNode != None and endNode != None: FreeCAD.ActiveDocument.openTransaction(translate("EM","Create FHPort")) FreeCADGui.addModule("EM") FreeCADGui.doCommand('obj=EM.makeFHPort(nodePos=FreeCAD.ActiveDocument.'+startNode.Name+',nodeNeg=FreeCAD.ActiveDocument.'+endNode.Name+')') diff --git a/EM_FHSegment.py b/EM_FHSegment.py index 23c73ee..1568725 100644 --- a/EM_FHSegment.py +++ b/EM_FHSegment.py @@ -137,12 +137,12 @@ class _FHSegment: #FreeCAD.Console.PrintWarning("_FHSegment execute()\n") #debug if obj.NodeStart == None: return - elif Draft.getType(obj.NodeStart) <> "FHNode": + 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": + elif Draft.getType(obj.NodeEnd) != "FHNode": FreeCAD.Console.PrintWarning(translate("EM","NodeEnd is not a FHNode")) return # the FHSegment has no Placement in itself: @@ -154,7 +154,7 @@ class _FHSegment: # So let's keep the FHSegment placement at zero, and use the FHNodes to move the segment position. # This is also necessary as we should not change the segment cross-section orientation # using the Placement, otherwise it will not be relative to the global axis system - if obj.Placement <> FreeCAD.Placement(): + if obj.Placement != FreeCAD.Placement(): obj.Placement = FreeCAD.Placement() # check if we have a 'Base' object; if so, if segment end nodes # were already defined, re-set them according to the 'Base' object; @@ -170,13 +170,13 @@ class _FHSegment: return # ok, it's valid. Let's verify if this is a Wire. if Draft.getType(obj.Base) == "Wire": - if obj.NodeStart <> None: + if obj.NodeStart != None: abs_pos = obj.NodeStart.Proxy.getAbsCoord() # 'obj.Base.Start' is an absolute position # if 'NodeStart' is not in that position, move it if (abs_pos-obj.Base.Start).Length > EMFHSEGMENT_LENTOL: obj.NodeStart.Proxy.setAbsCoord(obj.Base.Start) - if obj.NodeEnd <> None: + if obj.NodeEnd != None: abs_pos = obj.NodeEnd.Proxy.getAbsCoord() # 'obj.Base.Start' is an absolute position # if 'NodeStart' is not in that position, move it @@ -328,7 +328,7 @@ class _CommandFHSegment: 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: + 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+')') diff --git a/EM_FHSolver.py b/EM_FHSolver.py index 6255456..5685cb5 100644 --- a/EM_FHSolver.py +++ b/EM_FHSolver.py @@ -87,8 +87,8 @@ def makeFHSolver(units=None,sigma=None,nhinc=None,nwinc=None,rh=None,rw=None,fmi 'ndec' is the float value defining how many frequency points per decade will be simulated 'folder' is the folder into which the FastHenry file will be saved. - Defaults to the user's home path (e.g. in Windows "C:\Documents - and Settings\username\My Documents", in Linux "/home/username") + Defaults to the user's home path (e.g. in Windows "C:\\Documents + and Settings\\username\\My Documents", in Linux "/home/username") 'filename' is the name of the file that will be exported. Defaults to EMFHSOLVER_DEF_FILENAME 'name' is the name of the object @@ -98,7 +98,7 @@ def makeFHSolver(units=None,sigma=None,nhinc=None,nwinc=None,rh=None,rw=None,fmi 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 + #'obj' (e.g. 'Base' property) making it a _FHSolver _FHSolver(obj) # manage ViewProvider object if FreeCAD.GuiUp: diff --git a/EM_Globals.py b/EM_Globals.py index 92c58d3..18eb77d 100644 --- a/EM_Globals.py +++ b/EM_Globals.py @@ -33,6 +33,8 @@ from FreeCAD import Vector # defines # +# version information +EM_VERSION = '1.0.1' # default node color EMFHNODE_DEF_NODECOLOR = (1.0,0.0,0.0) # tolerance in degrees when verifying if vectors are parallel diff --git a/InitGui.py b/InitGui.py index e2e3557..f6fe15e 100644 --- a/InitGui.py +++ b/InitGui.py @@ -42,8 +42,11 @@ class EMWorkbench(Workbench): # import the EM module (and therefore all commands makeXXX) import EM # E.M. tools + self.emtools = ["EM_About"] self.emfhtools = ["EM_FHSolver", "EM_FHNode", "EM_FHSegment", "EM_FHPath", "EM_FHPlane", "EM_FHPlaneHole", "EM_FHPlaneAddRemoveNodeHole", "EM_FHEquiv", "EM_FHPort", "EM_FHInputFile"] + #self.emvhtools = ["EM_VHSolver"] + self.emvhtools = [] # draft tools # setup menus self.draftcmdList = ["Draft_Line","Draft_Rectangle"] @@ -59,10 +62,11 @@ class EMWorkbench(Workbench): 'Draft_Snap_Dimensions','Draft_Snap_WorkingPlane'] def QT_TRANSLATE_NOOP(scope, text): return text - self.appendToolbar(QT_TRANSLATE_NOOP("Workbench","E.M. tools"),self.emfhtools) + self.appendToolbar(QT_TRANSLATE_NOOP("Workbench","E.M. FastHenry tools"),self.emfhtools) + #self.appendToolbar(QT_TRANSLATE_NOOP("Workbench","E.M. VoxHenry tools"),self.emvhtools) self.appendToolbar(QT_TRANSLATE_NOOP("Workbench","Draft creation tools"),self.draftcmdList) self.appendToolbar(QT_TRANSLATE_NOOP("Workbench","Draft mod tools"),self.draftmodtools) - self.appendMenu(QT_TRANSLATE_NOOP("EM","&EM"),self.emfhtools) + self.appendMenu(QT_TRANSLATE_NOOP("EM","&EM"),self.emfhtools + self.emvhtools + self.emtools) self.appendMenu(QT_TRANSLATE_NOOP("EM","&Draft"),self.draftcmdList+self.draftmodtools+self.treecmdList) self.appendMenu([QT_TRANSLATE_NOOP("EM","&Draft"),QT_TRANSLATE_NOOP("arch","Snapping")],self.snapList) #FreeCADGui.addIconPath(":/icons") diff --git a/README.md b/README.md index 23bb6f4..25f978c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### FastHenry support -Copyright (c) 2018 +Copyright (c) 2019 Efficient Power Conversion Corporation, Inc. http://epc-co.com Developed by FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com under contract by EPC @@ -12,7 +12,7 @@ Developed by FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com under cont ### FasterCap and FastCap support -Copyright (c) 2018 +Copyright (c) 2019 FastFieldSolvers S.R.L. http://www.fastfieldsolvers.com ## Description @@ -25,14 +25,25 @@ 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 +## Version + +The current version of the ElectroMagnetic workbench can be shown, once installed, from the **EM** menu, selecting **About**. + +The version number is also reported in the sources, in the global variable **EM_VERSION** in the file EM_Globals.py. ## 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. +The ElectroMagnetic workbench is managed as a FreeCAD addon. It can be installed from within FreeCAD using the add-ons manager under the Tools menu, see the [FreeCAD add-on documentation](https://www.freecadweb.org/wiki/Std_AddonMgr) for more specific instructions + +### Manual installation + +This addon can also be manually installed, but this method is not recommended, as the add-ons manager provides a more user friendly experience. + +If you still wish to manually install the workbench, you can download it by clicking the **Download ZIP** button found on top of the page, or using **Git**. The addon must be un-zipped 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")`. -You must then rename the new folder 'EM'. I.e., the new folder structure must be /Mod/EM. Opening, or closing/reopening FreeCAD then reloads the workbenches, and the E.M. workbench will show up in the pull-down workbenches menu in FreeCAD. +You must then rename the new folder 'EM', i.e. the new folder structure must be /Mod/EM. Opening, or closing/reopening FreeCAD then reloads the workbenches, and the E.M. workbench will show up in the pull-down workbenches menu in FreeCAD. ## Additional information diff --git a/Resources/EM_FHSolver.svg b/Resources/EM_FHSolver.svg index 1fb9755..51d4a71 100644 --- a/Resources/EM_FHSolver.svg +++ b/Resources/EM_FHSolver.svg @@ -91,6 +91,45 @@ y1="7.794034" x2="47.247158" y2="52.521305" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.1846386,0,0,1.1846386,-9.501508,-5.4148589)" /> + + + + + inkscape:window-width="1920" + inkscape:window-height="1018" + inkscape:window-x="-8" + inkscape:window-y="-8" + inkscape:window-maximized="1" + inkscape:snap-grids="true" + showguides="false" + inkscape:snap-nodes="true" + inkscape:snap-others="true" + inkscape:snap-to-guides="false" + inkscape:snap-global="true"> + + @@ -147,15 +200,48 @@ inkscape:groupmode="layer"> S + x="1.483322" + y="58.55563" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Georgia;-inkscape-font-specification:Georgia;fill:url(#linearGradient4148);fill-opacity:1;stroke-width:2.918185;stroke-miterlimit:4;stroke-dasharray:none">S + + + FH diff --git a/export_to_FastHenry.py b/export_to_FastHenry.py index 50a081f..edbf07c 100644 --- a/export_to_FastHenry.py +++ b/export_to_FastHenry.py @@ -27,7 +27,8 @@ import FreeCAD, Mesh, Part, MeshPart, DraftGeomUtils, os from FreeCAD import Vector - +import numpy as np + if FreeCAD.GuiUp: import FreeCADGui from PySide import QtCore, QtGui @@ -562,25 +563,25 @@ def meshSolidWithSegments(obj=None,delta=1.0,deltaX=0.0,deltaY=0.0,deltaZ=0.0,st for step_y in range(0,stepsY+1): for step_x in range(0,stepsX): # if the node and the next are inside the object shape, create the segment - if nodes[step_x,step_y,step_z] <> None and nodes[step_x+1,step_y,step_z] <> None: + if nodes[step_x,step_y,step_z] != None and nodes[step_x+1,step_y,step_z] != None: segment = EM_FHSegment.makeFHSegment(nodeStart=nodes[step_x,step_y,step_z],nodeEnd=nodes[step_x+1,step_y,step_z],width=deltaX,height=deltaZ) # then along y for step_z in range(0,stepsZ+1): for step_x in range(0,stepsX+1): for step_y in range(0,stepsY): # if the node and the next are inside the object shape, create the segment - if nodes[step_x,step_y,step_z] <> None and nodes[step_x,step_y+1,step_z] <> None: + if nodes[step_x,step_y,step_z] != None and nodes[step_x,step_y+1,step_z] != None: segment = EM_FHSegment.makeFHSegment(nodeStart=nodes[step_x,step_y,step_z],nodeEnd=nodes[step_x,step_y+1,step_z],width=deltaY,height=deltaZ) # finally along z for step_x in range(0,stepsX+1): for step_y in range(0,stepsY+1): for step_z in range(0,stepsZ): # if the node and the next are inside the object shape, create the segment - if nodes[step_x,step_y,step_z] <> None and nodes[step_x,step_y,step_z+1] <> None: + if nodes[step_x,step_y,step_z] != None and nodes[step_x,step_y,step_z+1] != None: segment = EM_FHSegment.makeFHSegment(nodeStart=nodes[step_x,step_y,step_z],nodeEnd=nodes[step_x,step_y,step_z+1],width=deltaX,height=deltaY) -def meshSolidWithVoxels(obj=None,delta=1.0,stayInside=False): - ''' Voxel a solid object +def meshSolidWithVoxels(obj=None,delta=1.0): + ''' Voxelize a solid object ''' if obj == None: return @@ -612,74 +613,289 @@ def meshSolidWithVoxels(obj=None,delta=1.0,stayInside=False): pos_y = pos_y + delta pos_x = pos_x + delta return isNode - # if we must don't need to stay within the object shape boundaries, - # the voxel will overlap the shape contour -# nodes=np.full((stepsX+1,stepsY+1,stepsZ+1), None, np.object) -# if stayInside == False: -# pos_x = bbox.XMin + deltaSideX -# for step_x in range(0,stepsX+1): -# pos_y = bbox.YMin + deltaSideY -# for step_y in range(0,stepsY+1): -# pos_z = bbox.ZMin + deltaSideZ -# for step_z in range(0,stepsZ+1): -# # if the point is inside the object shape, or on the surface, flag it -# if isNode[step_x,step_y,step_z] == True: -# # create the node -# node = EM_FHNode.makeFHNode(X=pos_x, Y=pos_y, Z=pos_z) -# # store it in the array -# nodes[step_x,step_y,step_z] = node -# pos_z = pos_z + deltaZ -# pos_y = pos_y + deltaY -# pos_x = pos_x + deltaX -# # if we must stay within the object shape boundaries (within the accuracy -# # of the point sampling) -# else: -# pos_x = bbox.XMin + deltaSideX -# for step_x in range(0,stepsX): -# pos_y = bbox.YMin + deltaSideY -# for step_y in range(0,stepsY): -# pos_z = bbox.ZMin + deltaSideZ -# for step_z in range(0,stepsZ): -# # if all the eight cube corners are inside the object shape, -# # we consider the center point well inside the object shape, i.e. also -# # for a segment lying on a plane parallel to the plane xy, -# # with width=deltaX, height=deltaY we are within the object -# if (isNode[step_x,step_y,step_z] == True and isNode[step_x+1,step_y,step_z] == True and -# isNode[step_x,step_y+1,step_z] == True and isNode[step_x+1,step_y+1,step_z] == True and -# isNode[step_x,step_y,step_z+1] == True and isNode[step_x+1,step_y,step_z+1] == True and -# isNode[step_x,step_y+1,step_z+1] == True and isNode[step_x+1,step_y+1,step_z+1] == True): -# # create the node -# node = EM_FHNode.makeFHNode(X=pos_x+deltaX/2.0, Y=pos_y+deltaY/2.0, Z=pos_z+deltaZ/2.0) -# # store it in the array -# nodes[step_x,step_y,step_z] = node -# pos_z = pos_z + deltaZ -# pos_y = pos_y + deltaY -# pos_x = pos_x + deltaX -# # now create the grid of segments -# # first along x -# for step_z in range(0,stepsZ+1): -# for step_y in range(0,stepsY+1): -# for step_x in range(0,stepsX): -# # if the node and the next are inside the object shape, create the segment -# if nodes[step_x,step_y,step_z] <> None and nodes[step_x+1,step_y,step_z] <> None: -# segment = EM_FHSegment.makeFHSegment(nodeStart=nodes[step_x,step_y,step_z],nodeEnd=nodes[step_x+1,step_y,step_z],width=deltaX,height=deltaZ) -# # then along y -# for step_z in range(0,stepsZ+1): -# for step_x in range(0,stepsX+1): -# for step_y in range(0,stepsY): -# # if the node and the next are inside the object shape, create the segment -# if nodes[step_x,step_y,step_z] <> None and nodes[step_x,step_y+1,step_z] <> None: -# segment = EM_FHSegment.makeFHSegment(nodeStart=nodes[step_x,step_y,step_z],nodeEnd=nodes[step_x,step_y+1,step_z],width=deltaY,height=deltaZ) -# # finally along z -# for step_x in range(0,stepsX+1): -# for step_y in range(0,stepsY+1): -# for step_z in range(0,stepsZ): -# # if the node and the next are inside the object shape, create the segment -# if nodes[step_x,step_y,step_z] <> None and nodes[step_x,step_y,step_z+1] <> None: -# segment = EM_FHSegment.makeFHSegment(nodeStart=nodes[step_x,step_y,step_z],nodeEnd=nodes[step_x,step_y,step_z+1],width=deltaX,height=deltaY) -# +def getContainingBBox(objs): + ''' Get the bounding box containing all the listed objects + + 'objs' is the list of FreeCAD objects + + Returns the global bounding box. + If the list is None, or is not a list, or if the object have no Shape, + the returned BoundBox is None +''' + # create an empty bbox + gbbox = None + isfirst = True + # if 'objs' is not None + if objs: + if isinstance(objs,list): + for obj in objs: + if hasattr(obj,"Shape"): + if isfirst: + gbbox = obj.Shape.BoundBox + isfirst = False + else: + gbbox.add(obj.Shape.BoundBox) + return gbbox +def createVoxelSpace(bbox,delta): + ''' Creates the voxel tensor (3D array) in the given bounding box + + 'bbox' is the overall FreeCAD.BoundBox bounding box + 'delta' is the voxels size length + + Returns a voxel tensor as a Numpy 3D array. + If gbbox is None, returns None +''' + if bbox == None: + return None + if delta == None: + return None + # add 1.0 to always cover the bbox space with the voxels + stepsX = int(bbox.XLength/delta + 1.0) + stepsY = int(bbox.YLength/delta + 1.0) + stepsZ = int(bbox.ZLength/delta + 1.0) + # debug + print("X="+str(stepsX)+" Y="+str(stepsY)+" Z="+str(stepsZ)+" tot="+str(stepsX*stepsY*stepsZ)) + # create the 3D array of nodes as 16-bit integers (max 65k different conductivities) + voxelSpace=np.full((stepsX+1,stepsY+1,stepsZ+1), 0, np.int16) + return voxelSpace + +def voxelizeConductor(obj,condIndex,gbbox,delta,voxelSpace): + ''' Voxelize a solid object. The function will modify the 'voxelSpace' + by marking with 'condIndex' all the voxels that sample the object + 'obj' internal. + + 'obj' is the object to voxelize + 'condIndex' (integer) is the index of the object. It defines the object conductivity. + 'gbbox' (FreeCAD.BoundBox) is the overall bounding box + 'delta' is the voxels size length + 'voxelSpace' (Numpy 3D array) is the voxel tensor of the overall space +''' + if obj == None: + return + if not hasattr(obj,"Shape"): + return + # get this object bbox + bbox = obj.Shape.BoundBox + # now must find the voxel set that contains the object bounding box + # find the voxel that contains the bbox min point + min_x = int((bbox.XMin - gbbox.XMin)/delta) + min_y = int((bbox.YMin - gbbox.YMin)/delta) + min_z = int((bbox.ZMin - gbbox.ZMin)/delta) + # find the voxel that contains the bbox max point + max_x = int((bbox.XMax - gbbox.XMin)/delta) + max_y = int((bbox.YMax - gbbox.YMin)/delta) + max_z = int((bbox.ZMax - gbbox.ZMin)/delta) + # and now iterate to find which voxel is inside the object 'obj', + # sampling based on the voxel centers + pos_x = gbbox.XMin + min_x * delta + delta/2.0 + for step_x in range(min_x,max_x+1): + pos_y = gbbox.YMin + min_y * delta + delta/2.0 + for step_y in range(min_y,max_y+1): + pos_z = gbbox.ZMin + min_z * delta + delta/2.0 + for step_z in range(min_z,max_z+1): + # if the point is inside the object shape, or on the surface, flag it + if obj.Shape.isInside(Vector(pos_x,pos_y,pos_z),0.0,True): + # debug + #print("pos_x="+str(pos_x)+" pos_y="+str(pos_y)+" pos_z="+str(pos_z)) + voxelSpace[step_x,step_y,step_z] = condIndex + pos_z = pos_z + delta + pos_y = pos_y + delta + pos_x = pos_x + delta + +def createVoxelShell(obj,condIndex,gbbox,delta,voxelSpace=None): + ''' Creates a shell composed by the external faces of a voxelized object. + + 'obj' is the object whose shell must be created + 'condIndex' (integer) is the index of the object. It defines the object conductivity. + 'gbbox' (FreeCAD.BoundBox) is the overall bounding box + 'delta' is the voxels size length + 'voxelSpace' (Numpy 3D array) is the voxel tensor of the overall space +''' + if voxelSpace == None: + return + if not hasattr(obj,"Shape"): + return + surfList = [] + # get the object's bbox + bbox = obj.Shape.BoundBox + # now must find the voxel set that contains the object bounding box + # find the voxel that contains the bbox min point + min_x = int((bbox.XMin - gbbox.XMin)/delta) + min_y = int((bbox.YMin - gbbox.YMin)/delta) + min_z = int((bbox.ZMin - gbbox.ZMin)/delta) + # find the voxel that contains the bbox max point + max_x = int((bbox.XMax - gbbox.XMin)/delta) + max_y = int((bbox.YMax - gbbox.YMin)/delta) + max_z = int((bbox.ZMax - gbbox.ZMin)/delta) + # this is half the side of the voxel + halfdelta = delta/2.0 + # array to find the six neighbour + sides = [(1,0,0), (-1,0,0), (0,1,0), (0,-1,0), (0,0,1), (0,0,-1)] + # vertexes of the six faces + vertexes = [[Vector(delta,0,0), Vector(delta,delta,0), Vector(delta,delta,delta), Vector(delta,0,delta)], + [Vector(0,0,0), Vector(0,0,delta), Vector(0,delta,delta), Vector(0,delta,0)], + [Vector(0,delta,0), Vector(0,delta,delta), Vector(delta,delta,delta), Vector(delta,delta,0)], + [Vector(0,0,0), Vector(delta,0,0), Vector(delta,0,delta), Vector(0,0,delta)], + [Vector(0,0,delta), Vector(delta,0,delta), Vector(delta,delta,delta), Vector(0,delta,delta)], + [Vector(0,0,0), Vector(0,delta,0), Vector(delta,delta,0), Vector(delta,0,0)]] + # and now iterate to find which voxel is inside the object 'obj', + # sampling based on the voxel centers + vbase = Vector(gbbox.XMin + min_x * delta, gbbox.YMin + min_y * delta, gbbox.ZMin + min_z * delta) + for step_x in range(min_x,max_x+1): + vbase.y = gbbox.YMin + min_y * delta + for step_y in range(min_y,max_y+1): + vbase.z = gbbox.ZMin + min_z * delta + for step_z in range(min_z,max_z+1): + # check if voxel is belonging to the given object + if voxelSpace[step_x,step_y,step_z] == condIndex: + # scan the six neighbour voxels, to see if they are belonging to the same conductor or not. + # If they are not belonging to the same conductor, or if the voxel space is finished, the current voxel + # side in the direction of the empty voxel is an external surface + for side, vertex in zip(sides,vertexes): + is_surface = False + nextVoxelIndexes = [step_x+side[0],step_y+side[1],step_z+side[2]] + if (nextVoxelIndexes[0] > max_x or nextVoxelIndexes[0] < 0 or + nextVoxelIndexes[1] > max_y or nextVoxelIndexes[1] < 0 or + nextVoxelIndexes[2] > max_z or nextVoxelIndexes[2] < 0): + is_surface = True + else: + if voxelSpace[nextVoxelIndexes[0],nextVoxelIndexes[1],nextVoxelIndexes[2]] != condIndex: + is_surface = True + if is_surface == True: + # debug + #print("pos_x="+str(vbase.x)+" pos_y="+str(vbase.y)+" pos_z="+str(vbase.z)) + # create the face + # calculate the vertexes + v11 = vbase + vertex[0] + v12 = vbase + vertex[1] + v13 = vbase + vertex[2] + v14 = vbase + vertex[3] + # now make the face + poly = Part.makePolygon( [v11,v12,v13,v14,v11]) + face = Part.Face(poly) + surfList.append(face) + vbase.z += delta + vbase.y += delta + vbase.x += delta + # create a shell. Does not need to be solid. + objShell = Part.makeShell(surfList) + return objShell + +def findContactVoxelSurfaces(face,condIndex,gbbox,delta,voxelSpace=None,createShell=False): + ''' Find the voxel surface sides corresponding to the given contact surface + (face) of an object. The object must have already been voxelized. + + 'face' is the object face + 'condIndex' (integer) is the index of the object to which the face belongs. + It defines the object conductivity. + 'gbbox' (FreeCAD.BoundBox) is the overall bounding box + 'delta' is the voxels size length + 'voxelSpace' (Numpy 3D array) is the voxel tensor of the overall space + 'createShell' (bool) creates a shell out of the contact faces + + Returns a list of surfaces in the format [x,y,z,voxside] where + x, y, z are the voxel position indexes, while voxside is '+x', '-x', + '+y', '-y', '+z', '-z' according the the impacted surface of the voxel +''' + if voxelSpace == None: + return + surfList = [] + contactList = [] + # get the face's bbox + bbox = face.BoundBox + # now must find the voxel set that contains the face bounding box + # with a certain slack - it could be the next voxel, + # if the surface is at the boundary between voxels. + # Find the voxel that contains the bbox min point + min_x = int((bbox.XMin - gbbox.XMin)/delta)-1 + min_y = int((bbox.YMin - gbbox.YMin)/delta)-1 + min_z = int((bbox.ZMin - gbbox.ZMin)/delta)-1 + # find the voxel that contains the bbox max point + max_x = int((bbox.XMax - gbbox.XMin)/delta)+1 + max_y = int((bbox.YMax - gbbox.YMin)/delta)+1 + max_z = int((bbox.ZMax - gbbox.ZMin)/delta)+1 + # debug + #print(str(min_x)+" "+str(min_y)+" "+str(min_z)+" "+str(max_x)+" "+str(max_y)+" "+str(max_z)) + # create a Part.Vertex that we can use to test the distance + # to the face (as it is a TopoShape) + vec = FreeCAD.Vector(0,0,0) + testVertex = Part.Vertex(vec) + # this is half the side of the voxel + halfdelta = delta/2.0 + # small displacement w.r.t. delta + epsdelta = delta/100.0 + # array to find the six neighbour + sides = [(1,0,0), (-1,0,0), (0,1,0), (0,-1,0), (0,0,1), (0,0,-1)] + # string describing the side + sideStrs = ['+x', '-x', '+y', '-y', '+z', '-z'] + # centers of the sides, with respect to the lower corner (with the smallest coordinates) + sideCenters = [Vector(delta,halfdelta,halfdelta), Vector(0.0,halfdelta,halfdelta), + Vector(halfdelta,delta,halfdelta), Vector(halfdelta,0.0,halfdelta), + Vector(halfdelta,halfdelta,delta), Vector(halfdelta,halfdelta,0.0)] + # vertexes of the six faces (with a slight offset) + vertexes = [[Vector(delta+epsdelta,0,0), Vector(delta+epsdelta,delta,0), Vector(delta+epsdelta,delta,delta), Vector(delta+epsdelta,0,delta)], + [Vector(-epsdelta,0,0), Vector(-epsdelta,0,delta), Vector(-epsdelta,delta,delta), Vector(-epsdelta,delta,0)], + [Vector(0,delta+epsdelta,0), Vector(0,delta+epsdelta,delta), Vector(delta,delta+epsdelta,delta), Vector(delta,delta+epsdelta,0)], + [Vector(0,-epsdelta,0), Vector(delta,-epsdelta,0), Vector(delta,-epsdelta,delta), Vector(0,-epsdelta,delta)], + [Vector(0,0,delta+epsdelta), Vector(delta,0,delta+epsdelta), Vector(delta,delta,delta+epsdelta), Vector(0,delta,delta+epsdelta)], + [Vector(0,0,-epsdelta), Vector(0,delta,-epsdelta), Vector(delta,delta,-epsdelta), Vector(delta,0,-epsdelta)]] + + # and now iterate to find which voxel is inside the bounding box of the 'face', + vbase = Vector(gbbox.XMin + min_x * delta, gbbox.YMin + min_y * delta, gbbox.ZMin + min_z * delta) + for step_x in range(min_x,max_x+1): + vbase.y = gbbox.YMin + min_y * delta + for step_y in range(min_y,max_y+1): + vbase.z = gbbox.ZMin + min_z * delta + for step_z in range(min_z,max_z+1): + # check if voxel is belonging to the given object + if voxelSpace[step_x,step_y,step_z] == condIndex: + # scan the six neighbour voxels, to see if they are belonging to the same conductor or not. + # If they are not belonging to the same conductor, or if the voxel space is finished, the current voxel + # side in the direction of the empty voxel is an external surface + for side, sideStr, sideCenter, vertex in zip(sides,sideStrs,sideCenters,vertexes): + is_surface = False + nextVoxelIndexes = [step_x+side[0],step_y+side[1],step_z+side[2]] + if (nextVoxelIndexes[0] > max_x or nextVoxelIndexes[0] < 0 or + nextVoxelIndexes[1] > max_y or nextVoxelIndexes[1] < 0 or + nextVoxelIndexes[2] > max_z or nextVoxelIndexes[2] < 0): + is_surface = True + else: + if voxelSpace[nextVoxelIndexes[0],nextVoxelIndexes[1],nextVoxelIndexes[2]] != condIndex: + is_surface = True + if is_surface == True: + # debug + #print("pos_x="+str(vbase.x)+" pos_y="+str(vbase.y)+" pos_z="+str(vbase.z)) + testVertex.Placement.Base = vbase + sideCenter + # if the point is close enough to the face, we consider + # the voxel surface as belonging to the voxelized face + dist = testVertex.distToShape(face) + # debug + #print(str(dist)) + if abs(dist[0]) < halfdelta: + contactList.append([step_x,step_y,step_z,sideStr]) + if createShell: + # create the face + # calculate the vertexes + v11 = vbase + vertex[0] + v12 = vbase + vertex[1] + v13 = vbase + vertex[2] + v14 = vbase + vertex[3] + # now make the face + poly = Part.makePolygon( [v11,v12,v13,v14,v11]) + contFace = Part.Face(poly) + surfList.append(contFace) + vbase.z += delta + vbase.y += delta + vbase.x += delta + contactShell = None + if createShell: + if surfList != []: + # create a shell. Does not need to be solid. + contactShell = Part.makeShell(surfList) + return [contactList,contactShell] + #bb = App.BoundBox(); # #objects = App.ActiveDocument.findObjects("Part::Feature") diff --git a/import_fastercap.py b/import_fastercap.py index 3cb283e..10a6d15 100644 --- a/import_fastercap.py +++ b/import_fastercap.py @@ -180,7 +180,7 @@ def parse_3D_input_file(fileinname, parentFid, parentFilePosMap, isdiel = False, # build the file map (for single input file) ret = create_file_map(fileinname, fid, filePosMap) - if ret <> True: + if ret != True: return ret panelVertexes = [] @@ -223,14 +223,14 @@ def parse_3D_input_file(fileinname, parentFid, parentFilePosMap, isdiel = False, # conductor name because in the same file called more than once) if m_iParseLevel == 0: localGroupname = "g" - else + 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] <> '+': + if splitLine[6] != '+': # increase group name m_iGroupNum[m_iParseLevel] = m_iGroupNum[m_iParseLevel] + 1