diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py index 0807b0d599..e33c675231 100644 --- a/src/Mod/Path/PathScripts/PathAreaOp.py +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -21,22 +21,31 @@ # * USA * # * * # *************************************************************************** +# * * +# * Additional modifications and contributions beginning 2019 * +# * Focus: 4th-axis integration * +# * by Russell Johnson * +# * * +# *************************************************************************** # SCRIPT NOTES: # - FUTURE: Relocate rotational calculations to Job setup tool, creating a Machine section # with axis & rotation toggles and associated min/max values import FreeCAD -import FreeCADGui import Path import PathScripts.PathLog as PathLog import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils +import PathScripts.PathGeom as PathGeom import Draft +import math # from PathScripts.PathUtils import waiting_effects from PySide import QtCore -import math +if FreeCAD.GuiUp: + import FreeCADGui + __title__ = "Base class for PathArea based operations." __author__ = "sliptonic (Brad Collette)" @@ -44,8 +53,8 @@ __url__ = "http://www.freecadweb.org" __doc__ = "Base class and properties for Path.Area based operations." __contributors__ = "mlampert [FreeCAD], russ4262 (Russell Johnson)" __createdDate__ = "2017" -__scriptVersion__ = "2f testing" -__lastModified__ = "2019-06-12 14:12 CST" +__scriptVersion__ = "2g testing" +__lastModified__ = "2019-06-12 23:29 CST" if False: PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) @@ -189,7 +198,8 @@ class ObjectOp(PathOp.ObjectOp): if PathOp.FeatureDepths & self.opFeatures(obj): try: shape = self.areaOpShapeForDepths(obj, job) - except: + except Exception as ee: + PathLog.error(ee) shape = None # Set initial start and final depths @@ -316,10 +326,8 @@ class ObjectOp(PathOp.ObjectOp): areaOpShapes(obj) ... the shape for path area to process areaOpUseProjection(obj) ... return true if operation can use projection instead.''' - PathLog.debug("\n\n----- opExecute() in PathAreaOp.py") PathLog.track() - self.endVector = None - PathLog.debug("\n\n----- opExecute() in PathAreaOp.py") + PathLog.info("\n----- opExecute() in PathAreaOp.py") # PathLog.debug("OpDepths are Start: {}, and Final: {}".format(obj.OpStartDepth.Value, obj.OpFinalDepth.Value)) # PathLog.debug("Depths are Start: {}, and Final: {}".format(obj.StartDepth.Value, obj.FinalDepth.Value)) # PathLog.debug("initOpDepths are Start: {}, and Final: {}".format(self.initOpStartDepth, self.initOpFinalDepth)) @@ -331,6 +339,7 @@ class ObjectOp(PathOp.ObjectOp): self.cloneNames = [] self.guiMsgs = [] # list of message tuples (title, msg) to be displayed in GUI self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox + self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones # Import OpFinalDepth from pre-existing operation for recompute() scenarios if self.defValsSet is True: @@ -347,7 +356,6 @@ class ObjectOp(PathOp.ObjectOp): self.defValsSet = False if obj.EnableRotation != 'Off': - self.useRotJobClones('Start') # Calculate operation heights based upon rotation radii opHeights = self.opDetermineRotationRadii(obj) (self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0] @@ -360,16 +368,11 @@ class ObjectOp(PathOp.ObjectOp): self.strDep = self.yRotRad else: self.strDep = max(self.xRotRad, self.yRotRad) - self.finDep = -1 * self.strDep + self.finDep = -1 * self.strDep obj.ClearanceHeight.Value = self.strDep + self.safOfset obj.SafeHeight.Value = self.strDep + self.safOfst - # Set axial feed rates based upon horizontal feed rates - safeCircum = 2 * math.pi * obj.SafeHeight.Value - self.axialFeed = 360 / safeCircum * self.horizFeed - self.axialRapid = 360 / safeCircum * self.horizRapid - if self.initWithRotation == False: if obj.FinalDepth.Value == obj.OpFinalDepth.Value: obj.FinalDepth.Value = self.finDep @@ -383,6 +386,11 @@ class ObjectOp(PathOp.ObjectOp): self.strDep = obj.StartDepth.Value self.finDep = obj.FinalDepth.Value + # Set axial feed rates based upon horizontal feed rates + safeCircum = 2 * math.pi * obj.SafeHeight.Value + self.axialFeed = 360 / safeCircum * self.horizFeed + self.axialRapid = 360 / safeCircum * self.horizRapid + # Initiate depthparams and calculate operation heights for rotational operation finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 self.depthparams = PathUtils.depth_params( @@ -408,7 +416,7 @@ class ObjectOp(PathOp.ObjectOp): if len(shp) == 2: (fc, iH) = shp # fc, iH, sub, angle, axis - tup = fc, iH, 'otherOp', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + tup = fc, iH, 'otherOp', 0.0, 'S', obj.StartDepth.Value, obj.FinalDepth.Value shapes.append(tup) else: shapes.append(shp) @@ -427,12 +435,13 @@ class ObjectOp(PathOp.ObjectOp): # PathLog.debug("Pre_path depths are Start: {}, and Final: {}".format(obj.StartDepth.Value, obj.FinalDepth.Value)) sims = [] numShapes = len(shapes) + if numShapes == 1: nextAxis = shapes[0][4] elif numShapes > 1: nextAxis = shapes[1][4] else: - nextAxis = 'X' + nextAxis = 'L' for ns in range(0, numShapes): (shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns] @@ -460,26 +469,33 @@ class ObjectOp(PathOp.ObjectOp): ppCmds = pp.Commands if obj.EnableRotation != 'Off' and self.rotateFlag is True: # Rotate model to index for cut - axisOfRot = 'A' - if axis == 'Y': + if axis == 'X': + axisOfRot = 'A' + elif axis == 'Y': axisOfRot = 'B' # Reverse angle temporarily to match model. Error in FreeCAD render of B axis rotations if obj.B_AxisErrorOverride is True: angle = -1 * angle + elif axis == 'Z': + axisOfRot = 'C' + else: + axisOfRot = 'A' # Rotate Model to correct angle ppCmds.insert(0, Path.Command('G1', {axisOfRot: angle, 'F': self.axialFeed})) ppCmds.insert(0, Path.Command('N100', {})) # Raise cutter to safe depth and return index to starting position - ppCmds.insert(0, Path.Command('N200', {})) - ppCmds.append(Path.Command('G1', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) - # ppCmds.append(Path.Command('G0', {axisOfRot: 0.0, 'F': self.axialRapid})) + ppCmds.append(Path.Command('N200', {})) + ppCmds.append(Path.Command('G0', {'Z': obj.SafeHeight.Value, 'F': self.vertRapid})) if axis != nextAxis: ppCmds.append(Path.Command('G0', {axisOfRot: 0.0, 'F': self.axialRapid})) + # Eif + # Save gcode commands to object command list self.commandlist.extend(ppCmds) sims.append(sim) - + # Eif + if self.areaOpRetractTool(obj): self.endVector = None @@ -489,10 +505,9 @@ class ObjectOp(PathOp.ObjectOp): self.commandlist.append(Path.Command('G0', {'A': 0.0, 'F': self.axialRapid})) self.commandlist.append(Path.Command('G0', {'B': 0.0, 'F': self.axialRapid})) + self.useTempJobClones('Delete') # Delete temp job clone group and contents + self.guiMessage('title', None, show=True) # Process GUI messages to user PathLog.debug("obj.Name: " + str(obj.Name)) - if obj.EnableRotation != 'Off': - self.useRotJobClones('Delete') - self.guiMessage('title', None, show=True) return sims def areaOpRetractTool(self, obj): @@ -522,7 +537,7 @@ class ObjectOp(PathOp.ObjectOp): return False def opDetermineRotationRadii(self, obj): - '''opDetermineRotationRadii(self, obj) + '''opDetermineRotationRadii(obj) Determine rotational radii for 4th-axis rotations, for clearance/safe heights ''' parentJob = PathUtils.findParentJob(obj) @@ -561,36 +576,40 @@ class ObjectOp(PathOp.ObjectOp): return [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfst)] def faceRotationAnalysis(self, obj, norm, surf): - '''faceRotationAnalysis(self, obj, norm, surf) + '''faceRotationAnalysis(obj, norm, surf) Determine X and Y independent rotation necessary to make normalAt = Z=1 (0,0,1) ''' PathLog.track() praInfo = "faceRotationAnalysis() in PathAreaOp.py" - rtn = False + rtn = True axis = 'X' orientation = 'X' angle = 500.0 + precision = 6 - def roundRoughValues(val): - zTol = 1.0E-9 - rndTol = 1.0 - zTol + for i in range(0, 13): + if PathGeom.Tolerance * (i * 10) == 1.0: + precision = i + break + + def roundRoughValues(precision, val): # Convert VALxe-15 numbers to zero - if math.fabs(val) <= zTol: + if PathGeom.isRoughly(0.0, val) is True: return 0.0 # Convert VAL.99999999 to next integer - elif math.fabs(val % 1) > rndTol: + elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance: return round(val) else: - return val + return round(val, precision) - nX = roundRoughValues(norm.x) - nY = roundRoughValues(norm.y) - nZ = roundRoughValues(norm.z) + nX = roundRoughValues(precision, norm.x) + nY = roundRoughValues(precision, norm.y) + nZ = roundRoughValues(precision, norm.z) praInfo += "\n -normalAt(0,0): " + str(nX) + ", " + str(nY) + ", " + str(nZ) - saX = roundRoughValues(surf.x) - saY = roundRoughValues(surf.y) - saZ = roundRoughValues(surf.z) + saX = roundRoughValues(precision, surf.x) + saY = roundRoughValues(precision, surf.y) + saZ = roundRoughValues(precision, surf.z) praInfo += "\n -Surface.Axis: " + str(saX) + ", " + str(saY) + ", " + str(saZ) # Determine rotation needed and current orientation @@ -653,15 +672,20 @@ class ObjectOp(PathOp.ObjectOp): # Enforce enabled rotation in settings if orientation == 'Y': axis = 'X' - if obj.EnableRotation == 'B(y)': # Axis disabled - angle = 500.0 + if obj.EnableRotation == 'B(y)': # Required axis disabled + rtn = False else: axis = 'Y' - if obj.EnableRotation == 'A(x)': # Axis disabled - angle = 500.0 + if obj.EnableRotation == 'A(x)': # Required axis disabled + rtn = False - if angle != 500.0 and angle != 0.0: - praInfo += "\n - ... rotation triggered" + if angle == 500.0: + rtn = False + + if angle == 0.0: + rtn = False + + if rtn is True: self.rotateFlag = True rtn = True if obj.ReverseDirection is True: @@ -669,7 +693,11 @@ class ObjectOp(PathOp.ObjectOp): angle = angle + 180.0 else: angle = angle - 180.0 - praInfo += "\n -Suggested rotation: angle: " + str(angle) + ", axis: " + str(axis) + angle = round(angle, precision) + + praInfo += "\n -Rotation analysis: angle: " + str(angle) + ", axis: " + str(axis) + if rtn is True: + praInfo += "\n - ... rotation triggered" else: praInfo += "\n - ... NO rotation triggered" @@ -678,6 +706,8 @@ class ObjectOp(PathOp.ObjectOp): return (rtn, angle, axis, praInfo) def guiMessage(self, title, msg, show=False): + '''guiMessage(title, msg, show=False) + Handle op related GUI messages to user''' if msg is not None: self.guiMsgs.append((title, msg)) if show is True: @@ -712,6 +742,10 @@ class ObjectOp(PathOp.ObjectOp): return False def visualAxis(self): + '''visualAxis() + Create visual X & Y axis for use in orientation of rotational operations + Triggered only for PathLog.debug''' + if not FreeCAD.ActiveDocument.getObject('xAxCyl'): xAx = 'xAxCyl' yAx = 'yAxCyl' @@ -760,20 +794,20 @@ class ObjectOp(PathOp.ObjectOp): cylGui.Visibility = False vaGrp.addObject(cyl) - - def useRotJobClones(self, cloneName): + def useTempJobClones(self, cloneName): + '''useTempJobClones(cloneName) + Manage use of temporary model clones for rotational operation calculations. + Clones are stored in 'rotJobClones' group.''' if FreeCAD.ActiveDocument.getObject('rotJobClones'): - if cloneName == 'Delete': + if cloneName == 'Start': + if PathLog.getLevel(PathLog.thisModule()) < 4: + for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group: + FreeCAD.ActiveDocument.removeObject(cln.Name) + elif cloneName == 'Delete': if PathLog.getLevel(PathLog.thisModule()) < 4: for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group: FreeCAD.ActiveDocument.removeObject(cln.Name) FreeCAD.ActiveDocument.removeObject('rotJobClones') - return - if cloneName == 'Start': - for cln in FreeCAD.ActiveDocument.getObject('rotJobClones').Group: - FreeCAD.ActiveDocument.removeObject(cln.Name) - FreeCAD.ActiveDocument.removeObject('rotJobClones') - return else: FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup","rotJobClones") FreeCADGui.ActiveDocument.getObject('rotJobClones').Visibility = False @@ -783,6 +817,9 @@ class ObjectOp(PathOp.ObjectOp): FreeCADGui.ActiveDocument.getObject(cloneName).Visibility = False def cloneBaseAndStock(self, obj, base, angle, axis, subCount): + '''cloneBaseAndStock(obj, base, angle, axis, subCount) + Method called to create a temporary clone of the base and parent Job stock. + Clones are destroyed after usage for calculations related to rotational operations.''' # Create a temporary clone and stock of model for rotational use. rndAng = round(angle, 8) if rndAng < 0.0: # neg sign is converted to underscore in clone name creation. @@ -794,19 +831,23 @@ class ObjectOp(PathOp.ObjectOp): if clnNm not in self.cloneNames: self.cloneNames.append(clnNm) self.cloneNames.append(stckClnNm) + if FreeCAD.ActiveDocument.getObject(clnNm): + FreeCAD.ActiveDocument.removeObject(clnNm) + if FreeCAD.ActiveDocument.getObject(stckClnNm): + FreeCAD.ActiveDocument.removeObject(stckClnNm) FreeCAD.ActiveDocument.addObject('Part::Feature', clnNm).Shape = base.Shape FreeCAD.ActiveDocument.addObject('Part::Feature', stckClnNm).Shape = PathUtils.findParentJob(obj).Stock.Shape FreeCADGui.ActiveDocument.getObject(stckClnNm).Transparency=90 FreeCADGui.ActiveDocument.getObject(clnNm).ShapeColor = (1.000,0.667,0.000) - self.useRotJobClones(clnNm) - self.useRotJobClones(stckClnNm) + self.useTempJobClones(clnNm) + self.useTempJobClones(stckClnNm) clnBase = FreeCAD.ActiveDocument.getObject(clnNm) clnStock = FreeCAD.ActiveDocument.getObject(stckClnNm) tag = base.Name + '_' + tag return (clnBase, clnStock, tag) def getFaceNormAndSurf(self, face): - '''getFaceNormAndSurf(self, face) + '''getFaceNormAndSurf(face) Return face.normalAt(0,0) or face.normal(0,0) and face.Surface.Axis vectors ''' norm = FreeCAD.Vector(0.0, 0.0, 0.0) @@ -829,7 +870,7 @@ class ObjectOp(PathOp.ObjectOp): return (norm, surf) def applyRotationalAnalysis(self, obj, base, angle, axis, subCount): - '''applyRotationalAnalysis(self, obj, base, angle, axis, subCount) + '''applyRotationalAnalysis(obj, base, angle, axis, subCount) Create temp clone and stock and apply rotation to both. Return new rotated clones ''' @@ -852,7 +893,9 @@ class ObjectOp(PathOp.ObjectOp): clnStock.purgeTouched() return (clnBase, angle, clnStock, tag) - def applyInverseAngle(self, obj, clnBase, clnStock, axis, angle): + def applyInverseAngle(self, obj, clnBase, clnStock, axis, angle): + '''applyInverseAngle(obj, clnBase, clnStock, axis, angle) + Apply rotations to incoming base and stock objects.''' if axis == 'X': vect = FreeCAD.Vector(1, 0, 0) elif axis == 'Y': @@ -864,15 +907,15 @@ class ObjectOp(PathOp.ObjectOp): clnStock.purgeTouched() # Update property and angle values obj.InverseAngle = True + obj.AttemptInverseAngle = False angle = -1 * angle PathLog.info(translate("Path", "Rotated to inverse angle.")) return (clnBase, clnStock, angle) def calculateStartFinalDepths(self, obj, shape, stock): - '''calculateStartFinalDepths(self, obj, shape, stock) - Calculate correct start and final depths for the face - ''' + '''calculateStartFinalDepths(obj, shape, stock) + Calculate correct start and final depths for the shape(face) object provided.''' finDep = max(obj.FinalDepth.Value, shape.BoundBox.ZMin) stockTop = stock.Shape.BoundBox.ZMax if obj.EnableRotation == 'Off': @@ -883,14 +926,12 @@ class ObjectOp(PathOp.ObjectOp): strDep = min(obj.StartDepth.Value, stockTop) if strDep <= finDep: strDep = stockTop # self.strDep - msg = "Start depth <= face depth.\nIncreased to stock top." - # msg = translate('Path', msg + "\nFace depth is {} mm.".format(face.BoundBox.ZMax) - msg = translate('Path', msg) + msg = translate('Path', "Start depth <= face depth.\nIncreased to stock top.") PathLog.error(msg) return (strDep, finDep) def sortTuplesByIndex(self, TupleList, tagIdx): - '''sortTuplesByIndex(self, TupleList, tagIdx) + '''sortTuplesByIndex(TupleList, tagIdx) sort list of tuples based on tag index provided return (TagList, GroupList) ''' @@ -914,5 +955,14 @@ class ObjectOp(PathOp.ObjectOp): GroupList.pop(0) return (TagList, GroupList) - - + def warnDisabledAxis(self, obj, axis): + '''warnDisabledAxis(self, obj, axis) + Provide user feedback if required axis is disabled''' + if axis == 'X' and obj.EnableRotation == 'B(y)': + PathLog.warning(translate('Path', "Part feature is inaccessible. Selected feature(s) require 'A(x)' for access.")) + return True + elif axis == 'Y' and obj.EnableRotation == 'A(x)': + PathLog.warning(translate('Path', "Part feature is inaccessible. Selected feature(s) require 'B(y)' for access.")) + return True + else: + return False diff --git a/src/Mod/Path/PathScripts/PathPocketShape.py b/src/Mod/Path/PathScripts/PathPocketShape.py index c228b4f8d3..89cec6a5a1 100644 --- a/src/Mod/Path/PathScripts/PathPocketShape.py +++ b/src/Mod/Path/PathScripts/PathPocketShape.py @@ -21,16 +21,17 @@ # * USA * # * * # *************************************************************************** +# * * +# * Additional modifications and contributions beginning 2019 * +# * Focus: 4th-axis integration * +# * by Russell Johnson * +# * * +# *************************************************************************** # SCRIPT NOTES: # - Need test models for testing vertical faces scenarios. -# - Need to group VERTICAL faces per axis_angle tag just like horizontal faces. -# Then, need to run each grouping through -# PathGeom.combineConnectedShapes(vertical) algorithm grouping -# - Need to add face boundbox analysis code to vertical axis_angle -# section to identify highest zMax for all faces included in group -# - FUTURE: Re-iterate PathAreaOp.py need to relocate rotational settings -# to Job setup, under Machine settings tab +# - FUTURE: PathAreaOp.py need to relocate rotational settings +# to Job setup, under Machine settings tab import FreeCAD import Part @@ -50,8 +51,8 @@ __url__ = "http://www.freecadweb.org" __doc__ = "Class and implementation of shape based Pocket operation." __contributors__ = "mlampert [FreeCAD], russ4262 (Russell Johnson)" __created__ = "2017" -__scriptVersion__ = "2f testing" -__lastModified__ = "2019-06-12 14:12 CST" +__scriptVersion__ = "2g testing" +__lastModified__ = "2019-06-12 23:29 CST" if False: PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) @@ -173,8 +174,9 @@ class Extension(object): tangent = e0.tangentAt(midparam) try: normal = tangent.cross(FreeCAD.Vector(0, 0, 1)).normalize() - except: + except Exception as e: PathLog.error('getDirection(): tangent.cross(FreeCAD.Vector(0, 0, 1)).normalize()') + PathLog.error(e) return None else: poffPlus = e0.valueAt(midparam) + 0.01 * normal @@ -241,22 +243,27 @@ class ObjectPocket(PathPocketBase.ObjectPocket): '''areaOpShapes(obj) ... return shapes representing the solids to be removed.''' PathLog.track() PathLog.debug("----- areaOpShapes() in PathPocketShape.py") - - import Draft baseSubsTuples = [] subCount = 0 allTuples = [] - self.tempNameList = [] - self.delTempNameList = 0 + finalDepths = [] - def clasifySub(self, bs, sub): - face = bs.Shape.getElement(sub) - - def planarFaceFromExtrusionEdges(clsd): - useFace = 'useFaceName' - minArea = 0.0 - fCnt = 0 + def planarFaceFromExtrusionEdges(face, trans): + useFace = 'useFaceName' + minArea = 0.0 + fCnt = 0 + clsd = [] + planar = False + # Identify closed edges + for edg in face.Edges: + if edg.isClosed(): + PathLog.debug(' -e.isClosed()') + clsd.append(edg) + planar = True + # Attempt to create planar faces and select that with smallest area for use as pocket base + if planar is True: + planar = False for edg in clsd: fCnt += 1 fName = sub + '_face_' + str(fCnt) @@ -265,7 +272,10 @@ class ObjectPocket(PathPocketBase.ObjectPocket): if mFF.isNull(): PathLog.debug('Face(Part.Wire()) failed') else: - mFF.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - mFF.BoundBox.ZMin)) + if trans is True: + mFF.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - mFF.BoundBox.ZMin)) + if FreeCAD.ActiveDocument.getObject(fName): + FreeCAD.ActiveDocument.removeObject(fName) tmpFace = FreeCAD.ActiveDocument.addObject('Part::Feature', fName).Shape = mFF tmpFace = FreeCAD.ActiveDocument.getObject(fName) tmpFace.purgeTouched() @@ -279,7 +289,12 @@ class ObjectPocket(PathPocketBase.ObjectPocket): useFace = fName else: FreeCAD.ActiveDocument.removeObject(fName) - return (planar, useFace) + if useFace != 'useFaceName': + self.useTempJobClones(useFace) + return (planar, useFace) + + def clasifySub(self, bs, sub): + face = bs.Shape.getElement(sub) if type(face.Surface) == Part.Plane: PathLog.debug('type() == Part.Plane') @@ -292,7 +307,8 @@ class ObjectPocket(PathPocketBase.ObjectPocket): PathLog.debug(' -isHorizontal()') self.vert.append(face) return True - return False + else: + return False elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis): PathLog.debug('type() == Part.Cylinder') # vertical cylinder wall @@ -312,36 +328,22 @@ class ObjectPocket(PathPocketBase.ObjectPocket): elif type(face.Surface) == Part.SurfaceOfExtrusion: # extrusion wall PathLog.debug('type() == Part.SurfaceOfExtrusion') - clsd = [] - planar = False - # Identify closed edges - for edg in face.Edges: - if edg.isClosed(): - PathLog.debug(' -e.isClosed()') - clsd.append(edg) - planar = True - # Attempt to create planar faces and select that with smallest area for use as pocket base - if planar is True: - planar = False - (planar, useFace) = planarFaceFromExtrusionEdges(clsd) + # Attempt to extract planar face from surface of extrusion + (planar, useFace) = planarFaceFromExtrusionEdges(face, trans=True) # Save face object to self.horiz for processing or display error if planar is True: - self.tempNameList.append(useFace) - self.delTempNameList += 1 uFace = FreeCAD.ActiveDocument.getObject(useFace) self.horiz.append(uFace.Shape.Faces[0]) - msg = "Verify depth of pocket for '{}'.".format(sub) - msg += "\n
Pocket is based on extruded surface." - msg += "\n
Bottom of pocket might be non-planar and/or not normal to spindle axis." - msg += "\n
\n
3D pocket bottom is NOT available in this operation." - msg = translate('Path', msg) + msg = translate('Path', "Verify depth of pocket for '{}'.".format(sub)) + msg += translate('Path', "\n
Pocket is based on extruded surface.") + msg += translate('Path', "\n
Bottom of pocket might be non-planar and/or not normal to spindle axis.") + msg += translate('Path', "\n
\n
3D pocket bottom is NOT available in this operation.") PathLog.info(msg) title = translate('Path', 'Depth Warning') self.guiMessage(title, msg, False) else: PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub))) else: - PathLog.debug('type() == OTHER') PathLog.debug(' -type(face.Surface): {}'.format(type(face.Surface))) return False @@ -356,19 +358,23 @@ class ObjectPocket(PathPocketBase.ObjectPocket): else: for p in range(0, len(obj.Base)): (base, subsList) = obj.Base[p] - go = False + isLoop = False # First, check all subs collectively for loop of faces if len(subsList) > 2: - (go, norm, surf) = self.checkForFacesLoop(base, subsList) - if go is True: - PathLog.debug("Common Surface.Axis or normalAt() value found for loop faces.") + (isLoop, norm, surf) = self.checkForFacesLoop(base, subsList) + if isLoop is True: + PathLog.info("Common Surface.Axis or normalAt() value found for loop faces.") rtn = False subCount += 1 (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) + PathLog.info("angle: {}; axis: {}".format(angle, axis)) if rtn is True: - (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) + faceNums = "" + for f in subsList: + faceNums += '_' + f.replace('Face', '') + (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNums) # Verify faces are correctly oriented - InverseAngle might be necessary PathLog.debug("Checking if faces are oriented correctly after rotation...") @@ -386,7 +392,8 @@ class ObjectPocket(PathPocketBase.ObjectPocket): tup = clnBase, subsList, angle, axis, clnStock else: - PathLog.debug("No rotation used") + if self.warnDisabledAxis(obj, axis) is False: + PathLog.debug("No rotation used") axis = 'X' angle = 0.0 stock = PathUtils.findParentJob(obj).Stock @@ -396,7 +403,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket): baseSubsTuples.append(tup) # Eif - if go is False: + if isLoop is False: PathLog.debug(translate('Path', "Processing subs individually ...")) for sub in subsList: subCount += 1 @@ -405,11 +412,28 @@ class ObjectPocket(PathPocketBase.ObjectPocket): PathLog.debug(translate('Path', "Base Geometry sub: {}".format(sub))) face = base.Shape.getElement(sub) + + # -------------------------------------------------------- + if type(face.Surface) == Part.SurfaceOfExtrusion: + # extrusion wall + PathLog.debug('analyzing type() == Part.SurfaceOfExtrusion') + # Attempt to extract planar face from surface of extrusion + (planar, useFace) = planarFaceFromExtrusionEdges(face, trans=False) + # Save face object to self.horiz for processing or display error + if planar is True: + base = FreeCAD.ActiveDocument.getObject(useFace) + sub = 'Face1' + PathLog.debug(' -successful face crated: {}'.format(useFace)) + else: + PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub))) + # -------------------------------------------------------- + (norm, surf) = self.getFaceNormAndSurf(face) (rtn, angle, axis, praInfo)= self.faceRotationAnalysis(obj, norm, surf) if rtn is True: - (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) + faceNum = sub.replace('Face', '') + (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNum) # Verify faces are correctly oriented - InverseAngle might be necessary faceIA = clnBase.Shape.getElement(sub) (norm, surf) = self.getFaceNormAndSurf(faceIA) @@ -425,7 +449,8 @@ class ObjectPocket(PathPocketBase.ObjectPocket): tup = clnBase, [sub], angle, axis, clnStock else: - PathLog.debug(str(sub) + ": No rotation used") + if self.warnDisabledAxis(obj, axis) is False: + PathLog.debug(str(sub) + ": No rotation used") axis = 'X' angle = 0.0 stock = PathUtils.findParentJob(obj).Stock @@ -460,40 +485,44 @@ class ObjectPocket(PathPocketBase.ObjectPocket): self.horiz = [] self.vert = [] subBase = o[0] + subsList = o[1] angle = o[2] axis = o[3] stock = o[4] - for sub in o[1]: + for sub in subsList: if 'Face' in sub: if clasifySub(self, subBase, sub) is False: PathLog.error(translate('PathPocket', 'Pocket does not support shape %s.%s') % (subBase.Label, sub)) + if obj.EnableRotation != 'Off': + PathLog.info(translate('PathPocket', 'Face might not be within rotation accessibility limits.')) # Determine final depth as highest value of bottom boundbox of vertical face, # in case of uneven faces on bottom - vFinDep = self.vert[0].BoundBox.ZMin - for vFace in self.vert: - if vFace.BoundBox.ZMin > vFinDep: - vFinDep = vFace.BoundBox.ZMin - # Determine if vertical faces for a loop: Extract planar loop wire as new horizontal face. - self.vertical = PathGeom.combineConnectedShapes(self.vert) - self.vWires = [TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical] - for wire in self.vWires: - w = PathGeom.removeDuplicateEdges(wire) - face = Part.Face(w) - # face.tessellate(0.1) - if PathGeom.isRoughly(face.Area, 0): - msg = translate('PathPocket', 'Vertical faces do not form a loop - ignoring') - PathLog.error(msg) - # title = translate("Path", "Face Selection Warning") - # self.guiMessage(title, msg, True) - else: - face.translate(FreeCAD.Vector(0, 0, vFinDep - face.BoundBox.ZMin)) - self.horiz.append(face) - msg = translate('Path', 'Verify final depth of pocket shaped by vertical faces.') - PathLog.error(msg) - title = translate('Path', 'Depth Warning') - self.guiMessage(title, msg, False) + if len(self.vert) > 0: + vFinDep = self.vert[0].BoundBox.ZMin + for vFace in self.vert: + if vFace.BoundBox.ZMin > vFinDep: + vFinDep = vFace.BoundBox.ZMin + # Determine if vertical faces for a loop: Extract planar loop wire as new horizontal face. + self.vertical = PathGeom.combineConnectedShapes(self.vert) + self.vWires = [TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical] + for wire in self.vWires: + w = PathGeom.removeDuplicateEdges(wire) + face = Part.Face(w) + # face.tessellate(0.1) + if PathGeom.isRoughly(face.Area, 0): + msg = translate('PathPocket', 'Vertical faces do not form a loop - ignoring') + PathLog.error(msg) + # title = translate("Path", "Face Selection Warning") + # self.guiMessage(title, msg, True) + else: + face.translate(FreeCAD.Vector(0, 0, vFinDep - face.BoundBox.ZMin)) + self.horiz.append(face) + msg = translate('Path', 'Verify final depth of pocket shaped by vertical faces.') + PathLog.error(msg) + title = translate('Path', 'Depth Warning') + self.guiMessage(title, msg, False) # add faces for extensions self.exts = [] @@ -524,15 +553,19 @@ class ObjectPocket(PathPocketBase.ObjectPocket): for face in self.horizontal: # extrude all faces up to StartDepth and those are the removal shapes (strDep, finDep) = self.calculateStartFinalDepths(obj, face, stock) + finalDepths.append(finDep) extent = FreeCAD.Vector(0, 0, strDep - finDep) self.removalshapes.append((face.removeSplitter().extrude(extent), False, 'pathPocketShape', angle, axis, strDep, finDep)) PathLog.debug("Extent depths are str: {}, and fin: {}".format(strDep, finDep)) # Efor face # Adjust obj.FinalDepth.Value as needed. - if subCount == 1: - obj.FinalDepth.Value = finDep - else: # process the job base object as a whole + if len(finalDepths) > 0: + finalDep = min(finalDepths) + if subCount == 1: + obj.FinalDepth.Value = finDep + else: + # process the job base object as a whole PathLog.debug(translate("Path", 'Processing model as a whole ...')) finDep = obj.FinalDepth.Value strDep = obj.StartDepth.Value @@ -554,9 +587,10 @@ class ObjectPocket(PathPocketBase.ObjectPocket): if self.removalshapes: obj.removalshape = self.removalshapes[0][0] - if self.delTempNameList > 0: - for tmpNm in self.tempNameList: - FreeCAD.ActiveDocument.removeObject(tmpNm) + #if PathLog.getLevel(PathLog.thisModule()) != 4: + #if self.delTempNameList > 0: + # for tmpNm in self.tempNameList: + # FreeCAD.ActiveDocument.removeObject(tmpNm) return self.removalshapes @@ -590,7 +624,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket): obj.ExtensionFeature = [(ext.obj, ext.getSubLink()) for ext in extensions] def checkForFacesLoop(self, base, subsList): - '''checkForFacesLoop(self, base, subsList)... + '''checkForFacesLoop(base, subsList)... Accepts a list of face names for the given base. Checks to determine if they are looped together. ''' @@ -603,6 +637,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket): saSum = FreeCAD.Vector(0.0, 0.0, 0.0) norm = FreeCAD.Vector(0.0, 0.0, 0.0) surf = FreeCAD.Vector(0.0, 0.0, 0.0) + precision = 6 def makeTempExtrusion(base, sub, fCnt): extName = 'tmpExtrude' + str(fCnt) @@ -632,17 +667,21 @@ class ObjectPocket(PathPocketBase.ObjectPocket): tmpWire.purgeTouched() return (True, tmpWire, tmpExt) - def roundValue(val): - zTol = 1.0E-8 - rndTol = 1.0 - zTol + def roundValue(precision, val): # Convert VALxe-15 numbers to zero - if math.fabs(val) <= zTol: + if PathGeom.isRoughly(0.0, val) is True: return 0.0 # Convert VAL.99999999 to next integer - elif math.fabs(val % 1) > rndTol: + elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance: return round(val) else: - return round(val, 8) + return round(val, precision) + + # Determine precision from Tolerance + for i in range(0, 13): + if PathGeom.Tolerance * (i * 10) == 1.0: + precision = i + break # Sub Surface.Axis values of faces # Vector of (0, 0, 0) will suggests a loop @@ -692,21 +731,21 @@ class ObjectPocket(PathPocketBase.ObjectPocket): if len(matchList) == 0: for fc in lastExtrusion.Shape.Faces: (norm, raw) = self.getFaceNormAndSurf(fc) - rnded = FreeCAD.Vector(roundValue(raw.x), roundValue(raw.y), roundValue(raw.z)) + rnded = FreeCAD.Vector(roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z)) if rnded.x == 0.0 or rnded.y == 0.0 or rnded.z == 0.0: for fc2 in tmpExt.Shape.Faces: (norm2, raw2) = self.getFaceNormAndSurf(fc2) - rnded2 = FreeCAD.Vector(roundValue(raw2.x), roundValue(raw2.y), roundValue(raw2.z)) + rnded2 = FreeCAD.Vector(roundValue(precision, raw2.x), roundValue(precision, raw2.y), roundValue(precision, raw2.z)) if rnded == rnded2: matchList.append(fc2) go = True else: for m in matchList: (norm, raw) = self.getFaceNormAndSurf(m) - rnded = FreeCAD.Vector(roundValue(raw.x), roundValue(raw.y), roundValue(raw.z)) + rnded = FreeCAD.Vector(roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z)) for fc2 in tmpExt.Shape.Faces: (norm2, raw2) = self.getFaceNormAndSurf(fc2) - rnded2 = FreeCAD.Vector(roundValue(raw2.x), roundValue(raw2.y), roundValue(raw2.z)) + rnded2 = FreeCAD.Vector(roundValue(precision, raw2.x), roundValue(precision, raw2.y), roundValue(precision, raw2.z)) if rnded.x == 0.0 or rnded.y == 0.0 or rnded.z == 0.0: if rnded == rnded2: go = True @@ -722,7 +761,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket): saTotal = FreeCAD.Vector(0.0, 0.0, 0.0) for fc in matchList: (norm, raw) = self.getFaceNormAndSurf(fc) - rnded = FreeCAD.Vector(roundValue(raw.x), roundValue(raw.y), roundValue(raw.z)) + rnded = FreeCAD.Vector(roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z)) if (rnded.y > 0.0 or rnded.z > 0.0) and vertLoopFace is None: vertLoopFace = fc saTotal = saTotal.add(rnded) diff --git a/src/Mod/Path/PathScripts/PathProfileFaces.py b/src/Mod/Path/PathScripts/PathProfileFaces.py index b8b2b20035..d4e0095904 100644 --- a/src/Mod/Path/PathScripts/PathProfileFaces.py +++ b/src/Mod/Path/PathScripts/PathProfileFaces.py @@ -21,6 +21,12 @@ # * USA * # * * # *************************************************************************** +# * * +# * Additional modifications and contributions beginning 2019 * +# * Focus: 4th-axis integration * +# * by Russell Johnson * +# * * +# *************************************************************************** import ArchPanel import FreeCAD @@ -40,9 +46,10 @@ __title__ = "Path Profile Faces Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" __doc__ = "Path Profile operation based on faces." +__contributors__ = "russ4262 (Russell Johnson)" __created__ = "2014" -__scriptVersion__ = "2f testing" -__lastModified__ = "2019-06-12 14:12 CST" +__scriptVersion__ = "2g testing" +__lastModified__ = "2019-06-12 23:29 CST" if False: PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) @@ -136,7 +143,8 @@ class ObjectProfile(PathProfileBase.ObjectProfile): tup = clnBase, sub, tag, angle, axis, clnStock else: - PathLog.debug(str(sub) + ": no rotation used") + if self.warnDisabledAxis(obj, axis) is False: + PathLog.debug(str(sub) + ": No rotation used") axis = 'X' angle = 0.0 tag = base.Name + '_' + axis + str(angle).replace('.', '_') @@ -148,9 +156,8 @@ class ObjectProfile(PathProfileBase.ObjectProfile): # Efor # Efor if subCount > 1: - msg = "Multiple faces in Base Geometry." - msg += " Depth settings will be applied to all faces." - msg = translate("Path", msg) + msg = translate('Path', "Multiple faces in Base Geometry.") + " " + msg += translate('Path', "Depth settings will be applied to all faces.") PathLog.warning(msg) #title = translate("Path", "Depth Warning") #self.guiMessage(title, msg)