362 lines
15 KiB
Python
362 lines
15 KiB
Python
#***************************************************************************
|
|
#* *
|
|
#* Copyright (c) 2018 - 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 PartDesign Pattern object: a partdesign pattern based on Lattice array."
|
|
__author__ = "DeepSOIC"
|
|
__url__ = ""
|
|
|
|
import FreeCAD as App
|
|
import Part
|
|
|
|
from lattice2Common import *
|
|
import lattice2BaseFeature
|
|
import lattice2Executer
|
|
from lattice2ShapeCopy import shallowCopy, transformCopy_Smart
|
|
|
|
from lattice2PopulateCopies import DereferenceArray
|
|
|
|
|
|
class FeatureUnsupportedError(RuntimeError):
|
|
pass
|
|
class NotPartDesignFeatureError(RuntimeError):
|
|
pass
|
|
class FeatureFailure(RuntimeError):
|
|
pass
|
|
class ScopeError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class MultiTransformSettings(object):
|
|
selfintersections = False #if True, take care of intersections between occurrences. If False, optimize assuming occurrences do not intersect.
|
|
sign_override = +1 #+1 for keep sign, -1 for invert, +2 for force positive, -2 for force negative
|
|
|
|
|
|
def makeFeature():
|
|
'''makeFeature(): makes a PartDesignPattern object.'''
|
|
obj = activeBody().newObject("PartDesign::FeaturePython","LatticePattern")
|
|
LatticePDPattern(obj)
|
|
if FreeCAD.GuiUp:
|
|
ViewProviderLatticePDPattern(obj.ViewObject)
|
|
return obj
|
|
|
|
def getBodySequence(body, skipfirst = False):
|
|
visited = set()
|
|
result = []
|
|
curfeature = body.Tip
|
|
while True:
|
|
if curfeature in visited:
|
|
raise ValueError("Feature sequence is looped in {body}".format(body= body.Name))
|
|
if curfeature is None:
|
|
break
|
|
if not curfeature.isDerivedFrom('PartDesign::Feature'):
|
|
break
|
|
if not body.hasObject(curfeature):
|
|
break
|
|
if curfeature.isDerivedFrom('PartDesign::FeatureBase'):
|
|
#base feature for body. Do not include.
|
|
break
|
|
visited.add(curfeature)
|
|
result.insert(0, curfeature)
|
|
curfeature = curfeature.BaseFeature
|
|
if skipfirst:
|
|
result.pop(0)
|
|
return result
|
|
|
|
def feature_sign(feature, raise_if_unsupported = False):
|
|
"""feature_sign(feature, raise_if_unsupported = False): returns +1 for additive PD features, -1 for subtractive PD features, and 0 for the remaining (unsupported)"""
|
|
additive_types = [
|
|
'PartDesign::Pad',
|
|
'PartDesign::Revolution',
|
|
]
|
|
subtractive_types = [
|
|
'PartDesign::Pocket',
|
|
'PartDesign::Groove',
|
|
'PartDesign::Hole',
|
|
]
|
|
def unsupported():
|
|
if raise_if_unsupported:
|
|
raise FeatureUnsupportedError("Feature {name} is neither additive nor subtractive. Unsupported.".format(name= feature.Name))
|
|
else:
|
|
return 0
|
|
|
|
if not feature.isDerivedFrom('PartDesign::Feature'):
|
|
raise NotPartDesignFeatureError("Feature {name} is not a PartDesign feature. Unsupported.".format(name= feature.Name))
|
|
if hasattr(feature, 'AddSubType'): #part-o-magic; possibly PartDesign future
|
|
t = feature.AddSubType
|
|
if t == 'Additive':
|
|
return +1
|
|
elif t == 'Subtractive':
|
|
return -1
|
|
else:
|
|
return unsupported()
|
|
typ = feature.TypeId
|
|
if typ in additive_types:
|
|
return 1
|
|
if typ in subtractive_types:
|
|
return -1
|
|
if 'Additive' in typ:
|
|
return +1
|
|
if 'Subtractive' in typ:
|
|
return -1
|
|
if typ == 'PartDesign::Boolean':
|
|
t = feature.Type
|
|
if t == 'Fuse':
|
|
return +1
|
|
elif t == 'Cut':
|
|
return -1
|
|
else:
|
|
return unsupported()
|
|
return unsupported()
|
|
|
|
def getFeatureShapes(feature):
|
|
sign = feature_sign(feature, raise_if_unsupported= True)
|
|
if hasattr(feature, 'AddSubShape'):
|
|
sh = shallowCopy(feature.AddSubShape)
|
|
sh.Placement = feature.Placement
|
|
return [(sign, sh)]
|
|
elif feature.isDerivedFrom('PartDesign::Boolean'):
|
|
return [(sign, obj.Shape) for obj in feature.Group]
|
|
else:
|
|
raise FeatureUnsupportedError("Feature {name} is not supported.".format(name= feature.Name))
|
|
|
|
def is_supported(feature):
|
|
if hasattr(feature, 'Proxy') and hasattr(feature.Proxy, 'applyTransformed'):
|
|
return True
|
|
try:
|
|
sign = feature_sign(feature, raise_if_unsupported= True)
|
|
return True
|
|
except (FeatureUnsupportedError, NotPartDesignFeatureError):
|
|
return False
|
|
|
|
def applyFeature(baseshape, feature, transforms, mts):
|
|
if hasattr(feature, 'Proxy') and hasattr(feature.Proxy, 'applyTransformed'):
|
|
return feature.Proxy.applyTransformed(feature, baseshape, transforms, mts)
|
|
task = getFeatureShapes(feature)
|
|
for sign,featureshape in task:
|
|
actionshapes = []
|
|
for transform in transforms:
|
|
actionshapes.append(shallowCopy(featureshape, transform))
|
|
|
|
if mts.selfintersections:
|
|
pass #to fuse the shapes to baseshape one by one
|
|
else:
|
|
actionshapes = [Part.Compound(actionshapes)] #to fuse all at once, saving for computing intersections between the occurrences of the feature
|
|
|
|
for actionshape in actionshapes:
|
|
assert(sign != 0)
|
|
realsign = sign * mts.sign_override
|
|
if abs(mts.sign_override) == +2:
|
|
realsign = int(mts.sign_override / 2)
|
|
if realsign > 0:
|
|
baseshape = baseshape.fuse(actionshape)
|
|
elif realsign < 0:
|
|
baseshape = baseshape.cut(actionshape)
|
|
if baseshape.isNull():
|
|
raise FeatureFailure('applying {name} failed - returned shape is null'.format(name= feature.Name))
|
|
return baseshape
|
|
|
|
class LatticePDPattern(object):
|
|
def __init__(self,obj):
|
|
obj.addProperty('App::PropertyLinkListGlobal','FeaturesToCopy',"Lattice Pattern","Features to be copied (can be a body)")
|
|
obj.addProperty('App::PropertyLinkGlobal','PlacementsFrom',"Lattice Pattern","Reference placement (placement that marks where the original feature is)")
|
|
obj.addProperty('App::PropertyLink','PlacementsTo',"Lattice Pattern","Target placements")
|
|
|
|
obj.addProperty('App::PropertyEnumeration','Referencing',"Lattice Pattern","Reference placement mode (sets what to grab the feature by).")
|
|
obj.Referencing = ['Origin','First item', 'Last item', 'Use PlacementsFrom']
|
|
|
|
obj.addProperty('App::PropertyBool', 'IgnoreUnsupported', "Lattice Pattern", "Skip unsupported features such as fillets, instead of throwing errors")
|
|
obj.addProperty('App::PropertyBool', 'SkipFirstInBody', "Lattice Pattern", "Skip first body feature (which may be used as support for the important features).")
|
|
|
|
obj.addProperty('App::PropertyEnumeration', 'SignOverride', "Lattice Pattern", "Use it to change Pockets into Pads.")
|
|
obj.SignOverride = ['keep', 'invert', 'as additive', 'as subtractive']
|
|
|
|
obj.addProperty('App::PropertyBool', 'Selfintersections', "Lattice Pattern", "If True, take care of intersections between occurrences. If False, you get a slight speed boost.")
|
|
|
|
obj.addProperty('App::PropertyBool', 'Refine', "PartDesign", "If True, remove redundant edges after this operation.")
|
|
obj.Refine = getParamPDRefine()
|
|
|
|
obj.addProperty('App::PropertyBool', 'SingleSolid', "PartDesign", "If True, discard solids not joined with the base.")
|
|
|
|
obj.Proxy = self
|
|
|
|
def execute(self, selfobj):
|
|
if selfobj.BaseFeature is None:
|
|
baseshape = Part.Compound([])
|
|
else:
|
|
baseshape = selfobj.BaseFeature.Shape
|
|
|
|
mts = MultiTransformSettings()
|
|
mts.sign_override = {'keep': +1, 'invert': -1, 'as additive': +2 , 'as subtractive': -2}[selfobj.SignOverride]
|
|
mts.selfintersections = selfobj.Selfintersections
|
|
|
|
result = self.applyTransformed(selfobj, baseshape, None, mts)
|
|
if selfobj.SingleSolid:
|
|
# not proper implementation, but should do for majority of cases: pick the largest solid.
|
|
vmax = 0
|
|
vmax_solid = None
|
|
for s in result.Solids:
|
|
v = s.Volume
|
|
if v > vmax:
|
|
vmax = v
|
|
vmax_solid = s
|
|
if vmax_solid is None:
|
|
raise ValueError("No solids in result. Maybe the result is corrupted because of failed BOP, or all the material was removed in the end.")
|
|
result = vmax_solid
|
|
if selfobj.SingleSolid or len(result.Solids) == 1:
|
|
result = transformCopy_Smart(result.Solids[0], selfobj.Placement)
|
|
if selfobj.Refine:
|
|
result = result.removeSplitter()
|
|
selfobj.Shape = result
|
|
|
|
def applyTransformed(self, selfobj, baseshape, transforms, mts):
|
|
featurelist = []
|
|
has_bodies = False
|
|
has_features = False
|
|
for lnk in selfobj.FeaturesToCopy:
|
|
if lnk.isDerivedFrom('PartDesign::Body'):
|
|
featurelist.extend(getBodySequence(lnk, skipfirst= selfobj.SkipFirstInBody))
|
|
has_bodies = True
|
|
else:
|
|
featurelist.append(lnk)
|
|
has_features = True
|
|
|
|
#check cross-links
|
|
if selfobj.Referencing == 'Use PlacementsFrom':
|
|
body_ref = bodyOf(selfobj.PlacementsFrom)
|
|
else:
|
|
body_ref = bodyOf(selfobj)
|
|
for feature in featurelist:
|
|
if bodyOf(feature) is not body_ref:
|
|
raise ScopeError('Reference placement and the feature are not in the same body (use Shapebinder or Ghost to bring the placement in).')
|
|
|
|
|
|
placements = lattice2BaseFeature.getPlacementsList(selfobj.PlacementsTo, selfobj)
|
|
placements = DereferenceArray(selfobj, placements, selfobj.PlacementsFrom, selfobj.Referencing)
|
|
if selfobj.Referencing == 'First item' and transforms is None:
|
|
placements.pop(0) #to not repeat the feature where it was applied already
|
|
elif selfobj.Referencing == 'Last item' and transforms is None:
|
|
placements.pop() #to not repeat the feature where it was applied already
|
|
if not transforms is None:
|
|
newplacements = []
|
|
for transform in transforms:
|
|
newplacements += [transform.multiply(plm) for plm in placements]
|
|
placements = newplacements
|
|
for feature in featurelist:
|
|
try:
|
|
baseshape = applyFeature(baseshape, feature, placements, mts)
|
|
except FeatureUnsupportedError as err:
|
|
if not selfobj.IgnoreUnsupported:
|
|
raise
|
|
else:
|
|
App.Console.PrintLog('{name} is unsupported, skipped.\n'.format(name= feature.Name))
|
|
return baseshape
|
|
|
|
def __getstate__(self):
|
|
return None
|
|
|
|
def __setstate__(self,state):
|
|
return None
|
|
|
|
|
|
class ViewProviderLatticePDPattern:
|
|
"A View Provider for the Lattice PartDesign Pattern object"
|
|
|
|
def __init__(self,vobj):
|
|
vobj.Proxy = self
|
|
|
|
def getIcon(self):
|
|
return getIconPath("Lattice2_PDPattern.svg")
|
|
|
|
def attach(self, vobj):
|
|
self.ViewObject = vobj
|
|
self.Object = vobj.Object
|
|
|
|
|
|
def __getstate__(self):
|
|
return None
|
|
|
|
def __setstate__(self,state):
|
|
return None
|
|
|
|
def claimChildren(self):
|
|
return [self.Object.PlacementsTo]
|
|
|
|
def onDelete(self, feature, subelements): # subelements is a tuple of strings
|
|
return True
|
|
|
|
# -------------------------- /document object --------------------------------------------------
|
|
|
|
# -------------------------- Gui command --------------------------------------------------
|
|
|
|
|
|
def CreateLatticePDPattern(features, latticeObjFrom, latticeObjTo, refmode):
|
|
FreeCADGui.addModule("lattice2PDPattern")
|
|
FreeCADGui.addModule("lattice2Executer")
|
|
|
|
#fill in properties
|
|
FreeCADGui.doCommand("f = lattice2PDPattern.makeFeature()")
|
|
reprfeatures = ', '.join(['App.ActiveDocument.'+f.Name for f in features])
|
|
FreeCADGui.doCommand("f.FeaturesToCopy = [{features}]".format(features= reprfeatures))
|
|
FreeCADGui.doCommand("f.PlacementsTo = App.ActiveDocument."+latticeObjTo.Name)
|
|
if latticeObjFrom is not None:
|
|
FreeCADGui.doCommand("f.PlacementsFrom = App.ActiveDocument."+latticeObjFrom.Name)
|
|
FreeCADGui.doCommand("f.Referencing = "+repr(refmode))
|
|
|
|
#execute
|
|
FreeCADGui.doCommand("lattice2Executer.executeFeature(f)")
|
|
|
|
#hide something
|
|
FreeCADGui.doCommand("f.PlacementsTo.ViewObject.hide()")
|
|
FreeCADGui.doCommand("f.BaseFeature.ViewObject.hide()")
|
|
|
|
#finalize
|
|
FreeCADGui.doCommand("Gui.Selection.addSelection(f)")
|
|
FreeCADGui.doCommand("f = None")
|
|
|
|
|
|
def cmdPDPattern():
|
|
sel = FreeCADGui.Selection.getSelectionEx()
|
|
(lattices, shapes) = lattice2BaseFeature.splitSelection(sel)
|
|
if len(shapes) > 0 and len(lattices) == 2:
|
|
FreeCAD.ActiveDocument.openTransaction("Lattice Pattern")
|
|
latticeFrom = lattices[0]
|
|
latticeTo = lattices[1]
|
|
CreateLatticePDPattern([so.Object for so in shapes], latticeFrom.Object, latticeTo.Object,'Use PlacementsFrom')
|
|
deselect(sel)
|
|
FreeCAD.ActiveDocument.commitTransaction()
|
|
elif len(shapes) > 0 and len(lattices) == 1:
|
|
FreeCAD.ActiveDocument.openTransaction("Lattice Pattern")
|
|
latticeTo = lattices[0]
|
|
CreateLatticePDPattern([so.Object for so in shapes], None, latticeTo.Object,'First item')
|
|
deselect(sel)
|
|
FreeCAD.ActiveDocument.commitTransaction()
|
|
else:
|
|
raise SelectionError("Bad selection",
|
|
"Please select either:\n"
|
|
" one or more PartDesign features, and one or two placements/arrays \n"
|
|
"or\n"
|
|
" a template body and two placements/arrays, one from selected body and one from active body."
|
|
)
|
|
|
|
# command defined in lattice2PDPatternCommand.py
|