Merge pull request #4253 from J-Dunn/master

[Path]: fix several minor bugs in arc slot ops
This commit is contained in:
sliptonic
2021-02-08 09:44:30 -06:00
committed by GitHub

View File

@@ -421,7 +421,7 @@ class ObjectSlot(PathOp.ObjectOp):
def _makeOperation(self, obj):
"""This method controls the overall slot creation process."""
pnts = False
featureCnt = 0
featureCount = 0
if not hasattr(obj, 'Base'):
msg = translate('PathSlot',
@@ -444,9 +444,9 @@ class ObjectSlot(PathOp.ObjectOp):
baseGeom = obj.Base[0]
base, subsList = baseGeom
self.base = base
lenSL = len(subsList)
featureCnt = lenSL
if lenSL == 1:
featureCount = len(subsList)
if featureCount == 1:
PathLog.debug('Reference 1: {}'.format(obj.Reference1))
sub1 = subsList[0]
shape_1 = getattr(base.Shape, sub1)
@@ -467,9 +467,9 @@ class ObjectSlot(PathOp.ObjectOp):
return False
if self.isArc:
cmds = self._finishArc(obj, pnts, featureCnt)
cmds = self._finishArc(obj, pnts, featureCount)
else:
cmds = self._finishLine(obj, pnts, featureCnt)
cmds = self._finishLine(obj, pnts, featureCount)
if cmds:
return cmds
@@ -477,7 +477,8 @@ class ObjectSlot(PathOp.ObjectOp):
return False
def _finishArc(self, obj, pnts, featureCnt):
"""This method finishes an Arc Slot operation."""
"""This method finishes an Arc Slot operation.
It returns the gcode for the slot operation."""
PathLog.debug('arc center: {}'.format(self.arcCenter))
self._addDebugObject(Part.makeLine(self.arcCenter, self.arcMidPnt), 'CentToMidPnt')
@@ -488,7 +489,7 @@ class ObjectSlot(PathOp.ObjectOp):
PathLog.debug('arc radius: {}; offset radius: {}'.format(self.arcRadius, newRadius))
if newRadius <= 0:
msg = translate('PathSlot',
'Current offset value is not possible.')
'Current Extend Radius value produces negative arc radius.')
FreeCAD.Console.PrintError(msg + '\n')
return False
else:
@@ -514,7 +515,9 @@ class ObjectSlot(PathOp.ObjectOp):
(p1, p2) = pnts
begExt = obj.ExtendPathStart.Value
endExt = obj.ExtendPathEnd.Value
pnts = self._extendArcSlot(p1, p2, self.arcCenter, begExt, endExt)
# invert endExt, begExt args to apply extentions to correct ends
# XY geom is postitive CCW; Gcode postitive CW
pnts = self._extendArcSlot(p1, p2, self.arcCenter, endExt, begExt)
if not pnts:
return False
@@ -541,51 +544,44 @@ class ObjectSlot(PathOp.ObjectOp):
return cmds
def _makeArcGCode(self, obj, p1, p2):
"""This method is the last in the overall slot creation process.
"""This method is the last step in the overall arc slot creation process.
It accepts the operation object and two end points for the path.
It returns the slot gcode for the operation."""
It returns the gcode for the slot operation."""
CMDS = list()
PATHS = [(p2, p1, 'G2'), (p1, p2, 'G3')]
path_index = 0
if obj.ReverseDirection :
path_index = 1
else :
path_index = 0
def arcPass(PNTS, depth):
def arcPass(POINTS, depth):
cmds = list()
(p1, p2, cmd) = PNTS
(st_pt, end_pt, arcCmd) = POINTS
# cmds.append(Path.Command('N (Tool type: {})'.format(toolType), {}))
cmds.append(Path.Command('G0', {'X': p1.x, 'Y': p1.y, 'F': self.horizRapid}))
cmds.append(Path.Command('G0', {'X': st_pt.x, 'Y': st_pt.y, 'F': self.horizRapid}))
cmds.append(Path.Command('G1', {'Z': depth, 'F': self.vertFeed}))
vtc = self.arcCenter.sub(p1) # vector to center
vtc = self.arcCenter.sub(st_pt) # vector to center
cmds.append(
Path.Command(cmd,
{'X': p2.x, 'Y': p2.y, 'I': vtc.x,
Path.Command(arcCmd,
{'X': end_pt.x, 'Y': end_pt.y, 'I': vtc.x,
'J': vtc.y, 'F': self.horizFeed
}))
return cmds
if obj.LayerMode == 'Single-pass':
if obj.ReverseDirection:
path_index = 1
CMDS.extend(arcPass(PATHS[path_index], obj.FinalDepth.Value))
else:
if obj.CutPattern == 'Line':
if obj.ReverseDirection:
path_index = 1
for dep in self.depthParams:
CMDS.extend(arcPass(PATHS[path_index], dep))
for depth in self.depthParams:
CMDS.extend(arcPass(PATHS[path_index], depth))
CMDS.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
elif obj.CutPattern == 'ZigZag':
i = 0
for dep in self.depthParams:
if obj.ReverseDirection:
if i % 2.0 == 0: # even
CMDS.extend(arcPass(PATHS[0], dep))
else: # odd
CMDS.extend(arcPass(PATHS[1], dep))
else:
if i % 2.0 == 0: # even
CMDS.extend(arcPass(PATHS[1], dep))
else: # odd
CMDS.extend(arcPass(PATHS[0], dep))
for depth in self.depthParams:
if i % 2.0 == 0: # even
CMDS.extend(arcPass(PATHS[path_index], depth))
else: # odd
CMDS.extend(arcPass(PATHS[not path_index], depth))
i += 1
# Raise to SafeHeight when finished
CMDS.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid}))
@@ -596,7 +592,8 @@ class ObjectSlot(PathOp.ObjectOp):
return CMDS
def _finishLine(self, obj, pnts, featureCnt):
"""This method finishes a Line Slot operation."""
"""This method finishes a Line Slot operation.
It returns the gcode for the line slot operation."""
# Apply perpendicular rotation if requested
perpZero = True
if obj.PathOrientation == 'Perpendicular':
@@ -667,9 +664,9 @@ class ObjectSlot(PathOp.ObjectOp):
return cmds
def _makeLineGCode(self, obj, p1, p2):
"""This method is the last in the overall slot creation process.
"""This method is the last in the overall line slot creation process.
It accepts the operation object and two end points for the path.
It returns the slot gcode for the operation."""
It returns the gcode for the slot operation."""
CMDS = list()
def linePass(p1, p2, depth):
@@ -708,7 +705,7 @@ class ObjectSlot(PathOp.ObjectOp):
def _processSingle(self, obj, shape_1, sub1):
"""This is the control method for slots based on a
single Base Geometry feature."""
make = False
done = False
cat1 = sub1[:4]
if cat1 == 'Face':
@@ -735,21 +732,21 @@ class ObjectSlot(PathOp.ObjectOp):
if pnts:
(p1, p2) = pnts
make = True
done = True
elif cat1 == 'Edge':
PathLog.debug('Single edge')
pnts = self._processSingleEdge(obj, shape_1)
if pnts:
(p1, p2) = pnts
make = True
done = True
elif cat1 == 'Vert':
msg = translate('PathSlot',
'Only a vertex selected. Add another feature to the Base Geometry.')
FreeCAD.Console.PrintError(msg + '\n')
if make:
if done:
return (p1, p2)
return False
@@ -762,7 +759,7 @@ class ObjectSlot(PathOp.ObjectOp):
def getRadians(self, E):
vect = self._dXdYdZ(E)
norm = self._normalizeVector(vect)
rads = self._xyToRadians(norm)
rads = self._getVectorAngle(norm)
deg = math.degrees(rads)
if deg >= 180.0:
deg -= 180.0
@@ -909,7 +906,7 @@ class ObjectSlot(PathOp.ObjectOp):
return (p1, p2)
def _processSingleEdge(self, obj, edge):
"""Determine slot path endpoints from a single horizontally oriented face."""
"""Determine slot path endpoints from a single horizontally oriented edge."""
PathLog.debug('_processSingleEdge()')
tolrnc = 0.0000001
lineTypes = ['Part::GeomLine']
@@ -927,42 +924,44 @@ class ObjectSlot(PathOp.ObjectOp):
def isHorizontal(z1, z2, z3):
# Check that all Z values are equal (isRoughly same)
if (abs(z1 - z2) > tolrnc or
abs(z1 - z3) > tolrnc or
abs(z2 - z3) > tolrnc):
abs(z1 - z3) > tolrnc ):
# abs(z2 - z3) > tolrnc): 3rd test reduntant.
return False
return True
def circleCentFrom3Points(P1, P2, P3):
def circumCircleFrom3Points(P1, P2, P3):
# Source code for this function copied from (with modifications):
# https://wiki.freecadweb.org/Macro_Draft_Circle_3_Points_3D
P1P2 = (P2 - P1).Length
P2P3 = (P3 - P2).Length
P3P1 = (P1 - P3).Length
vP2P1 = (P2 - P1)
vP3P2 = (P3 - P2)
vP1P3 = (P1 - P3)
# Circle radius.
l = ((P1 - P2).cross(P2 - P3)).Length
# r = P1P2 * P2P3 * P3P1 / 2 / l
if round(l, 8) == 0.0:
PathLog.error("The three points are aligned.")
L = vP2P1.cross(vP3P2).Length
# Circle radius (not used)
# r = vP1P2.Length * vP2P3.Length * vP3P1.Length / 2 / l
if round(L, 8) == 0.0:
PathLog.error("The three points are colinear, arc is a straight.")
return False
# Sphere center.
a = P2P3**2 * (P1 - P2).dot(P1 - P3) / 2 / l**2
b = P3P1**2 * (P2 - P1).dot(P2 - P3) / 2 / l**2
c = P1P2**2 * (P3 - P1).dot(P3 - P2) / 2 / l**2
P1.multiply(a)
P2.multiply(b)
P3.multiply(c)
PC = P1 + P2 + P3
return PC
twolsqr= 2*L*L
a = -vP3P2.dot(vP3P2) * vP2P1.dot(vP1P3) / twolsqr
b = -vP1P3.dot(vP1P3) * vP3P2.dot(vP2P1) / twolsqr
c = -vP2P1.dot(vP2P1) * vP1P3.dot(vP3P2) / twolsqr
return P1*a + P2*b + P3*c
V1 = edge.Vertexes[0]
p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0)
if len(edge.Vertexes) == 1: # circle has one virtex
p2 = FreeCAD.Vector(p1)
else :
V2 = edge.Vertexes[1]
p2 = FreeCAD.Vector(V2.X, V2.Y, 0.0)
# Process edge based on curve type
if edge.Curve.TypeId in lineTypes:
V1 = edge.Vertexes[0]
V2 = edge.Vertexes[1]
p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0)
p2 = FreeCAD.Vector(V2.X, V2.Y, 0.0)
return (p1, p2)
elif edge.Curve.TypeId in curveTypes:
if len(edge.Vertexes) == 1:
# Circle edge
@@ -971,47 +970,41 @@ class ObjectSlot(PathOp.ObjectOp):
return False
self.isArc = 1
V1 = edge.Vertexes[0]
tp1 = edge.valueAt(edge.getParameterByLength(edge.Length * 0.33))
tp2 = edge.valueAt(edge.getParameterByLength(edge.Length * 0.66))
if not isHorizontal(V1.Z, tp1.z, tp2.z):
return False
cent = edge.BoundBox.Center
self.arcCenter = FreeCAD.Vector(cent.x, cent.y, 0.0)
center = edge.BoundBox.Center
self.arcCenter = FreeCAD.Vector(center.x, center.y, 0.0)
midPnt = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0))
self.arcMidPnt = FreeCAD.Vector(midPnt.x, midPnt.y, 0.0)
self.arcRadius = edge.BoundBox.XLength / 2.0
p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0)
p2 = FreeCAD.Vector(V1.X, V1.Y, 0.0)
else:
# Arc edge
PathLog.debug('Arc with multiple vertices.')
self.isArc = 2
V1 = edge.Vertexes[0]
V2 = edge.Vertexes[1]
midPnt = edge.valueAt(edge.getParameterByLength(edge.Length / 2.0))
if not isHorizontal(V1.Z, V2.Z, midPnt.z):
return False
p1 = FreeCAD.Vector(V1.X, V1.Y, 0.0)
p2 = FreeCAD.Vector(V2.X, V2.Y, 0.0)
# Duplicate points required because
# circleCentFrom3Points() alters original arguments
pA = FreeCAD.Vector(V1.X, V1.Y, 0.0)
pB = FreeCAD.Vector(V2.X, V2.Y, 0.0)
pC = FreeCAD.Vector(midPnt.x, midPnt.y, 0.0)
cCF3P = circleCentFrom3Points(pA, pB, pC)
if not cCF3P:
midPnt.z = 0.0
circleCenter = circumCircleFrom3Points(p1, p2, midPnt)
if not circleCenter:
return False
self.arcMidPnt = FreeCAD.Vector(midPnt.x, midPnt.y, 0.0)
self.arcCenter = cCF3P
self.arcRadius = p1.sub(cCF3P).Length
self.arcMidPnt = midPnt
self.arcCenter = circleCenter
self.arcRadius = p1.sub(circleCenter).Length
if oversizedTool(self.arcRadius * 2.0):
return False
return (p1, p2)
else :
msg = translate('PathSlot','Failed, slot from edge only accepts lines, arcs and circles.')
FreeCAD.Console.PrintError(msg + '\n')
return False # not line , not circle
# Methods for processing double geometry
def _processDouble(self, obj, shape_1, sub1, shape_2, sub2):
@@ -1064,8 +1057,6 @@ class ObjectSlot(PathOp.ObjectOp):
# Support methods
def _dXdYdZ(self, E):
"""_dXdYdZ(E) Calculates delta-X, delta-Y, and delta-Z between two vertexes
of edge passed in. Returns these three values as vector."""
v1 = E.Vertexes[0]
v2 = E.Vertexes[1]
dX = v2.X - v1.X
@@ -1076,7 +1067,7 @@ class ObjectSlot(PathOp.ObjectOp):
def _normalizeVector(self, v):
"""_normalizeVector(v)...
Returns a copy of the vector received with values rounded to 10 decimal places."""
posTol = 0.0000000001
posTol = 0.0000000001 # abitrary, use job Geometry Tolerance ???
negTol = -1 * posTol
V = FreeCAD.Vector(v.x, v.y, v.z)
V.normalize()
@@ -1219,64 +1210,65 @@ class ObjectSlot(PathOp.ObjectOp):
def _extendArcSlot(self, p1, p2, cent, begExt, endExt):
"""_extendArcSlot(p1, p2, cent, begExt, endExt)...
This function extends an arc defined by two end points, p1 and p2, and the center.
This function extends an arc defined by two end points, p1 and p2, and the center.
The arc is extended along the circumference with begExt and endExt values.
The function returns the new end points as tuple (n1, n2) to replace p1 and p2."""
cancel = True
if not begExt and not endExt :
return (p1, p2)
n1 = p1
n2 = p2
def getArcLine(length, rads):
rads = abs(length / self.newRadius)
# Create a chord of the right length, on XY plane, starting on x axis
def makeChord(rads):
x = self.newRadius * math.cos(rads)
y = self.newRadius * math.sin(rads)
a = FreeCAD.Vector(self.newRadius, 0.0, 0.0)
b = FreeCAD.Vector(x, y, 0.0)
return Part.makeLine(a, b)
if begExt or endExt:
cancel = False
if cancel:
return (p1, p2)
# Convert extension to radians
# Convert extension to radians; make a generic chord ( line ) on XY plane from the x axis
# rotate and shift into place so it has same vertices as the required arc extention
# adjust rotation angle to provide +ve or -ve extention as needed
origin = FreeCAD.Vector(0.0, 0.0, 0.0)
if begExt:
# Create arc representing extension
rads = abs(begExt / self.newRadius)
line = getArcLine(begExt, rads)
ExtRadians = abs(begExt / self.newRadius)
chord = makeChord(ExtRadians)
rotToRads = self._xyToRadians(p1.sub(self.arcCenter))
if begExt < 1:
rotToRads -= rads
rotToDeg = math.degrees(rotToRads)
# PathLog.debug('begExt angles are: {}, {}'.format(rotToRads, rotToDeg))
beginRadians = self._getVectorAngle(p1.sub(self.arcCenter))
if begExt < 0:
beginRadians += 0 # negative Ext shortens slot so chord endpoint is slot start point
else :
beginRadians -= 2*ExtRadians # positive Ext lengthens slot so decrease start point angle
# PathLog.debug('begExt angles are: {}, {}'.format(beginRadians, math.degrees(beginRadians)))
line.rotate(origin, FreeCAD.Vector(0, 0, 1), rotToDeg)
line.translate(self.arcCenter)
self._addDebugObject(line, 'ExtendStart')
v1 = line.Vertexes[1]
if begExt < 1:
v1 = line.Vertexes[0]
chord.rotate(origin, FreeCAD.Vector(0, 0, 1), math.degrees(beginRadians))
chord.translate(self.arcCenter)
self._addDebugObject(chord, 'ExtendStart')
v1 = chord.Vertexes[1]
n1 = FreeCAD.Vector(v1.X, v1.Y, 0.0)
if endExt:
# Create arc representing extension
rads = abs(endExt / self.newRadius)
line = getArcLine(endExt, rads)
ExtRadians = abs(endExt / self.newRadius)
chord = makeChord(ExtRadians)
rotToRads = self._xyToRadians(p2.sub(self.arcCenter)) - rads
if endExt < 1:
rotToRads += rads
rotToDeg = math.degrees(rotToRads)
# PathLog.debug('endExt angles are: {}, {}'.format(rotToRads, rotToDeg))
endRadians = self._getVectorAngle(p2.sub(self.arcCenter))
if endExt > 0:
endRadians += 0 # positive Ext lengthens slot so chord endpoint is good
else :
endRadians -= 2*ExtRadians # negative Ext shortens slot so decrease end point angle
# PathLog.debug('endExt angles are: {}, {}'.format(endRadians, math.degrees(endRadians)))
line.rotate(origin, FreeCAD.Vector(0, 0, 1), rotToDeg)
line.translate(self.arcCenter)
self._addDebugObject(line, 'ExtendEnd')
v1 = line.Vertexes[0]
if endExt < 1:
v1 = line.Vertexes[1]
chord.rotate(origin, FreeCAD.Vector(0, 0, 1), math.degrees(endRadians))
chord.translate(self.arcCenter)
self._addDebugObject(chord, 'ExtendEnd')
v1 = chord.Vertexes[1]
n2 = FreeCAD.Vector(v1.X, v1.Y, 0.0)
return (n1, n2)
@@ -1286,13 +1278,9 @@ class ObjectSlot(PathOp.ObjectOp):
This function offsets an arc defined by endpoints, p1 and p2, and the center.
New end points are returned at the radius passed by newRadius.
The angle of the original arc is maintained."""
n1 = p1.sub(center).normalize()
n2 = p2.sub(center).normalize()
n1.multiply(newRadius)
n2.multiply(newRadius)
p1 = n1.add(center)
p2 = n2.add(center)
return (p1, p2)
n1 = p1.sub(center).normalize()*newRadius
n2 = p2.sub(center).normalize()*newRadius
return (n1.add(center), n2.add(center))
def _extendLineSlot(self, p1, p2, begExt, endExt):
"""_extendLineSlot(p1, p2, begExt, endExt)...
@@ -1300,19 +1288,15 @@ class ObjectSlot(PathOp.ObjectOp):
The beginning is extended by begExt value and the end by endExt value."""
if begExt:
beg = p1.sub(p2)
beg.normalize()
beg.multiply(begExt)
n1 = p1.add(beg)
n1 = p1.add(beg.normalize()*begExt)
else:
n1 = p1
if endExt:
end = p2.sub(p1)
end.normalize()
end.multiply(endExt)
n2 = p2.add(end)
end = p2.sub(p1)
n2 = p2.add(end.normalize()*endExt)
else:
n2 = p2
return (n1, n2)
return (n1, n2)
def _getOppMidPoints(self, same):
"""_getOppMidPoints(same)...
@@ -1325,12 +1309,13 @@ class ObjectSlot(PathOp.ObjectOp):
def _isParallel(self, dYdX1, dYdX2):
"""Determine if two orientation vectors are parallel."""
if dYdX1.add(dYdX2).Length == 0:
return True
if ((dYdX1.x + dYdX2.x) / 2.0 == dYdX1.x and
(dYdX1.y + dYdX2.y) / 2.0 == dYdX1.y):
return True
return False
return (dYdX1.cross(dYdX2) == FreeCAD.Vector(0,0,0) )
# if dYdX1.add(dYdX2).Length == 0:
# return True
# if ((dYdX1.x + dYdX2.x) / 2.0 == dYdX1.x and
# (dYdX1.y + dYdX2.y) / 2.0 == dYdX1.y):
# return True
# return False
def _makePerpendicular(self, p1, p2, length):
"""_makePerpendicular(p1, p2, length)...
@@ -1453,7 +1438,7 @@ class ObjectSlot(PathOp.ObjectOp):
midLen = (L0 + L1) / 2.0
return E.valueAt(E.getParameterByLength(midLen))
def _xyToRadians(self, v):
def _getVectorAngle(self, v):
# Assumes Z value of vector is zero
halfPi = math.pi / 2
@@ -1686,7 +1671,7 @@ class ObjectSlot(PathOp.ObjectOp):
# PathLog.debug('arcRadius, newRadius: {}, {}'.format(arcRadius, newRadius))
if newRadius <= 0:
msg = translate('PathSlot',
'Current offset value is not possible.')
'Current offset value produces negative radius.')
FreeCAD.Console.PrintError(msg + '\n')
return False
else:
@@ -1699,7 +1684,7 @@ class ObjectSlot(PathOp.ObjectOp):
# PathLog.debug('arcRadius, newRadius: {}, {}'.format(arcRadius, newRadius))
if newRadius <= 0:
msg = translate('PathSlot',
'Current offset value is not possible.')
'Current offset value produces negative radius.')
FreeCAD.Console.PrintError(msg + '\n')
return False
else:
@@ -1736,6 +1721,7 @@ class ObjectSlot(PathOp.ObjectOp):
try:
cmn = self.base.Shape.common(pathTravel)
if cmn.Volume > 0.000001:
print ("volume=",cmn.Volume)
return True
except Exception:
PathLog.debug('Failed to complete path collision check.')