New feature: PartDesign Pattern!

This commit is contained in:
DeepSOIC 2018-06-11 18:41:31 +03:00
parent 52b80f44b6
commit af79de8c01
9 changed files with 474 additions and 15 deletions

View File

@ -83,6 +83,12 @@ class Lattice2Workbench (Workbench):
)
self.appendToolbar('Lattice2CompoundFeatures', cmdsCompoundTools)
self.appendMenu('Lattice2', cmdsCompoundTools)
cmdsPDTools = ([]
+ Lattice2.PartDesignFeatures.PDPattern.exportedCommands
)
self.appendToolbar('Lattice2PartDesignFeatres', cmdsPDTools)
self.appendMenu('Lattice2', cmdsPDTools)
cmdsGuiTools = ([]
+ Lattice2.GuiTools.Inspect.exportedCommands

View File

@ -31,6 +31,8 @@ import Lattice2CompoundFeatures as CompoundFeatures
import Lattice2ArrayFeatures as ArrayFeatures
import Lattice2PartDesignFeatures as PartDesignFeatures
import Lattice2GuiTools as GuiTools
import lattice2_rc as resource_module

View File

@ -0,0 +1 @@
import lattice2PDPattern as PDPattern

View File

@ -50,7 +50,8 @@ def makeAttachablePlacement(name):
ViewProviderAttachablePlacement(obj.ViewObject)
else:
obj = lattice2BaseFeature.makeLatticeFeature(name, AttachablePlacement, ViewProviderAttachablePlacement, no_disable_attacher= True)
obj.addExtension("Part::AttachExtensionPython", None)
if not obj.hasExtension('Part::AttachExtension'):
obj.addExtension("Part::AttachExtensionPython", None)
return obj
@ -67,6 +68,10 @@ class AttachablePlacement(lattice2BaseFeature.LatticeFeature):
obj.positionBySupport()
return [obj.Placement]
def onDocumentRestored(self, selfobj):
#override that disables disabling of attacher
pass
class ViewProviderAttachablePlacement(lattice2BaseFeature.ViewProviderLatticeFeature):

View File

@ -57,21 +57,12 @@ def makeLatticeFeature(name, AppClass, ViewClass, no_body = False, no_disable_at
body = activeBody()
if body and not no_body:
obj = body.newObject("Part::Part2DObjectPython",name)
obj = body.newObject("Part::Part2DObjectPython",name) #hack: body accepts any 2dobjectpython, thinking it is a sketch. Use it to get into body. This does cause some weirdness (e.g. one can Pad a placement), but that is rather minor.
obj.AttacherType = 'Attacher::AttachEngine3D'
if not no_disable_attacher:
attachprops = [
'Support',
'MapMode',
'MapReversed',
'MapPathParameter',
'AttachmentOffset',
]
for prop in attachprops:
obj.setEditorMode(prop, 2) #hidden
else:
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",name)
AppClass(obj)
if FreeCAD.GuiUp:
if ViewClass:
vp = ViewClass(obj.ViewObject)
@ -254,6 +245,25 @@ class LatticeFeature():
def __setstate__(self,state):
return None
def disableAttacher(self, selfobj, enable= False):
if selfobj.isDerivedFrom('Part::Part2DObject'):
attachprops = [
'Support',
'MapMode',
'MapReversed',
'MapPathParameter',
'AttachmentOffset',
]
for prop in attachprops:
selfobj.setEditorMode(prop, 0 if enable else 2)
if enable:
selfobj.MapMode = selfobj.MapMode #trigger attachment, to make it update property states
def onDocumentRestored(self, selfobj):
#override to have attachment!
self.disableAttacher(selfobj)
class ViewProviderLatticeFeature:
"A View Provider for base lattice object"

View File

@ -34,6 +34,8 @@ def translate(context, text, disambig):
def getParamRefine():
return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Part/Boolean").GetBool("RefineModel")
def getParamPDRefine():
return FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/PartDesign").GetBool("RefineModel")
def getIconPath(icon_dot_svg):
return ":/icons/" + icon_dot_svg
@ -93,3 +95,10 @@ def screen(feature):
def activeBody():
return FreeCADGui.ActiveDocument.ActiveView.getActiveObject("pdbody")
def bodyOf(feature):
body = feature.getParentGeoFeatureGroup()
if body.isDerivedFrom('PartDesign::Body'):
return body
else:
return None

View File

@ -30,6 +30,21 @@ __title__="Geometric utility routines for Lattice workbench for FreeCAD"
__author__ = "DeepSOIC"
__url__ = ""
def PlacementsFuzzyCompare(plm1, plm2):
pos_eq = (plm1.Base - plm2.Base).Length < 1e-7 # 1e-7 is OCC's Precision::Confusion
q1 = plm1.Rotation.Q
q2 = plm2.Rotation.Q
# rotations are equal if q1 == q2 or q1 == -q2.
# Invert one of Q's if their scalar product is negative, before comparison.
if q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2] + q1[3]*q2[3] < 0:
q2 = [-v for v in q2]
rot_eq = ( abs(q1[0]-q2[0]) +
abs(q1[1]-q2[1]) +
abs(q1[2]-q2[2]) +
abs(q1[3]-q2[3]) ) < 1e-12 # 1e-12 is OCC's Precision::Angular (in radians)
return pos_eq and rot_eq
def makeOrientationFromLocalAxes(ZAx, XAx = None):
'''

394
lattice2PDPattern.py Normal file
View File

@ -0,0 +1,394 @@
#***************************************************************************
#* *
#* 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 occurences. If False, optimize assuming occurences 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',
]
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 occurences 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 occurences. 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."
)
class CommandLatticePDPattern:
"Command to create Lattice PartDesign Pattern feature"
def GetResources(self):
return {'Pixmap' : getIconPath("Lattice2_PDPattern.svg"),
'MenuText': "Lattice PartDesign Pattern",
'Accel': "",
'ToolTip': "Lattice PartDesign Pattern command. Replicates partdesign features at every placement in array."}
def Activated(self):
try:
if len(FreeCADGui.Selection.getSelection())==0:
infoMessage("Lattice PartDesign Pattern",
"Lattice PartDesign Pattern command. Replicates partdesign features at every placement in array.\n\n"
"Please select features to repeat, reference placement (optional), and target placement/array. \n\n"
"You can use features from another body. Then, reference placement is required. You can also select a body (a \"template body\"), then all features from that body will be replicated.\n\n"
"Please observe scope restrictions. Reference placement must be in same body the original features are in; target placement/array must be in active body. You can create Lattice Arrays "
"right in PartDesign bodies, but you can't drag them in after the fact. You can import arrays of placements from elsewhere using a Shapebinder, or Part-o-Magic Ghost.")
return
if activeBody() is None:
infoMessage("Lattice PartDesign Pattern", "No active body. Please, activate a body, first.")
cmdPDPattern()
except Exception as err:
msgError(err)
def IsActive(self):
if FreeCAD.ActiveDocument:
return True
else:
return False
if FreeCAD.GuiUp:
FreeCADGui.addCommand('Lattice2_PDPattern', CommandLatticePDPattern())
exportedCommands = ['Lattice2_PDPattern']

View File

@ -28,10 +28,12 @@ __doc__ = "Utility methods to copy shapes"
import FreeCAD
import Part
from lattice2GeomUtils import PlacementsFuzzyCompare
def shallowCopy(shape, extra_placement = None):
"""shallowCopy(shape, extra_placement = None): creates a shallow copy of a shape. The
copy will match by isSame/isEqual/isPartner tests, but will have an independent placement."""
copy will match by isSame/isEqual/isPartner tests, but will have an independent placement.
Supports matrix, but the matrix should be pure placement (not be mirroring)."""
copiers = {
"Vertex": lambda sh: sh.Vertexes[0],
@ -59,7 +61,7 @@ def shallowCopy(shape, extra_placement = None):
def deepCopy(shape, extra_placement = None):
"""deepCopy(shape, extra_placement = None): Copies all subshapes. The copy will not match by isSame/isEqual/
isPartner tests."""
isPartner tests. If matrix is provided, redirects the call to transformCopy."""
if extra_placement is not None:
if hasattr(extra_placement, 'toMatrix'):
@ -72,7 +74,7 @@ def deepCopy(shape, extra_placement = None):
def transformCopy(shape, extra_placement = None):
"""transformCopy(shape, extra_placement = None): creates a deep copy shape with shape's placement applied to
the subelements (the placement of returned shape is zero)."""
the subelements (the placement of returned shape is zero). Supports matrices, including mirroring matrices."""
if extra_placement is None:
extra_placement = FreeCAD.Placement()
@ -88,6 +90,21 @@ def transformCopy(shape, extra_placement = None):
ret.transformShape(extra_placement.multiply(splm), True)
return ret
def transformCopy_Smart(shape, feature_placement):
"""transformCopy_Smart(shape, feature_placement): gets rid of shape's internal placement
(by applying transform to all its elements), and assigns feature_placement to the placement.
I.e. feature_placement is the additional transform to apply. Unlike transformCopy, creates
a shallow copy if possible. Does not support matrices."""
if shape.isNull():
return shape
if PlacementsFuzzyCompare(shape.Placement, FreeCAD.Placement()):
sh = shallowCopy(shape)
else:
sh = transformCopy(shape)
sh.Placement = feature_placement
return sh
copy_types = ["Shallow copy", "Deep copy", "Transformed deep copy"]
copy_functions = [shallowCopy, deepCopy, transformCopy]