-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathEM_FHPath.py
473 lines (444 loc) · 24.5 KB
/
EM_FHPath.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
#***************************************************************************
#* *
#* Copyright (c) 2018 *
#* Efficient Power Conversion Corporation, Inc. http://epc-co.com *
#* *
#* Developed by FastFieldSolvers S.R.L. under contract by EPC *
#* 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 Path Class"
__author__ = "FastFieldSolvers S.R.L."
__url__ = "http://www.fastfieldsolvers.com"
# defines
#
EMFHPATH_DEF_SEGWIDTH = 0.2
EMFHPATH_DEF_SEGHEIGHT = 0.2
# default max number of segments into which a curve is discretized
EMFHPATH_DEF_DISCR = 3
# the coefficient to apply to the segment width (height) to get
# the minimum radius of curvature allowed
EMFHPATH_TIMESWIDTH = 3
import FreeCAD, FreeCADGui, Mesh, Part, MeshPart, Draft, DraftGeomUtils, os
import DraftVecUtils
from FreeCAD import Vector
import EM
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):
return txt
def QT_TRANSLATE_NOOP(ctxt,txt):
return txt
# \endcond
__dir__ = os.path.dirname(__file__)
iconPath = os.path.join( __dir__, 'Resources' )
def makeFHPath(baseobj=None,name='FHPath'):
''' Creates a FastHenry Path (a set connected 'E' FastHenry statements)
'baseobj' is the object on which the path is based.
If no 'baseobj' is given, the user must assign a base
object later on, to be able to use this object.
The 'baseobj' is mandatory, and can be any shape containing edges,
even if the Path is designed to work best with the support of
a sketch or a wire.
'name' is the name of the object
Example:
path = makeFHPath(myWire)
'''
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 _FHPath
_FHPath(obj)
# manage ViewProvider object
if FreeCAD.GuiUp:
_ViewProviderFHPath(obj.ViewObject)
# set base ViewObject properties to user-selected values (if any)
# check if 'baseobj' is a wire (only base object allowed), and only if not passed any node
if baseobj:
# if right type of base
if not baseobj.isDerivedFrom("Part::Feature"):
FreeCAD.Console.PrintWarning(translate("EM","FHPath can only be based on objects derived from Part::Feature"))
return
# check validity
if baseobj.Shape.isNull():
FreeCAD.Console.PrintWarning(translate("EM","FHPath base object shape is null"))
return
if not baseobj.Shape.isValid():
FreeCAD.Console.PrintWarning(translate("EM","FHPath base object shape is invalid"))
return
obj.Base = baseobj
# hide the base object
if obj.Base and FreeCAD.GuiUp:
obj.Base.ViewObject.hide()
# return the newly created Python object
return obj
class _FHPath:
'''The EM FastHenry Path 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::PropertyLinkList","Nodes","EM",QT_TRANSLATE_NOOP("App::Property","The list of FHNodes along the path (read only)"),1)
obj.addProperty("App::PropertyLength","Width","EM",QT_TRANSLATE_NOOP("App::Property","Path width ('w' segment parameter)"))
obj.addProperty("App::PropertyLength","Height","EM",QT_TRANSLATE_NOOP("App::Property","Path height ('h' segment parameter)"))
obj.addProperty("App::PropertyInteger","Discr","EM",QT_TRANSLATE_NOOP("App::Property","Max number of segments into which curves will be discretized"))
obj.addProperty("App::PropertyFloat","Sigma","EM",QT_TRANSLATE_NOOP("App::Property","Path conductivity ('sigma' segment parameter)"))
obj.addProperty("App::PropertyVector","ww","EM",QT_TRANSLATE_NOOP("App::Property","Path cross-section direction along width at the start of the path ('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 = "FHPath"
obj.Discr = EMFHPATH_DEF_DISCR
# 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()
'''
#FreeCAD.Console.PrintWarning("_FHPath execute()\n") #debug
# the Path needs a 'Base' object
if not obj.Base:
return
# if right type of base
if not obj.Base.isDerivedFrom("Part::Feature"):
FreeCAD.Console.PrintWarning(translate("EM","FHPath can only be based on objects derived from Part::Feature"))
return
# check validity
if obj.Base.Shape.isNull():
FreeCAD.Console.PrintWarning(translate("EM","FHPath base object shape is null"))
return
if not obj.Base.Shape.isValid():
FreeCAD.Console.PrintWarning(translate("EM","FHPath base object shape is invalid"))
return
if obj.Width == None or obj.Width <= 0:
obj.Width = EMFHPATH_DEF_SEGWIDTH
if obj.Height == None or obj.Height <= 0:
obj.Height = EMFHPATH_DEF_SEGHEIGHT
# the FHPath has no Placement in itself; nodes positions will be in absolute
# coordinates, as this is what FastHenry understands.
# The FHSPath Placement is kept at zero, and the 'Base'
# object Position will be used to find the absolute coordinates
# 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():
obj.Placement = FreeCAD.Placement()
# define nodes and segments
edges_raw = []
# checking TypeId; cannot check type(obj), too generic
if obj.Base.TypeId == "Sketcher::SketchObject":
if obj.Base.Shape.ShapeType == "Wire":
edges_raw.extend(obj.Base.Shape.Edges)
# compound
elif obj.Base.TypeId == "Part::Compound":
edges_raw.extend(obj.Base.Shape.Edges)
# line or DWire (Draft Wire)
elif obj.Base.TypeId == "Part::Part2DObjectPython":
if obj.Base.Shape.ShapeType == "Wire" or obj.Base.Shape.ShapeType == "Edge":
edges_raw.extend(obj.Base.Shape.Edges)
# wire created by upgrading a set of (connected) edges
elif obj.Base.TypeId == "Part::Feature":
if obj.Base.Shape.ShapeType == "Wire":
edges_raw.extend(obj.Base.Shape.Edges)
# any other part, provided it has a 'Shape' attribute
else:
if hasattr(obj.Base, "Shape"):
edges_raw.extend(obj.Base.Shape.Edges)
else:
FreeCAD.Console.PrintWarning(translate("EM","Unsupported base object type for FHPath"))
return
# sort the edges. Remark: the edge list might be disconnected (e.g. can happen with a compound
# containing different edges / wires / sketches). We will join the dangling endpoints with segments later on
edges = Part.__sortEdges__(edges_raw)
if edges == []:
return
# get the max between the 'obj.Width' and the 'obj.Height'
if obj.Width > obj.Height:
geodim = obj.Width
else:
geodim = obj.Height
# scan edges and derive node positions
self.nodeCoords = []
# initialize 'lastvertex' to the position of the first vertex,
# (as if we had a previous segment)
lastvertex = edges[0].valueAt(edges[0].FirstParameter)
self.nodeCoords.append(lastvertex)
for edge in edges:
# might also rely on "edge.Curve.discretize(Deflection=geodim)"
# where Deflection is the max distance between any point on the curve,
# and the polygon approximating the curve
if type(edge.Curve) == Part.Circle:
# discretize only if required by the user, and if the curvature radius is not too small
# vs. the max between the 'obj.Width' and the 'obj.Height'
if obj.Discr <= 1 or edge.Curve.Radius < geodim*EMFHPATH_TIMESWIDTH:
ddisc = 1
else:
ddisc = obj.Discr
elif type(edge.Curve) == Part.Ellipse:
# discretize
if obj.Discr <= 1 or edge.Curve.MajorRadius < geodim*EMFHPATH_TIMESWIDTH or edge.Curve.MinorRadius < geodim*EMFHPATH_TIMESWIDTH:
ddisc = 1
else:
ddisc = obj.Discr
elif type(edge.Curve) == Part.Line:
# if Part.Line, do not discretize
ddisc = 1
else:
# if any other type of curve, discretize, no matter what.
# It will be up to the user to decide if the discretization is ok.
if obj.Discr <= 1:
ddisc = 1
else:
ddisc = obj.Discr
# check if the edge is not too short (could happen e.g. for Part.Line)
# Note that we calculate the length from 'lastvertex', as we may have skipped also
# some previous edges, if too short in their turn
if edge.Length < geodim*EMFHPATH_TIMESWIDTH:
FreeCAD.Console.PrintWarning(translate("EM","An edge of the Base object supporting the FHPath is too short. FastHenry simulation may fail."))
step = (edge.LastParameter - edge.FirstParameter) / ddisc
# if same the last vertex of the previous edge is coincident
# with the first vertex of the next edge, skip the vertex
if (lastvertex-edge.valueAt(edge.FirstParameter)).Length < EM.EMFHSEGMENT_LENTOL:
start = 1
else:
start = 0
for i in range(start, ddisc):
# always skip last vertex, will add this at the end
self.nodeCoords.append(edge.valueAt(edge.FirstParameter + i*step))
# now add the very last vertex ('LastParameter' provides the exact position)
lastvertex = edge.valueAt(edge.LastParameter)
self.nodeCoords.append(lastvertex)
if len(self.nodeCoords) < 2:
FreeCAD.Console.PrintWarning(translate("EM","Less than two nodes found, cannot create the FHPath"))
return
# find the cross-section orientation of the first segment, according to the 'Base' object Placement.
# If 'obj.ww' is not defined, use the FastHenry default (see makeSegShape() )
self.ww = []
if obj.ww.Length < EM.EMFHSEGMENT_LENTOL:
# this is zero anyway (i.e. below 'EMFHSEGMENT_LENTOL')
self.ww = [Vector(0,0,0)]
else:
# transform 'obj.ww' according to the 'Base' Placement
# (transation is don't care, we worry about rotation)
self.ww = [obj.Base.Placement.multVec(obj.ww)]
shapes = []
# get node positions in absolute coordinates (at least two nodes exist, checked above)
n1 = EM.getAbsCoordBodyPart(obj.Base,self.nodeCoords[0])
n2 = EM.getAbsCoordBodyPart(obj.Base,self.nodeCoords[1])
vNext = n2-n1
for i in range(1, len(self.nodeCoords)):
vPrev = vNext
shape = EM.makeSegShape(n1,n2,obj.Width,obj.Height,self.ww[-1])
shapes.append(shape)
# now we must calculate the cross-section orientation
# of the next segment, i.e. update 'ww'
if i < len(self.nodeCoords)-1:
n1 = n2
n2 = EM.getAbsCoordBodyPart(obj.Base,self.nodeCoords[i+1])
vNext = n2-n1
# get angle in radians
angle = vPrev.getAngle(vNext)
# if the angle is actually greater than EMFHSEGMENT_PARTOL (i.e. the segments are not co-linear
# or almost co-linear)
if angle*FreeCAD.Units.Radian > EM.EMFHSEGMENT_PARTOL:
normal = vPrev.cross(vNext)
# rotate 'ww'
ww = DraftVecUtils.rotate(self.ww[-1],angle,normal)
else:
# otherwise, keep the previous orientation
ww = self.ww[-1]
self.ww.append(ww)
shape = Part.makeCompound(shapes)
# now create or assign FHNodes
nodes = obj.Nodes
numnodes = len(nodes)
modified = False
import EM_FHNode
# if there are less FHNodes than required, extend them
if numnodes < len(self.nodeCoords):
modified = True
for index in range(0,len(self.nodeCoords)-numnodes):
# create a new FHNode at the nodeCoords position
node = EM_FHNode.makeFHNode(X=self.nodeCoords[numnodes+index].x, Y=self.nodeCoords[numnodes+index].y, Z=self.nodeCoords[numnodes+index].z)
# insert the new node before the last (the last node always stays the same,
# to preserve FHPath attachments to other structures, if the FHPath shape changes)
nodes.insert(-1,node)
# if instead there are more FHNodes than required, must remove some of them
elif numnodes > len(self.nodeCoords):
# but do it only if there are more than two nodes left in the FHPath,
# otherwise we assume this is a temporary change of FHPath shape,
# and we preserve the end nodes (do not remove them)
if numnodes > 2:
modified = True
# scan backwards, skipping the last node (last element is 'numnodes-1',
# and range scans up to the last element before 'numnodes-len(self.nodeCoords)-1'
for index in range(numnodes-2,len(self.nodeCoords)-2,-1):
# remove the node from the 'nodes' list, but keeping the last node
node = nodes[index]
nodes.pop(index)
# check if we can safely remove the extra nodes from the Document;
# this can be done only if they do not belong to any other object.
# So if the 'InList' member contains one element only, this is
# the parent FHPath (we actually check for zero as well, even if
# this should never happen), so we can remove the FHNode
if len(node.InList) <= 1:
node.Document.removeObject(node.Name)
# and finally correct node positions
for node, nodeCoord in zip(nodes, self.nodeCoords):
# only if node position is not correct, change it
if (node.Proxy.getAbsCoord()-nodeCoord).Length > EM.EMFHSEGMENT_LENTOL:
node.Proxy.setAbsCoord(nodeCoord)
# only if we modified the list of nodes, re-assign it to the FHPath
if modified:
obj.Nodes = nodes
# shape may be None, e.g. if endpoints coincide. Do not assign in this case
if shape:
obj.Shape = shape
#FreeCAD.Console.PrintWarning("_FHPath execute() ends\n") #debug
def onChanged(self, obj, prop):
''' take action if an object property 'prop' changed
'''
#FreeCAD.Console.PrintWarning("_FHPath onChanged(" + str(prop)+")\n") #debug
if not hasattr(self,"Object"):
# on restore, self.Object is not there anymore (JSON does not serialize complex objects
# members of the class, so __getstate__() and __setstate__() skip them);
# so we must "re-attach" (re-create) the 'self.Object'
self.Object = obj
if not hasattr(self,"ww"):
# on restore, self.ww is not there anymore; must recreate through execute(),
# but first check we have all the needed attributes
if hasattr(obj,"Base"):
if hasattr(obj.Base,"Shape"):
if not obj.Base.Shape.isNull():
if obj.Base.Shape.isValid():
self.execute(obj)
#FreeCAD.Console.PrintWarning("_FHPath onChanged(" + str(prop)+") ends\n") #debug
def serialize(self,fid):
''' Serialize the object to the 'fid' file descriptor
'''
if len(self.Object.Nodes) > 1:
if len(self.Object.Nodes) == len(self.ww)+1:
for index in range(0,len(self.Object.Nodes)-1):
fid.write("E" + self.Object.Label + str(index) + " N" + self.Object.Nodes[index].Label + " N" + self.Object.Nodes[index+1].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.ww[index].Length >= EM.EMFHSEGMENT_LENTOL:
fid.write(" wx=" + str(self.ww[index].x) + " wy=" + str(self.ww[index].y) + " wz=" + str(self.ww[index].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")
else:
FreeCAD.Console.PrintError(translate("EM","Error when serializing FHPath. Number of nodes does not match number of segments + 1"))
else:
FreeCAD.Console.PrintWarning(translate("EM","Cannot serialize FHPath. Less than two nodes found."))
def __getstate__(self):
return self.Type
def __setstate__(self,state):
if state:
self.Type = state
class _ViewProviderFHPath:
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 '''
# on restore, self.Object is not there anymore (JSON does not serialize complex objects
# members of the class, so __getstate__() and __setstate__() skip them);
# so we must "re-attach" (re-create) the 'self.Object'
self.Object = obj.Object
return
def updateData(self, fp, prop):
''' If a property of the handled feature has changed we have the chance to handle this here '''
#FreeCAD.Console.PrintMessage("ViewProvider updateData(), property: " + str(prop) + "\n") # debug
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):
''' If the 'prop' property changed for the ViewProvider 'vp' '''
#FreeCAD.Console.PrintMessage("ViewProvider onChanged(), property: " + str(prop) + "\n") # debug
def claimChildren(self):
''' Used to place other objects as children in the tree'''
c = []
if hasattr(self,"Object"):
if hasattr(self.Object,"Base"):
c.append(self.Object.Base)
if hasattr(self.Object,"Nodes"):
c.extend(self.Object.Nodes)
return c
def getIcon(self):
''' Return the icon 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, 'EM_FHPath.svg')
def __getstate__(self):
return None
def __setstate__(self,state):
return None
class _CommandFHPath:
''' The EM FastHenry Path (FHPath) command definition
'''
def GetResources(self):
return {'Pixmap' : os.path.join(iconPath, 'EM_FHPath.svg') ,
'MenuText': QT_TRANSLATE_NOOP("EM_FHPath","FHPath"),
'Accel': "E, T",
'ToolTip': QT_TRANSLATE_NOOP("EM_FHPath","Creates a Path object (set of connected FastHenry segments) from a selected base object (sketch, wire or any shape containing edges)")}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
def Activated(self):
# 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()
# if selection is not empty
done = False
for selobj in selection:
# automatic mode
if selobj.Object.isDerivedFrom("Part::Feature"):
FreeCAD.ActiveDocument.openTransaction(translate("EM","Create FHPath"))
FreeCADGui.addModule("EM")
FreeCADGui.doCommand('obj=EM.makeFHPath(FreeCAD.ActiveDocument.'+selobj.Object.Name+')')
# autogrouping, for later on
#FreeCADGui.addModule("Draft")
#FreeCADGui.doCommand("Draft.autogroup(obj)")
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
# this is not a mistake. The double recompute() is needed to show the new FHNode object
# that have been created by the first execute(), called upon the first recompute()
FreeCAD.ActiveDocument.recompute()
done = True
if done == False:
FreeCAD.Console.PrintWarning(translate("EM","No valid object found in the selection for the creation of a FHPath. Nothing done."))
if FreeCAD.GuiUp:
FreeCADGui.addCommand('EM_FHPath',_CommandFHPath())