apply algorithms to some pockets to eliminate air milling and adapt start and finish heights and paths to top and bottom of certain pockets. Depth correction & flake8 formatting use list to track successive cuts so variations of settings are tracked and applied correctly Apply faceType specific depth calculations Planar face requires one depth source, non-planar requires a different source.
781 lines
32 KiB
Python
781 lines
32 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
|
|
# * *
|
|
# * 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 *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
import FreeCAD
|
|
import Part
|
|
import PathScripts.PathLog as PathLog
|
|
import PathScripts.PathOp as PathOp
|
|
import PathScripts.PathPocketBase as PathPocketBase
|
|
import PathScripts.PathUtils as PathUtils
|
|
|
|
from PySide import QtCore
|
|
|
|
__title__ = "Path 3D Pocket Operation"
|
|
__author__ = "Yorik van Havre <yorik@uncreated.net>"
|
|
__url__ = "http://www.freecadweb.org"
|
|
__doc__ = "Class and implementation of the 3D Pocket operation."
|
|
__contributors__ = "russ4262 (Russell Johnson)"
|
|
__created__ = "2014"
|
|
__scriptVersion__ = "2g testing"
|
|
__lastModified__ = "2019-07-20 22:02 CST"
|
|
|
|
LOGLEVEL = False
|
|
|
|
if LOGLEVEL:
|
|
PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule())
|
|
PathLog.trackModule(PathLog.thisModule())
|
|
else:
|
|
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
|
|
|
|
|
# Qt translation handling
|
|
def translate(context, text, disambig=None):
|
|
return QtCore.QCoreApplication.translate(context, text, disambig)
|
|
|
|
|
|
class ObjectPocket(PathPocketBase.ObjectPocket):
|
|
'''Proxy object for Pocket operation.'''
|
|
|
|
def pocketOpFeatures(self, obj):
|
|
return PathOp.FeatureNoFinalDepth
|
|
|
|
def initPocketOp(self, obj):
|
|
'''initPocketOp(obj) ... setup receiver'''
|
|
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.'''
|
|
self.initPocketOp(obj)
|
|
|
|
def pocketInvertExtraOffset(self):
|
|
return False
|
|
|
|
def areaOpShapes(self, obj):
|
|
'''areaOpShapes(obj) ... return shapes representing the solids to be removed.'''
|
|
PathLog.track()
|
|
|
|
subObjTups = []
|
|
removalshapes = []
|
|
|
|
if obj.Base:
|
|
PathLog.debug("base items exist. Processing... ")
|
|
for base in obj.Base:
|
|
PathLog.debug("obj.Base item: {}".format(base))
|
|
|
|
# Check if all subs are faces
|
|
allSubsFaceType = True
|
|
Faces = []
|
|
for sub in base[1]:
|
|
if "Face" in sub:
|
|
face = getattr(base[0].Shape, sub)
|
|
Faces.append(face)
|
|
subObjTups.append((sub, face))
|
|
else:
|
|
allSubsFaceType = False
|
|
break
|
|
|
|
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:
|
|
shape = Part.makeCompound([getattr(base[0].Shape, sub)])
|
|
else:
|
|
edges = [getattr(base[0].Shape, sub) for sub in base[1]]
|
|
shape = Part.makeFace(edges, 'Part::FaceMakerSimple')
|
|
|
|
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:
|
|
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):
|
|
'''areaOpSetDefaultValues(obj, job) ... set default values'''
|
|
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():
|
|
return PathPocketBase.SetupProperties() + ["HandleMultipleFeatures"]
|
|
|
|
|
|
def Create(name, obj=None):
|
|
'''Create(name) ... Creates and returns a Pocket operation.'''
|
|
if obj is None:
|
|
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
|
obj.Proxy = ObjectPocket(obj, name)
|
|
return obj
|