Merge pull request #2366 from Russ4262/3D_Pocket_upgrade

[Path] 3D Pocket: upgrade to adaptive start and finish
This commit is contained in:
sliptonic
2019-07-24 08:21:52 -05:00
committed by GitHub
2 changed files with 664 additions and 30 deletions

View File

@@ -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

View File

@@ -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():