310 lines
15 KiB
Python
310 lines
15 KiB
Python
#***************************************************************************
|
|
#* *
|
|
#* Copyright (c) 2016 - 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 TopoSeries feature"
|
|
__author__ = "DeepSOIC"
|
|
__url__ = ""
|
|
__doc__ = "Lattice TopoSeries feature: generates series of shapes by subsequencing sublinks"
|
|
|
|
import math
|
|
|
|
import FreeCAD as App
|
|
import Part
|
|
|
|
from lattice2Common import *
|
|
import lattice2BaseFeature
|
|
import lattice2Executer as Executer
|
|
import lattice2Markers as markers
|
|
import lattice2Subsequencer as Subsequencer
|
|
|
|
# --------------------------- general routines ------------------------------------------------
|
|
|
|
def findAllLinksTo(doc_obj):
|
|
"""findAllLinksTo(doc_obj): finds all link properties pointing to supplied object.
|
|
Returns them as list of tuples (dependent_object_name, property_name). Does not include
|
|
expression links."""
|
|
ret = []
|
|
doc = doc_obj.Document
|
|
for obj in doc_obj.InList:
|
|
for prop_name in obj.PropertiesList:
|
|
typ = obj.getTypeIdOfProperty(prop_name)
|
|
if typ == 'App::PropertyLink':
|
|
if readProperty(doc, obj.Name, prop_name) is doc_obj:
|
|
ret.append((obj.Name, prop_name))
|
|
elif typ == 'App::PropertyLinkList':
|
|
if doc_obj in readProperty(doc, obj.Name, prop_name):
|
|
ret.append((obj.Name, prop_name))
|
|
elif typ == 'App::PropertyLinkSub':
|
|
val = readProperty(doc, obj.Name, prop_name)
|
|
if val is not None and doc_obj is val[0]:
|
|
ret.append((obj.Name, prop_name))
|
|
elif typ == 'App::PropertyLinkSubList':
|
|
if doc_obj in [tup[0] for tup in readProperty(doc, obj.Name, prop_name)]:
|
|
ret.append((obj.Name, prop_name))
|
|
return ret
|
|
|
|
def readProperty(doc, object_name, property_name):
|
|
return getattr(doc.getObject(object_name), property_name)
|
|
|
|
def writeProperty(doc, object_name, property_name, value):
|
|
setattr(doc.getObject(object_name), property_name, value)
|
|
|
|
# -------------------------- document object --------------------------------------------------
|
|
|
|
def makeLatticeTopoSeries(name):
|
|
'''makeLatticeTopoSeries(name): makes a LatticeTopoSeries object.'''
|
|
return lattice2BaseFeature.makeLatticeFeature(name, LatticeTopoSeries, ViewProviderLatticeTopoSeries)
|
|
|
|
class LatticeTopoSeries(lattice2BaseFeature.LatticeFeature):
|
|
"The Lattice TopoSeries object"
|
|
|
|
def derivedInit(self,obj):
|
|
self.Type = "LatticeTopoSeries"
|
|
|
|
obj.addProperty("App::PropertyLink","ObjectToTake","Lattice TopoSeries","Object to collect permutations of. Can be any generic shape, as well as an array of placements.")
|
|
obj.addProperty("App::PropertyLink","ObjectToLoopOver","Lattice TopoSeries","Array object to subsequence sublinks to.")
|
|
|
|
obj.addProperty("App::PropertyEnumeration", "CycleMode", "Lattice TopoSeries", "Sets how to treat the ObjectToLoopOver")
|
|
obj.CycleMode = ["Open", "Periodic"]
|
|
|
|
obj.addProperty("App::PropertyEnumeration","Recomputing","Lattice TopoSeries","Sets recomputing policy.")
|
|
obj.Recomputing = ["Disabled", "Recompute Once", "Enabled"]
|
|
obj.Recomputing = "Disabled" # recomputing TopoSeries can be very long, so disable it by default
|
|
|
|
def makeSubsequence(self, selfobj, object_to_loop):
|
|
|
|
# gather up the links
|
|
links = findAllLinksTo(object_to_loop)
|
|
if self.isVerbose():
|
|
print ("All links to {feature}:\n {links}"
|
|
.format(feature= object_to_loop.Document.Name+"."+object_to_loop.Name,
|
|
links= "\n ".join([link[0]+"."+link[1] for link in links]) ) )
|
|
|
|
# subsequencing
|
|
# prepare dict of link values
|
|
linkdict = {} #key is tuple (object_name, property_name). Value is the value of property.
|
|
for link in links:
|
|
link_val = readProperty(object_to_loop.Document, link[0], link[1])
|
|
linkdict[link] = link_val
|
|
# do the subsequencing
|
|
ret = Subsequencer.Subsequence_LinkDict(
|
|
linkdict,
|
|
loop= ('Till end' if selfobj.CycleMode == 'Open' else 'All around'),
|
|
object_filter= [object_to_loop] )
|
|
if self.isVerbose():
|
|
print ("Subsequence made. Length: {n_seq}".format(n_seq= ret[0]))
|
|
return ret
|
|
|
|
|
|
def isVerbose(self):
|
|
return True
|
|
|
|
def derivedExecute(self,selfobj):
|
|
|
|
if selfobj.Recomputing == "Disabled":
|
|
raise ValueError(selfobj.Name+": recomputing of this object is currently disabled. Modify 'Recomputing' property to enable it.")
|
|
try:
|
|
|
|
# do the subsequencing in this document first, to verify stuff is set up correctly, and to obtain sequence length
|
|
if self.isVerbose():
|
|
print ("In-place pre-subsequencing, for early check")
|
|
n_seq, subs_linkdict = self.makeSubsequence(selfobj, selfobj.ObjectToLoopOver)
|
|
|
|
|
|
bGui = bool(App.GuiUp) and Executer.globalIsCreatingLatticeFeature #disabled for most recomputes, 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, n_seq+1)
|
|
progress.setModal(True)
|
|
progress.show()
|
|
|
|
doc1 = selfobj.Document
|
|
doc2 = App.newDocument()
|
|
object_to_take_in_doc2 = None # define the variable, to prevent del() in finally block from raising another error
|
|
object_to_loop_in_doc2 = None
|
|
try:
|
|
if self.isVerbose():
|
|
print ("Copying object with dependencies to a temporary document...")
|
|
|
|
doc2.copyObject(selfobj.ObjectToTake, True)
|
|
|
|
if self.isVerbose():
|
|
print ("Enabling nested para/toposeries, if any...")
|
|
#if there are nested para/toposeries 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:
|
|
Executer.warning(selfobj,"Failed to enable recomputing of "+objd2.Name)
|
|
|
|
object_to_take_in_doc2 = doc2.getObject(selfobj.ObjectToTake.Name)
|
|
object_to_loop_in_doc2 = doc2.getObject(selfobj.ObjectToLoopOver.Name)
|
|
if bGui:
|
|
progress.setValue(1)
|
|
|
|
if self.isVerbose():
|
|
print ("Repeating subsequencing in temporary document...")
|
|
n_seq, subs_linkdict = self.makeSubsequence(selfobj, object_to_loop_in_doc2)
|
|
|
|
output_shapes = []
|
|
for i in range(n_seq):
|
|
if self.isVerbose():
|
|
print ("Computing {x}/{y}".format(x= i+1, y= n_seq))
|
|
|
|
for key in subs_linkdict:
|
|
writeProperty(doc2, key[0], key[1], subs_linkdict[key][i])
|
|
|
|
#recompute
|
|
doc2.recompute()
|
|
|
|
#get shape
|
|
shape = None
|
|
for obj in doc2.Objects:
|
|
if 'Invalid' in obj.State:
|
|
Executer.error(obj,"Recomputing shape for subsequence index "+repr(i)+" failed.")
|
|
|
|
scale = 1.0
|
|
try:
|
|
if not selfobj.ObjectToTake.Shape.isNull():
|
|
scale = selfobj.ObjectToTake.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_to_take_in_doc2.Shape.copy()
|
|
output_shapes.append(shape)
|
|
|
|
#update progress
|
|
if bGui:
|
|
progress.setValue(progress.value()+1)
|
|
if progress.wasCanceled():
|
|
raise Executer.CancelError()
|
|
|
|
finally:
|
|
#delete all references, before destroying the document. Probably not required, but to be sure...
|
|
if self.isVerbose():
|
|
print ("Cleanup...")
|
|
|
|
del(object_to_take_in_doc2)
|
|
del(object_to_loop_in_doc2)
|
|
doc2_name = doc2.Name
|
|
del(doc2)
|
|
App.closeDocument(doc2_name)
|
|
if bGui:
|
|
progress.setValue(n_seq+1)
|
|
|
|
|
|
selfobj.Shape = Part.makeCompound(output_shapes)
|
|
|
|
output_is_lattice = lattice2BaseFeature.isObjectLattice(selfobj.ObjectToTake)
|
|
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 ViewProviderLatticeTopoSeries(lattice2BaseFeature.ViewProviderLatticeFeature):
|
|
|
|
def getIcon(self):
|
|
return getIconPath("Lattice2_TopoSeries.svg")
|
|
|
|
def claimChildren(self):
|
|
return [self.Object.ObjectToTake]
|
|
|
|
# -------------------------- /document object --------------------------------------------------
|
|
|
|
# -------------------------- Gui command --------------------------------------------------
|
|
|
|
|
|
|
|
def CreateLatticeTopoSeries(name, shapeObj, loopObj):
|
|
FreeCADGui.addModule("lattice2TopoSeries")
|
|
FreeCADGui.addModule("lattice2Executer")
|
|
|
|
#fill in properties
|
|
FreeCADGui.doCommand("f = lattice2TopoSeries.makeLatticeTopoSeries(name='"+name+"')")
|
|
FreeCADGui.doCommand("f.ObjectToTake = App.ActiveDocument."+shapeObj.Name)
|
|
FreeCADGui.doCommand("f.ObjectToLoopOver = App.ActiveDocument."+loopObj.Name)
|
|
|
|
#execute
|
|
FreeCADGui.doCommand("f.Recomputing = 'Recompute Once'")
|
|
FreeCADGui.doCommand("lattice2Executer.executeFeature(f)")
|
|
|
|
#hide something
|
|
FreeCADGui.doCommand("f.ObjectToTake.ViewObject.hide()")
|
|
|
|
#finalize
|
|
FreeCADGui.doCommand("Gui.Selection.addSelection(f)")
|
|
FreeCADGui.doCommand("f = None")
|
|
|
|
def cmdCreateSeries():
|
|
sel = FreeCADGui.Selection.getSelectionEx()
|
|
if len(sel) == 2 :
|
|
doc = FreeCAD.ActiveDocument #remember it! Recomputing TopoSeries messes up ActiveDocument, so committing transaction is screwed up...
|
|
if sel[1].Object.Shape.ShapeType != "Compound":
|
|
raise SelectionError("Bad selection", "Second selected object ({label}) should be an array of shapes (a compound). It is not.".format(label= sel[1].Object.Label))
|
|
doc.openTransaction("TopoSeries")
|
|
CreateLatticeTopoSeries("TopoSeries",sel[0].Object, sel[1].Object)
|
|
deselect(sel)
|
|
doc.commitTransaction()
|
|
else:
|
|
raise SelectionError("Bad selection","Please select two objects, first. First one is the result shape to collect variation of. Second one is an array to loop over.")
|
|
|
|
class _CommandLatticeTopoSeries:
|
|
"Command to create LatticeTopoSeries feature"
|
|
def GetResources(self):
|
|
return {'Pixmap' : getIconPath("Lattice2_TopoSeries.svg"),
|
|
'MenuText': QtCore.QT_TRANSLATE_NOOP("Lattice2_TopoSeries","TopoSeries"),
|
|
'Accel': "",
|
|
'ToolTip': QtCore.QT_TRANSLATE_NOOP("Lattice2_TopoSeries","TopoSeries: generate an array of shapes by subsequencing (looping subelement links across an array).")}
|
|
|
|
def Activated(self):
|
|
try:
|
|
if len(FreeCADGui.Selection.getSelection())==0:
|
|
infoMessage("TopoSeries",
|
|
"TopoSeries command. Generates an array of shapes by subsequencing links in dependencies (looping subelement links across an array).\n\n"+
|
|
"Please select an object to generate array from, and an array object to loop over (order of selection matters!). Then invoke the command.\n\n"+
|
|
"TopoSeries will find all objects that link to the array object, and if a link is a link to subelement (e.g., to Edge1), it will advance it to the corresponding subelements of next array child. Then recompute the result shape and output it as a child. So on until any link goes out of bounds (or it will go around, if 'CycleMode' property is set to `Periodic`)."
|
|
)
|
|
return
|
|
cmdCreateSeries()
|
|
except Exception as err:
|
|
msgError(err)
|
|
|
|
def IsActive(self):
|
|
if FreeCAD.ActiveDocument:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
FreeCADGui.addCommand('Lattice2_TopoSeries', _CommandLatticeTopoSeries())
|
|
|
|
exportedCommands = ['Lattice2_TopoSeries']
|