lazy_loader is copied to Ext now, modified external imports to lazy_load add a few more imports to be lazy loaded, think the install path is correct now [TD]"<" symbol embedded in html revert changes to path modules for testing use lazyloader in PathAreaOp.py add back in deferred loading temp change to print error message in tests temp change to print error message in tests add _init__.py to lazy_loader make install in CMakeLists.txt one line
787 lines
33 KiB
Python
787 lines
33 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 PathScripts.PathLog as PathLog
|
|
import PathScripts.PathOp as PathOp
|
|
import PathScripts.PathPocketBase as PathPocketBase
|
|
import PathScripts.PathUtils as PathUtils
|
|
|
|
from PySide import QtCore
|
|
|
|
# lazily loaded modules
|
|
from lazy_loader.lazy_loader import LazyLoader
|
|
Part = LazyLoader('Part', globals(), 'Part')
|
|
|
|
__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__ = "2e"
|
|
__lastModified__ = "2020-02-13 17:22 CST"
|
|
|
|
PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule())
|
|
# PathLog.trackModule(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':
|
|
(fzmin, fzmax) = self.getMinMaxOfFaces(Faces)
|
|
if obj.FinalDepth.Value < fzmin:
|
|
PathLog.warning(translate('PathPocket', 'Final depth set below ZMin of face(s) selected.'))
|
|
'''
|
|
if obj.OpFinalDepth == obj.FinalDepth:
|
|
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)
|
|
'''
|
|
stockEnvShape = PathUtils.getEnvelope(job.Stock.Shape, subshape=None, depthparams=self.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
|
|
|
|
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
|