-
Notifications
You must be signed in to change notification settings - Fork 9
/
lattice2ParaSeries.py
365 lines (310 loc) · 17.9 KB
/
lattice2ParaSeries.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
#***************************************************************************
#* *
#* Copyright (c) 2015 - Victor Titov (DeepSOIC) *
#* <[email protected]> *
#* *
#* 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__="Lattice ParaSeries feature"
__author__ = "DeepSOIC"
__url__ = ""
__doc__ = "Lattice ParaSeries feature: generates series of shapes by modifying a parameter"
import math
import FreeCAD as App
import Part
from lattice2Common import *
import lattice2BaseFeature
import lattice2Executer
import lattice2Markers as markers
from lattice2ValueSeriesGenerator import ValueSeriesGenerator
# --------------------------- general routines ------------------------------------------------
def getParameter(doc, strParameter):
return setParameter(doc, strParameter, value= None, get_not_set= True)
def setParameter(doc, strParameter, value, get_not_set = False):
'''Sets parameter in the model. strParameter should be like "Box.Height"'''
pieces = strParameter.split(".")
objname = pieces[0]
obj_to_modify = doc.getObject(objname)
if obj_to_modify is None:
raise ValueError("failed to get the object named '"+objname+"'. Maybe you had put in its label instead?..")
if obj_to_modify.isDerivedFrom("Spreadsheet::Sheet"):
# SPECIAL CASE: spreadsheet cell
if len(pieces) != 2:
raise ValueError("failed to parse parameter reference: "+refstr )
oldval = obj_to_modify.get(pieces[1])
if get_not_set:
return oldval
if value != oldval:
obj_to_modify.set(pieces[1], str(value))
elif obj_to_modify.isDerivedFrom("Sketcher::SketchObject") and pieces[1] == "Constraints":
# SPECIAL CASE: sketcher constraint
if len(pieces) != 3:
raise ValueError("failed to parse parameter reference: "+refstr )
oldval = obj_to_modify.getDatum(pieces[2])
if get_not_set:
return oldval
if value != oldval:
try:
obj_to_modify.setDatum(pieces[2],value)
except ValueError as err:
# strangely. n setDatum, sketch attempts to solve itself, and if fails, throws. However, the constraint datum is actually modified... funny, isn't it?
App.Console.PrintWarning("Setting sketch constraint {constr} failed with a ValueError. This could have been caused by sketch failing to be solved.\n"
.format(constr= pieces[2]))
else:
# All other non-special cases: properties or subproperties of objects
if len(pieces) < 2:
raise ValueError("failed to parse parameter reference: "+refstr )
# Extract property, subproperty, subsub... FreeCAD doesn't track mutating returned objects, so we need to mutate them and write back explicitly.
stack = [obj_to_modify]
for piece in pieces[1:-1]:
stack.append(getattr(stack[-1],piece))
oldval = getattr(stack[-1], pieces[-1])
if get_not_set:
return oldval
if value != oldval:
setattr(stack[-1], pieces[-1], value)
for piece in pieces[1:-1:-1]:
compval = stack.pop()
setattr(stack[-1], piece, compval)
# -------------------------- document object --------------------------------------------------
def makeLatticeParaSeries(name):
'''makeLatticeParaSeries(name): makes a LatticeParaSeries object.'''
return lattice2BaseFeature.makeLatticeFeature(name, LatticeParaSeries, ViewProviderLatticeParaSeries)
class LatticeParaSeries(lattice2BaseFeature.LatticeFeature):
"The Lattice ParaSeries object"
def derivedInit(self,obj):
self.Type = "LatticeParaSeries"
obj.addProperty("App::PropertyLink","Object","Lattice ParaSeries","Object to make series from. Can be any generic shape, as well as an array of placements.")
obj.addProperty("App::PropertyEnumeration","ParameterType","Lattice ParaSeries","Data type of parameter to vary.")
obj.ParameterType = ['float','int','string']
obj.addProperty("App::PropertyString","ParameterRef","Lattice ParaSeries","Reference to the parameter to vary. Syntax: ObjectName.Property. Examples: 'Box.Height'; 'Sketch.Constraints.myLength'.")
obj.addProperty("App::PropertyEnumeration","Recomputing","Lattice ParaSeries","Sets recomputing policy.")
obj.Recomputing = ["Disabled", "Recompute Once", "Enabled"]
obj.Recomputing = "Disabled" # recomputing ParaSeries can be very long, so disable it by default
self.assureGenerator(obj)
def assureGenerator(self, obj):
'''Adds an instance of value series generator, if one doesn't exist yet.'''
if hasattr(self,"generator"):
return
self.generator = ValueSeriesGenerator(obj)
self.generator.addProperties(groupname= "Lattice ParaSeries",
groupname_gen= "Lattice ParaSeries Generator",
valuesdoc= "List of parameter values to compute object for.")
self.generator.updateReadonlyness()
def derivedExecute(self,selfobj):
# values generator should be functional even if recomputing is disabled, so do it first
self.assureGenerator(selfobj)
self.generator.updateReadonlyness()
self.generator.execute()
if selfobj.Recomputing == "Disabled":
raise ValueError(selfobj.Name+": recomputing of this object is currently disabled. Modify 'Recomputing' property to enable it.")
try:
#test parameter references and read out their current values
refstr = selfobj.ParameterRef #dict(selfobj.ExpressionEngine)["ParameterRef"]
refstrs = refstr.replace(";","\t").split("\t")
defvalues = []
for refstr in refstrs:
refstr = refstr.strip();
val = None;
try:
val = getParameter(selfobj.Document,refstr)
except Exception as err:
App.Console.PrintError("{obj}: failed to read out parameter '{param}': {err}\n"
.format(obj= selfobj.Name,
param= refstr,
err= str(err)))
defvalues.append(val)
N_params = len(defvalues)
if N_params == 0:
raise ValueError(selfobj.Name+": ParameterRef is not set. It is required.")
#parse values
values = []
for strrow in selfobj.Values:
if len(strrow) == 0:
break;
row = strrow.split(";")
row = [(strv.strip() if len(strv.strip())>0 else None) for strv in row] # clean out spaces and replace empty strings with None
if len(row) < N_params:
row += [None]*(N_params - len(row))
values.append(row)
# convert values to type, filling in defaults where values are missing
for row in values:
for icol in range(N_params):
strv = row[icol]
val = None
if strv is None:
val = defvalues[icol]
elif selfobj.ParameterType == 'float' or selfobj.ParameterType == 'int':
val = float(strv.replace(",","."))
if selfobj.ParameterType == 'int':
val = int(round(val))
elif selfobj.ParameterType == 'string':
val = strv.strip()
else:
raise ValueError(selfobj.Name + ": ParameterType option not implemented: "+selfobj.ParameterType)
row[icol] = val
if len(values) == 0:
scale = 1.0
try:
if not screen(selfobj.Object).Shape.isNull():
scale = screen(selfobj.Object).Shape.BoundBox.DiagonalLength/math.sqrt(3)
except Exception:
pass
if scale < DistConfusion * 100:
scale = 1.0
selfobj.Shape = markers.getNullShapeShape(scale)
raise ValueError(selfobj.Name + ": list of values is empty.")
bGui = False #bool(App.GuiUp) #disabled temporarily, because it causes a crash if property edits are approved by hitting Enter
if bGui:
import PySide
progress = PySide.QtGui.QProgressDialog(u"Recomputing "+selfobj.Label, u"Abort", 0, len(values)+1)
progress.setModal(True)
progress.show()
doc1 = selfobj.Document
doc2 = App.newDocument() #create temporary doc to do the computations
# assign doc's filename before copying objects, otherwise we get errors with xlinks
try:
doc2.FileName = doc1.FileName
except Exception as err:
pass #in old FreeCADs, FileName property is read-only, we can safely ignore that
object_in_doc2 = None # define the variable, to prevent del() in finally block from raising another error
try:
doc2.copyObject(screen(selfobj.Object), True)
#if there are nested paraseries in the dependencies, make sure to enable them
for objd2 in doc2.Objects:
if hasattr(objd2,"Recomputing"):
try:
objd2.Recomputing = "Enabled"
objd2.purgeTouched()
except exception:
lattice2Executer.warning(selfobj,"Failed to enable recomputing of "+objd2.Name)
object_in_doc2 = doc2.getObject(screen(selfobj.Object).Name)
if bGui:
progress.setValue(1)
output_shapes = []
for row in values:
for icol in range(len(row)):
setParameter(doc2, refstrs[icol].strip(), row[icol])
#recompute
doc2.recompute()
#get shape
shape = None
for obj in doc2.Objects:
if 'Invalid' in obj.State:
lattice2Executer.error(obj,"Recomputing shape for parameter value of "+repr(row)+" failed.")
scale = 1.0
try:
if not screen(selfobj.Object).Shape.isNull():
scale = screen(selfobj.Object).Shape.BoundBox.DiagonalLength/math.sqrt(3)
except Exception:
pass
if scale < DistConfusion * 100:
scale = 1.0
shape = markers.getNullShapeShape(scale)
if shape is None:
shape = object_in_doc2.Shape.copy()
output_shapes.append(shape)
#update progress
if bGui:
progress.setValue(progress.value()+1)
if progress.wasCanceled():
raise lattice2Executer.CancelError()
finally:
#delete all references, before destroying the document. Probably not required, but to be sure...
del(object_in_doc2)
doc2_name = doc2.Name
del(doc2)
App.closeDocument(doc2_name)
if bGui:
progress.setValue(len(values)+1)
selfobj.Shape = Part.makeCompound(output_shapes)
output_is_lattice = lattice2BaseFeature.isObjectLattice(screen(selfobj.Object))
if 'Auto' in selfobj.isLattice:
new_isLattice = 'Auto-On' if output_is_lattice else 'Auto-Off'
if selfobj.isLattice != new_isLattice:#check, to not cause onChanged without necessity (onChange messes with colors, it's better to keep user color)
selfobj.isLattice = new_isLattice
finally:
if selfobj.Recomputing == "Recompute Once":
selfobj.Recomputing = "Disabled"
return "suppress" # "suppress" disables most convenience code of lattice2BaseFeature. We do it because we build a nested array, which are not yet supported by lattice WB.
class ViewProviderLatticeParaSeries(lattice2BaseFeature.ViewProviderLatticeFeature):
def getIcon(self):
return getIconPath("Lattice2_ParaSeries.svg")
def claimChildren(self):
weakparenting = App.ParamGet("User parameter:BaseApp/Preferences/Mod/Lattice2").GetBool("WeakParenting", True)
if weakparenting:
return []
return [screen(self.Object.Object)]
# -------------------------- /document object --------------------------------------------------
# -------------------------- Gui command --------------------------------------------------
def CreateLatticeParaSeries(name, shapeObj):
'''utility function; sharing common code for all populate-copies commands'''
FreeCADGui.addModule("lattice2ParaSeries")
FreeCADGui.addModule("lattice2Executer")
#fill in properties
FreeCADGui.doCommand("f = lattice2ParaSeries.makeLatticeParaSeries(name='"+name+"')")
FreeCADGui.doCommand("f.Object = App.ActiveDocument."+shapeObj.Name)
#execute
FreeCADGui.doCommand("lattice2Executer.executeFeature(f)")
#hide something
FreeCADGui.doCommand("f.Object.ViewObject.hide()")
#finalize
FreeCADGui.doCommand("Gui.Selection.addSelection(f)")
FreeCADGui.doCommand("f = None")
def cmdCreateSeries():
sel = FreeCADGui.Selection.getSelectionEx()
if len(sel) == 1 :
FreeCAD.ActiveDocument.openTransaction("Populate with copies")
CreateLatticeParaSeries("ParaSeries",sel[0].Object)
deselect(sel)
FreeCAD.ActiveDocument.commitTransaction()
else:
raise SelectionError("Bad selection","Please select an object to generate series, first.")
class _CommandLatticeParaSeries:
"Command to create LatticeParaSeries feature"
def GetResources(self):
return {'Pixmap' : getIconPath("Lattice2_ParaSeries.svg"),
'MenuText': QtCore.QT_TRANSLATE_NOOP("Lattice2_ParaSeries","ParaSeries"),
'Accel': "",
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Lattice2_ParaSeries","ParaSeries: generate an array of shapes by varying a design parameter")}
def Activated(self):
try:
if len(FreeCADGui.Selection.getSelection())==0:
infoMessage("ParaSeries",
"ParaSeries command. Generates an array of shapes by varying a design parameter.\n\n"+
"Please select an object to generate array from. Then invoke the command. After that, set up the series in properties of ParaSeries feature created, and change Recomputing property to get a result.\n\n"+
"Setting up the series involves: specifying the parameter to modify (ParameterRef property), and setting up the value list.\n"+
"The reference is specified like an expression: ObjectName.Property. ObjectNane is the name of the object that has the parameter (name, not label - use Lattice Inspect to get the name).\n"+
"Examples of references:\n"+
"Box.Length\n"+
"Sketch001.Constraints.myLength (where myLength is the name of the constraint)\n"+
"Box.Placement.Base.y\n\n"+
"To set up the series of values for the parameter, you can simply edit the Values property. Or, a standard sequence can be generated (set ValuesSource to Generator)."
)
return
cmdCreateSeries()
except Exception as err:
msgError(err)
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
if FreeCAD.GuiUp:
FreeCADGui.addCommand('Lattice2_ParaSeries', _CommandLatticeParaSeries())
exportedCommands = ['Lattice2_ParaSeries']