From 94536f98774cd64a4e72f763492b419f7f9d0c1d Mon Sep 17 00:00:00 2001 From: J-Dunn Date: Wed, 6 Jan 2021 12:10:29 +0000 Subject: [PATCH] Path: fix several minor bugs in arc slot ops This corrects which end of the slot Extend Slot Start/End are applied; error with Extend lengths between 0 and 1 and direction of extention on arc slots ( neg. is shorten ). Some renaming to make code more readable and self documenting. --- src/Mod/Path/PathScripts/PathSlot.py | 288 +++++++++++++-------------- 1 file changed, 137 insertions(+), 151 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSlot.py b/src/Mod/Path/PathScripts/PathSlot.py index 2461f441b1..32ea97cf15 100644 --- a/src/Mod/Path/PathScripts/PathSlot.py +++ b/src/Mod/Path/PathScripts/PathSlot.py @@ -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.')