Merge pull request #2366 from Russ4262/3D_Pocket_upgrade
[Path] 3D Pocket: upgrade to adaptive start and finish
This commit is contained in:
@@ -43,13 +43,11 @@ __url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Base class and properties for Path.Area based operations."
|
||||
__contributors__ = "russ4262 (Russell Johnson)"
|
||||
__createdDate__ = "2017"
|
||||
__scriptVersion__ = "2j testing"
|
||||
__lastModified__ = "2019-07-12 00:11 CST"
|
||||
__scriptVersion__ = "2m testing"
|
||||
__lastModified__ = "2019-07-20 13:29 CST"
|
||||
|
||||
LOGLEVEL = PathLog.Level.INFO
|
||||
PathLog.setLevel(LOGLEVEL, PathLog.thisModule())
|
||||
# PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
||||
|
||||
|
||||
if LOGLEVEL is PathLog.Level.DEBUG:
|
||||
PathLog.trackModule()
|
||||
@@ -328,6 +326,7 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
self.leadIn = 2.0 # pylint: disable=attribute-defined-outside-init
|
||||
self.cloneNames = [] # pylint: disable=attribute-defined-outside-init
|
||||
self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init
|
||||
self.tempObjectNames = [] # pylint: disable=attribute-defined-outside-init
|
||||
self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init
|
||||
self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones
|
||||
|
||||
@@ -405,7 +404,7 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
for shp in aOS:
|
||||
if len(shp) == 2:
|
||||
(fc, iH) = shp
|
||||
# fc, iH, sub, angle, axis
|
||||
# fc, iH, sub, angle, axis, strtDep, finDep
|
||||
tup = fc, iH, 'otherOp', 0.0, 'S', obj.StartDepth.Value, obj.FinalDepth.Value
|
||||
shapes.append(tup)
|
||||
else:
|
||||
@@ -422,17 +421,9 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
|
||||
shapes = [j['shape'] for j in jobs]
|
||||
|
||||
# PathLog.debug("Pre_path depths are Start: {}, and Final: {}".format(obj.StartDepth.Value, obj.FinalDepth.Value))
|
||||
sims = []
|
||||
numShapes = len(shapes)
|
||||
|
||||
# if numShapes == 1:
|
||||
# nextAxis = shapes[0][4]
|
||||
# elif numShapes > 1:
|
||||
# nextAxis = shapes[1][4]
|
||||
# else:
|
||||
# nextAxis = 'L'
|
||||
|
||||
for ns in range(0, numShapes):
|
||||
(shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns] # pylint: disable=unused-variable
|
||||
if ns < numShapes - 1:
|
||||
@@ -497,6 +488,8 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
|
||||
self.useTempJobClones('Delete') # Delete temp job clone group and contents
|
||||
self.guiMessage('title', None, show=True) # Process GUI messages to user
|
||||
for ton in self.tempObjectNames: # remove temporary objects by name
|
||||
FreeCAD.ActiveDocument.removeObject(ton)
|
||||
PathLog.debug("obj.Name: " + str(obj.Name) + "\n\n")
|
||||
return sims
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ __url__ = "http://www.freecadweb.org"
|
||||
__doc__ = "Class and implementation of the 3D Pocket operation."
|
||||
__contributors__ = "russ4262 (Russell Johnson)"
|
||||
__created__ = "2014"
|
||||
__scriptVersion__ = "1b testing"
|
||||
__lastModified__ = "2019-07-01 20:13 CST"
|
||||
__scriptVersion__ = "2g testing"
|
||||
__lastModified__ = "2019-07-20 22:02 CST"
|
||||
|
||||
LOGLEVEL = False
|
||||
|
||||
@@ -48,6 +48,7 @@ if LOGLEVEL:
|
||||
else:
|
||||
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
||||
|
||||
|
||||
# Qt translation handling
|
||||
def translate(context, text, disambig=None):
|
||||
return QtCore.QCoreApplication.translate(context, text, disambig)
|
||||
@@ -64,6 +65,12 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
|
||||
if not hasattr(obj, 'HandleMultipleFeatures'):
|
||||
obj.addProperty('App::PropertyEnumeration', 'HandleMultipleFeatures', 'Pocket', QtCore.QT_TRANSLATE_NOOP('PathPocket', 'Choose how to process multiple Base Geometry features.'))
|
||||
obj.HandleMultipleFeatures = ['Collectively', 'Individually']
|
||||
if not hasattr(obj, 'AdaptivePocketStart'):
|
||||
obj.addProperty('App::PropertyBool', 'AdaptivePocketStart', 'Pocket', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Use adaptive algorithm to eliminate excessive air milling above planar pocket top.'))
|
||||
if not hasattr(obj, 'AdaptivePocketFinish'):
|
||||
obj.addProperty('App::PropertyBool', 'AdaptivePocketFinish', 'Pocket', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Use adaptive algorithm to eliminate excessive air milling below planar pocket bottom.'))
|
||||
if not hasattr(obj, 'ProcessStockArea'):
|
||||
obj.addProperty('App::PropertyBool', 'ProcessStockArea', 'Pocket', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Process the model and stock in an operation with no Base Geometry selected.'))
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
'''opOnDocumentRestored(obj) ... adds the properties if they doesn't exist.'''
|
||||
@@ -76,28 +83,58 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
|
||||
'''areaOpShapes(obj) ... return shapes representing the solids to be removed.'''
|
||||
PathLog.track()
|
||||
|
||||
subObjTups = []
|
||||
removalshapes = []
|
||||
|
||||
if obj.Base:
|
||||
PathLog.debug("base items exist. Processing...")
|
||||
PathLog.debug("base items exist. Processing... ")
|
||||
for base in obj.Base:
|
||||
PathLog.debug("Base item: {}".format(base))
|
||||
PathLog.debug("obj.Base item: {}".format(base))
|
||||
|
||||
# Check if all subs are faces
|
||||
allFaceSubs = True
|
||||
allSubsFaceType = True
|
||||
Faces = []
|
||||
for sub in base[1]:
|
||||
if "Face" in sub:
|
||||
Faces.append(getattr(base[0].Shape, sub))
|
||||
face = getattr(base[0].Shape, sub)
|
||||
Faces.append(face)
|
||||
subObjTups.append((sub, face))
|
||||
else:
|
||||
allFaceSubs = False
|
||||
allSubsFaceType = False
|
||||
break
|
||||
|
||||
if allFaceSubs is True and obj.HandleMultipleFeatures == 'Collectively':
|
||||
shape = Part.makeCompound(Faces)
|
||||
env = PathUtils.getEnvelope(base[0].Shape, subshape=shape, depthparams=self.depthparams)
|
||||
obj.removalshape = env.cut(base[0].Shape)
|
||||
obj.removalshape.tessellate(0.1)
|
||||
removalshapes.append((obj.removalshape, False))
|
||||
if len(Faces) == 0:
|
||||
allSubsFaceType = False
|
||||
|
||||
if allSubsFaceType is True and obj.HandleMultipleFeatures == 'Collectively':
|
||||
if obj.OpFinalDepth == obj.FinalDepth:
|
||||
(fzmin, fzmax) = self.getMinMaxOfFaces(Faces)
|
||||
obj.FinalDepth.Value = fzmin
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
self.depthparams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=obj.StartDepth.Value,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=fzmin,
|
||||
user_depths=None)
|
||||
PathLog.info("Updated obj.FinalDepth.Value and self.depthparams to zmin: {}".format(fzmin))
|
||||
|
||||
if obj.AdaptivePocketStart is True or obj.AdaptivePocketFinish is True:
|
||||
pocketTup = self.calculateAdaptivePocket(obj, base, subObjTups)
|
||||
if pocketTup is not False:
|
||||
removalshapes.append(pocketTup) # (shape, isHole, sub, angle, axis, strDep, finDep)
|
||||
else:
|
||||
strDep = obj.StartDepth.Value
|
||||
finDep = obj.FinalDepth.Value
|
||||
|
||||
shape = Part.makeCompound(Faces)
|
||||
env = PathUtils.getEnvelope(base[0].Shape, subshape=shape, depthparams=self.depthparams)
|
||||
obj.removalshape = env.cut(base[0].Shape)
|
||||
obj.removalshape.tessellate(0.1)
|
||||
# (shape, isHole, sub, angle, axis, strDep, finDep)
|
||||
removalshapes.append((obj.removalshape, False, '3DPocket', 0.0, 'X', strDep, finDep))
|
||||
else:
|
||||
for sub in base[1]:
|
||||
if "Face" in sub:
|
||||
@@ -109,15 +146,62 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
|
||||
env = PathUtils.getEnvelope(base[0].Shape, subshape=shape, depthparams=self.depthparams)
|
||||
obj.removalshape = env.cut(base[0].Shape)
|
||||
obj.removalshape.tessellate(0.1)
|
||||
|
||||
removalshapes.append((obj.removalshape, False))
|
||||
|
||||
else: # process the job base object as a whole
|
||||
PathLog.debug("processing the whole job base object")
|
||||
strDep = obj.StartDepth.Value
|
||||
finDep = obj.FinalDepth.Value
|
||||
recomputeDepthparams = False
|
||||
for base in self.model:
|
||||
env = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthparams)
|
||||
obj.removalshape = env.cut(base.Shape)
|
||||
obj.removalshape.tessellate(0.1)
|
||||
removalshapes.append((obj.removalshape, False))
|
||||
if obj.OpFinalDepth == obj.FinalDepth:
|
||||
if base.Shape.BoundBox.ZMin < obj.FinalDepth.Value:
|
||||
obj.FinalDepth.Value = base.Shape.BoundBox.ZMin
|
||||
finDep = base.Shape.BoundBox.ZMin
|
||||
recomputeDepthparams = True
|
||||
PathLog.info("Updated obj.FinalDepth.Value to {}".format(finDep))
|
||||
if obj.OpStartDepth == obj.StartDepth:
|
||||
if base.Shape.BoundBox.ZMax > obj.StartDepth.Value:
|
||||
obj.StartDepth.Value = base.Shape.BoundBox.ZMax
|
||||
finDep = base.Shape.BoundBox.ZMax
|
||||
recomputeDepthparams = True
|
||||
PathLog.info("Updated obj.StartDepth.Value to {}".format(strDep))
|
||||
if recomputeDepthparams is True:
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
self.depthparams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=obj.StartDepth.Value,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=obj.FinalDepth.Value,
|
||||
user_depths=None)
|
||||
recomputeDepthparams = False
|
||||
|
||||
if obj.ProcessStockArea is True:
|
||||
job = PathUtils.findParentJob(obj)
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
|
||||
depthparams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=obj.StartDepth.Value,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=base.Shape.BoundBox.ZMin,
|
||||
user_depths=None)
|
||||
stockEnvShape = PathUtils.getEnvelope(job.Stock.Shape, subshape=None, depthparams=depthparams)
|
||||
|
||||
obj.removalshape = stockEnvShape.cut(base.Shape)
|
||||
obj.removalshape.tessellate(0.1)
|
||||
else:
|
||||
env = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthparams)
|
||||
obj.removalshape = env.cut(base.Shape)
|
||||
obj.removalshape.tessellate(0.1)
|
||||
|
||||
removalshapes.append((obj.removalshape, False, '3DPocket', 0.0, 'X', strDep, finDep))
|
||||
|
||||
return removalshapes
|
||||
|
||||
def areaOpSetDefaultValues(self, obj, job):
|
||||
@@ -125,6 +209,563 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
|
||||
obj.StepOver = 100
|
||||
obj.ZigZagAngle = 45
|
||||
obj.HandleMultipleFeatures = 'Collectively'
|
||||
obj.AdaptivePocketStart = False
|
||||
obj.AdaptivePocketFinish = False
|
||||
obj.ProcessStockArea = False
|
||||
|
||||
# methods for eliminating air milling with some pockets: adpative start and finish
|
||||
def calculateAdaptivePocket(self, obj, base, subObjTups):
|
||||
'''calculateAdaptivePocket(obj, base, subObjTups)
|
||||
Orient multiple faces around common facial center of mass.
|
||||
Identify edges that are connections for adjacent faces.
|
||||
Attempt to separate unconnected edges into top and bottom loops of the pocket.
|
||||
Trim the top and bottom of the pocket if available and requested.
|
||||
return: tuple with pocket shape information'''
|
||||
low = []
|
||||
high = []
|
||||
removeList = []
|
||||
Faces = []
|
||||
allEdges = []
|
||||
makeHighFace = 0
|
||||
tryNonPlanar = False
|
||||
isHighFacePlanar = True
|
||||
isLowFacePlanar = True
|
||||
faceType = 0
|
||||
|
||||
for (sub, face) in subObjTups:
|
||||
Faces.append(face)
|
||||
|
||||
# identify max and min face heights for top loop
|
||||
(zmin, zmax) = self.getMinMaxOfFaces(Faces)
|
||||
|
||||
# Order faces around common center of mass
|
||||
subObjTups = self.orderFacesAroundCenterOfMass(subObjTups)
|
||||
# find connected edges and map to edge names of base
|
||||
(connectedEdges, touching) = self.findSharedEdges(subObjTups)
|
||||
(low, high) = self.identifyUnconnectedEdges(subObjTups, touching)
|
||||
|
||||
if len(high) > 0 and obj.AdaptivePocketStart is True:
|
||||
# attempt planar face with top edges of pocket
|
||||
allEdges = []
|
||||
makeHighFace = 0
|
||||
tryNonPlanar = False
|
||||
for (sub, face, ei) in high:
|
||||
allEdges.append(face.Edges[ei])
|
||||
|
||||
(hzmin, hzmax) = self.getMinMaxOfFaces(allEdges)
|
||||
|
||||
try:
|
||||
highFaceShape = Part.Face(Part.Wire(Part.__sortEdges__(allEdges)))
|
||||
except Exception as ee:
|
||||
PathLog.warning(ee)
|
||||
PathLog.error(translate("Path", "A planar adaptive start is unavailable. The non-planar will be attempted."))
|
||||
tryNonPlanar = True
|
||||
else:
|
||||
makeHighFace = 1
|
||||
|
||||
if tryNonPlanar is True:
|
||||
try:
|
||||
highFaceShape = Part.makeFilledFace(Part.__sortEdges__(allEdges)) # NON-planar face method
|
||||
except Exception as eee:
|
||||
PathLog.warning(eee)
|
||||
PathLog.error(translate("Path", "The non-planar adaptive start is also unavailable.") + "(1)")
|
||||
isHighFacePlanar = False
|
||||
else:
|
||||
makeHighFace = 2
|
||||
|
||||
if makeHighFace > 0:
|
||||
FreeCAD.ActiveDocument.addObject('Part::Feature', 'topEdgeFace')
|
||||
highFace = FreeCAD.ActiveDocument.ActiveObject
|
||||
highFace.Shape = highFaceShape
|
||||
removeList.append(highFace.Name)
|
||||
|
||||
# verify non-planar face is within high edge loop Z-boundaries
|
||||
if makeHighFace == 2:
|
||||
mx = hzmax + obj.StepDown.Value
|
||||
mn = hzmin - obj.StepDown.Value
|
||||
if highFace.Shape.BoundBox.ZMax > mx or highFace.Shape.BoundBox.ZMin < mn:
|
||||
PathLog.warning("ZMaxDiff: {}; ZMinDiff: {}".format(highFace.Shape.BoundBox.ZMax - mx, highFace.Shape.BoundBox.ZMin - mn))
|
||||
PathLog.error(translate("Path", "The non-planar adaptive start is also unavailable.") + "(2)")
|
||||
isHighFacePlanar = False
|
||||
makeHighFace = 0
|
||||
else:
|
||||
isHighFacePlanar = False
|
||||
|
||||
if len(low) > 0 and obj.AdaptivePocketFinish is True:
|
||||
# attempt planar face with bottom edges of pocket
|
||||
allEdges = []
|
||||
for (sub, face, ei) in low:
|
||||
allEdges.append(face.Edges[ei])
|
||||
|
||||
# (lzmin, lzmax) = self.getMinMaxOfFaces(allEdges)
|
||||
|
||||
try:
|
||||
lowFaceShape = Part.Face(Part.Wire(Part.__sortEdges__(allEdges)))
|
||||
# lowFaceShape = Part.makeFilledFace(Part.__sortEdges__(allEdges)) # NON-planar face method
|
||||
except Exception as ee:
|
||||
PathLog.error(ee)
|
||||
PathLog.error("An adaptive finish is unavailable.")
|
||||
isLowFacePlanar = False
|
||||
else:
|
||||
FreeCAD.ActiveDocument.addObject('Part::Feature', 'bottomEdgeFace')
|
||||
lowFace = FreeCAD.ActiveDocument.ActiveObject
|
||||
lowFace.Shape = lowFaceShape
|
||||
removeList.append(lowFace.Name)
|
||||
else:
|
||||
isLowFacePlanar = False
|
||||
|
||||
# Start with a regular pocket envelope
|
||||
strDep = obj.StartDepth.Value
|
||||
finDep = obj.FinalDepth.Value
|
||||
cuts = []
|
||||
starts = []
|
||||
finals = []
|
||||
starts.append(obj.StartDepth.Value)
|
||||
finals.append(zmin)
|
||||
if obj.AdaptivePocketStart is True or len(subObjTups) == 1:
|
||||
strDep = zmax + obj.StepDown.Value
|
||||
starts.append(zmax + obj.StepDown.Value)
|
||||
|
||||
finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
|
||||
depthparams = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep,
|
||||
user_depths=None)
|
||||
shape = Part.makeCompound(Faces)
|
||||
env = PathUtils.getEnvelope(base[0].Shape, subshape=shape, depthparams=depthparams)
|
||||
cuts.append(env.cut(base[0].Shape))
|
||||
|
||||
# Might need to change to .cut(job.Stock.Shape) if pocket has no bottom
|
||||
# job = PathUtils.findParentJob(obj)
|
||||
# envBody = env.cut(job.Stock.Shape)
|
||||
|
||||
if isHighFacePlanar is True and len(subObjTups) > 1:
|
||||
starts.append(hzmax + obj.StepDown.Value)
|
||||
# make shape to trim top of reg pocket
|
||||
strDep1 = obj.StartDepth.Value + (hzmax - hzmin)
|
||||
if makeHighFace == 1:
|
||||
# Planar face
|
||||
finDep1 = highFace.Shape.BoundBox.ZMin + obj.StepDown.Value
|
||||
else:
|
||||
# Non-Planar face
|
||||
finDep1 = hzmin + obj.StepDown.Value
|
||||
depthparams1 = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep1,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep1,
|
||||
user_depths=None)
|
||||
envTop = PathUtils.getEnvelope(base[0].Shape, subshape=highFace.Shape, depthparams=depthparams1)
|
||||
cbi = len(cuts) - 1
|
||||
cuts.append(cuts[cbi].cut(envTop))
|
||||
|
||||
if isLowFacePlanar is True and len(subObjTups) > 1:
|
||||
# make shape to trim top of pocket
|
||||
if makeHighFace == 1:
|
||||
# Planar face
|
||||
strDep2 = lowFace.Shape.BoundBox.ZMax
|
||||
else:
|
||||
# Non-Planar face
|
||||
strDep2 = hzmax
|
||||
finDep2 = obj.FinalDepth.Value
|
||||
depthparams2 = PathUtils.depth_params(
|
||||
clearance_height=obj.ClearanceHeight.Value,
|
||||
safe_height=obj.SafeHeight.Value,
|
||||
start_depth=strDep2,
|
||||
step_down=obj.StepDown.Value,
|
||||
z_finish_step=finish_step,
|
||||
final_depth=finDep2,
|
||||
user_depths=None)
|
||||
envBottom = PathUtils.getEnvelope(base[0].Shape, subshape=lowFace.Shape, depthparams=depthparams2)
|
||||
cbi = len(cuts) - 1
|
||||
cuts.append(cuts[cbi].cut(envBottom))
|
||||
|
||||
# package pocket details into tuple
|
||||
sdi = len(starts) - 1
|
||||
fdi = len(finals) - 1
|
||||
cbi = len(cuts) - 1
|
||||
pocket = (cuts[cbi], False, '3DPocket', 0.0, 'X', starts[sdi], finals[fdi])
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
for rn in removeList:
|
||||
FreeCADGui.ActiveDocument.getObject(rn).Visibility = False
|
||||
|
||||
for rn in removeList:
|
||||
FreeCAD.ActiveDocument.getObject(rn).purgeTouched()
|
||||
self.tempObjectNames.append(rn)
|
||||
return pocket
|
||||
|
||||
def orderFacesAroundCenterOfMass(self, subObjTups):
|
||||
'''orderFacesAroundCenterOfMass(subObjTups)
|
||||
Order list of faces by center of mass in angular order around
|
||||
average center of mass for all faces. Positive X-axis is zero degrees.
|
||||
return: subObjTups [ordered/sorted]'''
|
||||
import math
|
||||
newList = []
|
||||
vectList = []
|
||||
comList = []
|
||||
sortList = []
|
||||
subCnt = 0
|
||||
sumCom = FreeCAD.Vector(0.0, 0.0, 0.0)
|
||||
avgCom = FreeCAD.Vector(0.0, 0.0, 0.0)
|
||||
|
||||
def getDrctn(vectItem):
|
||||
return vectItem[3]
|
||||
|
||||
def getFaceIdx(sub):
|
||||
return int(sub.replace('Face', '')) - 1
|
||||
|
||||
# get CenterOfMass for each face and add to sumCenterOfMass for average calculation
|
||||
for (sub, face) in subObjTups:
|
||||
# for (bsNm, fIdx, eIdx, vIdx) in bfevList:
|
||||
# face = FreeCAD.ActiveDocument.getObject(bsNm).Shape.Faces[fIdx]
|
||||
subCnt += 1
|
||||
com = face.CenterOfMass
|
||||
comList.append((sub, face, com))
|
||||
sumCom = sumCom.add(com) # add sub COM to sum
|
||||
|
||||
# Calculate average CenterOfMass for all faces combined
|
||||
avgCom.x = sumCom.x / subCnt
|
||||
avgCom.y = sumCom.y / subCnt
|
||||
avgCom.z = sumCom.z / subCnt
|
||||
|
||||
# calculate vector (mag, direct) for each face from avgCom
|
||||
for (sub, face, com) in comList:
|
||||
adjCom = com.sub(avgCom) # effectively treats avgCom as origin for each face.
|
||||
mag = math.sqrt(adjCom.x**2 + adjCom.y**2) # adjCom.Length without Z values
|
||||
drctn = 0.0
|
||||
# Determine direction of vector
|
||||
if adjCom.x > 0.0:
|
||||
if adjCom.y > 0.0: # Q1
|
||||
drctn = math.degrees(math.atan(adjCom.y/adjCom.x))
|
||||
elif adjCom.y < 0.0:
|
||||
drctn = -math.degrees(math.atan(adjCom.x/adjCom.y)) + 270.0
|
||||
elif adjCom.y == 0.0:
|
||||
drctn = 0.0
|
||||
elif adjCom.x < 0.0:
|
||||
if adjCom.y < 0.0:
|
||||
drctn = math.degrees(math.atan(adjCom.y/adjCom.x)) + 180.0
|
||||
elif adjCom.y > 0.0:
|
||||
drctn = -math.degrees(math.atan(adjCom.x/adjCom.y)) + 90.0
|
||||
elif adjCom.y == 0.0:
|
||||
drctn = 180.0
|
||||
elif adjCom.x == 0.0:
|
||||
if adjCom.y < 0.0:
|
||||
drctn = 270.0
|
||||
elif adjCom.y > 0.0:
|
||||
drctn = 90.0
|
||||
vectList.append((sub, face, mag, drctn))
|
||||
|
||||
# Sort faces by directional component of vector
|
||||
sortList = sorted(vectList, key=getDrctn)
|
||||
|
||||
# remove magnitute and direction values
|
||||
for (sub, face, mag, drctn) in sortList:
|
||||
newList.append((sub, face))
|
||||
|
||||
# Rotate list items so highest face is first
|
||||
zmax = newList[0][1].BoundBox.ZMax
|
||||
idx = 0
|
||||
for i in range(0, len(newList)):
|
||||
(sub, face) = newList[i]
|
||||
fIdx = getFaceIdx(sub)
|
||||
# face = FreeCAD.ActiveDocument.getObject(bsNm).Shape.Faces[fIdx]
|
||||
if face.BoundBox.ZMax > zmax:
|
||||
zmax = face.BoundBox.ZMax
|
||||
idx = i
|
||||
if face.BoundBox.ZMax == zmax:
|
||||
if fIdx < getFaceIdx(newList[idx][0]):
|
||||
idx = i
|
||||
if idx > 0:
|
||||
for z in range(0, idx):
|
||||
newList.append(newList.pop(0))
|
||||
|
||||
return newList
|
||||
|
||||
def findSharedEdges(self, subObjTups):
|
||||
'''findSharedEdges(self, subObjTups)
|
||||
Find connected edges given a group of faces'''
|
||||
checkoutList = []
|
||||
searchedList = []
|
||||
shared = []
|
||||
touching = {}
|
||||
touchingCleaned = {}
|
||||
|
||||
# Prepare dictionary for edges in shared
|
||||
for (sub, face) in subObjTups:
|
||||
touching[sub] = []
|
||||
|
||||
# prepare list of indexes as proxies for subObjTups items
|
||||
numFaces = len(subObjTups)
|
||||
for nf in range(0, numFaces):
|
||||
checkoutList.append(nf)
|
||||
|
||||
for co in range(0, len(checkoutList)):
|
||||
if len(checkoutList) < 2:
|
||||
break
|
||||
|
||||
# Checkout first sub for analysis
|
||||
checkedOut1 = checkoutList.pop()
|
||||
searchedList.append(checkedOut1)
|
||||
(sub1, face1) = subObjTups[checkedOut1]
|
||||
|
||||
# Compare checked out sub to others for shared
|
||||
for co in range(0, len(checkoutList)):
|
||||
# Checkout second sub for analysis
|
||||
(sub2, face2) = subObjTups[co]
|
||||
|
||||
# analyze two subs for common faces
|
||||
for ei1 in range(0, len(face1.Edges)):
|
||||
edg1 = face1.Edges[ei1]
|
||||
for ei2 in range(0, len(face2.Edges)):
|
||||
edg2 = face2.Edges[ei2]
|
||||
if edg1.isSame(edg2) is True:
|
||||
PathLog.debug("{}.Edges[{}] connects at {}.Edges[{}]".format(sub1, ei1, sub2, ei2))
|
||||
shared.append((sub1, face1, ei1))
|
||||
touching[sub1].append(ei1)
|
||||
touching[sub2].append(ei2)
|
||||
# Efor
|
||||
# Remove duplicates from edge lists
|
||||
for sub in touching:
|
||||
touchingCleaned[sub] = []
|
||||
for s in touching[sub]:
|
||||
if s not in touchingCleaned[sub]:
|
||||
touchingCleaned[sub].append(s)
|
||||
|
||||
return (shared, touchingCleaned)
|
||||
|
||||
def identifyUnconnectedEdges(self, subObjTups, touching):
|
||||
'''identifyUnconnectedEdges(subObjTups, touching)
|
||||
Categorize unconnected edges into two groups, if possible: low and high'''
|
||||
# Identify unconnected edges
|
||||
# (should be top edge loop if all faces form loop with bottom face(s) included)
|
||||
high = []
|
||||
low = []
|
||||
holding = []
|
||||
|
||||
for (sub, face) in subObjTups:
|
||||
holding = []
|
||||
for ei in range(0, len(face.Edges)):
|
||||
if ei not in touching[sub]:
|
||||
holding.append((sub, face, ei))
|
||||
# Assign unconnected edges based upon category: high or low
|
||||
if len(holding) == 1:
|
||||
high.append(holding.pop())
|
||||
elif len(holding) == 2:
|
||||
edg0 = holding[0][1].Edges[holding[0][2]]
|
||||
edg1 = holding[1][1].Edges[holding[1][2]]
|
||||
if self.hasCommonVertex(edg0, edg1, show=False) < 0:
|
||||
# Edges not connected - probably top and bottom if faces in loop
|
||||
if edg0.CenterOfMass.z > edg1.CenterOfMass.z:
|
||||
high.append(holding[0])
|
||||
low.append(holding[1])
|
||||
else:
|
||||
high.append(holding[1])
|
||||
low.append(holding[0])
|
||||
else:
|
||||
# Edges are connected - all top, or all bottom edges
|
||||
com = FreeCAD.Vector(0, 0, 0)
|
||||
com.add(edg0.CenterOfMass)
|
||||
com.add(edg1.CenterOfMass)
|
||||
avgCom = FreeCAD.Vector(com.x/2.0, com.y/2.0, com.z/2.0)
|
||||
if avgCom.z > face.CenterOfMass.z:
|
||||
high.extend(holding)
|
||||
else:
|
||||
low.extend(holding)
|
||||
elif len(holding) > 2:
|
||||
# attempt to break edges into two groups of connected edges.
|
||||
# determine which group has higher center of mass, and assign as high, the other as low
|
||||
(lw, hgh) = self.groupConnectedEdges(holding)
|
||||
low.extend(lw)
|
||||
high.extend(hgh)
|
||||
# Eif
|
||||
# Efor
|
||||
return (low, high)
|
||||
|
||||
def hasCommonVertex(self, edge1, edge2, show=False):
|
||||
'''findCommonVertexIndexes(edge1, edge2, show=False)
|
||||
Compare vertexes of two edges to identify a common vertex.
|
||||
Returns the vertex index of edge1 to which edge2 is connected'''
|
||||
if show is True:
|
||||
PathLog.info("New findCommonVertex()... ")
|
||||
|
||||
oIdx = 0
|
||||
listOne = edge1.Vertexes
|
||||
listTwo = edge2.Vertexes
|
||||
|
||||
# Find common vertexes
|
||||
for o in listOne:
|
||||
if show is True:
|
||||
PathLog.info(" one ({}, {}, {})".format(o.X, o.Y, o.Z))
|
||||
for t in listTwo:
|
||||
if show is True:
|
||||
PathLog.error("two ({}, {}, {})".format(t.X, t.Y, t.Z))
|
||||
if o.X == t.X:
|
||||
if o.Y == t.Y:
|
||||
if o.Z == t.Z:
|
||||
if show is True:
|
||||
PathLog.info("found")
|
||||
return oIdx
|
||||
oIdx += 1
|
||||
return -1
|
||||
|
||||
def groupConnectedEdges(self, holding):
|
||||
'''groupConnectedEdges(self, holding)
|
||||
Take edges and determine which are connected.
|
||||
Group connected chains/loops into: low and high'''
|
||||
holds = []
|
||||
grps = []
|
||||
searched = []
|
||||
stop = False
|
||||
attachments = []
|
||||
loops = 1
|
||||
|
||||
def updateAttachments(grps):
|
||||
atchmnts = []
|
||||
lenGrps = len(grps)
|
||||
if lenGrps > 0:
|
||||
lenG0 = len(grps[0])
|
||||
if lenG0 < 2:
|
||||
atchmnts.append((0, 0))
|
||||
else:
|
||||
atchmnts.append((0, 0))
|
||||
atchmnts.append((0, lenG0 - 1))
|
||||
if lenGrps == 2:
|
||||
lenG1 = len(grps[1])
|
||||
if lenG1 < 2:
|
||||
atchmnts.append((1, 0))
|
||||
else:
|
||||
atchmnts.append((1, 0))
|
||||
atchmnts.append((1, lenG1 - 1))
|
||||
return atchmnts
|
||||
|
||||
def isSameVertex(o, t):
|
||||
if o.X == t.X:
|
||||
if o.Y == t.Y:
|
||||
if o.Z == t.Z:
|
||||
return True
|
||||
return False
|
||||
|
||||
for hi in range(0, len(holding)):
|
||||
holds.append(hi)
|
||||
|
||||
# Place initial edge in first group and update attachments
|
||||
h0 = holds.pop()
|
||||
grps.append([h0])
|
||||
attachments = updateAttachments(grps)
|
||||
|
||||
while len(holds) > 0:
|
||||
if loops > 500:
|
||||
PathLog.error('BREAK --- LOOPS LIMIT of 500 ---')
|
||||
break
|
||||
save = False
|
||||
|
||||
h2 = holds.pop()
|
||||
(sub2, face2, ei2) = holding[h2]
|
||||
|
||||
# Cycle through attachments for connection to existing
|
||||
for (g, t) in attachments:
|
||||
h1 = grps[g][t]
|
||||
(sub1, face1, ei1) = holding[h1]
|
||||
|
||||
edg1 = face1.Edges[ei1]
|
||||
edg2 = face2.Edges[ei2]
|
||||
|
||||
# CV = self.hasCommonVertex(edg1, edg2, show=False)
|
||||
|
||||
# Check attachment based on attachments order
|
||||
if t == 0:
|
||||
# is last vertex of h2 == first vertex of h1
|
||||
e2lv = len(edg2.Vertexes) - 1
|
||||
one = edg2.Vertexes[e2lv]
|
||||
two = edg1.Vertexes[0]
|
||||
if isSameVertex(one, two) is True:
|
||||
# Connected, insert h1 in front of h2
|
||||
grps[g].insert(0, h2)
|
||||
stop = True
|
||||
else:
|
||||
# is last vertex of h1 == first vertex of h2
|
||||
e1lv = len(edg1.Vertexes) - 1
|
||||
one = edg1.Vertexes[e1lv]
|
||||
two = edg2.Vertexes[0]
|
||||
if isSameVertex(one, two) is True:
|
||||
# Connected, append h1 after h2
|
||||
grps[g].append(h2)
|
||||
stop = True
|
||||
|
||||
if stop is True:
|
||||
# attachment was found
|
||||
attachments = updateAttachments(grps)
|
||||
holds.extend(searched)
|
||||
stop = False
|
||||
break
|
||||
else:
|
||||
# no attachment found
|
||||
save = True
|
||||
# Efor
|
||||
if save is True:
|
||||
searched.append(h2)
|
||||
if len(holds) == 0:
|
||||
if len(grps) == 1:
|
||||
h0 = searched.pop(0)
|
||||
grps.append([h0])
|
||||
attachments = updateAttachments(grps)
|
||||
holds.extend(searched)
|
||||
# Eif
|
||||
loops += 1
|
||||
# Ewhile
|
||||
|
||||
low = []
|
||||
high = []
|
||||
if len(grps) == 1:
|
||||
grps.append([])
|
||||
grp0 = []
|
||||
grp1 = []
|
||||
com0 = FreeCAD.Vector(0, 0, 0)
|
||||
com1 = FreeCAD.Vector(0, 0, 0)
|
||||
if len(grps[0]) > 0:
|
||||
for g in grps[0]:
|
||||
grp0.append(holding[g])
|
||||
(sub, face, ei) = holding[g]
|
||||
com0 = com0.add(face.Edges[ei].CenterOfMass)
|
||||
com0z = com0.z / len(grps[0])
|
||||
if len(grps[1]) > 0:
|
||||
for g in grps[1]:
|
||||
grp1.append(holding[g])
|
||||
(sub, face, ei) = holding[g]
|
||||
com1 = com1.add(face.Edges[ei].CenterOfMass)
|
||||
com1z = com1.z / len(grps[1])
|
||||
|
||||
if len(grps[1]) > 0:
|
||||
if com0z > com1z:
|
||||
low = grp1
|
||||
high = grp0
|
||||
else:
|
||||
low = grp0
|
||||
high = grp1
|
||||
else:
|
||||
low = grp0
|
||||
high = grp0
|
||||
|
||||
return (low, high)
|
||||
|
||||
def getMinMaxOfFaces(self, Faces):
|
||||
'''getMinMaxOfFaces(Faces)
|
||||
return the zmin and zmax values for given set of faces or edges.'''
|
||||
zmin = Faces[0].BoundBox.ZMax
|
||||
zmax = Faces[0].BoundBox.ZMin
|
||||
for f in Faces:
|
||||
if f.BoundBox.ZMin < zmin:
|
||||
zmin = f.BoundBox.ZMin
|
||||
if f.BoundBox.ZMax > zmax:
|
||||
zmax = f.BoundBox.ZMax
|
||||
return (zmin, zmax)
|
||||
|
||||
|
||||
def SetupProperties():
|
||||
|
||||
Reference in New Issue
Block a user