356 lines
18 KiB
Python
356 lines
18 KiB
Python
#***************************************************************************
|
|
#* *
|
|
#* Copyright (c) 2015 - Victor Titov (DeepSOIC) *
|
|
#* <vv.titov@gmail.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__="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(selfobj.Name+": 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(selfobj.Name + ": 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(selfobj.Name + ": 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(selfobj.Name + ": 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()
|
|
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):
|
|
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']
|