freecad-cam/Mod/CAM/Path/Op/Pocket.py
2026-02-01 01:59:24 +01:00

898 lines
35 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 *
# * *
# ***************************************************************************
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import Part
import Path
import Path.Op.Base as PathOp
import Path.Op.PocketBase as PathPocketBase
import PathScripts.PathUtils as PathUtils
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
__title__ = "CAM 3D Pocket Operation"
__author__ = "Yorik van Havre <yorik@uncreated.net>"
__url__ = "https://www.freecad.org"
__doc__ = "Class and implementation of the 3D Pocket operation."
__created__ = "2014"
if False:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
translate = FreeCAD.Qt.translate
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",
QT_TRANSLATE_NOOP(
"App::Property",
"Choose how to process multiple Base Geometry features.",
),
)
if not hasattr(obj, "AdaptivePocketStart"):
obj.addProperty(
"App::PropertyBool",
"AdaptivePocketStart",
"Pocket",
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",
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",
QT_TRANSLATE_NOOP(
"App::Property",
"Process the model and stock in an operation with no Base Geometry selected.",
),
)
# populate the property enumerations
for n in self.propertyEnumerations():
setattr(obj, n[0], n[1])
@classmethod
def propertyEnumerations(self, dataType="data"):
"""propertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
Args:
dataType = 'data', 'raw', 'translated'
Notes:
'data' is list of internal string literals used in code
'raw' is list of (translated_text, data_string) tuples
'translated' is list of translated string literals
"""
enums = {
"HandleMultipleFeatures": [
(translate("CAM_Pocket", "Collectively"), "Collectively"),
(translate("CAM_Pocket", "Individually"), "Individually"),
],
}
if dataType == "raw":
return enums
data = list()
idx = 0 if dataType == "translated" else 1
Path.Log.debug(enums)
for k, v in enumerate(enums):
data.append((v, [tup[idx] for tup in enums[v]]))
Path.Log.debug(data)
return data
def opOnDocumentRestored(self, obj):
"""opOnDocumentRestored(obj) ... adds the properties if they doesn't exist."""
super().opOnDocumentRestored(obj)
self.initPocketOp(obj)
def pocketInvertExtraOffset(self):
return False
def opUpdateDepths(self, obj):
"""opUpdateDepths(obj) ... Implement special depths calculation."""
# Set Final Depth to bottom of model if whole model is used
if not obj.Base or len(obj.Base) == 0:
if len(self.job.Model.Group) == 1:
finDep = self.job.Model.Group[0].Shape.BoundBox.ZMin
else:
finDep = min([m.Shape.BoundBox.ZMin for m in self.job.Model.Group])
obj.setExpression("OpFinalDepth", "{} mm".format(finDep))
def areaOpShapes(self, obj):
"""areaOpShapes(obj) ... return shapes representing the solids to be removed."""
Path.Log.track()
subObjTups = []
removalshapes = []
if obj.Base:
Path.Log.debug("base items exist. Processing... ")
for base in obj.Base:
Path.Log.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:
Path.Log.warning(
translate(
"CAM",
"Final depth set below ZMin of face(s) selected.",
)
)
if obj.AdaptivePocketStart is True or obj.AdaptivePocketFinish is True:
pocketTup = self.calculateAdaptivePocket(obj, base, subObjTups)
if pocketTup is not False:
obj.removalshape = pocketTup[0]
removalshapes.append(pocketTup) # (shape, isHole, detail)
else:
shape = Part.makeCompound(Faces)
env = PathUtils.getEnvelope(
base[0].Shape, subshape=shape, depthparams=self.depthparams
)
rawRemovalShape = env.cut(base[0].Shape)
faceExtrusions = [f.extrude(FreeCAD.Vector(0.0, 0.0, 1.0)) for f in Faces]
obj.removalshape = _identifyRemovalSolids(rawRemovalShape, faceExtrusions)
removalshapes.append(
(obj.removalshape, False, "3DPocket")
) # (shape, isHole, detail)
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
)
rawRemovalShape = env.cut(base[0].Shape)
faceExtrusions = [shape.extrude(FreeCAD.Vector(0.0, 0.0, 1.0))]
obj.removalshape = _identifyRemovalSolids(rawRemovalShape, faceExtrusions)
removalshapes.append((obj.removalshape, False, "3DPocket"))
else: # process the job base object as a whole
Path.Log.debug("processing the whole job base object")
for base in self.model:
if obj.ProcessStockArea is True:
job = PathUtils.findParentJob(obj)
stockEnvShape = PathUtils.getEnvelope(
job.Stock.Shape, subshape=None, depthparams=self.depthparams
)
rawRemovalShape = stockEnvShape.cut(base.Shape)
else:
env = PathUtils.getEnvelope(
base.Shape, subshape=None, depthparams=self.depthparams
)
rawRemovalShape = env.cut(base.Shape)
# Identify target removal shapes after cutting envelope with base shape
removalSolids = [
s
for s in rawRemovalShape.Solids
if Path.Geom.isRoughly(s.BoundBox.ZMax, rawRemovalShape.BoundBox.ZMax)
]
# Fuse multiple solids
if len(removalSolids) > 1:
seed = removalSolids[0]
for tt in removalSolids[1:]:
fusion = seed.fuse(tt)
seed = fusion
removalShape = seed
else:
removalShape = removalSolids[0]
obj.removalshape = removalShape
removalshapes.append((obj.removalshape, False, "3DPocket"))
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: adaptive 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:
Path.Log.warning(ee)
Path.Log.error(
translate(
"CAM",
"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:
Path.Log.warning(eee)
Path.Log.error(
translate("CAM", "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:
Path.Log.warning(
"ZMaxDiff: {}; ZMinDiff: {}".format(
highFace.Shape.BoundBox.ZMax - mx,
highFace.Shape.BoundBox.ZMin - mn,
)
)
Path.Log.error(
translate("CAM", "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:
Path.Log.error(ee)
Path.Log.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
cbi = len(cuts) - 1
pocket = (cuts[cbi], False, "3DPocket")
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:
Path.Log.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:
Path.Log.info("New findCommonVertex()... ")
oIdx = 0
listOne = edge1.Vertexes
listTwo = edge2.Vertexes
# Find common vertexes
for o in listOne:
if show is True:
Path.Log.info(" one ({}, {}, {})".format(o.X, o.Y, o.Z))
for t in listTwo:
if show is True:
Path.Log.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:
Path.Log.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:
Path.Log.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 _identifyRemovalSolids(sourceShape, commonShapes):
"""_identifyRemovalSolids(sourceShape, commonShapes)
Loops through solids in sourceShape to identify commonality with solids in commonShapes.
The sourceShape solids with commonality are returned as Part.Compound shape."""
common = Part.makeCompound(commonShapes)
removalSolids = [s for s in sourceShape.Solids if s.common(common).Volume > 0.0]
return Part.makeCompound(removalSolids)
def _extrudeBaseDown(base):
"""_extrudeBaseDown(base)
Extrudes and fuses all non-vertical faces downward to a level 1.0 mm below base ZMin."""
allExtrusions = list()
zMin = base.Shape.BoundBox.ZMin
bbFace = Path.Geom.makeBoundBoxFace(base.Shape.BoundBox, offset=5.0)
bbFace.translate(FreeCAD.Vector(0.0, 0.0, float(int(base.Shape.BoundBox.ZMin - 5.0))))
direction = FreeCAD.Vector(0.0, 0.0, -1.0)
# Make projections of each non-vertical face and extrude it
for f in base.Shape.Faces:
fbb = f.BoundBox
if not Path.Geom.isRoughly(f.normalAt(0, 0).z, 0.0):
pp = bbFace.makeParallelProjection(f.Wires[0], direction)
face = Part.Face(Part.Wire(pp.Edges))
face.translate(FreeCAD.Vector(0.0, 0.0, fbb.ZMin))
ext = face.extrude(FreeCAD.Vector(0.0, 0.0, zMin - fbb.ZMin - 1.0))
allExtrusions.append(ext)
# Fuse all extrusions together
seed = allExtrusions.pop()
fusion = seed.fuse(allExtrusions)
fusion.translate(FreeCAD.Vector(0.0, 0.0, zMin - fusion.BoundBox.ZMin - 1.0))
return fusion.cut(base.Shape)
def SetupProperties():
return PathPocketBase.SetupProperties() + ["HandleMultipleFeatures"]
def Create(name, obj=None, parentJob=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, parentJob)
return obj